Skip to content

Home Assistant

Home Assistant runs the house. Upstream publishes no Helm chart and the community ones are repackagers, so home-assistant/ is plain manifests around the official image: deployment.yaml, service.yaml, pvc.yaml, configmap.yaml, httproute.yaml, database.yaml with backup.yaml, networkpolicy.yaml and namespace.yaml, per Conventions → Official upstream sources only.

At a glance

URL home.k8s.wlkr.ch
Authentication Authentik proxy outpost, in front of Home Assistant's own login
Storage rook-ceph-block PVC for /config
Database CloudNativePG Cluster home-assistant-db, for the recorder
Files home-assistant/

State

State Where Rebuildable?
Registries, users, tokens, integration config (/config/.storage) The home-assistant-config PVC No. Losing it means re-pairing every device; the nightly Velero snapshot is the only copy
History and long-term statistics PostgreSQL, via CloudNativePG From its WAL archive, to any point inside its retention window
configuration.yaml A ConfigMap, in Git Yes

Configuration

Setting Why
Plain manifests, no chart Home Assistant is one stateful process with one volume and one port; a Deployment is less work than auditing a third-party chart on every bump
strategy: Recreate, one replica The /config PVC is ReadWriteOnce, and two Home Assistant processes against one /config corrupt the .storage registries
revisionHistoryLimit: 2 Superseded ReplicaSets keep their Trivy config-audit reports until they are garbage-collected; the Kubernetes default keeps ten
Root with every capability dropped but NET_BIND_SERVICE, no API token; the namespace enforces baseline The image runs as root under s6 and writes /config, and upstream does not support running it otherwise, so runAsNonRoot and a read-only root do not apply. Root owns everything it touches; deployment.yaml keeps NET_BIND_SERVICE, which no port it opens needs, 1900 and 8123 being above 1024. baseline forbids adding NET_RAW, so DHCP discovery joins the discovery that does not work here anyway. Nothing in the pod talks to the API server, so the default ServiceAccount's token stays unmounted, per Default ServiceAccount tokens
An init container from the same image seeds /config on every start configuration.yaml includes automations.yaml, scripts.yaml and scenes.yaml so the UI editors can save; on a fresh volume they do not exist and Home Assistant fails to start rather than create them. Same image as the main container because Renovate updates every occurrence in one pull request, so the two cannot drift
A long startupProbe Home Assistant loads every integration before it serves anything, minutes rather than seconds on a cold start with a populated registry. The startup probe buys that time without making the liveness probe useless afterwards
Recorder on CloudNativePG, not SQLite SQLite's locking on a network block device is the usual cause of a Home Assistant that feels slow. purge_keep_days is raised because the default of 10 is shorter than the moment you look for a graph. See the contract
secrets.yaml written by the init container on every start Home Assistant has no !env_var tag, so the database URL reaches configuration.yaml through !secret, from CloudNativePG's Secret, which the operator rotates. The value is written through json.dumps so it is valid YAML whatever the generated password contains. Anything added to that file by hand is lost at the next restart; secrets belong in OpenBao
use_x_forwarded_for with the pod CIDR in trusted_proxies Requests arrive from Cilium's Envoy and the outpost. Without it every request has one source address, so the ban list and rate limiting count the household as one user
HTTPRoute targets authentik-server, not the app The outpost authenticates and forwards to home-assistant.home-assistant.svc:8123. Home Assistant is the one proxied app with a login of its own, so browser users log in twice: defence in depth, not SSO. The cross-namespace backendRef works only because the platform's referencegrant.yaml names this namespace. Pointing the route at the Home Assistant Service instead silently removes the whole authentication layer, so it deserves a second look in review
frontend.extra_module_url, with umami.js from the ConfigMap mounted under /config/www The Umami snippet. The frontend loads the URL through import(), and the tracker reads its website ID off its own <script> tag, so the module appends that tag rather than being it. See Umami → Usage

Network policy

The rules in home-assistant/networkpolicy.yaml, and why each is there. The shape is Conventions → Ship a CiliumNetworkPolicy; rollout and debugging are in Security Policies.

Rule Why
Ingress from the namespace, from host and remote-node; egress to the namespace The rules every policy carries: What every policy contains
Ingress on 8123 from the Authentik server pods only The outpost is the only path to the UI. No fromEntities: ingress, so a request straight from the Gateway cannot skip it. Nothing else in the cluster has a reason to connect: this namespace holds a long-lived token for every device in the house. With the rule missing, the hostname answers with a gateway error after a successful Authentik login
Ingress from Prometheus on 9187, GET /metrics only The database's metrics; a week of Hubble records showed no other request
Ingress from cnpg-system on 8000 The operator polls the instance manager there; without the rule the Cluster never goes Healthy and the sync deadlocks. See Sync stuck on the database
Egress to *.home-assistant.io The alerts feed and version data. An integration that talks to a device or a cloud is a line here first
Egress to the multicast and limited-broadcast ranges on 5353 and 1900 The mDNS and SSDP discovery sockets keep sending, and the overlay carries it nowhere. Allowed only so it does not fill the drop log
Egress from the database pod to kube-apiserver The instance manager reports its status there; its liveness check fails otherwise. The object store for WAL and base backups is the platform's cnpg-backups policy, see CloudNativePG → Backups

Usage

The ConfigMap is mounted over /config/configuration.yaml, so Git is the source of truth and the UI cannot edit it; everything Home Assistant writes for itself stays on the PVC underneath. The file is read once at startup, so a merged change does nothing until the pod restarts:

kubectl -n home-assistant rollout restart deploy/home-assistant

The UI's own restart button does the same from inside.

Health check

kubectl -n home-assistant get pods,pvc
kubectl -n home-assistant get cluster home-assistant-db

The database should report Cluster in healthy state. A pod stuck in Init is waiting on it: the init container cannot write secrets.yaml until CloudNativePG has produced the Secret.

Pitfalls

The API is not behind Authentik

/api/, /auth/token and the external-auth callback are exempt on the provider, so Home Assistant's own accounts guard them and revoking an Authentik account does not revoke a Home Assistant token: do that under Settings → People. See Authentik → Pitfalls.

  • No local device discovery. mDNS and SSDP need host networking, which needs a privileged namespace. Add devices by IP, or through an integration that does not rely on broadcast, and add the address to the egress policy.
  • No USB devices. A Zigbee or Z-Wave stick pins the pod to one machine. Use a network-attached coordinator instead.
  • The cluster is now a dependency of the lights. A Ceph rebalance that pauses I/O pauses Home Assistant with it.