# preauth example Caddyfile # --- anti-caching guard for the login flow --- # The login page, failed logins, redirects, and rate-limit pages must never # be stored or replayed by a browser or intermediate cache. If they are, # an aggressive cache (notably older Safari) can resurrect a stale pre-auth # response — appearing to log a user back out after a refresh. preauth # sends these headers itself; mirroring them here with `header_down` keeps # the guarantee at the edge. Import this snippet inside every `forward_auth` # block: # # forward_auth preauth { ...; import preauth_no_store } # # Note: 2xx auth responses are consumed by Caddy's forward_auth check and # never reach the browser, and the protected service's own responses are # not affected — so the cache headers of your services are left alone. (preauth_no_store) { header_down Cache-Control "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0" header_down Pragma "no-cache" header_down Expires "0" header_down Surrogate-Control "no-store" header_down Vary "*" } # example of securing full service # TODO replace domain and service name and port service.example.com { forward_auth preauth { uri {uri} copy_headers Remote-User import preauth_no_store } reverse_proxy service-container:80 } # you can choose to only restrict select paths # or any other Caddy match criteria, if desired # IE: https://protected.example.com/secure/ protected.example.com { # note any request that does not start with "/secure/" is NOT protected forward_auth /secure/* preauth { uri {uri} copy_headers Remote-User import preauth_no_store } reverse_proxy protected-service:9000 } # optionally, if you want to use a subdomain for central preauth # set SUBDOMAIN_REDIRECT to true # and AUTH_SUBDOMAIN to match the subdomain you use here # # Passkeys (PASSKEY_ENABLED) require this block AND HTTPS: the ceremony runs # here and the credential is scoped to the base domain. Caddy provisions a # certificate automatically for a real hostname, so nothing extra is needed in # production. This block is also deliberately NOT behind forward_auth — the # browser talks to it directly during a ceremony. auth.example.com { reverse_proxy preauth } # --- local development with passkeys --- # Browsers only allow a WebAuthn ceremony over HTTPS, and preauth does not offer # an exemption for http://localhost (that would be a way to run passkeys # insecurely in production). So to exercise passkeys locally, give yourself a # real hostname and a locally-trusted certificate: # # 1. Point the names at your machine: # # /etc/hosts # 127.0.0.1 auth.preauthtest.local app.preauthtest.local # 2. Trust a certificate for them (mkcert installs a local CA): # mkcert auth.preauthtest.local app.preauthtest.local # # 3. In preauth's .env: # SUBDOMAIN_REDIRECT=true # AUTH_SUBDOMAIN=auth.preauthtest.local # PASSKEY_ENABLED=true # # 4. Terminate TLS here and proxy to the container: # # auth.preauthtest.local, "*.preauthtest.local" { # tls /path/to/auth.preauthtest.local+1.pem /path/to/auth.preauthtest.local+1-key.pem # reverse_proxy preauth # } # # Note "localhost" itself cannot be used: it has no base domain, so central # auth cannot be configured and passkeys stay disabled. # --- public rate-limited access (v1.1) --- # Configure PUBLIC_PATHS env var to specify which paths are public. # Example: PUBLIC_PATHS=/public/** # Unauthenticated visitors to public paths are rate-limited separately # from login attempts. Authenticated users bypass the public rate limiter. # # This example protects all of Gitea except /public/** which is # publicly accessible but rate-limited (e.g., 100 req/min, 500 req/hr). git.example.com { forward_auth preauth { uri {uri} copy_headers Remote-User import preauth_no_store } reverse_proxy gitea:3000 } # In preauth's .env: # PUBLIC_PATHS=/public/** # PUBLIC_BURST_COUNT=100 # PUBLIC_BURST_TIME=60 # PUBLIC_UPPER_COUNT=500 # PUBLIC_UPPER_TIME=3600