@@ -1,6 +1,8 @@
|
||||
package hostsecurity
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -73,3 +75,119 @@ func TestAuditWatchValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallProviderPlansPreserveForeignRules(t *testing.T) {
|
||||
p := FirewallPolicy{
|
||||
Provider: FirewallProviderUFW,
|
||||
Enabled: true,
|
||||
ManageDefault: true,
|
||||
DefaultInbound: "drop",
|
||||
TrustedCIDRs: []string{"192.0.2.0/24"},
|
||||
Rules: []FirewallRule{
|
||||
{Action: "accept", Protocol: "tcp", Port: "22", Comment: "ssh"},
|
||||
{Action: "limit", Protocol: "tcp", Port: "443", Source: "198.51.100.0/24", Comment: "https"},
|
||||
},
|
||||
}
|
||||
backend := FirewallBackendInfo{Selected: FirewallProviderUFW}
|
||||
out, warnings, err := renderFirewallPlan(p, backend, FirewallRuntimeView{Provider: FirewallProviderUFW})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"ufw default deny incoming", "ufw allow from 192.0.2.0/24", "ufw allow proto tcp", "ufw limit proto tcp"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("UFW plan missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(out), "ufw reset") {
|
||||
t.Fatal("UFW provider must never reset foreign rules")
|
||||
}
|
||||
if len(warnings) == 0 {
|
||||
t.Fatal("expected provider safety warnings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewalldPlanUsesNativeRichRules(t *testing.T) {
|
||||
p := FirewallPolicy{
|
||||
Provider: FirewallProviderFirewalld,
|
||||
Enabled: true,
|
||||
ManageDefault: true,
|
||||
DefaultInbound: "drop",
|
||||
Zone: "public",
|
||||
Rules: []FirewallRule{{Action: "reject", Protocol: "tcp", Port: "23", Source: "203.0.113.0/24"}},
|
||||
}
|
||||
backend := FirewallBackendInfo{Selected: FirewallProviderFirewalld, DefaultZone: "public"}
|
||||
out, _, err := renderFirewallPlan(p, backend, FirewallRuntimeView{Provider: FirewallProviderFirewalld, Zone: "public"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"firewall-cmd --zone=public --set-target=DROP", "--add-rich-rule", `source address=\"203.0.113.0/24\"`, "reject"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("firewalld plan missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "--remove-service") || strings.Contains(out, "--remove-port") {
|
||||
t.Fatal("preview must not suggest deleting foreign zone primitives")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallProviderNormalization(t *testing.T) {
|
||||
for in, want := range map[string]string{"": "auto", "AUTO": "auto", "ufw": "ufw", "Firewalld": "firewalld", "nftables": "nftables"} {
|
||||
if got := normalizeFirewallProvider(in); got != want {
|
||||
t.Fatalf("normalize %q = %q want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
if got := normalizeFirewallProvider("iptables"); got != "" {
|
||||
t.Fatalf("unsupported provider should normalize to empty, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderNftablesSupportsRejectAndLimit(t *testing.T) {
|
||||
p := FirewallPolicy{DefaultInbound: "accept", Rules: []FirewallRule{
|
||||
{Action: "reject", Protocol: "tcp", Port: "23"},
|
||||
{Action: "limit", Protocol: "tcp", Port: "22"},
|
||||
}}
|
||||
out, err := renderFirewall(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out, "tcp dport 23 reject") {
|
||||
t.Fatalf("reject rule missing:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "tcp dport 22 ct state new limit rate 6/minute accept") {
|
||||
t.Fatalf("limit rule missing:\n%s", out)
|
||||
}
|
||||
bad := FirewallPolicy{Rules: []FirewallRule{{Action: "limit", Protocol: "udp", Port: "53"}}}
|
||||
if _, err := renderFirewall(bad); err == nil {
|
||||
t.Fatal("UDP limit rule should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUFWDefaultFromConfigWhenInactive(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "etc", "default"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "etc", "default", "ufw"), []byte("DEFAULT_INPUT_POLICY=\"DROP\"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := New(Config{Enabled: true, HostRoot: root, DataDir: t.TempDir()})
|
||||
if got := s.ufwDefaultFromConfig(); got != "drop" {
|
||||
t.Fatalf("ufw default = %q want drop", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUFWRulesHaveDeterministicDockwatchOwnershipTags(t *testing.T) {
|
||||
r := FirewallRule{Action: "accept", Protocol: "tcp", Port: "22", Source: "192.0.2.0/24", Comment: "ssh admin"}
|
||||
a := ufwRuleArgs(r)
|
||||
b := ufwRuleArgs(r)
|
||||
if strings.Join(a, "|") != strings.Join(b, "|") {
|
||||
t.Fatal("UFW rule tag must be deterministic")
|
||||
}
|
||||
joined := strings.Join(a, " ")
|
||||
if !strings.Contains(joined, "dockwatch:rule:") {
|
||||
t.Fatalf("missing Dockwatch ownership tag: %s", joined)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(joined), "reset") {
|
||||
t.Fatal("UFW rule command must not reset firewall")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user