Make the Authelia login portal's subdomain configurable, not hardcoded
install_authelia() and add_authelia_domain() both hardcoded "auth." as the login portal's subdomain prefix everywhere -- configuration.yml's authelia_url, the Caddy portal block/domain, generated README/OIDC text. No prompt ever offered anything else, despite this repo otherwise treating "auth.<domain>" as just this one instance's own choice, not a protocol requirement. Both now prompt for the portal subdomain (default "auth", so existing behavior is unchanged for anyone who doesn't care) and use the actual chosen value throughout. Every function that operates on an EXISTING domain (remove_authelia_domain, _authelia_add_oidc_client) now reads the real portal domain back from that domain's own session.cookies authelia_url entry instead of assuming "auth.<domain>" -- matching the same read-back pattern _authelia_add_oidc_client already used for the apex domain itself, and _authelia_provision_oidc_client already used for the portal URL. _authelia_remove_caddy_portal_block now takes the portal's full domain directly rather than reconstructing it, so removing a domain whose portal used a custom prefix actually finds and removes the right Caddy block. Also fixed a real, separate small bug found while in here: the primary portal's Caddy log path was hardcoded to a generic auth.log (collides across instances/domains) instead of following every other site block's own <domain>.log convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
This commit is contained in:
+82
-44
@@ -287,9 +287,17 @@ install_authelia() {
|
||||
echo " Authelia needs a few details to configure."
|
||||
echo ""
|
||||
local CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
local AUTHELIA_DOMAIN AUTHELIA_ADMIN_USER AUTHELIA_ADMIN_DISPLAY AUTHELIA_ADMIN_EMAIL
|
||||
local AUTHELIA_DOMAIN AUTHELIA_PORTAL_SUBDOMAIN AUTHELIA_PORTAL_DOMAIN AUTHELIA_ADMIN_USER AUTHELIA_ADMIN_DISPLAY AUTHELIA_ADMIN_EMAIL
|
||||
local AUTHELIA_SMTP_HOST AUTHELIA_SMTP_PORT AUTHELIA_SMTP_USER AUTHELIA_SMTP_PASS AUTHELIA_TZ
|
||||
prompt_text " Your domain (e.g., example.com):" "${SITE_DOMAIN:-example.com}" AUTHELIA_DOMAIN
|
||||
# Subdomain the login portal itself lives on — "auth" is just the
|
||||
# default, not a fixed convention. Every later function that needs this
|
||||
# domain's portal (add_authelia_domain for a DIFFERENT domain's own
|
||||
# portal, remove_authelia_domain, OIDC client registration, etc.) reads
|
||||
# it back from configuration.yml's session.cookies authelia_url instead
|
||||
# of assuming "auth." — see those functions for why.
|
||||
prompt_text " Subdomain for the login portal (e.g. 'auth' -> auth.${AUTHELIA_DOMAIN}):" "auth" AUTHELIA_PORTAL_SUBDOMAIN
|
||||
AUTHELIA_PORTAL_DOMAIN="${AUTHELIA_PORTAL_SUBDOMAIN}.${AUTHELIA_DOMAIN}"
|
||||
prompt_text " Admin username:" "admin" AUTHELIA_ADMIN_USER
|
||||
prompt_text " Admin display name:" "Administrator" AUTHELIA_ADMIN_DISPLAY
|
||||
prompt_text " Admin email:" "admin@${AUTHELIA_DOMAIN}" AUTHELIA_ADMIN_EMAIL
|
||||
@@ -411,7 +419,7 @@ session:
|
||||
remember_me: 7d
|
||||
cookies:
|
||||
- domain: ${AUTHELIA_DOMAIN}
|
||||
authelia_url: https://auth.${AUTHELIA_DOMAIN}
|
||||
authelia_url: https://${AUTHELIA_PORTAL_DOMAIN}
|
||||
default_redirection_url: https://${AUTHELIA_DOMAIN}
|
||||
|
||||
storage:
|
||||
@@ -482,19 +490,19 @@ SNIPPET_EOF
|
||||
cat "$CADDY_FILE"; } > "$CADDY_FILE.tmp" && mv "$CADDY_FILE.tmp" "$CADDY_FILE"
|
||||
echo " ✓ Authelia snippet added to Caddyfile"
|
||||
fi
|
||||
if ! grep -q "auth.${AUTHELIA_DOMAIN}" "$CADDY_FILE"; then
|
||||
if ! grep -q "${AUTHELIA_PORTAL_DOMAIN}" "$CADDY_FILE"; then
|
||||
cat >> "$CADDY_FILE" << CADDY_AUTH_BLOCK
|
||||
|
||||
# ── Authelia login portal ──────────────────────────────────────────────────────
|
||||
auth.${AUTHELIA_DOMAIN} {
|
||||
${AUTHELIA_PORTAL_DOMAIN} {
|
||||
# header_up pins X-Forwarded-Host to whatever the client actually sent.
|
||||
# Without it, Caddy's reverse_proxy recomputes X-Forwarded-Host from its
|
||||
# own incoming request (always auth.${AUTHELIA_DOMAIN} itself) and
|
||||
# own incoming request (always ${AUTHELIA_PORTAL_DOMAIN} itself) and
|
||||
# overwrites the value a forward_auth caller (e.g. a remote site's
|
||||
# "forward_auth https://auth.${AUTHELIA_DOMAIN}" block, see
|
||||
# "forward_auth https://${AUTHELIA_PORTAL_DOMAIN}" block, see
|
||||
# services/asterisk.sh's droplet-mode Caddy block) set for its own domain. Confirmed
|
||||
# live: every forward-auth check evaluated as if it were for
|
||||
# auth.${AUTHELIA_DOMAIN} itself (which has policy: bypass in
|
||||
# ${AUTHELIA_PORTAL_DOMAIN} itself (which has policy: bypass in
|
||||
# access_control.rules so its own login portal isn't gated behind
|
||||
# itself), so every domain behind it silently passed through with no
|
||||
# 2FA prompt regardless of that domain's own policy.
|
||||
@@ -502,16 +510,16 @@ auth.${AUTHELIA_DOMAIN} {
|
||||
header_up X-Forwarded-Host {http.request.header.X-Forwarded-Host}
|
||||
}
|
||||
log {
|
||||
output file /var/log/caddy/auth.log
|
||||
output file /var/log/caddy/${AUTHELIA_PORTAL_DOMAIN}.log
|
||||
}
|
||||
}
|
||||
CADDY_AUTH_BLOCK
|
||||
echo " ✓ Authelia portal block added for auth.${AUTHELIA_DOMAIN}"
|
||||
echo " ✓ Authelia portal block added for ${AUTHELIA_PORTAL_DOMAIN}"
|
||||
fi
|
||||
docker ps --format '{{.Names}}' | grep -q "^caddy$" && \
|
||||
{ docker exec -w /etc/caddy caddy caddy reload 2>/dev/null && echo " ✓ Caddy reloaded" || echo " ⚠ Reload manually after checking the Caddyfile"; }
|
||||
else
|
||||
echo " ℹ Caddy not installed yet — add the (authelia) snippet + auth.${AUTHELIA_DOMAIN} block to your Caddyfile later (see README)."
|
||||
echo " ℹ Caddy not installed yet — add the (authelia) snippet + ${AUTHELIA_PORTAL_DOMAIN} block to your Caddyfile later (see README)."
|
||||
fi
|
||||
|
||||
# ── README for the service folder ────────────────────────────────────────
|
||||
@@ -519,7 +527,7 @@ CADDY_AUTH_BLOCK
|
||||
# Authelia — SSO + 2FA portal
|
||||
|
||||
Single login (with TOTP two-factor) that protects any Caddy subdomain via
|
||||
forward-auth. Portal: **https://auth.${AUTHELIA_DOMAIN}**
|
||||
forward-auth. Portal: **https://${AUTHELIA_PORTAL_DOMAIN}**
|
||||
|
||||
## Layout
|
||||
\`\`\`
|
||||
@@ -573,10 +581,10 @@ registered app gets its own Client ID/Secret under
|
||||
secret is shown once at registration time and only the hash is kept.
|
||||
|
||||
Endpoints (needed if an app asks for them instead of a discovery URL):
|
||||
- Discovery: \`https://auth.${AUTHELIA_DOMAIN}/.well-known/openid-configuration\`
|
||||
- Authorization: \`https://auth.${AUTHELIA_DOMAIN}/api/oidc/authorization\`
|
||||
- Token: \`https://auth.${AUTHELIA_DOMAIN}/api/oidc/token\`
|
||||
- UserInfo: \`https://auth.${AUTHELIA_DOMAIN}/api/oidc/userinfo\`
|
||||
- Discovery: \`https://${AUTHELIA_PORTAL_DOMAIN}/.well-known/openid-configuration\`
|
||||
- Authorization: \`https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/authorization\`
|
||||
- Token: \`https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/token\`
|
||||
- UserInfo: \`https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/userinfo\`
|
||||
|
||||
## Manage
|
||||
\`\`\`
|
||||
@@ -624,7 +632,7 @@ README_MD
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Auth portal: https://auth.${AUTHELIA_DOMAIN}"
|
||||
echo " Auth portal: https://${AUTHELIA_PORTAL_DOMAIN}"
|
||||
echo " Admin login: ${AUTHELIA_ADMIN_USER} (use Forgot Password to set a real password)"
|
||||
echo " README: $AUTHELIA_DIR/README.md"
|
||||
echo ""
|
||||
@@ -666,6 +674,10 @@ add_authelia_domain() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local NEW_PORTAL_SUBDOMAIN NEW_PORTAL_DOMAIN
|
||||
prompt_text " Subdomain for this domain's own login portal (e.g. 'auth' -> auth.${NEW_DOMAIN}):" "auth" NEW_PORTAL_SUBDOMAIN
|
||||
NEW_PORTAL_DOMAIN="${NEW_PORTAL_SUBDOMAIN}.${NEW_DOMAIN}"
|
||||
|
||||
# ── access_control.rules: insert right after "rules:" ────────────────────
|
||||
awk -v domain="$NEW_DOMAIN" '
|
||||
{ print }
|
||||
@@ -677,11 +689,11 @@ add_authelia_domain() {
|
||||
' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
|
||||
|
||||
# ── session.cookies: insert right after "cookies:" ────────────────────────
|
||||
awk -v domain="$NEW_DOMAIN" '
|
||||
awk -v domain="$NEW_DOMAIN" -v portal="$NEW_PORTAL_DOMAIN" '
|
||||
{ print }
|
||||
/^ cookies:$/ && !done {
|
||||
print " - domain: " domain
|
||||
print " authelia_url: https://auth." domain
|
||||
print " authelia_url: https://" portal
|
||||
print " default_redirection_url: https://" domain
|
||||
done=1
|
||||
}
|
||||
@@ -692,29 +704,29 @@ add_authelia_domain() {
|
||||
|
||||
# ── Caddy portal block for the new domain ─────────────────────────────────
|
||||
if [ -f "$CADDY_FILE" ]; then
|
||||
if ! grep -q "^auth.${NEW_DOMAIN} {" "$CADDY_FILE"; then
|
||||
if ! grep -qx "${NEW_PORTAL_DOMAIN} {" "$CADDY_FILE"; then
|
||||
cat >> "$CADDY_FILE" << CADDY_AUTH_BLOCK2
|
||||
|
||||
# ── Authelia login portal (${NEW_DOMAIN}) ─────────────────────────────────────
|
||||
auth.${NEW_DOMAIN} {
|
||||
# See auth.${AUTHELIA_DOMAIN:-<original domain>}'s block above for why
|
||||
# header_up X-Forwarded-Host is required here, not optional.
|
||||
${NEW_PORTAL_DOMAIN} {
|
||||
# See this instance's other portal block(s) 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
|
||||
output file /var/log/caddy/${NEW_PORTAL_DOMAIN}.log
|
||||
}
|
||||
}
|
||||
CADDY_AUTH_BLOCK2
|
||||
echo " ✓ Authelia portal block added for auth.${NEW_DOMAIN}"
|
||||
echo " ✓ Authelia portal block added for ${NEW_PORTAL_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"
|
||||
echo " ✓ ${NEW_PORTAL_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)."
|
||||
echo " ℹ Caddy not installed — add a ${NEW_PORTAL_DOMAIN} portal block manually later (see README)."
|
||||
fi
|
||||
|
||||
# ── Restart Authelia to pick up the new config ────────────────────────────
|
||||
@@ -727,7 +739,7 @@ CADDY_AUTH_BLOCK2
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Auth portal for $NEW_DOMAIN: https://auth.${NEW_DOMAIN}"
|
||||
echo " Auth portal for $NEW_DOMAIN: https://${NEW_PORTAL_DOMAIN}"
|
||||
echo " Protect a service under this domain the same way as any other:"
|
||||
echo " myservice.${NEW_DOMAIN} {"
|
||||
echo " import authelia"
|
||||
@@ -738,18 +750,21 @@ CADDY_AUTH_BLOCK2
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Removes the auth.<domain> Caddy portal block add_authelia_domain() writes —
|
||||
# same bounded-block technique used elsewhere in this repo for Caddy site
|
||||
# blocks (find the opening "<domain> {" line, walk forward to the matching
|
||||
# unindented "}"), just keyed on "auth.<domain> {" instead of a service
|
||||
# domain or a reverse_proxy marker.
|
||||
# Removes a login-portal Caddy block add_authelia_domain() writes — same
|
||||
# bounded-block technique used elsewhere in this repo for Caddy site blocks
|
||||
# (find the opening "<portal-domain> {" line, walk forward to the matching
|
||||
# unindented "}"). Takes the portal's own FULL domain, not the apex it
|
||||
# belongs to and an assumed "auth." prefix — the portal subdomain is
|
||||
# user-chosen at the time it's added (see add_authelia_domain), so it can't
|
||||
# be reconstructed from the apex alone. Callers read it back from that
|
||||
# domain's own session.cookies authelia_url entry before removing it.
|
||||
_authelia_remove_caddy_portal_block() {
|
||||
local domain="$1"
|
||||
local portal_domain="$1"
|
||||
local caddy_file="$DOCKER_DIR/caddy/Caddyfile"
|
||||
[ -f "$caddy_file" ] || return 0
|
||||
|
||||
local domain_line end_line start_line
|
||||
domain_line="$(grep -nx "auth.${domain} {" "$caddy_file" | head -1 | cut -d: -f1)"
|
||||
domain_line="$(grep -nx "${portal_domain} {" "$caddy_file" | head -1 | cut -d: -f1)"
|
||||
[ -z "$domain_line" ] && return 0
|
||||
|
||||
start_line="$domain_line"
|
||||
@@ -759,13 +774,13 @@ _authelia_remove_caddy_portal_block() {
|
||||
|
||||
end_line="$(tail -n "+$domain_line" "$caddy_file" | grep -nx '}' | head -1 | cut -d: -f1)"
|
||||
if [ -z "$end_line" ]; then
|
||||
log_warning "Could not find the end of auth.${domain}'s Caddy block — leaving it as-is."
|
||||
log_warning "Could not find the end of ${portal_domain}'s Caddy block — leaving it as-is."
|
||||
return 1
|
||||
fi
|
||||
end_line=$((domain_line + end_line - 1))
|
||||
|
||||
sed -i "${start_line},${end_line}d" "$caddy_file"
|
||||
log_info "Removed the auth.${domain} Caddy portal block."
|
||||
log_info "Removed the ${portal_domain} Caddy portal block."
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$" && \
|
||||
{ docker exec -w /etc/caddy caddy caddy reload 2>/dev/null && log_success "Caddy reloaded" \
|
||||
|| log_warning "Reload manually: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"; }
|
||||
@@ -814,9 +829,21 @@ remove_authelia_domain() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Read the portal's own domain back from this apex's session.cookies
|
||||
# entry — it's whatever subdomain was chosen when this domain was added
|
||||
# (see add_authelia_domain), not necessarily "auth.<domain>", so it
|
||||
# can't be assumed. Must happen before the removal below, which deletes
|
||||
# this exact entry.
|
||||
local RM_PORTAL_DOMAIN
|
||||
RM_PORTAL_DOMAIN="$(awk -v domain="$RM_DOMAIN" '
|
||||
$0 == " - domain: " domain { f=1; next }
|
||||
f && /authelia_url:/ { print $2; exit }
|
||||
' "$CONFIG_FILE" | sed -E 's#^https?://##')"
|
||||
[ -z "$RM_PORTAL_DOMAIN" ] && RM_PORTAL_DOMAIN="auth.${RM_DOMAIN}"
|
||||
|
||||
echo ""
|
||||
log_warning "This removes ${RM_DOMAIN}'s access rule, session-cookie scope, and its"
|
||||
log_warning "auth.${RM_DOMAIN} login portal from THIS Authelia instance."
|
||||
log_warning "${RM_PORTAL_DOMAIN} login portal from THIS Authelia instance."
|
||||
log_warning "Any service still using 'import authelia' or forward_auth pointed at"
|
||||
log_warning "${RM_DOMAIN} will start failing to authenticate — reconfigure or remove"
|
||||
log_warning "those first if they're still live."
|
||||
@@ -843,7 +870,7 @@ remove_authelia_domain() {
|
||||
chown 1000:1000 "$CONFIG_FILE" 2>/dev/null || true
|
||||
log_success "Removed ${RM_DOMAIN} from $CONFIG_FILE"
|
||||
|
||||
_authelia_remove_caddy_portal_block "$RM_DOMAIN"
|
||||
_authelia_remove_caddy_portal_block "$RM_PORTAL_DOMAIN"
|
||||
|
||||
local RESTART_AUTH=""
|
||||
prompt_yn " Restart Authelia to apply? (y/n):" "y" RESTART_AUTH
|
||||
@@ -1992,6 +2019,17 @@ _authelia_add_oidc_client() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# This domain's own portal — whatever subdomain was actually chosen at
|
||||
# install time (see install_authelia's own AUTHELIA_PORTAL_SUBDOMAIN
|
||||
# prompt), not necessarily "auth.<domain>". Read back the same way
|
||||
# AUTHELIA_DOMAIN itself is, from this entry's own authelia_url.
|
||||
local AUTHELIA_PORTAL_DOMAIN
|
||||
AUTHELIA_PORTAL_DOMAIN="$(tr -d '\r' < "$CONFIG_FILE" | awk -v domain="$AUTHELIA_DOMAIN" '
|
||||
$0 == " - domain: " domain { f=1; next }
|
||||
f && /authelia_url:/ { print $2; exit }
|
||||
' | sed -E 's#^https?://##')"
|
||||
[ -z "$AUTHELIA_PORTAL_DOMAIN" ] && AUTHELIA_PORTAL_DOMAIN="auth.${AUTHELIA_DOMAIN}"
|
||||
|
||||
echo ""
|
||||
echo " Register another app to log in via Authelia (OIDC/SSO)."
|
||||
echo ""
|
||||
@@ -2074,12 +2112,12 @@ _authelia_add_oidc_client() {
|
||||
echo ""
|
||||
echo " Client ID: ${CLIENT_ID}"
|
||||
echo " Client Secret: ${CLIENT_SECRET_PLAIN}"
|
||||
echo " Discovery URL: https://auth.${AUTHELIA_DOMAIN}/.well-known/openid-configuration"
|
||||
echo " Discovery URL: https://${AUTHELIA_PORTAL_DOMAIN}/.well-known/openid-configuration"
|
||||
echo ""
|
||||
echo " If it asks for individual endpoints instead of a discovery URL:"
|
||||
echo " Authorization: https://auth.${AUTHELIA_DOMAIN}/api/oidc/authorization"
|
||||
echo " Token: https://auth.${AUTHELIA_DOMAIN}/api/oidc/token"
|
||||
echo " UserInfo: https://auth.${AUTHELIA_DOMAIN}/api/oidc/userinfo"
|
||||
echo " Authorization: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/authorization"
|
||||
echo " Token: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/token"
|
||||
echo " UserInfo: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/userinfo"
|
||||
echo " Scopes: openid profile email"
|
||||
echo ""
|
||||
case "$APP_CHOICE" in
|
||||
@@ -2092,7 +2130,7 @@ _authelia_add_oidc_client() {
|
||||
2)
|
||||
echo " Add these to Vaultwarden's .env, then: cd \$VAULTWARDEN_DIR && docker compose up -d"
|
||||
echo " SSO_ENABLED=true"
|
||||
echo " SSO_AUTHORITY=https://auth.${AUTHELIA_DOMAIN}"
|
||||
echo " SSO_AUTHORITY=https://${AUTHELIA_PORTAL_DOMAIN}"
|
||||
echo " SSO_CLIENT_ID=${CLIENT_ID}"
|
||||
echo " SSO_CLIENT_SECRET=${CLIENT_SECRET_PLAIN}"
|
||||
echo " SSO_SCOPES=profile email"
|
||||
@@ -2103,7 +2141,7 @@ _authelia_add_oidc_client() {
|
||||
;;
|
||||
3)
|
||||
echo " Immich → Administration → Settings → OAuth Authentication:"
|
||||
echo " Issuer URL: https://auth.${AUTHELIA_DOMAIN}"
|
||||
echo " Issuer URL: https://${AUTHELIA_PORTAL_DOMAIN}"
|
||||
echo " (Immich appends /.well-known/openid-configuration itself — paste"
|
||||
echo " just the base URL above, not the full Discovery URL from earlier.)"
|
||||
echo " Client ID: ${CLIENT_ID}"
|
||||
|
||||
Reference in New Issue
Block a user