diff --git a/services/authelia.sh b/services/authelia.sh index 806c2df..fc76aaf 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -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