Add a generic "protect a site with Authelia" menu action

Every individual service so far offers its own "Protect X with
Authelia SSO?" prompt on install, but there was no way to gate an
arbitrary existing site from Authelia's own menu -- especially useful
for a site on a DIFFERENT box's Caddy than the one Authelia runs on,
this repo's own recurring case (a DigitalOcean droplet's site,
protected by an Authelia instance on a separate IONOS box).

New option 9, _authelia_protect_site(): lists this box's own local
Caddy sites by number (flagging ones already protected), or accepts a
typed domain that isn't on this box's Caddy at all. A local site gets
"import authelia" inserted as the first line of its existing block --
before reverse_proxy, same ordering rule as everywhere else in this
codebase, since Caddy runs directives in the order written and an auth
check after reverse_proxy never runs at all. A remote site can't be
edited from here, so it prints (and saves to caddy-snippets/) the
remote-hop-safe forward_auth block that box's own Caddyfile needs
instead, with the portal's actual domain read back from
configuration.yml rather than assumed. Either way finishes by calling
_authelia_scope_access for the domain, so protecting a site and
restricting who can reach it happen in one pass.

Verified the site-listing regex, insertion, idempotency detection, and
remote-domain/portal lookup against synthetic Caddyfile/configuration.yml
fixtures before shipping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
This commit is contained in:
Claude
2026-08-22 20:55:14 +00:00
parent ae77992869
commit 098bb833cf
+151 -2
View File
@@ -236,10 +236,13 @@ install_authelia() {
echo " existing sessions for every domain already on this instance)"
echo " 7) Show who has universal vs. service-scoped access"
echo " 8) Change \"Remember me\" session duration (stay logged in longer)"
echo " 9) Leave as-is"
echo " 9) Protect an existing site with this instance (pick a local Caddy site,"
echo " or type one on a different box — gates it with a login, same as any"
echo " other service already protected this way)"
echo " 10) Leave as-is"
echo ""
local EXISTING_CHOICE=""
prompt_text " Choice [1/2/3/4/5/6/7/8/9]:" "9" EXISTING_CHOICE
prompt_text " Choice [1/2/3/4/5/6/7/8/9/10]:" "10" EXISTING_CHOICE
case "$EXISTING_CHOICE" in
1)
add_authelia_domain
@@ -272,6 +275,10 @@ install_authelia() {
_authelia_set_remember_me
return 0
;;
9)
_authelia_protect_site
return 0
;;
*)
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
return 0
@@ -881,6 +888,148 @@ remove_authelia_domain() {
fi
}
# Generalizes the "Protect X with Authelia SSO?" prompt individual services
# (magicmirror, wolf-pair, security-dashboard, etc.) each offer on their own
# install into one menu action here: pick any existing LOCAL Caddy site by
# number, or type a domain that's on a DIFFERENT box's Caddy entirely (this
# box only runs Authelia, not that site) — e.g. this repo's own case of a
# DigitalOcean droplet's site protected by an Authelia instance on a
# separate IONOS box.
#
# Local site: inserts "import authelia" as the very first line inside its
# existing block — must come before reverse_proxy, since Caddy runs
# directives in the order they're written and an auth check placed after
# reverse_proxy is dead code that never runs (full bypass, not an error;
# see lib/common.sh's configure_caddy_for_service for the fuller version of
# this warning). Idempotent: an already-protected site is flagged in the
# list and skips re-inserting a duplicate import, going straight to access
# scoping below.
#
# Remote site: this box can't edit a file on another machine, so it prints
# (and saves to caddy-snippets/, same convention as every other remote-
# Authelia caller in this repo) the forward_auth block that box's OWN
# Caddyfile needs instead — the remote-hop-safe form with a literal
# X-Forwarded-Host, not the {host} placeholder, for the header-rewrite
# reasons documented at length in CLAUDE.md and services/asterisk.sh.
#
# Either way, finishes by offering _authelia_scope_access for the domain —
# identical either way, since it only cares about the domain, not which box
# is actually enforcing the gate.
_authelia_protect_site() {
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
local -a site_domains
if [ -f "$caddy_file" ]; then
mapfile -t site_domains < <(grep -oE '^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,} \{$' "$caddy_file" | sed 's/ {$//')
fi
echo ""
echo " Protect a site with this Authelia instance."
if [ "${#site_domains[@]}" -gt 0 ]; then
echo " Local Caddy sites on this box:"
local i d marker
for i in "${!site_domains[@]}"; do
d="${site_domains[$i]}"
marker=""
sed -n "/^${d} {\$/,/^}/p" "$caddy_file" | grep -qE 'import authelia|forward_auth' && marker=" (already protected)"
echo " $((i + 1))) ${d}${marker}"
done
else
echo " No local Caddy sites found."
fi
echo " Or type a domain directly — including one on a DIFFERENT box's Caddy"
echo " entirely (this box only needs to run Authelia itself for that to work)."
echo ""
local choice=""
prompt_text " Number or domain:" "" choice
if [ -z "$choice" ]; then
log_warning "Nothing entered — nothing to do."
return 0
fi
local target_domain="" is_local=false
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#site_domains[@]}" ]; then
target_domain="${site_domains[$((choice - 1))]}"
is_local=true
else
target_domain="$choice"
[ -f "$caddy_file" ] && grep -qx "${target_domain} {" "$caddy_file" 2>/dev/null && is_local=true
fi
if [ "$is_local" = true ]; then
if sed -n "/^${target_domain} {\$/,/^}/p" "$caddy_file" | grep -qE 'import authelia|forward_auth'; then
log_info "${target_domain} is already protected — moving on to access scoping."
else
cp "$caddy_file" "$caddy_file.backup.$(date +%Y%m%d-%H%M%S)"
sed -i "/^${target_domain} {\$/a\\ import authelia" "$caddy_file"
log_success "Inserted 'import authelia' into ${target_domain}'s Caddy block."
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$"; then
if docker exec -w /etc/caddy caddy caddy reload 2>/dev/null; then
log_success "Caddy reloaded"
elif docker restart caddy &>/dev/null; then
log_success "Caddy restarted (reload API is disabled by default)"
else
log_warning "Reload/restart failed — check: docker logs caddy"
fi
fi
fi
else
# This instance's own portal — read back from the primary apex's
# session.cookies entry (the first one; same read-back pattern
# every other caller in this file uses). A domain on a different
# box isn't "added" to this instance the way add_authelia_domain's
# apex domains are — it's just gated by THIS instance's existing
# portal, same as any local site above, so there's no per-domain
# cookie entry of its own to read from.
local portal_domain
portal_domain="$(tr -d '\r' < "$config_file" | awk '/^ cookies:$/{f=1; next} f && /authelia_url:/{print $2; exit}' | sed -E 's#^https?://##')"
if [ -z "$portal_domain" ]; then
log_warning "Couldn't determine this instance's own portal domain from $config_file — aborting."
return 1
fi
echo ""
log_info "${target_domain} isn't on this box's own Caddy — add this to the OTHER box's"
log_info "Caddyfile instead (the one that actually serves ${target_domain}), BEFORE reverse_proxy:"
echo ""
echo " forward_auth https://${portal_domain} {"
echo " uri /api/authz/forward-auth"
echo " copy_headers Remote-User Remote-Groups Remote-Name Remote-Email"
echo " header_up X-Forwarded-Method {method}"
echo " header_up X-Forwarded-Proto {scheme}"
echo " header_up X-Forwarded-Host ${target_domain}"
echo " header_up X-Forwarded-Uri {uri}"
echo " }"
echo ""
log_warning "Must come BEFORE reverse_proxy in that block, not after — Caddy runs"
log_warning "directives in the order they're written, and an auth check placed after"
log_warning "reverse_proxy never runs at all (full bypass, not an error)."
local snippet_dir="$DOCKER_DIR/caddy-snippets"
mkdir -p "$snippet_dir"
cat > "$snippet_dir/${target_domain}-authelia.caddy" << SNIPPET
forward_auth https://${portal_domain} {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
header_up X-Forwarded-Method {method}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host ${target_domain}
header_up X-Forwarded-Uri {uri}
}
SNIPPET
chown "$ACTUAL_USER:$ACTUAL_USER" "$snippet_dir/${target_domain}-authelia.caddy" 2>/dev/null || true
log_success "Also saved: $snippet_dir/${target_domain}-authelia.caddy"
fi
_authelia_scope_access "$(echo "$target_domain" | tr '.' '-')" "$target_domain"
}
# Picks "count" random characters from "charset" using an unbiased-enough
# per-byte modulo draw from /dev/urandom. Not part of lib/common.sh's shared
# generate_password (that one is deliberately alphanumeric-only — see its