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

Claude/frigate authelia openid 0l1htj
This commit is contained in:
Outis
2026-08-21 17:55:27 -04:00
committed by GitHub
8 changed files with 538 additions and 35 deletions
+96 -5
View File
@@ -191,13 +191,25 @@ pip_user_install PACKAGE... # pip3 --user with --break-system-packages o
### Caddy reverse proxy
```bash
configure_caddy_for_service "Display Name" "PORT" "default-subdomain" ["extra-block"]
configure_caddy_for_service "Display Name" "PORT" "default-subdomain" ["extra-block"] ["reverse_proxy-extra"]
```
Prompts the user for a domain, appends a site block to the Caddyfile, and
reloads Caddy. No-ops silently if Caddy isn't installed. The fourth argument
is an optional string inserted verbatim inside the Caddy site block (use it
for `import authelia` or custom matchers).
is an optional string inserted verbatim inside the Caddy site block, before
`reverse_proxy` (use it for `import authelia` or custom matchers). The fifth
argument is a different thing — an optional string inserted **inside** the
`reverse_proxy` block itself, as sub-directives (e.g.
`" header_up X-Proxy-Secret abc123"`), for a backend that needs a
header only `reverse_proxy`'s own `header_up` can set — the fourth
argument's block runs *before* `reverse_proxy` and can't reach into it.
`services/frigate.sh` is the reference caller: Frigate's `proxy` auth mode
trusts `Remote-User`/`Remote-Groups` headers from Authelia's forward_auth,
but only if a matching `X-Proxy-Secret` header is also present — otherwise
those headers could be spoofed by a request that reaches Frigate's
published host port directly, bypassing Caddy/Authelia entirely. Omit the
fifth argument and the generated `reverse_proxy` line is the same bare form
as before — every other caller is unaffected.
The function places that block **before** `reverse_proxy` in the generated
site block — don't reorder this. `forward_auth` (what `import authelia`
@@ -245,14 +257,19 @@ forward_auth https://auth.example.com {
This only affects the remote-Authelia path — same-machine `authelia:9091`
snippets (`services/authelia.sh`) are a single hop and don't need it.
Sets two out-params (not `local` — read them after the call returns) so the
caller can tell whether Caddy actually ended up fronting the service:
Sets three out-params (not `local` — read them after the call returns) so
the caller can tell whether Caddy actually ended up fronting the service:
```bash
CADDY_SERVICE_CONFIGURED # true/false
CADDY_SERVICE_MODE # "local" or "remote" (only meaningful if configured)
CADDY_SERVICE_DOMAIN # the domain actually configured (only meaningful if configured)
```
`CADDY_SERVICE_DOMAIN` is what `_authelia_scope_access()` (see below) wants
as its `DOMAIN` argument — read it right after the call instead of
recomputing/guessing the domain a second time.
Use this to skip opening a host firewall port for a service Caddy already
fronts *locally* (it reaches the service over `host.docker.internal`, not
the network) — but still open it when `CADDY_SERVICE_MODE` is `"remote"`,
@@ -461,6 +478,80 @@ for Authelia to protect. Removed from this list; if it grows a web UI in
the future, add it back and wire up the same prompt other services here
use.
**`frigate` — a third pattern, neither of the two above.** Frigate *does*
have built-in auth (username/password, `admin`/`viewer` roles, on by
default) so it isn't "no built-in auth" — but unlike the has-built-in-auth
list, that auth is designed to be handed off to an upstream proxy instead
of just living alongside it. Frigate has its own `proxy` auth mode built
specifically for Authelia/Authentik/oauth2_proxy/traefik-forward-auth:
given trusted `Remote-User`/`Remote-Groups` headers it can skip its own
login screen entirely (`auth.enabled: False`), rather than showing a
second, independently-expiring login *after* Authelia's. `services/frigate.sh`
wires this up: `import authelia` (fourth arg) plus a
`header_up X-Proxy-Secret <secret>` (fifth arg, see
`configure_caddy_for_service` above) into the reverse_proxy block, with
the matching `proxy.auth_secret`/`header_map`/`default_role: admin` block
written into `config/config.yml` — and only written at all once
`CADDY_SERVICE_CONFIGURED` confirms Caddy actually ended up fronting the
domain, so Frigate's own login is never disabled with nothing else in
front of it. `default_role: admin` (default in this repo's install) means
anyone who passes Authelia gets full access, same as the login it
replaces; use `proxy.role_map`/Authelia groups instead if some users
should be view-only. Reuses the same `FRIGATE_PROXY_AUTH_SECRET` on
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:
+20 -3
View File
@@ -841,11 +841,19 @@ find_free_coturn_range() {
}
# ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"]
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"] ["reverse_proxy-extra"]
# UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"),
# or plain port number for localhost fallback (e.g. "8085").
# The optional 5th arg is inserted as sub-directives *inside* the
# reverse_proxy block itself (e.g. " header_up X-Proxy-Secret abc123")
# — for the rare case a backend needs a header only reverse_proxy's own
# header_up can set, as opposed to EXTRA_CONFIG's auth-gate directives that
# run before reverse_proxy entirely. See services/frigate.sh's Authelia
# integration for the reference caller (pins X-Proxy-Secret so Frigate's
# proxy-auth trust can't be spoofed by a request that reaches it directly,
# bypassing Caddy/Authelia).
configure_caddy_for_service() {
local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}"
local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}" REVERSE_PROXY_EXTRA="${5:-}"
# Out-params (not `local` — callers read these after the call returns) so
# a caller can tell whether Caddy actually ended up fronting the service
@@ -941,6 +949,15 @@ configure_caddy_for_service() {
_BLOCK_UPSTREAM="${_THIS_IP}:${_DISPLAY_PORT}"
fi
# Bare "reverse_proxy upstream" unless a caller needs sub-directives
# (header_up, etc.) inside it — see the REVERSE_PROXY_EXTRA comment above.
local _REVERSE_PROXY_LINE="reverse_proxy ${_BLOCK_UPSTREAM}"
if [ -n "$REVERSE_PROXY_EXTRA" ]; then
_REVERSE_PROXY_LINE="reverse_proxy ${_BLOCK_UPSTREAM} {
${REVERSE_PROXY_EXTRA}
}"
fi
local _SITE_BLOCK
_SITE_BLOCK="$(cat << CADDY_BLOCK
@@ -954,7 +971,7 @@ ${SERVICE_DOMAIN} {
# after it would be dead code that never runs — full bypass regardless
# of what the auth server's own rules say.
${EXTRA_CONFIG}
reverse_proxy ${_BLOCK_UPSTREAM}
${_REVERSE_PROXY_LINE}
# Security headers
header {
+39 -1
View File
@@ -2241,8 +2241,46 @@ install_asterisk() {
local _EXISTING_DOMAIN _EXISTING_PORT
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
# A domain was set at some point (DOMAIN_NAME in .env) but
# Caddy never ended up with a site block for it — declined
# at install time, DNS wasn't ready yet, or Caddy itself was
# reinstalled/reset since. "update" never re-asks the
# domain/networking/firewall questions (see this branch's
# own comment above), but leaving a configured-but-unwired
# domain broken forever with no way back short of a full
# reinstall (which rotates coturn/TURN credentials — see the
# "fresh" branch's own warning below) defeats the point of
# "update" being the safe, no-side-effects path.
# _asterisk_configure_caddy_public() only ever touches the
# Caddyfile and .env's WEB_ADMIN_AUTH_DISABLED line — never
# coturn, extensions, or anything a full reinstall would put
# at risk — so it's safe to offer here even though nothing
# else in "update" touches Caddy.
local _CADDY_JUST_CONFIGURED=false
if [[ -n "$_EXISTING_DOMAIN" ]] && [[ -d "$DOCKER_DIR/caddy" ]] \
&& ! grep -q "^${_EXISTING_DOMAIN}" "$DOCKER_DIR/caddy/Caddyfile" 2>/dev/null; then
echo ""
log_warning "DOMAIN_NAME (${_EXISTING_DOMAIN}) is set, but Caddy has no site"
log_warning "block for it — nothing is actually serving that domain."
local _FIX_CADDY=""
prompt_yn " Configure Caddy for ${_EXISTING_DOMAIN} now? (y/n):" "y" _FIX_CADDY
if [[ "$_FIX_CADDY" =~ ^[Yy]$ ]]; then
local _CURRENT_PUBLIC_IP=""
_CURRENT_PUBLIC_IP="$(curl -fsS --max-time 2 http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address 2>/dev/null || true)"
[[ -z "$_CURRENT_PUBLIC_IP" ]] && _CURRENT_PUBLIC_IP="$(curl -fsS --max-time 3 https://ifconfig.me 2>/dev/null || true)"
[[ -z "$_CURRENT_PUBLIC_IP" ]] && _CURRENT_PUBLIC_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
_asterisk_configure_caddy_public "$_EXISTING_DOMAIN" "${_EXISTING_PORT:-8081}" "$_CURRENT_PUBLIC_IP"
_CADDY_JUST_CONFIGURED=true
fi
fi
echo ""
log_success "Existing .env, firewall rules, and Caddy/Authelia config were left untouched."
if [[ "$_CADDY_JUST_CONFIGURED" == true ]]; then
log_success "Existing .env and firewall rules were left untouched; Caddy was just configured above."
else
log_success "Existing .env, firewall rules, and Caddy/Authelia config were left untouched."
fi
if [[ -n "$_EXISTING_DOMAIN" ]]; then
echo " Web admin: https://${_EXISTING_DOMAIN}/"
else
+90 -8
View File
@@ -83,7 +83,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
}
configure_caddy_for_service() {
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" _rp_extra="${5:-}"
local _caddy_dir="$DOCKER_DIR/caddy"
local _caddyfile="$_caddy_dir/Caddyfile"
local _display_port="${_upstream##*:}"
@@ -126,12 +126,23 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
fi
local _rp_line="reverse_proxy ${_block_upstream}"
if [[ -n "$_rp_extra" ]]; then
_rp_line="reverse_proxy ${_block_upstream} {
${_rp_extra}
}"
fi
local _site_block
_site_block="$(cat << CBLOCK
# $_name
${_domain} {
reverse_proxy ${_block_upstream}
# Auth (if any) must come before reverse_proxy — see lib/common.sh's
# configure_caddy_for_service for why (reverse_proxy first would answer
# every request itself, making an auth block after it dead code).
${_extra}
${_rp_line}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
@@ -144,7 +155,6 @@ ${_domain} {
output file /var/log/caddy/${_domain}.log
format json
}
${_extra}
}
CBLOCK
)"
@@ -526,6 +536,10 @@ install_frigate() {
echo " - Prompt to add cameras interactively (RTSP creds go in .env)"
echo " or write a starter config.yml if none are added"
echo " - Offer a Caddy reverse proxy and to start the container"
echo " - If Authelia is installed: offer to protect Frigate with it —"
echo " disables Frigate's own login (auth.enabled: False) and pins a"
echo " proxy.auth_secret/X-Proxy-Secret handshake so only Caddy can"
echo " satisfy Frigate's proxy-auth trust"
return 0
fi
@@ -646,6 +660,51 @@ FRIGATE_COMPOSE
mkdir -p config
mkdir -p "$FRIGATE_MEDIA"
# Authelia SSO — decided (and, if accepted, wired into Caddy) before
# config.yml is written, so the auth block baked into config.yml only
# ever reflects a gate that's actually in place (never "native login
# disabled, but nothing put in front of it instead"). Frigate has its
# own built-in login (username/password) separate from Authelia's —
# left alone it would show *after* Authelia's forward_auth already
# gated the domain: a redundant second login, and worse, a second
# session that can expire independently and force a re-login on its
# own schedule regardless of Authelia's "remember me" duration. The
# proxy.auth_secret/X-Proxy-Secret handshake (pinned into the Caddy
# reverse_proxy block) stops that trust from being spoofed by a
# request that reaches Frigate's published host port directly,
# bypassing Caddy/Authelia entirely.
local FRIGATE_USE_AUTHELIA="n" FRIGATE_PROXY_SECRET="" AUTH_CONFIG_BLOCK=""
if [ -d "$DOCKER_DIR/authelia" ]; then
echo ""
prompt_yn "Protect Frigate with Authelia SSO (disables Frigate's own login)? (y/n):" "y" FRIGATE_USE_AUTHELIA
fi
if [[ "$FRIGATE_USE_AUTHELIA" =~ ^[Yy]$ ]]; then
FRIGATE_PROXY_SECRET="${ENV_MAP[FRIGATE_PROXY_AUTH_SECRET]:-$(generate_password 32)}"
configure_caddy_for_service "Frigate" "frigate:5000" "frigate" \
" import authelia" \
" header_up X-Proxy-Secret ${FRIGATE_PROXY_SECRET}"
if [ "${CADDY_SERVICE_CONFIGURED:-false}" = true ]; then
AUTH_CONFIG_BLOCK="auth:
enabled: False # Authelia already gates the whole domain — its own login would be redundant
proxy:
auth_secret: \"{FRIGATE_PROXY_AUTH_SECRET}\" # must match the X-Proxy-Secret header Caddy sends
header_map:
user: remote-user
role: remote-groups
default_role: admin # anyone who passes Authelia gets full access, same as the disabled local login did
"
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "frigate" "$CADDY_SERVICE_DOMAIN"
else
log_warning "Caddy wasn't configured for Frigate — leaving Frigate's own login enabled (nothing else is gating access)."
FRIGATE_PROXY_SECRET=""
fi
else
configure_caddy_for_service "Frigate" "frigate:5000" "frigate"
fi
# Credentials/IPs go in .env as FRIGATE_* variables; Frigate substitutes
# any {FRIGATE_VAR} placeholder in config.yml from its container env at
# startup, so RTSP secrets never need to be typed into the YAML directly.
@@ -654,12 +713,12 @@ FRIGATE_COMPOSE
if [ "${#CAM_NAME[@]}" -eq 0 ]; then
# No cameras entered — write a starter config the operator edits by hand.
cat > config/config.yml << 'FRIGATE_CONFIG'
cat > config/config.yml << FRIGATE_CONFIG
# Frigate Configuration — Docs: https://docs.frigate.video
#
# ⚠️ YOU MUST EDIT THIS FILE to add your cameras before starting Frigate.
mqtt:
${AUTH_CONFIG_BLOCK}mqtt:
enabled: false # Set to true and configure if you use Home Assistant
cameras:
@@ -696,7 +755,7 @@ FRIGATE_CONFIG
# RTSP credentials/IPs come from .env — Frigate substitutes {FRIGATE_VAR}
# placeholders below from the container's environment at startup.
mqtt:
${AUTH_CONFIG_BLOCK}mqtt:
enabled: false # Set to true and configure if you use Home Assistant
go2rtc:
@@ -724,6 +783,7 @@ FRIGATE_CONFIG
cat > .env << FRIGATE_ENV
FRIGATE_MEDIA=$FRIGATE_MEDIA
CADDY_NET=$SITE_CADDY_NET
FRIGATE_PROXY_AUTH_SECRET=$FRIGATE_PROXY_SECRET
${ENV_CAM_VARS}
FRIGATE_ENV
chmod 600 .env
@@ -732,7 +792,29 @@ FRIGATE_ENV
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
log_success "Frigate configured at $FRIGATE_DIR"
configure_caddy_for_service "Frigate" "frigate:5000" "frigate"
local AUTH_README_SECTION=""
if [ -n "$AUTH_CONFIG_BLOCK" ]; then
AUTH_README_SECTION="
## Authelia SSO
Frigate's own login is disabled (\`auth.enabled: False\` in
\`config/config.yml\`) — Authelia gates the whole domain instead via Caddy's
\`import authelia\` plus a \`proxy.auth_secret\`/\`X-Proxy-Secret\` handshake
(the secret lives in \`.env\` as \`FRIGATE_PROXY_AUTH_SECRET\`) so that trust
can't be spoofed by a request that reaches Frigate's published port
directly, bypassing Caddy.
Everyone who passes Authelia gets full (admin) access to Frigate —
adjust \`config/config.yml\`'s \`proxy.role_map\`/\`default_role\` plus
Authelia's own group assignments if you want to give some users
view-only access instead.
To stop Authelia asking for a login again on repeat visits (e.g. from a
phone) for as long as possible, increase its \"remember me\" session
duration: \`sudo ./setup.sh authelia\` → \"Change 'remember me' session
duration\" (this affects every domain that instance protects, not just
Frigate).
"
fi
write_readme "$FRIGATE_DIR" << MD
# Frigate NVR
@@ -746,7 +828,7 @@ security cameras. Detects people, cars, animals, and more.
- Recordings: \`$FRIGATE_MEDIA\`
- Config: \`config/config.yml\` — cameras configured during install (${#CAM_NAME[@]} total)
- Credentials: \`.env\` — RTSP user/pass/IP per camera as FRIGATE_* variables
${AUTH_README_SECTION}
## Manage
\`\`\`bash
cd $FRIGATE_DIR
+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
+84 -1
View File
@@ -1956,6 +1956,79 @@ def ea_reload_voicemail():
run_sudo(["docker", "exec", ASTERISK_EA_CONTAINER, "asterisk", "-rx", "module reload app_voicemail.so"])
def _ea_endpoint_stanza_bounds(lines, ext):
"""Line-index range (start, end-exclusive) of the `[ext]\\ntype=endpoint`
PJSIP stanza for one extension, or None if not found. pjsip.conf reuses
the same [ext] bracket name for three separate stanzas per device
(type=endpoint, type=auth, type=aor — see easy-asterisk-v0.10.0.sh's
add_device()), so matching on the bracket alone would land in the wrong
one; this only matches the occurrence immediately followed by
"type=endpoint", bounded by the next blank line or next [section] the
same way lib/common.sh's _remove_caddy_site_block is bounded for Caddy
blocks — never an unbounded scan past this one device's own stanza."""
target = "[%s]" % ext
i, n = 0, len(lines)
while i < n:
if lines[i].strip() == target and i + 1 < n and lines[i + 1].strip() == "type=endpoint":
j = i + 1
while j < n and lines[j].strip() != "" and not lines[j].strip().startswith("["):
j += 1
return i, j
i += 1
return None
def _ea_set_endpoint_mailboxes(ext, enabled):
"""Adds/updates (enabled) or removes (disabled) the extension's PJSIP
`mailboxes=` line, so a phone can actually SUBSCRIBE for MWI (the "new
voicemail" notice) on this extension.
Confirmed live: nothing anywhere in this repo or the vendored
easy-asterisk script ever sets this. add_device()'s own device_config
template (easy-asterisk-v0.10.0.sh) never writes it, and until this,
write_voicemail() below only ever touched voicemail.conf — so recording
a voicemail worked fine (voicemail.conf + the dialplan's VoiceMail()
call), but no phone ever actually subscribed to be told about it,
regardless of whether the voicemail flag was on. `mailboxes=<ext>@default`
matches the "default" context name voicemail.conf's [default] section
uses (see _asterisk_write_voicemail_conf in services/asterisk.sh)
same context, just referenced from the endpoint side instead of the
dialplan side."""
path = _ea_pjsip_host_path()
if not path or not os.path.isfile(path):
return False, "No pjsip.conf found"
with open(path) as f:
lines = f.readlines()
bounds = _ea_endpoint_stanza_bounds(lines, ext)
if not bounds:
return False, "No PJSIP endpoint found for extension %s" % ext
start, end = bounds
existing_idx = None
for k in range(start, end):
if lines[k].lstrip().startswith("mailboxes="):
existing_idx = k
break
if enabled:
mailbox_line = "mailboxes=%s@default\n" % ext
if existing_idx is not None:
lines[existing_idx] = mailbox_line
else:
lines.insert(end, mailbox_line)
elif existing_idx is not None:
del lines[existing_idx]
else:
return True, ""
ok, err = ea_docker_write(EA_PJSIP_CONTAINER_PATH, "".join(lines))
if not ok:
return False, err
ea_reload_pjsip()
return True, ""
def write_voicemail(ext, enabled):
"""Sets/clears the voicemail flag for one extension, then regenerates
voicemail.conf and reloads app_voicemail so the change takes effect
@@ -1968,7 +2041,12 @@ def write_voicemail(ext, enabled):
pstn-permissions.conf even after disabling — toggling it off and back on
later reuses the same PIN instead of silently changing it on the user.
Independent of pstn_installed() the same way messaging is: voicemail has
no PSTN/trunk dependency."""
no PSTN/trunk dependency.
Also wires up (or tears down) MWI via _ea_set_endpoint_mailboxes() — the
extension's PJSIP endpoint needs its own `mailboxes=` line for a phone
to ever be told about a new voicemail; voicemail.conf alone is only
enough for the recording itself, not the notification."""
if not ASTERISK_CONFIG_DIR:
return False, "No Asterisk install detected on this box"
ext = str(ext).strip()
@@ -1990,6 +2068,11 @@ def write_voicemail(ext, enabled):
return True, "Saved, but voicemail.conf couldn't be regenerated: %s" % err
ea_reload_voicemail()
mok, merr = _ea_set_endpoint_mailboxes(ext, enabled)
if not mok:
return True, "Saved, but couldn't wire up the phone's voicemail notification (MWI): %s" % merr
return True, "Saved"
+53 -14
View File
@@ -629,17 +629,11 @@ CBLOCK
_sms_write_readme() {
local _url="$1" _relay_domain="$2"
write_readme "$SMS_APP_DIR" << MD
# Inbound SMS → Sipnetic (via AMI)
Gets SMS sent to one of your PSTN DIDs delivered into Asterisk as a SIP
MESSAGE, landing in Sipnetic the same way internal texting already does —
not a push notification, a real message in the softphone.
## The URL to paste into your DID provider
In the provider portal, open the DID's SMS settings and paste this into the
"Forward to URL" field (on Anveo: Phone Numbers → the DID → SMS tab, tick
local _url_section
if [ -n "$_url" ]; then
_url_section="In the provider portal, open the DID's SMS settings and paste this into the
\"Forward to URL\" field (on Anveo: Phone Numbers → the DID → SMS tab, tick
the checkbox, paste, press SAVE — RETURN discards):
\`\`\`
@@ -652,7 +646,21 @@ query parameters; with the message last, everything after it can be read back
verbatim.
Treat this URL like a password — anyone holding it can trigger a message
delivery into your Asterisk.
delivery into your Asterisk."
else
_url_section="**Not set up yet — no public domain was entered.** Re-run \`sudo ./setup.sh sms-inbound\` and choose \"Full reinstall\" once DNS for the webhook's domain points at this box; nothing here works until then."
fi
write_readme "$SMS_APP_DIR" << MD
# Inbound SMS → Sipnetic (via AMI)
Gets SMS sent to one of your PSTN DIDs delivered into Asterisk as a SIP
MESSAGE, landing in Sipnetic the same way internal texting already does —
not a push notification, a real message in the softphone.
## The URL to paste into your DID provider
${_url_section}
## How delivery is decided
@@ -770,8 +778,24 @@ install_sms-inbound() {
&& log_success "Relay refreshed and restarted." \
|| log_warning "Restart failed — check: journalctl -u sms-inbound -n 50"
echo ""
log_success "Settings, Caddy and firewall rules were left untouched."
echo " Provider URL: ${SMS_FORWARD_URL}"
# A missing/placeholder domain here means an earlier run was
# left with no real webhook URL (RELAY_DOMAIN entered blank,
# or DNS wasn't ready yet) — "update" mode never re-prompts
# for the domain (by design, same as every other service's
# non-destructive update path), so silently repeating that
# broken URL forever, looking like nothing is wrong, is worse
# than saying so plainly. Confirmed live: this is exactly
# what a DID provider like Anveo rejects — "<your-domain>"
# isn't a resolvable hostname.
if [[ -z "${SMS_RELAY_DOMAIN:-}" || "${SMS_FORWARD_URL:-}" == *"<your-domain>"* ]]; then
log_warning "No real webhook domain was ever set for this install — the stored"
log_warning "provider URL is a placeholder, not something a DID provider can use."
log_warning "Re-run 'sudo ./setup.sh sms-inbound' and choose \"2) Full reinstall\""
log_warning "to be asked for the domain again (needs DNS pointed at this box first)."
else
log_success "Settings, Caddy and firewall rules were left untouched."
echo " Provider URL: ${SMS_FORWARD_URL}"
fi
echo ""
return 0
;;
@@ -898,7 +922,14 @@ install_sms-inbound() {
ensure_ufw_enabled
fi
local FORWARD_URL="https://${RELAY_DOMAIN:-<your-domain>}/sms/${RELAY_TOKEN}?from=\$[from]\$&to=\$[to]\$&message=\$[message]\$"
# Empty (not a "<your-domain>" placeholder) when no domain was entered —
# a placeholder here used to get persisted to settings.env and silently
# re-served as-is on every later "update" run (which never re-prompts
# for the domain, by design), looking like a valid webhook URL right up
# until a DID provider like Anveo rejected it as an unresolvable host.
# Confirmed live.
local FORWARD_URL=""
[ -n "$RELAY_DOMAIN" ] && FORWARD_URL="https://${RELAY_DOMAIN}/sms/${RELAY_TOKEN}?from=\$[from]\$&to=\$[to]\$&message=\$[message]\$"
# ── Persist settings ──────────────────────────────────────────────────────
# Single-quoted values: this file gets `source`d again on the next
@@ -929,6 +960,14 @@ ENV
# ── Summary ───────────────────────────────────────────────────────────────
echo ""
if [ -z "$FORWARD_URL" ]; then
log_warning "Inbound SMS relay is running, but nothing can reach it yet — no domain was entered."
log_warning "Point an A record at this box, then re-run 'sudo ./setup.sh sms-inbound' and"
log_warning "choose \"2) Full reinstall\" to be asked for the domain again and get a real"
log_warning "\"Forward to URL\" to paste into your DID provider."
echo ""
return 0
fi
log_success "Inbound SMS → Sipnetic configured."
echo ""
echo " 1. In your DID provider's portal, open the number's SMS settings and"
+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