Skip to content

Cilium PR2 — Install Cilium Alongside Calico (Migration Mode) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Install Cilium 1.19.3 alongside the existing Calico deployment in upstream’s prescribed live-migration mode. Pre-stage the bastion’s CiliumNetworkPolicy (which won’t enforce until PR4 due to policyEnforcementMode: "never"). No workload pod is migrated in this PR — that’s PR3.

Architecture: Run the ansible/roles/cilium role added in PR1. Cilium’s Helm install lands with cni.customConf: true so it doesn’t write a CNI config file; the CiliumNodeConfig CR (also applied) matches no nodes yet. Calico continues full enforcement of all existing NetworkPolicies. The bastion’s new CiliumNetworkPolicy is created in cf-ssh-bastion namespace via ArgoCD; it does NOT enforce until PR4.

Tech Stack: Ansible playbook from PR1, Velero, ArgoCD, cilium-cli (local workstation), kubectl.

Reference spec: docs/engineering/specs/2026-05-10-calico-to-cilium-design.md (commit fe05a87c).


Files created:

  • argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml — pre-staged CiliumNetworkPolicy translating the existing Calico policy.

Files modified:

  • argocd/app-configs/cf-ssh-bastion/kustomization.yaml — add the new policy file to the kustomization.

Cluster state changes:

  • New namespace state in kube-system: cilium DaemonSet, cilium-operator Deployment, hubble-relay, hubble-ui, CiliumNodeConfig resource
  • New cluster-scoped CRDs: Cilium’s own (CiliumNode, CiliumEndpoint, CiliumIdentity, CiliumNetworkPolicy, CiliumClusterwideNetworkPolicy, CiliumNodeConfig, CiliumEnvoyConfig, etc.) + Gateway API v1.4.1 Standard CRDs

Task 1: Operator workstation prerequisite — cilium-cli

Section titled “Task 1: Operator workstation prerequisite — cilium-cli”

Files: none (workstation setup)

  • Step 1: Verify cilium-cli is installed

Run: cilium version --client 2>&1 | head -3

If not installed:

Terminal window
# macOS
brew install cilium-cli
# Linux (x86_64) — see https://github.com/cilium/cilium-cli/releases for current
CILIUM_CLI_VERSION="v0.16.20" # check releases page for version matching Cilium 1.19.x
curl -L --remote-name-all "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz"
tar xzf cilium-linux-amd64.tar.gz
sudo mv cilium /usr/local/bin/
rm cilium-linux-amd64.tar.gz

Verify: cilium version --client returns a version line. Expected client version: matches or exceeds 0.16.x.

  • Step 2: Verify connectivity to the cluster (read-only)

Run: kubectl --context fzymgc-house get nodes Expected: 8 nodes listed, all Ready, all running Calico (look for calico-system pods on each).


Task 2: Pre-flight — Velero backup and Gateway API CRD check

Section titled “Task 2: Pre-flight — Velero backup and Gateway API CRD check”

Files: none (read-only verification + backup)

  • Step 1: Take a Velero backup
Terminal window
BACKUP_NAME="pre-cilium-install-$(date +%Y%m%d-%H%M%S)"
velero backup create "${BACKUP_NAME}" --wait
echo "Backup name: ${BACKUP_NAME}"

Expected: Velero reports backup completed Completed with no errors. Save the backup name; you’ll reference it if rollback is needed.

  • Step 2: Clean up orphaned Gateway API resources from PR1’s ownership transfer

PR1 removed argocd/app-configs/gateway-api/ and argocd/cluster-app/templates/gateway-api.yaml, transferring Gateway API ownership to the Cilium Ansible role. ArgoCD prunes the Application object on PR1 merge, but the managed CRDs + the safe-upgrades ValidatingAdmissionPolicy do NOT cascade (per repo memory argocd-app-deletion-doesnt-cascade-without-finalizer). This step deletes them so the Cilium role’s v1.4.1 Standard CRD install can succeed.

