network-policies: agent pods -> shelly-manager API via header-stripping :8081 (REN-931, REN-935) #1161

Merged
Ghost merged 2 commits from atlas/ren-931-agent-shelly-manager into master 2026-10-06 20:00:49 +00:00
6 changed files with 175 additions and 1 deletions
@@ -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
@@ -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;
}
}
@@ -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
@@ -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
@@ -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
@@ -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