From 760e187ba19def37dc7b147d73413fa632419636 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 9 Jul 2026 01:17:46 +0200 Subject: [PATCH] feat(home-assistant): gate vscode/code-server behind Authentik passkey step-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit code-server (the HA /config editor sidecar) runs with --auth=none, as root, mounting the entire /config incl. plaintext secrets.yaml, .storage, and the git PAT in the PVC remote URL. Until now the only control on it was network reachability of vscode.home.renner.uno — any LAN client could read every HA secret and edit config. Put it behind Authentik using the existing paperless-gate proxy-outpost pattern: - New Authentik proxy provider + application + dedicated outpost (ak-outpost-vscode-gate) in mode: proxy. authorization_flow is the passkey-stepup-authorization flow (same one Homeassistant/Immich/ Audiobookshelf/Netbird use), so every authorize requires a passkey. - New "Home Assistant Config Editors" group (git-owned membership, k.renner only) bound to the app — explicit allowlist for who can read HA secrets via the editor. - vscode Ingress now backends the gate outpost (ExternalName mirror) instead of code-server directly. - home-assistant CNP tightened: traefik no longer allowed to :8080; only the vscode-gate outpost pod may reach code-server. Matching scoped egress CNP added in the authentik namespace. - code-server is a service-worker app; skip_path_regex carves out its static/manifest/SW tree to avoid the credentials:'omit' redirect loop that forced retirement of the music-assistant gate. Exact skip set to be confirmed post-deploy (see PR checklist). Verified: kubectl kustomize builds clean for home-assistant, authentik, and network-policies. The Authentik proxy flow + code-server PWA behaviour require a browser passkey login test after apply. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../apps/authentik/blueprints-configmap.yaml | 114 ++++++++++++++++++ kubernetes/apps/authentik/kustomization.yaml | 1 + .../apps/authentik/vscode-gate-egress.yaml | 26 ++++ .../apps/home-assistant/kustomization.yaml | 1 + .../home-assistant/vscode-gate-service.yaml | 23 ++++ .../apps/home-assistant/vscode-ingress.yaml | 15 ++- .../network-policies/home-assistant-ns.yaml | 17 ++- 7 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 kubernetes/apps/authentik/vscode-gate-egress.yaml create mode 100644 kubernetes/apps/home-assistant/vscode-gate-service.yaml diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index e08981ab..00e587c8 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -214,6 +214,120 @@ data: slug: paperless-proxy state: absent + # ------------------------------------------------------------------ + # vscode config-editor gate + # + # code-server (the HA /config editor sidecar) runs with --auth=none, + # as root, mounting the whole HA /config incl. plaintext secrets.yaml + # and .storage. This gate moves it behind Authentik with a passkey + # step-up, mirroring the paperless-gate proxy-outpost pattern above: + # the dedicated ak-outpost-vscode-gate terminates the request, + # enforces the session, and forwards to the code-server Service. + # Traefik only ever routes vscode.home.renner.uno to the outpost; the + # home-assistant CNP is tightened so ONLY the gate outpost may reach + # :8080 (see home-assistant-ns.yaml + vscode-gate-egress.yaml). + # ------------------------------------------------------------------ + - id: vscode-config-editors-group + model: authentik_core.group + identifiers: + name: Home Assistant Config Editors + state: present + attrs: + is_superuser: false + # Git owns membership for this gate — the group's entire purpose + # is to be the explicit allowlist of who can read HA secrets via + # the editor. Add/remove members HERE, not in the UI (blueprint + # apply is authoritative for this M2M and will revert UI edits). + users: + - !Find [authentik_core.user, [username, k.renner]] + + - id: vscode-gate-provider + model: authentik_providers_proxy.proxyprovider + identifiers: + name: vscode-gate + state: present + attrs: + # Passkey step-up on every authorize — the same flow the + # Homeassistant, Immich, Audiobookshelf and Netbird OAuth2 + # providers use (passkey-stepup.yaml blueprint). Referenced by + # !Find (not !KeyOf) because that flow is defined in a different + # blueprint document within this ConfigMap. + authorization_flow: !Find [authentik_flows.flow, [slug, passkey-stepup-authorization]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + # Proxy mode: the outpost terminates the request and forwards to + # code-server, moving the trust boundary onto the gate. Traefik + # never talks to code-server directly. + mode: proxy + internal_host: http://home-assistant-vscode.home-assistant.svc.cluster.local + internal_host_ssl_validation: false + external_host: https://vscode.home.renner.uno + cookie_domain: renner.uno + access_token_validity: hours=24 + refresh_token_validity: days=30 + intercept_header_auth: true + # code-server is a service-worker app: and + # the service worker are fetched with credentials:'omit', which + # turned into a /authorize redirect loop and forced the RETIREMENT + # of the music-assistant gate (see comment above). Carve its + # static/manifest/SW tree out so those cookieless fetches pass + # through instead of 302-looping; the editor's actual content + # (the /config tree) is served over authenticated websocket/API + # paths, which stay gated. + # NOTE: the exact skip set is code-server-version-specific and + # MUST be confirmed post-deploy against the real redirect-loop + # behaviour (see the PR validation checklist). + skip_path_regex: ^/(_static/|manifest\.json|.*\.webmanifest|.*serviceWorker\.js).* + + - model: authentik_core.application + identifiers: + slug: vscode-gate + state: present + attrs: + name: HA Config Editor (Gate) + slug: vscode-gate + provider: !KeyOf vscode-gate-provider + meta_launch_url: https://vscode.home.renner.uno + meta_description: "Passkey-gated VS Code editor for Home Assistant /config" + + # Only the Home Assistant Config Editors group passes the gate. + - model: authentik_policies.policybinding + identifiers: + target: !Find [authentik_core.application, [slug, vscode-gate]] + group: !KeyOf vscode-config-editors-group + order: 0 + state: present + attrs: + enabled: true + negate: false + failure_result: false + + - model: authentik_outposts.outpost + identifiers: + name: vscode-gate + state: present + attrs: + type: proxy + service_connection: !KeyOf k8s-conn + providers: + - !KeyOf vscode-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 65a8ed2b..a56edf0a 100644 --- a/kubernetes/apps/authentik/kustomization.yaml +++ b/kubernetes/apps/authentik/kustomization.yaml @@ -7,6 +7,7 @@ resources: - helmrelease.yaml - paperless-gate-ingress.yaml - paperless-gate-egress.yaml + - vscode-gate-egress.yaml - outpost-servicemonitors.yaml - ldap-outpost-cert.yaml - ldap-outpost-lb.yaml diff --git a/kubernetes/apps/authentik/vscode-gate-egress.yaml b/kubernetes/apps/authentik/vscode-gate-egress.yaml new file mode 100644 index 00000000..f38de821 --- /dev/null +++ b/kubernetes/apps/authentik/vscode-gate-egress.yaml @@ -0,0 +1,26 @@ +--- +# Allow the dedicated vscode-gate outpost to egress into the +# home-assistant namespace and reach the code-server sidecar (:8080) on +# the HA pod. 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 code-server. Mirrors paperless-gate-egress.yaml. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: vscode-gate-egress + namespace: authentik +spec: + endpointSelector: + matchLabels: + goauthentik.io/outpost-name: vscode-gate + egress: + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": home-assistant + app.kubernetes.io/name: home-assistant + toPorts: + - ports: + - port: "8080" + protocol: TCP diff --git a/kubernetes/apps/home-assistant/kustomization.yaml b/kubernetes/apps/home-assistant/kustomization.yaml index 46c0343f..1f4d3984 100644 --- a/kubernetes/apps/home-assistant/kustomization.yaml +++ b/kubernetes/apps/home-assistant/kustomization.yaml @@ -7,6 +7,7 @@ resources: - service.yaml - bgp-service.yaml - vscode-service.yaml + - vscode-gate-service.yaml - ingress.yaml - vscode-ingress.yaml - prometheus-token.sops.yaml diff --git a/kubernetes/apps/home-assistant/vscode-gate-service.yaml b/kubernetes/apps/home-assistant/vscode-gate-service.yaml new file mode 100644 index 00000000..e09dbc14 --- /dev/null +++ b/kubernetes/apps/home-assistant/vscode-gate-service.yaml @@ -0,0 +1,23 @@ +--- +# ExternalName mirror so the home-assistant-namespace vscode Ingress can +# backend to the vscode-gate outpost that Authentik deploys into the +# authentik namespace (cross-namespace Service refs aren't supported in a +# standard Ingress). Mirrors the ak-outpost-paperless-gate ExternalName in +# kubernetes/apps/paperless/paperless.yaml. +# +# Traffic path: +# browser → traefik (vscode.home.renner.uno) +# → ak-outpost-vscode-gate (ExternalName → authentik ns) +# → outpost validates session + passkey step-up +# → http://home-assistant-vscode.home-assistant.svc:80 (code-server :8080) +apiVersion: v1 +kind: Service +metadata: + name: ak-outpost-vscode-gate + namespace: home-assistant +spec: + type: ExternalName + externalName: ak-outpost-vscode-gate.authentik.svc.cluster.local + ports: + - port: 9000 + targetPort: 9000 diff --git a/kubernetes/apps/home-assistant/vscode-ingress.yaml b/kubernetes/apps/home-assistant/vscode-ingress.yaml index 3303b7bc..20910ba5 100644 --- a/kubernetes/apps/home-assistant/vscode-ingress.yaml +++ b/kubernetes/apps/home-assistant/vscode-ingress.yaml @@ -1,3 +1,14 @@ +--- +# vscode.home.renner.uno — the code-server sidecar that edits HA /config. +# +# code-server runs with --auth=none as root and mounts the entire /config +# (plaintext secrets.yaml, .storage, the git PAT in the PVC remote URL). +# Traefik no longer routes to code-server directly: this Ingress backends +# the Authentik vscode-gate proxy outpost (ExternalName → authentik ns), +# which enforces a passkey step-up and forwards to code-server only after +# the session is validated. The home-assistant CNP is tightened so ONLY +# that outpost pod may reach :8080 (see home-assistant-ns.yaml). Pattern +# mirrors kubernetes/apps/paperless (paperless.renner.uno → gate outpost). apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -15,9 +26,9 @@ spec: pathType: Prefix backend: service: - name: home-assistant-vscode + name: ak-outpost-vscode-gate port: - number: 80 + number: 9000 tls: - secretName: home-assistant-vscode-tls hosts: diff --git a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml index 88479195..0b2b5212 100644 --- a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml +++ b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml @@ -12,8 +12,7 @@ spec: - fromEndpoints: - matchLabels: "k8s:io.kubernetes.pod.namespace": home-assistant - # Public web UI: traefik → home-assistant Service → pod:8124 (proxy sidecar) - # VSCode editor: traefik → home-assistant-vscode Service → pod:8080 (code-server) + # Public web UI: traefik → home-assistant Service → pod:8124 (proxy sidecar) # The proxy sidecar is the XFF trust anchor: HA's trusted_proxies is # [127.0.0.1, ::1] only, so this port-scoped rule — restricting :8124 # to traefik — is what guarantees nothing else can present @@ -26,6 +25,20 @@ spec: - ports: - port: "8124" protocol: TCP + # VSCode editor: code-server (:8080) is NO LONGER reached by traefik + # directly. It sits behind the Authentik vscode-gate proxy outpost, + # which terminates the request, enforces a passkey step-up, and + # forwards here. Only that outpost pod (in the authentik namespace) + # may reach :8080 — this is what turns the --auth=none/root editor + # from "any LAN client can edit" into "authenticated editors only". + # See kubernetes/apps/authentik/vscode-gate-egress.yaml (the matching + # egress rule) and the vscode-gate blueprint in blueprints-configmap.yaml. + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": authentik + goauthentik.io/outpost-name: vscode-gate + toPorts: + - ports: - port: "8080" protocol: TCP # Prometheus scrape: kube-prometheus-stack hits HA's /api/prometheus. -- 2.54.0