@@ -2,6 +2,8 @@ package v1
22
33import (
44 "context"
5+ _ "embed"
6+ "encoding/base64"
57 "fmt"
68 "strings"
79 "time"
@@ -20,6 +22,12 @@ const (
2022 nebiusCPUImageFamily = "ubuntu24.04-driverless"
2123)
2224
25+ //go:embed scripts/brev-apply-docker-firewall.sh
26+ var dockerFirewallScript string
27+
28+ //go:embed scripts/10-brev-firewall.conf
29+ var dockerFirewallDropIn string
30+
2331//nolint:gocyclo,funlen // Complex instance creation with resource management
2432func (c * NebiusClient ) CreateInstance (ctx context.Context , attrs v1.CreateInstanceAttrs ) (* v1.Instance , error ) {
2533 // Track created resources for automatic cleanup on failure
@@ -1576,7 +1584,7 @@ func (c *NebiusClient) cleanupOrphanedBootDisks(ctx context.Context, testID stri
15761584}
15771585
15781586// generateCloudInitUserData generates a cloud-init user-data script for SSH key injection and firewall configuration
1579- // This is inspired by Shadeform's LaunchConfiguration approach but uses cloud-init instead of base64 scripts
1587+ // This is inspired by Shadeform's LaunchConfiguration approach but uses cloud-init directly.
15801588func generateCloudInitUserData (publicKey string , firewallRules v1.FirewallRules ) string {
15811589 // Start with cloud-init header
15821590 script := `#cloud-config
@@ -1591,13 +1599,11 @@ packages:
15911599` , publicKey )
15921600 }
15931601
1602+ script += generateDockerFirewallWriteFiles ()
1603+
15941604 var commands []string
15951605
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 ()... )
1606+ commands = append (commands , "sudo systemctl daemon-reload" )
16011607
16021608 // Generate UFW firewall commands (similar to Shadeform's approach)
16031609 // UFW (Uncomplicated Firewall) is available on Ubuntu/Debian instances
@@ -1648,14 +1654,18 @@ func generateUFWCommands(firewallRules v1.FirewallRules) []string {
16481654}
16491655
16501656const (
1651- // Keep these paths stable: they are useful operator touchpoints when
1652- // debugging instance firewall state with systemctl/cat/iptables .
1657+ // Keep these generated paths stable: cloud-init, systemd, and validation
1658+ // tests all depend on this Docker firewall handoff .
16531659 dockerFirewallScriptPath = "/usr/local/sbin/brev-apply-docker-firewall.sh"
16541660 dockerServiceDropInDir = "/etc/systemd/system/docker.service.d"
16551661 dockerFirewallDropInPath = dockerServiceDropInDir + "/10-brev-firewall.conf"
16561662)
16571663
1658- func generateDockerFirewallInstallCommands () []string {
1664+ func generateDockerFirewallWriteFiles () string {
1665+ // This function emits the only write_files block in this cloud-config. If
1666+ // another generated file is added later, merge it into this block instead of
1667+ // adding a second top-level write_files key.
1668+ //
16591669 // Docker published ports are not governed by UFW's INPUT policy. Docker adds
16601670 // NAT/FORWARD rules that can make `docker run -p host:container` reachable
16611671 // from the public internet even when UFW says incoming traffic is denied.
@@ -1671,65 +1681,22 @@ func generateDockerFirewallInstallCommands() []string {
16711681 // because failing Docker startup would be worse operationally. Validation
16721682 // tests assert that the rule set is actually present and blocks published
16731683 // ports.
1674- scriptLines := append ([]string {
1675- "#!/bin/sh" ,
1676- "set +e" ,
1677- }, generateIPTablesCommands ()... )
1678- scriptLines = append (scriptLines , "exit 0" )
1679-
1680- return []string {
1681- generatePrintfToFileCommand (scriptLines , dockerFirewallScriptPath ),
1682- "sudo chmod 0755 " + dockerFirewallScriptPath ,
1683- "sudo mkdir -p " + dockerServiceDropInDir ,
1684- generatePrintfToFileCommand ([]string {
1685- "[Service]" ,
1686- "ExecStartPost=" + dockerFirewallScriptPath ,
1687- }, dockerFirewallDropInPath ),
1688- "sudo systemctl daemon-reload" ,
1689- }
1690- }
1691-
1692- func generatePrintfToFileCommand (lines []string , path string ) string {
1693- quotedLines := make ([]string , 0 , len (lines ))
1694- for _ , line := range lines {
1695- quotedLines = append (quotedLines , shellSingleQuote (line ))
1696- }
1697-
1698- return fmt .Sprintf ("printf '%%s\\ n' %s | sudo tee %s > /dev/null" , strings .Join (quotedLines , " " ), path )
1699- }
1700-
1701- func shellSingleQuote (value string ) string {
1702- return "'" + strings .ReplaceAll (value , "'" , `'\''` ) + "'"
1703- }
1704-
1705- // generateIPTablesCommands generates IPTables firewall commands to ensure docker ports are not made immediately
1706- // accessible from the internet by default.
1707- func generateIPTablesCommands () []string {
1708- commands := []string {
1709- // CPU images can run cloud-init before Docker has created DOCKER-USER.
1710- // Create it first so the immediate cloud-init run succeeds; when Docker
1711- // starts later, the systemd ExecStartPost hook above re-applies the same
1712- // rules after Docker has finished creating/resetting its own chains.
1713- "iptables -N DOCKER-USER 2>/dev/null || true" ,
1714- "iptables -F DOCKER-USER || true" ,
1715-
1716- // Preserve already-established connections, then allow container bridge
1717- // egress and intra-bridge traffic. The final DROP below is what prevents
1718- // externally-published Docker ports from being reachable by default.
1719- "iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" ,
1720- "iptables -A DOCKER-USER -i docker0 ! -o docker0 -j ACCEPT" ,
1721- "iptables -A DOCKER-USER -i br+ ! -o br+ -j ACCEPT" ,
1722- "iptables -A DOCKER-USER -i cni+ ! -o cni+ -j ACCEPT" , // TODO: add these back in when we have a way to test it
1723- "iptables -A DOCKER-USER -i cali+ ! -o cali+ -j ACCEPT" ,
1724- "iptables -A DOCKER-USER -i docker0 -o docker0 -j ACCEPT" ,
1725- "iptables -A DOCKER-USER -i br+ -o br+ -j ACCEPT" ,
1726- "iptables -A DOCKER-USER -i cni+ -o cni+ -j ACCEPT" ,
1727- "iptables -A DOCKER-USER -i cali+ -o cali+ -j ACCEPT" ,
1728- "iptables -A DOCKER-USER -i lo -j ACCEPT" ,
1729- "iptables -A DOCKER-USER -j DROP" ,
1730- "iptables -A DOCKER-USER -j RETURN" , // Expected by Docker
1731- }
1732- return commands
1684+ //
1685+ // UFW persists its own rules in /etc/ufw; only DOCKER-USER needed a Docker
1686+ // startup hook after removing netfilter-persistent.
1687+ return fmt .Sprintf (`
1688+ write_files:
1689+ - path: %s
1690+ owner: root:root
1691+ permissions: '0755'
1692+ encoding: b64
1693+ content: %s
1694+ - path: %s
1695+ owner: root:root
1696+ permissions: '0644'
1697+ encoding: b64
1698+ content: %s
1699+ ` , dockerFirewallScriptPath , base64 .StdEncoding .EncodeToString ([]byte (dockerFirewallScript )), dockerFirewallDropInPath , base64 .StdEncoding .EncodeToString ([]byte (dockerFirewallDropIn )))
17331700}
17341701
17351702// convertIngressRuleToUFW converts an ingress firewall rule to UFW command(s)
0 commit comments