Merge pull request #389 from outis1one/claude/frigate-authelia-openid-0l1htj
Claude/frigate authelia openid 0l1htj
This commit is contained in:
+181
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user