From 2efd54ef826c2aadeb59e816576c7e61601f6f57 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 9 Jul 2026 13:42:51 +0200 Subject: [PATCH] Gate shelly-manager behind Authentik passkey step-up 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) --- .../apps/authentik/blueprints-configmap.yaml | 105 ++++++++++++++++++ kubernetes/apps/authentik/kustomization.yaml | 1 + .../authentik/shelly-manager-gate-egress.yaml | 26 +++++ .../apps/home-assistant/kustomization.yaml | 1 + .../shelly-manager-gate-service.yaml | 23 ++++ .../shelly-manager-ingress.yaml | 13 ++- .../network-policies/home-assistant-ns.yaml | 20 ++++ 7 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 kubernetes/apps/authentik/shelly-manager-gate-egress.yaml create mode 100644 kubernetes/apps/home-assistant/shelly-manager-gate-service.yaml diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index 00e587c8..08fb73a3 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -328,6 +328,111 @@ data: kubernetes_ingress_secret_name: "authentik-outpost-tls" kubernetes_json_patches: null + # ------------------------------------------------------------------ + # shelly-manager gate + # + # 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 was reachability of shelly-manager.home.renner.uno. + # + # Same shape as the vscode-gate above: a dedicated outpost terminates + # the request, enforces a passkey step-up, and forwards to the + # shelly-manager Service. Traefik only ever routes the hostname to the + # outpost; the home-assistant CNP allows ONLY this outpost to reach + # :8080 (see home-assistant-ns.yaml + shelly-manager-gate-egress.yaml). + # ------------------------------------------------------------------ + - id: shelly-managers-group + model: authentik_core.group + identifiers: + name: Shelly Managers + state: present + attrs: + is_superuser: false + # Git owns membership, as with Home Assistant Config Editors: this + # group is the explicit allowlist for who may reconfigure the + # Shellies. Add/remove members HERE, not in the UI — blueprint + # apply is authoritative for this M2M and reverts UI edits. + users: + - !Find [authentik_core.user, [username, k.renner]] + + - id: shelly-manager-gate-provider + model: authentik_providers_proxy.proxyprovider + identifiers: + name: shelly-manager-gate + state: present + attrs: + authorization_flow: !Find [authentik_flows.flow, [slug, passkey-stepup-authorization]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + mode: proxy + internal_host: http://shelly-manager.home-assistant.svc.cluster.local + internal_host_ssl_validation: false + external_host: https://shelly-manager.home.renner.uno + cookie_domain: renner.uno + access_token_validity: hours=24 + refresh_token_validity: days=30 + intercept_header_auth: true + # 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 + # no manifest and no service worker — wwwroot is app.css, favicon.png + # and lib/ only — so every asset is a same-origin credentialed fetch + # and nothing needs to bypass the gate. It is a Blazor Web app: the + # SignalR circuit at /_blazor upgrades to a websocket carrying the + # session cookie, which the proxy outpost forwards. If the UI ever + # fails to establish its circuit, look there first, not at static + # assets. + + - model: authentik_core.application + identifiers: + slug: shelly-manager-gate + state: present + attrs: + name: Shelly Manager (Gate) + slug: shelly-manager-gate + provider: !KeyOf shelly-manager-gate-provider + meta_launch_url: https://shelly-manager.home.renner.uno + meta_description: "Passkey-gated Shelly device manager" + + # Only the Shelly Managers group passes the gate. + - model: authentik_policies.policybinding + identifiers: + target: !Find [authentik_core.application, [slug, shelly-manager-gate]] + group: !KeyOf shelly-managers-group + order: 0 + state: present + attrs: + enabled: true + negate: false + failure_result: false + + - model: authentik_outposts.outpost + identifiers: + name: shelly-manager-gate + state: present + attrs: + type: proxy + service_connection: !KeyOf k8s-conn + providers: + - !KeyOf shelly-manager-gate-provider + config: + log_level: info + authentik_host: "http://authentik-server.authentik.svc.cluster.local" + authentik_host_insecure: false + authentik_host_browser: "https://auth.renner.uno/" + refresh_interval: "minutes=5" + container_image: null + object_naming_template: "ak-outpost-%(name)s" + kubernetes_namespace: authentik + kubernetes_replicas: 1 + kubernetes_service_type: ClusterIP + kubernetes_disabled_components: ["ingress"] + kubernetes_image_pull_secrets: [] + kubernetes_ingress_annotations: {} + kubernetes_ingress_class_name: null + kubernetes_ingress_secret_name: "authentik-outpost-tls" + kubernetes_json_patches: null + - model: authentik_outposts.outpost identifiers: name: authentik Embedded Outpost diff --git a/kubernetes/apps/authentik/kustomization.yaml b/kubernetes/apps/authentik/kustomization.yaml index a56edf0a..c124cc73 100644 --- a/kubernetes/apps/authentik/kustomization.yaml +++ b/kubernetes/apps/authentik/kustomization.yaml @@ -8,6 +8,7 @@ resources: - paperless-gate-ingress.yaml - paperless-gate-egress.yaml - vscode-gate-egress.yaml + - shelly-manager-gate-egress.yaml - outpost-servicemonitors.yaml - ldap-outpost-cert.yaml - ldap-outpost-lb.yaml diff --git a/kubernetes/apps/authentik/shelly-manager-gate-egress.yaml b/kubernetes/apps/authentik/shelly-manager-gate-egress.yaml new file mode 100644 index 00000000..467e0446 --- /dev/null +++ b/kubernetes/apps/authentik/shelly-manager-gate-egress.yaml @@ -0,0 +1,26 @@ +--- +# Allow the dedicated shelly-manager-gate outpost to egress into the +# home-assistant namespace and reach shelly-manager (:8080). Without this, +# the authentik-namespace baseline CNP +# (kubernetes/infrastructure/network-policies/authentik-ns.yaml) has no +# egress rule to home-assistant, so the gate outpost's forwarded requests +# are dropped. Scoped to the gate outpost's pod label so no other authentik +# component can reach it. Mirrors vscode-gate-egress.yaml. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: shelly-manager-gate-egress + namespace: authentik +spec: + endpointSelector: + matchLabels: + goauthentik.io/outpost-name: shelly-manager-gate + egress: + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": home-assistant + app.kubernetes.io/name: shelly-manager + toPorts: + - ports: + - port: "8080" + protocol: TCP diff --git a/kubernetes/apps/home-assistant/kustomization.yaml b/kubernetes/apps/home-assistant/kustomization.yaml index 2a3ca390..1951f6e0 100644 --- a/kubernetes/apps/home-assistant/kustomization.yaml +++ b/kubernetes/apps/home-assistant/kustomization.yaml @@ -23,6 +23,7 @@ resources: - shelly-manager-network-attachment.yaml - shelly-manager-deployment.yaml - shelly-manager-service.yaml + - shelly-manager-gate-service.yaml - shelly-manager-ingress.yaml configMapGenerator: # nginx config for the reverse-proxy sidecar. Hash suffix disabled so diff --git a/kubernetes/apps/home-assistant/shelly-manager-gate-service.yaml b/kubernetes/apps/home-assistant/shelly-manager-gate-service.yaml new file mode 100644 index 00000000..9adf42cf --- /dev/null +++ b/kubernetes/apps/home-assistant/shelly-manager-gate-service.yaml @@ -0,0 +1,23 @@ +--- +# ExternalName mirror so the home-assistant-namespace shelly-manager Ingress +# can backend to the shelly-manager-gate outpost that Authentik deploys into +# the authentik namespace (cross-namespace Service refs aren't supported in a +# standard Ingress). Mirrors ak-outpost-vscode-gate in +# vscode-gate-service.yaml. +# +# Traffic path: +# browser → traefik (shelly-manager.home.renner.uno) +# → ak-outpost-shelly-manager-gate (ExternalName → authentik ns) +# → outpost validates session + passkey step-up +# → http://shelly-manager.home-assistant.svc:80 (shelly-manager :8080) +apiVersion: v1 +kind: Service +metadata: + name: ak-outpost-shelly-manager-gate + namespace: home-assistant +spec: + type: ExternalName + externalName: ak-outpost-shelly-manager-gate.authentik.svc.cluster.local + ports: + - port: 9000 + targetPort: 9000 diff --git a/kubernetes/apps/home-assistant/shelly-manager-ingress.yaml b/kubernetes/apps/home-assistant/shelly-manager-ingress.yaml index b63667d8..2d2d2e45 100644 --- a/kubernetes/apps/home-assistant/shelly-manager-ingress.yaml +++ b/kubernetes/apps/home-assistant/shelly-manager-ingress.yaml @@ -12,6 +12,15 @@ spec: # SignalR 15s server-side keepalive keeps alive. If the UI starts # losing scan-progress updates, raise idleTimeout on the websecure # entrypoint in kubernetes/infrastructure/traefik/helmrelease.yaml. + # + # Traefik no longer routes to shelly-manager directly. The app has no + # authentication of its own — anything reaching :8080 can rewrite Shelly + # config, push scripts, and read stored device AdminPasswords — so this + # Ingress backends the Authentik shelly-manager-gate proxy outpost + # (ExternalName → authentik ns), which enforces a passkey step-up and + # forwards only after the session is validated. The home-assistant CNP + # allows ONLY that outpost to reach :8080 (see home-assistant-ns.yaml). + # Pattern mirrors vscode-ingress.yaml. ingressClassName: traefik rules: - host: shelly-manager.home.renner.uno @@ -21,9 +30,9 @@ spec: pathType: Prefix backend: service: - name: shelly-manager + name: ak-outpost-shelly-manager-gate port: - number: 80 + number: 9000 tls: - secretName: shelly-manager-tls hosts: diff --git a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml index 11e0e1e6..3b5cc64f 100644 --- a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml +++ b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml @@ -41,6 +41,26 @@ spec: - ports: - port: "8080" protocol: TCP + # shelly-manager (:8080) is likewise reached only through its own + # Authentik gate outpost. Note this rule and the vscode one are + # port-identical: because this CNP's endpointSelector is {}, a + # `toPorts: 8080` grant here applies to EVERY pod in the namespace that + # listens on 8080, not just the intended one — which is exactly how + # shelly-manager silently rode on the old traefik→:8080 allow written + # for code-server, and why removing that allow in 760e187 took + # shelly-manager's ingress down with it. The `fromEndpoints` selector is + # what keeps these two apart: each gate outpost can only reach its own + # target, enforced on the egress side by the matching CNPs in the + # authentik namespace (vscode-gate-egress.yaml, + # shelly-manager-gate-egress.yaml). + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": authentik + goauthentik.io/outpost-name: shelly-manager-gate + toPorts: + - ports: + - port: "8080" + protocol: TCP # Prometheus scrape: kube-prometheus-stack hits HA's /api/prometheus. - fromEndpoints: - matchLabels: -- 2.54.0