Terminal window
# Delete the safe-upgrades policy + binding first (they block CRD changes below)
kubectl delete validatingadmissionpolicybinding safe-upgrades.gateway.networking.k8s.io --ignore-not-found
kubectl delete validatingadmissionpolicy safe-upgrades.gateway.networking.k8s.io --ignore-not-found
# Delete the 12 orphaned Gateway API CRDs from the prior gateway-api Application
kubectl delete crd \
listenersets.gateway.networking.k8s.io \
xmeshes.gateway.networking.x-k8s.io \
xbackendtrafficpolicies.gateway.networking.x-k8s.io \
tcproutes.gateway.networking.k8s.io \
udproutes.gateway.networking.k8s.io \
tlsroutes.gateway.networking.k8s.io \
backendtlspolicies.gateway.networking.k8s.io \
gateways.gateway.networking.k8s.io \
gatewayclasses.gateway.networking.k8s.io \
httproutes.gateway.networking.k8s.io \
grpcroutes.gateway.networking.k8s.io \
referencegrants.gateway.networking.k8s.io \
--ignore-not-found

Verify clean state:

Terminal window
kubectl get crd | rg 'gateway\.networking' | wc -l
# Expected: 0
kubectl get validatingadmissionpolicy | rg gateway | wc -l
# Expected: 0

If either count is non-zero, halt and investigate — there’s an unexpected Gateway API installer in the cluster.

Safety check: confirm zero Gateway API resources are in use (this was verified once during PR1 execution, but re-verify in case the cluster changed since):

Terminal window
for kind in gateway gatewayclass httproute grpcroute referencegrant tlsroute tcproute udproute backendtlspolicy; do
COUNT=$(kubectl get "${kind}" -A --no-headers 2>/dev/null | wc -l)
echo "${kind}: ${COUNT}"
done

Expected: all zeros (or “the server doesn’t have a resource type” if the CRD was already deleted, which is fine). If any returns > 0, HALT — there are Gateway API resources in use that would be destroyed.

Note: the safe-upgrades.gateway.networking.k8s.io ValidatingAdmissionPolicy is deleted here, but the Cilium role re-installs it in Phase 1 with the bundle-version floor adjusted to v1.4.0 (the version Cilium manages). The cluster’s guard against unsafe Gateway API CRD downgrades is preserved.

  • Step 3: Verify existing NetworkPolicy inventory
Terminal window
kubectl get networkpolicy -A
echo "---"
kubectl get networkpolicies.crd.projectcalico.org -A
echo "---"
kubectl get globalnetworkpolicies.crd.projectcalico.org -A

Expected: Calico policies under cf-ssh-bastion namespace only (6 policies: bastion-ingress-cloudflared, bastion-allow-kube-dns, bastion-allow-k8s-api, bastion-allow-node-ssh, bastion-allow-icmp, bastion-allow-external-https). No NetworkPolicy (networking.k8s.io) resources expected. Document the actual list in the PR description for traceability.


Task 3: Run the cilium-install Ansible task

Section titled “Task 3: Run the cilium-install Ansible task”

Files: none (operational)

  • Step 1: Run the playbook with cilium-install tag

From the worktree root:

Terminal window
# Capture playbook start time BEFORE running (must be before tee, because tee updates mtime)
date +%s > /tmp/cilium-install.start
ansible-playbook \
-i ansible/inventory/hosts.yml \
ansible/k3s-playbook.yml \
--tags cilium-install \
-v 2>&1 \
| tee /tmp/cilium-install.log

Writing the start timestamp to a dedicated file before the playbook runs ensures Step 5 captures the correct start time. (tee updates the log file’s mtime on every write, so the log’s mtime reflects the END of the run, not the start.)

This runs Phase 5b (Cilium install) from PR1: Gateway API CRDs → Helm install → CiliumNodeConfig apply → DaemonSet/operator/Hubble Relay readiness waits.

Expected: each task reports ok or changed; final play recap shows failed=0. Total runtime: ~5-10 minutes (DaemonSet rollout + Hubble Relay startup).

If a task fails:

  • Install Gateway API ... Standard CRDs failure → check CRD YAML validity, network connectivity to kubectl proxy, kubeconfig path.
  • Install or upgrade Cilium via Helm failure → run helm template locally with the same values to see schema errors.
  • Wait for Cilium DaemonSet timeout → check kubectl get pods -n kube-system -l k8s-app=cilium and kubectl describe pods -n kube-system -l k8s-app=cilium for the failing pods.
  • Step 2: Verify cilium-cli reports OK
Terminal window
cilium status --wait

Expected: Cilium: 8/8 Ready, Operator: 2/2 Ready, Hubble Relay: 1/1 Ready, Cluster Pods: <count>/<count> managed by Cilium — note: many pods at this point are still Calico-managed because no node has been migrated yet. The “managed by Cilium” count reflects pods that landed AFTER the cilium-operator started watching, NOT a migrated count.

cilium status should report no failures.

  • Step 3: Verify Cilium DaemonSet pods on all 8 nodes
Terminal window
kubectl get pods -n kube-system -l k8s-app=cilium -o wide --no-headers | wc -l

Expected: 8.

Terminal window
kubectl get pods -n kube-system -l k8s-app=cilium -o wide --no-headers \
| awk '{print $NF, $3}' | sort

Expected: 8 lines, each showing a node name and Running status.

  • Step 4: Verify CiliumNodeConfig is applied (matching zero nodes)
Terminal window
kubectl get ciliumnodeconfig -n kube-system cilium-default -o yaml | rg -A 3 'nodeSelector'

Expected: matchLabels: { io.cilium.migration/cilium-default: "true" } is present.

Terminal window
kubectl get nodes -l 'io.cilium.migration/cilium-default=true' --no-headers | wc -l

Expected: 0 (no nodes labeled yet — that’s PR3’s job).

  • Step 5: Confirm no workload pod restart from this install
Terminal window
# Read the captured start time (NOT the log's mtime — tee updates mtime on every write)
START_TS=$(cat /tmp/cilium-install.start 2>/dev/null) \
|| { echo "ERROR: /tmp/cilium-install.start missing — was the playbook run with the start-capture step from Task 3 Step 1?"; exit 1; }
echo "Playbook start ts: ${START_TS}"
# Count workload pods that have started since the playbook began (excludes kube-system)
kubectl get pods -A --field-selector status.phase=Running -o json \
| jq --argjson since "${START_TS}" \
'[.items[] | select(.metadata.namespace != "kube-system") | select((.status.startTime // "1970-01-01T00:00:00Z" | fromdateiso8601) > $since) | .metadata.namespace + "/" + .metadata.name] | length'

This shows count of non-kube-system pods started since the playbook began. Expected: 0. The kube-system exclusion isolates the Cilium/Hubble pods from this count; inspect them separately with kubectl get pods -n kube-system --sort-by=.status.startTime. If there are unexpected workload pod restarts, investigate kubectl describe pod/X -n Y for the eviction reason.

  • Step 6: Verify no node is on the Cilium pod CIDR yet
Terminal window
kubectl get pods -A -o json \
| jq -r '[.items[] | select(.status.podIP // "" | startswith("10.245."))] | length'

Expected: 0 (no pods on Cilium CIDR yet — Cilium has cni.customConf: true and no per-node override is active).


Task 4: Create the bastion CiliumNetworkPolicy file

Section titled “Task 4: Create the bastion CiliumNetworkPolicy file”

Files:

  • Create: argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml

  • Step 1: Read the existing Calico policy to anchor the translation

Run: cat argocd/app-configs/cf-ssh-bastion/calico-network-policy.yaml — confirm the 6 policies (cloudflared-ingress, kube-dns egress, k8s-api egress, node-ssh egress, ICMP egress, external-https egress).

  • Step 2: Create the translated CiliumNetworkPolicy file
