Give admins guaranteed access to every Authelia-protected site, old and new
Adds a "subject: group:admins" rule ahead of every domain's other rules, so admins always match first regardless of any per-service scoping (existing or future) on that domain — a group's deny-elsewhere rule can no longer catch an admin even if they're accidentally added to that group later. - install_authelia and add_authelia_domain bake the rule in at creation time - _authelia_scope_access retrofits it just-in-time before inserting its own deny-elsewhere rule, and anchors that rule below it instead of at the top - new menu option 13 (_authelia_ensure_admin_access_everywhere) backfills it across every domain on an install that predates this - remove_authelia_domain cleans the rule up too when a domain is removed, and its domain picker dedupes since two rules now share one domain string
This commit is contained in:
+175
-4
@@ -244,10 +244,12 @@ install_authelia() {
|
||||
echo " 11) Un-protect a site (undoes option 10 for one site)"
|
||||
echo " 12) Export/import user data (backup accounts + 2FA before a reinstall,"
|
||||
echo " or restore a previous export)"
|
||||
echo " 13) Make sure admins always have access to every site (old and new —"
|
||||
echo " safe to re-run any time)"
|
||||
echo " 0) Leave as-is / exit"
|
||||
echo ""
|
||||
local EXISTING_CHOICE=""
|
||||
prompt_text " Choice [1-12, 0 to exit]:" "0" EXISTING_CHOICE
|
||||
prompt_text " Choice [1-13, 0 to exit]:" "0" EXISTING_CHOICE
|
||||
case "$EXISTING_CHOICE" in
|
||||
1)
|
||||
add_authelia_domain
|
||||
@@ -296,6 +298,10 @@ install_authelia() {
|
||||
_authelia_export_import_users_menu
|
||||
return 0
|
||||
;;
|
||||
13)
|
||||
_authelia_ensure_admin_access_everywhere
|
||||
return 0
|
||||
;;
|
||||
0|*)
|
||||
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
|
||||
return 0
|
||||
@@ -451,6 +457,14 @@ authentication_backend:
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
# Admins always match first, on every domain this instance protects — this
|
||||
# rule must stay above any per-service scoping rule (old or new) below it,
|
||||
# or a group's "deny elsewhere" rule could accidentally catch an admin.
|
||||
# add_authelia_domain and _authelia_scope_access both preserve this
|
||||
# ordering automatically — see _authelia_ensure_admin_bypass.
|
||||
- domain: "*.${AUTHELIA_DOMAIN}"
|
||||
subject: "group:admins"
|
||||
policy: two_factor
|
||||
- domain: "*.${AUTHELIA_DOMAIN}"
|
||||
policy: two_factor
|
||||
|
||||
@@ -733,9 +747,16 @@ add_authelia_domain() {
|
||||
NEW_PORTAL_DOMAIN="${NEW_PORTAL_SUBDOMAIN}.${NEW_DOMAIN}"
|
||||
|
||||
# ── access_control.rules: insert right after "rules:" ────────────────────
|
||||
# The admin-bypass rule goes first, this domain's own catch-all second —
|
||||
# both inserted together in one shot so a fresh domain never has a window
|
||||
# where the catch-all exists without the bypass above it. See
|
||||
# _authelia_ensure_admin_bypass for why this ordering has to hold.
|
||||
awk -v domain="$NEW_DOMAIN" '
|
||||
{ print }
|
||||
/^ rules:$/ && !done {
|
||||
print " - domain: \"*." domain "\""
|
||||
print " subject: \"group:admins\""
|
||||
print " policy: two_factor"
|
||||
print " - domain: \"*." domain "\""
|
||||
print " policy: two_factor"
|
||||
done=1
|
||||
@@ -861,7 +882,10 @@ remove_authelia_domain() {
|
||||
fi
|
||||
|
||||
local -a apex_domains
|
||||
mapfile -t apex_domains < <(grep -oE '^ - domain: "\*\.[^"]+"' "$CONFIG_FILE" | sed -E 's/^ - domain: "\*\.(.+)"$/\1/')
|
||||
# Each apex domain now has two rules sharing this exact domain string —
|
||||
# its admin-bypass rule and its plain catch-all (see
|
||||
# _authelia_ensure_admin_bypass) — so dedupe or it'd list every domain twice.
|
||||
mapfile -t apex_domains < <(grep -oE '^ - domain: "\*\.[^"]+"' "$CONFIG_FILE" | sed -E 's/^ - domain: "\*\.(.+)"$/\1/' | awk '!seen[$0]++')
|
||||
|
||||
echo ""
|
||||
if [ "${#apex_domains[@]}" -eq 0 ]; then
|
||||
@@ -913,6 +937,23 @@ remove_authelia_domain() {
|
||||
prompt_yn " Continue? (y/n):" "n" CONFIRM_RM
|
||||
[[ "$CONFIRM_RM" =~ ^[Yy]$ ]] || { log_info "Cancelled — nothing changed."; return 0; }
|
||||
|
||||
# ── access_control.rules: remove this domain's admin-bypass rule first ────
|
||||
# (3 lines: domain/subject/policy — see _authelia_ensure_admin_bypass) —
|
||||
# has to run before the plain catch-all removal below, which only knows
|
||||
# how to strip a 2-line domain/policy pair and would otherwise leave this
|
||||
# rule's own "policy:" line orphaned, the exact corruption class this
|
||||
# instance was repaired from once already.
|
||||
awk -v domain="$RM_DOMAIN" '
|
||||
BEGIN { skip=0 }
|
||||
skip > 0 { skip--; next }
|
||||
$0 == " - domain: \"*." domain "\"" {
|
||||
getline nxt
|
||||
if (nxt == " subject: \"group:admins\"") { skip=1; next }
|
||||
print; print nxt; next
|
||||
}
|
||||
{ print }
|
||||
' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
|
||||
|
||||
# ── access_control.rules: remove the "- domain: "*.X"" + "policy: ..." pair ──
|
||||
awk -v domain="$RM_DOMAIN" '
|
||||
BEGIN { skip=0 }
|
||||
@@ -1543,6 +1584,71 @@ _authelia_create_user_noninteractive() {
|
||||
# domain that's already scoped just report the existing group instead of
|
||||
# duplicating rules.
|
||||
#
|
||||
# Idempotent: makes sure a "subject: group:admins" rule exists for $domain's
|
||||
# access_control.rules, above every other rule for that domain, then returns
|
||||
# the 1-indexed line number its "policy:" line ended up on (via ADMIN_BYPASS_LINE,
|
||||
# not stdout — awk already uses stdout for the rewritten file). Callers that
|
||||
# insert a NEW rule for this domain (_authelia_scope_access's deny-elsewhere
|
||||
# rule in particular) must insert it AFTER that line, never at the literal
|
||||
# top of "rules:" — Authelia takes the first matching rule, so a deny rule
|
||||
# landing above the admin-bypass rule would catch an admin who's also (by
|
||||
# mistake, or by some future feature) a member of the group being denied.
|
||||
# install_authelia and add_authelia_domain both bake this rule in directly
|
||||
# at creation time instead of calling this — it's for retrofitting an apex
|
||||
# domain that predates this (see _authelia_ensure_admin_access_everywhere,
|
||||
# the menu-driven bulk version of this for an existing install) and for
|
||||
# _authelia_scope_access's own just-in-time safety net.
|
||||
_authelia_ensure_admin_bypass() {
|
||||
local config_file="$1" domain="$2"
|
||||
ADMIN_BYPASS_LINE=""
|
||||
ADMIN_BYPASS_ADDED="false"
|
||||
|
||||
local result
|
||||
result="$(awk -v domain="$domain" '
|
||||
{ lines[NR] = $0 }
|
||||
END {
|
||||
for (i = 1; i < NR; i++) {
|
||||
if (lines[i] == " - domain: \"*." domain "\"" && lines[i+1] == " subject: \"group:admins\"") {
|
||||
print i + 2
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$config_file")"
|
||||
|
||||
if [ -n "$result" ]; then
|
||||
ADMIN_BYPASS_LINE="$result"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ADMIN_BYPASS_ADDED="true"
|
||||
awk -v domain="$domain" '
|
||||
{ print }
|
||||
/^ rules:$/ && !done {
|
||||
print " - domain: \"*." domain "\""
|
||||
print " subject: \"group:admins\""
|
||||
print " policy: two_factor"
|
||||
done=1
|
||||
}
|
||||
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
|
||||
# The block we just inserted is always lines 2-4 counting from " rules:"
|
||||
# (which awk just placed the block right after) — but simplest and least
|
||||
# fragile is to just re-run the same lookup now that it exists.
|
||||
ADMIN_BYPASS_LINE="$(awk -v domain="$domain" '
|
||||
{ lines[NR] = $0 }
|
||||
END {
|
||||
for (i = 1; i < NR; i++) {
|
||||
if (lines[i] == " - domain: \"*." domain "\"" && lines[i+1] == " subject: \"group:admins\"") {
|
||||
print i + 2
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$config_file")"
|
||||
}
|
||||
|
||||
# Args: SERVICE_ID DOMAIN
|
||||
_authelia_scope_access() {
|
||||
local service_id="$1" domain="$2"
|
||||
@@ -1624,6 +1730,16 @@ _authelia_scope_access() {
|
||||
# before access_control's existing "*.${AUTHELIA_DOMAIN}" catch-all.
|
||||
local authelia_domain
|
||||
authelia_domain="$(awk '/^ cookies:$/{f=1; next} f && /domain:/{print $3; exit}' "$config_file")"
|
||||
|
||||
# ...but BELOW the admin-bypass rule for this apex — otherwise the
|
||||
# deny-elsewhere rule below would outrank it, and an admin who ever ends
|
||||
# up in this group (by mistake, or a future feature) would be locked out
|
||||
# of every other domain on the instance. Ensures the bypass rule exists
|
||||
# first (retrofits it if this instance predates the feature), then
|
||||
# inserts right after it rather than at the literal top of "rules:".
|
||||
_authelia_ensure_admin_bypass "$config_file" "$authelia_domain"
|
||||
local anchor_line="$ADMIN_BYPASS_LINE"
|
||||
|
||||
local scope_rules=" - domain: \"${domain}\"
|
||||
subject: \"group:${group}\"
|
||||
policy: two_factor
|
||||
@@ -1631,9 +1747,10 @@ _authelia_scope_access() {
|
||||
subject: \"group:${group}\"
|
||||
policy: deny"
|
||||
|
||||
awk -v block="$scope_rules" '
|
||||
/^ rules:$/ && !done { print; print block; done=1; next }
|
||||
awk -v block="$scope_rules" -v anchor="$anchor_line" '
|
||||
{ print }
|
||||
anchor != "" && NR == anchor + 0 { print block }
|
||||
anchor == "" && /^ rules:$/ && !done { print block; done=1 }
|
||||
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
|
||||
@@ -1646,6 +1763,60 @@ _authelia_scope_access() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Menu-driven, idempotent bulk version of _authelia_ensure_admin_bypass —
|
||||
# backfills the admin-bypass rule for every apex domain currently on this
|
||||
# instance in one pass. install_authelia and add_authelia_domain bake the
|
||||
# rule in automatically for anything created from here on, and
|
||||
# _authelia_scope_access retrofits it just-in-time for whichever domain it's
|
||||
# scoping — this is for catching every OTHER domain an install already had
|
||||
# before this feature existed (or just double-checking one that's fine).
|
||||
# Safe to re-run any time; only touches domains actually missing the rule.
|
||||
_authelia_ensure_admin_access_everywhere() {
|
||||
local config_file="$DOCKER_DIR/authelia/config/configuration.yml"
|
||||
[ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; }
|
||||
|
||||
local -a apex_domains
|
||||
mapfile -t apex_domains < <(grep -oE '^ - domain: "\*\.[^"]+"' "$config_file" | sed -E 's/^ - domain: "\*\.(.+)"$/\1/' | awk '!seen[$0]++')
|
||||
|
||||
if [ "${#apex_domains[@]}" -eq 0 ]; then
|
||||
log_info "No apex domains found on this instance."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Checking the admin-bypass rule for every domain on this instance:"
|
||||
local d any_added="false"
|
||||
for d in "${apex_domains[@]}"; do
|
||||
_authelia_ensure_admin_bypass "$config_file" "$d"
|
||||
if [ "$ADMIN_BYPASS_ADDED" = "true" ]; then
|
||||
echo " ${d} — added"
|
||||
any_added="true"
|
||||
else
|
||||
echo " ${d} — already present"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$any_added" = "false" ]; then
|
||||
log_success "Every domain already had it — nothing to change."
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_success "Members of the 'admins' group now always have access to every domain"
|
||||
log_success "listed above, regardless of any per-service scoping already in place —"
|
||||
log_success "or added later, since _authelia_scope_access checks for this automatically"
|
||||
log_success "from now on."
|
||||
log_warning "Make sure your admin account(s) are actually in the 'admins' group in"
|
||||
log_warning "config/users.yml — the default admin created at install time already is."
|
||||
|
||||
local restart_auth=""
|
||||
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
|
||||
if [[ "$restart_auth" =~ ^[Yy]$ ]]; then
|
||||
(cd "$DOCKER_DIR/authelia" && docker compose restart authelia 2>/dev/null) \
|
||||
&& log_success "Authelia restarted" \
|
||||
|| log_warning "Restart failed — check: docker compose logs authelia"
|
||||
fi
|
||||
}
|
||||
|
||||
# Reporting/management: lists which users have "universal" access (every
|
||||
# protected domain — anyone not locked into a "<service>-only" group) versus
|
||||
# which are scoped to specific services, then offers to promote a scoped
|
||||
|
||||
Reference in New Issue
Block a user