Skip to content

feat(proxy): expose the kamal-proxy run flag surface and a run.flags escape hatch - #92

Merged
mhenrixon merged 1 commit into
dashfrom
issue-81-run-flag-surface
Aug 2, 2026
Merged

mhenrixon merged 1 commit into
dashfrom
issue-81-run-flag-surface

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

kamal-proxy run accepts 29 flags; run_command_options emitted three, and there was no passthrough either — proxy.run.options goes to docker run, not to kamal-proxy run. Fourteen named keys plus an escape hatch close the gap.

proxy:
  exclude_metrics_paths: [ /up ]
  run:
    log_format: text
    trace_context: generate
    min_tls: "1.3"
    http3: true
    reuse_port: true
    ignore_restore_errors: true
    proxy_protocol: true
    proxy_protocol_allow_ips: [ 10.0.0.0/8 ]
    metrics_allow_ips: [ 10.0.0.0/8 ]
    read_header_timeout: 10
    read_timeout: 30
    write_timeout: 30
    idle_timeout: 60
    shutdown_timeout: 15
    flags:                       # -> kamal-proxy run
      some-new-flag: value
    options:                     # -> docker run (existing, unchanged)
      memory: 512m

Closes #81

🎉 This empties R7_BACKLOG

Every flag kamal-proxy accepts is now either emitted from a deploy.yml key or waived with a stated reason — which test/proxy_flag_coverage_test.rb defines as "R7 is done". A future proxy release that adds a flag fails the build at the MINIMUM_VERSION bump, the one moment anyone is thinking about gem/proxy compatibility.

The escape hatch is a hazard for exactly that check, so the fixture's flags: entry is not-a-real-kamal-proxy-flag. run.flags forwards anything, so dropping a genuinely new proxy flag in there would turn the canary green while leaving the flag unreachable by name — precisely the silence the test exists to break. The R7_BACKLOG comment now says so in as many words.

Test plan

  • bundle exec rubocop --parallel — clean, 216 files
  • Unit suite — 1350 runs; 4 failures + 3 errors, all in builder_test/build_test, identical on pristine dash HEAD (host-arch dependent, pass in CI)
  • Full generated run command inspected by hand — all 14 named flags, the passthrough, and --exclude-metrics-path on the deploy command
  • Boot with log_format: text and confirm the access log switches to logfmt
  • Boot with min_tls: "1.3" and confirm a TLS 1.2 client is refused
  • Boot with http3: true and confirm HTTP/3 negotiates (needs UDP open on the HTTPS port)
  • Behind an L4 balancer, confirm proxy_protocol + proxy_protocol_allow_ips preserves client addresses in the access log
  • Change any new run key and confirm exactly one proxy reboot per host

Automated coverage

Test Proves
every new run key moves config_digest AC1, for all 13 scalar keys in one loop
run.flags are optionized onto the run command / is distinct from run.options AC2 — the escape hatch works and does not leak into docker run
a run.flags key colliding with a named key is rejected (+ the always-emitted case) AC2 — rejected, not emitted twice
proxy_protocol without an allow list warns (+ negative case) AC3
no new keys leave the run command byte-identical AC4
booleans left false emit nothing matches the proxy defaults without redundant flags
an explicit zero timeout reaches the proxy zero disables these rather than meaning unset
the enum tests, incl. logfmt, tls1_2 and the 1.0/1.1 message typos caught here instead of killing the container at boot
exclude_metrics_paths lands on the deploy command it is a deploy flag, not a run flag
every kamal-proxy deploy/run flag is exposed or waived with an empty backlog

Deviations & judgment calls

Deviations

  • The issue lists --data-dir as unreachable. It is not — it sits in NEVER_EXPOSED because the kamal-proxy-config volume mount fixes it, and exposing it would let an operator point the proxy's state outside the volume that persists it. Left waived with the existing reason.
  • proxy_protocol_allow_ips and metrics_allow_ips are plural, matching allow_ips from Expose rate limiting, IP allow lists and client-IP identification in deploy.yml #77 rather than the singular flag names.

Judgment calls

  • exclude_metrics_paths sits at proxy.exclude_metrics_paths, not under run. The issue groups it with metrics config, but it is a deploy flag — under run it would be a key that looks proxy-wide feeding a per-service flag, and a config_digest that moves when nothing about the container changed. The docs cross-reference run.metrics_port and run.metrics_allow_ips instead.
  • The run.flags collision check lives in Proxy::Run#initialize, not the validator. Whether a passthrough key duplicates a named one is only knowable once the named options are built, and only Proxy::Run builds them. It raises a Kamal::ConfigurationError with the same proxy/run context, so an operator cannot tell the difference.
  • run.flags values are forwarded exactly as written — true renders a bare flag, anything else --flag value. No guessing at Go duration or list syntax: the point of an escape hatch is that the gem holds no opinion about the flag it is passing.
  • proxy_protocol without an allow list warns rather than errors. It is a real spoofing hole — the header rewrites the connecting address that allow_ips, rate_limit and the access log all key on — but a proxy on a private network with no other route in is a legitimate configuration.
  • flags needed the proxy validator's validate_key_override! arm, alongside headers from Expose header rules, redirects, rewrites and error interception in deploy.yml #78 — the example's key is an illustration, so the unknown-key check would reject every real use.

…escape hatch

kamal-proxy run accepts 29 flags; run_command_options emitted three, and there
was no passthrough either -- proxy.run.options goes to `docker run`, not to
`kamal-proxy run`. Fourteen named keys and the escape hatch close that.

    proxy:
      exclude_metrics_paths: [ /up ]
      run:
        log_format: text
        trace_context: generate
        min_tls: "1.3"
        http3: true
        reuse_port: true
        ignore_restore_errors: true
        proxy_protocol: true
        proxy_protocol_allow_ips: [ 10.0.0.0/8 ]
        metrics_allow_ips: [ 10.0.0.0/8 ]
        read_header_timeout: 10
        read_timeout: 30
        write_timeout: 30
        idle_timeout: 60
        shutdown_timeout: 15
        flags:
          some-new-flag: value

This empties R7_BACKLOG. Every flag kamal-proxy accepts is now either emitted
from a deploy.yml key or waived with a reason, which is what the coverage test
defines as "R7 is done".

That makes the escape hatch a hazard for the canary, so the fixture's flags:
entry is a name kamal-proxy will never have. run.flags forwards anything, so
putting a real new flag there would turn the check green while leaving the flag
unreachable by name -- exactly the silence the test exists to break. The
R7_BACKLOG comment now says so.

The collision check lives in Proxy::Run#initialize rather than the validator:
whether a passthrough key duplicates a named one is only knowable once the named
options are built, and only Proxy::Run builds them. It raises with the same
proxy/run context the validator uses.

exclude_metrics_paths sits at proxy.exclude_metrics_paths, not under run. The
issue groups it with metrics config, but it is a deploy flag -- under run it
would be a key that looks proxy-wide feeding a per-service flag, and a
config_digest that moves when nothing about the container changed.

log_format, trace_context and min_tls are validated because kamal-proxy checks
them in preRun: a typo does not fail the deploy, it stops the container from
booting. min_tls accepts the spellings normalizeTLSVersion reduces (tls1_2 and
friends) and gives 1.0/1.1 their own message -- the flag narrows what the
listener negotiates and was never a way to widen it.

proxy_protocol without proxy_protocol_allow_ips warns. The header rewrites the
connecting address that allow_ips, rate_limit and the access log all key on, so
honouring it from any peer that can reach the port hands that address to whoever
asks. A warning rather than an error: a proxy on a private network with no other
route in is legitimate.

The issue also lists --data-dir as unreachable. It is not -- it is in
NEVER_EXPOSED because the kamal-proxy-config volume mount fixes it, and exposing
it would let an operator point the proxy's state outside the volume that
persists it. Left waived.

Closes #81
@mhenrixon
mhenrixon merged commit 8bd5dd1 into dash Aug 2, 2026
9 checks passed
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.

Expose the kamal-proxy run flag surface (26 unreachable) plus a run.flags escape hatch

1 participant