Add site un-protect and OIDC client removal; fix a duplicate-ID dead end

Three related fixes so undoing an Authelia change never requires
hand-editing configuration.yml or the Caddyfile:

- _authelia_add_oidc_client() had its own redundant duplicate-client-ID
  check that dead-ended with "pick a different app, or edit that entry
  by hand" -- even though _authelia_provision_oidc_client (called a
  few lines later in the same function) already handles that exact
  case safely by replacing the stale registration. Removed the
  redundant check; the flow now always reaches the safe path. This was
  the actual blocker in the reported "client with ID 'actualbudget' is
  already registered" error -- re-registering the same app a second
  time was never actually broken, just gated by dead code.

- New option 6, _authelia_remove_oidc_client_menu(): lists registered
  OIDC clients by ID and name, removes one via the existing internal
  _authelia_remove_oidc_client() helper (previously only reachable
  from the replace-on-duplicate path, never exposed directly).

- New option 11, _authelia_unprotect_site(): reverse of option 10
  (_authelia_protect_site). Removes a local site's "import authelia"
  or forward_auth block from its own Caddy block and reloads Caddy;
  for a domain on a different box's Caddy, cleans up its access-
  scoping rules here (the actual gate needs removing on that box by
  hand, same one-way limitation option 10 already has in reverse).
  Also removes any _authelia_scope_access rules for the domain, found
  by the same "<domain>-only" group name convention, verified against
  a synthetic multi-domain configuration.yml before shipping so an
  unrelated domain's rules sharing the same "*.<apex>" line are left
  untouched. Both the local-block removal (import authelia one-liner
  and multi-line forward_auth block shapes) and the access-rule
  removal were tested against realistic fixtures first.

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-23 19:41:47 +00:00
parent 098bb833cf
commit b2b83948d8
+213 -14
View File
@@ -232,17 +232,20 @@ install_authelia() {
echo " promote/demote admin, per-service access, delete)"
echo " 5) Register an app to log in VIA Authelia (OIDC/SSO — e.g. ActualBudget,"
echo " Vaultwarden, or any other app with its own \"Enable OpenID\" setting)"
echo " 6) Reconfigure from scratch (regenerates secrets/users — breaks"
echo " 6) Remove a registered OIDC app (undoes option 5 for one app — its own"
echo " separate password login, if it has one, is untouched)"
echo " 7) Reconfigure from scratch (regenerates secrets/users — breaks"
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) Protect an existing site with this instance (pick a local Caddy site,"
echo " 8) Show who has universal vs. service-scoped access"
echo " 9) Change \"Remember me\" session duration (stay logged in longer)"
echo " 10) 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 " 11) Un-protect a site (undoes option 10 for one site)"
echo " 12) Leave as-is"
echo ""
local EXISTING_CHOICE=""
prompt_text " Choice [1/2/3/4/5/6/7/8/9/10]:" "10" EXISTING_CHOICE
prompt_text " Choice [1/2/3/4/5/6/7/8/9/10/11/12]:" "12" EXISTING_CHOICE
case "$EXISTING_CHOICE" in
1)
add_authelia_domain
@@ -265,20 +268,28 @@ install_authelia() {
return 0
;;
6)
: # fall through to the full reinstall flow below
_authelia_remove_oidc_client_menu
return 0
;;
7)
: # fall through to the full reinstall flow below
;;
8)
_authelia_report_access_scope
return 0
;;
8)
9)
_authelia_set_remember_me
return 0
;;
9)
10)
_authelia_protect_site
return 0
;;
11)
_authelia_unprotect_site
return 0
;;
*)
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
return 0
@@ -1030,6 +1041,132 @@ SNIPPET
_authelia_scope_access "$(echo "$target_domain" | tr '.' '-')" "$target_domain"
}
# Reverse of _authelia_protect_site — removes a local site's "import
# authelia" line from its own Caddy block (reloading Caddy), and the two
# access_control.rules entries _authelia_scope_access may have added for
# it, if any (found by the same "<domain-with-dots-as-hyphens>-only" group
# name _authelia_protect_site used). Each scoped rule is a 3-line
# "- domain: ...\n subject: ...\n policy: ..." block — bounded removal by
# buffering exactly 3 lines at a time from each " - domain:" line and
# only dropping the buffer if the group's subject line is inside it, so an
# unrelated rule sharing the same "*.<apex>" domain line for a DIFFERENT
# group is untouched.
#
# Does NOT remove the user group itself from users.yml (a user's ["x-only"]
# membership with no matching rule left is inert, not a live grant) or
# touch a domain on a different box's Caddy (nothing here can edit that
# file) — for a remote site, this only cleans up the access rules on this
# side; removing the forward_auth block itself is a manual edit on the box
# that actually serves it.
_authelia_unprotect_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 protected_domains
if [ -f "$caddy_file" ]; then
mapfile -t protected_domains < <(
grep -oE '^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,} \{$' "$caddy_file" | sed 's/ {$//' |
while read -r d; do
sed -n "/^${d} {\$/,/^}/p" "$caddy_file" | grep -qE 'import authelia|forward_auth' && echo "$d"
done
)
fi
echo ""
echo " Un-protect a site (remove its Authelia gate)."
if [ "${#protected_domains[@]}" -gt 0 ]; then
echo " Currently-protected local Caddy sites:"
local i
for i in "${!protected_domains[@]}"; do
echo " $((i + 1))) ${protected_domains[$i]}"
done
else
echo " No locally-gated Caddy sites found."
fi
echo " Or type a domain directly — including one on a different box's Caddy, to"
echo " clean up its access-scoping rules here even though the gate itself lives"
echo " elsewhere and needs removing there by hand."
echo ""
local choice=""
prompt_text " Number or domain (blank to cancel):" "" choice
if [ -z "$choice" ]; then
log_info "Cancelled — nothing changed."
return 0
fi
local target_domain="" is_local=false
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#protected_domains[@]}" ]; then
target_domain="${protected_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
cp "$caddy_file" "$caddy_file.backup.$(date +%Y%m%d-%H%M%S)"
sed -i "/^${target_domain} {\$/,/^}/{/^ *import authelia\$/d; /^ *forward_auth /,/^ *}\$/d}" "$caddy_file"
log_success "Removed the Authelia gate from ${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
else
log_info "${target_domain} isn't currently gated — nothing to remove there."
fi
else
log_info "${target_domain} isn't on this box's own Caddy — only cleaning up its access"
log_info "rules here. Remove the forward_auth block from the box that actually serves"
log_info "it yourself (see $DOCKER_DIR/caddy-snippets/ if it was added via option 9)."
fi
local group="$(echo "$target_domain" | tr '.' '-')-only"
if grep -qF "subject: \"group:${group}\"" "$config_file" 2>/dev/null; then
awk -v grp="\"group:${group}\"" '
BEGIN { buf=""; n=0; hit=0 }
/^ - domain:/ {
if (n > 0) { if (!hit) printf "%s", buf; buf=""; n=0; hit=0 }
buf = $0 "\n"; n=1
if ($0 ~ grp) hit=1
next
}
n > 0 && n < 3 {
buf = buf $0 "\n"; n++
if ($0 ~ grp) hit=1
if (n == 3) { if (!hit) printf "%s", buf; buf=""; n=0; hit=0 }
next
}
{ if (n > 0) { if (!hit) printf "%s", buf; buf=""; n=0; hit=0 } print }
END { if (n > 0 && !hit) printf "%s", buf }
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
chown 1000:1000 "$config_file" 2>/dev/null || true
log_success "Removed the '${group}' access-scoping rules — ${target_domain} is open to any"
log_success "Authelia user again (users' membership in '${group}' is left as harmless"
log_success "unused metadata — remove it by hand in users.yml if you want it fully gone)."
fi
local RESTART_AUTH=""
prompt_yn " Restart Authelia to apply? (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
}
# 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
@@ -2218,11 +2355,14 @@ _authelia_add_oidc_client() {
return 0
fi
if grep -qF "client_id: '${CLIENT_ID}'" "$CONFIG_FILE" 2>/dev/null; then
log_warning "A client with ID '$CLIENT_ID' is already registered in $CONFIG_FILE."
log_warning "Pick a different app, or edit that entry by hand."
return 0
fi
# No duplicate-ID check here — _authelia_provision_oidc_client below
# already handles that safely by replacing the stale registration (its
# old secret was shown once and is unrecoverable either way, so there's
# nothing to lose). An earlier version of this function dead-ended here
# instead ("pick a different app, or edit by hand") before ever
# reaching that safe path — confirmed live: this blocked re-registering
# ActualBudget after nothing more than a first attempt, with no way
# through except hand-editing configuration.yml.
local APP_DOMAIN_DEFAULT="" APP_DOMAIN=""
[ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ] && APP_DOMAIN_DEFAULT="${CLIENT_ID}.${SITE_DOMAIN}"
@@ -2308,5 +2448,64 @@ _authelia_add_oidc_client() {
log_warning "The Client Secret above is shown once — it isn't stored in plaintext anywhere. Save it now."
}
# Interactive wrapper around _authelia_remove_oidc_client (the internal
# helper _authelia_provision_oidc_client already uses to replace a stale
# registration) — exposes it as its own menu action so removing an app's
# OIDC client doesn't require hand-editing configuration.yml either. Lists
# every registered client's ID and display name, numbered; removing one
# doesn't affect that app's own separate password login (if it has one) or
# any other client on this instance.
_authelia_remove_oidc_client_menu() {
local AUTHELIA_DIR="$DOCKER_DIR/authelia"
local CONFIG_FILE="$AUTHELIA_DIR/config/configuration.yml"
if [ ! -f "$CONFIG_FILE" ]; then
log_warning "No configuration.yml found at $CONFIG_FILE — install Authelia first."
return 1
fi
local -a client_ids client_names
mapfile -t client_ids < <(grep -oP "(?<=- client_id: ')[^']+" "$CONFIG_FILE")
mapfile -t client_names < <(grep -oP "(?<=client_name: ')[^']+" "$CONFIG_FILE")
if [ "${#client_ids[@]}" -eq 0 ]; then
log_info "No OIDC clients registered on this instance."
return 0
fi
echo ""
echo " Registered OIDC clients:"
local i
for i in "${!client_ids[@]}"; do
echo " $((i + 1))) ${client_ids[$i]} (${client_names[$i]:-unnamed})"
done
echo ""
local choice=""
prompt_text " Number to remove (blank to cancel):" "" choice
if [ -z "$choice" ] || ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#client_ids[@]}" ]; then
log_info "Cancelled — nothing changed."
return 0
fi
local target_id="${client_ids[$((choice - 1))]}"
log_warning "This removes the OIDC client '${target_id}' — anyone using it to log into that app"
log_warning "via Authelia will no longer be able to until it's re-registered (option 5). It does"
log_warning "NOT touch that app's own separate password login, if it has one."
local confirm=""
prompt_yn " Continue? (y/n):" "n" confirm
[[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled — nothing changed."; return 0; }
_authelia_remove_oidc_client "$CONFIG_FILE" "$target_id"
log_success "Removed OIDC client '${target_id}'."
local RESTART_AUTH=""
prompt_yn " Restart Authelia to apply? (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
}
# Run immediately when executed directly (deferred until after function definition)
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_authelia