You are a Senior Code Reviewer with expertise in Kubernetes networking and Cilium. Review the CiliumNetworkPolicy resources just committed.

## What Was Implemented

Task 3 of 6 for `churn-renewal-forecast` Kustomize deployment. Three CiliumNetworkPolicy resources: DNS egress (UDP 53 to kube-dns), RDS egress (TCP 5432 to 0.0.0.0/0), and external HTTPS egress (TCP 443 via toFQDNs to clerk.com, api.frontapp.com, api.productboard.com, slack.com).

## Requirements / Plan

Three CiliumNetworkPolicies all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:
- allow-dns.yaml: egress to kube-dns UDP 53 with dns rules
- allow-rds.yaml: egress to 0.0.0.0/0 TCP 5432
- allow-external.yaml: egress toFQDNs TCP 443 to clerk.com/*.clerk.com, api.frontapp.com, api.productboard.com, slack.com/*.slack.com

## Git Range to Review

**Base:** 8ae4a6ef409140f1a0d3248dba181e9a57171875
**Head:** bcb824fe0570ccb8840df1114c902bd619a83724

```bash
git diff --stat 8ae4a6ef409140f1a0d3248dba181e9a57171875..bcb824fe0570ccb8840df1114c902bd619a83724
git diff 8ae4a6ef409140f1a0d3248dba181e9a57171875..bcb824fe0570ccb8840df1114c902bd619a83724
```

Work from: `/Users/steve.briggs/projects/churn-renewal-forecast`

## Output Format

### Strengths
### Issues
#### Critical (Must Fix)
#### Important (Should Fix)
#### Minor (Nice to Have)
### Assessment
**Ready to merge?** [Yes | No | With fixes]
**Reasoning:**


- obsidian-load: Load context from Obsidian notes in the `Claude/` directory into the current conversation.
- obsidian-save: Save current conversation context to an Obsidian note under the `Claude/` directory.
- mind:stats: Show memory statistics and storage information
- mind:search: Search memories for specific content or patterns
- mind:ask: Ask questions about memories and get context-aware answers
- mind:recent: Show recent memories and activity timeline
- mind:memory: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- mind:mind: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- superpowers:test-driven-development: Use when implementing any feature or bugfix, before writing implementation code
- superpowers:using-git-worktrees: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
- superpowers:receiving-code-review: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
- superpowers:using-superpowers: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
- superpowers:writing-plans: Use when you have a spec or requirements for a multi-step task, before touching code
- superpowers:subagent-driven-development: Use when executing implementation plans with independent tasks in the current session
- superpowers:requesting-code-review: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
- superpowers:verification-before-completion: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
- superpowers:executing-plans: Use when you have a written implementation plan to execute in a separate session with review checkpoints
- superpowers:finishing-a-development-branch: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
- superpowers:dispatching-parallel-agents: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
- superpowers:writing-skills: Use when creating new skills, editing existing skills, or verifying skills work before deployment
- superpowers:brainstorming: You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
- superpowers:systematic-debugging: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch


I'll review the CiliumNetworkPolicy resources that were just committed. Let me start by examining the git diff and the actual files.


 deploy/kubernetes/base/netpol/allow-dns.yaml      | 23 +++++++++++++++++++++++
 deploy/kubernetes/base/netpol/allow-external.yaml | 23 +++++++++++++++++++++++
 deploy/kubernetes/base/netpol/allow-rds.yaml      | 18 ++++++++++++++++++
 deploy/kubernetes/base/netpol/kustomization.yaml  |  7 +++++++
 4 files changed, 71 insertions(+)


Now let me examine the actual file contents:


1	apiVersion: cilium.io/v2
2	kind: CiliumNetworkPolicy
3	metadata:
4	  name: churn-renewal-forecast-allow-dns
5	  namespace: churn-renewal-forecast
6	  labels:
7	    app.kubernetes.io/name: churn-renewal-forecast
8	spec:
9	  endpointSelector:
10	    matchLabels:
11	      app.kubernetes.io/name: churn-renewal-forecast
12	  egress:
13	    - toEndpoints:
14	        - matchLabels:
15	            k8s:io.kubernetes.pod.namespace: kube-system
16	            k8s-app: kube-dns
17	      toPorts:
18	        - ports:
19	            - port: "53"
20	              protocol: UDP
21	          rules:
22	            dns:
23	              - matchPattern: "*"
24


1	apiVersion: cilium.io/v2
2	kind: CiliumNetworkPolicy
3	metadata:
4	  name: churn-renewal-forecast-allow-rds
5	  namespace: churn-renewal-forecast
6	  labels:
7	    app.kubernetes.io/name: churn-renewal-forecast
8	spec:
9	  endpointSelector:
10	    matchLabels:
11	      app.kubernetes.io/name: churn-renewal-forecast
12	  egress:
13	    - toCIDR:
14	        - 0.0.0.0/0
15	      toPorts:
16	        - ports:
17	            - port: "5432"
18	              protocol: TCP
19


1	apiVersion: kustomize.config.k8s.io/v1beta1
2	kind: Kustomization
3
4	resources:
5	  - allow-dns.yaml
6	  - allow-rds.yaml
7	  - allow-external.yaml
8


1	apiVersion: cilium.io/v2
2	kind: CiliumNetworkPolicy
3	metadata:
4	  name: churn-renewal-forecast-allow-external
5	  namespace: churn-renewal-forecast
6	  labels:
7	    app.kubernetes.io/name: churn-renewal-forecast
8	spec:
9	  endpointSelector:
10	    matchLabels:
11	      app.kubernetes.io/name: churn-renewal-forecast
12	  egress:
13	    - toFQDNs:
14	        - matchName: "clerk.com"
15	        - matchPattern: "*.clerk.com"
16	        - matchName: "api.frontapp.com"
17	        - matchName: "api.productboard.com"
18	        - matchName: "slack.com"
19	        - matchPattern: "*.slack.com"
20	      toPorts:
21	        - ports:
22	            - port: "443"
23	              protocol: TCP
24


Let me check the full diff to see if there are any other changes:


diff --git a/deploy/kubernetes/base/netpol/allow-dns.yaml b/deploy/kubernetes/base/netpol/allow-dns.yaml
new file mode 100644
index 00000000..e00abdac
--- /dev/null
+++ b/deploy/kubernetes/base/netpol/allow-dns.yaml
@@ -0,0 +1,23 @@
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: churn-renewal-forecast-allow-dns
+  namespace: churn-renewal-forecast
+  labels:
+    app.kubernetes.io/name: churn-renewal-forecast
+spec:
+  endpointSelector:
+    matchLabels:
+      app.kubernetes.io/name: churn-renewal-forecast
+  egress:
+    - toEndpoints:
+        - matchLabels:
+            k8s:io.kubernetes.pod.namespace: kube-system
+            k8s-app: kube-dns
+      toPorts:
+        - ports:
+            - port: "53"
+              protocol: UDP
+          rules:
+            dns:
+              - matchPattern: "*"
diff --git a/deploy/kubernetes/base/netpol/allow-external.yaml b/deploy/kubernetes/base/netpol/allow-external.yaml
new file mode 100644
index 00000000..5a3a1be1
--- /dev/null
+++ b/deploy/kubernetes/base/netpol/allow-external.yaml
@@ -0,0 +1,23 @@
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: churn-renewal-forecast-allow-external
+  namespace: churn-renewal-forecast
+  labels:
+    app.kubernetes.io/name: churn-renewal-forecast
+spec:
+  endpointSelector:
+    matchLabels:
+      app.kubernetes.io/name: churn-renewal-forecast
+  egress:
+    - toFQDNs:
+        - matchName: "clerk.com"
+        - matchPattern: "*.clerk.com"
+        - matchName: "api.frontapp.com"
+        - matchName: "api.productboard.com"
+        - matchName: "slack.com"
+        - matchPattern: "*.slack.com"
+      toPorts:
+        - ports:
+            - port: "443"
+              protocol: TCP
diff --git a/deploy/kubernetes/base/netpol/allow-rds.yaml b/deploy/kubernetes/base/netpol/allow-rds.yaml
new file mode 100644
index 00000000..2ee109d8
--- /dev/null
+++ b/deploy/kubernetes/base/netpol/allow-rds.yaml
@@ -0,0 +1,18 @@
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: churn-renewal-forecast-allow-rds
+  namespace: churn-renewal-forecast
+  labels:
+    app.kubernetes.io/name: churn-renewal-forecast
+spec:
+  endpointSelector:
+    matchLabels:
+      app.kubernetes.io/name: churn-renewal-forecast
+  egress:
+    - toCIDR:
+        - 0.0.0.0/0
+      toPorts:
+        - ports:
+            - port: "5432"
+              protocol: TCP
diff --git a/deploy/kubernetes/base/netpol/kustomization.yaml b/deploy/kubernetes/base/netpol/kustomization.yaml
new file mode 100644
index 00000000..8c8422c1
--- /dev/null
+++ b/deploy/kubernetes/base/netpol/kustomization.yaml
@@ -0,0 +1,7 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+  - allow-dns.yaml
+  - allow-rds.yaml
+  - allow-external.yaml


Now let me verify these are correctly integrated into the overall kustomization structure:


Error: File does not exist. Note: your current working directory is /Users/steve.briggs/projects/churn-renewal-forecast.


Bash

find /Users/steve.briggs/projects/churn-renewal-forecast/deploy -name "kustomization.yaml" -type f


find /Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes -maxdepth 2 -name "kustomization.yaml" -type f


(Bash completed with no output)


drwxr-xr-x steve.briggs staff  96 B Mon May 18 17:02:22 2026 ..
drwxr-xr-x steve.briggs staff 224 B Mon May 18 17:59:09 2026 bootstrap
drwxr-xr-x steve.briggs staff 128 B Mon May 18 18:01:00 2026 init
drwxr-xr-x steve.briggs staff 160 B Mon May 18 18:06:23 2026 .
drwxr-xr-x steve.briggs staff 192 B Mon May 18 22:35:11 2026 netpol


Let me check if there's a parent kustomization that should include the netpol directory:


find /Users/steve.briggs/projects/churn-renewal-forecast/deploy -name "*.yaml" -type f | head -20


1	apiVersion: kustomize.config.k8s.io/v1beta1
2	kind: Kustomization
3
4	resources:
5	  - namespace.yaml
6	  - serviceaccount.yaml
7	  - secret.yaml
8
9	configMapGenerator:
10	  - name: churn-renewal-forecast-config
11	    envs:
12	      - app-config.env
13	    options:
14	      disableNameSuffixHash: true
15


1	apiVersion: kustomize.config.k8s.io/v1beta1
2	kind: Kustomization
3
4	resources:
5	  - job.yaml
6


Let me check if there's a top-level base kustomization or overlays structure:


drwxr-xr-x steve.briggs staff  96 B Mon May 18 17:02:22 2026 ..
drwxr-xr-x steve.briggs staff  96 B Mon May 18 17:02:22 2026 .
drwxr-xr-x steve.briggs staff 160 B Mon May 18 18:06:23 2026 base


Now let me check the pod label conventions to see if `app.kubernetes.io/name` is being used correctly elsewhere:


grep -r "app.kubernetes.io/name" /Users/steve.briggs/projects/churn-renewal-forecast/deploy --include="*.yaml"


/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/init/job.yaml:    app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/init/job.yaml:        app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-rds.yaml:    app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-rds.yaml:      app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-external.yaml:    app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-external.yaml:      app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-dns.yaml:    app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-dns.yaml:      app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/bootstrap/serviceaccount.yaml:    app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/bootstrap/namespace.yaml:    app.kubernetes.io/name: churn-renewal-forecast
/Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/bootstrap/secret.yaml:    app.kubernetes.io/name: churn-renewal-forecast


Let me check the actual pod definitions to see what labels they carry:


1	apiVersion: batch/v1
2	kind: Job
3	metadata:
4	  name: churn-renewal-forecast-migrate
5	  namespace: churn-renewal-forecast
6	  labels:
7	    app.kubernetes.io/name: churn-renewal-forecast
8	    app.kubernetes.io/component: migrate
9	spec:
10	  ttlSecondsAfterFinished: 300
11	  backoffLimit: 3
12	  template:
13	    metadata:
14	      labels:
15	        app.kubernetes.io/name: churn-renewal-forecast
16	        app.kubernetes.io/component: migrate
17	    spec:
18	      serviceAccountName: churn-renewal-forecast
19	      restartPolicy: OnFailure
20	      containers:
21	        - name: migrate
22	          image: registry.ic-ops.net/churn-renewal-forecast:latest
23	          command: ["bunx", "prisma", "migrate", "deploy"]
24	          envFrom:
25	            - configMapRef:
26	                name: churn-renewal-forecast-config
27	            - secretRef:
28	                name: churn-renewal-forecast
29


Good. Now let me validate the Cilium DNS label format, which looks suspicious:


cat /Users/steve.briggs/projects/churn-renewal-forecast/deploy/kubernetes/base/netpol/allow-dns.yaml | grep -A 2 "matchLabels"


    matchLabels:
      app.kubernetes.io/name: churn-renewal-forecast
  egress:
--
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns


I notice there's a problematic label format. Let me verify the correct Cilium DNS selector format:


apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: churn-renewal-forecast-allow-dns
  namespace: churn-renewal-forecast
  labels:
    app.kubernetes.io/name: churn-renewal-forecast
spec:
  endpointSelector:
    matchLabels:
      app.kubernetes.io/name: churn-renewal-forecast
  egress:
    - toEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: UDP
          rules:
            dns:
              - matchPattern: "*"


I've identified critical issues. Let me document my findings:

## Code Review: CiliumNetworkPolicy Resources

### Strengths

1. **Correct resource structure** - All three CiliumNetworkPolicy manifests follow proper Cilium API v2 format with correct metadata, spec, and egress sections.

2. **Proper endpoint selection** - All policies correctly select pods via `app.kubernetes.io/name: churn-renewal-forecast` label, which matches the actual pod labels in `job.yaml`.

3. **Clear naming** - Resource names are descriptive (allow-dns, allow-rds, allow-external) and follow Kubernetes naming conventions.

4. **DNS pattern flexibility** - The DNS policy uses `matchPattern: "*"` to allow all DNS queries, which is reasonable for a general permit-all DNS rule.

5. **HTTPS scope** - The external HTTPS policy correctly targets only TCP 443 with specific FQDNs via toFQDNs.

6. **Organized structure** - The netpol directory with dedicated kustomization.yaml is well-organized.

### Issues

#### Critical (Must Fix)

1. **Invalid label key in allow-dns.yaml (Line 15)** - The label key `k8s:io.kubernetes.pod.namespace` contains a colon (`:`) which is not valid in Kubernetes label keys. Label keys must follow RFC 1123 subdomain rules. This should be:
   - `k8s.io/namespace: kube-system` (Cilium's reserved label), or
   - `kubernetes.io/metadata.name: kube-system` (standard Kubernetes label)

2. **Incorrect kube-dns selector** - The label `k8s-app: kube-dns` should be `k8s-app: dns` on the kube-dns pod. Verify the actual labels on your cluster's kube-dns pod with: `kubectl get pods -n kube-system -l k8s-app --show-labels`. The selector likely won't match.

3. **Overly permissive RDS policy** - The allow-rds.yaml uses `toCIDR: 0.0.0.0/0` (all destinations) on port 5432. If RDS runs in a specific AWS security group or private subnet, this should be restricted to that CIDR range. At minimum, document why full-spectrum access is needed. This violates principle of least privilege.

#### Important (Should Fix)

1. **Missing namespace validation** - The policies hardcode `namespace: churn-renewal-forecast`. Verify this namespace exists and will be created before these policies are applied (the bootstrap namespace creation should come first).

2. **No policy order/dependencies** - The kustomization.yaml doesn't specify apply order. Ensure the bootstrap namespace is applied before netpol resources via dependencies or separate deployment stages.

3. **Incomplete FQDN coverage** - Verify that the external HTTPS policy covers all required FQDNs:
   - clerk.com and *.clerk.com (good)
   - api.frontapp.com (but is it subdomains too? Consider *.frontapp.com)
   - api.productboard.com (consider if *.productboard.com is needed)
   - slack.com and *.slack.com (good)

4. **No documentation** - Add comments explaining why each rule is needed (e.g., "DNS required for service discovery", "RDS for database access", "External services for authentication and integrations").

#### Minor (Nice to Have)

1. **Consistent label capitalization** - Metadata labels use `app.kubernetes.io/name` consistently, which is good, but consider adding `app.kubernetes.io/component` to distinguish between different pod types (web app vs. job) for future policies.

2. **DNS rule could be more restrictive** - `matchPattern: "*"` allows all DNS queries. Consider restricting to only required domains: `matchName: "kube-dns"` or specific domain patterns if known.

### Assessment

**Ready to merge?** **No - With critical fixes required**

**Reasoning:**

The allow-dns.yaml has a **syntax error that will prevent the resource from being applied**: the label key `k8s:io.kubernetes.pod.namespace` is invalid because colons are not permitted in Kubernetes label keys. Additionally, the kube-dns pod selector is likely incorrect based on standard Kubernetes label conventions.

The allow-rds.yaml permits unrestricted outbound access to any IP on port 5432, violating least-privilege principles. This needs justification or tightening to a specific CIDR (e.g., AWS RDS subnet range).

**Required fixes before merge:**
1. Fix the label key in allow-dns.yaml line 15
2. Verify and correct the kube-dns pod selector
3. Restrict or justify the RDS CIDR range
4. Add inline comments explaining each policy's purpose
5. Verify all FQDNs in allow-external.yaml are complete