Repository navigation
feat(proxy): expose on-demand TLS, mTLS client CA and the ACME cache path - #85
Merged
Merged
Conversation
…path
Three TLS deploy flags shipped in the proxy with no deploy.yml key. On-demand
TLS is the largest single differentiator in the roadmap and there was no way to
turn it on; mTLS was in the same position.
proxy:
ssl: true
tls:
on_demand_url: https://app.example.com/api/v1/tls/ask
client_ca_path: config/tls/client-ca.pem
acme_cache_path: /home/kamal-proxy/.config/kamal-proxy/acme
Three things the issue left open, decided here:
client_ca_path names a file on the machine running kamal, not on the deploy
host. A deploy host path cannot work — the flag is an argument to `docker exec
kamal-proxy kamal-proxy deploy`, and a deploy cannot add a mount to a container
that is already running and shared by every app on the host. The bundle is
uploaded into the apps-config tree the proxy already mounts, the same shape
error_pages_path and ssl.certificate_pem use, and the flag gets the container
path. acme_cache_path is the opposite: it names a directory inside the
container, so it passes through untranslated.
There is no precedence between on_demand_url and host/hosts to document —
kamal-proxy hard-rejects the combination, along with a custom certificate and
tls_domains, and requires TLS enabled. The gem now fails on the same rules at
config time rather than after the SSH round-trip. The existing "Must set a host
to enable automatic SSL" check had to stand aside for on-demand, whose required
shape is ssl with no host.
All three are stripped from the per-app deploy when load balancing and re-added
by the load balancer, like host/tls/tls-domains. TLS terminates at the edge, so
an ask endpoint or a client CA on a proxy that never sees a handshake does
nothing — that would have made the feature a silent no-op for load-balanced
deploys.
on_demand_url cannot coexist with the maximal fixture's hosts, custom cert and
tls_domains, so the flag-coverage canary now unions two maximal fixtures. The
alternative was waiving the headline flag as unprovable.
Closes #74
2 of 4 tasks
mhenrixon
added a commit
that referenced
this pull request
Aug 3, 2026
## Summary The 3.0.0 release train's proxy side is publishing: v1.0.0.1 carries the certificate-system unification (kamal-proxy#84/#85 - no issuance for a name nobody asked for) and the disk-backed cache store (kamal-proxy#83). MINIMUM_VERSION moves to it; v1.0.0.0 must never be the minimum (unbounded SNI issuance hole). The flag manifest was regenerated from a binary built at the v1.0.0.1 tag (bin/sync-proxy-flags --binary): the only change is the version stamp - v1.0.0.1 adds no deploy or run flags, so the coverage canary had nothing new to decide. The integration seed (setup.sh) reads the constant dynamically and needs no edit. Full bin/test against the published ghcr image (the E4 handoff gate) runs before bin/release-dash 3.0.0, per the release ordering. ## Verification - [x] bundle exec rubocop --parallel passes - [x] unit suite passes (1311 runs) - flag canary green against the regenerated manifest Refs #93 (workstream G)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
proxy.tlsblock for the three TLS deploy flags that shipped in the proxy with nodeploy.ymlkey.lib/kamal/configuration/proxy.rb— thetlsblock, atls_optionsgroup merged intodeploy_options, andhost_client_ca/container_client_cabuilt from the existingtls_pathplumbing (split intotls_file_pathso a client CA no longer has to pretend a server certificate was configured).lib/kamal/configuration/loadbalancer.rb— re-addstls_optionsat the edge.lib/kamal/configuration/validator/proxy.rb— mirrors kamal-proxy's exclusivity rules; exemptson_demand_urlfrom the "Must set a host to enable automatic SSL" check.lib/kamal/cli/app/ssl_certificates.rb— uploads the CA bundle alongside the server certificate.test/proxy_flag_coverage_test.rb— unions two maximal fixtures instead of one.Closes #74
Where each value lives
client_ca_pathandacme_cache_pathlook alike and are opposites:client_ca_path.kamal/proxy/apps-config/<service>/tls/<role>/client-ca.pemacme_cache_pathA deploy-host path for the CA cannot work: the flag is an argument to
docker exec kamal-proxy kamal-proxy deploy, and a deploy cannot add a mount to a proxy container that is already running and shared by every app on the host. Uploading into the apps-config tree the proxy already mounts is the shapeerror_pages_pathandssl.certificate_pemboth use.Test plan
bundle exec rubocop --parallel— clean, 209 filesbuilder_test/build_test, byte-identical on pristinedashHEAD (host-arch dependent, pass in CI)on_demand_urlpointing at a real ask endpoint; confirm a cert issues for an approved hostname and is refused for a rejected oneclient_ca_path; confirm a client without a certificate is rejected and one signed by the CA is acceptedacme_cache_pathinside thekamal-proxy-configvolume surviveskamal proxy rebootdeployand not on the per-app oneAutomated coverage
on_demand_url becomes --tls-on-demand-url+reaches the generated deploy commandclient_ca_path is translated to a container path/is role-scoped like the server certificateboot with an mTLS client CA(CLI)on_demand_url cannot be combined with hosts/a custom certificate/tls_domains/requires sslServiceOptions.Validatessl without a host is allowed when on_demand_url is setno tls block leaves the deploy command unchangedthe tls flags move to the load balancer when load balancingevery kamal-proxy deploy flag is exposed in deploy.yml or waivedDeviations & judgment calls
Deviations from the issue
ServiceOptions.Validate(internal/server/service.go:218-253) hard-rejectson_demand_urlalongside hosts, a custom certificate,canonical-hostortls_domains, and requires TLS. The gem mirrors those rules rather than inventing a precedence the proxy would then refuse to honour.on_demand_url— it setshosts, a custom certificate andtls_domains, all three forbidden alongside on-demand.test/proxy_flag_coverage_test.rbnow unions the flags emitted by two maximal fixtures. The alternative was waivingtls-on-demand-urlas unprovable, which would ship this issue's headline feature unverified — exactly what the canary from test(proxy): fail the build when kamal-proxy grows a flag the gem cannot emit #83 exists to stop.client_ca_pathis read locally, not from the deploy host. The issue's "pointing at a host file" is ambiguous; see the table above for why a deploy-host path is not implementable at deploy time.Judgment calls
host,tlsandtls-domains-*. Not in the issue. Leaving it out would have made the feature a silent no-op for load-balanced dash users, who are the majority — an ask endpoint on a proxy that never terminates TLS is never consulted.client_ca_pathexistence is checked at config time. The upload would fail anyway, but mid-deploy, after containers may have started. mTLS quietly not being enforced is a security outcome, so it fails before the first SSH connection.canonical_hostexclusivity is not implemented. The proxy also rejects on-demand + canonical host, butcanonical_hostis not a deploy.yml key yet (R7 backlog, Expose header rules, redirects, rewrites and error interception in deploy.yml #78). Nothing to check against until it lands — worth remembering when Expose header rules, redirects, rewrites and error interception in deploy.yml #78 does.tls_pathwas split rather than relaxed.Proxy#tls_pathis guarded bycustom_ssl_certificate?; the guard now sits intls_pathand the bare path construction intls_file_path, so the client CA reuses the path shape without inheriting an unrelated condition.