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