diff --git a/CLAUDE.md b/CLAUDE.md index 6f661f0..0bd7821 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -502,6 +502,56 @@ reinstall (from `.env` via `ENV_MAP`, the same array `_frigate_parse_existing` already builds) rather than rotating it and breaking the existing Caddy pairing. +**`gitea` and `uptimekuma` — two more "disable/bypass built-in login, +Authelia is the only gate" integrations, each with its own trust model.** +Both are opt-in extras layered on top of the has-built-in-auth entries +those services already had; neither replaces the existing behavior for +anyone who doesn't ask for it. + +- `gitea`'s `_gitea_offer_reverse_proxy_auth()` is a *second*, stronger + Authelia integration alongside the OIDC "Sign in with Authelia" button + (`_gitea_offer_authelia_sso()`, unchanged): Gitea's own + `ENABLE_REVERSE_PROXY_AUTHENTICATION` mode auto-logs in as whatever + username arrives in a trusted header — no click, no separate Gitea + session with its own expiry. Unlike Frigate, Gitea's own login page + isn't disabled — it stays as a fallback for anyone not arriving through + the trusted path, so there's no "native login off with nothing gating + it" failure mode to guard against here. The trust boundary is + `REVERSE_PROXY_TRUSTED_PROXIES` (an IP range), not a shared secret — + Gitea's own Docker image has shipped this wildcarded before (a real CVE, + GHSA-f75j-4cw6-rmx4: any source IP could set `X-WEBAUTH-USER` and log in + as anyone), so this always computes the range from caddy_net's actual + subnet (`docker network inspect ... --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'`, + the same lookup `ufw_allow_from_caddy_net` uses) and refuses to enable + the feature at all if that can't be determined — never falls back to a + permissive default. `REVERSE_PROXY_AUTHENTICATION_USER`/`_EMAIL` are set + to `Remote-User`/`Remote-Email` to match Authelia's `import authelia` + snippet's own `copy_headers` output directly, rather than renaming + headers in Caddy to match Gitea's own `X-WEBAUTH-USER` default. Gitea + currently reaches Caddy over its published host port + (`host.docker.internal:PORT`), not caddy_net, because it predates this + feature — enabling it rewires Gitea onto caddy_net (like every other + locally-Caddy-fronted service) and re-points Caddy's upstream at + `gitea:3000`, replacing the old site block via + `configure_caddy_for_service`'s own existing "already exists — + overwrite?" prompt. Local Caddy only; a remote Caddy machine's source + address isn't a stable, narrowly-scopeable range the way caddy_net's + bridge subnet is. +- `uptimekuma`'s equivalent is much simpler: Uptime Kuma's `DISABLE_AUTH=true` + env var turns its own login off *completely*, with no IP-range or secret + check left at all — once set, anything that can reach its port is in, no + questions asked. That makes it the one of these three where getting the + ordering wrong is worst: `services/uptimekuma.sh` only ever sets + `DISABLE_AUTH=true` after `configure_caddy_for_service "Uptime Kuma" "uptime-kuma:3001" "uptime" " import authelia"` + confirms `CADDY_SERVICE_CONFIGURED` — the same never-disable-native-auth- + without-a-confirmed-gate rule Frigate follows. Uptime Kuma already joined + caddy_net unconditionally before this (see its own `_CADDY_NET_BLOCK`), + so no networking change was needed here, just the env var and the + Authelia-gated Caddy call happening earlier (before `docker-compose.yml` + is written) instead of the plain unconditional call this file already + had at the end — which now only runs as a fallback when the Authelia + path wasn't used or wasn't completed. + For services without built-in auth, prompt the user before calling `configure_caddy_for_service` and pass `import authelia` as the extra block if Authelia is installed and the user wants SSO protection: diff --git a/services/gitea.sh b/services/gitea.sh index 30c4f50..aedbd97 100644 --- a/services/gitea.sh +++ b/services/gitea.sh @@ -240,6 +240,92 @@ _gitea_offer_authelia_sso() { declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "gitea" "$GITEA_OIDC_DOMAIN" } +# Offers Gitea's OTHER Authelia integration — not the OIDC button above, but +# ENABLE_REVERSE_PROXY_AUTHENTICATION: Gitea auto-logs in as whatever user +# name arrives in a trusted header, no click and no separate Gitea session +# to expire on its own schedule. This is genuinely stronger than the OIDC +# button (which still shows a login page, just with an extra option on it) +# and matches the pattern services/frigate.sh uses — except Gitea's own +# login form stays available as a fallback for anyone NOT arriving from a +# trusted source, so there's no "native login disabled with nothing gating +# it" failure mode to guard against here the way Frigate's had. +# +# The security boundary is REVERSE_PROXY_TRUSTED_PROXIES, not a shared +# secret: Gitea only honors the identity header from source IPs inside that +# range. Gitea's own Docker image shipped this wildcarded (GHSA-f75j-4cw6- +# rmx4 — any IP could set X-WEBAUTH-USER and log in as anyone), so this is +# always computed from caddy_net's real subnet (same lookup +# ufw_allow_from_caddy_net uses) and refuses to enable the feature at all if +# that can't be determined — never falls back to a permissive default. +# +# Requires Gitea to actually be reachable from an address inside that range, +# which means joining caddy_net like every other locally-Caddy-fronted +# service in this repo (Gitea currently reaches Caddy via its published +# host port instead — host.docker.internal upstream — because it predates +# this feature). Local Caddy only: a remote Caddy machine's source address +# isn't a stable, narrowly-scopeable range the way caddy_net's bridge subnet +# is, so this skips remote mode rather than guess at a trust range worth +# getting wrong. +_gitea_offer_reverse_proxy_auth() { + local DIR="$1" + + [ -d "$DOCKER_DIR/authelia" ] || return 0 + [ -d "$DOCKER_DIR/caddy" ] || return 0 + + if grep -q 'ENABLE_REVERSE_PROXY_AUTHENTICATION=true' "$DIR/docker-compose.yml" 2>/dev/null; then + log_info "Gitea's zero-click Authelia login (reverse-proxy auth) is already enabled — skipping." + return 0 + fi + + echo "" + local USE_RP="" + prompt_yn " Skip Gitea's own login entirely for anyone arriving via Authelia — fully transparent, no click, no separate Gitea session to re-expire? Rewires Gitea onto Caddy's internal network (Caddy must be on this same machine). (y/n):" "n" USE_RP + [[ "$USE_RP" =~ ^[Yy]$ ]] || return 0 + + local _subnet + _subnet="$(docker network inspect "${SITE_CADDY_NET:-caddy_net}" \ + --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null)" + if [ -z "$_subnet" ]; then + log_warning "Couldn't determine ${SITE_CADDY_NET:-caddy_net}'s subnet — refusing to enable" + log_warning "reverse-proxy auth without a scoped trust range. An unscoped default lets ANY" + log_warning "client impersonate ANY Gitea user via a spoofed header (this was a real Gitea" + log_warning "CVE — GHSA-f75j-4cw6-rmx4). Skipping." + return 1 + fi + + log_info "Wiring Gitea onto caddy_net and enabling reverse-proxy authentication..." + sed -i "/GITEA__security__INSTALL_LOCK=true/a\\ - GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION=true\\n - GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true\\n - GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=true\\n - GITEA__security__REVERSE_PROXY_AUTHENTICATION_USER=Remote-User\\n - GITEA__security__REVERSE_PROXY_AUTHENTICATION_EMAIL=Remote-Email\\n - GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES=${_subnet}" \ + "$DIR/docker-compose.yml" + cat >> "$DIR/docker-compose.yml" << EOF + networks: + - caddy_net + +networks: + caddy_net: + external: true + name: ${SITE_CADDY_NET:-caddy_net} +EOF + + _gitea_fix_ownership "$DIR" + (cd "$DIR" && docker compose up -d) \ + && log_success "Gitea restarted on caddy_net (trusted range: ${_subnet})." \ + || { log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"; return 1; } + + # Re-point Caddy at the container (gitea:3000, now reachable over + # caddy_net) instead of the host-published port, with the auth gate in + # front. This replaces the plain block set up earlier in this install — + # configure_caddy_for_service's own "already exists — overwrite?" prompt + # covers that; nothing here bypasses it. + configure_caddy_for_service "Gitea" "gitea:3000" "git" " import authelia" + if [ "${CADDY_SERVICE_CONFIGURED:-false}" = true ]; then + log_success "Gitea now signs in transparently via Authelia at https://${CADDY_SERVICE_DOMAIN} — its own login page is still there for anyone reaching it another way." + declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "gitea" "$CADDY_SERVICE_DOMAIN" + else + log_warning "Caddy wasn't reconfigured — env vars are set, but nothing is routing Gitea through Authelia yet." + log_warning "Point Gitea's Caddy entry at gitea:3000 (not the old host.docker.internal upstream) with 'import authelia' in front, or just re-run this offer." + fi +} + # Offers to enable Gitea Actions (Gitea's own CI, largely GitHub-Actions- # workflow-compatible) with a local runner — mainly useful as a fallback so # .gitea/workflows/*.yml can still run something like a GitHub Actions build @@ -445,6 +531,8 @@ install_gitea() { echo "[DRY-RUN] to install a systemd timer for automatic sync, or print manual instructions" echo "[DRY-RUN] Would offer to run a sync now (dry-run preview or for real), off-schedule" echo "[DRY-RUN] Would offer \"Sign in with Authelia\" (OIDC) if Authelia is installed" + echo "[DRY-RUN] Would offer zero-click Authelia login (reverse-proxy auth) if Authelia" + echo "[DRY-RUN] and local Caddy are both installed — rewires Gitea onto caddy_net" echo "[DRY-RUN] Would offer to enable Gitea Actions (CI) with a local act_runner container" echo "[DRY-RUN] Would write $DIR/README.md" return 0 @@ -473,6 +561,7 @@ install_gitea() { || log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs" _gitea_run_sync_direction_step "$DIR" _gitea_offer_authelia_sso "$DIR" + _gitea_offer_reverse_proxy_auth "$DIR" _gitea_offer_actions_runner "$DIR" log_success "Existing .env (tokens) and web/SSH ports were left untouched." return 0 @@ -643,6 +732,7 @@ ENV configure_caddy_for_service "Gitea" "host.docker.internal:${WEB_PORT}" "git" _gitea_offer_authelia_sso "$DIR" + _gitea_offer_reverse_proxy_auth "$DIR" _gitea_offer_actions_runner "$DIR" write_readme "$DIR" << MD @@ -685,6 +775,21 @@ on Gitea's own login page. Local admin login keeps working exactly as before — this is additive, not a replacement. Managed in Gitea under Site Administration -> Authentication Sources (source name: \`authelia\`). +## Zero-click Authelia login (optional, stronger) + +A second, separate Authelia integration: instead of an extra button on +Gitea's login page, Gitea auto-logs in as whoever Authelia says you are — +no click, and no separate Gitea session that can expire on its own and +force a re-login later. Re-run \`sudo ./setup.sh gitea\` (Update mode) and +answer yes to the "Skip Gitea's own login entirely..." prompt. Requires +Authelia and Caddy on this same machine — it moves Gitea onto Caddy's +internal Docker network (\`caddy_net\`) and Gitea only trusts the identity +header from that network's address range, not from the internet or from +its own host-published port. Gitea's own login page keeps working for +anyone who reaches it any other way (e.g. directly on its port). New +users arriving this way get an ordinary (non-admin) Gitea account created +automatically the first time they show up. + ## Gitea Actions (CI) — optional local runner Re-run \`sudo ./setup.sh gitea\` (Update mode is fine) and answer yes to diff --git a/services/uptimekuma.sh b/services/uptimekuma.sh index dac8d28..67f9e08 100644 --- a/services/uptimekuma.sh +++ b/services/uptimekuma.sh @@ -206,6 +206,9 @@ install_uptimekuma() { if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $UPTIME_DIR" echo "[DRY-RUN] Would auto-scan for a free host port" + echo "[DRY-RUN] If Authelia is installed: would offer to protect Uptime Kuma with it —" + echo "[DRY-RUN] sets DISABLE_AUTH=true (Kuma's own login off) only once Caddy's" + echo "[DRY-RUN] 'import authelia' gate is actually confirmed in front of it" return 0 fi @@ -241,6 +244,39 @@ networks: " fi + # Authelia SSO — decided (and, if accepted, wired into Caddy) before + # docker-compose.yml is written, so DISABLE_AUTH only ever gets set once + # Caddy's "import authelia" gate is actually confirmed in front of Kuma. + # Unlike Frigate/Gitea, Uptime Kuma with DISABLE_AUTH=true has NO + # internal check left at all — it's not IP-scoped (Gitea) or secret- + # pinned (Frigate), just fully open to whatever reaches its port, so + # this is the one place getting the ordering wrong is worst: a login- + # disabled Kuma with nothing gating it is wide open to anyone who can + # reach the port, not just spoofable. + local UPTIME_USE_AUTHELIA="n" UPTIME_ENV_BLOCK="" _uptime_caddy_done=false + if [ -d "$DOCKER_DIR/authelia" ]; then + echo "" + prompt_yn "Protect Uptime Kuma with Authelia SSO (disables Kuma's own login entirely)? (y/n):" "y" UPTIME_USE_AUTHELIA + fi + + if [[ "$UPTIME_USE_AUTHELIA" =~ ^[Yy]$ ]]; then + configure_caddy_for_service "Uptime Kuma" "uptime-kuma:3001" "uptime" " import authelia" + if [ "${CADDY_SERVICE_CONFIGURED:-false}" = true ]; then + UPTIME_ENV_BLOCK=" - DISABLE_AUTH=true" + _uptime_caddy_done=true + declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "uptimekuma" "$CADDY_SERVICE_DOMAIN" + else + log_warning "Caddy wasn't configured — leaving Uptime Kuma's own login enabled (nothing else would be gating access)." + fi + fi + + local UPTIME_ENV_SECTION="" + if [ -n "$UPTIME_ENV_BLOCK" ]; then + UPTIME_ENV_SECTION=" environment: +${UPTIME_ENV_BLOCK} +" + fi + cat > docker-compose.yml << UPTIME_COMPOSE name: uptime-kuma @@ -250,7 +286,7 @@ services: container_name: uptime-kuma hostname: uptime-kuma restart: unless-stopped - volumes: +${UPTIME_ENV_SECTION} volumes: - ./data:/app/data - /var/run/docker.sock:/var/run/docker.sock:ro ports: @@ -282,6 +318,15 @@ Docker containers. If Caddy is installed, you can expose this via the prompt during install (see configure_caddy_for_service). Default subdomain: uptime. +## Authelia SSO (optional) +If Authelia is installed, the installer offers to protect Uptime Kuma with +it instead of Kuma's own login — this sets \`DISABLE_AUTH=true\` (Kuma's own +account/login screen goes away entirely) and puts Caddy's \`import authelia\` +gate in front instead, so Authelia is the only thing checking who you are. +This only gets set once Caddy confirms it's actually fronting the domain — +never with nothing else gating access. Re-run \`sudo ./setup.sh uptimekuma\` +to add or change this later. + ## Manage \`\`\` cd $UPTIME_DIR @@ -291,8 +336,11 @@ docker compose logs -f # logs \`\`\` MD - # Configure Caddy reverse proxy before starting - configure_caddy_for_service "Uptime Kuma" "uptime-kuma:3001" "uptime" + # Configure Caddy reverse proxy before starting (skip if the Authelia + # step above already did it) + if [ "$_uptime_caddy_done" != true ]; then + configure_caddy_for_service "Uptime Kuma" "uptime-kuma:3001" "uptime" + fi local START_UPTIME="" prompt_yn "Start Uptime Kuma now? (y/n):" "y" START_UPTIME