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 " promote/demote admin, per-service access, delete)"
|
||||||
echo " 5) Register an app to log in VIA Authelia (OIDC/SSO — e.g. ActualBudget,"
|
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 " 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 " existing sessions for every domain already on this instance)"
|
||||||
echo " 7) Show who has universal vs. service-scoped access"
|
echo " 8) Show who has universal vs. service-scoped access"
|
||||||
echo " 8) Change \"Remember me\" session duration (stay logged in longer)"
|
echo " 9) Change \"Remember me\" session duration (stay logged in longer)"
|
||||||
echo " 9) Protect an existing site with this instance (pick a local Caddy site,"
|
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 " or type one on a different box — gates it with a login, same as any"
|
||||||
echo " other service already protected this way)"
|
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 ""
|
echo ""
|
||||||
local EXISTING_CHOICE=""
|
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
|
case "$EXISTING_CHOICE" in
|
||||||
1)
|
1)
|
||||||
add_authelia_domain
|
add_authelia_domain
|
||||||
@@ -265,20 +268,28 @@ install_authelia() {
|
|||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
6)
|
6)
|
||||||
: # fall through to the full reinstall flow below
|
_authelia_remove_oidc_client_menu
|
||||||
|
return 0
|
||||||
;;
|
;;
|
||||||
7)
|
7)
|
||||||
|
: # fall through to the full reinstall flow below
|
||||||
|
;;
|
||||||
|
8)
|
||||||
_authelia_report_access_scope
|
_authelia_report_access_scope
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
8)
|
9)
|
||||||
_authelia_set_remember_me
|
_authelia_set_remember_me
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
9)
|
10)
|
||||||
_authelia_protect_site
|
_authelia_protect_site
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
11)
|
||||||
|
_authelia_unprotect_site
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
|
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
|
||||||
return 0
|
return 0
|
||||||
@@ -1030,6 +1041,132 @@ SNIPPET
|
|||||||
_authelia_scope_access "$(echo "$target_domain" | tr '.' '-')" "$target_domain"
|
_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
|
# 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
|
# 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
|
# generate_password (that one is deliberately alphanumeric-only — see its
|
||||||
@@ -2218,11 +2355,14 @@ _authelia_add_oidc_client() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -qF "client_id: '${CLIENT_ID}'" "$CONFIG_FILE" 2>/dev/null; then
|
# No duplicate-ID check here — _authelia_provision_oidc_client below
|
||||||
log_warning "A client with ID '$CLIENT_ID' is already registered in $CONFIG_FILE."
|
# already handles that safely by replacing the stale registration (its
|
||||||
log_warning "Pick a different app, or edit that entry by hand."
|
# old secret was shown once and is unrecoverable either way, so there's
|
||||||
return 0
|
# nothing to lose). An earlier version of this function dead-ended here
|
||||||
fi
|
# 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=""
|
local APP_DOMAIN_DEFAULT="" APP_DOMAIN=""
|
||||||
[ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ] && APP_DOMAIN_DEFAULT="${CLIENT_ID}.${SITE_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."
|
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 immediately when executed directly (deferred until after function definition)
|
||||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_authelia
|
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_authelia
|
||||||
|
|||||||
Reference in New Issue
Block a user