@@ -1582,7 +1582,6 @@ func generateCloudInitUserData(publicKey string, firewallRules v1.FirewallRules)
15821582 script := `#cloud-config
15831583packages:
15841584 - ufw
1585- - iptables-persistent
15861585`
15871586
15881587 // Add SSH key configuration if provided
@@ -1594,33 +1593,19 @@ packages:
15941593
15951594 var commands []string
15961595
1597- // Fix a systemd race condition: ufw.service and netfilter-persistent.service
1598- // both start in parallel (both are Before=network-pre.target with no mutual
1599- // ordering). Both call iptables-restore concurrently, and with the iptables-nft
1600- // backend the competing nftables transactions cause UFW to fail with
1601- // "iptables-restore: line 4 failed". This drop-in forces UFW to wait for
1602- // netfilter-persistent to finish first.
1603- commands = append (commands ,
1604- "sudo mkdir -p /etc/systemd/system/ufw.service.d" ,
1605- `printf '[Unit]\nAfter=netfilter-persistent.service\n' | sudo tee /etc/systemd/system/ufw.service.d/after-netfilter.conf > /dev/null` ,
1606- "sudo systemctl daemon-reload" ,
1607- )
1596+ // Install an idempotent Docker firewall hook before enabling UFW. Some
1597+ // Nebius images start Docker after cloud-init runcmd; Docker creates or
1598+ // resets DOCKER-USER during startup, so the rules need to be re-applied after
1599+ // docker.service starts instead of only once during runcmd.
1600+ commands = append (commands , generateDockerFirewallInstallCommands ()... )
16081601
16091602 // Generate UFW firewall commands (similar to Shadeform's approach)
16101603 // UFW (Uncomplicated Firewall) is available on Ubuntu/Debian instances
16111604 commands = append (commands , generateUFWCommands (firewallRules )... )
16121605
1613- // Generate IPTables firewall commands to ensure docker ports are not made immediately
1614- // accessible from the internet by default.
1615- commands = append (commands , generateIPTablesCommands ()... )
1616-
1617- // Save the complete iptables state (UFW chains + DOCKER-USER rules) so it
1618- // survives instance stop/start cycles. Cloud-init runcmd only executes on
1619- // first boot; on subsequent boots netfilter-persistent restores this snapshot,
1620- // then UFW starts after it (due to the drop-in above) and re-applies its rules.
1621- // This provides defense-in-depth: even if UFW fails for any reason, the
1622- // netfilter-persistent snapshot ensures port 22 and DOCKER-USER rules persist.
1623- commands = append (commands , "sudo netfilter-persistent save" )
1606+ // Apply immediately for images where Docker is already running. The
1607+ // docker.service ExecStartPost hook handles images where Docker starts later.
1608+ commands = append (commands , "sudo /usr/local/sbin/brev-apply-docker-firewall.sh || true" )
16241609
16251610 if len (commands ) > 0 {
16261611 // Use runcmd to execute firewall setup commands
@@ -1662,11 +1647,50 @@ func generateUFWCommands(firewallRules v1.FirewallRules) []string {
16621647 return commands
16631648}
16641649
1650+ const (
1651+ dockerFirewallScriptPath = "/usr/local/sbin/brev-apply-docker-firewall.sh"
1652+ dockerServiceDropInDir = "/etc/systemd/system/docker.service.d"
1653+ dockerFirewallDropInPath = dockerServiceDropInDir + "/10-brev-firewall.conf"
1654+ )
1655+
1656+ func generateDockerFirewallInstallCommands () []string {
1657+ scriptLines := append ([]string {
1658+ "#!/bin/sh" ,
1659+ "set +e" ,
1660+ }, generateIPTablesCommands ()... )
1661+ scriptLines = append (scriptLines , "exit 0" )
1662+
1663+ return []string {
1664+ generatePrintfToFileCommand (scriptLines , dockerFirewallScriptPath ),
1665+ "sudo chmod 0755 " + dockerFirewallScriptPath ,
1666+ "sudo mkdir -p " + dockerServiceDropInDir ,
1667+ generatePrintfToFileCommand ([]string {
1668+ "[Service]" ,
1669+ "ExecStartPost=" + dockerFirewallScriptPath ,
1670+ }, dockerFirewallDropInPath ),
1671+ "sudo systemctl daemon-reload" ,
1672+ }
1673+ }
1674+
1675+ func generatePrintfToFileCommand (lines []string , path string ) string {
1676+ quotedLines := make ([]string , 0 , len (lines ))
1677+ for _ , line := range lines {
1678+ quotedLines = append (quotedLines , shellSingleQuote (line ))
1679+ }
1680+
1681+ return fmt .Sprintf ("printf '%%s\\ n' %s | sudo tee %s > /dev/null" , strings .Join (quotedLines , " " ), path )
1682+ }
1683+
1684+ func shellSingleQuote (value string ) string {
1685+ return "'" + strings .ReplaceAll (value , "'" , `'\''` ) + "'"
1686+ }
1687+
16651688// generateIPTablesCommands generates IPTables firewall commands to ensure docker ports are not made immediately
16661689// accessible from the internet by default.
16671690func generateIPTablesCommands () []string {
16681691 commands := []string {
1669- "iptables -F DOCKER-USER" ,
1692+ "iptables -N DOCKER-USER 2>/dev/null || true" ,
1693+ "iptables -F DOCKER-USER || true" ,
16701694 "iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" ,
16711695 "iptables -A DOCKER-USER -i docker0 ! -o docker0 -j ACCEPT" ,
16721696 "iptables -A DOCKER-USER -i br+ ! -o br+ -j ACCEPT" ,
0 commit comments