Access protection

A password in front of an app, without touching the app

Staging environments, pre-launch sites, and client review builds usually need to be reachable but not public. Access protection is a per-app toggle that puts an HTTP Basic password challenge in front of your app at the proxy, before a single request reaches your container. You don't add a middleware, a route guard, or a line of code.

What the toggle does

When protection is on, the shared Caddy proxy prepends an authentication handler to the app's serving leg. Anything past that point — your reverse-proxied container, or the file server for a static site — is only reached by a request that presented the right credentials. Everything else about the app is unchanged: same container, same image, same port, same certificate.

The username is fixed at preview and isn't editable. You choose the password. There is exactly one credential pair per app, shared by everyone you hand it to.

This blocks machines too, not just humans.

The gate is unconditional: every caller must present the password, including programmatic ones. Webhook senders, API clients, uptime monitors, and crawlers all get a 401 while protection is on. If something automated has to keep reaching the app, it needs the credentials — or the app needs to stay public.

Turning it on

Open the app, go to SettingsNetworking & TLS, and flip Password protection on. Set a password and save. That's the whole flow.

  • The app needs a domain first. The toggle only appears once one is attached, because the gate lives on the app's proxy route and a domainless app has no route to gate. Attach a domain on the same tab and the control unlocks in place.
  • Worker apps can't be protected. A worker has no public ingress at all, so there is nothing in front of it to put a password on.
  • You need to be a member or above. Viewers can see the app, but the change is rejected for them at the API, not just hidden in the UI.

It applies live, not on the next deploy

Most app settings wait for a redeploy. This one doesn't. Saving the toggle persists the change and immediately re-points the server's proxy routes with the new gate state. There's no rebuild, no image pull, and no container swap — your running container is never touched, so there's no restart and no dropped connection. Protection is up (or down) as soon as the proxy reloads.

The same goes for turning it off: flip it, save, and the app is public again within seconds.

How the password is stored

The password is hashed with bcrypt once, at save time, in the control plane. Only the hash is stored, and only the hash ever travels onward — into the plan sent to your server, and into the proxy's route configuration, where Caddy verifies against it directly. The plaintext exists only for the duration of the form submission that set it.

  • Nobody can read it back, including you. There is no "show password" and no API that returns it. If you lose it, set a new one.
  • Changing the password while protection is on: type a new one and save. Leave the field blank and save, and the current password is kept — useful when you're only touching something else on the form.
  • Turning protection off clears the stored hash. Switching it back on later is a fresh start: you'll be asked for a new password, because the old one is genuinely gone rather than parked somewhere.
  • The audit log records the change, not the secret. Toggling protection writes an audit entry saying who changed it and whether it's now on or off. The password and its hash are deliberately excluded from that record.

What stays open on purpose

The gate sits on the app-serving leg only. A few things in front of it are left un-authenticated, and each for a reason:

  • The HTTP→HTTPS redirect. A plain-HTTP request is upgraded to HTTPS first, then challenged. Without this you'd be asked for a password over cleartext before the redirect ever happened.
  • The www→apex redirect. Same idea: the browser is sent to the canonical host, and the challenge happens there. You authenticate once, at the URL you actually end up on.
  • Certificate issuance and renewal. ACME runs through the proxy's own automatic HTTPS machinery, not through your app's route. A protected app renews its certificate exactly like a public one. See Networking & TLS.
  • The deploy health gate. The health probe talks to the new container directly over the internal network, never through the proxy. A protected app deploys, health-gates, and cuts over exactly like an unprotected one — the password never causes a deploy to fail its own health check.

Deploys, rollbacks, and reconnects can't drop it

Protection is live app state, not something frozen into a release. Every path that writes your app's proxy route re-reads the current gate at the moment it runs:

  • A deploy carries the gate as it stands right now, so shipping a new version never quietly exposes a protected app.
  • A rollback re-reads it too, rather than restoring whatever was true when that old release was built. Rolling back to a release from before you enabled protection keeps the app protected.
  • An agent reconnect re-emits every route on the server with the gate intact, so a reboot or a network blip can't leave an app briefly public.

PR previews inherit the parent app's gate. When previews are enabled, each preview environment is published behind the same password as the app it was branched from, using the same credentials. It follows the parent live, on the same route push: protect the app, and every open preview of it is protected at the same moment. A preview is a real URL on the public internet — this is what keeps unreleased branches from being one lucky guess away from anyone.

This is a gate, not an authentication system.

One shared credential, no user accounts, no roles, no per-person sign-in record. It answers "keep the internet out of my staging site," and it answers it well. It does not answer "who is logged in and what may they see" — that belongs in your application. Don't use it as the only thing standing between the public and real production data.

Next