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:
+151
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user