feat(home-assistant): gate vscode/code-server behind Authentik passkey step-up

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 01:17:46 +02:00
co-authored by Claude Opus 4.8
parent dcf053cdd3
commit 760e187ba1
7 changed files with 193 additions and 4 deletions
@@ -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: <link rel="manifest"> 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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.