refactor: decommission secubox-authelia (SSO IdP) — remove package, keep gate authelia-free

Authelia (secubox-authelia, #239) was a failed/half-baked SSO PoC. Its partial
teardown also masked then unmasked the login-loop bug (hub-proxied /auth/login
never recorded sessions). Removing it for good:

- delete packages/secubox-authelia (package, service, nginx confs, LXC installer)
- move the durable pieces into secubox-hub (always installed):
  - nginx/secubox-lan-geo.conf  → /etc/nginx/conf.d/        (defines $lan_client)
  - nginx/zz-sbx-authgate.conf  → secubox.d/ + secubox-routes.d/
    keeps /__sbx_auth_verify + @sbx_auth_login defined WITHOUT Authelia:
    LAN clients pass (204), everything else denied (403 / default-deny).
    SSO-gated vhosts (lyrion, yacy, grafana, rustdesk, fmrelay) thus become
    LAN-only instead of losing their gate.
- drop secubox-authelia from image/build-live-usb.sh module list

No hard Depends referenced authelia (only description prose in
users/nextcloud/identity), so nothing else breaks. Live: purged on gk2 + c3box,
gate stubbed, lan-geo restored; verified lyrion/yacy/metablogizer still serve.
This commit is contained in:
CyberMind-FR 2026-07-02 08:14:08 +02:00
parent 8adac26e55
commit 6476897873
21 changed files with 45 additions and 1646 deletions

View File

@ -2778,7 +2778,6 @@ INCOMPLETE_MODULES=(
secubox-yacy
secubox-rustdesk
secubox-lyrion
secubox-authelia
secubox-mail
secubox-gitea
secubox-matrix

View File

@ -1,40 +0,0 @@
# 🔐 Authelia SSO
Single sign-on identity provider (AUTH-BRIDGE)
**Category:** Access
## Screenshot
![Authelia SSO](../../docs/screenshots/vm/authelia.png)
## Features
- SSO
- 2FA / TOTP
- Access policies
- LDAP / file backend
## Installation
```bash
# Add SecuBox repository
curl -fsSL https://apt.secubox.in/install.sh | sudo bash
# Install package
sudo apt install secubox-authelia
```
## Configuration
Configuration file: `/etc/secubox/authelia.toml`
## API Endpoints
- `GET /api/v1/authelia/status` - Module status
- `GET /api/v1/authelia/health` - Health check
## License
LicenseRef-CMSD-1.0 (Source-Disclosed License) — CyberMind © 2024-2026.
See [LICENCE-CMSD-1.0.md](../../LICENCE-CMSD-1.0.md).

View File

@ -1,165 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# See LICENCE-CMSD-1.0.md for terms.
"""
SecuBox-Deb :: secubox-authelia host control plane API.
FastAPI on /run/secubox/authelia.sock, proxied by nginx at /api/v1/authelia/.
Mandatory endpoints per docs/MODULE-GUIDELINES.md §8.
Plus the `verify` endpoint used as the nginx `auth_request` target for the
SSO-less backends (yacy / rustdesk-web / mitmproxy-web): see #239 SSO bridge
spec.
"""
from __future__ import annotations
import json
import os
import shutil
import socket
import subprocess
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Dict
from fastapi import FastAPI, HTTPException, Header, Request, Response
VERSION = "1.0.8"
CTL = shutil.which("autheliactl") or "/usr/sbin/autheliactl"
# Authelia LXC (provisioned by install-lxc.sh at 10.100.0.20:9091). Override
# via SECUBOX_AUTHELIA_URL if the LXC IP/port differs (e.g. for tests).
AUTHELIA_URL = os.environ.get("SECUBOX_AUTHELIA_URL", "http://10.100.0.20:9091")
# Authelia headers worth forwarding back to nginx (and through to the
# protected backend). nginx auth_request can capture these via
# `auth_request_set $sbx_user $upstream_http_remote_user;` etc.
_AUTH_REMOTE_HEADERS = (
"Remote-User", "Remote-Groups", "Remote-Email", "Remote-Name",
)
app = FastAPI(
title="SecuBox Authelia",
version=VERSION,
docs_url=None,
redoc_url=None,
openapi_url=None,
)
def _ctl_json(*args: str) -> Dict[str, Any]:
cmd = [CTL, *args, "--json"]
try:
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=15)
except subprocess.CalledProcessError as e:
raise HTTPException(status_code=500, detail=f"autheliactl failed: {e.output!r}")
except FileNotFoundError:
raise HTTPException(status_code=500, detail=f"autheliactl not found at {CTL}")
try:
return json.loads(out.decode())
except json.JSONDecodeError as e:
raise HTTPException(status_code=500, detail=f"autheliactl emitted non-JSON: {e}; raw={out!r}")
@app.get("/healthz")
def healthz() -> Dict[str, bool]:
return {"ok": True}
@app.get("/version")
def version() -> Dict[str, str]:
build_file = Path("/usr/share/doc/secubox-authelia/.build-sha")
build = build_file.read_text().strip() if build_file.is_file() else "unknown"
return {"version": VERSION, "build": build}
@app.get("/status")
def status() -> Dict[str, Any]:
return _ctl_json("status")
@app.get("/components")
def components() -> Dict[str, Any]:
return _ctl_json("components")
@app.get("/access")
def access() -> Dict[str, Any]:
return _ctl_json("access")
@app.get("/verify")
@app.head("/verify")
def verify(
request: Request,
response: Response,
authorization: str | None = Header(default=None),
cookie: str | None = Header(default=None),
) -> Response:
"""
nginx `auth_request` target.
Reverse-proxies to Authelia's native /api/verify which:
- validates the `authelia_session` cookie against the session store
- returns 200 + Remote-User / Remote-Groups / Remote-Email headers
when the session is valid
- returns 401 otherwise
We propagate those Remote-* headers back to nginx so the protected
backend's `proxy_set_header` can capture them via
`auth_request_set $sbx_user $upstream_http_remote_user;` (etc).
"""
# Pass through Cookie + Authorization headers to Authelia. nginx already
# stripped the request body (proxy_pass_request_body off) so we're just
# forwarding metadata.
# Authelia 4.39+ is path-prefixed at /auth (server.address). The verify
# endpoint is therefore at /auth/api/verify (both /api/verify legacy
# and the prefixed path respond, but prefix is the canonical one).
upstream = f"{AUTHELIA_URL.rstrip('/')}/auth/api/verify"
headers = {}
if cookie:
headers["Cookie"] = cookie
if authorization:
headers["Authorization"] = authorization
# Authelia 4.39+ needs the original URL/host so it can (a) match the
# right session.cookies[].domain entry — multi-cookie deployments have
# several — and (b) apply access_control rules. nginx sets these in
# `location = /__sbx_auth_verify`; we forward verbatim. Without them
# Authelia falls back to the first cookies[] entry, doesn't find the
# session, returns 401 → infinite redirect loop.
for h in (
"X-Original-URL",
"X-Forwarded-Method",
"X-Forwarded-Proto",
"X-Forwarded-Host",
"X-Forwarded-Uri",
"X-Forwarded-For",
):
v = request.headers.get(h)
if v is not None:
headers[h] = v
req = urllib.request.Request(upstream, method="GET", headers=headers)
try:
with urllib.request.urlopen(req, timeout=3) as r:
status_code = r.status
for h in _AUTH_REMOTE_HEADERS:
v = r.headers.get(h)
if v is not None:
response.headers[h] = v
except urllib.error.HTTPError as e:
# Authelia returned a non-2xx (typically 401). Propagate the code so
# nginx's `error_page 401 = @sbx_auth_login;` can fire.
return Response(status_code=e.code)
except (urllib.error.URLError, socket.timeout, ConnectionError, OSError) as e:
# Authelia LXC unreachable — fail closed (deny by default).
raise HTTPException(
status_code=503,
detail=f"authelia upstream unreachable: {e!r}",
)
# 2xx from Authelia → authenticated. Return 200 (with the Remote-* headers
# already written into `response`).
response.status_code = 200 if status_code < 300 else status_code
return response

View File

@ -1,61 +0,0 @@
# /etc/secubox/authelia.toml — SecuBox Authelia (SSO IdP) configuration
[lxc]
name = "authelia"
ip = "10.100.0.20"
gateway = "10.100.0.1"
bridge = "br-lxc"
path = "/data/lxc"
debian_suite = "bookworm"
[authelia]
# Authelia binary version + checksum (verified at install).
version = "4.39.5"
# Internal HTTP port (LXC-side). nginx proxies /auth/ to this port.
http_port = 9091
# JWT signing secret + storage encryption key are auto-generated on first
# install and written to /etc/secubox/secrets/{authelia-jwt,authelia-store}.
[users]
# Use the SecuBox canonical user store. argon2id schema already matches
# Authelia's file backend ($argon2id$v=19$...).
backend = "file"
file_path = "/etc/secubox/users.json"
[session]
domain = "maegia.tv"
expiration = "1h"
inactivity = "5m"
[totp]
# 2FA TOTP enrollment per user. Disabled by default — wizard prompts to
# enable for the bootstrap admin.
enabled = false
[exposure]
# Public hostname for the Authelia login portal. Used by the SecuBox
# admin webui's "Open SSO Portal" button + the /access endpoint.
public_hostname = "sso.gk2.secubox.in"
waf_inspect = true # route through mitmproxy (canonical maegia.tv pattern)
[oidc]
# OIDC clients pre-provisioned by the wizard. Empty fields are filled
# interactively (client_secret generated, redirect_uri prompted).
[oidc.clients.grafana]
enabled = true
client_id = "secubox-grafana"
redirect_uris = ["https://grafana.maegia.tv/login/generic_oauth"]
scopes = ["openid", "profile", "email", "groups"]
[oidc.clients.gitea]
enabled = true
client_id = "secubox-gitea"
redirect_uris = ["https://gitea.gk2.secubox.in/user/oauth2/secubox/callback"]
scopes = ["openid", "profile", "email"]
[oidc.clients.nextcloud]
enabled = false
client_id = "secubox-nextcloud"
redirect_uris = ["https://nextcloud.maegia.tv/apps/user_oidc/code"]
scopes = ["openid", "profile", "email"]

View File

@ -1,202 +0,0 @@
secubox-authelia (1.0.10-1~bookworm1) bookworm; urgency=medium
* Remove Authelia SSO entirely.
- nginx/authelia.conf: reduced to a permissive no-op gate. The
/__sbx_auth_verify auth_request endpoint now returns 200 for every
request; @sbx_auth_login is a harmless fallback to the app root.
Retained only because grafana/lyrion/yacy/rustdesk/fmrelay/zigbee/
nextcloud vhosts still reference these two named locations — without
them nginx fails to load. No SSO portal, no session check, no
Authelia socket dependency. Fixes the dead-portal 302 that produced
a password prompt on LAN clients.
- debian/postinst: stop enabling the FastAPI daemon; disable + mask
secubox-authelia.service so it cannot be resurrected.
- Apps keep their own native auth; LAN/exposure boundaries are
enforced by HAProxy + the WAF, not by this layer.
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 10:00:00 +0200
secubox-authelia (1.0.9-1~bookworm1) bookworm; urgency=medium
* Split SSO portal from operator dashboard (#310):
- nginx/authelia.conf: /auth/ on the canonical hub vhost is now a
static alias to /usr/share/secubox/www/authelia/ (control + status
dashboard). Used to reverse-proxy the Authelia LXC portal.
- nginx/authelia.conf: @sbx_auth_login redirects to
https://sso.gk2.secubox.in/?rd=… (was https://$host/auth/?rd=…).
- nginx/authelia-vhost.conf: new public vhost sso.gk2.secubox.in,
reverse-proxies / → /auth/ → Authelia LXC 10.100.0.20:9091. Same
pattern as the existing auth.maegia.tv block, on the
.gk2.secubox.in cookie scope.
- www/authelia/index.html: rewrite as the SecuBox AUTH config
module — service state, version, user count, cookie scopes,
access rules, components / status / access cards. Charter AUTH
palette (#C04E24 orange). Was a Lyrion copy-paste stub.
- lib/authelia/install-lxc.sh: session.cookies[] for the hub
domain now points authelia_url at sso.${SECUBOX_HUB_DOMAIN}.
-- Gerald Kerma <devel@cybermind.fr> Thu, 21 May 2026 12:30:00 +0000
secubox-authelia (1.0.8-1~bookworm1) bookworm; urgency=medium
* nginx/authelia.conf: own the `@sbx_auth_login` named location.
Previously defined in secubox-zigbee, meaning lyrion (and any other
SSO-gated app) silently broke if zigbee was uninstalled. The handler
logically belongs to the SSO module — moved verbatim from
zigbee.conf v2.4.5 (no functional change; named-location lookup
happens within the same canonical hub server block via
`include /etc/nginx/secubox.d/*`).
* Closes #278.
-- Gerald Kerma <devel@cybermind.fr> Thu, 21 May 2026 07:30:00 +0000
secubox-authelia (1.0.7-1~bookworm1) bookworm; urgency=medium
* nginx/authelia.conf (`location = /__sbx_auth_verify`): forward
X-Original-URL + X-Forwarded-{Method,Proto,Host,Uri,For} to the
FastAPI /verify socket. Authelia's /api/verify needs the original
URL to (a) match the right session.cookies[].domain entry —
multi-cookie deployments have several — and (b) apply access_control
rules. Without these headers Authelia defaults to the first cookies[]
entry (maegia.tv), the gk2.secubox.in session isn't found, /verify
returns 401 even after successful login → infinite redirect loop.
* api/main.py (`/verify`): forward the same X-Original-URL +
X-Forwarded-* headers verbatim to the Authelia LXC upstream.
* Closes #274.
-- Gerald Kerma <devel@cybermind.fr> Wed, 20 May 2026 19:00:00 +0000
secubox-authelia (1.0.6-1~bookworm1) bookworm; urgency=medium
* lib/authelia/install-lxc.sh: render session.cookies[] with TWO entries
— one per top-level domain where the SSO portal is consumed:
`maegia.tv` (auth.maegia.tv) and `${SECUBOX_HUB_DOMAIN}` (default:
gk2.secubox.in, override via env). Without the second entry, Authelia
rejects /auth/api/state with 403 ERR_BAD_REQUEST when the SPA is
loaded on admin.gk2.secubox.in/auth/ because no cookies[].domain
matched the request Host. Symptom in browser: AxiosError 403 inside
index.Bzd5GHAP.js after the page paints.
* lib/authelia/install-lxc.sh: add matching access_control rules
for `${SECUBOX_HUB_DOMAIN}` and `*.${SECUBOX_HUB_DOMAIN}`
(one_factor) so nginx auth_request → /auth/api/verify resolves
cleanly for the canonical hub vhost and every sibling app vhost
(lyrion., zigbee., grafana., etc.).
* lib/authelia/install-lxc.sh: new readonly SECUBOX_HUB_DOMAIN env
knob (default gk2.secubox.in) — operators on other hubs override
it before running `autheliactl install`.
* Closes #272.
-- Gerald Kerma <devel@cybermind.fr> Wed, 20 May 2026 18:00:00 +0000
secubox-authelia (1.0.5-1~bookworm1) bookworm; urgency=medium
* nginx/authelia.conf: add `sub_filter "__SBX_BANNER_OFF" "";` inside
the /auth/ location to suppress the inherited server-level banner
injection from webui.conf. Per nginx docs, sub_filter inherits
from the parent server block when no child-level directive exists;
just deleting our location-level sub_filter in v1.0.3 left the
server-level injection still active. Authelia's strict CSP
(default-src 'self'; style-src 'self' 'nonce-...'; base-uri 'self')
then rejected the injected script-src + caused
base-uri / style-src-elem violations + an AxiosError 403 because
the SPA's API calls couldn't fire.
The never-match pattern replaces the inherited sub_filter with a
noop → response passes through unmodified.
* Closes #270.
-- Gerald KERMA <devel@cybermind.fr> Thu, 21 May 2026 04:10:00 +0200
secubox-authelia (1.0.4-1~bookworm1) bookworm; urgency=medium
* nginx/authelia.conf + nginx/authelia-vhost.conf: hardcode
X-Forwarded-Proto: https on the proxy to the Authelia LXC.
v1.0.3 used `$scheme` (nginx's internal scheme), which is `http`
because TLS terminates at HAProxy. Authelia then built its
post-login redirect URL as `http://admin.gk2.secubox.in:9080/...`
(nginx's listen port). Browser saw `https://...:9080/...`, tried
a TLS handshake on the plain-HTTP port, and bailed with
SSL_ERROR_RX_RECORD_TOO_LONG.
HAProxy is the only TLS terminator on the path; the user-facing
scheme is always https — hardcoding it is correct.
* Closes #268.
-- Gerald KERMA <devel@cybermind.fr> Thu, 21 May 2026 03:55:00 +0200
secubox-authelia (1.0.3-1~bookworm1) bookworm; urgency=medium
* install-lxc.sh: set Authelia server.address path prefix /auth
(NO trailing slash — 4.39+ validator rejects it). Without this,
the React SPA's API calls 404'd → browser AxiosError ERR_BAD_REQUEST.
* nginx/authelia.conf: drop trailing / on proxy_pass to preserve the
/auth/ prefix expected by Authelia 4.39+. Remove the health-banner
sub_filter — Authelia's strict CSP (default-src 'self';
style-src 'self' 'nonce-...'; base-uri 'self') rejects the injection.
* nginx/authelia-vhost.conf (auth.maegia.tv): same proxy_pass /
banner fixes + add `location = /` → 302 to /auth/ so the portal
URL is consistent across canonical hub + public vhost.
* api/main.py: /verify now reverse-proxies to /auth/api/verify
(matches the path-prefixed Authelia layout).
* Closes #266 (authelia portion).
-- Gerald KERMA <devel@cybermind.fr> Thu, 21 May 2026 03:30:00 +0200
secubox-authelia (1.0.2-1~bookworm1) bookworm; urgency=medium
* api/main.py: implement /verify properly. v1.0.1 returned HTTP 501
(stub) which blocked SSO-style auth_request gating on sibling
modules — see #259 (zigbee v2.4.1) and #261 (lyrion v1.0.4) which
both fell back to LAN-only allow/deny for this exact reason.
.
The /verify handler now reverse-proxies to Authelia's native
/api/verify endpoint (10.100.0.20:9091), which:
- validates the authelia_session cookie against the session store
- returns 200 + Remote-User / Remote-Groups / Remote-Email headers
when the session is valid
- returns 401 otherwise
.
The Remote-* headers are propagated back to nginx so protected
backends can capture them via
auth_request_set $sbx_user $upstream_http_remote_user;
and feed them into proxy_set_header X-Forwarded-User $sbx_user;
.
Fail-closed: if the Authelia LXC is unreachable, /verify returns
503 rather than 200 (nginx auth_request treats anything ≥500 as
deny, matching the LAN-only fallback semantics).
.
After this lands, sibling modules can swap their LAN-only
allow/deny for:
auth_request /__sbx_auth_verify;
error_page 401 = @sbx_auth_login;
See follow-up PRs to be filed against #259 / #261.
* Closes #262
-- Gerald KERMA <devel@cybermind.fr> Thu, 21 May 2026 02:30:00 +0200
secubox-authelia (1.0.1-1~bookworm1) bookworm; urgency=medium
* sbin/autheliactl: detect `authelia` daemon (not the relic
`authelia-server` from the grafana sed-copy).
* lib/authelia/install-lxc.sh: render configuration.yml HOST-side with
proper variable substitution, then pipe via lxc-attach stdin (the
previous `env VAR=val sh -c 'cat ... <<CONF'` left ${VARS} literal).
* Authelia 4.39+ schema migration: server.address replaces server.host/
port; identity_validation.reset_password.jwt_secret replaces top-level
jwt_secret; session.cookies[] replaces session.{name,domain}.
* OIDC identity_providers block disabled in v1.0.0 (requires jwks RSA
key pair + non-empty clients list — both planned for v1.1.0 wizard).
-- Gerald KERMA <devel@cybermind.fr> Wed, 20 May 2026 16:00:00 +0200
secubox-authelia (1.0.0-1~bookworm1) bookworm; urgency=medium
* Initial release: Authelia SSO IdP in LXC at 10.100.0.20.
* File backend reading /etc/secubox/users.json (argon2 — matches Authelia spec).
* OIDC + 2FA TOTP (opt-in per user).
* Three-fold CTL grammar (autheliactl: components, status, access).
* Lifecycle verbs: install, reload, repair, wizard, uninstall.
* Pre-configured OIDC clients: grafana, gitea, nextcloud (operator fills client secrets).
* Inherits all 9 install-lxc fixes from v2.11.1 (download template, masquerade,
resolv via lxc-attach, ExecStartPre+ prefix, nginx routes.d/, etc.).
* Closes: #239
-- Gerald KERMA <devel@cybermind.fr> Wed, 20 May 2026 15:00:00 +0200

View File

@ -1,37 +0,0 @@
Source: secubox-authelia
Section: admin
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://cybermind.fr/secubox
Package: secubox-authelia
Architecture: all
Depends: ${misc:Depends},
secubox-core (>= 1.0),
secubox-users (>= 1.0),
secubox-haproxy,
lxc,
lxc-templates,
debootstrap,
python3-uvicorn,
python3-fastapi,
python3-toml,
python3-yaml,
openssl,
curl,
jq
Recommends: secubox-mitmproxy
Description: SecuBox Authelia — SSO IdP (AUTH-BRIDGE layer)
Hosts Authelia in a Debian bookworm LXC at 10.100.0.20 on br-lxc.
File-backend identity provider reading /etc/secubox/users.json (the same
argon2-hashed user store already managed by secubox-users) so the canonical
SecuBox identity is the single source of truth for SSO.
.
OIDC clients pre-provisioned for the three native admin UIs that already
support OIDC: Grafana, Gitea, Nextcloud. YaCy/RustDesk-web/mitmproxy-web
(no native OIDC) get nginx `auth_request` integration to the same Authelia.
.
Health-banner injected on the Authelia login page via nginx sub_filter so
the operator sees the SecuBox system health bar even on the SSO screen.

View File

@ -1,38 +0,0 @@
#!/bin/sh
# SecuBox Authelia — post-install
set -e
case "$1" in
configure)
# secubox system user/group (idempotent, matches sibling modules)
getent group secubox >/dev/null || groupadd --system secubox
getent passwd secubox >/dev/null || useradd --system --gid secubox \
--home /var/lib/secubox --no-create-home --shell /usr/sbin/nologin secubox
install -d -o secubox -g secubox -m 755 /etc/secubox
install -d -m 0755 -o secubox -g secubox /var/lib/secubox/authelia
install -d -m 0755 -o secubox -g secubox /var/log/secubox
# Seed config from example if no live config exists
if [ ! -f /etc/secubox/authelia.toml ] && [ -f /etc/secubox/authelia.toml.example ]; then
cp /etc/secubox/authelia.toml.example /etc/secubox/authelia.toml
chmod 640 /etc/secubox/authelia.toml
chown root:secubox /etc/secubox/authelia.toml
fi
# Reload nginx if it's running and our snippet parses
if systemctl is-active --quiet nginx 2>/dev/null; then
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
fi
# Authelia SSO removed: never start/enable the daemon. Mask it so a
# stale unit or sibling dependency cannot resurrect it. The nginx
# gate is now a permissive no-op (see nginx/authelia.conf).
systemctl daemon-reload 2>/dev/null || true
systemctl disable --now secubox-authelia.service 2>/dev/null || true
systemctl mask secubox-authelia.service 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0

View File

@ -1,16 +0,0 @@
#!/bin/sh
# SecuBox Authelia — post-remove
set -e
case "$1" in
purge)
# Only on purge — preserve operator data on plain remove/upgrade.
rm -f /etc/secubox/authelia.toml /etc/secubox/authelia.toml.example
rm -rf /var/lib/secubox/authelia
# LXC is left intact: operator removes it explicitly via
# `autheliactl uninstall` (TODO: future verb).
;;
esac
#DEBHELPER#
exit 0

View File

@ -1,14 +0,0 @@
#!/bin/sh
# SecuBox Authelia — pre-remove
set -e
case "$1" in
remove|deconfigure|upgrade)
systemctl stop secubox-authelia.service 2>/dev/null || true
systemctl disable secubox-authelia.service 2>/dev/null || true
# We do not destroy the LXC here — purge does that via postrm if asked.
;;
esac
#DEBHELPER#
exit 0

View File

@ -1,33 +0,0 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_install:
# Host control plane (FastAPI)
install -d debian/secubox-authelia/usr/lib/secubox/authelia
cp -r api debian/secubox-authelia/usr/lib/secubox/authelia/
# CTL
install -d debian/secubox-authelia/usr/sbin
install -m 755 sbin/autheliactl debian/secubox-authelia/usr/sbin/autheliactl
# Web UI
install -d debian/secubox-authelia/usr/share/secubox/www
[ -d www ] && cp -r www/. debian/secubox-authelia/usr/share/secubox/www/ || true
# Menu entry
install -d debian/secubox-authelia/usr/share/secubox/menu.d
[ -d menu.d ] && cp -r menu.d/. debian/secubox-authelia/usr/share/secubox/menu.d/ || true
# nginx snippet — install to BOTH secubox.d/ and secubox-routes.d/ (per v2.11.1 lesson)
install -d debian/secubox-authelia/etc/nginx/secubox.d
[ -f nginx/authelia.conf ] && cp nginx/authelia.conf debian/secubox-authelia/etc/nginx/secubox.d/ && (install -d debian/secubox-authelia/etc/nginx/secubox-routes.d && cp nginx/authelia.conf debian/secubox-authelia/etc/nginx/secubox-routes.d/) || true
# LAN-bypass geo block — http{}-level, MUST live in conf.d/ so it
# loads before secubox.d/authelia.conf inside the server{} blocks.
install -d debian/secubox-authelia/etc/nginx/conf.d
[ -f nginx/secubox-lan-geo.conf ] && cp nginx/secubox-lan-geo.conf debian/secubox-authelia/etc/nginx/conf.d/ || true
# Public vhost (auth.maegia.tv-style)
install -d debian/secubox-authelia/etc/nginx/sites-available
[ -f nginx/authelia-vhost.conf ] && cp nginx/authelia-vhost.conf debian/secubox-authelia/etc/nginx/sites-available/authelia.conf || true
# Config example
install -d debian/secubox-authelia/etc/secubox
[ -f conf/authelia.toml.example ] && cp conf/authelia.toml.example debian/secubox-authelia/etc/secubox/authelia.toml.example || true
# LXC bootstrap + Authelia configuration template
install -d debian/secubox-authelia/usr/share/secubox/lib/authelia
[ -d lib/authelia ] && cp -r lib/authelia/. debian/secubox-authelia/usr/share/secubox/lib/authelia/ || true

View File

@ -1,27 +0,0 @@
[Unit]
Description=SecuBox Authelia — host control plane API
Documentation=file:///usr/share/doc/secubox-authelia/README
After=network.target secubox-core.service
Wants=secubox-core.service
[Service]
UMask=0007
Type=simple
User=secubox
Group=secubox
WorkingDirectory=/usr/lib/secubox/authelia
RuntimeDirectory=secubox
RuntimeDirectoryMode=0755
RuntimeDirectoryPreserve=yes
ExecStartPre=+/bin/mkdir -p /etc/secubox
ExecStartPre=+/bin/chown secubox:secubox /etc/secubox
ExecStart=/usr/bin/python3 -m uvicorn api.main:app --uds /run/secubox/authelia.sock --log-level warning
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
LogsDirectory=secubox
LogsDirectoryMode=0755
ReadWritePaths=/run/secubox /var/lib/secubox /etc/secubox /var/log/secubox
[Install]
WantedBy=multi-user.target

View File

@ -1,355 +0,0 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# SecuBox-Deb :: secubox-authelia :: install-lxc.sh
#
# Idempotent LXC bootstrap for the Authelia SSO IdP. Safe to re-run.
# Inherits all 9 install-lxc fixes from v2.11.1:
# 1. lxc-create -t download (unprivileged-compatible)
# 2. ensure_masquerade() 10.100.0.0/24
# 3. ensure_resolv() via lxc-attach + unlink symlink
# 4. (no apt repo dance like grafana — Authelia is a single binary)
# Follows docs/MODULE-GUIDELINES.md §3.
set -euo pipefail
readonly LXC_NAME="${SECUBOX_LXC_NAME:-authelia}"
readonly LXC_IP="${SECUBOX_LXC_IP:-10.100.0.20}"
readonly LXC_PATH="${SECUBOX_LXC_PATH:-/data/lxc}"
readonly LXC_BRIDGE="${SECUBOX_LXC_BRIDGE:-br-lxc}"
readonly LXC_GW="${SECUBOX_LXC_GW:-10.100.0.1}"
readonly DEBIAN_SUITE="${SECUBOX_DEBIAN_SUITE:-bookworm}"
readonly AUTHELIA_VERSION="${SECUBOX_AUTHELIA_VERSION:-4.39.5}"
readonly AUTHELIA_HTTP_PORT="${SECUBOX_AUTHELIA_PORT:-9091}"
# Hub domain — the canonical SecuBox WebUI vhost where the SSO portal is
# mounted under /auth/. Authelia REQUIRES a session.cookies[] entry per
# top-level domain it serves, otherwise the SPA gets 403 on /auth/api/state.
# Override via env when deploying on a non-default hub (gk2.secubox.in etc.).
readonly SECUBOX_HUB_DOMAIN="${SECUBOX_HUB_DOMAIN:-gk2.secubox.in}"
readonly PROVISION_DIR="${SECUBOX_PROVISION_DIR:-/usr/share/secubox/lib/authelia/provision}"
readonly STATE_DIR="${SECUBOX_STATE_DIR:-/var/lib/secubox/authelia}"
readonly SECRETS_DIR="${SECUBOX_SECRETS_DIR:-/etc/secubox/secrets}"
readonly SENTINEL="$STATE_DIR/.lxc-provisioned"
log() { printf '[authelia-install] %s\n' "$*"; }
fail() { printf '[authelia-install] ERROR: %s\n' "$*" >&2; exit 1; }
require_cmds() {
for c in lxc-create lxc-info lxc-start lxc-attach openssl nft; do
command -v "$c" >/dev/null 2>&1 || fail "$c not installed"
done
}
ensure_dirs() {
install -d -m 0755 -o root -g root "$LXC_PATH"
install -d -m 0755 -o secubox -g secubox "$STATE_DIR"
install -d -m 0700 -o root -g root "$SECRETS_DIR"
}
ensure_bridge() {
if ! ip link show "$LXC_BRIDGE" >/dev/null 2>&1; then
log "Creating bridge $LXC_BRIDGE @ ${LXC_GW}/24 ..."
ip link add name "$LXC_BRIDGE" type bridge
ip addr add "${LXC_GW}/24" dev "$LXC_BRIDGE"
ip link set "$LXC_BRIDGE" up
fi
}
ensure_masquerade() {
if ! nft list table ip lxc 2>/dev/null | grep -q 'saddr 10.100.0.0/24'; then
log "Adding nftables MASQUERADE for 10.100.0.0/24 ..."
nft 'add table ip lxc' 2>/dev/null || true
nft 'add chain ip lxc postrouting { type nat hook postrouting priority srcnat ; policy accept ; }' 2>/dev/null || true
nft 'add rule ip lxc postrouting ip saddr 10.100.0.0/24 ip daddr != 10.100.0.0/24 counter masquerade' 2>/dev/null || true
fi
}
lxc_state() {
lxc-info -n "$LXC_NAME" -P "$LXC_PATH" 2>/dev/null \
| awk -F: '/^State:/ { gsub(/ /,"",$2); print tolower($2) }'
}
create_lxc() {
if [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]; then
log "LXC '$LXC_NAME' already exists — skipping debootstrap"
return
fi
log "Creating LXC '$LXC_NAME' (debian $DEBIAN_SUITE) ..."
lxc-create -n "$LXC_NAME" -t download -P "$LXC_PATH" \
-- --dist debian --release "$DEBIAN_SUITE" \
--arch "$(dpkg --print-architecture)"
}
write_lxc_config() {
log "Pinning network: $LXC_IP/24 on $LXC_BRIDGE"
cat > "$LXC_PATH/$LXC_NAME/config" <<EOF
# SecuBox-managed — see secubox-authelia / install-lxc.sh
lxc.uts.name = $LXC_NAME
lxc.net.0.type = veth
lxc.net.0.link = $LXC_BRIDGE
lxc.net.0.flags = up
lxc.net.0.ipv4.address = $LXC_IP/24
lxc.net.0.ipv4.gateway = $LXC_GW
lxc.net.0.name = eth0
lxc.rootfs.path = dir:$LXC_PATH/$LXC_NAME/rootfs
lxc.include = /usr/share/lxc/config/common.conf
lxc.apparmor.profile = generated
lxc.start.auto = 1
lxc.start.delay = 5
EOF
}
start_lxc() {
[ "$(lxc_state)" = "running" ] && { log "Already running"; return; }
log "Starting LXC '$LXC_NAME' ..."
lxc-start -n "$LXC_NAME" -P "$LXC_PATH"
}
wait_for_network() {
log "Waiting for LXC network ..."
for _ in $(seq 1 30); do
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- ping -c1 -W1 "$LXC_GW" >/dev/null 2>&1 && return 0
sleep 1
done
fail "LXC '$LXC_NAME' did not reach $LXC_GW within 30s"
}
ensure_resolv() {
log "Seeding /etc/resolv.conf in LXC ..."
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- sh -c '
rm -f /etc/resolv.conf
printf "nameserver 1.1.1.1\nnameserver 9.9.9.9\n" > /etc/resolv.conf
'
}
# ── Authelia install inside LXC ──────────────────────────────────────────────
# Authelia is a single static Go binary. We download from the upstream GitHub
# release matching the LXC's architecture, drop into /opt/authelia/, write a
# systemd unit, and let preflight regenerate the configuration.yml from the
# host-mounted /etc/secubox/authelia.toml on every restart.
install_authelia_in_lxc() {
local lxc_arch
lxc_arch=$(lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- dpkg --print-architecture 2>/dev/null | tr -d '\r\n')
log "Installing Authelia $AUTHELIA_VERSION ($lxc_arch) in '$LXC_NAME' ..."
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- env \
AUTHELIA_VERSION="$AUTHELIA_VERSION" \
AUTHELIA_ARCH="$lxc_arch" \
bash -e <<'INNER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y --no-install-recommends \
ca-certificates curl tar
if ! id -u authelia >/dev/null 2>&1; then
useradd -r -s /bin/false -d /var/lib/authelia -m authelia
fi
install -d -m 0750 -o authelia -g authelia \
/etc/authelia /var/lib/authelia /var/log/authelia
if [ ! -x /opt/authelia/authelia ]; then
mkdir -p /opt/authelia
cd /tmp
url="https://github.com/authelia/authelia/releases/download/v${AUTHELIA_VERSION}/authelia-v${AUTHELIA_VERSION}-linux-${AUTHELIA_ARCH}.tar.gz"
echo "Downloading $url ..."
curl -fsSL "$url" -o authelia.tar.gz
tar xzf authelia.tar.gz
# Tarball ships authelia-linux-<arch> binary
for f in authelia-linux-*; do
[ -f "$f" ] && mv "$f" /opt/authelia/authelia
done
chmod +x /opt/authelia/authelia
rm -f authelia.tar.gz authelia-*.sig man/* completions/* 2>/dev/null || true
chown -R authelia:authelia /opt/authelia
fi
cat > /etc/systemd/system/authelia.service <<UNIT
[Unit]
Description=Authelia SSO portal
After=network.target
[Service]
Type=simple
User=authelia
Group=authelia
WorkingDirectory=/var/lib/authelia
ExecStart=/opt/authelia/authelia --config /etc/authelia/configuration.yml
Restart=on-failure
RestartSec=5
ReadWritePaths=/var/lib/authelia /var/log/authelia /etc/authelia
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable authelia.service
INNER
}
# ── Render configuration.yml from /etc/secubox/authelia.toml ────────────────
# The host-side TOML is the operator-facing config; we render Authelia's YAML
# host-side (with proper variable substitution) and pipe into the LXC via
# lxc-attach stdin. Authelia 4.39+ schema (server.address, identity_validation
# replaces top-level jwt_secret, jwks for OIDC).
render_authelia_config() {
local jwt_secret store_key
[ -f "$SECRETS_DIR/authelia-jwt" ] || openssl rand -hex 32 > "$SECRETS_DIR/authelia-jwt"
[ -f "$SECRETS_DIR/authelia-store" ] || openssl rand -hex 32 > "$SECRETS_DIR/authelia-store"
chmod 600 "$SECRETS_DIR/authelia-jwt" "$SECRETS_DIR/authelia-store"
jwt_secret=$(cat "$SECRETS_DIR/authelia-jwt")
store_key=$(cat "$SECRETS_DIR/authelia-store")
log "Rendering /etc/authelia/configuration.yml ..."
# 1. users_database.yml — convert /etc/secubox/users.json (v2 schema, argon2id
# hashes) to Authelia's file-backend format.
python3 - <<PYEOF
import json, os, sys
users_json = "/etc/secubox/users.json"
out = "/tmp/authelia-users.yml"
if not os.path.exists(users_json):
sys.exit("/etc/secubox/users.json missing — secubox-users must be installed first")
doc = json.load(open(users_json))
yml = "users:\n"
n = 0
for u in doc.get("users", []):
if not u.get("enabled", True): continue
if not u.get("password_hash"): continue # skip users with null hash
yml += f" {u['username']}:\n"
yml += f" displayname: {u['username']}\n"
yml += f" password: '{u['password_hash']}'\n"
yml += f" email: {u.get('email') or u['username']+'@secubox.local'}\n"
yml += f" groups: [{u.get('role', 'user')}]\n"
n += 1
open(out, "w").write(yml)
print(f"Rendered {n} users to {out}")
PYEOF
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- sh -c "cat > /etc/authelia/users_database.yml" \
< /tmp/authelia-users.yml
rm -f /tmp/authelia-users.yml
# 2. configuration.yml — Authelia 4.39+ schema. Variables substituted
# HOST-SIDE (we don't trust env-expansion-inside-quoted-heredoc).
# OIDC disabled for v1.0.0: enabling it requires a JWKS RSA key pair
# and at least one fully-configured client. v1.1.0 wizard handles that.
cat > /tmp/authelia-config.yml <<CONF
# /etc/authelia/configuration.yml — SecuBox-managed (Authelia 4.39+)
# Re-rendered by autheliactl reload from /etc/secubox/authelia.toml + secrets.
# Do NOT edit by hand — your changes will be overwritten.
theme: dark
server:
# Path prefix /auth (NO trailing slash — Authelia 4.39+ rejects it).
# Tells Authelia the React UI lives under /auth/ on the canonical hub
# vhost. Without this, the SPA makes API calls to /api/state etc.
# which 404 because nginx only routes /auth/ → the LXC.
address: 'tcp://0.0.0.0:${AUTHELIA_HTTP_PORT}/auth'
log:
level: info
format: text
file_path: /var/log/authelia/authelia.log
identity_validation:
reset_password:
jwt_secret: ${jwt_secret}
authentication_backend:
file:
path: /etc/authelia/users_database.yml
password:
algorithm: argon2id
access_control:
default_policy: deny
rules:
- domain: "*.maegia.tv"
policy: one_factor
# SecuBox hub vhost (admin.<hub>, lyrion.<hub>, zigbee.<hub>, etc.)
- domain: "${SECUBOX_HUB_DOMAIN}"
policy: one_factor
- domain: "*.${SECUBOX_HUB_DOMAIN}"
policy: one_factor
session:
# One cookie entry per top-level domain. Authelia returns 403 on /auth/api/*
# if the request's Host header doesn't match any cookies[].domain — that's
# the symptom we saw on admin.${SECUBOX_HUB_DOMAIN} before this entry existed.
cookies:
- name: authelia_session
domain: maegia.tv
authelia_url: 'https://auth.maegia.tv/'
expiration: 3600
inactivity: 300
- name: authelia_session
domain: ${SECUBOX_HUB_DOMAIN}
# SSO portal moved to its own vhost in #310 — sibling vhosts (zigbee,
# lyrion, nextcloud) now redirect here for login. The canonical hub
# /auth/ is the operator dashboard, not the portal anymore.
authelia_url: 'https://sso.${SECUBOX_HUB_DOMAIN}/'
expiration: 3600
inactivity: 300
secret: ${jwt_secret}
storage:
encryption_key: ${store_key}
local:
path: /var/lib/authelia/db.sqlite3
notifier:
filesystem:
filename: /var/lib/authelia/notifications.txt
# OIDC identity provider — disabled in v1.0.0. Enable via 'autheliactl wizard'
# in v1.1.0 (will generate the JWKS RSA key + at least one client).
# identity_providers:
# oidc:
# hmac_secret: ${jwt_secret}
# jwks:
# - key_id: 'secubox-oidc'
# algorithm: 'RS256'
# key: |
# -----BEGIN RSA PRIVATE KEY-----
# ...
# clients:
# - client_id: 'secubox-grafana'
# ...
CONF
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- sh -c "cat > /etc/authelia/configuration.yml" \
< /tmp/authelia-config.yml
rm -f /tmp/authelia-config.yml
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- chown -R authelia:authelia /etc/authelia
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl restart authelia.service
}
mark_provisioned() {
install -d -m 0755 -o secubox -g secubox "$STATE_DIR"
date -Iseconds > "$SENTINEL"
}
main() {
require_cmds
ensure_dirs
ensure_bridge
ensure_masquerade
create_lxc
write_lxc_config
start_lxc
wait_for_network
ensure_resolv
install_authelia_in_lxc
render_authelia_config
mark_provisioned
log "OK — LXC '$LXC_NAME' at $LXC_IP, authelia provisioned + running on port $AUTHELIA_HTTP_PORT."
log "Web UI: https://auth.maegia.tv/ (operator: configure DNS + HAProxy vhost)"
log "Secrets: $SECRETS_DIR/authelia-{jwt,store}"
}
main "$@"

View File

@ -1,10 +0,0 @@
{
"subtitle": "Authelia · OIDC IdP",
"icon": "🔑",
"order": 40,
"id": "authelia",
"name": "SSO",
"path": "/auth/",
"category": "auth",
"description": ""
}

View File

@ -1,130 +0,0 @@
# /etc/nginx/sites-available/authelia.conf — public vhost auth.maegia.tv
# Installed by secubox-authelia (#239).
#
# Routing flow (same as yacy.maegia.tv validated 2026-05-20):
# Browser → HAProxy:443 (TLS, cert /data/haproxy/certs/auth.maegia.tv.pem)
# → mitmproxy WAF (LXC) — entry registered in
# /srv/mitmproxy/haproxy-routes.json INSIDE the mitmproxy LXC
# → nginx:9080 (this vhost)
# → Authelia LXC at 10.100.0.20:9091
#
# Operator-side prerequisites (NOT done by this snippet):
# 1. DNS A record auth.maegia.tv → public IP
# 2. TLS cert in /data/haproxy/certs/auth.maegia.tv.pem
# 3. haproxyctl vhost add auth.maegia.tv nginx_vhosts ssl
# ↑ CAUTION: known to drop backend defs (see #238 / v2.11.1 commit message).
# Prefer manual ACL injection or repair haproxy.cfg from backup.
# 4. mitmproxy route INSIDE the LXC:
# lxc-attach -n mitmproxy -- python3 -c \
# "import json; p='/srv/mitmproxy/haproxy-routes.json'; d=json.load(open(p)); \
# d['auth.maegia.tv']=['192.168.1.200',9080]; json.dump(d, open(p,'w'), indent=2, sort_keys=True)"
# lxc-attach -n mitmproxy -- systemctl restart mitmproxy
server {
listen 0.0.0.0:9080;
server_name auth.maegia.tv;
# Banner injection REMOVED here too: Authelia ships a strict CSP
# that rejects the sub_filter <script src="/shared/health-banner.js">.
# Shared assets (banner JS, error pages)
location /shared/ {
add_header Access-Control-Allow-Origin "*" always;
alias /usr/share/secubox/www/shared/;
}
# Redirect root → /auth/ so the SPA lives at the same path on this
# public vhost as on the canonical hub. Authelia is path-aware at
# /auth/ (server.address: tcp://0.0.0.0:9091/auth).
location = / {
# Preserve $args so the `rd=` callback URL from @sbx_auth_login
# survives the redirect — without $is_args$args, Authelia's
# login form has no return target and users land on the hub
# root after authentication.
return 302 https://$host/auth/$is_args$args;
}
# Authelia portal UI — proxy_pass with NO trailing slash to preserve
# the /auth/ prefix expected by Authelia 4.39+.
location /auth/ {
proxy_pass http://10.100.0.20:9091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 300s;
}
access_log /var/log/nginx/authelia_access.log;
error_log /var/log/nginx/authelia_error.log;
}
# ── Public SSO portal — sso.gk2.secubox.in (#310) ────────────────────────────
#
# Multi-SP entry point for SecuBox SSO. All SSO-gated apps on the gk2 board
# (zigbee, lyrion, nextcloud, …) redirect their unauthenticated requests
# here for login. Same chain as auth.maegia.tv above but on the
# .gk2.secubox.in cookie scope (session.cookies[] #272 already covers it).
#
# Routing: Browser → HAProxy:443 → mitmproxy LXC → nginx:9080 (this vhost)
# → Authelia LXC at 10.100.0.20:9091
#
# Operator-side prerequisites:
# 1. DNS A record sso.gk2.secubox.in → public IP
# 2. TLS — covered by the existing wildcard *.gk2.secubox.in.pem in
# /data/haproxy/certs/ (no new cert needed)
# 3. haproxyctl vhost add sso.gk2.secubox.in mitmproxy_inspector
# (then repair haproxy.cfg backend defs per #286)
# 4. mitmproxy route INSIDE the LXC:
# lxc-attach -n mitmproxy -- python3 -c \
# "import json; p='/srv/mitmproxy/haproxy-routes.json'; \
# d=json.load(open(p)); d['sso.gk2.secubox.in']=['192.168.1.200',9080]; \
# open(p,'w').write(json.dumps(d, indent=2, sort_keys=True))"
# lxc-attach -n mitmproxy -- systemctl restart mitmproxy.service
server {
listen 0.0.0.0:9080;
listen [::]:9080;
server_name sso.gk2.secubox.in;
# ACME HTTP-01 challenge — exempt from any gate so renewals work
# from the public internet. MUST come before catch-all locations.
location ^~ /.well-known/acme-challenge/ {
root /usr/share/secubox/www;
try_files $uri =404;
}
# Shared assets (banner JS, error pages)
location /shared/ {
add_header Access-Control-Allow-Origin "*" always;
alias /usr/share/secubox/www/shared/;
}
# Redirect root → /auth/ so the SPA lives at the same path the
# Authelia binary expects (server.address: tcp://0.0.0.0:9091/auth).
location = / {
# Preserve $args so the `rd=` callback URL from @sbx_auth_login
# survives the redirect — without $is_args$args, Authelia's
# login form has no return target and users land on the hub
# root after authentication.
return 302 https://$host/auth/$is_args$args;
}
# Authelia portal UI — proxy_pass with NO trailing slash to preserve
# the /auth/ prefix expected by Authelia 4.39+.
location /auth/ {
proxy_pass http://10.100.0.20:9091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 300s;
}
access_log /var/log/nginx/sso_access.log;
error_log /var/log/nginx/sso_error.log;
}

View File

@ -1,26 +0,0 @@
# /etc/nginx/secubox.d/authelia.conf
#
# Authelia SSO has been REMOVED from SecuBox. This file is retained as a
# permissive no-op only because several app vhosts (grafana, lyrion, yacy,
# rustdesk, fmrelay, zigbee, nextcloud) still carry the directives
# auth_request /__sbx_auth_verify;
# error_page 401 = @sbx_auth_login;
# in their location blocks. Without the two named locations below, nginx
# would fail to load. The gate now allows every request unconditionally —
# there is no SSO portal, no session check, no Authelia socket dependency.
#
# Apps keep their own native authentication (Grafana login, Nextcloud
# login, etc.). LAN/exposure boundaries are enforced by HAProxy + the WAF,
# not by this layer.
# Always-allow gate. auth_request succeeds for every request.
location = /__sbx_auth_verify {
internal;
return 200;
}
# Fallback target for any lingering `error_page 401`. Never reached while
# the gate above returns 200; sends the user back to the app root.
location @sbx_auth_login {
return 302 https://$host/;
}

View File

@ -1,268 +0,0 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# SecuBox-Deb :: autheliactl
# CyberMind — https://cybermind.fr
#
# Authelia host-side controller. LXC at 10.100.0.20 on br-lxc.
# Three-fold introspection (components/status/access) + dashboard, datasource,
# alert, user, api-key, install, reload verbs.
#
# Grammar: docs/grammar.md, OPS MONITORING layer.
# Conventions: docs/MODULE-GUIDELINES.md §7.
set -u
readonly VERSION="1.0.0"
readonly CONFIG_FILE="${SECUBOX_GRAFANA_CONFIG:-/etc/secubox/authelia.toml}"
readonly STATE_DIR="${SECUBOX_GRAFANA_STATE:-/var/lib/secubox/authelia}"
readonly SECRETS_DIR="${SECUBOX_SECRETS_DIR:-/etc/secubox/secrets}"
readonly INSTALL_LIB="${SECUBOX_INSTALL_LIB:-/usr/share/secubox/lib/authelia/install-lxc.sh}"
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
log() { printf '%b[authelia]%b %s\n' "$GREEN" "$NC" "$*"; }
warn() { printf '%b[warn]%b %s\n' "$YELLOW" "$NC" "$*" >&2; }
err() { printf '%b[error]%b %s\n' "$RED" "$NC" "$*" >&2; }
# ── TOML config (best-effort, grep-style — same as giteactl) ─────────────────
config_get() {
# Strips TOML inline `# comment`, surrounding whitespace, and quotes.
# Previous version concatenated `# comment text` into the value
# (eg HTTP_PORT became "9091 # comment"), breaking URL construction.
local key="$1" default="${2:-}" raw=""
if [ -f "$CONFIG_FILE" ]; then
raw=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "$CONFIG_FILE" 2>/dev/null | head -1)
fi
if [ -z "$raw" ]; then
echo "$default"
return
fi
local val
val=$(echo "$raw" | cut -d= -f2- | sed 's/[[:space:]]*#.*$//' \
| sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' \
| tr -d '"' | tr -d "'")
if [ -z "$val" ]; then
echo "$default"
else
echo "$val"
fi
}
LXC_NAME=$(config_get "name" "authelia")
LXC_IP=$(config_get "ip" "10.100.0.20")
LXC_PATH=$(config_get "path" "/data/lxc")
HTTP_PORT=$(config_get "http_port" "9091")
PUBLIC_HOSTNAME=$(config_get "public_hostname" "sso.gk2.secubox.in")
# ── LXC helpers ───────────────────────────────────────────────────────────────
lxc_state() {
lxc-info -n "$LXC_NAME" -P "$LXC_PATH" 2>/dev/null \
| awk -F: '/^State:/ { gsub(/ /,"",$2); print tolower($2) }'
}
lxc_running() { [ "$(lxc_state)" = "running" ]; }
lxc_exists() { [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]; }
daemon_running() {
lxc_running || return 1
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl is-active --quiet authelia 2>/dev/null
}
host_api_running() {
systemctl is-active --quiet secubox-authelia.service 2>/dev/null
}
# ── Three-fold output (matches docs/MODULE-GUIDELINES.md §7) ──────────────────
emit_components_json() {
local lxc_st daemon_st api_st
if lxc_exists; then
lxc_st=$(lxc_state)
[ -z "$lxc_st" ] && lxc_st="absent"
else
lxc_st="absent"
fi
daemon_running && daemon_st="running" || daemon_st="stopped"
host_api_running && api_st="running" || api_st="stopped"
cat <<EOF
{
"module": "authelia",
"version": "$VERSION",
"components": [
{"name": "lxc", "state": "$lxc_st", "detail": "$LXC_NAME @ $LXC_IP on br-lxc"},
{"name": "daemon", "state": "$daemon_st", "detail": "authelia, port $HTTP_PORT"},
{"name": "host-api", "state": "$api_st", "detail": "secubox-authelia.service (uvicorn @ /run/secubox/authelia.sock)"}
]
}
EOF
}
emit_components_text() {
printf '%-12s %-12s %s\n' "COMPONENT" "STATE" "DETAIL"
if lxc_exists; then
local s; s=$(lxc_state); [ -z "$s" ] && s="absent"
printf '%-12s %-12s %s\n' "lxc" "$s" "$LXC_NAME @ $LXC_IP on br-lxc"
else
printf '%-12s %-12s %s\n' "lxc" "absent" "$LXC_NAME @ $LXC_IP on br-lxc — run 'autheliactl install'"
fi
if daemon_running; then
printf '%-12s %-12s %s\n' "daemon" "running" "authelia, port $HTTP_PORT"
else
printf '%-12s %-12s %s\n' "daemon" "stopped" "authelia, port $HTTP_PORT"
fi
if host_api_running; then
printf '%-12s %-12s %s\n' "host-api" "running" "secubox-authelia.service (uvicorn)"
else
printf '%-12s %-12s %s\n' "host-api" "stopped" "secubox-authelia.service (uvicorn)"
fi
}
emit_status_json() {
local overall
if lxc_running && daemon_running && host_api_running; then
overall="green"
elif lxc_exists && (daemon_running || host_api_running); then
overall="yellow"
else
overall="red"
fi
cat <<EOF
{
"module": "authelia",
"version": "$VERSION",
"overall": "$overall"
}
EOF
}
emit_access_json() {
# Real Authelia login portal URLs — NOT the /authelia/ admin subpath.
# The admin webui's "Open SSO Portal" button reads this; consistency
# comes from one source.
# lan: http://<lxc_ip>:9091/ direct to Authelia daemon (LAN-only)
# public: https://<PUBLIC_HOSTNAME>/ dedicated vhost
local lan_url public_url
lan_url="http://${LXC_IP}:${HTTP_PORT}/"
if [ -n "$PUBLIC_HOSTNAME" ]; then
public_url="https://${PUBLIC_HOSTNAME}/"
else
public_url=""
fi
cat <<EOF
{
"module": "authelia",
"access": [
{"url": "$lan_url", "auth": "lan-only", "scope": "lan"}$( [ -n "$public_url" ] && echo ',' )
$( [ -n "$public_url" ] && cat <<PUB
{"url": "$public_url", "auth": "Authelia SSO", "scope": "public"}
PUB
)
]
}
EOF
}
# ── Verbs ─────────────────────────────────────────────────────────────────────
cmd_components() {
case "${1:-}" in
--json) emit_components_json ;;
""|list|status) emit_components_text ;;
*) err "unknown verb for 'components': $1"; exit 1 ;;
esac
}
cmd_status() {
case "${1:-}" in
--json) emit_status_json ;;
""|list) emit_components_text ; echo ; emit_status_json | python3 -c 'import json,sys; d=json.load(sys.stdin); print("overall:", d["overall"])' ;;
*) err "unknown verb for 'status': $1"; exit 1 ;;
esac
}
cmd_access() {
case "${1:-}" in
--json) emit_access_json ;;
""|list) emit_access_json | python3 -m json.tool ;;
show)
local name="${2:-lan}"
emit_access_json | python3 -c "import json,sys; d=json.load(sys.stdin); m=[a for a in d['access'] if a['scope']=='$name']; print(m[0] if m else 'not found')"
;;
*) err "unknown verb for 'access': $1"; exit 1 ;;
esac
}
cmd_install() {
if [ ! -x "$INSTALL_LIB" ]; then
err "install script not found at $INSTALL_LIB (package not installed correctly?)"
exit 2
fi
log "Running LXC bootstrap from $INSTALL_LIB ..."
SECUBOX_LXC_NAME="$LXC_NAME" SECUBOX_LXC_IP="$LXC_IP" SECUBOX_LXC_PATH="$LXC_PATH" \
bash "$INSTALL_LIB"
log "Starting host-side FastAPI ..."
systemctl start secubox-authelia.service 2>/dev/null || true
log "Done. Try 'autheliactl status' to verify."
}
cmd_reload() {
systemctl restart secubox-authelia.service 2>/dev/null || true
if lxc_running; then
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl restart authelia-server 2>/dev/null || true
fi
log "Reloaded host FastAPI and authelia-server (if running)."
}
cmd_help() {
cat <<'HELP'
autheliactl — SecuBox Authelia SSO IdP controller (AUTH-BRIDGE layer)
Three-fold introspection:
autheliactl components [--json] # LXC + authelia daemon + host-API states
autheliactl status [--json] # overall green/yellow/red
autheliactl access [list|show <name>] [--json]
Lifecycle (per docs/MODULE-GUIDELINES.md §7):
autheliactl install # idempotent LXC + Authelia bootstrap
autheliactl reload # restart host FastAPI + authelia daemon
autheliactl repair # detect + fix drift (NOT YET IMPLEMENTED)
autheliactl wizard # interactive seed + install (NOT YET IMPLEMENTED)
autheliactl uninstall # remove LXC + state + secrets (NOT YET IMPLEMENTED)
SSO nouns:
autheliactl provider list|add <name>|remove <name>|test <name>
autheliactl oidc-client list|add <toml>|remove <id>|rotate-secret <id>
autheliactl user list|enable <name>|disable <name>|enrol-totp <name>
autheliactl session list|kill <id>|killall <user>
autheliactl totp list|reset <user>|verify <user> <code>
(--json on any verb returns machine-readable output)
For grammar details: /usr/share/doc/secubox-authelia/README.gz
docs/grammar.md (AUTH-BRIDGE layer, #239)
HELP
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
main() {
local noun="${1:-help}"
shift || true
case "$noun" in
components|status|access|install|reload) "cmd_${noun}" "$@" ;;
# The remaining nouns are stubs in v1.0.0 — see #230 task G5 for
# full implementation. They print a clear "not implemented yet" so
# the help/grammar surface is consistent now and the API contract
# is testable.
provider|user|session|totp|oidc-client)
err "noun '$noun' not yet implemented in v$VERSION — see #239 follow-up"
exit 2
;;
--help|-h|help|"") cmd_help ;;
--version|-V) echo "autheliactl $VERSION" ;;
*) err "unknown noun: $noun"; cmd_help; exit 1 ;;
esac
}
main "$@"

View File

@ -1,210 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecuBox - Auth</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/shared/crt-light.css">
<link rel="stylesheet" href="/shared/sidebar-light.css">
<style>
:root {
/* AUTH palette (Charte SecuBox §Six Module Color System) — orange. */
--auth-main: #C04E24;
--auth-light: #E8845A;
--auth-xlt: #FAECE7;
--accent: var(--auth-main);
--accent-light: var(--auth-light);
--bg: #FFFFFF;
--surface: #FAFAF8;
--surface2: #F4F3EF;
--border: #E2E0D8;
--border2: #C8C6BE;
--text: #1A1A18;
--muted: #6B6963;
--green: #0A5840;
--yellow: #9A6010;
--red: #803018;
--gmb-h: 48px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Space Grotesk', sans-serif; background: var(--bg); color: var(--text); display: flex; min-height: 100vh; font-size: 14px; line-height: 1.6; }
.main { flex: 1; margin-left: 220px; padding: calc(var(--gmb-h) + 1.5rem) 1.5rem 1.5rem; min-width: 0; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; gap: 1rem; flex-wrap: wrap; }
.header h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.4px; color: var(--accent); }
.header h1 .icon { margin-right: 0.5rem; }
.header-actions { display: flex; gap: 0.5rem; }
.btn { padding: 0.5rem 1rem; border-radius: 6px; border: 1px solid var(--border2); background: var(--surface); color: var(--text); cursor: pointer; font-family: inherit; font-size: 14px; text-decoration: none; display: inline-flex; align-items: center; gap: 0.4rem; transition: all 0.15s; }
.btn:hover { border-color: var(--accent); color: var(--accent); background: var(--auth-xlt); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-light); color: #fff; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 1rem 1.25rem; }
.stat .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); margin-bottom: 0.4rem; }
.stat .value { font-family: 'JetBrains Mono', monospace; font-size: 22px; font-weight: 700; color: var(--accent); }
.stat .sub { font-size: 12px; color: var(--muted); margin-top: 0.2rem; }
.threefold { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 1.25rem 1.5rem; }
.card h3 { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); margin-bottom: 0.75rem; }
.card .v { font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.7; color: var(--text); }
.card .v a { color: var(--accent); text-decoration: none; }
.card .v a:hover { text-decoration: underline; }
.card .v strong { color: var(--accent); font-weight: 700; }
.status-pill { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 12px; font-weight: 700; letter-spacing: 0.05em; }
.status-pill.green { background: rgba(10,88,64,0.1); color: var(--green); }
.status-pill.yellow { background: rgba(154,96,16,0.1); color: var(--yellow); }
.status-pill.red { background: rgba(128,48,24,0.1); color: var(--red); }
.about { background: var(--surface2); border: 1px solid var(--border); border-radius: 14px; padding: 1.25rem 1.5rem; color: var(--muted); }
.about h2 { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text); margin-bottom: 0.5rem; }
.about p { margin-bottom: 0.5rem; }
.about code { background: var(--bg); padding: 0.05rem 0.4rem; border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text); border: 1px solid var(--border); }
@media (max-width: 900px) {
.main { margin-left: 0; padding-top: calc(var(--gmb-h) + 1rem); }
.stats, .threefold { grid-template-columns: 1fr; }
}
</style>
</head>
<body class="crt-light">
<nav class="sidebar" id="sidebar"></nav>
<main class="main">
<div class="header">
<h1><span class="icon">🎯</span> Auth — SSO control</h1>
<div class="header-actions">
<a class="btn" href="/" title="Back to SecuBox hub">← hub</a>
<a class="btn primary" id="open-sso" href="#" target="_blank" rel="noopener" title="Open the Authelia SSO portal (login surface)">Open SSO Portal →</a>
</div>
</div>
<section class="stats">
<div class="stat">
<div class="label">Authelia</div>
<div class="value" id="stat-status"></div>
<div class="sub" id="stat-version">probing</div>
</div>
<div class="stat">
<div class="label">Users (file backend)</div>
<div class="value" id="stat-users"></div>
<div class="sub">enabled accounts</div>
</div>
<div class="stat">
<div class="label">Cookie domains</div>
<div class="value" id="stat-cookies"></div>
<div class="sub" id="stat-cookies-sub">session.cookies[]</div>
</div>
<div class="stat">
<div class="label">Access rules</div>
<div class="value" id="stat-rules"></div>
<div class="sub">access_control</div>
</div>
</section>
<section class="threefold">
<div class="card">
<h3>Components</h3>
<div class="v" id="components-v">loading…</div>
</div>
<div class="card">
<h3>Status</h3>
<div class="v" id="status-v">loading…</div>
</div>
<div class="card">
<h3>Access</h3>
<div class="v" id="access-v">loading…</div>
</div>
</section>
<div class="about">
<h2>About this module</h2>
<p>
SecuBox runs <strong>Authelia 4.39+</strong> in an LXC at
<code>10.100.0.20</code>. This page surfaces operator metrics —
service state, configured users, cookie scopes, access policies.
The actual login portal lives on its own public vhost
<code>sso.gk2.secubox.in</code>; every sibling app (zigbee, lyrion,
nextcloud, …) redirects unauthenticated requests there.
</p>
<p>
Click <strong>Open SSO Portal →</strong> to reach the login UI.
Backups of <code>/etc/secubox/secrets/authelia-*</code> survive
package upgrades.
</p>
</div>
</main>
<script src="/shared/sidebar.js"></script>
<script src="/shared/api-utils.js"></script>
<script>
(function () {
function getToken() { return localStorage.getItem('sbx_token'); }
function checkAuth() {
if (!getToken()) {
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
return false;
}
return true;
}
if (!checkAuth()) {
document.querySelector('main.main').innerHTML = '<div style="padding:2rem;color:var(--muted);">Redirecting to login…</div>';
return;
}
function get(path) {
return fetch('/api/v1/authelia' + path, { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); });
}
get('/version').then(function (d) {
document.getElementById('stat-version').textContent = 'v' + (d.version || '?');
}).catch(function () {});
get('/status').then(function (d) {
var pill = (d.overall === 'green') ? 'green' : (d.overall === 'yellow') ? 'yellow' : 'red';
document.getElementById('stat-status').innerHTML = '<span class="status-pill ' + pill + '">' + (d.overall || 'unknown').toUpperCase() + '</span>';
document.getElementById('status-v').innerHTML = '<span class="status-pill ' + pill + '">' + (d.overall || 'unknown').toUpperCase() + '</span>';
}).catch(function () {
document.getElementById('stat-status').innerHTML = '<span class="status-pill red">DOWN</span>';
document.getElementById('status-v').innerHTML = '<span style="color:var(--muted)">unavailable</span>';
});
get('/components').then(function (d) {
var list = (d.components || []).map(function (c) { return '<div>' + c.name + ': <strong>' + c.state + '</strong></div>'; }).join('');
document.getElementById('components-v').innerHTML = list || '<span style="color:var(--muted)">no components</span>';
}).catch(function () { document.getElementById('components-v').innerHTML = '<span style="color:var(--muted)">unavailable</span>'; });
get('/access').then(function (d) {
var rules = (d.rules || d.access_rules || []);
var cookies = (d.cookies || d.session_cookies || []);
var users = (d.users != null) ? d.users : (d.user_count != null ? d.user_count : '?');
document.getElementById('stat-users').textContent = String(users);
document.getElementById('stat-cookies').textContent = String(cookies.length || '?');
document.getElementById('stat-cookies-sub').textContent = cookies.map(function (c) { return c.domain || c; }).join(', ') || 'session.cookies[]';
document.getElementById('stat-rules').textContent = String(rules.length || '?');
var access = d.access || [];
var v = access.map(function (a) { return '<div>' + (a.scope || '') + ': <a href="' + (a.url || '#') + '">' + (a.url || '?') + '</a></div>'; }).join('');
document.getElementById('access-v').innerHTML = v || '<span style="color:var(--muted)">no access points</span>';
// Single source of truth for the "Open SSO Portal" button.
var pub = access.find(function (a) { return a.scope === 'public'; });
var lan = access.find(function (a) { return a.scope === 'lan'; });
var target = (pub && pub.url) || (lan && lan.url) || '#';
var btn = document.getElementById('open-sso');
if (btn) btn.href = target;
}).catch(function () {
document.getElementById('access-v').innerHTML = '<span style="color:var(--muted)">unavailable</span>';
});
})();
</script>
</body>
</html>

View File

@ -12,6 +12,15 @@ override_dh_auto_install:
# Modular nginx config
install -d debian/secubox-hub/etc/nginx/secubox.d
[ -f nginx/hub.conf ] && cp nginx/hub.conf debian/secubox-hub/etc/nginx/secubox.d/ || true
# Authelia-free auth-gate stub + LAN geo (moved here from the decommissioned
# secubox-authelia). Ships $lan_client and the /__sbx_auth_verify +
# @sbx_auth_login named locations into BOTH include dirs so SSO-gated vhosts
# keep resolving them; LAN passes, everything else is denied.
install -d debian/secubox-hub/etc/nginx/secubox-routes.d
[ -f nginx/zz-sbx-authgate.conf ] && cp nginx/zz-sbx-authgate.conf debian/secubox-hub/etc/nginx/secubox.d/ || true
[ -f nginx/zz-sbx-authgate.conf ] && cp nginx/zz-sbx-authgate.conf debian/secubox-hub/etc/nginx/secubox-routes.d/ || true
install -d debian/secubox-hub/etc/nginx/conf.d
[ -f nginx/secubox-lan-geo.conf ] && cp nginx/secubox-lan-geo.conf debian/secubox-hub/etc/nginx/conf.d/ || true
# Nginx snippets (shared CORS config)
install -d debian/secubox-hub/etc/nginx/snippets
[ -d nginx/snippets ] && cp -r nginx/snippets/. debian/secubox-hub/etc/nginx/snippets/ || true

