@@ -1752,6 +1752,7 @@ func generateCloudInitUserData(publicKey string, firewallRules v1.FirewallRules)
17521752 script := `#cloud-config
17531753packages:
17541754 - ufw
1755+ - iptables-persistent
17551756`
17561757
17571758 // Add SSH key configuration if provided
@@ -1762,6 +1763,19 @@ packages:
17621763 }
17631764
17641765 var commands []string
1766+
1767+ // Fix a systemd race condition: ufw.service and netfilter-persistent.service
1768+ // both start in parallel (both are Before=network-pre.target with no mutual
1769+ // ordering). Both call iptables-restore concurrently, and with the iptables-nft
1770+ // backend the competing nftables transactions cause UFW to fail with
1771+ // "iptables-restore: line 4 failed". This drop-in forces UFW to wait for
1772+ // netfilter-persistent to finish first.
1773+ commands = append (commands ,
1774+ "sudo mkdir -p /etc/systemd/system/ufw.service.d" ,
1775+ `printf '[Unit]\nAfter=netfilter-persistent.service\n' | sudo tee /etc/systemd/system/ufw.service.d/after-netfilter.conf > /dev/null` ,
1776+ "sudo systemctl daemon-reload" ,
1777+ )
1778+
17651779 // Generate UFW firewall commands (similar to Shadeform's approach)
17661780 // UFW (Uncomplicated Firewall) is available on Ubuntu/Debian instances
17671781 commands = append (commands , generateUFWCommands (firewallRules )... )
@@ -1770,11 +1784,21 @@ packages:
17701784 // accessible from the internet by default.
17711785 commands = append (commands , generateIPTablesCommands ()... )
17721786
1787+ // Save the complete iptables state (UFW chains + DOCKER-USER rules) so it
1788+ // survives instance stop/start cycles. Cloud-init runcmd only executes on
1789+ // first boot; on subsequent boots netfilter-persistent restores this snapshot,
1790+ // then UFW starts after it (due to the drop-in above) and re-applies its rules.
1791+ // This provides defense-in-depth: even if UFW fails for any reason, the
1792+ // netfilter-persistent snapshot ensures port 22 and DOCKER-USER rules persist.
1793+ commands = append (commands , "sudo netfilter-persistent save" )
1794+
17731795 if len (commands ) > 0 {
17741796 // Use runcmd to execute firewall setup commands
17751797 script += "\n runcmd:\n "
17761798 for _ , cmd := range commands {
1777- script += fmt .Sprintf (" - %s\n " , cmd )
1799+ escaped := strings .ReplaceAll (cmd , `\` , `\\` )
1800+ escaped = strings .ReplaceAll (escaped , `"` , `\"` )
1801+ script += fmt .Sprintf (" - \" %s\" \n " , escaped )
17781802 }
17791803 }
17801804
0 commit comments