feat: allow running over plain http #37

Merged
ryan.mcgee merged 2 commits from feat/plain-http into main 2026-10-21 15:08:38 +00:00
Owner

Summary

Traefik Control can now be served over plain HTTP, for example over a VPN at http://control.vpn.example:4000 or behind a reverse proxy without TLS. Until now it could not: force_ssl was set at compile time in config/prod.exs (every plain-HTTP request was redirected to https://PHX_HOST and HSTS was sent), and config/runtime.exs hardcoded url: [host: host, port: 443, scheme: "https"]. Deployments that leave the new variables unset behave as before.

Changes

  • config/runtime.exs: new PHX_SCHEME (https default, or http) and PHX_URL_PORT (default 443 for https, 80 for http, integer 1-65535). Any other value raises at boot, like the other env checks. The endpoint gets url: [host: PHX_HOST, port: PHX_URL_PORT, scheme: PHX_SCHEME], and :force_ssl is set to scheme == "https" (production only).
  • config/prod.exs: removed the compile-time force_ssl.
  • New TraefikControlWeb.Plugs.ForceSSL (endpoint, right after HealthPlug): applies Plug.SSL with the same options as before (rewrite_on: [:x_forwarded_proto], exclude /healthz /readyz and localhost/127.0.0.1, default HSTS, redirect to the configured host via {Endpoint, :host, []} as Phoenix did). The options are computed once at compile time and only the boolean switch is read per request. Dev and test never set the switch, so they never redirect.
  • Absolute URLs still come from configuration only, never from the request Host header (invites, confirmations, the OIDC callback, node snippets, the OpenAPI server URL and the WebAuthn origin all go through Endpoint.url() / url(~p...)).
  • WebAuthn: new WebAuthn.available?/1 (https, or a loopback name over http). When it is false, the security keys section of the two-factor settings page says that keys need https or localhost and does not show "Add a key". Authenticator apps and recovery codes still work.
  • README: PHX_SCHEME and PHX_URL_PORT rows next to PHX_HOST, a "Running over plain HTTP" paragraph (VPN or your own proxy, the WebAuthn limit, TRAEFIK_CONTROL_TRUST_PROXY only behind a proxy you control, node snippets showing http://), and the WebAuthn paragraph. Comments that described force_ssl as compile-time were updated (HealthPlug moduledoc, runtime.exs).
  • Tests: test/traefik_control_web/plugs/force_ssl_test.exs covers https mode (301/307 to https://<configured host> even for a foreign Host header; HSTS on https and on X-Forwarded-Proto: https; /healthz, /readyz, localhost and 127.0.0.1 left alone), http mode (no redirect, no HSTS) and the unset switch (dev/test). web_authn_test.exs covers available?/1. Existing runtime.exs options have no tests, so the new parsing has none either; it was checked against the release image instead (see below).

Verification

All commands ran in the repo's tooling image (podman build -f Containerfile.dev -t traefik-control-dev ., run with --userns=keep-id -v "$PWD":/work):

  • TRAEFIK_CONTROL_DB=sqlite3: mix format --check-formatted passed; mix compile --warnings-as-errors passed; mix credo --strict: 2324 mods/funs, no issues; mix dialyzer: Total errors: 0, passed successfully.
  • TRAEFIK_CONTROL_DB=sqlite3 MIX_ENV=test mix test: 1522 passed.
  • TRAEFIK_CONTROL_DB=postgres PGHOST=127.0.0.1 MIX_ENV=test mix test (with postgres:latest in a keep-id pod): 1522 passed.

Smoke test of the release image (podman build --build-arg DB=sqlite3 .) with a throwaway /data volume, PHX_HOST=localhost.test:

# PHX_SCHEME=http PHX_URL_PORT=4000
$ curl -sS -o /dev/null -D - -H 'Host: localhost.test:4000' http://127.0.0.1:14000/users/log-in
HTTP/1.1 200 OK
(no location, no strict-transport-security header)
$ curl -sS -H 'Host: evil.example' http://127.0.0.1:14000/api/v1/openapi.json   # servers
[{'url': 'http://localhost.test:4000/api/v1'}]

# PHX_SCHEME unset
$ curl -sS -o /dev/null -D - -H 'Host: localhost.test' http://127.0.0.1:14001/users/log-in
HTTP/1.1 301 Moved Permanently
location: https://localhost.test/users/log-in
$ curl ... -H 'X-Forwarded-Proto: https' http://127.0.0.1:14001/users/log-in
HTTP/1.1 200 OK
strict-transport-security: max-age=31536000
$ curl ... http://127.0.0.1:14001/healthz
HTTP/1.1 200 OK
openapi servers: [{'url': 'https://localhost.test/api/v1'}]

# Invalid values raise at boot
PHX_SCHEME=ftp       -> ** (RuntimeError) PHX_SCHEME must be "https" or "http", got: "ftp"
PHX_URL_PORT=70000   -> ** (RuntimeError) PHX_URL_PORT must be a port number (1-65535), got: "70000"
PHX_URL_PORT=abc     -> ** (RuntimeError) PHX_URL_PORT must be a port number (1-65535), got: "abc"
## Summary Traefik Control can now be served over plain HTTP, for example over a VPN at `http://control.vpn.example:4000` or behind a reverse proxy without TLS. Until now it could not: `force_ssl` was set at compile time in `config/prod.exs` (every plain-HTTP request was redirected to `https://PHX_HOST` and HSTS was sent), and `config/runtime.exs` hardcoded `url: [host: host, port: 443, scheme: "https"]`. Deployments that leave the new variables unset behave as before. ## Changes - `config/runtime.exs`: new `PHX_SCHEME` (`https` default, or `http`) and `PHX_URL_PORT` (default `443` for https, `80` for http, integer 1-65535). Any other value raises at boot, like the other env checks. The endpoint gets `url: [host: PHX_HOST, port: PHX_URL_PORT, scheme: PHX_SCHEME]`, and `:force_ssl` is set to `scheme == "https"` (production only). - `config/prod.exs`: removed the compile-time `force_ssl`. - New `TraefikControlWeb.Plugs.ForceSSL` (endpoint, right after `HealthPlug`): applies `Plug.SSL` with the same options as before (`rewrite_on: [:x_forwarded_proto]`, exclude `/healthz` `/readyz` and `localhost`/`127.0.0.1`, default HSTS, redirect to the configured host via `{Endpoint, :host, []}` as Phoenix did). The options are computed once at compile time and only the boolean switch is read per request. Dev and test never set the switch, so they never redirect. - Absolute URLs still come from configuration only, never from the request `Host` header (invites, confirmations, the OIDC callback, node snippets, the OpenAPI server URL and the WebAuthn origin all go through `Endpoint.url()` / `url(~p...)`). - WebAuthn: new `WebAuthn.available?/1` (https, or a loopback name over http). When it is false, the security keys section of the two-factor settings page says that keys need https or localhost and does not show "Add a key". Authenticator apps and recovery codes still work. - README: `PHX_SCHEME` and `PHX_URL_PORT` rows next to `PHX_HOST`, a "Running over plain HTTP" paragraph (VPN or your own proxy, the WebAuthn limit, `TRAEFIK_CONTROL_TRUST_PROXY` only behind a proxy you control, node snippets showing `http://`), and the WebAuthn paragraph. Comments that described `force_ssl` as compile-time were updated (`HealthPlug` moduledoc, `runtime.exs`). - Tests: `test/traefik_control_web/plugs/force_ssl_test.exs` covers https mode (301/307 to `https://<configured host>` even for a foreign Host header; HSTS on https and on `X-Forwarded-Proto: https`; `/healthz`, `/readyz`, `localhost` and `127.0.0.1` left alone), http mode (no redirect, no HSTS) and the unset switch (dev/test). `web_authn_test.exs` covers `available?/1`. Existing `runtime.exs` options have no tests, so the new parsing has none either; it was checked against the release image instead (see below). ## Verification All commands ran in the repo's tooling image (`podman build -f Containerfile.dev -t traefik-control-dev .`, run with `--userns=keep-id -v "$PWD":/work`): - `TRAEFIK_CONTROL_DB=sqlite3`: `mix format --check-formatted` passed; `mix compile --warnings-as-errors` passed; `mix credo --strict`: 2324 mods/funs, no issues; `mix dialyzer`: Total errors: 0, passed successfully. - `TRAEFIK_CONTROL_DB=sqlite3 MIX_ENV=test mix test`: 1522 passed. - `TRAEFIK_CONTROL_DB=postgres PGHOST=127.0.0.1 MIX_ENV=test mix test` (with `postgres:latest` in a keep-id pod): 1522 passed. Smoke test of the release image (`podman build --build-arg DB=sqlite3 .`) with a throwaway `/data` volume, `PHX_HOST=localhost.test`: ``` # PHX_SCHEME=http PHX_URL_PORT=4000 $ curl -sS -o /dev/null -D - -H 'Host: localhost.test:4000' http://127.0.0.1:14000/users/log-in HTTP/1.1 200 OK (no location, no strict-transport-security header) $ curl -sS -H 'Host: evil.example' http://127.0.0.1:14000/api/v1/openapi.json # servers [{'url': 'http://localhost.test:4000/api/v1'}] # PHX_SCHEME unset $ curl -sS -o /dev/null -D - -H 'Host: localhost.test' http://127.0.0.1:14001/users/log-in HTTP/1.1 301 Moved Permanently location: https://localhost.test/users/log-in $ curl ... -H 'X-Forwarded-Proto: https' http://127.0.0.1:14001/users/log-in HTTP/1.1 200 OK strict-transport-security: max-age=31536000 $ curl ... http://127.0.0.1:14001/healthz HTTP/1.1 200 OK openapi servers: [{'url': 'https://localhost.test/api/v1'}] # Invalid values raise at boot PHX_SCHEME=ftp -> ** (RuntimeError) PHX_SCHEME must be "https" or "http", got: "ftp" PHX_URL_PORT=70000 -> ** (RuntimeError) PHX_URL_PORT must be a port number (1-65535), got: "70000" PHX_URL_PORT=abc -> ** (RuntimeError) PHX_URL_PORT must be a port number (1-65535), got: "abc" ```
feat: allow running over plain http
Some checks failed
CI / format, compile, credo, dialyzer (pull_request) Successful in 1m36s
CI / tests (SQLite) (pull_request) Successful in 2m34s
CI / dependency audit (pull_request) Has been cancelled
CI / tests (PostgreSQL) (pull_request) Has been cancelled
Development images / images (push) Has been cancelled
1f4750be4d
- Add PHX_SCHEME (https default, or http) and PHX_URL_PORT (default
  443/80) to config/runtime.exs, validated at boot like the other
  options; generated URLs use them with PHX_HOST
- Replace the compile-time force_ssl in config/prod.exs with a runtime
  plug (TraefikControlWeb.Plugs.ForceSSL): same Plug.SSL options, on
  only in production with https; http serves without redirect or HSTS
- Tell users on the two-factor page that security keys need https or
  localhost instead of offering to add one over plain http
- Document both variables and running over plain HTTP in the README
test: use a generic example host
All checks were successful
Development images / images (push) Successful in 54s
CI / format, compile, credo, dialyzer (pull_request) Successful in 1m12s
CI / tests (SQLite) (pull_request) Successful in 2m3s
CI / tests (PostgreSQL) (pull_request) Successful in 2m34s
CI / dependency audit (pull_request) Successful in 54s
965d18175c
- the plain-http web_authn case named a real peer; use a reserved
  example domain like the other cases
ryan.mcgee deleted branch feat/plain-http 2026-10-21 15:08:39 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
White_Ghost_Cyber_Systems/traefik-control!37
No description provided.