Add Caddy-style tls internal local CA#54
Conversation
Generates a 10-year ECDSA P-256 root + 7-day intermediate on first start,
signs 12-hour leaf certificates on demand at TLS handshake time, and
exposes `gatel trust` / `gatel untrust` subcommands to install or remove
the root in the OS trust store (Windows via schannel without UAC; macOS
shells out to `security`; Linux drops into the distro CA anchors dir and
runs `update-ca-trust`/`update-ca-certificates`).
Opt in per-site with `tls internal`, or globally via `tls { internal }`
as a fallback when ACME is not configured. The composite cert resolver
now consults the local CA between manual certs and the ACME / on-demand
paths, so existing setups are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d64e6e6941
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if opted_in || self.global_internal { | ||
| return resolve_local_ca(local_ca, &normalized); |
There was a problem hiding this comment.
Gate global internal fallback when ACME is enabled
Avoid resolving tls internal for every SNI whenever tls { internal } is present. In this resolver path, self.global_internal causes the local CA branch to run before ACME for all hostnames, so a config that enables both ACME and internal will serve local-CA certs instead of ACME certs. This contradicts the documented "internal as fallback only when ACME isn't configured" behavior and can break public HTTPS deployments that expect ACME-issued chains.
Useful? React with 👍 / 👎.
| let mut guard = self.resolver.internal_hosts.write().await; | ||
| *guard = new_internal; |
There was a problem hiding this comment.
Persist global/internal CA state on TLS reload
Make reload update all local-CA resolver state, not just hostnames. reload() recomputes global_internal and new_internal, but only writes internal_hosts; it never updates self.resolver.global_internal and cannot initialize self.resolver.local_ca if internal TLS is enabled after startup. As a result, toggling tls internal via hot reload does not reliably take effect (or disable) until a full process restart.
Useful? React with 👍 / 👎.
- P1: gate the global `tls { internal }` fallback so it only kicks in
when ACME is *not* also configured. With ACME present the local-CA
branch would otherwise shadow ACME-issued certs for every unknown
SNI, breaking public deployments.
- P2: make the resolver's `local_ca` an `ArcSwapOption<LocalCa>` and
`global_internal` an `AtomicBool` so a hot reload can both toggle
the flag and bootstrap a `LocalCa` instance on first need, without
a process restart.
- Add regression tests covering both: ACME + global internal must not
produce a local CA at startup, and a config that flips a site to
`tls internal` must bring the local CA up on reload.
- Fix clippy `collapsible_if` violations flagged by CI on Rust 1.95.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Adds a Caddy-style local CA so users can serve HTTPS for
localhost/ internal hosts without ACME and without managing PEM files. Mirrors the feature you can quote from Caddy:tls internal) or globally (tls { internal }, used as a fallback only when ACME is not configured). Per-sitetls internalalways wins.Rootviaschannel— no UAC.security add-trusted-cert -k login.keychain-db.update-ca-certificates/update-ca-trust extract.Test plan
cargo test --lib— 177 passed (3 new intls::local_ca).gatel runwithtls internalonlocalhost,curl --resolve localhost:18443:127.0.0.1 https://localhost:18443/returns 200.gatel trustinstalls the root into the Windows current-userRootstore; verified with PowerShellGet-ChildItem Cert:\CurrentUser\Rootandcertutil -user -store Root.gatel untrustremoves it.gatel trust,curl --ssl-no-revoke https://localhost:18443/validates the chain end-to-end.securityCLI and follows mkcert/Caddy conventions.Docs
docs/en/tls-and-acme.mdanddocs/zh/tls-and-acme.mdcovering quick start, storage layout, lifetimes, and thegatel trustflow.examples/tls-internal.kdl.🤖 Generated with Claude Code