Add zero-click Authelia login to Gitea and Uptime Kuma

Follow-up to Frigate's Authelia integration: both of these can also skip
their own login entirely once Authelia is doing the gating, each with a
different trust model appropriate to what the app actually supports.

- gitea: new _gitea_offer_reverse_proxy_auth(), a second Authelia
  integration alongside the existing OIDC "Sign in with Authelia" button.
  Enables Gitea's own ENABLE_REVERSE_PROXY_AUTHENTICATION so it auto-logs
  in from a trusted Remote-User header — no click, no separate Gitea
  session to expire on its own. Trust is IP-range based
  (REVERSE_PROXY_TRUSTED_PROXIES), computed from caddy_net's real subnet
  the same way ufw_allow_from_caddy_net does; refuses to enable the
  feature at all if that can't be determined rather than fall back to a
  permissive default — Gitea's own Docker image has shipped an unscoped
  default before (GHSA-f75j-4cw6-rmx4, any IP could impersonate any user).
  Rewires Gitea onto caddy_net and re-points Caddy at gitea:3000, since it
  previously only reached Caddy via its published host port. Gitea's own
  login stays available as a fallback, so unlike Frigate there's no
  "native login off with nothing gating it" state to guard against.

- uptimekuma: sets DISABLE_AUTH=true only once Caddy's "import authelia"
  gate is confirmed in front of it. Uptime Kuma already joined caddy_net
  unconditionally, so this only needed the env var plus moving the
  Authelia-gated Caddy call earlier (before docker-compose.yml is
  written); the existing unconditional call at the end now only runs as a
  fallback when the Authelia path wasn't used or wasn't completed. Kuma's
  DISABLE_AUTH has no IP-scoping or secret check left once set — the
  strictest of the three to get the ordering right on, since a mistake
  here means wide open, not just spoofable.

Verified with a local test harness (fake Authelia/Caddy/docker-network
state): both the happy path and the "Caddy declined" safety fallback
produce the expected docker-compose.yml/.env/Caddyfile output for each
service, and Gitea's subnet-detection refusal + idempotent-rerun guard
were exercised directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
This commit is contained in:
Claude
2026-08-21 21:07:02 +00:00
parent cd33b7ce71
commit be60f475c3
3 changed files with 206 additions and 3 deletions
+50
View File
@@ -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:
+105
View File
@@ -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
+51 -3
View File
@@ -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