Imported from ak64th/authelia-caddy-demo (
AGENTS.md). Install upstream withnpx skills add ak64th/authelia-caddy-demo. Copyright stays with the author.
Project Agent Guide
Scope: Root project (applies to all subdirectories unless overridden)
Quick Facts
- Primary language: JavaScript (Node.js built-in
httpmodule) - Orchestration: Docker Compose
- Services: Authelia (auth), Caddy (reverse proxy / TLS), Node service (protected demo app)
- Network model: Internal
webbridge; only Caddy exposes ports80and443 - Auth flow: Browser session cookie (
authelia_session) via Autheliaforward_auth; no JWTs
Repository Tour
.
├── authelia/ # Authelia config, user DB, SQLite storage
│ ├── configuration.yml # Session, cookie, auth backend config
│ └── users_database.yml # Local file-based users (demo / admin)
├── caddy_data/ # Caddy TLS/data state (created at runtime)
├── node-app/
│ └── server.js # Protected Node demo server
├── public/ # Static files served by Caddy
│ ├── index.html # Public landing page
│ └── protected/
│ └── index.html # Protected static landing page
├── Caddyfile # Routing, forward_auth, header copying
├── compose.yml # Service wiring and internal network
└── AGENTS.md # This file
compose.yml— Definesauthelia,caddy, andnode-serviceon the internalwebnetwork.Caddyfile— Public/protected routing andforward_auth.handle_path /protected/api/*strips the prefix before proxying to Node.authelia/configuration.yml— Session cookie and auth backend config. UsesDEMO_IPenv variable.node-app/server.js— Minimal Node HTTP server readingRemote-*headers. Zero dependencies; nonpm installneeded.public/— Static content served by Caddy (root * /srv).
Tooling & Setup
- Required: Docker + Docker Compose
- Environment variable:
DEMO_IP(defaults to192.168.10.208)- Must match the current LAN IP in
compose.yml,Caddyfile, andauthelia/configuration.ymlsession cookie settings.
- Must match the current LAN IP in
- No package manager for the Node service; it uses the built-in
httpmodule only. - Start the stack:
docker compose up -d - Restart after config changes:
docker compose restart caddydocker compose restart autheliadocker compose restart node-service
Common Tasks
docker compose up -d— Start all servicesdocker compose ps— Check service healthdocker compose logs -f <service>— Tail logs (caddy,authelia,node-service)docker compose restart <service>— Reload config changesdocker compose exec node-service sh -lc 'wget -qO- http://127.0.0.1:3000/json'— Test Node directly (no auth headers expected)docker compose exec authelia sh -lc 'chown -R 1000:1000 /config && chmod -R u+rwX /config'— Fix bind-mount ownership if files become root-owned
Testing & Quality Gates
- No automated test suite exists for this demo.
- Manual verification with
curl:- Unauthenticated probe (expect
302to/authelia/):curl -k -sS -D - https://<DEMO_IP>/protected/api/json -o /tmp/out
- Authenticated probe (expect
200+ JSON):curl -k -sS -c /tmp/demo.cookies -b /tmp/demo.cookies https://<DEMO_IP>/protected/api/json
- WebSocket probe (expect
101 Switching Protocols):- Requires HTTP/1.1 and
Upgrade: websocketheaders. See the Debugging section below for the full command.
- Requires HTTP/1.1 and
- Unauthenticated probe (expect
- Browser automation with
agent-browser:- Use
--session <unique-id>for each run to avoid state collisions with existing sessions. - Use
--ignore-https-errorsbecause the demo uses raw-IP self-signed certificates. - Unset
http_proxy/https_proxyif your environment sets them, or the browser may route through a proxy and fail. - Example workflow:
SESSION_ID="test-$(date +%s)" http_proxy= https_proxy= agent-browser --session "$SESSION_ID" --ignore-https-errors \ open "https://192.168.10.208/authelia/?rd=https%3A%2F%2F192.168.10.208%2Fprotected%2F&rm=GET" http_proxy= https_proxy= agent-browser --session "$SESSION_ID" wait --load networkidle http_proxy= https_proxy= agent-browser --session "$SESSION_ID" snapshot -i # Note the refs, then fill and submit: http_proxy= https_proxy= agent-browser --session "$SESSION_ID" fill @e11 "demo" http_proxy= https_proxy= agent-browser --session "$SESSION_ID" fill @e12 "demo-password" http_proxy= https_proxy= agent-browser --session "$SESSION_ID" click @e8 http_proxy= https_proxy= agent-browser --session "$SESSION_ID" wait --load networkidle http_proxy= https_proxy= agent-browser --session "$SESSION_ID" snapshot -i - After testing, close the session:
http_proxy= https_proxy= agent-browser --session "$SESSION_ID" close
- Use
Workflow Expectations
- This is a demo / sandbox repo. There is no formal CI/CD or branch policy.
- Keep
node-serviceinternal-only; do not publish port3000on the host. Remote-*headers are unsigned and must only be trusted because requests arrive from Caddy on the internal network.- If a real production app is added later, prefer OIDC/bearer tokens over trusted headers for stronger delegation.
Documentation Duties
- There is currently no
README.md. If one is created, update it when routes, setup steps, or auth behavior change. - Update
AGENTS.mdwhen:- New protected endpoints or services are added
- The auth model or header contract changes
- New debugging commands become relevant
- If a
plans/ordocs/directory is created later, link to it here and treat it as the canonical source for architectural decisions.
Finish the Task Checklist
- Update relevant docs (
AGENTS.mdand any newREADME.mdif significant changes landed) - Summarize changes in conventional commit format (e.g.,
feat: ...,fix: ...)
Route Map
/-> public static content frompublic//authelia/-> Authelia portal proxied by Caddy/protected/-> static protected page after Autheliaforward_auth/protected/api/-> protected Node HTML page showing forwarded identity headers/protected/api/json-> protected Node JSON endpoint showing forwarded identity headers/protected/api/ws-> protected WebSocket stream sending timestamp + identity every second
Core Auth Model
- This project uses Authelia's browser session flow, not JWTs.
- Authelia sets
authelia_session; the browser sends it automatically on later requests. - Caddy calls Authelia
forward_authfor/protected/*. - On success, Caddy copies
Remote-User,Remote-Groups,Remote-Email, andRemote-Nameinto the upstream request. - Backends must trust these headers only because the request came from Caddy on the internal network.
Trusted Header Rules
Remote-*headers are unsigned. Never trust them from public clients.- Backend services must not be directly exposed.
- If a real app is added later, trust
Remote-*only from the reverse proxy path. - For stronger delegation, prefer OIDC/bearer tokens instead of trusted headers.
Caddy Notes
default_sni {$DEMO_IP}is required for raw-IP HTTPS to work reliably when clients do not send SNI.handle_path /protected/api/*strips/protected/apibefore proxying to Node./protected/api/reaches Node as//protected/api/jsonreaches Node as/json/protected/api/wsreaches Node as/ws
- Keep
forward_authbeforefile_server/reverse_proxyin protected routes. - Caddy's
reverse_proxyautomatically handles WebSocket upgrades; no extra config is needed for/protected/api/ws.
Raw-IP Caveat
DEMO_IPmust match the current LAN IP in:compose.ymlCaddyfileauthelia/configuration.ymlsession cookie settings
- Authelia raw-IP mode is best-effort. If session/cookie behavior becomes brittle, move to a hostname-based setup.
Node Demo Behavior
node-app/server.jsuses the built-in Nodehttpmodule; no framework or package install is needed.- Endpoints:
/-> HTML page showing forwarded identity headers (includes a WebSocket UI)/json-> JSON payload with identity headers andx-forwarded-for/healthz-> simple health response/ws-> WebSocket upgrade; sends{ timestamp, identity }JSON every 1 second
Operational Notes
- Some bind-mounted files may become
root-owned after container writes. - If Authelia config files become hard to edit, ownership can be restored from inside the container, e.g.:
docker compose exec authelia sh -lc 'chown -R 1000:1000 /config && chmod -R u+rwX /config'
- After changing
Caddyfile, restart Caddy:docker compose restart caddy - After changing Authelia config, restart Authelia:
docker compose restart authelia
Quick Verification
- Unauthenticated:
/protected/->302to/authelia//protected/api/json->302to/authelia//protected/api/ws->302to/authelia/
- Authenticated:
/protected/-> static protected page/protected/api/-> Node HTML header demo/protected/api/json-> JSON withremoteUser,remoteGroups,remoteEmail,remoteName/protected/api/ws->101 Switching Protocols, then JSON frames every 1 second
Debugging with curl and Cookies
- Use a cookie jar file to simulate the browser session flow.
- Unauthenticated probe:
curl -k -sS -D - https://192.168.10.208/protected/api/json -o /tmp/out- Expect
302redirect to/authelia/
- Establish a session and reuse it:
curl -k -sS -c /tmp/demo.cookies -b /tmp/demo.cookies https://192.168.10.208/protected/api/json -o /tmp/prelogin.outcurl -k -sS -c /tmp/demo.cookies -b /tmp/demo.cookies -H 'Content-Type: application/json' -d '{"username":"demo","password":"demo-password","keepMeLoggedIn":false,"targetURL":"https://192.168.10.208/protected/api/json"}' https://192.168.10.208/authelia/api/firstfactorcurl -k -sS -c /tmp/demo.cookies -b /tmp/demo.cookies https://192.168.10.208/protected/api/json
- Expected authenticated JSON shape:
identity.remoteUseridentity.remoteGroupsidentity.remoteEmailidentity.remoteNameforwardedFor
- To inspect the protected static page with the same session:
curl -k -sS -c /tmp/demo.cookies -b /tmp/demo.cookies https://192.168.10.208/protected/
- To test the WebSocket handshake with curl:
KEY=$(openssl rand -base64 16) && \ curl -k -sS --http1.1 -c /tmp/demo.cookies -b /tmp/demo.cookies \ -N -D - \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: $KEY" \ -H "Sec-WebSocket-Version: 13" \ "https://192.168.10.208/protected/api/ws" - To test the Node service itself without Caddy/Auth, exec into the container:
docker compose exec node-service sh -lc 'wget -qO- http://127.0.0.1:3000/json'- This should return JSON with empty
identityfields because noRemote-*headers are injected on direct access.