# CiliumNetworkPolicies for cf-ssh-bastion
# Translated from calico-network-policy.yaml (Calico projectcalico.org/v3) to
# Cilium cilium.io/v2. Pre-staged in PR2; does NOT enforce until PR4 flips
# policyEnforcementMode from "never" to "default".
#
# Sources/destinations preserved verbatim from the Calico policy:
# - Cloudflared ingress (port 2222)
# - kube-dns egress
# - kubernetes API egress
# - SSH egress to specific nodes
# - ICMP egress to specific nodes
# - HTTPS egress to non-RFC1918
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: bastion-ingress-cloudflared
namespace: cf-ssh-bastion
spec:
endpointSelector:
matchLabels:
app: cf-ssh-bastion
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: cloudflared
toPorts:
- ports:
- port: "2222"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: bastion-allow-kube-dns
namespace: cf-ssh-bastion
spec:
endpointSelector: {}
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: bastion-allow-k8s-api
namespace: cf-ssh-bastion
spec:
endpointSelector: {}
egress:
- toEntities:
- kube-apiserver
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: bastion-allow-node-ssh
namespace: cf-ssh-bastion
spec:
endpointSelector: {}
egress:
- toCIDRSet:
- cidr: 192.168.20.1/32
- cidr: 192.168.20.141/32
- cidr: 192.168.20.142/32
- cidr: 192.168.20.143/32
- cidr: 192.168.20.144/32
- cidr: 192.168.20.151/32
- cidr: 192.168.20.152/32
- cidr: 192.168.20.153/32
- cidr: 192.168.20.154/32
- cidr: 192.168.23.109/32
toPorts:
- ports:
- port: "22"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: bastion-allow-icmp
namespace: cf-ssh-bastion
spec:
endpointSelector: {}
egress:
- toCIDRSet:
- cidr: 192.168.20.1/32
- cidr: 192.168.20.141/32
- cidr: 192.168.20.142/32
- cidr: 192.168.20.143/32
- cidr: 192.168.20.144/32
- cidr: 192.168.20.151/32
- cidr: 192.168.20.152/32
- cidr: 192.168.20.153/32
- cidr: 192.168.20.154/32
- cidr: 192.168.23.109/32
icmps:
- fields:
- type: 8
family: IPv4
- type: 128
family: IPv6
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: bastion-allow-external-https
namespace: cf-ssh-bastion
spec:
endpointSelector: {}
egress:
- toCIDRSet:
- cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
toPorts:
- ports:
- port: "443"
protocol: TCP
  • Step 3: Validate yamllint

Run: uvx --from yamllint yamllint argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml Expected: clean.

  • Step 4: Validate against the cilium.io/v2 schema (kubectl dry-run)
Terminal window
kubectl --context fzymgc-house apply --dry-run=server \
-f argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml

Expected: 6 lines like ciliumnetworkpolicy.cilium.io/<name> created (server dry run). No schema errors. (Cilium 1.19’s CRDs are now installed in the cluster after Task 3, so server-side dry-run works.)

  • Step 5: Commit

Commit message: feat(cf-ssh-bastion): pre-stage CiliumNetworkPolicy for migration (PR2/5)


Task 5: Add the new policy to the kustomization

Section titled “Task 5: Add the new policy to the kustomization”

Files:

  • Modify: argocd/app-configs/cf-ssh-bastion/kustomization.yaml

  • Step 1: Inspect the current kustomization

Run: cat argocd/app-configs/cf-ssh-bastion/kustomization.yaml

Note where calico-network-policy.yaml appears under resources:.

  • Step 2: Add the new file

Edit argocd/app-configs/cf-ssh-bastion/kustomization.yaml. In the resources: list, find the line - calico-network-policy.yaml (or similar) and add IMMEDIATELY AFTER it:

- cilium-network-policy.yaml

Both policy files coexist from PR2 through PR4. PR5 removes the Calico one.

  • Step 3: Verify kustomize renders cleanly
Terminal window
kubectl --context fzymgc-house kustomize argocd/app-configs/cf-ssh-bastion/ > /tmp/bastion-kustomize.yaml
echo "Rendered $(rg -c '^kind:' /tmp/bastion-kustomize.yaml) resources"
rg '^kind:' /tmp/bastion-kustomize.yaml | sort | uniq -c
rm /tmp/bastion-kustomize.yaml

Expected: rendered output contains both NetworkPolicy (Calico) and CiliumNetworkPolicy kinds, plus the bastion’s other resources (Deployment, Service, etc.). No kustomize errors.

  • Step 4: Validate yamllint

