diff --git a/kubernetes/apps/home-assistant/kustomization.yaml b/kubernetes/apps/home-assistant/kustomization.yaml index 3cdf773d..289c6ac9 100644 --- a/kubernetes/apps/home-assistant/kustomization.yaml +++ b/kubernetes/apps/home-assistant/kustomization.yaml @@ -123,5 +123,13 @@ configMapGenerator: namespace: home-assistant files: - mqtt-accounts.yaml + # shelly-manager api-proxy sidecar config (REN-935). Hash suffix disabled + # (name stays stable), so editing shelly-manager-api-proxy.conf does not + # auto-roll the pod — restart manually with + # `kubectl rollout restart deploy/shelly-manager -n home-assistant`. + - name: shelly-manager-api-proxy + namespace: home-assistant + files: + - api.conf.template=shelly-manager-api-proxy.conf generatorOptions: disableNameSuffixHash: true diff --git a/kubernetes/apps/home-assistant/shelly-manager-api-proxy.conf b/kubernetes/apps/home-assistant/shelly-manager-api-proxy.conf new file mode 100644 index 00000000..d42f3d12 --- /dev/null +++ b/kubernetes/apps/home-assistant/shelly-manager-api-proxy.conf @@ -0,0 +1,30 @@ +# API-key-only listener for shelly-manager (REN-931, REN-935). Rendered by the +# nginx-unprivileged entrypoint (envsubst, filtered to POD_IP) into +# /etc/nginx/conf.d/ at start. +# +# shelly-manager signs in whoever X-authentik-username names when the peer is +# in Auth__ReverseProxy__TrustedProxies (the pod CIDR). Only the Authentik +# outpost may reach :8080 directly. Paperclip agent pods reach this :8081 +# instead, which drops the identity header, so every request here falls through +# to the API key (or the login cookie) and a forged header gets 401. +# +# Bound to ${POD_IP}, not 0.0.0.0, so it stays off the net1 macvlan (see the +# ASPNETCORE_URLS comment in shelly-manager-deployment.yaml). +server { + listen ${POD_IP}:8081; + server_tokens off; + client_max_body_size 64m; + + location / { + proxy_pass http://${POD_IP}:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + # Empty value = nginx does not pass the header at all, whatever the + # client's case or how many copies it sent. X-Forwarded-User is the + # app's default HeaderName, dropped too in case the setting changes. + # Header names with underscores never pass (ignore_invalid_headers). + proxy_set_header X-authentik-username ""; + proxy_set_header X-Forwarded-User ""; + proxy_read_timeout 300s; + } +} diff --git a/kubernetes/apps/home-assistant/shelly-manager-deployment.yaml b/kubernetes/apps/home-assistant/shelly-manager-deployment.yaml index 955a9453..0f3c09b4 100644 --- a/kubernetes/apps/home-assistant/shelly-manager-deployment.yaml +++ b/kubernetes/apps/home-assistant/shelly-manager-deployment.yaml @@ -121,7 +121,9 @@ spec: # outpost pod's address is dynamic, so this trusts the cluster pod # CIDR and leans on the namespace CNP to keep other pods off the # app port. ADMIN_PASSWORD (above) stays as break-glass, e.g. for - # port-forward access when authentik is down. + # port-forward access when authentik is down. Never open :8080 to + # another client: they get the api-proxy sidecar's :8081 (below), + # which drops the header (REN-935). - name: Auth__ReverseProxy__Enabled value: "true" - name: Auth__ReverseProxy__HeaderName @@ -168,7 +170,62 @@ spec: memory: 128Mi limits: memory: 512Mi + # API-key-only listener for clients other than the Authentik outpost + # (Paperclip agents, REN-931). nginx on ${POD_IP}:8081 proxies to + # :8080 and drops X-authentik-username, so the ProxyHeader sign-in + # cannot be reached through it. Config and reasoning: + # shelly-manager-api-proxy.conf. Service: shelly-manager-api. + # No probes: it only serves the agent path, and a probe failure would + # also pull the outpost's endpoint out of the shelly-manager Service. + - name: api-proxy + image: nginxinc/nginx-unprivileged:1.31-alpine@sha256:26b0bf6fbf07297983cb341998d79c831508787de26627dd2a112321b9c3a4af + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: NGINX_ENVSUBST_FILTER + value: "^POD_IP$" + ports: + - name: api + containerPort: 8081 + protocol: TCP + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + memory: 64Mi + volumeMounts: + - name: api-proxy-config + mountPath: /etc/nginx/templates + readOnly: true + # envsubst output of the template above. + - name: api-proxy-conf-d + mountPath: /etc/nginx/conf.d + # pid file and proxy temp paths of the unprivileged image. + - name: api-proxy-tmp + mountPath: /tmp volumes: - name: data persistentVolumeClaim: claimName: shelly-manager-data + - name: api-proxy-config + configMap: + name: shelly-manager-api-proxy + - name: api-proxy-conf-d + emptyDir: + medium: Memory + sizeLimit: 1Mi + - name: api-proxy-tmp + emptyDir: + medium: Memory + sizeLimit: 32Mi diff --git a/kubernetes/apps/home-assistant/shelly-manager-service.yaml b/kubernetes/apps/home-assistant/shelly-manager-service.yaml index 795a85d9..8237c744 100644 --- a/kubernetes/apps/home-assistant/shelly-manager-service.yaml +++ b/kubernetes/apps/home-assistant/shelly-manager-service.yaml @@ -11,3 +11,21 @@ spec: port: 80 targetPort: http protocol: TCP +--- +# API-key-only entry for Paperclip agent pods (REN-931, REN-935): the +# api-proxy sidecar, which drops the X-authentik-username header. Agents call +# http://shelly-manager-api.home-assistant.svc/api/v1/... with the API key. +# The CNPs admit agents to :8081 only; :8080 stays outpost-only. +apiVersion: v1 +kind: Service +metadata: + name: shelly-manager-api + namespace: home-assistant +spec: + selector: + app.kubernetes.io/name: shelly-manager + ports: + - name: http + port: 80 + targetPort: api + protocol: TCP diff --git a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml index 9d639783..b6e33197 100644 --- a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml +++ b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml @@ -407,3 +407,32 @@ spec: - ports: - port: "5432" protocol: TCP +--- +# Paperclip agent pods reach the shelly-manager API on :8081 (REN-931), the +# api-proxy sidecar that drops X-authentik-username. NEVER :8080: the app signs +# in whoever that header names when the peer is in the pod CIDR, so :8080 is +# for the Authentik outpost only (REN-935). Through :8081 every call needs the +# API key. Egress half: agent-egress-shelly-manager in paperclip-renner-ns.yaml. +# +# DEDICATED policy selecting shelly-manager alone, not a rule on the +# endpointSelector:{} default-deny (see the caution there). +# +# Revert: delete this document and agent-egress-shelly-manager. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: shelly-manager-agent-ingress + namespace: home-assistant +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: shelly-manager + ingress: + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": paperclip-renner + paperclip.io/role: agent + toPorts: + - ports: + - port: "8081" + protocol: TCP diff --git a/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml b/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml index 5b9cc63d..43f40ff0 100644 --- a/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml +++ b/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml @@ -134,3 +134,35 @@ spec: - ports: - port: "443" protocol: TCP +--- +# Agent pods reach the shelly-manager API (REN-931, for Argus's REN-929 live +# MQTT credential test). Service shelly-manager-api:80 -> targetPort api = +# container port 8081, the api-proxy sidecar that drops X-authentik-username; +# socket-LB translates before egress policy, so the rule names 8081. Never +# :8080: there a forged header signs in without a key (REN-935). Ingress side: +# shelly-manager-agent-ingress in home-assistant-ns.yaml. +# +# Selects every agent pod: the sandbox pods carry no per-agent label (the +# plugin writes the run id into `paperclip.io/agent-id`, see above), so it +# cannot be narrowed to Argus. The TCP path is not the grant: every call on +# :8081 needs the shelly-manager API key, which only Argus holds (REN-930). +# +# Revert: delete this document and shelly-manager-agent-ingress. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: agent-egress-shelly-manager + namespace: paperclip-renner +spec: + endpointSelector: + matchLabels: + paperclip.io/role: agent + egress: + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": home-assistant + app.kubernetes.io/name: shelly-manager + toPorts: + - ports: + - port: "8081" + protocol: TCP