Merge pull request #389 from outis1one/claude/frigate-authelia-openid-0l1htj

Claude/frigate authelia openid 0l1htj
This commit is contained in:
Outis
2026-08-23 21:42:17 -04:00
committed by GitHub
+181 -1
View File
@@ -242,10 +242,12 @@ install_authelia() {
echo " or type one on a different box — gates it with a login, same as any"
echo " other service already protected this way)"
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 " 0) Leave as-is / exit"
echo ""
local EXISTING_CHOICE=""
prompt_text " Choice [1-11, 0 to exit]:" "0" EXISTING_CHOICE
prompt_text " Choice [1-12, 0 to exit]:" "0" EXISTING_CHOICE
case "$EXISTING_CHOICE" in
1)
add_authelia_domain
@@ -290,6 +292,10 @@ install_authelia() {
_authelia_unprotect_site
return 0
;;
12)
_authelia_export_import_users_menu
return 0
;;
0|*)
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
return 0
@@ -315,6 +321,24 @@ install_authelia() {
# it back from configuration.yml's session.cookies authelia_url instead
# of assuming "auth." — see those functions for why.
prompt_text " Subdomain for the login portal (e.g. 'auth' -> auth.${AUTHELIA_DOMAIN}):" "auth" AUTHELIA_PORTAL_SUBDOMAIN
# Auto-correct the full domain being typed here by mistake (e.g.
# "authelia.mydomain.com" instead of just "authelia") — concatenating
# that with .${AUTHELIA_DOMAIN} below would otherwise silently produce
# a doubled, broken domain like "authelia.mydomain.com.mydomain.com"
# that never matches any real request. Confirmed live: this is exactly
# what happened on a real box, and it explained a much bigger mystery
# than the obviously-wrong hostname alone would suggest — every
# forward_auth-gated site on the instance silently bypassed Authelia,
# because Caddy had no site block matching the real portal hostname at
# all, so the forward_auth subrequest never reached real policy
# evaluation in the first place.
if [[ "$AUTHELIA_PORTAL_SUBDOMAIN" == *".${AUTHELIA_DOMAIN}" ]]; then
AUTHELIA_PORTAL_SUBDOMAIN="${AUTHELIA_PORTAL_SUBDOMAIN%.${AUTHELIA_DOMAIN}}"
log_info "That already included the domain — using just '${AUTHELIA_PORTAL_SUBDOMAIN}' as the subdomain."
elif [[ "$AUTHELIA_PORTAL_SUBDOMAIN" == "$AUTHELIA_DOMAIN" ]]; then
log_warning "That's the apex domain itself, not a subdomain — the portal can't live at the bare apex (it would collide with the wildcard rule protecting everything else). Using 'auth' instead."
AUTHELIA_PORTAL_SUBDOMAIN="auth"
fi
AUTHELIA_PORTAL_DOMAIN="${AUTHELIA_PORTAL_SUBDOMAIN}.${AUTHELIA_DOMAIN}"
prompt_text " Admin username:" "admin" AUTHELIA_ADMIN_USER
prompt_text " Admin display name:" "Administrator" AUTHELIA_ADMIN_DISPLAY
@@ -695,6 +719,17 @@ add_authelia_domain() {
local NEW_PORTAL_SUBDOMAIN NEW_PORTAL_DOMAIN
prompt_text " Subdomain for this domain's own login portal (e.g. 'auth' -> auth.${NEW_DOMAIN}):" "auth" NEW_PORTAL_SUBDOMAIN
# See install_authelia's identical guard on AUTHELIA_PORTAL_SUBDOMAIN
# for why this matters — typing the full domain here instead of just
# the subdomain silently produces a doubled, broken hostname that
# never matches any real request.
if [[ "$NEW_PORTAL_SUBDOMAIN" == *".${NEW_DOMAIN}" ]]; then
NEW_PORTAL_SUBDOMAIN="${NEW_PORTAL_SUBDOMAIN%.${NEW_DOMAIN}}"
log_info "That already included the domain — using just '${NEW_PORTAL_SUBDOMAIN}' as the subdomain."
elif [[ "$NEW_PORTAL_SUBDOMAIN" == "$NEW_DOMAIN" ]]; then
log_warning "That's the apex domain itself, not a subdomain — the portal can't live at the bare apex (it would collide with the wildcard rule protecting everything else). Using 'auth' instead."
NEW_PORTAL_SUBDOMAIN="auth"
fi
NEW_PORTAL_DOMAIN="${NEW_PORTAL_SUBDOMAIN}.${NEW_DOMAIN}"
# ── access_control.rules: insert right after "rules:" ────────────────────
@@ -1747,6 +1782,151 @@ _authelia_set_remember_me() {
log_info "changes how long checking it actually keeps you signed in."
}
# Export/import accounts (+ optionally 2FA/session state) — for migrating to
# a fresh instance or restoring after a reinstall without losing accounts or
# forcing everyone to re-enroll 2FA. Passwords are never exported as
# plaintext — Authelia only ever stores an argon2id hash — but that hash is
# fully portable: dropping it into another instance's users.yml (same
# hashing settings, which this repo's installer always uses) makes the
# original password keep working, no reset required.
#
# TOTP secrets inside data/db.sqlite3 are AES-encrypted with this instance's
# own storage encryption key (config/secrets/storage_secret) — NOT with
# anything derived from the password. install_authelia's fresh-install path
# generates a brand-new storage_secret every time (openssl rand -hex 32,
# same as jwt_secret/session_secret), so a db.sqlite3 copied onto an
# instance with a different storage_secret has 2FA data Authelia can't
# decrypt. Export/import both carry storage_secret alongside db.sqlite3 so a
# "remove and recreate" round-trip (export, reinstall, import) keeps 2FA
# working — session_secret/jwt_secret don't need to match (only sign
# cookies / password-reset links, safe to rotate) so those are left alone.
_authelia_export_import_users_menu() {
local authelia_dir="$DOCKER_DIR/authelia"
[ -f "$authelia_dir/config/users.yml" ] || { log_warning "No users.yml found — install Authelia first."; return 1; }
echo ""
echo " Export/import user data"
echo " 1) Export (users.yml + 2FA/session data) to a backup folder"
echo " 2) Import from a previous export (overwrites current users)"
echo " 0) Back"
local choice=""
prompt_text " Choice [1-2, 0 to go back]:" "0" choice
case "$choice" in
1) _authelia_export_users ;;
2) _authelia_import_users ;;
0|*) return 0 ;;
esac
}
_authelia_export_users() {
local authelia_dir="$DOCKER_DIR/authelia"
local users_file="$authelia_dir/config/users.yml"
local db_file="$authelia_dir/data/db.sqlite3"
local storage_secret_file="$authelia_dir/config/secrets/storage_secret"
local default_dest="${ACTUAL_HOME:-$HOME}/authelia-export-$(date +%Y%m%d)"
local dest=""
prompt_text " Export to which directory? [${default_dest}]:" "$default_dest" dest
[ -z "$dest" ] && dest="$default_dest"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would export $users_file, $db_file, and $storage_secret_file to $dest"
return 0
fi
mkdir -p "$dest"
cp "$users_file" "$dest/users.yml"
local exported_2fa="no"
if [ -f "$db_file" ] && [ -f "$storage_secret_file" ]; then
cp "$db_file" "$dest/db.sqlite3"
cp "$storage_secret_file" "$dest/storage_secret"
exported_2fa="yes"
fi
# Readable summary alongside the raw file — username / display name /
# email / groups, no password hash — handy to eyeball or hand off
# without pasting the full users.yml.
awk '
/^ [a-zA-Z0-9_-]+:$/ { if (u) print u, "|", d, "|", e, "|", g; u=$1; sub(":","",u); d=""; e=""; g="" }
/^ displayname:/ { d=$0; sub(/^ displayname: */,"",d) }
/^ email:/ { e=$0; sub(/^ email: */,"",e) }
/^ - / { line=$0; gsub(/^ - /,"",line); g = g line "," }
END { if (u) print u, "|", d, "|", e, "|", g }
' "$users_file" > "$dest/users-summary.txt"
chown -R "${ACTUAL_USER:-$(id -un)}:${ACTUAL_USER:-$(id -un)}" "$dest" 2>/dev/null || true
chmod 600 "$dest/users.yml" "$dest/storage_secret" 2>/dev/null || true
log_success "Exported to $dest"
echo " users.yml — full account data incl. password hashes (portable, works as-is on import)"
if [ "$exported_2fa" = "yes" ]; then
echo " db.sqlite3 — 2FA/TOTP registrations + session storage"
echo " storage_secret — required alongside db.sqlite3 to decrypt the 2FA data (keep this file private)"
else
log_warning " No data/db.sqlite3 or secrets/storage_secret found — 2FA registrations were NOT exported. Users will need to re-enroll 2FA after an import."
fi
echo " users-summary.txt — readable username/displayname/email/groups list, no password hash"
}
_authelia_import_users() {
local authelia_dir="$DOCKER_DIR/authelia"
local users_file="$authelia_dir/config/users.yml"
local db_file="$authelia_dir/data/db.sqlite3"
local storage_secret_file="$authelia_dir/config/secrets/storage_secret"
local src=""
prompt_text " Import from which directory (containing users.yml)?:" "" src
[ -z "$src" ] && { log_info "Cancelled — nothing changed."; return 0; }
src="${src%/}"
if [ ! -f "$src/users.yml" ]; then
log_warning "No users.yml found in $src — nothing to import."
return 1
fi
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would replace $users_file with $src/users.yml"
[ -f "$src/db.sqlite3" ] && echo "[DRY-RUN] Would replace $db_file and $storage_secret_file with the exported copies"
return 0
fi
local ts
ts="$(date +%Y%m%d-%H%M%S)"
[ -f "$users_file" ] && cp "$users_file" "$users_file.bak.$ts"
cp "$src/users.yml" "$users_file"
chown 1000:1000 "$users_file"
log_success "Imported users.yml (previous version backed up to $(basename "$users_file").bak.$ts)"
if [ -f "$src/db.sqlite3" ] && [ -f "$src/storage_secret" ]; then
local import_db=""
prompt_yn " Also import 2FA/session data (db.sqlite3 + storage_secret) — restores everyone's existing TOTP enrollment instead of forcing a re-scan? (y/n):" "y" import_db
if [[ "$import_db" =~ ^[Yy]$ ]]; then
[ -f "$db_file" ] && cp "$db_file" "$db_file.bak.$ts"
[ -f "$storage_secret_file" ] && cp "$storage_secret_file" "$storage_secret_file.bak.$ts"
cp "$src/db.sqlite3" "$db_file"
cp "$src/storage_secret" "$storage_secret_file"
chown 1000:1000 "$db_file" "$storage_secret_file"
chmod 600 "$storage_secret_file"
log_success "Imported db.sqlite3 + storage_secret (previous versions backed up alongside them)."
log_warning "storage_secret must match what encrypted this db.sqlite3 — don't import one without the other, or 2FA data becomes undecryptable."
fi
elif [ -f "$src/db.sqlite3" ] || [ -f "$src/storage_secret" ]; then
log_warning "Found only one of db.sqlite3 / storage_secret in $src — need both together to safely restore 2FA data, so skipping. Imported users will need to re-enroll 2FA on first login."
else
log_info "No 2FA/session export found in $src — imported users will need to re-enroll 2FA on first login."
fi
local restart_auth=""
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
if [[ "$restart_auth" =~ ^[Yy]$ ]]; 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
}
# action="exempt": inserts a "policy: one_factor / subject: user:<name>" rule
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
# configuration.yml (handles multi-domain instances from add_authelia_domain