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 " 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 " 11) Un-protect a site (undoes option 10 for one site)"
|
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 " 0) Leave as-is / exit"
|
||||||
echo ""
|
echo ""
|
||||||
local EXISTING_CHOICE=""
|
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
|
case "$EXISTING_CHOICE" in
|
||||||
1)
|
1)
|
||||||
add_authelia_domain
|
add_authelia_domain
|
||||||
@@ -290,6 +292,10 @@ install_authelia() {
|
|||||||
_authelia_unprotect_site
|
_authelia_unprotect_site
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
12)
|
||||||
|
_authelia_export_import_users_menu
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
0|*)
|
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
|
||||||
@@ -315,6 +321,24 @@ install_authelia() {
|
|||||||
# it back from configuration.yml's session.cookies authelia_url instead
|
# it back from configuration.yml's session.cookies authelia_url instead
|
||||||
# of assuming "auth." — see those functions for why.
|
# 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
|
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}"
|
AUTHELIA_PORTAL_DOMAIN="${AUTHELIA_PORTAL_SUBDOMAIN}.${AUTHELIA_DOMAIN}"
|
||||||
prompt_text " Admin username:" "admin" AUTHELIA_ADMIN_USER
|
prompt_text " Admin username:" "admin" AUTHELIA_ADMIN_USER
|
||||||
prompt_text " Admin display name:" "Administrator" AUTHELIA_ADMIN_DISPLAY
|
prompt_text " Admin display name:" "Administrator" AUTHELIA_ADMIN_DISPLAY
|
||||||
@@ -695,6 +719,17 @@ add_authelia_domain() {
|
|||||||
|
|
||||||
local NEW_PORTAL_SUBDOMAIN NEW_PORTAL_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
|
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}"
|
NEW_PORTAL_DOMAIN="${NEW_PORTAL_SUBDOMAIN}.${NEW_DOMAIN}"
|
||||||
|
|
||||||
# ── access_control.rules: insert right after "rules:" ────────────────────
|
# ── 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."
|
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
|
# action="exempt": inserts a "policy: one_factor / subject: user:<name>" rule
|
||||||
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
|
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
|
||||||
# configuration.yml (handles multi-domain instances from add_authelia_domain
|
# configuration.yml (handles multi-domain instances from add_authelia_domain
|
||||||
|
|||||||
Reference in New Issue
Block a user