Skip to content

Commit cd0ea82

Browse files
committed
Merge branch 'firewall-fix' into main
- Fix firewall race condition with k3s on boot - Fix SSH lockout issues during initialization - Handle empty network configuration gracefully - Fix set -e compatibility issues - Reduce iptables rules file bloat by filtering kube-router chains - Ensure INPUT DROP policy persistence
2 parents ec67a98 + 03af923 commit cd0ea82

2 files changed

Lines changed: 53 additions & 11 deletions

File tree

templates/firewall/firewall.service

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[Unit]
22
Description=K3s Node Firewall Service
3-
After=network-online.target
3+
# Start AFTER k3s so kube-router has already set up its iptables chains
4+
# This prevents race condition where kube-router modifies INPUT after firewall restore
5+
After=network-online.target k3s.service
46
Wants=network-online.target
57

68
[Service]

templates/firewall/firewall.sh

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/bin/bash
22
set -euo pipefail
33

4+
trap 'log "ERROR: Command failed at line $LINENO: $BASH_COMMAND"; exit 1' ERR
5+
46
# =============================================================================
57
# Unified Firewall Script for hetzner-k3s
68
# Handles initial setup, ongoing updates, and restoration on boot
@@ -45,12 +47,13 @@ validate_ip_network() {
4547
}
4648

4749
normalise_networks() {
48-
grep -v '^[[:space:]]*$' | \
49-
grep -v '^[[:space:]]*#' | \
50+
# Use || true to handle empty input gracefully with pipefail
51+
{ grep -v '^[[:space:]]*$' || true; } | \
52+
{ grep -v '^[[:space:]]*#' || true; } | \
5053
tr -d '\r' | \
5154
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | \
5255
sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/\1\/32/' | \
53-
sort -u
56+
sort -u || true
5457
}
5558

5659
# =============================================================================
@@ -112,15 +115,13 @@ update_ipset() {
112115
local networks=$2
113116
local temp_name="${name}_temp"
114117

115-
# Get current networks
116118
local current=""
117119
if ipset list -n 2>/dev/null | grep -q "^${name}$"; then
118-
current=$(ipset list "$name" 2>/dev/null | sed -n '/^Members:/,$p' | tail -n +2 | normalise_networks)
120+
current=$(ipset list "$name" 2>/dev/null | sed -n '/^Members:/,$p' | tail -n +2 | normalise_networks || echo "")
119121
fi
120122

121-
# Normalise new networks
122123
local new
123-
new=$(echo "$networks" | normalise_networks)
124+
new=$(echo "$networks" | normalise_networks || echo "")
124125

125126
# Check for changes
126127
if [ "$current" = "$new" ]; then
@@ -135,7 +136,7 @@ update_ipset() {
135136
while IFS= read -r network; do
136137
if [ -n "$network" ] && validate_ip_network "$network"; then
137138
if ipset add "$temp_name" "$network" 2>/dev/null; then
138-
((count++))
139+
count=$((count + 1))
139140
fi
140141
fi
141142
done <<< "$new"
@@ -202,14 +203,28 @@ setup_iptables() {
202203

203204
save_iptables() {
204205
mkdir -p "$(dirname "$IPTABLES_RULES_FILE")"
205-
iptables-save > "$IPTABLES_RULES_FILE"
206+
iptables -P INPUT DROP 2>/dev/null || true
207+
iptables-save | grep -v -E 'KUBE-|FLANNEL-|CNI-' > "$IPTABLES_RULES_FILE"
206208
}
207209

208210
restore_iptables() {
211+
# First ensure ipsets exist (rules reference them)
212+
create_ipset_if_missing "$IPSET_NODES"
213+
create_ipset_if_missing "$IPSET_SSH"
214+
create_ipset_if_missing "$IPSET_API"
215+
209216
if [ -f "$IPTABLES_RULES_FILE" ]; then
210217
iptables-restore -w < "$IPTABLES_RULES_FILE" 2>/dev/null || true
211218
log "Restored iptables from $IPTABLES_RULES_FILE"
212219
fi
220+
221+
# Ensure INPUT policy is DROP (defense in depth)
222+
local current_policy
223+
current_policy=$(iptables -S INPUT 2>/dev/null | head -1)
224+
if [[ ! "$current_policy" =~ "-P INPUT DROP" ]]; then
225+
iptables -P INPUT DROP
226+
log "Set INPUT policy is DROP (was: $current_policy)"
227+
fi
213228
}
214229

215230
# =============================================================================
@@ -260,6 +275,15 @@ read_networks_file() {
260275
# Update Loop
261276
# =============================================================================
262277

278+
populate_ssh_ipset() {
279+
local ssh_networks
280+
ssh_networks=$(read_networks_file "$SSH_NETWORKS_FILE")
281+
if [ -n "$ssh_networks" ]; then
282+
update_ipset "$IPSET_SSH" "$ssh_networks"
283+
log "Populated SSH ipset from $SSH_NETWORKS_FILE"
284+
fi
285+
}
286+
263287
update_ipsets() {
264288
# Fetch node IPs from Hetzner API
265289
local node_ips
@@ -325,25 +349,41 @@ main() {
325349
initial_setup
326350
;;
327351
restore)
352+
create_ipset_if_missing "$IPSET_NODES"
353+
create_ipset_if_missing "$IPSET_SSH"
354+
create_ipset_if_missing "$IPSET_API"
355+
populate_ssh_ipset
328356
restore_ipsets
329357
restore_iptables
358+
update_ipsets
330359
;;
331360
update)
332361
update_ipsets
333362
;;
334363
daemon)
335364
log "Starting firewall daemon..."
365+
# Create ipsets first (rules reference them)
366+
create_ipset_if_missing "$IPSET_NODES"
367+
create_ipset_if_missing "$IPSET_SSH"
368+
create_ipset_if_missing "$IPSET_API"
369+
# Populate SSH ipset BEFORE iptables restore (so SSH works immediately)
370+
populate_ssh_ipset
336371
restore_ipsets
337372
restore_iptables
373+
update_ipsets
338374
run_update_loop
339375
;;
340376
*)
341-
# Default: setup then daemon mode
342377
if [ ! -f "$IPTABLES_RULES_FILE" ]; then
343378
initial_setup
344379
else
380+
create_ipset_if_missing "$IPSET_NODES"
381+
create_ipset_if_missing "$IPSET_SSH"
382+
create_ipset_if_missing "$IPSET_API"
383+
populate_ssh_ipset
345384
restore_ipsets
346385
restore_iptables
386+
update_ipsets
347387
fi
348388
run_update_loop
349389
;;

0 commit comments

Comments
 (0)