mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 15:28:24 +00:00
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:
parent
8adac26e55
commit
6476897873
|
|
@ -2778,7 +2778,6 @@ INCOMPLETE_MODULES=(
|
|||
secubox-yacy
|
||||
secubox-rustdesk
|
||||
secubox-lyrion
|
||||
secubox-authelia
|
||||
secubox-mail
|
||||
secubox-gitea
|
||||
secubox-matrix
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
# 🔐 Authelia SSO
|
||||
|
||||
Single sign-on identity provider (AUTH-BRIDGE)
|
||||
|
||||
**Category:** Access
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"subtitle": "Authelia · OIDC IdP",
|
||||
"icon": "🔑",
|
||||
"order": 40,
|
||||
"id": "authelia",
|
||||
"name": "SSO",
|
||||
"path": "/auth/",
|
||||
"category": "auth",
|
||||
"description": ""
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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/;
|
||||
}
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
24
packages/secubox-hub/nginx/zz-sbx-authgate.conf
Normal file
24
packages/secubox-hub/nginx/zz-sbx-authgate.conf
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user