Skip to content

Route all pod egress via Cilium Egress Gateway; restrict node1 SSH to Servers - #246

Merged
kid merged 2 commits into
mainfrom
cilium-egress-gateway
Aug 20, 2026
Merged

Route all pod egress via Cilium Egress Gateway; restrict node1 SSH to Servers#246
kid merged 2 commits into
mainfrom
cilium-egress-gateway

Conversation

@kid

@kid kid commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Root cause of the external-dns/MikroTik connectivity failure (Add external-dns for MikroTik DNS record management #245): node1 is multi-homed (Servers/K3s/Storage VLANs). The kernel's own routing table sends all outbound traffic — including pod traffic — via the Servers interface regardless of the pod's originating network, so nothing from the cluster ever reached rb5009's Management API even though the firewall already allowed it on the K3s-facing chain (input-K3s). Confirmed directly on both node1 (ip route get 10.99.0.1, plain curl from the host) and on rb5009 (/ip firewall connection print showed literally nothing arriving from node1).
  • Adds a CiliumEgressGatewayPolicy (modules/kubernetes/cilium/egress-gateway.nix) that SNATs all pod egress traffic, any destination, through a kidibox.net/egress-gateway: "true"-labeled node's k3s interface — independent of the host's own default-route choice. Deliberately cluster-wide rather than Management-CIDR-specific, so K3s becomes the one deterministic egress path for pod traffic and any future consumer (e.g. mikrotik-exporter) needs no policy change.
  • Raw YAML (applications.cilium.yamls), not nixidy's typed CRD generator — confirmed Cilium's chart ships no crds/ directory (its CRDs are installed by the running cilium-operator), so generators.fromChartCRDModule can't introspect them. Matches the existing pattern already used in modules/kubernetes/cilium/bgp.nix for the same reason.
  • modules/hosts/node1.nix gets the kidibox.net/egress-gateway=true k3s node label (generic, not hostname-pinned, so a second node just needs the same flag) and, in a separate commit, restricts sshd to node1's Servers address only (previously reachable from every VLAN node1 touches).

Test plan

  • nix run .#write-manifests — rendered manifests/prd/cilium/CiliumEgressGatewayPolicy-egress-gateway.yaml
  • nix flake check --print-build-logs — all checks pass
  • nix build .#nixosConfigurations.node1.config.system.build.toplevel — builds cleanly; confirmed --node-label=kidibox.net/egress-gateway=true in the built k3s unit and ListenAddress 10.0.10.10:22 in the built sshd config
  • After merge: deploy node1 (applies the node label + SSH restriction) and let ArgoCD sync (applies the Cilium Helm value + policy) — label should land before/alongside the policy sync
  • kubectl get ciliumegressgatewaypolicy egress-gateway -o yaml — check .status
  • Re-test the external-dns webhook pod reaches 10.99.0.1:443 and goes Ready
  • Spot-check general pod egress (e.g. internet-bound) still works post-change
  • From a Management-VLAN host, confirm SSH to node1 now fails, while ssh kid@10.0.10.10 still works

🤖 Generated with Claude Code

https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR

kid and others added 2 commits August 20, 2026 19:19
node1 is multi-homed (Servers/K3s/Storage VLANs); the kernel's own
routing table sends everything - including pod traffic - out via
Servers regardless of Cilium's own network config, so nothing from the
cluster could ever reach rb5009's Management API even though the
firewall already allowed it on the K3s-facing chain. A
CiliumEgressGatewayPolicy SNATs all pod egress through a
kidibox.net/egress-gateway-labeled node's k3s interface instead,
independent of the host's own default-route choice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
sshd previously listened on every interface node1 has (Servers, K3s,
Storage), reachable from any VLAN. Bind it to node1's Servers address
only, matching how deploy/nixos-anywhere-install already reach it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
@kid
kid force-pushed the cilium-egress-gateway branch from e6b227b to 7e2aa0e Compare August 20, 2026 17:19
@kid
kid enabled auto-merge August 20, 2026 17:21
@kid
kid merged commit 406112b into main Aug 20, 2026
1 check passed
@kid
kid deleted the cilium-egress-gateway branch August 20, 2026 17:21
kid added a commit that referenced this pull request Aug 20, 2026
## Summary
Follow-up to #246: node1 got a fresh `nixos-anywhere` reinstall after
two problems surfaced.

- **SSH lockout**: binding `sshd` to node1's Servers IP
(`services.openssh.listenAddresses`, from #246) locked out SSH entirely
after a reboot — the address is DHCP-assigned, not guaranteed present
when sshd starts. Reverted to plain `enable = true;` for now; the
interface/firewall-scoped version of this restriction is a separate
follow-up.
- **k3s/etcd outage**: node1 is multi-homed (Servers/K3s/Storage VLANs)
and k3s's own `--node-ip` auto-detection isn't stable across reboots.
After the reboot, k3s picked a different address than etcd's on-disk
membership record expected (`this server is not a member of the etcd
cluster`), and the control plane got stuck retrying forever. Pinned
`--node-ip` explicitly to each host's own `<hostname>-k3s` device
address (the K3s VLAN) so it's deterministic — same root-cause class as
the pod-egress routing issue in #246, just hitting k3s's own
control-plane binding this time.

Verified against node1's current live state (post-reinstall, currently
healthy on `10.0.40.10` — the K3s VLAN address) — the pin matches what's
already running, so applying it is a same-address restart, not an IP
change.

## Test plan
- [x] `nix build
.#nixosConfigurations.node1.config.system.build.toplevel` — builds
cleanly, confirmed `--node-ip=10.0.40.10` in the built k3s unit
- [x] `nix flake check --print-build-logs` — all checks pass
- [ ] `deploy node1` and confirm k3s restarts cleanly with no etcd
membership error
- [ ] Confirm SSH access is restored from any VLAN (matching pre-#246
behavior) until the interface-scoped restriction is designed properly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant