feat: serve env WordPress sites over HTTPS (TLS phase 2, fresh-env path) - #77
Open
louisreingold wants to merge 4 commits into
Open
feat: serve env WordPress sites over HTTPS (TLS phase 2, fresh-env path)#77louisreingold wants to merge 4 commits into
louisreingold wants to merge 4 commits into
Conversation
The control plane got TLS in PR #72; this extends it to the per-env sites (issue #73). Key enabler: a Let's Encrypt IP certificate is valid for any port, so Caddy terminates TLS for the whole env port range with the cert it already holds — no domain needed. Scaffolder: - --bind=IP publishes ports (WP + app ports) on one host interface only, via a new WP_BIND prefix in .env interpolated into docker-compose.yml ("127.0.0.1:" when a proxy owns the public side; empty = unchanged local-dev behavior). - --public-scheme=http|https lands in .env as PUBLIC_SCHEME and reaches setup scripts as SANDBOX_PUBLIC_SCHEME (run-setup-script.sh). - wp-config forwarded-proto shim: X-Forwarded-Proto: https sets $_SERVER['HTTPS']='on' so the existing per-request WP_HOME/WP_SITEURL scheme detection (and is_ssl()) yields https behind the proxy. - `update` carries WP_BIND/PUBLIC_SCHEME through from the project's .env. Server: - DEVBOX_PUBLIC_SCHEME=https makes new envs scaffold with --bind=127.0.0.1 --public-scheme=https. - The scheme is recorded PER ENV at allocation (record.scheme): envs created before a switch keep working http URLs (and their public port binds) until migrated; a warm env claimed after the switch reports the scheme it was built with; a duplicate inherits its source's scheme (the copy is the source's dir verbatim). - wpUrl / admin-login loginUrl / UI links all follow the record's scheme (ops.js, status.js, ui/app.js). clone.js handles the new bind-aware app-port line shape. - Built-in FutureLayer preset URLs use SANDBOX_PUBLIC_SCHEME. Deploy: - deploy/gen-env-sites.sh generates ONE Caddy site block listing every range port as an address, proxying port-for-port via the {http.request.local.port} placeholder. The explicit `bind <public-ip>` is load-bearing: it keeps loopback free for docker's publishes and the allocator's port probe (Caddy would otherwise bind 0.0.0.0). - Caddyfile.example + READMEs document the switch; warm pools must be rebuilt after it (they bake the scaffold at build time). Migration of pre-existing envs is deliberately NOT in this change — they continue exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erator, global cert_issuer)
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.
What
Serves env WordPress sites over HTTPS (issue #73, phase 2 — the fresh-env path). The control plane got TLS in #72; this extends it to the per-env sites using the key fact that a Let's Encrypt IP certificate is valid for any port — Caddy terminates TLS for the whole env port range with the cert it already holds. No domain needed.
How
Scaffolder
--bind=IPpublishes ports (WP + app ports) on one host interface only, via a newWP_BINDprefix in.envinterpolated intodocker-compose.yml("${WP_BIND:-}${WP_PORT}:80"). Empty = unchanged local-dev behavior; local scaffolds are byte-identical to before.--public-scheme=http|httpslands in.envasPUBLIC_SCHEMEand reaches setup scripts asSANDBOX_PUBLIC_SCHEME.X-Forwarded-Proto: httpssets$_SERVER['HTTPS']='on', so the existing per-requestWP_HOME/WP_SITEURLscheme detection (andis_ssl()) yields https behind the proxy. In-networkhttp://wordpressaccess is untouched (header never set there).--bind=127.0.0.1, Caddy's listener on the public address of the same port is not a conflict (this bit an E2E run before the fix).updatecarriesWP_BIND/PUBLIC_SCHEMEthrough from the project's.env.Server
DEVBOX_PUBLIC_SCHEME=https→ new envs scaffold with--bind=127.0.0.1 --public-scheme=https.record.scheme). Envs created before a switch keep their working http URLs and public port binds until migrated; a warm env claimed after the switch reports the scheme it was built with; a duplicate inherits its source's scheme (the copy is the source's dir verbatim).wpUrl, admin-loginloginUrl, and all UI links follow the record.clone.jshandles the bind-aware app-port line shape.SANDBOX_PUBLIC_SCHEME.Deploy
deploy/gen-env-sites.shemits ONE Caddy site block listing every range port as an address, proxied port-for-port via{http.request.local.port}. Two load-bearing details, both learned the hard way: explicitbind <public-ip>(Caddy's default 0.0.0.0 would collide with docker's loopback publishes and the allocator's probe), and notlsdirective (a second automation policy for the same IP is rejected — the env sites share the control-plane block's certificate).Verified on the live box
Deployed with
WP_PORT_RANGE=9100-9299(existing envs sit on 9000–9058; the range starts above them so Caddy never collides with their public binds):https://200 with a verified LE cert (ssl_verify_result: 0, no-k), 30/30 rendered URLs https (no mixed content),docker-proxyon127.0.0.1:9100+caddyon174.138.43.144:9100, plain http to the port → 400 (nothing served).https://…/wp-admin/index.php, session cookie valid for subsequent https requests. No redirect loops.http://wordpress→ 200 (no canonical bounce); DB siteurl untouched.http://URLs and work unchanged.warm: 0), so no rebuilds were needed.Not in this change
🤖 Generated with Claude Code