Run: uvx --from yamllint yamllint argocd/app-configs/cf-ssh-bastion/kustomization.yaml Expected: clean.

  • Step 5: Commit

Commit message: feat(cf-ssh-bastion): add CiliumNetworkPolicy to kustomization (PR2/5)


Task 6: Verify ArgoCD sync and CiliumNetworkPolicy state

Section titled “Task 6: Verify ArgoCD sync and CiliumNetworkPolicy state”

Files: none (verification — will run after the PR is merged to main, but the dry-run below validates the chain works)

  • Step 1: Run the ArgoCD app diff locally
Terminal window
kubectl --context fzymgc-house get application cf-ssh-bastion -n argocd -o yaml \
| rg -A 2 'syncPolicy|targetRevision'

Note the current sync policy. If automated sync is enabled, the new CRs will be applied after PR2 merges to main. If manual, an operator triggers the sync.

  • Step 2: After PR2 merges, verify the bastion CiliumNetworkPolicies exist

(This step runs post-merge; document the expected post-merge state in the PR description.)

Terminal window
kubectl --context fzymgc-house get ciliumnetworkpolicies.cilium.io -n cf-ssh-bastion

Expected: 6 policies listed (bastion-ingress-cloudflared, bastion-allow-kube-dns, bastion-allow-k8s-api, bastion-allow-node-ssh, bastion-allow-icmp, bastion-allow-external-https).

  • Step 3: Verify the policies do NOT enforce yet (policyEnforcementMode: "never")
Terminal window
kubectl --context fzymgc-house get cm -n kube-system cilium-config -o jsonpath='{.data.enable-policy}'
echo ""

Expected: never. Cilium is installed but policy enforcement is fully disabled cluster-wide. PR4 flips this to default.


Task 7: Create the cilium-migration.md migration log (start the running runbook)

Section titled “Task 7: Create the cilium-migration.md migration log (start the running runbook)”

Files:

  • Create: docs/operations/cilium-migration.md

  • Step 1: Create the migration log skeleton

# Cilium Migration Log
This document is the chronological log of the Calico→Cilium migration
described in `docs/engineering/specs/2026-05-10-calico-to-cilium-design.md`.
It is updated iteratively during PR3 (per-node migration) and finalized in PR4.
After PR4 merges, this log is retained as a historical artifact (not deleted)
so future debugging can reference what was done and when.
## PR2 — Cilium installed alongside Calico
- **Date:** <fill in at execution>
- **Operator:** <fill in>
- **Velero backup:** `pre-cilium-install-<timestamp>` (succeeded)
- **Cilium version:** 1.19.3
- **Cluster state pre-install:** 8 nodes on Calico CNI; no Calico-only NetworkPolicies outside `cf-ssh-bastion`.
- **Install duration:** <fill in>
- **Post-install verification:**
- `cilium status --wait` reports OK
- Cilium DaemonSet 8/8 Ready
- cilium-operator 2/2 Ready
- Hubble Relay 1/1 Ready
- `CiliumNodeConfig cilium-default` applied; matches 0 nodes
- `policyEnforcementMode: never` confirmed via cilium-config ConfigMap
- 0 pods on `10.245.0.0/16` (no nodes migrated yet)
- 6 `CiliumNetworkPolicy` resources created in `cf-ssh-bastion` (not enforcing)
## Constraint reminder
**Between PR2 merge and PR4 merge, no new `NetworkPolicy` or
`CiliumNetworkPolicy` resources may merge to `argocd/`.** During this
window, `policyEnforcementMode: "never"` makes Cilium ignore all policies
cluster-wide, so newly-added policies would silently fail to enforce and
create a security regression. The bastion's pre-staged policies (this PR)
are exempt because they're the migration target, not new functionality.
## PR3 — Per-node migration
(Entries appended per-node as each migration completes.)
## PR4 — Decommission Calico
(Entry appended when PR4 merges.)
  • Step 2: Validate markdown lint

Run: uvx --from rumdl rumdl check docs/operations/cilium-migration.md Expected: clean (if rumdl reports MD code-fence-language warnings, accept — the file is operational, not user-facing).

  • Step 3: Commit

Commit message: docs(cilium): start migration log (PR2/5)


