Skip to content

feat(proxy): freeze the v3 config surface — renames, cuts, ssl unification, validators (#93 workstream D) - #96

Merged
mhenrixon merged 1 commit into
dashfrom
feat/v3-surface-freeze
Aug 3, 2026
Merged

mhenrixon merged 1 commit into
dashfrom
feat/v3-surface-freeze

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

Workstream D of the v3.0.0 release gate (#93) — the surface freeze. Only dash 2.12.0 is on rubygems, so every key touched here is unreleased: renames, regroupings and cuts are free now and breaking forever after.

  • D1 — renames. tls_domains → ssl_domains; path_timeouts → path_response_timeouts. Emitted flags (--tls-domains-*, --path-timeout) unchanged; no back-compat aliases.
  • D2 — read-routing nests as read_routing: {targets, websockets, writer_affinity_timeout}. Edge-only disposition (from feat(proxy): explicit loadbalancer layering contract + LB run plumbing (#93 workstream B) #94) unchanged.
  • D3 — mTLS reshape + tls: block deleted. tls.client_ca_path (local file path, File.exist?-checked at config time — which bricked kamal app logs/rollback on machines without the file) becomes ssl.client_ca_pem, a secret name mirroring ssl.certificate_pem, resolved at upload time; an empty secret raises like basic_auth.password_secret. on_demand_url folds into the ssl hash; the emptied tls: block is gone — TLS has one naming family (ssl*). The LB boot path now uploads role TLS material (custom certs and client CA) to the loadbalancer host, where TLS actually terminates.
  • D4 — cuts, each recorded in the flag-coverage waiver list with its reason: tls.acme_cache_path, run.cache.lease_ttl, run.cache.lease_wait, scope_cookie_paths.
  • D5 — validators. canonical_host × on_demand_url conflict (was a post-SSH failure); canonical_host must be in hosts (when a static list exists); client_ip.header without trusted_proxies rejected unconditionally (was a client-spoofable X-Forwarded-For rewrite when it slipped the old gate); negative response_timeout rejected; target.max_idle_conns: 0 warns ("means the proxy default of 100, not none"); = inside redirect/rewrite from: rejected (the <from>=<to> wire format cuts at the first =).
  • D6 — compress: {enabled: false, encodings: […]} silently kept compression on. An explicit false now wins, and is a legal off switch (validator updated to match).
  • D7 — DNS provider short aliases (cf, r53, …) documented in the dns_provider docs instead of undocumented magic.

Refs #93 (workstream D — E and F/G continue in follow-up PRs)

Test plan

  • Renames: full suite green with keys renamed in code, docs schema, fixtures, validator (incl. the base validator's path_timeouts hash special-case)
  • client_ca_pem: container-path translation, role scoping, secret resolution at upload time, empty-secret raise, LB re-add, CLI upload of secret content
  • on-demand combos re-validated in the ssl-hash shape (hosts / certificate / ssl_domains / URL shape / sleep)
  • Each D5 validator has a rejection test and (where relevant) an acceptance test; max_idle_conns warns via stderr
  • D6: enabled: false beats encodings at emission, and is legal with tuned settings
  • Cuts: no flag emitted, docs/validator arms gone, waivers recorded; flag-coverage + layering canaries green
  • bundle exec rubocop --parallel clean; unit suite green (builder failures are the known Apple-Silicon artifacts)
  • CI (incl. integration on amd64)

Deviations & judgment calls

  • D3 widened one notch: the LB boot upload covers custom certificates and the client CA — the issue named only the CA, but custom certs are the same edge-terminated material with the same missing-upload gap in the same topology, via the same SslCertificates class. Uploading one and not the other would leave certs broken in exactly the topology D3 fixes mTLS for.
  • D5 canonical_host∈hosts only fires when a static host/hosts list exists — behind a loadbalancer or with ssl_domains/on-demand there is nothing to check against.
  • D5 client_ip.header implemented by dropping the allow_ips/rate_limit gate on the existing check rather than adding a second rule — one rule, one message.
  • D6 semantic choice: explicit enabled: false with tuned compress settings is now legal and off; the orphan error only fires when enabled is absent. The old validator errored on that shape, which made role-level opt-out (role sets enabled: false over a root block with encodings) impossible. This deliberately diverges from cache's hard-error gating — cache has no implicit-on-via-list, so the shapes aren't parallel. One existing test updated accordingly.
  • max_idle_conns warning lives in Kamal::Configuration#ensure_max_idle_conns_meaningful beside the other warnings (validators only error; the roles loop avoids duplicates).
  • Discovery: the base validator special-cases path_timeouts for hash validation (validator.rb), outside the proxy validator — renamed there too.
  • Failure-mode change (D3): a missing CA is now a missing-secret error at deploy time (or an explicit empty-secret raise), never a config-time File.exist? failure on unrelated commands.
  • Pre-release configs using ssl: true + tls: {…} now fail with an unknown-key error on tls — intentional, per the no-aliases decision.

…ation, validators

## Summary

Workstream D of the v3.0.0 release gate (#93). Only dash 2.12.0 is on
rubygems, so every key added since is unreleased - renames, regroupings and
cuts are free now and breaking forever after. This freezes the surface.

- D1 renames: `tls_domains` -> `ssl_domains`, `path_timeouts` ->
  `path_response_timeouts`. Emitted flags unchanged; no back-compat aliases.
- D2: the read-routing trio nests as
  `read_routing: {targets, websockets, writer_affinity_timeout}`.
- D3 mTLS reshape: `tls.client_ca_path` (a local file path checked with
  File.exist? at config time - which bricked `kamal app logs` on machines
  without the file) becomes `ssl.client_ca_pem`, a secret name mirroring
  `ssl.certificate_pem`, resolved at upload time via StringIO; an empty
  secret raises like basic_auth.password_secret. `on_demand_url` folds into
  the `ssl` hash too, and the emptied `tls:` block is deleted - TLS has one
  naming family. The LB boot path now uploads the role TLS material (custom
  certs + client CA) to the loadbalancer host, where TLS actually terminates.
- D4 cuts: `tls.acme_cache_path` (proxy default already persists in the
  volume), `run.cache.lease_ttl`/`lease_wait`, `scope_cookie_paths`. Each cut
  is recorded in the flag-coverage waiver list with its reason.
- D5 validators: canonical_host x on_demand_url conflict; canonical_host must
  be in hosts; client_ip.header without trusted_proxies is now rejected
  unconditionally (was a client-spoofable X-Forwarded-For rewrite); negative
  response_timeout rejected; warn on target.max_idle_conns: 0 (means the
  proxy default of 100, not none); '=' inside redirect/rewrite `from` rejected
  (the <from>=<to> wire format cuts at the first '=').
- D6: `compress: {enabled: false, encodings: [...]}` used to silently keep
  compression on - an explicit false now wins, and is a legal off switch for
  a block whose tuning stays.
- D7: the DNS provider short aliases are documented instead of undocumented
  magic.

## Verification

- [x] bundle exec rubocop --parallel passes
- [x] unit suite passes (builder failures are the known host-arch artifacts)

Refs #93
@mhenrixon mhenrixon self-assigned this Aug 3, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Aug 3, 2026
@mhenrixon
mhenrixon merged commit 4098b60 into dash Aug 3, 2026
9 checks passed
@mhenrixon
mhenrixon deleted the feat/v3-surface-freeze branch August 3, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant