feat: Declarative OIDC enablement capability (Plan B Units 1-4)#215
Merged
Conversation
OidcConfig (+ OidcRedirect) under routing.auth.oidc models a native-OAuth app's
Authelia client: client_name/scopes/consent_mode/token_endpoint_auth_method, a
discriminated redirect {style: port|path, path}, and a free-form env map
{container_var: secret|issuer|redirect|external_port|client_id}. mode: oidc now
requires the oidc block. Round-trips both real consumers (grafana port-based +
client_secret_basic; signalk path-based + client_secret_post).
The generated prestart, when routing.auth.oidc is present, resolves the external
port (port-based redirects, integer-validated from the port registry), appends the
container's OIDC env vars to runtime.env per the env map, and writes the Authelia
client snippet to /etc/halos/oidc-clients.d/<client_id>.yml — literal ${HALOS_DOMAIN}
(merger expands), port resolved at write. Snippet is written at prestart time (not
build time) so port-based redirects work; the dead rules.j2 build-time install is
removed. postinst remains the sole secret-gen owner; is_oidc_app still drives Authelia
ordering and postrm cleanup. Reproduces grafana (port) and signalk (path) generatively.
Plan B Units 2-3.
…t model Plan B Unit 4.
Address code-review findings on the OIDC capability: - Charset-constrain client_id, client_name, scopes, env var names, and redirect path at the schema level — they are interpolated into root-executed generated bash (heredoc/echo/path/grep); a newline+EOF in client_name was a root-RCE path, relevant since imported-containers auto-converts third-party app definitions. - Make oidc/mode bidirectional: an oidc block is only valid with mode: oidc, and the prestart OIDC section gates on mode == oidc — so it can't diverge from is_oidc_app (postinst secret-gen, Authelia ordering). Prevents writing a snippet whose secret is never provisioned. - Reject env source external_port with a non-port redirect (would emit an empty port). - Tests: injection rejections, bidirectional gating, end-to-end prestart wiring, bash -n validity, and the client_id/issuer env sources.
Contributor
Author
Code review — declarative OIDC capabilityAutomated multi-persona review (correctness, security, adversarial, testing, project-standards lenses). It caught real P1s in the bash-generating path; all are fixed in Verdict: Ready to merge (after the fixes). Project-standards clean (version cycle open → no bump; no hardcoded hostnames; changelog untouched; doc accurate). Secret-at-rest handling verified sound (runtime.env 600 before write; Fixed in this PR
Deferred to Unit 5 (the marine migration, separate PR)
Verification
Part of #211. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Makes OIDC enablement declarative (Plan B of the #207 series). grafana and signalk hand-roll ~80–180 lines of prestart each to register as Authelia OIDC clients (secret gen, port resolution, container env vars, client snippet) — the duplication that shipped the
HALOS_DOMAINbug (#177). Now an app declaresrouting.auth.mode: oidc+ arouting.auth.oidcblock and the generator produces the lifecycle.Builds on the composable prestart (#212, merged) and the legacy cleanup (#214, merged), so it lands on a clean slate.
Approach (Option X)
mode: oidcis the native-OAuth contract: it adds no Traefik edge gate (configure-container-routingonly chains Authelia middleware forforward_auth), but makesis_oidc_apptrue, reusing the existing scaffolding —postinstprovisions the secret once (mode 600),service.j2ordersAfter=halos-authelia-container.service,postrmcleans up. So OIDC client config lives underrouting.auth.oidc, not an orthogonal stanza that would strand that scaffolding.OidcConfig):client_name,scopes,consent_mode,token_endpoint_auth_method, a discriminatedredirect {style: port|path, path}, and a free-formenv {container_var: source}map. The map is free-form because the two consumers differ: grafana needs onlysecret+external_port(derives its redirect fromGF_SERVER_ROOT_URL); signalk needssecret+issuer+redirect. A fixed triad can't express both.runtime.env, and writes the Authelia client snippet — at prestart time for all OIDC apps (the port-based redirect embeds the runtime port, and the merger only expands${HALOS_DOMAIN}). This removes the dead build-timerules.j2install entirely (one code path), resolving the dangling-install issue noted in refactor: Remove dead legacy traefik/OIDC code (Plan C) #214's review.Verification
ruff,ruff format --check,uvx ty check src/clean.client_secret_basic) and signalk (path-based,client_secret_post) andbash -nvalid — it reproduces their hand-rolled snippets/env, with literal${HALOS_DOMAIN}(merger expands) and the port resolved at write.+N).Threat model
Documented in
AGENTS.md: secret ispostinst-provisioned (root, 600) and env-delivered viaruntime.env(600);hostnames.confis the redirect allow-list trust anchor (generator emits literal${HALOS_DOMAIN}only, port integer-validated); the merger fixespublic: false/one_factor(apps needing other policies are unsupported here).Scope
Plan B Units 1–4 (capability + docs). Unit 5 (migrate grafana + signalk to the stanza) is deliberately not here — it's the marine repo, and it merges with Plan A's avnav/influxdb migration in one coordinated
CONTAINER_TOOLS_REFwindow after this releases (the rollout entanglement).Post-Deploy Monitoring & Validation
No production runtime impact from merging — changes generated package output, not a running service. Validation happens when grafana/signalk migrate (Unit 5, separate PR): on-device
tools/test-oidc-all.shfor login/callback/SSO, and confirm the generated snippet matches today's hand-rolled one.Part of #211.