Skip to content

Fix node1 SSH lockout and pin k3s node-ip to K3s VLAN - #247

Merged
kid merged 5 commits into
mainfrom
k3s-node-ip-pin
Aug 20, 2026
Merged

Fix node1 SSH lockout and pin k3s node-ip to K3s VLAN#247
kid merged 5 commits into
mainfrom
k3s-node-ip-pin

Conversation

@kid

@kid kid commented Aug 20, 2026

Copy link
Copy Markdown
Owner

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 Route all pod egress via Cilium Egress Gateway; restrict node1 SSH to Servers #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 Route all pod egress via Cilium Egress Gateway; restrict node1 SSH to Servers #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

  • nix build .#nixosConfigurations.node1.config.system.build.toplevel — builds cleanly, confirmed --node-ip=10.0.40.10 in the built k3s unit
  • 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-Route all pod egress via Cilium Egress Gateway; restrict node1 SSH to Servers #246 behavior) until the interface-scoped restriction is designed properly

🤖 Generated with Claude Code

https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR

kid and others added 3 commits August 20, 2026 20:51
Binding sshd to node1's Servers IP address locked out SSH entirely
after a reboot (the address is DHCP-assigned, not guaranteed present
at sshd start). Reverting to plain enable=true until this is done at
the interface/firewall level instead of by IP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
Hosts here are multi-homed (Servers/K3s/Storage). k3s's own node-ip
auto-detection isn't stable across reboots, and a mismatch breaks
etcd — its persisted peer URL stops matching whatever address k3s
picks this time ("this server is not a member of the etcd cluster").
Pin --node-ip explicitly to the host's own <hostname>-k3s device
address so it's deterministic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
NixOS's own firewall is force-disabled on k3s hosts (Cilium owns
packet filtering via eBPF), so binding sshd or the host firewall to
node1's Servers IP doesn't work — nothing enforces it. Enable Cilium's
host firewall (devices pinned explicitly, not auto-detected — it's
already picked the wrong interface twice today on this multi-homed
host) and add a CiliumClusterwideNetworkPolicy: cluster-internal
traffic to the host is always allowed, SSH is only allowed from the
Servers CIDR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
@kid

kid commented Aug 20, 2026

Copy link
Copy Markdown
Owner Author

Added: Cilium host-firewall SSH restriction (3rd commit)

Replaces the reverted IP-based approach: NixOS's firewall is force-disabled on k3s hosts (Cilium owns filtering via eBPF), so binding to an IP/interface at the OS level was never going to work. Enables Cilium's Host Firewall + a CiliumClusterwideNetworkPolicy restricting SSH to the Servers CIDR (10.0.10.0/24), while explicitly allowing all cluster-internal traffic to the host so kubelet/apiserver/etc. keep working.

This needs a careful rollout, not a plain merge-and-deploy — per Cilium's own docs, once a CiliumClusterwideNetworkPolicy selects a node, that node's host traffic goes default-deny except what the policy allows. Get this wrong on a single-node cluster and the only recovery path is IPMI/console access (Cilium's docs have an explicit "Emergency Recovery" section for exactly this).

Recommended sequence:

  1. deploy node1 first — lands the kidibox.net/ssh-servers-only=true node label (no firewall behavior change yet).
  2. Merge this PR, let ArgoCD sync — turns on hostFirewall.enabled and applies the policy.
  3. Immediately after sync, before trusting it: kubectl -n kube-system exec ds/cilium -- cilium-dbg endpoint config $(cilium-dbg endpoint list -o jsonpath='{[?(@.status.identity.id==1)].id}') PolicyAuditMode=Enabled, then watch cilium-dbg monitor -t policy-verdict --related-to <id> for a while to confirm nothing unexpected gets an action deny verdict.
  4. Only once that looks clean, disable audit mode to actually enforce: same command with PolicyAuditMode=Disabled.

Audit mode doesn't persist across cilium-agent restarts — a pod restart during step 3 puts it back in enforce mode, so don't leave step 3 running unattended for long.

🤖 Generated with Claude Code

kid and others added 2 commits August 20, 2026 23:32
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
Cilium's devices/hostFirewall previously covered every interface on
node1, forcing NixOS's own firewall off entirely. Scope Cilium to just
k3s (the only interface BGP, native routing, and the egress-gateway
policy actually touch) and let NixOS's real firewall govern everything
else, trusting only the interfaces Cilium itself manages (k3s plus its
own cilium_host/cilium_net/cilium_vxlan/lxc* interfaces) so the two
don't double-filter the same traffic.

This retires the Cilium host-firewall SSH policy from the previous
commit — it can't do anything once Cilium stops attaching to the
Servers interface — in favor of a plain NixOS firewall rule scoping
SSH to that interface, which only failed the first time because the
firewall was globally disabled.

The egress-gateway policy is unrelated and unaffected: it forces pod
egress routing/masquerade explicitly, independent of Cilium's devices
list, which only controls where Cilium's own programs attach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CezjyaVpC3FVPTUECp7cMR
@kid

kid commented Aug 20, 2026

Copy link
Copy Markdown
Owner Author

Superseded: SSH restriction moved off Cilium host-firewall (4th commit)

The audit-mode rollout procedure in my previous comment no longer applies — that whole approach is gone. Turns out scoping Cilium's devices to just the interfaces it actually needs (k3s) makes more sense than sweeping every interface into Cilium's eBPF scope and using its host-firewall feature for one narrow SSH rule. Also matters for node1's other stated role as a future Incus VM host — those bridges should be filtered by the plain NixOS firewall, not swept into Cilium's scope.

Changed:

  • modules/kubernetes/cilium/default.nix: devices scoped to ["k3s"] only (was all three VLAN interfaces); dropped hostFirewall.enabled.
  • modules/den/aspects/services/k3s/cilium.nix: no longer force-disables NixOS's firewall — instead trusts exactly the interfaces Cilium itself manages (k3s, cilium_net, cilium_host, cilium_vxlan, lxc* — confirmed these are the real interfaces Cilium creates by checking node1 directly over SSH).
  • Removed modules/kubernetes/cilium/host-firewall.nix (the CiliumClusterwideNetworkPolicy from the previous commit) — dead once Cilium stops attaching to the Servers interface.
  • SSH restriction is back to a plain NixOS firewall rule (networking.firewall.interfaces."enp36s0f1".allowedTCPPorts = [ 22 ]) — this is the same thing that broke SSH access two commits ago, but that failure was because the firewall was globally disabled, not because the rule itself was wrong. Confirmed the actual generated nftables ruleset (not just that it builds) — iifname enp36s0f1 tcp dport { 22 } accept, default-drop everywhere else, k3s/Cilium's own interfaces fully trusted.

The egress-gateway policy (pod egress routing) is untouched — confirmed against Cilium's own masquerade docs that devices doesn't affect which interface egress-gateway forces traffic through, so narrowing it here doesn't reintroduce the original routing bug.

🤖 Generated with Claude Code

@kid
kid merged commit 9ca2ced into main Aug 20, 2026
1 check passed
@kid
kid deleted the k3s-node-ip-pin branch August 20, 2026 22:06
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