Skip to content

Commit 287dc95

Browse files
committed
fix ufw issue
1 parent ec6ab83 commit 287dc95

2 files changed

Lines changed: 68 additions & 24 deletions

File tree

v1/providers/nebius/instance.go

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,7 +1582,6 @@ func generateCloudInitUserData(publicKey string, firewallRules v1.FirewallRules)
15821582
script := `#cloud-config
15831583
packages:
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.
16671690
func 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",

v1/providers/nebius/instance_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ func TestNebiusClient_MergeInstanceForUpdate(t *testing.T) {
8484
assert.Equal(t, newInstance.Status, merged.Status)
8585
}
8686

87+
func TestGenerateCloudInitUserDataInstallsDockerFirewallHook(t *testing.T) {
88+
script := generateCloudInitUserData("ssh-rsa test", v1.FirewallRules{})
89+
90+
assert.NotContains(t, script, "iptables-persistent")
91+
assert.NotContains(t, script, "netfilter-persistent")
92+
assert.Contains(t, script, "/usr/local/sbin/brev-apply-docker-firewall.sh")
93+
assert.Contains(t, script, "/etc/systemd/system/docker.service.d")
94+
assert.Contains(t, script, "ExecStartPost=/usr/local/sbin/brev-apply-docker-firewall.sh")
95+
assert.Contains(t, script, "sudo /usr/local/sbin/brev-apply-docker-firewall.sh || true")
96+
}
97+
98+
func TestGenerateIPTablesCommandsCreateDockerUserChainBeforeFlush(t *testing.T) {
99+
commands := generateIPTablesCommands()
100+
101+
assert.GreaterOrEqual(t, len(commands), 2)
102+
assert.Equal(t, "iptables -N DOCKER-USER 2>/dev/null || true", commands[0])
103+
assert.Equal(t, "iptables -F DOCKER-USER || true", commands[1])
104+
assert.Contains(t, strings.Join(commands, "\n"), "iptables -A DOCKER-USER -j DROP")
105+
}
106+
87107
// BenchmarkCreateInstance benchmarks the CreateInstance method
88108
func BenchmarkCreateInstance(b *testing.B) {
89109
b.Skip("CreateInstance requires real SDK initialization - use integration tests instead")

0 commit comments

Comments
 (0)