Skip to content

feat(firewall): Enforce the host firewall policy through nftables#180

Merged
nfebe merged 1 commit into
mainfrom
feat/firewall-enforcement
Jul 17, 2026
Merged

feat(firewall): Enforce the host firewall policy through nftables#180
nfebe merged 1 commit into
mainfrom
feat/firewall-enforcement

Conversation

@nfebe

@nfebe nfebe commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

The firewall app persisted, validated, and previewed a host-wide inbound and outbound policy but never enforced it. Saving a policy now translates it to nftables and applies it, the stored policy is re-applied at startup so it survives a restart, and disabling the firewall removes only its rules.

Enforcement only ever touches FlatRun's own nftables table, so rules managed by Docker or the operator elsewhere are left alone. The generated ruleset always keeps loopback, established connections, and the active SSH port open (detected from the current session, else port 22), so switching to a default-deny inbound policy cannot lock the operator out. A new ruleset is snapshotted before it loads and restored if the load fails, so a rejected policy never leaves the host half-configured. Where nftables is not available the policy is still saved but reported as not enforced.

Verification: the translation and the apply/rollback logic are unit-tested with a fake runner that never touches the host firewall, and the generated ruleset was separately loaded into real nft inside an isolated container network namespace, where it loaded, read back correctly, re-applied idempotently, and disabled cleanly. It was deliberately not applied to a live host's firewall, so please review the ruleset and the SSH safeguard before rolling out.

The firewall app persisted, validated, and previewed a host-wide inbound and
outbound policy but never enforced it. Saving a policy now translates it to
nftables and applies it, and the stored policy is re-applied at startup so it
survives a restart. Disabling the firewall removes only its rules.

Enforcement only ever touches FlatRun's own nftables table, so rules managed by
Docker or the operator elsewhere are left alone, and the generated ruleset
always keeps loopback, established connections, and the active SSH port open, so
switching to a default-deny inbound policy cannot lock the operator out. A new
ruleset is snapshotted before it loads and restored if the load fails, so a
rejected policy never leaves the host half-configured. Where nftables is not
available the policy is still saved but reported as not enforced.
@sourceant

sourceant Bot commented Jul 17, 2026

Copy link
Copy Markdown

Code Review Summary

The PR successfully implements firewall enforcement using nftables. The logic is well-contained and respects existing host rules. However, the manual rollback mechanism is redundant due to nft's atomicity and poses a significant risk to system stability.

🚀 Key Improvements

  • Introduction of a dedicated flatrun_firewall table for isolation.
  • Atomic rule application via nft -f -.
  • Safe default policies that include loopback and connection tracking.

💡 Minor Suggestions

  • Address the fragility of SSH_CONNECTION detection in daemonized environments.

🚨 Critical Issues

  • The manual rollback logic involving flush ruleset should be removed to prevent catastrophic failure of the host firewall state.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

@@ -151,10 +151,59 @@ func Plan(cfg *Config) []string {
return plan
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nft -f provides atomic execution; if the script contains an error, the kernel transaction is aborted and the previous ruleset remains completely untouched. The manual rollback implemented here is not only redundant but dangerous: flush ruleset clears all tables on the host, including those managed by Docker or other system services. If the subsequent reload of the snapshot fails (e.g., due to syntax issues in nft list ruleset output), the host could be left with no firewall or lost connectivity. It is safer to rely on nftables' native atomicity.

Suggested change
}
script := renderNftables(cfg, detectSSHSession())
if err := runner.ApplyScript(script); err != nil {
return false, fmt.Errorf("failed to apply firewall rules: %w", err)
}

Comment on lines +25 to +26
// default-deny policy never locks the operator out.
func detectSSHSession() sshSession {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detecting the SSH port via SSH_CONNECTION is a fragile heuristic. While it works when the process is started manually from an SSH session, background services (like those started via systemd) will typically have an empty environment. This results in the firewall defaulting to port 22. If the user has configured SSH on a different port and expects the automatic safeguard to work, they may be locked out. Consider making the safeguard port configurable or adding an explicit check/warning.

Suggested change
// default-deny policy never locks the operator out.
func detectSSHSession() sshSession {
// detectSSHSession reads SSH_CONNECTION from the environment.
// Warning: This environment variable is usually missing for daemonized processes.
func detectSSHSession() sshSession {
parts := strings.Fields(os.Getenv("SSH_CONNECTION"))
if len(parts) != 4 {
return sshSession{}
}
port, err := strconv.Atoi(parts[3])
if err != nil || port <= 0 || port > 65535 {
return sshSession{}
}
return sshSession{clientIP: parts[0], serverPort: port}
}

@nfebe
nfebe merged commit 5324087 into main Jul 17, 2026
6 checks passed
@nfebe
nfebe deleted the feat/firewall-enforcement branch July 17, 2026 18:43
@nfebe nfebe mentioned this pull request Jul 19, 2026
42 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant