From dec3c8e9f5362230d5c3368d7b8d3dfa72e72d4f Mon Sep 17 00:00:00 2001 From: David Boreham Date: Sun, 23 Aug 2026 16:31:46 -0600 Subject: [PATCH 1/2] Add support for the auth mechanism in the static content wrapper image --- docs/test-coverage.md | 2 + docs/wrappers.md | 52 ++++++++++ skills/deploy-with-stack/SKILL.md | 5 +- tests/lib/common.sh | 21 ++++ .../run-static-content-test.sh | 99 +++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) diff --git a/docs/test-coverage.md b/docs/test-coverage.md index 63aa8d2..8e393e3 100644 --- a/docs/test-coverage.md +++ b/docs/test-coverage.md @@ -32,6 +32,7 @@ file listing is its own index. | Rebuild giving a dirty checkout a `stackdev-` identity | [`tests/app-deploy/run-test.sh`](../tests/app-deploy/run-test.sh) | `deploy update content` | compose + kind per-PR; remote + remote-compose weekly | | `webapp` wrapper (build and serve a Vite/React app) | [`tests/webapp-test/run-webapp-test.sh`](../tests/webapp-test/run-webapp-test.sh) | whole script | compose, per-PR and weekly | | `static-content` wrapper | [`tests/static-content-test/run-static-content-test.sh`](../tests/static-content-test/run-static-content-test.sh) | whole script | compose, per-PR and weekly | +| `static-content` wrapper: HTTP basic auth, configured and unconfigured | [`tests/static-content-test/run-static-content-test.sh`](../tests/static-content-test/run-static-content-test.sh) | `AUTH-CHALLENGED`, `AUTH-SERVED`, `AUTH-EXCLUDE`, `AUTH-HTPASSWD`, `AUTH-HALF-CREDENTIAL` | compose, per-PR and weekly | ## Init and deploy @@ -52,6 +53,7 @@ file listing is its own index. | `manage update`: config change reaches the containers | [`tests/app-deploy/run-test.sh`](../tests/app-deploy/run-test.sh) | `deploy update config` | compose + kind per-PR; remote + remote-compose weekly | | `manage update`: data survives the in-place update | [`tests/app-deploy/run-test.sh`](../tests/app-deploy/run-test.sh) | `deploy update storage` | compose + kind per-PR; remote + remote-compose weekly | | `manage update`: rebuilt image content reaches the deployment | [`tests/app-deploy/run-test.sh`](../tests/app-deploy/run-test.sh) | `deploy update content` | compose + kind per-PR; remote + remote-compose weekly | +| `manage update`: authentication turned on and off after deployment | [`tests/static-content-test/run-static-content-test.sh`](../tests/static-content-test/run-static-content-test.sh) | `DEPLOY-AUTH-SERVED`, `DEPLOY-AUTH-CHALLENGED`, `DEPLOY-AUTH-REMOVED` | compose, per-PR and weekly | | Spec-mapped volume path: pre-existing host data reaches the container | [`tests/volumes/run-test.sh`](../tests/volumes/run-test.sh) | `external data visible test`, `unmapped volume fresh test`, `volume write-back test` | compose + kind per-PR; remote weekly | | `manage destroy`: the deployment is finished; later `manage` commands refuse its directory | [`tests/smoke-test/run-smoke-test.sh`](../tests/smoke-test/run-smoke-test.sh) | `deploy destroy` | compose, per-PR | | `manage exec` against a running service | [`tests/database/run-backup-test.sh`](../tests/database/run-backup-test.sh) | `Replay dump test` | compose per-PR; remote weekly | diff --git a/docs/wrappers.md b/docs/wrappers.md index 7d0bf8c..e31f449 100644 --- a/docs/wrappers.md +++ b/docs/wrappers.md @@ -127,6 +127,58 @@ $ stack webapp build --wrapper static-content --source-repo ~/my-static-site --c build the same way. See [stack-files.md](./stack-files.md#path-vs-content-root) for how it relates to `path`, with worked examples of each combination. +## Authentication for static content + +The `static-content` wrapper can put the site it serves behind HTTP basic authentication. +Nothing is gated by default; a username and password in the container's environment turns +it on. Deployment config reaches every service of a deployment, so the composefile needs +no entry of its own: + +``` +$ stack init --stack my-site --output spec.yml \ + --config STACK_AUTH_USER=alice --config STACK_AUTH_PASSWORD=secret +``` + +| Variable | Meaning | +|----------|---------| +| `STACK_AUTH_USER`, `STACK_AUTH_PASSWORD` | One credential. The password is hashed when the container starts, and the two must be set together — half a credential refuses to start rather than guessing which half was meant. | +| `STACK_AUTH_HTPASSWD` | The content of an htpasswd file, already hashed: several users, and no plaintext password in the environment. Additive with the pair above. | +| `STACK_AUTH_REALM` | The name the browser's prompt shows. Defaults to `Restricted`. | +| `STACK_AUTH_EXCLUDE` | Space-separated path prefixes served without credentials. | + +The variables are read when the container starts rather than baked into the image, which +makes this a decision that can be made after deploying — the usual way round, since a site +is normally published before anyone asks for it to be private. Adding them to a running +deployment's `config.env` and running `update` gates it: + +``` +$ echo 'STACK_AUTH_USER=alice' >> /config.env +$ echo 'STACK_AUTH_PASSWORD=secret' >> /config.env +$ stack manage --dir update +``` + +`update` applies environment changes on every target (on Kubernetes that is explicitly all +it applies, along with images and secrets), and removing the variables again ungates the +site. The image is the same either way, so neither direction is a rebuild. + +Two things to know before turning it on: + +- **A composefile `healthcheck` must be excluded.** On Kubernetes a healthcheck becomes + the container's liveness probe, and a probe answered with a 401 restarts the pod for as + long as authentication is configured. Name its path in `STACK_AUTH_EXCLUDE`. +- **Basic authentication is only worth having over HTTPS**, since the password travels + with every request protected by nothing but base64. See [ingress.md](./ingress.md). + +A password that matters belongs in `secrets:` rather than `--config`: a config value is +written to `config.env` in the clear, while a secret is resolved at up time from an +`env:`/`file:`/`exec:` reference and never lands in the deployment directory. `generate` +is no use here — someone has to know this password to type it — so give the secret a +reference. See [secrets.md](./secrets.md). + +The other wrappers deliberately have no equivalent. They build applications that can +authenticate their own users, with a session and an account database, and a gate in front +of the whole site would be in the way of that rather than an alternative to it. + ## Runtime environment A service reads its configuration from the environment at startup, so deploying the same image diff --git a/skills/deploy-with-stack/SKILL.md b/skills/deploy-with-stack/SKILL.md index 80c0bba..1f661c2 100644 --- a/skills/deploy-with-stack/SKILL.md +++ b/skills/deploy-with-stack/SKILL.md @@ -107,7 +107,10 @@ Rules that matter: name in the composefile (`image: postgres:16`); an app without a Dockerfile can use a wrapper — `static-content` for a static site, `webapp` for a built frontend, `nextjs` for Next.js, `node-service` for a long-running node service (list them with - `stack webapp wrappers`). + `stack webapp wrappers`). A `static-content` site can be put behind HTTP basic + authentication with the `STACK_AUTH_USER` and `STACK_AUTH_PASSWORD` config + variables, which is a decision that can be made (and reversed) after deploying; + see https://github.com/bozemanpass/stack/blob/main/docs/wrappers.md - Pods are the unit of deployment grouping; one composefile each. A single pod holding all services is the right default for a small system. - A pod entry can carry `pre_start_command` / `post_start_command` (host-side scripts, diff --git a/tests/lib/common.sh b/tests/lib/common.sh index dfae9cf..29546b1 100644 --- a/tests/lib/common.sh +++ b/tests/lib/common.sh @@ -827,6 +827,27 @@ assert_file_not_contains () { fi } +# Assert that fetching $1 answers with HTTP status $2, reporting "