+215
-110
@@ -84,17 +84,18 @@ type Finding struct {
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Capabilities Capabilities `json:"capabilities"`
|
||||
OS OSInfo `json:"os"`
|
||||
PackageManager string `json:"package_manager,omitempty"`
|
||||
InitSystem string `json:"init_system,omitempty"`
|
||||
Firewall ComponentStatus `json:"firewall"`
|
||||
Fail2Ban ComponentStatus `json:"fail2ban"`
|
||||
Auditd ComponentStatus `json:"auditd"`
|
||||
Conflicts []string `json:"firewall_conflicts"`
|
||||
Findings []Finding `json:"findings"`
|
||||
Score int `json:"score"`
|
||||
Managed map[string]ManagedConfig `json:"managed"`
|
||||
Capabilities Capabilities `json:"capabilities"`
|
||||
OS OSInfo `json:"os"`
|
||||
PackageManager string `json:"package_manager,omitempty"`
|
||||
InitSystem string `json:"init_system,omitempty"`
|
||||
Firewall ComponentStatus `json:"firewall"`
|
||||
FirewallBackend FirewallBackendInfo `json:"firewall_backend"`
|
||||
Fail2Ban ComponentStatus `json:"fail2ban"`
|
||||
Auditd ComponentStatus `json:"auditd"`
|
||||
Conflicts []string `json:"firewall_conflicts"`
|
||||
Findings []Finding `json:"findings"`
|
||||
Score int `json:"score"`
|
||||
Managed map[string]ManagedConfig `json:"managed"`
|
||||
}
|
||||
|
||||
type ManagedConfig struct {
|
||||
@@ -113,22 +114,28 @@ type FirewallRule struct {
|
||||
}
|
||||
|
||||
type FirewallPolicy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DefaultInbound string `json:"default_inbound"`
|
||||
AllowICMP bool `json:"allow_icmp"`
|
||||
TrustedCIDRs []string `json:"trusted_cidrs"`
|
||||
Rules []FirewallRule `json:"rules"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
ResolvedProvider string `json:"resolved_provider,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ManageDefault bool `json:"manage_default"`
|
||||
DefaultInbound string `json:"default_inbound"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
AllowICMP bool `json:"allow_icmp"`
|
||||
TrustedCIDRs []string `json:"trusted_cidrs"`
|
||||
Rules []FirewallRule `json:"rules"`
|
||||
}
|
||||
|
||||
type FirewallPreview struct {
|
||||
Policy FirewallPolicy `json:"policy"`
|
||||
Rendered string `json:"rendered"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Conflict []string `json:"conflicts"`
|
||||
CanApply bool `json:"can_apply"`
|
||||
Rollback bool `json:"rollback_supported"`
|
||||
ManagedTable string `json:"managed_table"`
|
||||
Persistence string `json:"persistence"`
|
||||
Policy FirewallPolicy `json:"policy"`
|
||||
Backend FirewallBackendInfo `json:"backend"`
|
||||
Runtime FirewallRuntimeView `json:"runtime"`
|
||||
Rendered string `json:"rendered"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Conflict []string `json:"conflicts"`
|
||||
CanApply bool `json:"can_apply"`
|
||||
Rollback bool `json:"rollback_supported"`
|
||||
ManagedTable string `json:"managed_table,omitempty"`
|
||||
Persistence string `json:"persistence"`
|
||||
}
|
||||
|
||||
type FirewallApplyResult struct {
|
||||
@@ -182,13 +189,16 @@ type PolicyResult struct {
|
||||
}
|
||||
|
||||
type InstallInput struct {
|
||||
Enable bool `json:"enable"`
|
||||
Enable bool `json:"enable"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
}
|
||||
|
||||
type pendingFirewall struct {
|
||||
ID string `json:"id"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Previous *FirewallPolicy `json:"previous,omitempty"`
|
||||
ID string `json:"id"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Previous *FirewallPolicy `json:"previous,omitempty"`
|
||||
Snapshot FirewallRuntimeSnapshot `json:"snapshot,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -221,26 +231,22 @@ func (s *Service) Status(ctx context.Context) Status {
|
||||
osInfo := s.osInfo()
|
||||
pm := s.packageManager()
|
||||
initSystem := s.initSystem()
|
||||
fwPolicy := s.FirewallPolicy()
|
||||
fwBackend := s.firewallBackend(ctx, fwPolicy.Provider)
|
||||
st := Status{
|
||||
Capabilities: caps,
|
||||
OS: osInfo,
|
||||
PackageManager: pm,
|
||||
InitSystem: initSystem,
|
||||
Firewall: s.componentStatus(ctx, "firewall", initSystem),
|
||||
Fail2Ban: s.componentStatus(ctx, "fail2ban", initSystem),
|
||||
Auditd: s.componentStatus(ctx, "auditd", initSystem),
|
||||
Managed: map[string]ManagedConfig{},
|
||||
Capabilities: caps,
|
||||
OS: osInfo,
|
||||
PackageManager: pm,
|
||||
InitSystem: initSystem,
|
||||
Firewall: s.firewallComponentStatus(ctx, initSystem, fwBackend),
|
||||
FirewallBackend: fwBackend,
|
||||
Fail2Ban: s.componentStatus(ctx, "fail2ban", initSystem),
|
||||
Auditd: s.componentStatus(ctx, "auditd", initSystem),
|
||||
Managed: map[string]ManagedConfig{},
|
||||
}
|
||||
st.Conflicts = s.firewallConflicts(ctx)
|
||||
if p, ok := s.loadFirewallPolicy(); ok {
|
||||
rendered, _ := renderFirewall(p)
|
||||
if p.Enabled {
|
||||
st.Managed["firewall"] = s.managedConfig(firewallHostPath, rendered)
|
||||
} else {
|
||||
m := ManagedConfig{Configured: true, Path: firewallHostPath, Hash: hashString(rendered)}
|
||||
m.Drift = fileExists(s.hostPath(firewallHostPath)) || st.Firewall.Active
|
||||
st.Managed["firewall"] = m
|
||||
}
|
||||
st.Conflicts = append([]string(nil), fwBackend.Conflicts...)
|
||||
if _, ok := s.loadFirewallPolicy(); ok {
|
||||
st.Managed["firewall"] = s.firewallManagedConfig(ctx, fwPolicy, fwBackend)
|
||||
st.Firewall.Drift = st.Managed["firewall"].Drift
|
||||
}
|
||||
if p, ok := s.loadFail2BanPolicy(); ok {
|
||||
@@ -268,15 +274,19 @@ func findings(st Status) ([]Finding, int) {
|
||||
score -= 25
|
||||
}
|
||||
if !st.Firewall.Installed {
|
||||
fs = append(fs, Finding{Severity: "high", Title: "nftables not installed", Detail: "Dockwatch cannot manage its isolated inbound firewall table without nftables.", Action: "Install nftables"})
|
||||
provider := st.FirewallBackend.Selected
|
||||
if provider == "" {
|
||||
provider = "firewall"
|
||||
}
|
||||
fs = append(fs, Finding{Severity: "high", Title: "Firewall provider not installed", Detail: "Selected provider " + provider + " is not installed on this host.", Action: "Install " + provider})
|
||||
score -= 25
|
||||
} else if !st.Firewall.Active {
|
||||
fs = append(fs, Finding{Severity: "medium", Title: "Dockwatch firewall policy not active", Detail: "The managed inet/dockwatch nftables table is not currently loaded."})
|
||||
fs = append(fs, Finding{Severity: "medium", Title: "Firewall frontend inactive", Detail: "Selected provider " + st.FirewallBackend.Selected + " is installed but not currently active."})
|
||||
score -= 15
|
||||
}
|
||||
if len(st.Conflicts) > 0 {
|
||||
fs = append(fs, Finding{Severity: "medium", Title: "Another firewall frontend is active", Detail: "Dockwatch detected: " + strings.Join(st.Conflicts, ", ") + ". Managed firewall changes are blocked by default to avoid conflicting rule owners."})
|
||||
score -= 5
|
||||
fs = append(fs, Finding{Severity: "high", Title: "Competing firewall frontends detected", Detail: "Dockwatch detected conflicting active frontends: " + strings.Join(st.Conflicts, ", ") + ". Resolve this before applying changes."})
|
||||
score -= 10
|
||||
}
|
||||
if !st.Fail2Ban.Installed {
|
||||
fs = append(fs, Finding{Severity: "medium", Title: "Fail2Ban not installed", Detail: "Brute-force protection is not available through Dockwatch.", Action: "Install Fail2Ban"})
|
||||
@@ -580,47 +590,84 @@ func (s *Service) persistenceEnabled(ctx context.Context, initSystem, service st
|
||||
}
|
||||
|
||||
func (s *Service) firewallConflicts(ctx context.Context) []string {
|
||||
if !s.capabilitiesNoCommand().TargetVerified {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
x, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
if fileExists(s.hostPath("/usr/sbin/ufw")) || fileExists(s.hostPath("/usr/bin/ufw")) {
|
||||
if v, err := s.hostCommand(x, nil, "ufw", "status"); err == nil && strings.Contains(strings.ToLower(v), "status: active") {
|
||||
out = append(out, "ufw")
|
||||
}
|
||||
}
|
||||
if fileExists(s.hostPath("/usr/bin/firewall-cmd")) {
|
||||
if _, err := s.hostCommand(x, nil, "firewall-cmd", "--state"); err == nil {
|
||||
out = append(out, "firewalld")
|
||||
}
|
||||
}
|
||||
return out
|
||||
return append([]string(nil), s.firewallBackend(ctx, FirewallProviderAuto).Conflicts...)
|
||||
}
|
||||
|
||||
func (s *Service) FirewallPolicy() FirewallPolicy {
|
||||
if p, ok := s.loadFirewallPolicy(); ok {
|
||||
if normalizeFirewallProvider(p.Provider) == "" {
|
||||
p.Provider = FirewallProviderAuto
|
||||
}
|
||||
if p.DefaultInbound == "" {
|
||||
p.DefaultInbound = "accept"
|
||||
}
|
||||
if p.TrustedCIDRs == nil {
|
||||
p.TrustedCIDRs = []string{}
|
||||
}
|
||||
if p.Rules == nil {
|
||||
p.Rules = []FirewallRule{}
|
||||
}
|
||||
return p
|
||||
}
|
||||
return FirewallPolicy{Enabled: false, DefaultInbound: "accept", AllowICMP: true, TrustedCIDRs: []string{}, Rules: []FirewallRule{{Action: "accept", Protocol: "tcp", Port: "22", Comment: "SSH"}}}
|
||||
return FirewallPolicy{Provider: FirewallProviderAuto, Enabled: false, ManageDefault: false, DefaultInbound: "accept", AllowICMP: true, TrustedCIDRs: []string{}, Rules: []FirewallRule{{Action: "accept", Protocol: "tcp", Port: "22", Comment: "SSH"}}}
|
||||
}
|
||||
|
||||
func (s *Service) PreviewFirewall(ctx context.Context, p FirewallPolicy) (FirewallPreview, error) {
|
||||
rendered, err := renderFirewall(p)
|
||||
provider := normalizeFirewallProvider(p.Provider)
|
||||
if provider == "" {
|
||||
return FirewallPreview{}, errors.New("provider must be auto, ufw, firewalld or nftables")
|
||||
}
|
||||
p.Provider = provider
|
||||
if p.DefaultInbound == "" {
|
||||
p.DefaultInbound = "accept"
|
||||
}
|
||||
if p.TrustedCIDRs == nil {
|
||||
p.TrustedCIDRs = []string{}
|
||||
}
|
||||
if p.Rules == nil {
|
||||
p.Rules = []FirewallRule{}
|
||||
}
|
||||
backend := s.firewallBackend(ctx, provider)
|
||||
p.ResolvedProvider = backend.Selected
|
||||
runtime := s.firewallRuntime(ctx, provider)
|
||||
if backend.Selected == FirewallProviderFirewalld && p.Zone == "" {
|
||||
p.Zone = runtime.Zone
|
||||
if p.Zone == "" {
|
||||
p.Zone = backend.DefaultZone
|
||||
}
|
||||
}
|
||||
if backend.Selected == FirewallProviderFirewalld && p.Zone != "" && (!safeNameRE.MatchString(p.Zone) || strings.HasPrefix(p.Zone, "-")) {
|
||||
return FirewallPreview{}, errors.New("firewalld zone name is invalid")
|
||||
}
|
||||
rendered, warnings, err := renderFirewallPlan(p, backend, runtime)
|
||||
if err != nil {
|
||||
return FirewallPreview{}, err
|
||||
}
|
||||
conflicts := s.firewallConflicts(ctx)
|
||||
warnings := []string{"Dockwatch manages only table inet dockwatch and never flushes the global nftables ruleset.", "Docker forwarding/NAT chains are not changed; this policy hooks only host INPUT traffic."}
|
||||
if p.DefaultInbound == "drop" {
|
||||
if p.DefaultInbound == "drop" && (backend.Selected == FirewallProviderNftables || p.ManageDefault) {
|
||||
warnings = append(warnings, "Default inbound DROP can lock out SSH or Dockwatch. Apply uses a timed rollback until explicitly committed.")
|
||||
}
|
||||
if len(conflicts) > 0 {
|
||||
warnings = append(warnings, "Another firewall frontend is active: "+strings.Join(conflicts, ", ")+". Apply is blocked to prevent multiple owners of host filtering policy.")
|
||||
if len(backend.Conflicts) > 0 {
|
||||
warnings = append(warnings, "Competing active firewall frontends detected: "+strings.Join(backend.Conflicts, ", ")+". Apply is blocked until only the selected frontend owns host filtering.")
|
||||
}
|
||||
if backend.Selected == FirewallProviderFirewalld && p.ManageDefault && !runtime.Active {
|
||||
warnings = append(warnings, "firewalld is inactive. Start it before changing the zone target so Dockwatch can snapshot the current runtime/permanent target for rollback.")
|
||||
}
|
||||
if oneOf(backend.Selected, FirewallProviderUFW, FirewallProviderFirewalld) && p.Enabled && !runtime.Active {
|
||||
warnings = append(warnings, "The selected firewall frontend is inactive. Applying an enabled Dockwatch policy will activate it; rollback restores the previous active state.")
|
||||
}
|
||||
if !containsString(backend.Available, backend.Selected) {
|
||||
warnings = append(warnings, backend.Selected+" is not installed on this host.")
|
||||
}
|
||||
caps := s.capabilities(ctx)
|
||||
return FirewallPreview{Policy: p, Rendered: rendered, Warnings: warnings, Conflict: conflicts, CanApply: caps.AllowChanges && caps.ExecutorAvailable && caps.HostRootWritable && len(conflicts) == 0, Rollback: true, ManagedTable: "inet dockwatch", Persistence: s.initSystem()}, nil
|
||||
managedTable := ""
|
||||
if backend.Selected == FirewallProviderNftables {
|
||||
managedTable = "inet dockwatch"
|
||||
}
|
||||
canApply := caps.AllowChanges && caps.ExecutorAvailable && caps.HostRootWritable && len(backend.Conflicts) == 0 && containsString(backend.Available, backend.Selected)
|
||||
if backend.Selected == FirewallProviderFirewalld && p.ManageDefault && !runtime.Active {
|
||||
canApply = false
|
||||
}
|
||||
return FirewallPreview{Policy: p, Backend: backend, Runtime: runtime, Rendered: rendered, Warnings: warnings, Conflict: backend.Conflicts, CanApply: canApply, Rollback: true, ManagedTable: managedTable, Persistence: s.initSystem()}, nil
|
||||
}
|
||||
|
||||
func renderFirewall(p FirewallPolicy) (string, error) {
|
||||
@@ -636,8 +683,11 @@ func renderFirewall(p FirewallPolicy) (string, error) {
|
||||
}
|
||||
}
|
||||
for i, r := range p.Rules {
|
||||
if r.Action != "accept" && r.Action != "drop" {
|
||||
return "", fmt.Errorf("rule %d action must be accept or drop", i+1)
|
||||
if !oneOf(r.Action, "accept", "drop", "reject", "limit") {
|
||||
return "", fmt.Errorf("rule %d action must be accept, drop, reject or limit", i+1)
|
||||
}
|
||||
if r.Action == "limit" && r.Protocol != "tcp" {
|
||||
return "", fmt.Errorf("rule %d limit action is supported only for tcp", i+1)
|
||||
}
|
||||
if r.Protocol != "tcp" && r.Protocol != "udp" {
|
||||
return "", fmt.Errorf("rule %d protocol must be tcp or udp", i+1)
|
||||
@@ -687,7 +737,12 @@ func renderFirewall(p FirewallPolicy) (string, error) {
|
||||
comment = "dockwatch " + strings.TrimSpace(r.Comment)
|
||||
}
|
||||
comment = strings.ReplaceAll(comment, `"`, `'`)
|
||||
b.WriteString(fmt.Sprintf(" %s%s dport %s %s comment %s\n", src, r.Protocol, r.Port, r.Action, strconv.Quote(comment)))
|
||||
action := r.Action
|
||||
if action == "limit" {
|
||||
b.WriteString(fmt.Sprintf(" %s%s dport %s ct state new limit rate 6/minute accept comment %s\n", src, r.Protocol, r.Port, strconv.Quote(comment)))
|
||||
continue
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s%s dport %s %s comment %s\n", src, r.Protocol, r.Port, action, strconv.Quote(comment)))
|
||||
}
|
||||
b.WriteString(" }\n}\n")
|
||||
return b.String(), nil
|
||||
@@ -715,16 +770,20 @@ func (s *Service) ApplyFirewall(ctx context.Context, p FirewallPolicy, rollbackS
|
||||
if err := s.requireChanges(ctx); err != nil {
|
||||
return FirewallApplyResult{}, err
|
||||
}
|
||||
if len(s.firewallConflicts(ctx)) > 0 {
|
||||
return FirewallApplyResult{}, errors.New("another firewall frontend is active (ufw/firewalld); Dockwatch refuses to apply a competing managed ruleset")
|
||||
}
|
||||
if !fileExists(s.hostPath("/usr/sbin/nft")) && !fileExists(s.hostPath("/sbin/nft")) {
|
||||
return FirewallApplyResult{}, errors.New("nftables is not installed")
|
||||
}
|
||||
preview, err := s.PreviewFirewall(ctx, p)
|
||||
if err != nil {
|
||||
return FirewallApplyResult{}, err
|
||||
}
|
||||
p = preview.Policy
|
||||
if len(preview.Backend.Conflicts) > 0 {
|
||||
return FirewallApplyResult{}, fmt.Errorf("competing firewall frontend(s) active: %s", strings.Join(preview.Backend.Conflicts, ", "))
|
||||
}
|
||||
if !containsString(preview.Backend.Available, preview.Backend.Selected) {
|
||||
return FirewallApplyResult{}, fmt.Errorf("selected firewall provider %s is not installed", preview.Backend.Selected)
|
||||
}
|
||||
if preview.Backend.Selected == FirewallProviderFirewalld && p.ManageDefault && !preview.Runtime.Active {
|
||||
return FirewallApplyResult{}, errors.New("firewalld must be active before Dockwatch can change and safely roll back the zone target")
|
||||
}
|
||||
if rollbackSeconds == 0 {
|
||||
rollbackSeconds = 90
|
||||
}
|
||||
@@ -736,30 +795,34 @@ func (s *Service) ApplyFirewall(ctx context.Context, p FirewallPolicy, rollbackS
|
||||
if hadPrevious {
|
||||
previousPtr = &previous
|
||||
}
|
||||
rollbackState := pendingFirewall{Previous: previousPtr}
|
||||
if p.Enabled {
|
||||
if err := s.validateFirewallRuntime(ctx, preview.Rendered); err != nil {
|
||||
return FirewallApplyResult{}, err
|
||||
snapshot := s.snapshotFirewallRuntime(ctx, preview.Backend.Selected, p.Zone)
|
||||
rollbackState := pendingFirewall{Provider: preview.Backend.Selected, Previous: previousPtr, Snapshot: snapshot}
|
||||
if hadPrevious {
|
||||
oldProvider := s.providerForPolicy(ctx, previous)
|
||||
if oldProvider != "" && oldProvider != preview.Backend.Selected {
|
||||
if err := s.removeCurrentManagedFirewall(ctx, previous); err != nil {
|
||||
return FirewallApplyResult{}, fmt.Errorf("remove previous %s managed firewall before provider switch: %w", oldProvider, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.configureFirewallPersistence(ctx, p.Enabled, preview.Rendered); err != nil {
|
||||
_ = s.rollbackFirewallLocked(ctx, rollbackState)
|
||||
return FirewallApplyResult{}, err
|
||||
}
|
||||
if err := s.applyFirewallRuntime(ctx, p, preview.Rendered); err != nil {
|
||||
if err := s.applyFirewallProvider(ctx, p, previousPtr, preview.Backend); err != nil {
|
||||
s.cleanupAttemptedFirewall(ctx, p, preview.Backend)
|
||||
_ = s.rollbackFirewallLocked(ctx, rollbackState)
|
||||
return FirewallApplyResult{}, err
|
||||
}
|
||||
if err := s.savePolicy("firewall.json", p); err != nil {
|
||||
s.cleanupAttemptedFirewall(ctx, p, preview.Backend)
|
||||
_ = s.rollbackFirewallLocked(ctx, rollbackState)
|
||||
return FirewallApplyResult{}, err
|
||||
}
|
||||
id := randomID()
|
||||
pending := pendingFirewall{ID: id, ExpiresAt: time.Now().Add(time.Duration(rollbackSeconds) * time.Second).Unix(), Previous: previousPtr}
|
||||
pending := pendingFirewall{ID: id, ExpiresAt: time.Now().Add(time.Duration(rollbackSeconds) * time.Second).Unix(), Provider: preview.Backend.Selected, Previous: previousPtr, Snapshot: snapshot}
|
||||
if err := s.savePendingFirewall(pending); err != nil {
|
||||
_ = s.rollbackFirewallLocked(ctx, rollbackState)
|
||||
return FirewallApplyResult{}, err
|
||||
}
|
||||
s.scheduleFirewallRollback(pending)
|
||||
return FirewallApplyResult{OK: true, ChangeID: id, ExpiresAt: pending.ExpiresAt, Preview: preview, Message: "Firewall applied temporarily. Commit the change before the rollback timer expires."}, nil
|
||||
return FirewallApplyResult{OK: true, ChangeID: id, ExpiresAt: pending.ExpiresAt, Preview: preview, Message: "Firewall policy applied through " + preview.Backend.Selected + ". Commit the change before the rollback timer expires."}, nil
|
||||
}
|
||||
|
||||
func (s *Service) CommitFirewall(id string) error {
|
||||
@@ -917,25 +980,43 @@ func (s *Service) scheduleFirewallRollback(p pendingFirewall) {
|
||||
}
|
||||
|
||||
func (s *Service) rollbackFirewallLocked(ctx context.Context, p pendingFirewall) error {
|
||||
current, hasCurrent := s.loadFirewallPolicy()
|
||||
if p.Previous == nil {
|
||||
disabled := FirewallPolicy{Enabled: false, DefaultInbound: "accept", AllowICMP: true}
|
||||
if err := s.configureFirewallPersistence(ctx, false, ""); err != nil {
|
||||
return err
|
||||
if hasCurrent {
|
||||
if err := s.removeCurrentManagedFirewall(ctx, current); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.applyFirewallRuntime(ctx, disabled, ""); err != nil {
|
||||
if err := s.restoreFirewallSnapshot(ctx, p.Snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(s.securityPath("firewall.json"))
|
||||
return nil
|
||||
}
|
||||
rendered, err := renderFirewall(*p.Previous)
|
||||
if err != nil {
|
||||
return err
|
||||
provider := s.providerForPolicy(ctx, *p.Previous)
|
||||
backend := s.firewallBackend(ctx, provider)
|
||||
var currentPtr *FirewallPolicy
|
||||
restoredSnapshot := false
|
||||
if hasCurrent {
|
||||
currentPtr = ¤t
|
||||
currentProvider := s.providerForPolicy(ctx, current)
|
||||
if currentProvider != "" && currentProvider != provider {
|
||||
if err := s.removeCurrentManagedFirewall(ctx, current); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.restoreFirewallSnapshot(ctx, p.Snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
restoredSnapshot = true
|
||||
currentPtr = nil
|
||||
}
|
||||
}
|
||||
if err := s.configureFirewallPersistence(ctx, p.Previous.Enabled, rendered); err != nil {
|
||||
return err
|
||||
if !restoredSnapshot && !p.Previous.ManageDefault && oneOf(p.Snapshot.Provider, FirewallProviderUFW, FirewallProviderFirewalld) {
|
||||
if err := s.restoreFirewallSnapshot(ctx, p.Snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.applyFirewallRuntime(ctx, *p.Previous, rendered); err != nil {
|
||||
if err := s.applyFirewallProvider(ctx, *p.Previous, currentPtr, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.savePolicy("firewall.json", *p.Previous)
|
||||
@@ -1185,7 +1266,13 @@ func (s *Service) Install(ctx context.Context, component string, in InstallInput
|
||||
if err := s.requirePackages(ctx); err != nil {
|
||||
return PolicyResult{}, err
|
||||
}
|
||||
pkg, service, err := s.packageFor(component)
|
||||
var pkg, service string
|
||||
var err error
|
||||
if component == "firewall" {
|
||||
pkg, service, err = s.firewallPackage(in.Provider)
|
||||
} else {
|
||||
pkg, service, err = s.packageFor(component)
|
||||
}
|
||||
if err != nil {
|
||||
return PolicyResult{}, err
|
||||
}
|
||||
@@ -1232,8 +1319,23 @@ func (s *Service) Install(ctx context.Context, component string, in InstallInput
|
||||
default:
|
||||
return PolicyResult{}, errors.New("unsupported package manager")
|
||||
}
|
||||
if in.Enable && service != "" {
|
||||
_, _ = s.serviceActionUnlocked(ctx, component, "enable")
|
||||
if component == "firewall" {
|
||||
// Persist an explicit provider selection even when installation does not
|
||||
// activate the frontend. This keeps Auto from immediately switching back
|
||||
// to another already-installed firewall after package installation.
|
||||
p := s.FirewallPolicy()
|
||||
if v := normalizeFirewallProvider(in.Provider); v != "" && v != FirewallProviderAuto {
|
||||
p.Provider = v
|
||||
p.ResolvedProvider = v
|
||||
_ = s.savePolicy("firewall.json", p)
|
||||
}
|
||||
}
|
||||
if in.Enable {
|
||||
if component == "firewall" {
|
||||
_, _ = s.firewallServiceAction(ctx, "enable")
|
||||
} else if service != "" {
|
||||
_, _ = s.serviceActionUnlocked(ctx, component, "enable")
|
||||
}
|
||||
}
|
||||
return PolicyResult{OK: true, Message: component + " installed/updated.", Output: limit(out, 12000)}, nil
|
||||
}
|
||||
@@ -1258,7 +1360,10 @@ func (s *Service) serviceActionUnlocked(ctx context.Context, component, action s
|
||||
if !oneOf(action, "enable", "disable", "restart", "reload", "start", "stop") {
|
||||
return "", errors.New("unsupported service action")
|
||||
}
|
||||
service := map[string]string{"firewall": "dockwatch-firewall", "fail2ban": "fail2ban", "auditd": "auditd"}[component]
|
||||
if component == "firewall" {
|
||||
return s.firewallServiceAction(ctx, action)
|
||||
}
|
||||
service := map[string]string{"fail2ban": "fail2ban", "auditd": "auditd"}[component]
|
||||
if component == "auditd" && action == "reload" {
|
||||
x, c := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer c()
|
||||
@@ -1302,7 +1407,7 @@ func (s *Service) packageFor(component string) (pkg, service string, err error)
|
||||
osid := s.osInfo().ID
|
||||
switch component {
|
||||
case "firewall":
|
||||
return "nftables", "", nil
|
||||
return s.firewallPackage(FirewallProviderAuto)
|
||||
case "fail2ban":
|
||||
return "fail2ban", "fail2ban", nil
|
||||
case "auditd":
|
||||
|
||||
Reference in New Issue
Block a user