Linux Firewall Hardening

Safe Linux firewall hardening with backend detection, idempotent atomic rules, rollback protection, and AI-executable state-machine workflows. Covers ufw, firewalld, nftables, iptables, Docker, Kubernetes CNI awareness, and fail2ban with compliance mapping to CIS/PCI-DSS/SOC2.

Audits

Pass

Install

openclaw skills install linux-firewall-hardening

Linux Firewall Hardening

When to Use

  • Check if a Linux server has active firewall protection.
  • Enable and configure a firewall without locking yourself out of SSH.
  • Audit existing rules, troubleshoot connectivity, or apply a security profile.
  • Automate firewall hardening via an AI agent or CI/CD pipeline.

When NOT to Use

ConditionAlternative
Kubernetes worker nodeUse NetworkPolicies / CiliumNetworkPolicy
Firewall managed by Terraform/Ansible/Puppet/ChefUpdate IaC source of truth
Cloud workload with Security Group / NSG onlyUse cloud provider's firewall API
Inside a containerEscalate to host operator
WSL2, macOS, or shared/managed hostingSee references/special-environments.md

Support files: scripts/audit-firewall.sh (run first), scripts/firewall-plan.sh (dry-run), scripts/firewall-verify.sh (post-apply). Detailed backend guides, Docker/K8s policies, observability, compliance, and recovery are in references/.


Prerequisites

  • Root or sudo access.
  • An active SSH session (risk of lockout).
  • Know which ports your services use.

NEVER DO (14 Rules)

  1. Never flush iptables/nftables on Kubernetes nodes. CNI plugins manage netfilter.
  2. Never run iptables -F or nft flush ruleset without a verified backup. Docker/K8s networking will break.
  3. Never disable firewalld and use raw iptables simultaneously. Undefined behavior.
  4. Never set DROP policy on INPUT before allowing your current SSH port. Immediate lockout.
  5. Never disable Docker's iptables management without replacement NAT/routing rules.
  6. Never restart networking.service or NetworkManager remotely without console access.
  7. Never apply cloud SG and host firewall changes simultaneously without testing.
  8. Never enable logging on high-traffic DROP rules without limit rate. Disk flood.
  9. Never manage nftables/iptables directly when ufw or firewalld owns the policy. Split-brain state.
  10. Never apply outbound default-deny without explicitly allowing DNS, NTP, package mirrors.
  11. Never restore firewall backups from a different host, kernel version, or backend mode.
  12. Never assume IPv4 rules protect IPv6. Verify both stacks separately.
  13. Never change sysctl hardening values on K8s/CNI hosts without explicit CNI profile support.
  14. Never enable verbose packet logging without rate limits and log rotation.

State Machine

Follow states in order. Do not skip.

DETECT → SELECT → PLAN → VALIDATE → APPLY → VERIFY

State: DETECT

Run the audit script:

bash scripts/audit-firewall.sh           # Human-readable
bash scripts/audit-firewall.sh --json    # Machine-readable

Key outputs: confidence, risk_tier, recommended_backend, halt_reasons, k8s_node, iac_owner.

Risk Tiers & Confidence Gating

TierConfidenceAgent Behavior
auto≥ 90%Proceed automatically to PLAN
confirmed70–89%Proceed but require human confirmation before APPLY
manual50–69%Audit-only mode. Generate recommendations, do not apply.
halt< 50%Stop immediately. Escalate findings to operator.

Additional halt triggers (regardless of confidence): containerized, K8s node, IaC managed, no rollback mechanism available.

Decision Tree

ConditionPathDetail
Risk tier = haltSTOPResolve blockers first
Inside containerSTOPEscalate to host operator
K8s node detectedSTOPreferences/k8s-policy.md
Ubuntu/Debian + ufw activePhase: UFWreferences/backend-ufw.md
ufw + firewalld both activeSTOPResolve conflict
RHEL/Rocky/Alma + firewalld activePhase: firewalldreferences/backend-firewalld.md
nftables active, no frontendPhase: nftablesreferences/backend-nftables.md
iptables onlyPhase: iptablesreferences/backend-iptables.md
Docker hostApply Docker Hardening after phase abovereferences/docker-hardening.md

Ownership Boundary

Before modifying rules, verify no IaC tool manages the firewall. If Terraform/Ansible/Puppet/Chef/cloud-init is detected → do not mutate. Update the source of truth instead. Full detection logic is in scripts/audit-firewall.sh.


State: SELECT

Optionally load a pre-built security profile (references/security-profiles.md):

ProfileUse Case
public-web-serverOpen 22, 80, 443. Rate-limit SSH.
internal-databaseSSH from mgmt subnet only. DB port from app subnet only.
bastion-hostSSH only. Aggressive rate limiting.
zero-trust-nodeDefault deny all inbound and outbound.

Or use declarative YAML (references/declarative-policy.md):

Imperative (state machine) → Ad-hoc hardening, incident response
Declarative (YAML)        → GitOps, multi-host, reproducible
Mixed                     → YAML as source-of-truth, state machine for verification

State: PLAN

Generate a dry-run diff before applying:

bash scripts/firewall-plan.sh --profile public-web-server
bash scripts/firewall-plan.sh --ports 22,80,443
bash scripts/firewall-plan.sh --json     # Machine-readable diff

Review the output. If risk_tier is confirmed, present the plan and wait for human confirmation before APPLY.

Plan JSON schema (machine-readable output):

{
  "backend": "ufw",
  "current_state": "active",
  "diff": {
    "add":    [{"port": 80, "proto": "tcp", "source": "any"}],
    "skip":   [{"port": 22, "proto": "tcp", "reason": "already_exists"}],
    "remove": []
  },
  "risk_assessment": "low",
  "estimated_disruption": "none",
  "approval_token": "sha256:abc123..."
}

Approval gate: PLAN output includes an approval_token (hash of plan content). APPLY must be called with --approved-plan=<token>. Token mismatch → exit code 41. This forces explicit human confirmation before Apply.

Audit caching: firewall-plan.sh internally calls audit-firewall.sh --json and caches to /tmp/firewall-audit.json (TTL 5 min). Use --refresh-audit to force refresh.


State: VALIDATE

1. Create Backup (Mandatory)

BACKUP_DIR="$HOME/firewall-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"

sudo iptables-save > "$BACKUP_DIR/iptables-v4.rules" 2>/dev/null || true
sudo ip6tables-save > "$BACKUP_DIR/iptables-v6.rules" 2>/dev/null || true
sudo nft list ruleset > "$BACKUP_DIR/nftables.rules" 2>/dev/null || true
sudo ufw status verbose > "$BACKUP_DIR/ufw-status.txt" 2>/dev/null || true
sudo firewall-cmd --list-all --zone=$(sudo firewall-cmd --get-default-zone) > "$BACKUP_DIR/firewalld-default.txt" 2>/dev/null || true

echo "Backup saved to $BACKUP_DIR"

2. Schedule Rollback (Mandatory for Remote)

The rollback restores from backup — not just disables the firewall — so Docker NAT and pre-existing rules are preserved. Dual-backend: at preferred, systemd-run fallback. Full script in SKILL.md under Validate state (see previous version), also summarized in references/recovery.md.

3. Pre-Flight Checklist

  • Backup created successfully
  • Rollback scheduled (verify with atq or systemctl --user list-units)
  • Second SSH session open and idle
  • Real SSH port identified
  • Confidence ≥ 70% and risk_tier is auto or confirmed
  • Ownership verified — no IaC managing firewall
  • Change window appropriate (maintenance window or low traffic)
  • PLAN output reviewed and approved

State: APPLY

All commands use idempotent patterns: check-before-set. Safe to run repeatedly.

BackendPattern
ufwsudo ufw status | awk '{print $1}' | grep -qx "22/tcp" || sudo ufw allow 22/tcp
firewalldsudo firewall-cmd --query-service=ssh || sudo firewall-cmd --permanent --add-service=ssh
iptablessudo iptables -C INPUT -p tcp --dport 22 -j ACCEPT 2>/dev/null || sudo iptables -A ...
nftablesAtomic ruleset: nft -c -f /etc/nftables.conf.new && nft -f /etc/nftables.conf.new

Full step-by-step commands per backend: references/backend-ufw.md, references/backend-firewalld.md, references/backend-nftables.md, references/backend-iptables.md.

Docker Hosts

Docker bypasses ufw by default. Use DOCKER-USER chain. Full guide: references/docker-hardening.md.

Kubernetes Nodes

Default: AUDIT-ONLY. Never modify host firewall. Full policy: references/k8s-policy.md.


State: VERIFY

Run post-hardening checks:

bash scripts/firewall-verify.sh

Success criteria (all must pass):

  1. SSH remains reachable from current and second session
  2. Only intended ports are externally reachable
  3. Rules survive reboot (verified via service persistence)
  4. IPv6 exposure matches IPv4 policy
  5. Docker-published ports are intentional (no accidental 0.0.0.0)
  6. fail2ban jails active (if installed) with correct backend
  7. Rollback timer cancelled after successful verification

Verify behavior contract:

  • Verify MUST complete within the rollback timer window (default 5 min)
  • If verify times out before completion → timer auto-fires rollback (system-level protection)
  • If verify FAILS but timer was already cancelled → manual rollback: bash scripts/manual-rollback.sh <backup_dir>
  • The rollback is triggered by the timer (systemd-run/at), NOT by verify.sh itself — verify.sh exits with code 60 to signal failure, and the calling agent/scheduler handles the rollback decision

Exit Codes (Core Contract)

CodeMeaningAgent Action
0SuccessContinue
10Backend conflictHalt; resolve manually
11Backend detection failedHalt; check firewall stack
12Multiple backends activeHalt; resolve conflict
20IaC-managedHalt; update IaC source
21Inside containerHalt; escalate to host operator
22K8s node detectedHalt; audit-only mode
30Low confidence (<70%)Drop to audit-only mode
31No rollback capabilityHalt; ensure at or systemd-run
40Preflight failedHalt; check prerequisites
41Plan approval mismatchHalt; re-run PLAN with approval
42Backup failedHalt; resolve disk/permissions
50Apply failedAuto-rollback triggered
51Apply partialAuto-rollback triggered; verify backup
60Verify failedAuto-rollback triggered
61State file conflictAbort; resolve stale state

fail2ban Integration

If fail2ban is installed:

Host FirewallRecommended backend
ufwufw or systemd
firewalldfirewalld
nftablesnftables
iptablesauto (default)

After changing backend: sudo fail2ban-client restart && sudo fail2ban-client status sshd.


Recovery

If you lose connectivity, priority order:

  1. Wait for auto-rollback (scheduled during VALIDATE)
  2. Use second SSH session
  3. Cloud serial console / hypervisor console
  4. Restore from backup
  5. Emergency ACCEPT (last resort — exposes host completely)

Full procedures: references/recovery.md.

State Persistence & Interrupt-Resume

For agent interrupt-resume scenarios (e.g., Apply failed mid-run, agent restarted), the state machine writes a lightweight state file to enable recovery without starting from Detect:

STATE_DIR="$HOME/.firewall-hardening"
STATE_FILE="$STATE_DIR/state.json"

State file structure:

{
  "state": "validate",
  "started_at": "2026-05-11T16:00:00Z",
  "backend": "ufw",
  "risk_tier": "auto",
  "backup_dir": "/home/user/firewall-backup-20260511-160000",
  "rollback_timer_id": "firewall-rollback-12345",
  "plan_hash": "sha256:abc123..."
}

Resume logic:

  • If state.json exists and started_at is within 1 hour → resume from that state
  • If state.json is stale (>1 hour) → delete it and start fresh from Detect
  • The file is advisory-only; agent can always restart from Detect

State persistence is optional. The skill defaults to restarting from Detect each run. Enable by creating $STATE_DIR before starting.


Cloud Security Group Reminder

The host firewall is your second layer. Verify cloud SGs are aligned:

CloudOuter Firewall
AWSSecurity Groups
GCPVPC Firewall Rules
AzureNetwork Security Groups
DigitalOcean/Linode/VultrCloud Firewall

Compatibility Matrix

Distro/EnvufwfirewalldnftablesiptablesCoverage
Ubuntu 22.04/24.04PrimaryBackendFallbackFull
Debian 12PrimaryBackendFallbackFull
RHEL 9PrimaryNativeBackendFull
Rocky/Alma 9PrimaryNativeBackendFull
Fedora 40+PrimaryNativeBackendPartial
Alpine 3.18+NativeFallbackPartial
ArchNativeFallbackCommunity
Docker host✅ DOCKER-USER chaindocker-hardening.mddocker-hardening.mddocker-hardening.mdFull
LXC/LXD container⚠️ Limited⚠️ Limited⚠️ Limited⚠️ LimitedPartial
systemd-nspawn⚠️ Limited⚠️ Limited⚠️ Limited⚠️ LimitedPartial
WSL2❌ Not supported❌ Not supported❌ Not supported❌ Not supportedNone

Container environments: Docker host is fully supported via DOCKER-USER chain. LXC/LXD/systemd-nspawn have limited support (kernel shares netfilter with host). WSL2 is explicitly unsupported. See references/special-environments.md.


Observability

Establish baselines after hardening: conntrack usage, dropped packet rates, fail2ban ban rate. Monitor for anomalies. Full guide: references/observability.md.

Compliance

Practices map to CIS, PCI-DSS, and SOC2 controls. Full mapping: references/compliance.md.

Quick Reference

TaskCommand
Audit environmentbash scripts/audit-firewall.sh --json
Plan changesbash scripts/firewall-plan.sh --profile web
Verify after applybash scripts/firewall-verify.sh
Allow port (ufw, idempotent)sudo ufw status | awk '{print $1}' | grep -qx "80/tcp" || sudo ufw allow 80/tcp
View ufw rulessudo ufw status numbered
View nft rulessudo nft list ruleset
View iptables rulessudo iptables -L -n -v
View ip6tables rulessudo ip6tables -L -n -v
Atomic iptables replacesudo iptables-restore < /tmp/rules.v4
Dry-run nftablessudo nft -c -f /etc/nftables.conf
Backup rulessudo iptables-save > ~/iptables.backup
fail2ban statussudo fail2ban-client status sshd
Cancel rollback (at)atrm <jobid>
Cancel rollback (systemd-run)systemctl --user stop firewall-rollback-<pid>

See Also

  • references/backend-ufw.md — Full UFW phase
  • references/backend-firewalld.md — Full firewalld phase
  • references/backend-nftables.md — Full nftables phase
  • references/backend-iptables.md — Full iptables phase
  • references/docker-hardening.md — Docker firewall hardening
  • references/k8s-policy.md — Kubernetes node policy
  • references/security-profiles.md — Pre-built configurations
  • references/declarative-policy.md — YAML policy schema
  • references/observability.md — Monitoring and baselines
  • references/compliance.md — CIS/PCI-DSS/SOC2 mapping
  • references/recovery.md — Recovery procedures
  • references/special-environments.md — WSL2, containers, exit codes
  • scripts/audit-firewall.sh — Environment detection
  • scripts/firewall-plan.sh — Dry-run diff
  • scripts/firewall-verify.sh — Post-apply verification