Skip to content

Serve env WordPress sites over HTTPS (TLS phase 2: port-range proxy + siteurl scheme + migration) #73

Description

@louisreingold

Context

Phase 1 (PR #72) put the control plane behind TLS on the bare IP: Caddy terminates HTTPS on public :4000 with a Let's Encrypt short-lived IP certificate (shortlived ACME profile, GA since 2026-01, auto-renewed), and devbox-server binds 127.0.0.1:4001. See create-katalystwp/server/deploy/Caddyfile.example and the README "HTTPS on a bare IP" section.

Still plain HTTP: every per-env WordPress site on ports 9000–9999 (wpUrl), including wp-admin browsing after redeeming a minted admin-login link. WP session cookies and page traffic are MITM-able. This issue is Phase 2: serve the env sites over HTTPS too.

Key enabler: an IP certificate is valid for any port, so https://<ip>:9033 works with the same cert Caddy already holds — no domain or per-env subdomains needed.

Work items

  1. Rebind env containers to loopback — the generated docker-compose.yml publishes the WP port (and appPorts) publicly, e.g. "9033:80". Template change: "127.0.0.1:9033:80" so Caddy owns the public side. Applies to the WP port and workspace appPorts alike.
  2. Caddy on the port range — terminate TLS for WP_PORT_RANGE (default 9000–9999), proxying port-for-port to 127.0.0.1. Options, in rough order of preference:
    • generate site blocks for the range with a small script (1000 listeners is unusual for Caddy but workable; consider narrowing the range first);
    • drive Caddy's admin API from the server's allocator (add/remove a route per env on create/destroy) — cleaner but couples the server to Caddy;
    • the layer4 plugin.
  3. WordPress must know it's HTTPSsiteurl/home are baked as http://<ip>:<port> at install:
    • wp-config forwarded-proto shim in the template (set $_SERVER['HTTPS']='on' when X-Forwarded-Proto: https), and Caddy must send the header (default for reverse_proxy);
    • new server/scaffolder config (e.g. DEVBOX_PUBLIC_SCHEME=https) so fresh installs get https:// siteurls and the server's wpUrl / env-card links follow;
    • UI: the admin-login rebase in ui/app.js currently rebases host:port — must carry the scheme too.
  4. Migration for existing envs (~49 on the prod box, all frozen at scaffold time): script that per env (a) edits compose to loopback-bind, docker compose up -d to rebind; (b) adds the forwarded-proto shim; (c) wp option update home/siteurl to https://. Must handle both bare-named and katalyst--prefixed compose projects (see PR The Katalyst menu: create lands on an interactive hub #68) WITHOUT regenerating compose files (regeneration would rename the project and orphan containers).
  5. Warm pools bake at build time — after the template/scheme change, POST /pool/:presetId/rebuild every pool.
  6. Server-side probes stay HTTP — health checks and wp exec go through docker/loopback; they must NOT be switched to the public HTTPS URL.

Risk areas to test first

  • In-container access: setup/dev scripts and health checks hit http://wordpress from inside the compose network. With an https:// siteurl, WP canonical redirects can bounce those requests. The forwarded-proto shim usually suffices, but verify: plugin activation, agent-connector install, admin-login mint (wp eval), and the Breakdance/Oxygen builders.
  • Admin-login flow end-to-end: mint returns a http://localhost:<port> URL that the UI rebases; token redemption must land on https://<ip>:<port> and the resulting wp-admin session must stay https (no mixed-content or redirect loop).
  • SANDBOX_PUBLIC_HOST consumers: preset setup scripts build browser-valid URLs from it; audit for hardcoded http://.
  • Do it on one fresh test env + one migrated env before touching the fleet.

Acceptance

  • New env via a warm claim: wpUrl is https://, site + wp-admin load with a valid cert, no mixed content.
  • Migrated old env (bare compose name): same, containers not orphaned.
  • curl http://<ip>:<wp-port> from outside: connection refused (nothing publicly bound but Caddy).
  • All pools rebuilt green; MCP mint_admin_login + browser admin-login both work.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions