Gate shelly-manager behind Authentik passkey step-up #138

Merged
k.renner merged 1 commits from feat/shelly-manager-authentik-gate into master 2026-07-09 11:44:48 +00:00
Owner

Why

shelly-manager has no authentication of its own. Anything that reaches :8080 can rewrite Shelly device config, push scripts, and read the stored per-device AdminPassword values. The only control on it was reachability of shelly-manager.home.renner.uno.

Same treatment as code-server got in #132.

What

Mirrors the vscode-gate pattern exactly:

  • Proxy provider + application + dedicated outpost (ak-outpost-shelly-manager-gate), mode: proxy, authorization_flow: passkey-stepup-authorization — the same flow vscode-gate uses. Every authorize needs a passkey.
  • New git-owned Shelly Managers group, membership in the blueprint (k.renner), bound to the application. Blueprint apply is authoritative and reverts UI edits — add people in git.
  • Ingress repointed from shelly-manager:80 to ak-outpost-shelly-manager-gate:9000 via an ExternalName mirror.
  • Scoped CNPs: ingress rule in home-assistant allowing only outpost-name=shelly-manager-gate → :8080; matching egress rule in authentik allowing that outpost → app.kubernetes.io/name=shelly-manager:8080 only.

No skip_path_regex, deliberately

The music-assistant gate was retired because Chrome fetches manifest.webmanifest / sw.js with credentials: 'omit', producing a /authorize redirect loop; paperless and vscode carve those paths out.

shelly-manager ships neither — wwwroot/ is app.css, favicon.png, lib/. Every asset is a same-origin credentialed fetch. It's a Blazor Web app whose SignalR circuit at /_blazor upgrades to a websocket carrying the session cookie, which the proxy outpost forwards. Starting with no skip set is the safer default here; adding one blindly would punch an unauthenticated hole.

This also un-breaks the app

shelly-manager has been returning 504 through Traefik since 01:20 today, when #132 merged. Cause:

default-deny-ingress has endpointSelector: {} — it applies to every pod in home-assistant. Its traefik → :8080 grant was written for the code-server sidecar, but namespace-wide it was in fact the only rule letting Traefik reach shelly-manager, which also listens on 8080. Removing that grant to gate vscode took shelly-manager's ingress down as collateral.

Hubble, on the shelly-manager endpoint:

ingress/traefik-…:47524 -> home-assistant/shelly-manager-…:8080   Policy denied DROPPED (SYN)
10.244.2.60 (host)     -> home-assistant/shelly-manager-…:8080   to-endpoint FORWARDED (SYN,ACK)

The kubelet probe matches reserved:host / ANY, so the pod stayed Ready while Traefik was dropped — which is why nothing alerted.

Gating the app is the right fix rather than re-widening the rule to Traefik. It should not have been reachable unauthenticated to begin with.

I've added a comment above the two gate rules noting the endpointSelector: {} footgun explicitly, so the next person who edits a toPorts there knows it lands on every pod in the namespace.

Unrelated note

This PR does not touch the DHCP/sbr macvlan work from #135/#137. That change was a genuine, separate regression (a DHCP-supplied default route on net1 shadowing eth0's, taking the pod NotReady), fixed by #137, and verified: one default route via eth0, sbr rule from 10.2.30.122 lookup 100, pod serves 200 in 3 ms to a Traefik-shaped request from inside the cluster.

Verified

  • kubectl kustomize builds clean for apps/authentik, apps/home-assistant, infrastructure/network-policies.
  • Rendered Ingress backends ak-outpost-shelly-manager-gate:9000.
  • Rendered egress CNP: outpost-name=shelly-manager-gate → shelly-manager:8080.
  • Rendered ingress CNP: vscode-gate → :8080 and shelly-manager-gate → :8080 coexist; neither outpost can reach the other's target (enforced by the egress CNPs).
  • Traefik is already permitted to reach every pod in the authentik namespace, so the outpost needs no additional ingress allow.

Requires a browser test after apply

Not verifiable from the cluster side, and I'm not going to claim otherwise:

  1. https://shelly-manager.home.renner.uno → Authentik → passkey step-up → app loads.
  2. The Blazor SignalR circuit establishes through the outpost (scan progress updates live). If it doesn't, that's the websocket upgrade, not static assets.
  3. A user outside Shelly Managers is refused.

Unlike paperless-gate, neither this nor vscode-gate adds an /outpost.goauthentik.io Ingress on auth.renner.uno — proxy-mode outposts serve that path on their own external_host. If the callback 401-loops, that's the first thing to add.

## Why `shelly-manager` has **no authentication of its own**. Anything that reaches `:8080` can rewrite Shelly device config, push scripts, and read the stored per-device `AdminPassword` values. The only control on it was reachability of `shelly-manager.home.renner.uno`. Same treatment as code-server got in #132. ## What Mirrors the `vscode-gate` pattern exactly: - **Proxy provider + application + dedicated outpost** (`ak-outpost-shelly-manager-gate`), `mode: proxy`, `authorization_flow: passkey-stepup-authorization` — the same flow `vscode-gate` uses. Every authorize needs a passkey. - **New git-owned `Shelly Managers` group**, membership in the blueprint (`k.renner`), bound to the application. Blueprint apply is authoritative and reverts UI edits — add people in git. - **Ingress repointed** from `shelly-manager:80` to `ak-outpost-shelly-manager-gate:9000` via an ExternalName mirror. - **Scoped CNPs**: ingress rule in `home-assistant` allowing only `outpost-name=shelly-manager-gate` → `:8080`; matching egress rule in `authentik` allowing that outpost → `app.kubernetes.io/name=shelly-manager:8080` only. ## No `skip_path_regex`, deliberately The music-assistant gate was retired because Chrome fetches `manifest.webmanifest` / `sw.js` with `credentials: 'omit'`, producing a `/authorize` redirect loop; paperless and vscode carve those paths out. `shelly-manager` ships **neither** — `wwwroot/` is `app.css`, `favicon.png`, `lib/`. Every asset is a same-origin credentialed fetch. It's a Blazor Web app whose SignalR circuit at `/_blazor` upgrades to a websocket carrying the session cookie, which the proxy outpost forwards. Starting with no skip set is the safer default here; adding one blindly would punch an unauthenticated hole. ## This also un-breaks the app `shelly-manager` has been returning **504 through Traefik since 01:20 today**, when #132 merged. Cause: `default-deny-ingress` has `endpointSelector: {}` — it applies to **every** pod in `home-assistant`. Its `traefik → :8080` grant was written for the code-server sidecar, but namespace-wide it was in fact the only rule letting Traefik reach `shelly-manager`, which also listens on `8080`. Removing that grant to gate vscode took `shelly-manager`'s ingress down as collateral. Hubble, on the `shelly-manager` endpoint: ``` ingress/traefik-…:47524 -> home-assistant/shelly-manager-…:8080 Policy denied DROPPED (SYN) 10.244.2.60 (host) -> home-assistant/shelly-manager-…:8080 to-endpoint FORWARDED (SYN,ACK) ``` The kubelet probe matches `reserved:host / ANY`, so the pod stayed `Ready` while Traefik was dropped — which is why nothing alerted. Gating the app is the right fix rather than re-widening the rule to Traefik. It should not have been reachable unauthenticated to begin with. I've added a comment above the two gate rules noting the `endpointSelector: {}` footgun explicitly, so the next person who edits a `toPorts` there knows it lands on every pod in the namespace. ## Unrelated note This PR does **not** touch the DHCP/`sbr` macvlan work from #135/#137. That change was a genuine, separate regression (a DHCP-supplied default route on `net1` shadowing `eth0`'s, taking the pod `NotReady`), fixed by #137, and verified: one default route via `eth0`, `sbr` rule `from 10.2.30.122 lookup 100`, pod serves 200 in 3 ms to a Traefik-shaped request from inside the cluster. ## Verified - `kubectl kustomize` builds clean for `apps/authentik`, `apps/home-assistant`, `infrastructure/network-policies`. - Rendered Ingress backends `ak-outpost-shelly-manager-gate:9000`. - Rendered egress CNP: `outpost-name=shelly-manager-gate` → `shelly-manager:8080`. - Rendered ingress CNP: `vscode-gate → :8080` and `shelly-manager-gate → :8080` coexist; neither outpost can reach the other's target (enforced by the egress CNPs). - Traefik is already permitted to reach every pod in the `authentik` namespace, so the outpost needs no additional ingress allow. ## Requires a browser test after apply Not verifiable from the cluster side, and I'm not going to claim otherwise: 1. `https://shelly-manager.home.renner.uno` → Authentik → passkey step-up → app loads. 2. The Blazor SignalR circuit establishes through the outpost (scan progress updates live). If it doesn't, that's the websocket upgrade, not static assets. 3. A user outside `Shelly Managers` is refused. Unlike `paperless-gate`, neither this nor `vscode-gate` adds an `/outpost.goauthentik.io` Ingress on `auth.renner.uno` — proxy-mode outposts serve that path on their own `external_host`. If the callback 401-loops, that's the first thing to add.
k.renner added 1 commit 2026-07-09 11:44:01 +00:00
shelly-manager has no authentication of its own. Anything that can reach
:8080 can rewrite Shelly device config, push scripts, and read the stored
per-device AdminPassword values. Until now the only control on it was
network reachability of shelly-manager.home.renner.uno.

Put it behind Authentik using the same proxy-outpost pattern as the
vscode gate (760e187) and paperless before it:

- New proxy provider + application + dedicated outpost
  (ak-outpost-shelly-manager-gate) in mode: proxy. authorization_flow is
  passkey-stepup-authorization, the same flow vscode-gate uses, so every
  authorize requires a passkey.
- New git-owned "Shelly Managers" group (membership in the blueprint,
  k.renner only) bound to the application as the explicit allowlist.
- shelly-manager Ingress now backends the gate outpost via an ExternalName
  mirror instead of the app Service directly.
- home-assistant CNP gains a scoped ingress rule: only the
  shelly-manager-gate outpost pod may reach :8080. Matching scoped egress
  CNP added in the authentik namespace.

No skip_path_regex, deliberately. The music-assistant gate died on PWA
assets fetched with credentials:'omit' (manifest, service worker), and
paperless/vscode carve those out. shelly-manager ships neither -- wwwroot
is app.css, favicon.png and lib/ only -- so every asset is a same-origin
credentialed fetch. It is a Blazor Web app whose SignalR circuit at
/_blazor upgrades to a websocket carrying the session cookie, which the
proxy outpost forwards.

This also restores the app. It has been returning 504 through Traefik
since 760e187 merged at 01:20 today: `default-deny-ingress` has
endpointSelector {}, so its traefik -> :8080 grant, written for the
code-server sidecar, was namespace-wide and was in fact the only rule
letting Traefik reach shelly-manager, which also listens on 8080. Removing
that grant to gate vscode took shelly-manager's ingress down as collateral.
Gating it properly is the correct fix rather than re-widening the rule --
the app should not have been reachable unauthenticated in the first place.

Verified: kubectl kustomize builds clean for authentik, home-assistant and
network-policies; the rendered Ingress backends ak-outpost-shelly-manager-
gate:9000, the egress CNP is scoped to shelly-manager:8080, and the two
gate ingress rules cannot reach each other's target.

The Authentik proxy flow, the passkey login, and the SignalR circuit
through the outpost all require a browser test after apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
k.renner merged commit d57087d746 into master 2026-07-09 11:44:48 +00:00
Sign in to join this conversation.