View File

@ -1,19 +1,18 @@
# /etc/nginx/conf.d/secubox-lan-geo.conf
# Installed by secubox-authelia (#239).
# Shipped by secubox-hub (moved here from the decommissioned secubox-authelia).
#
# Defines $lan_client = 1 for trusted internal networks. Consumed by
# the auth_request handler in /etc/nginx/secubox.d/authelia.conf to
# short-circuit SSO for LAN/LXC clients — the dashboard at
# admin.gk2.secubox.in is auth-free from inside, gated by SSO from
# outside.
# Defines $lan_client = 1 for trusted internal networks. Consumed by the
# auth-gate stub in zz-sbx-authgate.conf: LAN/LXC/localhost clients pass the
# gate, everything else is denied (default-deny). The dashboard at
# admin.<host> is auth-free from inside, denied from outside.
#
# Add new internal ranges here (route subnets, VPN pools) — do NOT
# add public IPs.
# Add new internal ranges here (route subnets, VPN pools) — do NOT add public
# IPs.
# Trust the local HAProxy + mitmproxy chain so $remote_addr resolves
# to the real client IP (not 127.0.0.1 / the LXC bridge address).
# Without this every request looks local and the LAN bypass below
# would whitelist every external visitor.
# Trust the local HAProxy + mitmproxy chain so $remote_addr resolves to the
# real client IP (not 127.0.0.1 / the LXC bridge address). Without this every
# request looks local and the LAN bypass below would whitelist every external
# visitor.
set_real_ip_from 127.0.0.1;
set_real_ip_from 10.100.0.0/24;
real_ip_header X-Forwarded-For;
@ -25,6 +24,6 @@ geo $lan_client {
192.168.1.0/24 1; # primary LAN (gk2 / SecuBox dashboards)
192.168.255.0/24 1; # secondary lan3 segment
10.55.0.0/24 1; # eye-br0 bridge (eye-remote / Pi Zero W)
10.100.0.0/24 1; # br-lxc (Authelia/Lyrion/Grafana/... LXCs)
10.100.0.0/24 1; # br-lxc (Lyrion / Grafana / ... LXCs)
10.0.3.0/24 1; # lxcbr0 (legacy LXC default bridge)
}

View File

@ -0,0 +1,24 @@
# /etc/nginx/{secubox.d,secubox-routes.d}/zz-sbx-authgate.conf
# Shipped by secubox-hub.
#
# Authelia-free auth-gate stubs. secubox-authelia (the SSO IdP / auth-bridge,
# #239) was decommissioned; these keep the two named locations that SSO-gated
# vhosts still reference (`auth_request /__sbx_auth_verify;` and
# `error_page 401 = @sbx_auth_login;`) working without any Authelia dependency.
#
# Posture (default-deny / CSPN): LAN clients pass, everything else is denied.
# WAN-exposed apps that relied on the old SSO gate are therefore LAN-only.
# `$lan_client` is defined in /etc/nginx/conf.d/secubox-lan-geo.conf (also
# shipped by secubox-hub).
#
# The `zz-` prefix keeps this sorted after the per-vhost include files so the
# named locations exist in the same server block that references them.
location = /__sbx_auth_verify {
internal;
if ($lan_client) { return 204; }
return 403;
}
location @sbx_auth_login {
return 403;
}