From 6a2fedf82a659f28a4bc4bddc2a801f8d782c86c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 02:43:09 +0000 Subject: [PATCH 1/2] Document running multiple independent Authelia instances Confirms services/authelia.sh's standalone pattern and asterisk-digital-ocean.sh's local-vs-remote auto-detection already support a second, fully independent instance on another machine with no code changes needed. Documents the one real constraint: two instances must not share the same AUTHELIA_DOMAIN, since the session cookie scope and the auth. portal hostname would collide. --- CLAUDE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5869174..9a82cfd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -281,6 +281,24 @@ self-documenting on the deployed box. Some services have their own login screens; others have none and need Caddy to gate them via Authelia. +**Running more than one Authelia instance (e.g. one per machine).** `services/authelia.sh` +runs standalone on any box (`sudo bash authelia.sh`, same pattern as `crowdsec.sh`) and +`asterisk-digital-ocean.sh` already auto-detects a local install (`if [ -d +"$DOCKER_DIR/authelia" ]`), switching from the remote-Authelia `forward_auth` flow to the +local `import authelia` snippet automatically — so a second, fully independent instance on +another machine (e.g. a droplet, for resilience if the first machine goes down) works with +no code changes. + +The one real constraint: Authelia's session cookie is scoped to `AUTHELIA_DOMAIN` (the apex +domain entered at install time) with `includeSubDomains`-style matching, and the portal +itself lives at `auth.${AUTHELIA_DOMAIN}`. **Two independent instances must not share the +same `AUTHELIA_DOMAIN`.** If they did, both would try to claim the same `auth.` +hostname (DNS can only point that at one machine) and the same cookie scope with completely +separate session stores — users bouncing between subdomains fronted by different instances +would see confusing repeated logins as each instance's cookie gets overwritten/rejected by +the other's. Give each instance either a genuinely separate apex domain, or a distinct +subdomain tree the other instance doesn't also claim. + **Has built-in auth — no Authelia needed:** `emby`, `jellyfin`, `audiobookshelf`, `immich`, `mealie`, `actualbudget`, `homeassistant`, `portainer`, `meshcentral`, `traccar`, `uptimekuma`, From f7db82333a806f0467a5c22a5e5981ecbababde1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 03:03:33 +0000 Subject: [PATCH 2/2] Add authelia.sh support for protecting multiple apex domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Add another protected domain to this instance" option on re-run, via add_authelia_domain(): appends a session.cookies entry and an access_control.rules entry (both YAML lists Authelia natively supports) plus a Caddy auth. portal block for the new domain, all on the same Authelia + Redis container instead of standing up a second full stack. Each domain gets its own login/session, sharing one user database — the right fit when a single (possibly upsized) droplet ends up fronting more than one domain, without doubling the RAM cost of a second Authelia+Redis instance. Documents both this and the already-working separate-instance path in CLAUDE.md, with the per-approach tradeoffs. --- CLAUDE.md | 36 +++++++---- services/authelia.sh | 148 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9a82cfd..ee4a85c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -281,23 +281,37 @@ self-documenting on the deployed box. Some services have their own login screens; others have none and need Caddy to gate them via Authelia. -**Running more than one Authelia instance (e.g. one per machine).** `services/authelia.sh` +**Protecting more than one apex domain from the same box — same instance, not a second +one.** `services/authelia.sh`, re-run against an existing install, offers "Add another +protected domain to this instance": it appends a new `access_control.rules` entry and a new +`session.cookies` entry (both are YAML lists — Authelia natively supports multiple +independent cookie scopes) plus a Caddy `auth.` portal block for the new domain, all +on the **same** Authelia + Redis container. Each domain gets its own login/session (no +cross-domain SSO between them) and shares one user database, without the RAM cost of a +second full Authelia+Redis stack — the right choice whenever the domains are going to live +on the same machine anyway. See `add_authelia_domain()` in `services/authelia.sh`. + +**Running a genuinely separate instance (e.g. one per machine).** `services/authelia.sh` runs standalone on any box (`sudo bash authelia.sh`, same pattern as `crowdsec.sh`) and `asterisk-digital-ocean.sh` already auto-detects a local install (`if [ -d "$DOCKER_DIR/authelia" ]`), switching from the remote-Authelia `forward_auth` flow to the local `import authelia` snippet automatically — so a second, fully independent instance on another machine (e.g. a droplet, for resilience if the first machine goes down) works with -no code changes. +no code changes. Use this instead of the same-instance approach above when the two domains +are on different machines, not just different domains on one machine. -The one real constraint: Authelia's session cookie is scoped to `AUTHELIA_DOMAIN` (the apex -domain entered at install time) with `includeSubDomains`-style matching, and the portal -itself lives at `auth.${AUTHELIA_DOMAIN}`. **Two independent instances must not share the -same `AUTHELIA_DOMAIN`.** If they did, both would try to claim the same `auth.` -hostname (DNS can only point that at one machine) and the same cookie scope with completely -separate session stores — users bouncing between subdomains fronted by different instances -would see confusing repeated logins as each instance's cookie gets overwritten/rejected by -the other's. Give each instance either a genuinely separate apex domain, or a distinct -subdomain tree the other instance doesn't also claim. +The one real constraint for genuinely separate instances: Authelia's session cookie is +scoped to `AUTHELIA_DOMAIN` (the apex domain entered at install time) with +`includeSubDomains`-style matching, and the portal itself lives at `auth.${AUTHELIA_DOMAIN}`. +**Two independent instances must not share the same `AUTHELIA_DOMAIN`.** If they did, both +would try to claim the same `auth.` hostname (DNS can only point that at one +machine) and the same cookie scope with completely separate session stores — users bouncing +between subdomains fronted by different instances would see confusing repeated logins as +each instance's cookie gets overwritten/rejected by the other's. Give each instance either a +genuinely separate apex domain, or a distinct subdomain tree the other instance doesn't also +claim. (This constraint doesn't apply to the same-instance, multiple-domains approach above — +each domain there gets its own cookie entry by design, which is exactly what avoids the +collision.) **Has built-in auth — no Authelia needed:** `emby`, `jellyfin`, `audiobookshelf`, `immich`, `mealie`, `actualbudget`, diff --git a/services/authelia.sh b/services/authelia.sh index 974668b..613085f 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -206,13 +206,29 @@ install_authelia() { # Don't clobber an existing install (it would regenerate secrets and break sessions). if [ -f "$AUTHELIA_DIR/docker-compose.yml" ]; then - local RECONF="" - echo " ⚠ Authelia already exists at $AUTHELIA_DIR (secrets/users would be regenerated)." - prompt_yn " Reconfigure from scratch? (y/n):" "n" RECONF - if [ "$RECONF" != "y" ] && [ "$RECONF" != "Y" ]; then - echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)" - return 0 - fi + echo " ⚠ Authelia already exists at $AUTHELIA_DIR." + echo "" + echo " 1) Add another protected domain to this instance (non-destructive —" + echo " one Authelia+Redis, multiple independent apex domains/logins)" + echo " 2) Reconfigure from scratch (regenerates secrets/users — breaks" + echo " existing sessions for every domain already on this instance)" + echo " 3) Leave as-is" + echo "" + local EXISTING_CHOICE="" + prompt_text " Choice [1/2/3]:" "3" EXISTING_CHOICE + case "$EXISTING_CHOICE" in + 1) + add_authelia_domain + return 0 + ;; + 2) + : # fall through to the full reinstall flow below + ;; + *) + echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)" + return 0 + ;; + esac fi log_info "Installing Authelia..." @@ -471,6 +487,16 @@ myservice.${AUTHELIA_DOMAIN} { The \`(authelia)\` snippet and the \`auth.${AUTHELIA_DOMAIN}\` portal block were added to \`$DOCKER_DIR/caddy/Caddyfile\` automatically. +## Protecting a second (or third) apex domain +Re-run this installer (\`sudo ./setup.sh authelia\` or \`sudo bash authelia.sh\`) +and choose **"Add another protected domain to this instance"** when it detects +the existing install. That domain gets its own \`session.cookies\` entry and its +own \`auth.\` portal — a separate login/session from ${AUTHELIA_DOMAIN}, +so no accidental cross-domain SSO — but it's still one shared Authelia + Redis +container and one shared user database, not a second full stack. Cheaper than +standing up an entirely separate instance, and the right way to protect +multiple unrelated domains from the same box. + ## Manage \`\`\` cd $AUTHELIA_DIR @@ -510,5 +536,113 @@ README_MD echo "" } +# Adds a second (or third, etc.) independent apex domain to an EXISTING Authelia +# instance instead of standing up a whole separate Authelia+Redis stack for it. +# Authelia natively supports this: session.cookies and access_control.rules are +# both lists, so one instance can hold a distinct cookie scope + login portal per +# domain, each with its own session (no cross-domain SSO, but also no collision — +# see the "Running more than one Authelia instance" note in CLAUDE.md for why two +# domains can't just share one session.cookies entry). Far cheaper on RAM than a +# second full instance, which matters most on a small droplet. +add_authelia_domain() { + local AUTHELIA_DIR="$DOCKER_DIR/authelia" + local CONFIG_FILE="$AUTHELIA_DIR/config/configuration.yml" + local CADDY_FILE="$DOCKER_DIR/caddy/Caddyfile" + + if [ ! -f "$CONFIG_FILE" ]; then + log_warning "No configuration.yml found at $CONFIG_FILE — install Authelia first." + return 1 + fi + + echo "" + echo " Add another apex domain to this Authelia instance." + echo " It gets its own session-cookie scope and its own auth. portal —" + echo " a separate login/session from your other domain(s) — but shares this" + echo " same Authelia + Redis container, not a second full stack." + echo "" + local NEW_DOMAIN="" + prompt_text " New domain (e.g., example.com):" "" NEW_DOMAIN + if [ -z "$NEW_DOMAIN" ]; then + log_warning "No domain entered — nothing to do." + return 0 + fi + + if grep -qF "\"*.${NEW_DOMAIN}\"" "$CONFIG_FILE" 2>/dev/null; then + log_warning "$NEW_DOMAIN is already configured in $CONFIG_FILE — nothing to do." + return 0 + fi + + # ── access_control.rules: insert right after "rules:" ──────────────────── + awk -v domain="$NEW_DOMAIN" ' + { print } + /^ rules:$/ && !done { + print " - domain: \"*." domain "\"" + print " policy: two_factor" + done=1 + } + ' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" + + # ── session.cookies: insert right after "cookies:" ──────────────────────── + awk -v domain="$NEW_DOMAIN" ' + { print } + /^ cookies:$/ && !done { + print " - domain: " domain + print " authelia_url: https://auth." domain + print " default_redirection_url: https://" domain + done=1 + } + ' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" + + chown 1000:1000 "$CONFIG_FILE" 2>/dev/null || true + log_success "Added $NEW_DOMAIN to $CONFIG_FILE (access_control rule + session cookie scope)" + + # ── Caddy portal block for the new domain ───────────────────────────────── + if [ -f "$CADDY_FILE" ]; then + if ! grep -q "^auth.${NEW_DOMAIN} {" "$CADDY_FILE"; then + cat >> "$CADDY_FILE" << CADDY_AUTH_BLOCK2 + +# ── Authelia login portal (${NEW_DOMAIN}) ───────────────────────────────────── +auth.${NEW_DOMAIN} { + # See auth.${AUTHELIA_DOMAIN:-}'s block above for why + # header_up X-Forwarded-Host is required here, not optional. + reverse_proxy authelia:9091 { + header_up X-Forwarded-Host {http.request.header.X-Forwarded-Host} + } + log { + output file /var/log/caddy/auth.${NEW_DOMAIN}.log + } +} +CADDY_AUTH_BLOCK2 + echo " ✓ Authelia portal block added for auth.${NEW_DOMAIN}" + docker ps --format '{{.Names}}' | grep -q "^caddy$" && \ + { docker exec -w /etc/caddy caddy caddy reload 2>/dev/null && echo " ✓ Caddy reloaded" || echo " ⚠ Reload manually: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"; } + else + echo " ✓ auth.${NEW_DOMAIN} portal block already exists in the Caddyfile" + fi + else + echo " ℹ Caddy not installed — add an auth.${NEW_DOMAIN} portal block manually later (see README)." + fi + + # ── Restart Authelia to pick up the new config ──────────────────────────── + local RESTART_AUTH="" + prompt_yn " Restart Authelia to apply the new domain? (y/n):" "y" RESTART_AUTH + if [ "$RESTART_AUTH" = "y" ] || [ "$RESTART_AUTH" = "Y" ]; then + (cd "$AUTHELIA_DIR" && docker compose restart authelia 2>/dev/null) \ + && log_success "Authelia restarted" \ + || log_warning "Restart failed — check: docker compose logs authelia" + fi + + echo "" + echo " Auth portal for $NEW_DOMAIN: https://auth.${NEW_DOMAIN}" + echo " Protect a service under this domain the same way as any other:" + echo " myservice.${NEW_DOMAIN} {" + echo " import authelia" + echo " reverse_proxy localhost:PORT" + echo " }" + echo " Same users/passwords work across every domain on this instance —" + echo " it's one shared user database, just separate sessions per domain." + echo "" +} + # Run immediately when executed directly (deferred until after function definition) [[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_authelia