Repository navigation
feat(proxy): first-class proxy.run.acme with secrets-backed DNS credentials - #84
Merged
Merged
Conversation
…ntials kamal-proxy grew a full ACME/DNS-01 story and none of it was reachable from deploy.yml. The only way in was proxy.run.options.env, a docker run escape hatch with no .kamal/secrets integration — so a DNS API token that can rewrite your zone sat literal in deploy.yml unless the operator hand-rolled ERB. proxy/run/acme now emits --acme-email, --acme-dns-provider, --acme-directory, --acme-prefer-wildcard and --acme-http-fallback, and takes a credentials list of secret names that never touches a command line. Two details worth knowing: The acme flags render with `=` while the rest of the run command renders with a space. Cobra only reads a boolean flag's value in --flag=false form, and both booleans default to true in the proxy — so false is exactly the value an operator writes and exactly the one that would otherwise have been ignored. Switching the whole run command to `=` would have been cleaner but changes --metrics-port for every existing deploy, and with it the config digest. Credentials go through a 0600 env file under .kamal/proxy, not --env: docker run --env leaves the token in host process listings and in the audit log kamal writes. Their names ride along in config_digest so swapping a credential reboots the proxy; their values do not, because the digest is published as a docker label. The DNS provider allowlist is drift-checked against a generated manifest entry rather than left as a hand-maintained constant — a provider the proxy grows and the gem does not is a rejected deploy, not a missing feature. Closes #73
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 first-class
proxy.run.acmeblock, with DNS credentials routed through.kamal/secretsinstead of thedocker runescape hatch.lib/kamal/configuration/proxy/acme.rb(new) — owns the ACME surface: the five run flags, the provider allowlist, and the credential env file.lib/kamal/configuration/proxy/run.rb—run_commandgains the acme flags;docker_options_argsgains--env-filewhen credentials are set;config_digestgains the credential names.lib/kamal/configuration/validator/proxy.rb— rejects an unsupporteddns_providernaming the supported list, and requiresemail.lib/kamal/cli/proxy.rb,lib/kamal/cli/proxy/reboot.rb— upload the credential env file at mode 0600 before the container is created.lib/kamal/configuration.rb—validate_secrets!resolves the credentials, so a missing secret fails before any SSH.bin/sync-proxy-flags,test/fixtures/kamal_proxy_flags.yml— the manifest now records the DNS providers kamal-proxy advertises.Closes #73
Why the acme flags render differently
The rest of
run_commandrenders--flag value. The acme flags render--flag=value, because Cobra only reads a boolean flag's value in--flag=falseform —--acme-http-fallback falsesets the flag true and leaves a stray positional argument. Both booleans default totruein the proxy, sofalseis exactly the value an operator writes.Rendering the whole run command with
=would have been tidier, but it turns--metrics-port 9090into--metrics-port=9090for every existing deploy — a differentconfig_digest, and a fleet-wide proxy reboot on upgrade. Acceptance criterion 4 rules that out.Why credentials never touch the command line
docker run --env TOKEN=...puts a credential that can rewrite your DNS zone into the host's process listing and into the audit log kamal writes. The credentials go to a 0600 env file at.kamal/proxy/acme.envand are passed with--env-file. That path sits under the proxy's own directory rather than the app's env directory, because the container is host-scoped and shared by every app on the host — and sokamal proxy removetakes the credentials with it.Test plan
bundle exec rubocop --parallel— cleanbuilder_test/build_test; identical on pristinedashHEAD (host-arch dependent, pass in CI)dns_provider: cloudflare+ a realCF_API_TOKENand confirm a certificate issues via DNS-01.kamal/proxy/acme.envlands at 0600 and is removed bykamal proxy removeacmeblock does not reboot an existing proxy (digest unchanged)Automated coverage
acme settings become kamal-proxy run flags--acme-email/--acme-dns-providerreach the run commandboolean acme settings render with an explicit value when false--acme-http-fallback=falseis emitted in the form Cobra readscredentials are read from secrets and never reach the command line.kamal/secrets,--env-fileonly, no value in the argvboot writes acme credentials to a 0600 env file instead of the command linekamal proxy boot, including the file modereboot re-uploads the acme credentials before replacing the containeran unknown dns_provider fails validation naming the supported providersvalidate_secrets! resolves the acme credentials before any host is contactedno acme block leaves the run command untouched+config_digest is unchanged for a config with no acme blockconfig_digest changes when acme settings change/when the credential names changethe gem's DNS provider allowlist matches what kamal-proxy advertisesDeviations & judgment calls
Deviations from the issue
dns_provideragainst the provider list". Implemented as a hardcoded allowlist inProxy::Acmeplus a generatedacme_dns_providersentry intest/fixtures/kamal_proxy_flags.ymland a canary asserting the two agree. A bare hardcoded list drifts the moment Epic: ACME DNS-01 provider coverage — registry refactor + curated providers dash-proxy#77 lands its 21 providers, and the drift's symptom is "the gem rejects a provider the proxy supports" — a blocked deploy for an operator who is holding it right. The canary makes that fail at theMINIMUM_VERSIONbump instead, next to the existing flag-coverage check from test(proxy): fail the build when kamal-proxy grows a flag the gem cannot emit #83.acme_dns_providersfixture entry was written by hand — the Docker daemon was not running here, sobin/sync-proxy-flagscould not be re-run. The value is transcribed frominternal/cmd/run.goat tagv1.0.0.0in the sibling proxy repo, and the script now generates it, so the next regeneration overwrites it. Worth a second pair of eyes.dns_provider: loopia, whichv1.0.0.0does not support — so that exact config is now rejected. It becomes valid when Expose rate limiting, IP allow lists and client-IP identification in deploy.yml #77 ships andMINIMUM_VERSIONmoves.Judgment calls
emailis required whenever anacmeblock is present. ACME account registration needs it, and the issue's stated motivation is that misconfigured ACME "silently never issues". The cost: an operator who setsACME_EMAILthroughproxy.run.options.envand only wantsprefer_wildcard: falseis now forced to also name the email here. Judged the better trade.credentials.env.secretsupportsNAME:SECRET_NAMEaliasing;credentialstakes a plain list, because the env var name is dictated by lego (CF_API_TOKEN,LOOPIA_API_USER) and has to match. Add aliasing if someone hits it.config_digest; credential values are not. Adding or renaming a credential reboots the proxy. Rotating a value does not — the digest is published as a docker label, and hashing secret material into a world-readable label buys an offline guessing target for nothing. Rotating a DNS token needs an explicitkamal proxy reboot; that is documented indocs/proxy.yml.Kamal::Configuration::Loadbalanceremits no run flags whatsoever today, so--debug,--metrics-portand--recheck-targets-on-restoredo not reach it either. Fixing that means making the load balancer emitrun_command, which turns those on for existing load-balanced deploys. Out of scope for First-class proxy.run.acme block with secrets-backed DNS credentials #73 — worth its own issue.cf,r53,do, ...) are accepted but not drift-checked. kamal-proxy'sParseProviderNametakes them; its--helpdoes not advertise them, so they cannot be generated. They live in a separateDNS_PROVIDER_ALIASESconstant.