Skip to content

feat(gateway): support HTTPS with manual and ACME certificates - #408

Open
zlzGithub-0801 wants to merge 1 commit into
kvcache-ai:mainfrom
zlzGithub-0801:feat/https-gateway
Open

zlzGithub-0801 wants to merge 1 commit into
kvcache-ai:mainfrom
zlzGithub-0801:feat/https-gateway

Conversation

@zlzGithub-0801

Copy link
Copy Markdown
Collaborator

What

Add gateway HTTPS support with three modes:

  • off (default): HTTP only.
  • manual: HTTPS using supplied certificate and private key files.
  • acme: HTTPS with automatic certificate issuance and renewal through CertMagic.

Why

Allow clients to connect securely to the gateway.

Related issue

N/A

Scope and non-goals

Gateway TLS termination only. Rust nodes and CLI certificate trust configuration are unchanged.

Design and behavior changes

The gateway runs either HTTP or HTTPS. Manual certificates load at startup; ACME certificates are managed by CertMagic.

Compatibility and operations

  • Public API or generated protocol: Unchanged.
  • Configuration or defaults: Adds gateway.tls; defaults to off.
  • Snapshot manifest, artifact layout, or storage format: Unchanged.
  • Upgrade and rollback: Existing HTTP deployments remain compatible. Set TLS mode to off to restore HTTP.
  • Host requirements, permissions, ports, or dependencies: Adds CertMagic. Manual mode requires readable certificate files; ACME requires persistent storage and challenge connectivity.

Validation

  • make fmt
  • make clippy
  • make test-unit
  • Relevant Rust integration tests
  • make -C services test (required when services/ changes)
  • Generated clients/server regenerated with the documented make target
  • Documentation updated
  • Benchmarks or performance comparison completed

Risks and reviewer notes

Manual certificates require replacement and gateway restart before expiry. ACME deployments require correct challenge configuration and persistent certificate storage.

Checklist

  • The PR contains one coherent change and no unrelated formatting or refactoring.
  • New behavior is covered by tests, or I explained why testing is impractical.
  • Logs and examples contain no credentials, tokens, or private registry information.
  • I did not manually edit generated code without updating its source and regenerating it.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 2 issue(s) in this PR.

  • ✅ Successfully posted inline: 2 comment(s)

Comment on lines +301 to +304
DisableHTTPChallenge: true,
DisableTLSALPNChallenge: solver != nil,
ListenHost: host,
AltTLSALPNPort: portNumber,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
When solver == nil (the default tls-alpn-01 path), CertMagic uses AltTLSALPNPort for its TLS-ALPN challenge listener. This is set to the gateway's own HTTPS port, which has already been bound by net.Listen before ManageAsync starts, so CertMagic cannot bind the challenge listener and issuance/renewal fails. Configure a distinct challenge port with the appropriate public 443 mapping, or arrange for the existing listener to handle the challenge instead of starting a second listener.

Comment thread services/shared/config/config.go Outdated
Challenge string `json:"challenge"`
DNSProvider string `json:"dns_provider"`
// Sensitive: never include this field in logs.
CloudflareAPIToken string `json:"cloudflare_api_token"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
[high] This makes the credential part of the normal JSON representation of GatewayTLSConfig/Config. Any generic config dump, diagnostic response, or log using json.Marshal will emit the API token, contradicting the “never include this field in logs” requirement. Keep the field out of marshaling (while retaining a custom/unmarshal path for loading the JSON value), or implement an explicit redacted marshaler for the containing config types.

blocked := []string{
"0.0.0.0/8", "100.64.0.0/10", "192.0.0.0/24", "192.0.2.0/24",
"192.88.99.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4",
"2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
[medium] This allowlist still accepts the IPv6 well-known IPv4/IPv6 translation prefix 64:ff9b::/96: it is global-unicast and within 2000::/3, but is not in the blocked prefixes. That permits an address that cannot be used as a publicly issued certificate identity, and the repository's TestACMEIPAddressValidation explicitly expects 64:ff9b::808:808 to be rejected. Add this and the other relevant special-purpose prefixes to the rejection policy (or use an authoritative special-purpose check).

Suggestion:

Suggested change
"2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20",
"64:ff9b::/96", "2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20",

Comment on lines +548 to +550
_, port, err := net.SplitHostPort(g.HTTPSListenAddr)
n, portErr := strconv.Atoi(port)
if err != nil || portErr != nil || n < 1 || n > 65535 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
This only validates that the port parses; it does not validate the host or detect an address collision with g.MetricsListenAddr. For example, HTTPSListenAddr set to "bad host:443" passes here and fails later at net.Listen, while setting it equal to the metrics address causes the metrics server's bind to fail after startup. Validate the listener address/collision (or return a configuration error) before starting the gateway.

Comment on lines +636 to +640
blocked := []string{
"0.0.0.0/8", "100.64.0.0/10", "192.0.0.0/24", "192.0.2.0/24",
"192.88.99.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4",
"2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
The IPv6 special-purpose 64:ff9b::/96 range is not included, so publicCertificateIP(netip.MustParseAddr("64:ff9b::808:808")) returns true even though this range is reserved for IPv4/IPv6 translation and cannot be used as a public ACME certificate identifier. The repository's TestACMEIPAddressValidation already expects this address to be rejected, so validation currently fails; add the missing reserved ranges (at least this one) or use a maintained special-purpose classification.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant