feat(secubox-authelia): v1.0.2 — implement /verify (real auth_request target) (closes #262) (#263)

v1.0.1 returned HTTP 501 (stub) for /verify 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
exactly this reason.

The /verify handler now reverse-proxies to Authelia's native /api/verify
endpoint at http://10.100.0.20:9091/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

The Remote-* headers are propagated back to nginx so protected backends
can capture them via:
    auth_request_set $sbx_user $upstream_http_remote_user;
    proxy_set_header X-Forwarded-User $sbx_user;

Fail-closed: if the Authelia LXC is unreachable (LXC stopped, broker
crash, etc.) /verify returns 503 — which nginx auth_request treats as
deny, matching the LAN-only fallback semantics.

Board-validated on gk2:

    $ curl -sI --unix-socket /run/secubox/authelia.sock http://localhost/verify
    HTTP/1.1 401 Unauthorized             # no cookie → deny

    $ curl -sI -H 'Cookie: authelia_session=BOGUS' \
        --unix-socket /run/secubox/authelia.sock http://localhost/verify
    HTTP/1.1 401 Unauthorized             # bogus cookie → deny

    $ curl -sI http://10.100.0.20:9091/api/verify
    HTTP/1.1 401 Unauthorized             # upstream sanity check OK

Follow-ups (separate PRs after this merges):

* secubox-zigbee v2.4.2: uncomment the auth_request block in
  nginx/zigbee.conf, drop the LAN-only allow/deny — replaces #259's
  IP-based gate with proper Authelia SSO.
* secubox-lyrion v1.0.5: same — uncomment auth_request in
  nginx/lyrion.conf + nginx/lyrion-vhost.conf, drop the LAN-only blocks.
* (Optional v1.1.0 cleanup) consolidate the verify logic into a shared
  secubox-core helper so other modules can call it directly.

Closes #262

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
This commit is contained in:
CyberMind 2026-05-20 17:59:08 +02:00 committed by GitHub
parent 5fa88f082f
commit 04503c8b38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 93 additions and 14 deletions

View File

@ -15,16 +15,30 @@ 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, Response
from fastapi import FastAPI, HTTPException, Header, Request, Response
VERSION = "1.0.0"
VERSION = "1.0.2"
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,
@ -76,23 +90,56 @@ def access() -> Dict[str, Any]:
@app.get("/verify")
@app.head("/verify")
def verify(
request: Request,
response: Response,
authorization: str | None = Header(default=None),
cookie: str | None = Header(default=None),
) -> Dict[str, Any]:
) -> Response:
"""
nginx `auth_request` target for SSO-less backends.
nginx `auth_request` target.
Returns 200 + X-Sbx-User / X-Sbx-Role headers if the SecuBox JWT is valid.
Returns 401 otherwise.
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
Stub for v1.0.0 full JWT validation lives in secubox-portal/api/main.py
today. This endpoint reverse-proxies to the portal's existing /verify or
re-implements the JWT check locally; the v1.1.0 plan is to consolidate
into a shared secubox-core helper.
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).
"""
# TODO v1.1.0: validate JWT against /etc/secubox/secubox.conf:[api].jwt_secret
# For now, this endpoint is a stub that returns 401 — wire to secubox-portal
# /api/v1/portal/verify once that endpoint exists (see #239 spec §Phase A).
raise HTTPException(status_code=501, detail="auth_request /verify not yet wired — see #239 Phase A")
# Pass through Cookie + Authorization headers to Authelia. nginx already
# stripped the request body (proxy_pass_request_body off) so we're just
# forwarding metadata.
upstream = f"{AUTHELIA_URL.rstrip('/')}/api/verify"
headers = {}
if cookie:
headers["Cookie"] = cookie
if authorization:
headers["Authorization"] = authorization
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,3 +1,35 @@
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