feat(esphome): deploy ESPHome Device Builder behind an Authentik gate #156
@@ -433,6 +433,105 @@ data:
|
||||
kubernetes_ingress_secret_name: "authentik-outpost-tls"
|
||||
kubernetes_json_patches: null
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# esphome-gate
|
||||
#
|
||||
# ESPHome Device Builder (esphome.home.renner.uno, home-assistant ns,
|
||||
# :6052) has no auth of its own: anyone reaching :6052 can edit device
|
||||
# YAML (incl. wifi/api secrets in secrets.yaml), compile, and flash/OTA
|
||||
# firmware. Same proxy-outpost pattern as shelly-manager-gate above: a
|
||||
# dedicated outpost terminates the request, enforces a passkey step-up,
|
||||
# and forwards to the esphome Service. Traefik only ever routes the
|
||||
# hostname to the outpost; the home-assistant CNP allows ONLY this outpost
|
||||
# to reach :6052 (see home-assistant-ns.yaml + esphome-gate-egress.yaml).
|
||||
# ------------------------------------------------------------------
|
||||
- id: esphome-managers-group
|
||||
model: authentik_core.group
|
||||
identifiers:
|
||||
name: ESPHome Managers
|
||||
state: present
|
||||
attrs:
|
||||
is_superuser: false
|
||||
# Git owns membership, as with Shelly Managers / HA Config Editors:
|
||||
# the explicit allowlist for who may build/flash ESPHome firmware.
|
||||
# Add/remove members HERE, not in the UI — blueprint apply is
|
||||
# authoritative and reverts UI edits.
|
||||
users:
|
||||
- !Find [authentik_core.user, [username, k.renner]]
|
||||
|
||||
- id: esphome-gate-provider
|
||||
model: authentik_providers_proxy.proxyprovider
|
||||
identifiers:
|
||||
name: esphome-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://esphome.home-assistant.svc.cluster.local
|
||||
internal_host_ssl_validation: false
|
||||
external_host: https://esphome.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 initially. Confirm post-deploy whether the ESPHome
|
||||
# dashboard ships a service worker / manifest fetched with
|
||||
# credentials:'omit' that 302-loops at the gate (the failure mode that
|
||||
# forced carve-outs on paperless/vscode and killed the music-assistant
|
||||
# gate). If so, add the offending paths here. The build/OTA log
|
||||
# websocket carries the session cookie and is forwarded by the outpost.
|
||||
|
||||
- model: authentik_core.application
|
||||
identifiers:
|
||||
slug: esphome-gate
|
||||
state: present
|
||||
attrs:
|
||||
name: ESPHome Device Builder (Gate)
|
||||
slug: esphome-gate
|
||||
provider: !KeyOf esphome-gate-provider
|
||||
meta_launch_url: https://esphome.home.renner.uno
|
||||
meta_description: "Passkey-gated ESPHome Device Builder"
|
||||
|
||||
# Only the ESPHome Managers group passes the gate.
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
target: !Find [authentik_core.application, [slug, esphome-gate]]
|
||||
group: !KeyOf esphome-managers-group
|
||||
order: 0
|
||||
state: present
|
||||
attrs:
|
||||
enabled: true
|
||||
negate: false
|
||||
failure_result: false
|
||||
|
||||
- model: authentik_outposts.outpost
|
||||
identifiers:
|
||||
name: esphome-gate
|
||||
state: present
|
||||
attrs:
|
||||
type: proxy
|
||||
service_connection: !KeyOf k8s-conn
|
||||
providers:
|
||||
- !KeyOf esphome-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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# alertmanager gate
|
||||
#
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# Allow the dedicated esphome-gate outpost to egress into the home-assistant
|
||||
# namespace and reach esphome (:6052). Without this, the authentik-namespace
|
||||
# baseline CNP 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 shelly-manager-gate-egress.yaml.
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: esphome-gate-egress
|
||||
namespace: authentik
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
goauthentik.io/outpost-name: esphome-gate
|
||||
egress:
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
"k8s:io.kubernetes.pod.namespace": home-assistant
|
||||
app.kubernetes.io/name: esphome
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "6052"
|
||||
protocol: TCP
|
||||
@@ -9,6 +9,7 @@ resources:
|
||||
- paperless-gate-egress.yaml
|
||||
- vscode-gate-egress.yaml
|
||||
- shelly-manager-gate-egress.yaml
|
||||
- esphome-gate-egress.yaml
|
||||
- alertmanager-gate-egress.yaml
|
||||
- prometheus-gate-egress.yaml
|
||||
- outpost-servicemonitors.yaml
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: esphome
|
||||
namespace: home-assistant
|
||||
labels:
|
||||
app.kubernetes.io/name: esphome
|
||||
spec:
|
||||
replicas: 1
|
||||
# RWO PVC → single writer; never run two pods against the same /config.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: esphome
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: esphome
|
||||
spec:
|
||||
# No macvlan on this pod (deliberately — v1). Unlike shelly-manager, the
|
||||
# ESPHome Device Builder needs no L2 presence on the IoT VLAN: initial
|
||||
# flashing is done in the browser over USB (ESP Web Tools / Web Serial),
|
||||
# and building/compiling is offline. So the 0.0.0.0:6052 bind stays on the
|
||||
# Cilium-governed pod network only, where the home-assistant CNP admits
|
||||
# ONLY the esphome-gate Authentik outpost (see home-assistant-ns.yaml +
|
||||
# esphome-gate-egress.yaml) — no unauthenticated IoT-VLAN exposure like the
|
||||
# macvlan-on-0.0.0.0 hole shelly-manager had to pin Kestrel around.
|
||||
#
|
||||
# Live device status / logs / OTA would need to reach devices on the IoT
|
||||
# VLAN; that rides on the still-open cluster→v30iot reachability work and
|
||||
# a macvlan can be added later if wanted.
|
||||
enableServiceLinks: false
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: esphome
|
||||
image: ghcr.io/esphome/esphome:2026.6.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
# Default entrypoint runs the dashboard (Device Builder) against the
|
||||
# mounted /config — no command override needed.
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 6052
|
||||
env:
|
||||
- name: TZ
|
||||
value: Europe/Berlin
|
||||
# Status/OTA reach devices by ping+API rather than mDNS (the pod has
|
||||
# no L2 presence on the IoT VLAN; mDNS can't cross the L3 boundary).
|
||||
- name: ESPHOME_DASHBOARD_USE_PING
|
||||
value: "true"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /config
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
# Compiling firmware (GCC/LTO under PlatformIO) is memory-hungry
|
||||
# and bursty; no CPU limit so a build isn't throttled to a crawl.
|
||||
memory: 4Gi
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: esphome-data
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# ExternalName mirror so the home-assistant-namespace esphome Ingress can
|
||||
# backend to the esphome-gate outpost that Authentik deploys into the authentik
|
||||
# namespace (cross-namespace Service refs aren't supported in a standard
|
||||
# Ingress). Mirrors shelly-manager-gate-service.yaml.
|
||||
#
|
||||
# Traffic path:
|
||||
# browser → traefik (esphome.home.renner.uno)
|
||||
# → ak-outpost-esphome-gate (ExternalName → authentik ns)
|
||||
# → outpost validates session + passkey step-up
|
||||
# → http://esphome.home-assistant.svc:80 (esphome :6052)
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ak-outpost-esphome-gate
|
||||
namespace: home-assistant
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: ak-outpost-esphome-gate.authentik.svc.cluster.local
|
||||
ports:
|
||||
- port: 9000
|
||||
targetPort: 9000
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: esphome
|
||||
namespace: home-assistant
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-cluster
|
||||
spec:
|
||||
# ESPHome Device Builder has no auth of its own — anyone reaching :6052 can
|
||||
# edit device YAML (incl. wifi/api secrets), compile, and flash/OTA firmware.
|
||||
# So Traefik never routes to it directly; this Ingress backends the Authentik
|
||||
# esphome-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 :6052 (see
|
||||
# home-assistant-ns.yaml). Pattern mirrors shelly-manager-ingress.yaml.
|
||||
#
|
||||
# WebSocket: the dashboard streams build/OTA logs over a websocket; Traefik's
|
||||
# websecure entrypoint passes it through (no response buffering, 180s
|
||||
# idleTimeout). Browser USB flashing uses ESP Web Tools / Web Serial, which
|
||||
# requires a secure context — served here over HTTPS in Chrome/Edge, the
|
||||
# serial link is browser↔USB and never traverses the proxy.
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: esphome.home.renner.uno
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ak-outpost-esphome-gate
|
||||
port:
|
||||
number: 9000
|
||||
tls:
|
||||
- secretName: esphome-tls
|
||||
hosts:
|
||||
- esphome.home.renner.uno
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: esphome-data
|
||||
namespace: home-assistant
|
||||
annotations:
|
||||
kustomize.toolkit.fluxcd.io/prune: disabled
|
||||
spec:
|
||||
# Single writer (Recreate strategy on the Deployment). Holds the device YAML
|
||||
# configs, secrets.yaml, and the PlatformIO/GCC build cache — the cache is
|
||||
# what makes the 10Gi (vs shelly-manager's 1Gi): a first compile pulls the
|
||||
# full toolchain + framework per platform.
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: ceph-block
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: esphome
|
||||
namespace: home-assistant
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: esphome
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
@@ -25,6 +25,11 @@ resources:
|
||||
- shelly-manager-service.yaml
|
||||
- shelly-manager-gate-service.yaml
|
||||
- shelly-manager-ingress.yaml
|
||||
- esphome-pvc.yaml
|
||||
- esphome-deployment.yaml
|
||||
- esphome-service.yaml
|
||||
- esphome-gate-service.yaml
|
||||
- esphome-ingress.yaml
|
||||
configMapGenerator:
|
||||
# nginx config for the reverse-proxy sidecar. Hash suffix disabled so
|
||||
# the cm name stays stable; editing proxy.conf does not auto-roll the
|
||||
|
||||
@@ -61,6 +61,19 @@ spec:
|
||||
- ports:
|
||||
- port: "8080"
|
||||
protocol: TCP
|
||||
# esphome (ESPHome Device Builder, :6052) is likewise reached only through
|
||||
# its own Authentik gate outpost. Unlike the :8080 vscode/shelly rules
|
||||
# above, :6052 is unique to esphome, so this namespace-wide (endpointSelector
|
||||
# {}) toPorts grant doesn't silently apply to any other app — the
|
||||
# fromEndpoints selector still scopes it to the esphome-gate outpost.
|
||||
- fromEndpoints:
|
||||
- matchLabels:
|
||||
"k8s:io.kubernetes.pod.namespace": authentik
|
||||
goauthentik.io/outpost-name: esphome-gate
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "6052"
|
||||
protocol: TCP
|
||||
# Prometheus scrape: kube-prometheus-stack hits HA's /api/prometheus.
|
||||
- fromEndpoints:
|
||||
- matchLabels:
|
||||
|
||||
Reference in New Issue
Block a user