Files: none (final checks)

  • Step 1: Run all repo linters
Terminal window
lefthook run pre-commit

Expected: all checks pass.

  • Step 2: Verify worktree is clean and ready
Terminal window
git status

Expected: clean working tree, branch feat/cilium-migration (or a PR2-specific branch if you’ve split the worktree).

  • Step 3: Open the PR
Terminal window
git push -u origin feat/cilium-migration # or PR2-specific branch
gh pr create \
--title "feat(cilium): install Cilium alongside Calico in migration mode (PR2/5)" \
--body "$(cat <<'EOF'
## Summary
- Runs `ansible-playbook --tags cilium-install` to install Cilium 1.19.3 in migration mode (cni.customConf: true, policyEnforcementMode: never, bpf.hostLegacyRouting: true).
- Pre-stages `CiliumNetworkPolicy` resources for `cf-ssh-bastion` (does not enforce until PR4).
- Starts `docs/operations/cilium-migration.md` runbook.
## Cluster impact
- Cilium DaemonSet running on all 8 nodes (no pods migrated yet).
- Calico continues full enforcement of existing NetworkPolicies.
- Gateway API v1.4.1 Standard CRDs installed.
## Constraint
**No new NetworkPolicy / CiliumNetworkPolicy may merge to `argocd/` between this PR and PR4.** During the migration window, Cilium's policyEnforcementMode is "never" cluster-wide.
## Spec / prior PR
- Spec: `docs/engineering/specs/2026-05-10-calico-to-cilium-design.md`
- PR1 (Ansible role): #<PR1-number>
- Next: PR3 (per-node migration, iterative)
## Test plan
- [x] Velero backup completed
- [x] `ansible-playbook --tags cilium-install` succeeded
- [x] `cilium status --wait` reports OK
- [x] 6 CiliumNetworkPolicies present in cf-ssh-bastion (not enforcing)
- [x] No workload pod restart from install
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"

  • Spec coverage — Spec’s PR2 section: pre-flight Velero (T2), Gateway API CRD check (T2), Helm install via Ansible (T3), CiliumNodeConfig apply (T3), DaemonSet readiness (T3), bastion CiliumNetworkPolicy create (T4), kustomization update (T5), ArgoCD verify (T6), migration log (T7).
  • Placeholder scan — no “TODO/TBD/etc.”; every command is explicit. The migration-log entry has <fill in> placeholders for operator/date — these are values the operator fills at execution time, not plan-author gaps; they’re explicitly bracketed as such.
  • Type/symbol consistencycf-ssh-bastion namespace and policy names match the spec; k8s-app=cilium selector matches PR1’s template.
  • No invented references — every kubectl/helm/velero/ansible command refers to real commands and verified flags.

  • The bastion CiliumNetworkPolicy translation is L3/L4 only. Spec is explicit that DNS-based egress (L7) is out of scope for this migration. If you find yourself wanting to add dnsEgress rules, stop — that’s a follow-up security-hardening PR.
  • endpointSelector: {} matches all pods in the namespace (Cilium equivalent of Calico’s selector: all()). Don’t change this to a more specific selector.
  • bastion-allow-k8s-api uses toEntities: kube-apiserver which is a Cilium reserved entity. This is cleaner than the Calico services approach (which named the kubernetes service in default namespace) and is the Cilium-idiomatic way to allow API server traffic.
  • bastion-allow-kube-dns uses toEndpoints with k8s-app label rather than toServices. Cilium’s toServices is more recent and historically less reliable than direct endpoint matching; the spec’s CiliumNetworkPolicy dialect decision favors this approach.

Plan complete and saved to docs/engineering/plans/2026-05-11-cilium-pr2-install-alongside-calico.md. Two execution options:

  1. Subagent-Driven (recommended for code tasks) — fresh subagent per task, review between tasks. Note: PR2 has multiple human-in-the-loop operational steps (Velero backup, Ansible playbook against the live cluster); a subagent can drive these but you should be reviewing each step’s output.

  2. Inline Execution — execute tasks in this session using executing-plans, with explicit human approval at each cluster-affecting step.

Which approach?