# ============================================================================= # NetworkPolicy (TASK-0135, phase-15) — per-app ingress+egress allow-lists, # ns `factory`. # # DESIGN CHOICE (recorded in the TASK-0135 report, AGENTS rule 29): # - ONE policy per app pod (api, web, worker, realtime) with BOTH # policyTypes [Ingress, Egress] → a default-deny EFFECT for those four # pods only, with the explicit edges below. # - NO namespace-wide default-deny: the namespace also hosts the # coordinator-owned postgres/redis/pgbouncer/replica workloads # (infrastructure/kubernetes/postgres/**, redis.yaml — NOT touched by # this task), whose edges (postgres→S3 for wal-g WAL ship + base # backups, replica→primary WAL streaming, pgbouncer→postgres) are owned # by that workstream. A catch-all `podSelector: {}` default-deny would # silently break those pods until their own policies exist. If the # postgres workstream later adds its own policies, a namespace default- # deny can be added on top. # # VERIFICATION NOTE (staging-only): a wrong policy breaks the app. The # release pipeline's smoke suite (login + page create + search) exercises # every edge below; a curl matrix is the deny-test. Not runnable in this # environment (AGENTS rule 29 — recorded in the TASK-0135 report). # # EDGE MAP (verified against the code this task): # ingress-nginx → web:3000 (/), api:4000 (/api), realtime:4001 (/ws) # web → api:4000 (server-side: the /health/ready route's # liveness fetch; client traffic is browser→nginx) # realtime → api:4000 (internal endpoints, http-store.ts), # redis:6379 (presence + update log), # postgres 6432/5432 (prisma-store.ts) # api → postgres/pgbouncer 6432+5432, redis:6379, opensearch:9200, # OTLP collector:4318, https/mail/webhooks 443+587+80 # worker → postgres/pgbouncer 6432+5432, redis:6379, opensearch:9200, # OTLP collector:4318, https/mail/webhooks 443+587+80 # monitoring → app metrics ports (ingress side of each policy below) # Broad-egress note: webhook deliveries are to arbitrary user-configured # URLs → 443/80/587 to any namespace is the one recorded wide edge # (per-host policies are not possible for user-supplied targets in v1). # ============================================================================= # --- api -------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: factory-api namespace: factory labels: app.kubernetes.io/part-of: ai-software-factory spec: podSelector: matchLabels: app.kubernetes.io/name: api policyTypes: ["Ingress", "Egress"] ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx ports: - { port: 4000, protocol: TCP } # web's server-side readiness fetch (apps/web/src/app/health/ready) - from: - podSelector: matchLabels: app.kubernetes.io/name: web ports: - { port: 4000, protocol: TCP } # Prometheus scrape (job factory-api, /metrics on 4000) - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring ports: - { port: 4000, protocol: TCP } egress: # DNS - to: - namespaceSelector: {} ports: - { port: 53, protocol: UDP } - { port: 53, protocol: TCP } # postgres (pgbouncer + primary/replica) + redis - to: - podSelector: matchLabels: app.kubernetes.io/name: pgbouncer - podSelector: matchLabels: app.kubernetes.io/name: postgres - podSelector: matchLabels: app.kubernetes.io/name: redis ports: - { port: 6432, protocol: TCP } - { port: 5432, protocol: TCP } - { port: 6379, protocol: TCP } # opensearch (configmap SEARCH_URL placeholder `opensearch:9200`) - to: - podSelector: {} ports: - { port: 9200, protocol: TCP } # OTLP push (factory-config: otel-collector.monitoring.svc:4318) - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app.kubernetes.io/name: otel-collector ports: - { port: 4318, protocol: TCP } # S3/MinIO + AI provider + mail relay + webhook deliveries (broad, see header) - to: - namespaceSelector: {} ports: - { port: 443, protocol: TCP } - { port: 587, protocol: TCP } - { port: 80, protocol: TCP } --- # --- web ---------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: factory-web namespace: factory labels: app.kubernetes.io/part-of: ai-software-factory spec: podSelector: matchLabels: app.kubernetes.io/name: web policyTypes: ["Ingress", "Egress"] ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx ports: - { port: 3000, protocol: TCP } - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring ports: - { port: 3000, protocol: TCP } # scrape /api/metrics (path not matchable) egress: - to: - namespaceSelector: {} ports: - { port: 53, protocol: UDP } - { port: 53, protocol: TCP } # api (readiness fetch) + OTLP push - to: - podSelector: matchLabels: app.kubernetes.io/name: api ports: - { port: 4000, protocol: TCP } - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app.kubernetes.io/name: otel-collector ports: - { port: 4318, protocol: TCP } --- # --- worker ------------------------------------------------------------------- # The worker has NO Service (its health/metrics server binds the pod IP:4003) # → only Prometheus (pod-SD job factory-worker) ever reaches it. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: factory-worker namespace: factory labels: app.kubernetes.io/part-of: ai-software-factory spec: podSelector: matchLabels: app.kubernetes.io/name: worker policyTypes: ["Ingress", "Egress"] ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring ports: - { port: 4003, protocol: TCP } egress: - to: - namespaceSelector: {} ports: - { port: 53, protocol: UDP } - { port: 53, protocol: TCP } - to: - podSelector: matchLabels: app.kubernetes.io/name: pgbouncer - podSelector: matchLabels: app.kubernetes.io/name: postgres - podSelector: matchLabels: app.kubernetes.io/name: redis ports: - { port: 6432, protocol: TCP } - { port: 5432, protocol: TCP } - { port: 6379, protocol: TCP } - to: - podSelector: {} ports: - { port: 9200, protocol: TCP } - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app.kubernetes.io/name: otel-collector ports: - { port: 4318, protocol: TCP } - to: - namespaceSelector: {} ports: - { port: 443, protocol: TCP } - { port: 587, protocol: TCP } - { port: 80, protocol: TCP } --- # --- realtime ----------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: factory-realtime namespace: factory labels: app.kubernetes.io/part-of: ai-software-factory spec: podSelector: matchLabels: app.kubernetes.io/name: realtime policyTypes: ["Ingress", "Egress"] ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx ports: - { port: 4001, protocol: TCP } - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring ports: - { port: 4001, protocol: TCP } egress: - to: - namespaceSelector: {} ports: - { port: 53, protocol: UDP } - { port: 53, protocol: TCP } - to: - podSelector: matchLabels: app.kubernetes.io/name: api - podSelector: matchLabels: app.kubernetes.io/name: redis - podSelector: matchLabels: app.kubernetes.io/name: pgbouncer - podSelector: matchLabels: app.kubernetes.io/name: postgres ports: - { port: 4000, protocol: TCP } - { port: 6379, protocol: TCP } - { port: 6432, protocol: TCP } - { port: 5432, protocol: TCP } - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app.kubernetes.io/name: otel-collector ports: - { port: 4318, protocol: TCP }