{"id":"374a8634-af52-47f0-b733-55165fc58b92","entityType":"agent","slug":"clawhub-discovery219-linux-firewall-hardening","name":"linux-firewall-hardening","canonicalUrl":"https://www.xpersona.co/agent/clawhub-discovery219-linux-firewall-hardening","canonicalPath":"/agent/clawhub-discovery219-linux-firewall-hardening","generatedAt":"2026-10-10T21:48:01.355Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":null},"description":"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. Skill: linux-firewall-hardening Owner: discovery219 Summary: 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. Tags: devsecops:2.1.0, docker:2.1.0, fail2ban:2.1.0, firewall:2.1.0, firewalld:2.","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.3K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s173agn7542hhrs96f5sqxpwwx86k4fs:linux-firewall-hardening","sourceUrl":"https://clawhub.ai/discovery219/linux-firewall-hardening","homepage":"https://clawhub.ai/discovery219/skills/linux-firewall-hardening","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/discovery219/linux-firewall-hardening","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/discovery219/skills/linux-firewall-hardening","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Safe Linux firewall hardening with backend detection, idempotent atomic rules, rollback protection, and AI-executable state-machine workflows. Covers ufw, firew"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":null},"stars":null,"forks":null,"downloads":1315,"packageName":null,"latestVersion":"2.7.0","tractionLabel":"1.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T17:35:08.257Z","lastCrawledAt":"2026-10-10T17:35:08.257Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T17:35:08.257Z","lastVerifiedAt":null,"highlights":[{"version":"2.7.0","createdAt":"2026-08-07T00:28:44.752Z","changelog":"linux-firewall-hardening 2.7.0 - Added container and network audit tools: `scripts/container-port-audit.sh` (detects Docker DNAT/port-forwarding) and `scripts/ip-consistency.sh` (checks IPv4/IPv6 drift). - Expanded emergency lockout procedure: clarified that the \"emergency ACCEPT\" command may not be auto-executed and must only be performed by a human via serial console. - Introduced explicit CONFIRM state in the state machine, making human confirmation mandatory before any firewall changes. - Added full documentation for `firewall-apply.sh` in `references/firewall-apply.md`. - Removed files related to publishing and the skill card (PUBLISH.md, skill-card.md) to streamline the distribution. - Updated documentation and references to reflect all new scripts and required confirmation process.","fileCount":24,"zipByteSize":53308},{"version":"2.5.0","createdAt":"2026-05-31T09:35:02.375Z","changelog":"v2.5: Exit code contract hardened — PASS 16/18 (Claude Opus 4.7). Fixed 8 items: audit comment block, apply token cache, exit code semantics, audit exit 31, verify set -e footgun, systemd scope, RESERVED codes.","fileCount":22,"zipByteSize":53936},{"version":"2.2.0","createdAt":"2026-05-27T09:21:45.313Z","changelog":"v2.2.0 — SkillOpt Double-Epoch Optimization (Claude Opus 4.7 optimizer, 10 patches total) Epoch 1 (7 patches): Synced diverged exit code tables between SKILL.md and special-environments.md (P0). Replaced dead cross-references to non-existent scripts and previous versions with inline rollback scheduling code (P0). Added SSH port detection to iptables/nftables/firewalld backends to prevent hardcoded-port-22 lockouts (P1). Expanded second-SSH-session explanation in preflight checklist (P1). Fixed contradictory state persistence implementation status (P1). Added emergency lockout quick-nav header for panic-mode recovery (P2). Epoch 2 (3 patches): Upgraded all UFW security profiles from grep -q (substring-match, false-positives) to awk+grep -qx exact match, consistent with main SKILL.md APPLY section (P1). Fixed firewall-verify.sh exit code 1→60 to match core exit-code contract (P1). Fixed nftables dead rate-limiting code and firewalld hardcoded SSH port in security profiles (P2). All 10 patches applied across 2 epochs with cosine-decay learning rate (0.7→0.3). 20/20 functional score maintained throughout. Full changelog in PUBLISH.md.","fileCount":21,"zipByteSize":48663},{"version":"2.1.0","createdAt":"2026-05-13T08:13:59.024Z","changelog":"Phase 6: state persistence (interrupt-resume), Plan JSON schema + approval_token gate, exit code table in SKILL.md, Verify behavior contract, Compatibility Matrix with Docker/LXC/WSL2, tags restored (policy-as-code/devsecops/ipv6), firewalld example session","fileCount":20,"zipByteSize":43710},{"version":"1.0.0","createdAt":"2026-05-12T02:41:46.262Z","changelog":"Phase 5: risk-tier gating, PLAN state, split SKILL.md to 305 lines, 12 reference files, standardized exit codes","fileCount":21,"zipByteSize":41576}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s173agn7542hhrs96f5sqxpwwx86k4fs:linux-firewall-hardening","setupComplexity":"medium","setupSteps":["Setup complexity is MEDIUM. Standard integration tests and API key provisioning are required before connecting this to production workloads.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T21:48:01.351Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-discovery219-linux-firewall-hardening/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":null},"readme":"Skill: linux-firewall-hardening\n\nOwner: discovery219\n\nSummary: 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.\n\nTags: devsecops:2.1.0, docker:2.1.0, fail2ban:2.1.0, firewall:2.1.0, firewalld:2.1.0, hardening:2.1.0, iptables:2.1.0, ipv6:2.1.0, latest:2.7.0, nftables:2.1.0, policy-as-code:2.1.0, security:2.1.0, ufw:2.1.0\n\nVersion history:\n\nv2.7.0 | 2026-08-07T00:28:44.752Z | user\n\nlinux-firewall-hardening 2.7.0\n\n- Added container and network audit tools: `scripts/container-port-audit.sh` (detects Docker DNAT/port-forwarding) and `scripts/ip-consistency.sh` (checks IPv4/IPv6 drift).\n- Expanded emergency lockout procedure: clarified that the \"emergency ACCEPT\" command may not be auto-executed and must only be performed by a human via serial console.\n- Introduced explicit CONFIRM state in the state machine, making human confirmation mandatory before any firewall changes.\n- Added full documentation for `firewall-apply.sh` in `references/firewall-apply.md`.\n- Removed files related to publishing and the skill card (PUBLISH.md, skill-card.md) to streamline the distribution.\n- Updated documentation and references to reflect all new scripts and required confirmation process.\n\nv2.5.0 | 2026-05-31T09:35:02.375Z | user\n\nv2.5: Exit code contract hardened — PASS 16/18 (Claude Opus 4.7). Fixed 8 items: audit comment block, apply token cache, exit code semantics, audit exit 31, verify set -e footgun, systemd scope, RESERVED codes.\n\nv2.2.0 | 2026-05-27T09:21:45.313Z | user\n\nv2.2.0 — SkillOpt Double-Epoch Optimization (Claude Opus 4.7 optimizer, 10 patches total)\n\nEpoch 1 (7 patches): Synced diverged exit code tables between SKILL.md and special-environments.md (P0). Replaced dead cross-references to non-existent scripts and previous versions with inline rollback scheduling code (P0). Added SSH port detection to iptables/nftables/firewalld backends to prevent hardcoded-port-22 lockouts (P1). Expanded second-SSH-session explanation in preflight checklist (P1). Fixed contradictory state persistence implementation status (P1). Added emergency lockout quick-nav header for panic-mode recovery (P2).\n\nEpoch 2 (3 patches): Upgraded all UFW security profiles from grep -q (substring-match, false-positives) to awk+grep -qx exact match, consistent with main SKILL.md APPLY section (P1). Fixed firewall-verify.sh exit code 1→60 to match core exit-code contract (P1). Fixed nftables dead rate-limiting code and firewalld hardcoded SSH port in security profiles (P2).\n\nAll 10 patches applied across 2 epochs with cosine-decay learning rate (0.7→0.3). 20/20 functional score maintained throughout. Full changelog in PUBLISH.md.\n\nv2.1.0 | 2026-05-13T08:13:59.024Z | user\n\nPhase 6: state persistence (interrupt-resume), Plan JSON schema + approval_token gate, exit code table in SKILL.md, Verify behavior contract, Compatibility Matrix with Docker/LXC/WSL2, tags restored (policy-as-code/devsecops/ipv6), firewalld example session\n\nv1.0.0 | 2026-05-12T02:41:46.262Z | user\n\nPhase 5: risk-tier gating, PLAN state, split SKILL.md to 305 lines, 12 reference files, standardized exit codes\n\nArchive index:\n\nArchive v2.7.0: 24 files, 53308 bytes\n\nFiles: references/backend-firewalld.md (2505b), references/backend-iptables.md (2294b), references/backend-nftables.md (2614b), references/backend-ufw.md (2130b), references/compliance.md (1281b), references/declarative-policy.md (15127b), references/docker-hardening.md (2578b), references/firewall-apply.md (3473b), references/k8s-policy.md (2181b), references/observability.md (2299b), references/policy-schema.json (4066b), references/recovery.md (2031b), references/remaining-improvements.md (7873b), references/security-profiles.md (11431b), references/special-environments.md (6529b), scripts/audit-firewall.sh (12604b), scripts/container-port-audit.sh (3612b), scripts/firewall-apply.sh (6793b), scripts/firewall-plan.sh (6922b), scripts/firewall-verify.sh (4386b), scripts/ip-consistency.sh (3052b), skill-card.md (3039b), SKILL.md (24836b), _meta.json (143b)\n\nFile v2.7.0:SKILL.md\n\n---\nname: linux-firewall-hardening\ntitle: Linux Firewall Hardening\ndescription: 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.\nlicense: Dual MIT / Apache-2.0\nskill_version: 2.7.0\nschema_version: 2\ntags: [security, firewall, ufw, iptables, nftables, firewalld, hardening, docker, fail2ban, policy-as-code, devsecops, ipv6]\n---\n\n# Linux Firewall Hardening\n\n## When to Use\n\n- Check if a Linux server has active firewall protection.\n- Enable and configure a firewall without locking yourself out of SSH.\n- Audit existing rules, troubleshoot connectivity, or apply a security profile.\n- Automate firewall hardening via an AI agent or CI/CD pipeline.\n\n## When NOT to Use\n\n| Condition | Alternative |\n|-----------|-------------|\n| Kubernetes worker node | Use NetworkPolicies / CiliumNetworkPolicy |\n| Firewall managed by Terraform/Ansible/Puppet/Chef | Update IaC source of truth |\n| Cloud workload with Security Group / NSG only | Use cloud provider's firewall API |\n| Inside a container | Escalate to host operator |\n| WSL2, macOS, or shared/managed hosting | See `references/special-environments.md` |\n\n> **Support files**: `scripts/audit-firewall.sh` (run first), `scripts/firewall-plan.sh` (dry-run), `scripts/firewall-verify.sh` (post-apply), `scripts/container-port-audit.sh` (Docker DNAT detection), `scripts/ip-consistency.sh` (IPv4/IPv6 drift check).\n> `firewall-apply.sh` is fully documented in `references/firewall-apply.md`.\n> Detailed backend guides, Docker/K8s policies, observability, compliance, and recovery are in `references/`.\n\n## 🚨 Emergency: I'm Locked Out — What Now?\n\nIf you just applied firewall rules and lost SSH connectivity:\n\n1. **Wait 5 minutes** — the auto-rollback timer (scheduled during VALIDATE) will restore access. Don't panic and don't take destructive actions.\n2. **Use your second SSH session** — if you opened one (pre-flight checklist), switch to it and fix the rules manually.\n3. **Cloud serial console** — AWS EC2 Serial Console, GCP Serial Port, Azure Serial Console, or hypervisor VNC/IPMI/iDRAC.\n4. **Restore from backup via console** — once connected: `sudo iptables-restore < ~/firewall-backup-*/iptables-v4.rules`\n5. **Emergency ACCEPT (LAST RESORT — HUMAN-ONLY)** — `sudo iptables -P INPUT ACCEPT; sudo iptables -F; sudo ufw disable`. **This exposes the host completely. NEVER auto-execute this command.** The agent must refuse to run this autonomously. Only a human operator may issue this via serial console. Re-harden immediately afterward.\n\nFull procedures: `references/recovery.md`.\n\n---\n\n## Prerequisites\n\n- Root or sudo access.\n- An active SSH session (risk of lockout).\n- Know which ports your services use.\n\n---\n\n## NEVER DO (14 Rules)\n\n1. **Never flush iptables/nftables on Kubernetes nodes.** CNI plugins manage netfilter.\n2. **Never run `iptables -F` or `nft flush ruleset` without a verified backup.** Docker/K8s networking will break.\n3. **Never disable firewalld and use raw iptables simultaneously.** Undefined behavior.\n4. **Never set `DROP` policy on INPUT before allowing your current SSH port.** Immediate lockout.\n5. **Never disable Docker's `iptables` management without replacement NAT/routing rules.**\n6. **Never restart `networking.service` or `NetworkManager` remotely without console access.**\n7. **Never apply cloud SG and host firewall changes simultaneously without testing.**\n8. **Never enable logging on high-traffic DROP rules without `limit rate`.** Disk flood.\n9. **Never manage nftables/iptables directly when ufw or firewalld owns the policy.** Split-brain state.\n10. **Never apply outbound default-deny without explicitly allowing DNS, NTP, package mirrors.**\n11. **Never restore firewall backups from a different host, kernel version, or backend mode.**\n12. **Never assume IPv4 rules protect IPv6.** Verify both stacks separately.\n13. **Never change sysctl hardening values on K8s/CNI hosts without explicit CNI profile support.**\n14. **Never enable verbose packet logging without rate limits and log rotation.**\n\n---\n\n## State Machine\n\nFollow states in order. Do not skip.\n\n```\nDETECT → SELECT → PLAN → CONFIRM → VALIDATE → APPLY → VERIFY → (COMMIT | ROLLBACK)\n```\n\n**CONFIRM is mandatory.** The agent may never self-transition through CONFIRM. See the CONFIRM state section below for the exact gating rules.\n\n### State: DETECT\n\nRun the audit script:\n\n```bash\nbash scripts/audit-firewall.sh           # Human-readable\nbash scripts/audit-firewall.sh --json    # Machine-readable\n```\n\n**Key outputs**: `confidence`, `risk_tier`, `recommended_backend`, `halt_reasons`, `k8s_node`, `iac_owner`.\n\n### Risk Tiers & Confidence Gating\n\n| Tier | Confidence | Agent Behavior |\n|------|-----------|----------------|\n| `auto` | ≥ 90% | Proceed automatically to PLAN |\n| `confirmed` | 70–89% | Proceed but require human confirmation before APPLY |\n| `manual` | 50–69% | Audit-only mode. Generate recommendations, do not apply. |\n| `halt` | < 50% | Stop immediately. Escalate findings to operator. |\n\n**Additional halt triggers** (regardless of confidence): containerized, K8s node, IaC managed, no rollback mechanism available.\n\n### Decision Tree\n\n| Condition | Path | Detail |\n|-----------|------|--------|\n| Risk tier = `halt` | **STOP** | Resolve blockers first |\n| Inside container | **STOP** | Escalate to host operator |\n| K8s node detected | **STOP** | `references/k8s-policy.md` |\n| Ubuntu/Debian + ufw active | **Phase: UFW** | `references/backend-ufw.md` |\n| ufw + firewalld both active | **STOP** | Resolve conflict |\n| RHEL/Rocky/Alma + firewalld active | **Phase: firewalld** | `references/backend-firewalld.md` |\n| nftables active, no frontend | **Phase: nftables** | `references/backend-nftables.md` |\n| iptables only | **Phase: iptables** | `references/backend-iptables.md` |\n| Docker host | Apply **Docker Hardening** after phase above | `references/docker-hardening.md` |\n\n### Ownership Boundary\n\nBefore 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`.\n\n---\n\n### State: SELECT\n\nOptionally load a pre-built security profile (`references/security-profiles.md`):\n\n| Profile | Use Case |\n|---------|----------|\n| `public-web-server` | Open 22, 80, 443. Rate-limit SSH. |\n| `internal-database` | SSH from mgmt subnet only. DB port from app subnet only. |\n| `bastion-host` | SSH only. Aggressive rate limiting. |\n| `zero-trust-node` | Default deny all inbound and outbound. |\n\nOr use declarative YAML (`references/declarative-policy.md`):\n\n```\nImperative (state machine) → Ad-hoc hardening, incident response\nDeclarative (YAML)        → GitOps, multi-host, reproducible\nMixed                     → YAML as source-of-truth, state machine for verification\n```\n\n---\n\n### State: PLAN\n\nGenerate a dry-run diff before applying:\n\n```bash\nbash scripts/firewall-plan.sh --profile public-web-server\nbash scripts/firewall-plan.sh --ports 22,80,443\nbash scripts/firewall-plan.sh --json     # Machine-readable diff with approval_token\nbash scripts/firewall-plan.sh --refresh-audit --json  # Force re-audit + plan\n```\n\nReview the output. If `risk_tier` is `confirmed`, present the plan and wait for human confirmation before APPLY.\n\n**Plan JSON schema** (matches `firewall-plan.sh --json` output):\n\n```json\n{\n  \"backend\": \"ufw\",\n  \"active_frontend\": \"ufw\",\n  \"profile\": \"public-web-server\",\n  \"target_ports\": [22, 80, 443],\n  \"diff\": {\n    \"add\":    [{\"port\": 80, \"proto\": \"tcp\", \"source\": \"any\"}],\n    \"skip\":   [{\"port\": 22, \"proto\": \"tcp\", \"reason\": \"already_exists\"}],\n    \"remove\": []\n  },\n  \"risk_assessment\": \"low\",\n  \"estimated_disruption\": \"none\",\n  \"approval_token\": \"sha256:abc123...\",\n  \"audit_cached\": false,\n  \"audit_cache_file\": \"/tmp/firewall-audit.json\"\n}\n```\n\n**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.\n\n**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.\n\n---\n\n### State: CONFIRM (Mandatory — No Exceptions)\n\nThis gate prevents the agent from applying firewall rules without explicit human approval.\n\n**Why this exists:** The agent could (a) invoke `firewall-apply.sh` without the human seeing the plan, or (b) the human \"approves\" a stale plan that changed between approval and execution. CONFIRM defeats both — including the agent hallucinating an approval.\n\n**Rules (HARD — do not violate):**\n\n1. After PLAN, present the full ruleset diff AND the approval token printed by `firewall-plan.sh --json` (field: `approval_token`).\n2. **STOP.** Do not proceed in the same response. Do not type the token on the user's behalf.\n3. You may ONLY invoke APPLY if the user's most recent message contains that exact approval token. Never infer approval from phrases like \"looks good\" or \"go ahead\".\n4. APPLY always runs with auto-rollback armed. Committing (disarming rollback) requires a **second** explicit user instruction after VERIFY passes.\n5. A plan token mismatch at APPLY time produces exit code 41 — this is by design. Never bypass it.\n\n**Enforcement mechanism:** The token is a hash of the plan contents. If the plan changes (e.g., network state drifted between PLAN and APPLY), the token no longer matches → apply is refused. This converts a fuzzy judgment (\"did the user approve?\") into a cryptographic check the agent cannot rationalize around.\n\nConcrete flow:\n\n```text\nAgent: PLAN output → shows rule diff + PLAN-TOKEN: a1b2c3d4e5f6\nAgent: STOPS. Waits.\nUser:  \"Approved. a1b2c3d4e5f6\"\nAgent: Runs APPLY with --approved-plan=a1b2c3d4e5f6\nAgent: After APPLY → runs VERIFY\nAgent: STOPS. Shows VERIFY results.\nUser:  \"VERIFY passed. Commit.\"\nAgent: Runs COMMIT (disarms rollback timer)\n```\n\n---\n\n### State: VALIDATE\n\n#### 1. Create Backup (Mandatory)\n\n```bash\nBACKUP_DIR=\"$HOME/firewall-backup-$(date +%Y%m%d-%H%M%S)\"\nmkdir -p \"$BACKUP_DIR\"\n\nsudo iptables-save > \"$BACKUP_DIR/iptables-v4.rules\" 2>/dev/null || true\nsudo ip6tables-save > \"$BACKUP_DIR/iptables-v6.rules\" 2>/dev/null || true\nsudo nft list ruleset > \"$BACKUP_DIR/nftables.rules\" 2>/dev/null || true\nsudo ufw status verbose > \"$BACKUP_DIR/ufw-status.txt\" 2>/dev/null || true\nsudo firewall-cmd --list-all --zone=$(sudo firewall-cmd --get-default-zone) > \"$BACKUP_DIR/firewalld-default.txt\" 2>/dev/null || true\n\necho \"Backup saved to $BACKUP_DIR\"\n```\n\n#### 2. Schedule Rollback (Mandatory for Remote)\n\nThe 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.\n\n```bash\n# Build rollback script from backup dir\nROLLBACK_SCRIPT=$(cat <<'RB'\n#!/bin/bash\nBACKUP_DIR=\"REPLACE_ME\"\n[ -f \"$BACKUP_DIR/iptables-v4.rules\" ] && sudo iptables-restore < \"$BACKUP_DIR/iptables-v4.rules\" || { sudo iptables -P INPUT ACCEPT; sudo iptables -F; }\n[ -f \"$BACKUP_DIR/iptables-v6.rules\" ] && sudo ip6tables-restore < \"$BACKUP_DIR/iptables-v6.rules\" || { sudo ip6tables -P INPUT ACCEPT; sudo ip6tables -F; }\n[ -f \"$BACKUP_DIR/nftables.rules\" ] && sudo nft -f \"$BACKUP_DIR/nftables.rules\" || sudo nft flush ruleset\nsystemctl is-active ufw &>/dev/null && sudo ufw disable\nsudo firewall-cmd --panic-off 2>/dev/null\nRB\n)\nROLLBACK_SCRIPT=\"${ROLLBACK_SCRIPT/REPLACE_ME/$BACKUP_DIR}\"\n\n# Schedule (at preferred, systemd-run fallback)\nif command -v at &>/dev/null; then\n    ROLLBACK_JOB_ID=$(echo \"sudo bash -c '$ROLLBACK_SCRIPT'\" | at now + 5 minutes 2>&1 | grep -oP 'job \\K\\d+')\n    echo \"Rollback scheduled: at job $ROLLBACK_JOB_ID (cancel with: atrm $ROLLBACK_JOB_ID)\"\nelif command -v systemd-run &>/dev/null; then\n    UNIT_NAME=\"firewall-rollback-$$\"\n    echo \"$ROLLBACK_SCRIPT\" > /tmp/firewall-rollback-$$.sh\n    chmod +x /tmp/firewall-rollback-$$.sh\n    systemd-run --on-active=5m --unit=\"$UNIT_NAME\" --user /tmp/firewall-rollback-$$.sh\n    echo \"Rollback scheduled: systemd unit $UNIT_NAME (cancel with: systemctl --user stop $UNIT_NAME)\"\nfi\n```\n\nSee `references/recovery.md` for advanced recovery scenarios.\n\n#### 2a. Verify Rollback Timer Is Armed (Mandatory, Before Apply)\n\nAfter scheduling the rollback but BEFORE applying new rules, confirm the timer is actually running:\n\n```bash\n# For systemd-run\nsystemctl is-active --quiet firewall-rollback-$$.timer 2>/dev/null \\\n  && systemctl list-timers firewall-rollback-$$.timer --no-pager 2>/dev/null | grep -q firewall-rollback \\\n  || { echo \"FATAL: rollback timer not scheduled — aborting before applying rules\" >&2; exit 1; }\n\n# For at\natq | grep -q \"$ROLLBACK_JOB_ID\" \\\n  || { echo \"FATAL: rollback job $ROLLBACK_JOB_ID not found in queue — aborting\" >&2; exit 1; }\n```\n\n**Ordering constraint:** This check runs AFTER scheduling but BEFORE apply. If the timer cannot be confirmed as armed, the script exits with code 31 and **no rule change ever happens.**\n\n#### 3. Pre-Flight Checklist\n\n- [ ] Backup created successfully\n- [ ] Rollback scheduled (verify with `atq` or `systemctl --user list-units`)\n- [ ] **Second SSH session open and tested** — open a second terminal, SSH in, and confirm you can run `sudo whoami`. This is your emergency console if the primary session loses connectivity. Keep it open until VERIFY passes. **Why**: existing ESTABLISHED conntrack entries usually keep your current session alive, but if conntrack is flushed or the policy change drops your session silently, this second session is your only way back in.\n- [ ] Real SSH port identified (not assumed to be 22)\n- [ ] Confidence ≥ 70% and risk_tier is `auto` or `confirmed`\n- [ ] Ownership verified — no IaC managing firewall\n- [ ] Change window appropriate (maintenance window or low traffic)\n- [ ] PLAN output reviewed and approved\n\n---\n\n### State: APPLY\n\nApply firewall rules using the approved plan from PLAN state.\n\n```bash\n# Get the approval_token from firewall-plan.sh --json output\nbash scripts/firewall-apply.sh --approved-plan=sha256:abc123...\nbash scripts/firewall-apply.sh --approved-plan=sha256:abc123... --dry-run\n```\n\n**Apply behavior:**\n- Verifies `approval_token` matches current plan (exit 41 on mismatch — plan changed since approval)\n- Checks for active rollback timer (exit 40 if VALIDATE was skipped)\n- Supports all backends: ufw, firewalld, nftables, iptables\n- Automatically runs `firewall-verify.sh` after applying\n- Passes `--dry-run` for preview-only mode\n\n**Idempotent inline commands** (for manual/scriptless use):\n\n| Backend | Pattern |\n|---------|---------|\n| ufw | `sudo ufw status \\| awk '{print $1}' \\| grep -qx \"22/tcp\" \\|\\| sudo ufw allow 22/tcp` |\n| firewalld | `sudo firewall-cmd --query-service=ssh \\|\\| sudo firewall-cmd --permanent --add-service=ssh` |\n| iptables | `sudo iptables -C INPUT -p tcp --dport 22 -j ACCEPT 2>/dev/null \\|\\| sudo iptables -A ...` |\n| nftables | Atomic ruleset: `nft -c -f /etc/nftables.conf.new && nft -f /etc/nftables.conf.new` |\n\n#### Docker Hosts\n\nDocker bypasses ufw by default. Use DOCKER-USER chain. Full guide: `references/docker-hardening.md`.\n\n#### Kubernetes Nodes\n\n**Default: AUDIT-ONLY**. Never modify host firewall. Full policy: `references/k8s-policy.md`.\n\n---\n\n### State: VERIFY\n\n**Phase 1 — Host-local verification** (`scripts/firewall-verify.sh`):\n\n```bash\nbash scripts/firewall-verify.sh\n```\n\n**Phase 2 — External reachability check** (`scripts/container-port-audit.sh`):\n\n> **Requirement:** All connectivity verification steps MUST be executed from a second, external host with an independent network path. Verifying from the target itself cannot detect a lockout.\n\nFrom a **second host** on the same network segment, run:\n\n```bash\nbash scripts/container-port-audit.sh <target-ip> <target-ip6>\n```\n\nThis uses `nc` + `/dev/tcp` (not nmap) to verify expected-open ports are reachable and expected-closed canary ports are filtered, for both IPv4 and IPv6. UDP probes use `nc -uzvw3`; note that a \"pass\" may be a false-positive due to UDP being connectionless (a silently-dropping firewall produces the same result as an open port). Use local `ss -ulnp` as the deterministic anchor for UDP state.\n\nIf a second host is unavailable, the VERIFY stage also runs the container port audit and IPv4/IPv6 consistency checks locally:\n\n```bash\nbash scripts/container-port-audit.sh   # local-only: container DNAT detection\nbash scripts/ip-consistency.sh          # compare iptables vs ip6tables\n```\n\n**Phase 3 — Container port DNAT audit:**\n\nDetects ports published by container runtimes that bypass the host's INPUT chain. Checks `ss` listeners + `iptables -t nat` DNAT rules + FORWARD chain entries. Flags any published port not reflected in explicit INPUT rules.\n\n**Success criteria** (all must pass):\n1. SSH remains reachable from current and second session\n2. Only intended ports are externally reachable (verified from second host if available; otherwise local audit)\n3. Rules survive reboot (verified via service persistence)\n4. IPv6 exposure matches IPv4 policy (verified by `ip-consistency.sh`)\n5. Docker-published ports are intentional (verified by `container-port-audit.sh`, no accidental `0.0.0.0`)\n6. fail2ban jails active (if installed) with correct backend\n7. Rollback timer cancelled after successful verification\n\n**Verify behavior contract:**\n- Verify MUST complete within the rollback timer window (default 5 min)\n- If verify times out before completion → timer auto-fires rollback (system-level protection)\n- If verify FAILS but timer was already cancelled → manual rollback from the backup directory. Restore commands (in priority order):\n  1. `sudo iptables-restore < \"$BACKUP_DIR/iptables-v4.rules\"`\n  2. `sudo ip6tables-restore < \"$BACKUP_DIR/iptables-v6.rules\"`\n  3. `sudo nft -f \"$BACKUP_DIR/nftables.rules\"`\n  4. `sudo ufw reset && sudo ufw disable`\n  See `references/recovery.md` for full recovery procedures including emergency ACCEPT fallback.\n- 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\n\n## Exit Codes (Core Contract)\n\n| Code | Meaning | Agent Action |\n|------|---------|-------------|\n| 0 | Success | Continue |\n| 10 | Backend conflict | Halt; resolve manually |\n| 11 | Backend detection failed | Halt; check firewall stack |\n| 12 | Multiple backends active | Halt; resolve conflict |\n| 20 | IaC-managed | Halt; update IaC source |\n| 21 | Inside container | Halt; escalate to host operator |\n| 22 | K8s node detected | Halt; audit-only mode |\n| 30 | Low confidence (<70%) | Drop to audit-only mode |\n| 31 | No rollback capability | Halt; ensure at or systemd-run |\n| 40 | Preflight failed | Halt; check prerequisites |\n| 41 | Plan approval mismatch | Halt; re-run PLAN with approval |\n| 42 | RESERVED (Backup failed) | Halt; resolve disk/permissions |\n| 50 | RESERVED (Apply failed) | Auto-rollback triggered |\n| 51 | Apply partial | Auto-rollback triggered; verify backup |\n| 60 | Verify failed | Auto-rollback triggered |\n| 61 | RESERVED (State file conflict) | Abort; resolve stale state |\n\n---\n\n## fail2ban Integration\n\nIf fail2ban is installed:\n\n| Host Firewall | Recommended `backend` |\n|--------------|----------------------|\n| ufw | `ufw` or `systemd` |\n| firewalld | `firewalld` |\n| nftables | `nftables` |\n| iptables | `auto` (default) |\n\nAfter changing backend: `sudo fail2ban-client restart && sudo fail2ban-client status sshd`.\n\n---\n\n## Recovery\n\nIf you lose connectivity, priority order:\n1. Wait for auto-rollback (scheduled during VALIDATE)\n2. Use second SSH session\n3. Cloud serial console / hypervisor console\n4. Restore from backup\n5. Emergency ACCEPT (last resort — exposes host completely)\n\nFull procedures: `references/recovery.md`.\n\n## State Persistence & Interrupt-Resume\n\nFor 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:\n\n```bash\nSTATE_DIR=\"$HOME/.firewall-hardening\"\nSTATE_FILE=\"$STATE_DIR/state.json\"\n```\n\n**State file structure:**\n\n```json\n{\n  \"state\": \"validate\",\n  \"started_at\": \"2026-05-11T16:00:00Z\",\n  \"backend\": \"ufw\",\n  \"risk_tier\": \"auto\",\n  \"backup_dir\": \"/home/user/firewall-backup-20260511-160000\",\n  \"rollback_timer_id\": \"firewall-rollback-12345\",\n  \"plan_hash\": \"sha256:abc123...\"\n}\n```\n\n**Resume logic:**\n- If `state.json` exists and `started_at` is within 1 hour → resume from that state\n- If `state.json` is stale (>1 hour) → delete it and start fresh from Detect\n- The file is advisory-only; agent can always restart from Detect\n\n> State persistence is optional. The skill defaults to restarting from Detect each run. Enable by creating `$STATE_DIR` before starting.\n\n---\n\n## Cloud Security Group Reminder\n\nThe host firewall is your **second** layer. Verify cloud SGs are aligned:\n\n| Cloud | Outer Firewall |\n|-------|---------------|\n| AWS | Security Groups |\n| GCP | VPC Firewall Rules |\n| Azure | Network Security Groups |\n| DigitalOcean/Linode/Vultr | Cloud Firewall |\n\n## Compatibility Matrix\n\n| Distro/Env | ufw | firewalld | nftables | iptables | Coverage |\n|------------|-----|-----------|----------|----------|----------|\n| Ubuntu 22.04/24.04 | Primary | — | Backend | Fallback | Full |\n| Debian 12 | Primary | — | Backend | Fallback | Full |\n| RHEL 9 | — | Primary | Native | Backend | Full |\n| Rocky/Alma 9 | — | Primary | Native | Backend | Full |\n| Fedora 40+ | — | Primary | Native | Backend | Partial |\n| Alpine 3.18+ | — | — | Native | Fallback | Partial |\n| Arch | — | — | Native | Fallback | Community |\n| Docker host | ✅ DOCKER-USER chain | ✅ `docker-hardening.md` | ✅ `docker-hardening.md` | ✅ `docker-hardening.md` | Full |\n| LXC/LXD container | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | Partial |\n| systemd-nspawn | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | Partial |\n| WSL2 | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | None |\n\n> 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`.\n\n---\n\n## Observability\n\nEstablish baselines after hardening: conntrack usage, dropped packet rates, fail2ban ban rate. Monitor for anomalies. Full guide: `references/observability.md`.\n\n## Compliance\n\nPractices map to CIS, PCI-DSS, and SOC2 controls. Full mapping: `references/compliance.md`.\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Audit environment | `bash scripts/audit-firewall.sh --json` |\n| Plan changes | `bash scripts/firewall-plan.sh --profile web` |\n| Verify after apply | `bash scripts/firewall-verify.sh` |\n| Allow port (ufw, idempotent) | `sudo ufw status \\| awk '{print $1}' \\| grep -qx \"80/tcp\" \\|\\| sudo ufw allow 80/tcp` |\n| View ufw rules | `sudo ufw status numbered` |\n| View nft rules | `sudo nft list ruleset` |\n| View iptables rules | `sudo iptables -L -n -v` |\n| View ip6tables rules | `sudo ip6tables -L -n -v` |\n| Atomic iptables replace | `sudo iptables-restore < /tmp/rules.v4` |\n| Dry-run nftables | `sudo nft -c -f /etc/nftables.conf` |\n| Backup rules | `sudo iptables-save > ~/iptables.backup` |\n| fail2ban status | `sudo fail2ban-client status sshd` |\n| Cancel rollback (at) | `atrm <jobid>` |\n| Cancel rollback (systemd-run) | `systemctl --user stop firewall-rollback-<pid>` |\n\n## See Also\n\n- `references/backend-ufw.md` — Full UFW phase\n- `references/backend-firewalld.md` — Full firewalld phase\n- `references/backend-nftables.md` — Full nftables phase\n- `references/backend-iptables.md` — Full iptables phase\n- `references/docker-hardening.md` — Docker firewall hardening\n- `references/k8s-policy.md` — Kubernetes node policy\n- `references/security-profiles.md` — Pre-built configurations\n- `references/declarative-policy.md` — YAML policy schema\n- `references/observability.md` — Monitoring and baselines\n- `references/compliance.md` — CIS/PCI-DSS/SOC2 mapping\n- `references/recovery.md` — Recovery procedures\n- `references/special-environments.md` — WSL2, containers, exit codes\n- `scripts/audit-firewall.sh` — Environment detection\n- `scripts/firewall-plan.sh` — Dry-run diff\n- `scripts/firewall-verify.sh` — Post-apply verification\n\nFile v2.7.0:_meta.json\n\n{\n  \"ownerId\": \"kn7e8vz4v0f8vr8dh2yr78fjkd86jgk3\",\n  \"slug\": \"linux-firewall-hardening\",\n  \"version\": \"2.7.0\",\n  \"publishedAt\": 1786062524752\n}\n\nFile v2.7.0:references/backend-firewalld.md\n\n# Backend: firewalld (RHEL / Rocky / Alma / Fedora)\n\nfirewalld is zone-aware. Always specify the zone. Default on most servers is `public`.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo firewall-cmd --state\n# \"running\" or \"not running\"\n```\n\n## Prerequisites\n\n- firewalld must be active but with known rules.\n- No other frontend (ufw) must be active.\n- Never modify iptables/nftables directly when firewalld owns the policy.\n\n## Apply: Idempotent Zone Rules\n\n### Step 1: Identify Active Zone\n\n```bash\nDEFAULT_ZONE=$(sudo firewall-cmd --get-default-zone)\necho \"Default zone: $DEFAULT_ZONE\"\nsudo firewall-cmd --get-active-zones\n```\n\n### Step 2: Add Rules\n\n```bash\nZONE=\"${DEFAULT_ZONE:-public}\"\nSSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk -F: '{print $NF}' | head -1)\nSSH_PORT=${SSH_PORT:-22}\n\n# SSH (service definition)\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=ssh >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=ssh\n\n# HTTP / HTTPS\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=http >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=http\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=https >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=https\n\n# Custom port\n# sudo firewall-cmd --zone=\"$ZONE\" --query-port=8080/tcp >/dev/null 2>&1 || #   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-port=8080/tcp\n\n# Rate-limit SSH (rich rule)\nsudo firewall-cmd --zone=\"$ZONE\" --query-rich-rule='rule service name=ssh limit value=3/m accept' >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule service name=ssh limit value=3/m accept'\n\n# Apply\nsudo firewall-cmd --reload\n```\n\n### Step 3: Verify\n\n```bash\nsudo firewall-cmd --list-all --zone=\"$ZONE\"\n```\n\n## Zone Commands Quick Reference\n\n```bash\n# List all zones\nsudo firewall-cmd --get-zones\n\n# List all zones with rules\nsudo firewall-cmd --list-all-zones\n\n# Change default zone\nsudo firewall-cmd --set-default-zone=drop\n\n# Move interface to different zone\nsudo firewall-cmd --zone=internal --change-interface=eth1\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built firewalld configurations\n- `references/declarative-policy.md` — YAML-to-firewalld rendering\n\nFile v2.7.0:references/backend-iptables.md\n\n# Backend: iptables (Legacy Fallback)\n\nUse atomic `iptables-restore` instead of `-F` followed by individual `-A` commands. Build a complete ruleset file, then swap it in one operation. Always manage IPv4 (`iptables`) and IPv6 (`ip6tables`) separately.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. If your SSH runs on a different port, replace `22` with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> echo \"Detected SSH port: $SSH_PORT\"\n> ```\n> Using the wrong SSH port in the ruleset below will lock you out.\n\n## Key Principle: Atomic Restore\n\n```bash\n# Validate syntax first\nsudo iptables-restore --test /tmp/iptables-v4.rules\nsudo ip6tables-restore --test /tmp/iptables-v6.rules\n\n# Apply atomically\nsudo iptables-restore /tmp/iptables-v4.rules\nsudo ip6tables-restore /tmp/iptables-v6.rules\n```\n\n## Apply: Atomic Rulesets\n\n### Step 1: Build IPv4 Ruleset (`/tmp/iptables-v4.rules`)\n\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n### Step 2: Build IPv6 Ruleset (`/tmp/iptables-v6.rules`)\n\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p icmpv6 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n### Step 3: Persist\n\n- **Debian/Ubuntu**: `sudo apt install iptables-persistent`, then `sudo netfilter-persistent save`\n- **RHEL/CentOS**: `sudo service iptables save` or migrate to `firewalld`\n\n## Idempotent Single-Rule Pattern\n\nIf adding a single rule instead of full restore:\n\n```bash\n# Check if rule exists before adding\nsudo iptables -C INPUT -p tcp --dport 8080 -j ACCEPT 2>/dev/null ||   sudo iptables -A INPUT -p tcp --dport 8080 -j ACCEPT\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built iptables configurations\n- `references/declarative-policy.md` — YAML-to-iptables rendering\n\nFile v2.7.0:references/backend-nftables.md\n\n# Backend: nftables (Modern Dual-Stack)\n\nnftables is the modern replacement for iptables. It supports IPv4 and IPv6 in a single `inet` table, has atomic ruleset replacement, and uses a cleaner syntax.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo nft list ruleset\n# Shows current rules if active\n```\n\n## Key Principle: Atomic Replacement\n\nBuild a new ruleset file, validate with `nft -c`, then apply in one shot. **Never `flush ruleset` manually** on a production host without a backup.\n\n## Apply: Atomic Ruleset\n\n### Step 1: Build Ruleset File\n\n```bash\nsudo tee /etc/nftables.conf.new << 'EOF'\n#!/usr/sbin/nft -f\n\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 80, 443 }\n    }\n\n    chain input {\n        type filter hook input priority 0; policy drop;\n\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n\n        tcp dport @allowed_tcp_ports accept\n\n        # Rate limit new SSH connections\n        tcp dport 22 ct state new limit rate 10/second burst 20 packets accept\n\n        # Log with rate limit to prevent syslog flood\n        log prefix \"nft-drop: \" limit rate 5/second\n        drop\n    }\n\n    chain forward {\n        type filter hook forward priority 0; policy drop;\n    }\n\n    chain output {\n        type filter hook output priority 0; policy accept;\n    }\n}\nEOF\n```\n\n> **Warning**: Excessive logging can overwhelm syslog/journald on high-traffic systems. Always use `limit rate` on log rules and monitor after enabling.\n\n### Step 2: Dry-Run (Validate Syntax)\n\n```bash\nsudo nft -c -f /etc/nftables.conf.new\n```\n\nIf this returns errors, fix the file and re-validate. **Do not proceed until dry-run passes.**\n\n### Step 3: Atomic Apply\n\n```bash\n# Backup current ruleset (belt-and-suspenders)\nsudo nft list ruleset > \"$BACKUP_DIR/nftables-pre-apply.rules\" 2>/dev/null || true\n\n# Atomic replace\nsudo nft -f /etc/nftables.conf.new\nsudo mv /etc/nftables.conf.new /etc/nftables.conf\n\n# Enable persistence\nsudo systemctl enable nftables\nsudo systemctl restart nftables\n```\n\n### Step 4: Verify\n\n```bash\nsudo nft list ruleset\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built nftables configurations\n- `references/declarative-policy.md` — YAML-to-nftables rendering\n\nFile v2.7.0:references/backend-ufw.md\n\n# Backend: UFW (Recommended for Ubuntu / Debian)\n\nUFW (Uncomplicated Firewall) is the easiest path for Ubuntu and Debian. It handles IPv4 and IPv6 together, uses simple commands, and is managed by a systemd service.\n\n## Detection\n\n```bash\nsudo ufw status\n# \"Status: active\" or \"Status: inactive\"\n```\n\n## Prerequisites\n\n- UFW must be installed but inactive (or with known rules).\n- No other frontend (firewalld) must be active.\n- Never modify iptables/nftables directly when ufw owns the policy.\n\n## Apply: Idempotent Rules\n\n### Step 1: Default Policies\n\n```bash\n# Idempotent — safe to repeat\nsudo ufw --dry-run default deny incoming\nsudo ufw --dry-run default allow outgoing\n\n# Apply\nsudo ufw default deny incoming\nsudo ufw default allow outgoing\n```\n\n### Step 2: SSH (Detect Real Port)\n\n```bash\nSSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk -F: '{print $NF}' | head -1)\nSSH_PORT=${SSH_PORT:-22}\n\n# Idempotent — check before add, exact port match\nsudo ufw status | awk '{print $1}' | grep -qx \"${SSH_PORT}/tcp\" || sudo ufw allow \"${SSH_PORT}/tcp\"\n```\n\n### Step 3: Service Rules\n\n```bash\n# HTTP / HTTPS (uses exact port match to avoid matching 8080 when checking 80)\nsudo ufw status | awk '{print $1}' | grep -qx \"80/tcp\"  || sudo ufw allow 80/tcp\nsudo ufw status | awk '{print $1}' | grep -qx \"443/tcp\" || sudo ufw allow 443/tcp\n\n# Custom ports (same pattern)\n# sudo ufw status | awk '{print $1}' | grep -qx \"8080/tcp\" || sudo ufw allow 8080/tcp\n```\n\n### Step 4: Enable\n\n```bash\n# --force prevents interactive prompt during automation\nsudo ufw --force enable\n```\n\n### Step 5: Verify\n\n```bash\nsudo ufw status verbose\nsudo ufw status numbered\n```\n\n## Outbound Policy\n\nUFW defaults to `allow outgoing`. For high-security environments:\n\n```bash\n# WARNING: restrict OUTPUT only after inventorying all outbound dependencies\nsudo ufw default deny outgoing\nsudo ufw allow out 53/udp      # DNS\nsudo ufw allow out 123/udp     # NTP\nsudo ufw allow out 80/tcp\nsudo ufw allow out 443/tcp\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built UFW configurations\n- `references/declarative-policy.md` — YAML-to-UFW rendering\n\nFile v2.7.0:references/compliance.md\n\n# Compliance Mapping\n\nThis skill's hardening practices map to common security standards. Use for audit evidence.\n\n| Control | CIS Benchmark | PCI-DSS | SOC2 | How This Skill Relates |\n|---------|--------------|---------|------|--------------------------|\n| Default deny inbound | CIS 3.5.1.x | Req 1.3 | CC6.1 | Automated detection and enforcement |\n| Restrict outbound | CIS 3.5.2.x | Req 1.3 | CC6.1 | Supports evidence collection and rule generation |\n| Disable unused ports | CIS 2.1.x | Req 2.2 | CC6.2 | Exposure analysis mapping |\n| Rate-limit SSH | CIS 5.2.x | Req 1.2 | CC6.1 | Idempotent rate-limit patterns |\n| Backup before change | CIS 3.4 | Req 12.10 | CC7.2 | Automated backup and rollback |\n| IPv6 symmetric policy | CIS 3.5.3.x | Req 1.3 | CC6.1 | IPv4/IPv6 symmetry validation |\n| Log dropped packets | CIS 3.5.1.x | Req 10.2 | CC7.2 | Rate-limited logging |\n| fail2ban / intrusion prev | CIS 5.2.x | Req 1.2 | CC6.1 | Backend alignment and verification |\n| Docker port exposure | CIS 4.1.x | Req 1.3 | CC6.1 | DOCKER-USER chain enforcement |\n| Kernel hardening | CIS 3.3.x | Req 2.2 | CC6.1 | Guard against CNI mutations |\n\n**Note**: Specific control numbers vary by benchmark version (CIS v2.x, v3.x). Always reference the latest version applicable to your OS.\n\nFile v2.7.0:references/declarative-policy.md\n\n# Declarative Firewall Policy\n\nThis document defines a machine-readable YAML schema for expressing firewall policy independent of the backend implementation. AI agents and CI/CD pipelines can use this schema to generate backend-specific configurations for ufw, firewalld, nftables, and iptables.\n\n## Design Principles\n\n1. **Backend-agnostic**: The policy describes intent, not implementation.\n2. **Idempotent by design**: Rendering the same policy twice produces the same ruleset.\n3. **Atomic application**: Rendered output uses atomic replace (`iptables-restore`, `nft -f`) where possible.\n4. **Validation-integrated**: The policy includes success criteria that the verifier uses.\n5. **Fail hard on unknown fields**: Unrecognized schema keys or unsupported feature/backend combinations produce an error, not silent ignorance. This prevents the dangerous assumption that a field was processed when it wasn't.\n\n## Schema Version\n\nCurrent version: **2.0**. Renderers must refuse to process policies with a higher major version.\n\n| Version | Changes |\n|---------|---------|\n| 1.0 | Initial schema (deprecated) |\n| 2.0 | Added `schema_version`, `metadata` block. Hard-fail on unknown fields. Added `backend_compat` field-level hints. |\n\n## JSON Schema\n\nA formal JSON Schema for validation is available at `references/policy-schema.json`. Renderers should validate policy input against this schema before rendering.\n\n## Schema\n\n```yaml\nschema_version: \"2.0\"          # Required. Must be exactly \"2.0\".\nmetadata:                       # Optional. Tagging and auditing.\n  name: \"web-server-policy\"\n  description: \"Public web server firewall policy\"\n  author: \"ops-team\"\n  last_modified: \"2026-05-11\"\n\nbackend: auto                   # Optional. auto | ufw | firewalld | nftables | iptables\n                                # \"auto\" detects and picks the best available backend.\n                                # Explicit backend restricts rendering to that target.\n\ninbound:\n  default: deny                 # Required. deny | accept\n  rules:\n    - proto: tcp                # tcp | udp | icmp | icmpv6 | any\n      port: 22                  # integer or \"any\"\n      action: accept            # accept | drop | reject\n      source: any               # any | CIDR | [CIDR, CIDR]\n      rate_limit: \"10/min\"      # Optional. e.g. \"10/min\", \"3/sec\"\n      comment: \"SSH access\"     # Optional. Maps to log prefix or rule comment\n      backend_compat:           # Optional. Override which backends support this rule.\n        ufw: true               # Default: true for all. Set false to skip.\n        firewalld: true\n        nftables: true\n        iptables: true\n\n    - proto: tcp\n      port: 443\n      action: accept\n      source: any\n\n    - proto: udp\n      port: 53\n      action: accept\n      source: \"10.0.0.0/8\"\n\noutbound:\n  default: accept               # Required. deny | accept\n  rules: []                     # Optional. Same structure as inbound\n\nforward:\n  default: deny                 # Required. deny | accept\n\nicmp:\n  ipv4: accept                  # accept | deny\n  ipv6: accept                  # accept | deny\n\nlogging:\n  dropped: true                 # Log dropped packets\n  rate_limit: \"5/sec\"           # Prevent log flood\n  prefix: \"fw-drop\"             # Log prefix string\n\nvalidation:\n  ssh_must_reachable: true\n  intended_ports_only: true\n  ipv6_symmetric: true\n  rules_persist_after_reboot: true\n```\n\n## Field Reference\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `schema_version` | string | Yes | Schema version. Current: `\"2.0\"` |\n| `metadata` | map | No | Human-readable tagging (name, description, author, last_modified) |\n| `backend` | enum | No | Target backend: `auto`, `ufw`, `firewalld`, `nftables`, `iptables`. Default: `auto` |\n| `inbound.default` | enum | Yes | `deny` or `accept` |\n| `inbound.rules` | list | No | Ordered list of allow/deny rules |\n| `outbound.default` | enum | Yes | `deny` or `accept` |\n| `outbound.rules` | list | No | Ordered list of allow/deny rules |\n| `forward.default` | enum | Yes | `deny` or `accept` |\n| `icmp.ipv4` | enum | Yes | `accept` or `deny` |\n| `icmp.ipv6` | enum | Yes | `accept` or `deny` |\n| `logging.dropped` | bool | No | Whether to log dropped packets |\n| `logging.rate_limit` | string | No | Rate limit for log entries |\n| `logging.prefix` | string | No | Prefix string for log lines |\n| `validation` | map | No | Success criteria flags |\n\n## Rule Object\n\n```yaml\n- proto: tcp              # Required: tcp | udp | icmp | icmpv6 | any\n  port: 80                # Required for tcp/udp: integer or \"any\"\n  action: accept          # Required: accept | drop | reject\n  source: any             # Optional: any (default) | CIDR | list of CIDRs\n  rate_limit: \"10/min\"    # Optional: rate string\n  comment: \"HTTP\"         # Optional: human description\n  backend_compat:         # Optional: per-backend enable/disable\n    ufw: true\n    firewalld: false      # Skip this rule on firewalld\n    nftables: true\n    iptables: true\n```\n\n## Backend Feature Differences\n\nNot all features are available on all backends. The renderer must hard-fail when a requested feature is unsupported for the target backend, rather than silently ignoring it.\n\n| Feature | ufw | firewalld | nftables | iptables |\n|---------|-----|-----------|----------|----------|\n| Rate limiting | Yes (ufw limit) | Yes (rich rule) | Yes (limit rate) | Yes (recent + hashlimit) |\n| Source CIDR restriction | Yes | Yes | Yes | Yes |\n| Source range (list of CIDRs) | Yes | Yes (multiple rich rules) | Yes (concatenated) | Yes (multiple rules) |\n| Logging dropped packets | Yes (built-in) | Yes (--set-log-denied) | Yes (log statement) | Yes (LOG target) |\n| Custom log prefix | No | No | Yes | Yes |\n| IPv6 (same ruleset as IPv4) | Yes | Yes (separate zone) | Yes (inet family) | No (separate ip6tables) |\n| Sets/maps for port groups | No | No | Yes | No (use multiport) |\n| Connection tracking state | No (implicit) | Yes (rich rule) | Yes (ct state) | Yes (conntrack) |\n| Atomic ruleset replacement | No | Yes (--reload) | Yes (nft -f) | Yes (iptables-restore) |\n| Zone-based rules | No | Yes | No | No |\n\n## Validation Rules\n\nRenderers MUST apply these validation rules before rendering:\n\n1. **Unknown top-level keys**: If the policy contains keys not in the schema, fail with error listing the unknown keys.\n2. **Unknown rule fields**: If a rule object contains fields not in the rule schema, fail.\n3. **Unsupported feature for target backend**: If a rule uses a feature (e.g., custom log prefix) and `backend_compat` is not explicitly set to `false` for that backend, fail with a message like: \"log_prefix is not supported on ufw. Set `backend_compat.ufw: false` to skip this rule, or remove the unsupported field.\"\n4. **Missing required fields**: Fail if `inbound.default`, `outbound.default`, `forward.default`, `icmp.ipv4`, or `icmp.ipv6` are missing.\n5. **Invalid action values**: Fail if `action` is not one of `accept`, `drop`, `reject`.\n6. **Schema version mismatch**: Fail if `schema_version` major version is higher than the renderer supports.\n\n## Rendering to Backends\n\n### ufw Renderer\n\n```bash\n# Generated from policy inbound.default = deny\nsudo ufw --force default deny incoming\nsudo ufw --force default allow outgoing\n\n# Rule: proto=tcp port=22 action=accept\nsudo ufw allow 22/tcp\n\n# Rule: proto=tcp port=22 rate_limit=\"10/min\"\nsudo ufw limit 22/tcp\n\n# Rule: proto=tcp port=443 action=accept\nsudo ufw allow 443/tcp\n\n# Rule: proto=udp port=53 source=\"10.0.0.0/8\"\nsudo ufw allow from 10.0.0.0/8 to any port 53 proto udp\n```\n\n### firewalld Renderer\n\n```bash\nZONE=$(sudo firewall-cmd --get-default-zone)\n\n# Service rules map to --add-service if known, else --add-port\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=ssh\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=https\n\n# Rate limit maps to rich rule\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule service name=ssh limit value=10/min accept'\n\n# Source restriction maps to rich rule\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule family=ipv4 source address=10.0.0.0/8 port port=53 protocol=udp accept'\n\nsudo firewall-cmd --reload\n```\n\n### nftables Renderer\n\n```nft\n#!/usr/sbin/nft -f\n\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 443 }\n    }\n\n    chain input {\n        type filter hook input priority 0; policy drop;\n\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n\n        # Source-restricted UDP\n        ip saddr 10.0.0.0/8 udp dport 53 accept\n\n        # Allowed TCP ports from set\n        tcp dport @allowed_tcp_ports accept\n\n        # Rate-limited SSH\n        tcp dport 22 ct state new limit rate 10/minute accept\n\n        # Logging\n        log prefix \"fw-drop: \" limit rate 5/second\n        drop\n    }\n\n    chain forward { type filter hook forward priority 0; policy drop; }\n    chain output  { type filter hook output  priority 0; policy accept; }\n}\n```\n\n### iptables Renderer\n\n**IPv4** (`iptables-v4.rules`):\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -s 10.0.0.0/8 -p udp --dport 53 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n**IPv6** (`iptables-v6.rules`):\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p icmpv6 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\nApply:\n```bash\nsudo iptables-restore --test /tmp/iptables-v4.rules && sudo iptables-restore /tmp/iptables-v4.rules\nsudo ip6tables-restore --test /tmp/iptables-v6.rules && sudo ip6tables-restore /tmp/iptables-v6.rules\n```\n\n## Golden Test Fixtures\n\nThese test fixtures verify that the policy renderer produces the expected output for each backend. Run these after any renderer change.\n\n### Fixture: public-web-server -> ufw\n\n**Input** (`fixtures/public-web-server.yaml`):\n```yaml\nschema_version: \"2.0\"\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"10/min\"\n    - proto: tcp\n      port: 80\n      action: accept\n    - proto: tcp\n      port: 443\n      action: accept\noutbound:\n  default: accept\nforward:\n  default: deny\nicmp:\n  ipv4: accept\n  ipv6: accept\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n```\n\n**Expected ufw output** (`fixtures/public-web-server.ufw.expected`):\n```\nsudo ufw --force default deny incoming\nsudo ufw --force default allow outgoing\nsudo ufw limit 22/tcp\nsudo ufw allow 80/tcp\nsudo ufw allow 443/tcp\nsudo ufw --force enable\n```\n\n### Fixture: public-web-server -> nftables\n\n**Expected nftables output** (`fixtures/public-web-server.nftables.expected`):\n```nft\n#!/usr/sbin/nft -f\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 80, 443 }\n    }\n    chain input {\n        type filter hook input priority 0; policy drop\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n        tcp dport @allowed_tcp_ports accept\n        tcp dport 22 ct state new limit rate 10/minute accept\n        log prefix \"fw-drop: \" limit rate 5/second\n        drop\n    }\n    chain forward { type filter hook forward priority 0; policy drop }\n    chain output { type filter hook output priority 0; policy accept }\n}\n```\n\n### Fixture: bastion-host -> iptables (v4)\n\n**Input** (`fixtures/bastion-host.yaml`):\n```yaml\nschema_version: \"2.0\"\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"5/min\"\noutbound:\n  default: accept\nforward:\n  default: deny\nicmp:\n  ipv4: accept\n  ipv6: accept\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n  prefix: \"bastion-drop\"\n```\n\n**Expected iptables v4 output** (`fixtures/bastion-host.iptables-v4.expected`):\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set\n-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 6 -j DROP\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -m limit --limit 5/sec -j LOG --log-prefix \"bastion-drop: \"\nCOMMIT\n```\n\n## Example Policies\n\n### Public Web Server (v2.0)\n\n```yaml\nschema_version: \"2.0\"\nmetadata:\n  name: \"public-web-server\"\n  description: \"Standard web server with SSH, HTTP, HTTPS\"\n\nbackend: auto\n\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"10/min\"\n      comment: \"SSH rate-limited\"\n    - proto: tcp\n      port: 80\n      action: accept\n    - proto: tcp\n      port: 443\n      action: accept\n\noutbound:\n  default: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n  prefix: \"fw-drop\"\n```\n\n### Bastion Host (v2.0)\n\n```yaml\nschema_version: \"2.0\"\nmetadata:\n  name: \"bastion-host\"\n  description: \"SSH-only jump box with aggressive rate limiting\"\n\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"5/min\"\n\noutbound:\n  default: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n```\n\n### Internal Database (v2.0)\n\n```yaml\nschema_version: \"2.0\"\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      source: \"10.0.1.0/24\"\n    - proto: tcp\n      port: 3306\n      action: accept\n      source: \"10.0.2.0/24\"\n\noutbound:\n  default: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n```\n\n### Zero Trust Node (v2.0)\n\n```yaml\nschema_version: \"2.0\"\nmetadata:\n  name: \"zero-trust-node\"\n  description: \"Default deny all inbound and outbound with explicit allowlist\"\n\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      source: \"10.0.1.0/24\"\n\noutbound:\n  default: deny\n  rules:\n    - proto: udp\n      port: 53\n      action: accept\n    - proto: udp\n      port: 123\n      action: accept\n    - proto: tcp\n      port: 80\n      action: accept\n    - proto: tcp\n      port: 443\n      action: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n```\n\n## Future Enhancements\n\n- GeoIP restrictions (`source_geo: [\"US\", \"CA\"]`)\n- Time-based rules (`time_range: \"09:00-17:00\"`)\n- Connection limits (`max_connections: 100`)\n- Custom chains and forwarding rules for complex topologies\n- Integration with cloud security group APIs (AWS, GCP, Azure)\n- Policy diff tool (compare two policies, show backend-specific rule differences)\n- `hermes skill render-policy` CLI command to validate and render a policy file\n\nFile v2.7.0:references/docker-hardening.md\n\n# Docker Firewall Hardening\n\nDocker manipulates iptables/nftables directly. By default, `dockerd` inserts rules **above** ufw in the FORWARD chain, meaning `docker run -p` exposes containers to the world even when ufw INPUT policy is DROP.\n\n## Detection\n\n```bash\ndocker ps --format \"table {{.Names}}\\t{{.Ports}}\"\nsudo iptables -L DOCKER -n -v 2>/dev/null     # Docker-managed NAT rules\nsudo iptables -L DOCKER-USER -n -v 2>/dev/null # User chain (may be empty)\n\n# Check for unintended 0.0.0.0 exposure\ndocker inspect $(docker ps -q) --format '{{.Name}}: {{range $p, $c := .NetworkSettings.Ports}}{{$p}} -> {{(index $c 0).HostIp}}:{{(index $c 0).HostPort}} {{end}}' 2>/dev/null | grep \"0.0.0.0\"\n```\n\n## Key Principle\n\nDocker manages its own chains (DOCKER, DOCKER-ISOLATION-STAGE-\\*) for container NAT and inter-container communication. **Do NOT modify those chains directly.** Use the DOCKER-USER chain which Docker guarantees it will never touch.\n\n## Mitigation Options\n\n### Option A: DOCKER-USER Chain (RECOMMENDED)\n\nThe DOCKER-USER chain is evaluated BEFORE Docker's own rules but only affects the FORWARD chain. Use it to restrict which external interfaces can reach published ports:\n\n```bash\n# Block all external access to published ports on eth0 (public interface)\nsudo iptables -C DOCKER-USER -i eth0 -j DROP 2>/dev/null || \\\n  sudo iptables -I DOCKER-USER 1 -i eth0 -j DROP\n\n# Allow external HTTPS to reach containers\nsudo iptables -C DOCKER-USER -i eth0 -p tcp --dport 443 -j ACCEPT 2>/dev/null || \\\n  sudo iptables -I DOCKER-USER 2 -i eth0 -p tcp --dport 443 -j ACCEPT\n\n# Persist DOCKER-USER rules\nsudo iptables-save | grep DOCKER-USER | sudo tee /etc/iptables/docker-user.rules\n```\n\n### Option B: Bind Published Ports to Specific IPs\n\nInstead of `-p 8080:8080` (binds `0.0.0.0`):\n```bash\ndocker run -p 127.0.0.1:8080:8080 myapp   # Loopback only\ndocker run -p 10.0.1.10:8080:8080 myapp    # Internal IP only\n```\n\n### Option C: Disable Docker's iptables Management\n\nEdit `/etc/docker/daemon.json`:\n```json\n{ \"iptables\": false }\n```\nThen `sudo systemctl restart docker`.\n\n**WARNING**: Makes you fully responsible for all NAT, port mapping, inter-container communication, and outbound masquerading rules.\n\n## Docker + nftables Hosts\n\nOn hosts where nftables is the primary backend but Docker still uses iptables (legacy mode), the two systems coexist but do not share state. Docker's iptables rules are invisible to `nft list ruleset`. Always check both:\n```bash\nsudo iptables -L -n -v | grep -i docker\nsudo nft list ruleset | grep -i docker  # will likely be empty\n```\n\nFile v2.7.0:references/firewall-apply.md\n\n# firewall-apply.sh\n\n## Purpose\n\nApplies a declarative firewall policy to the host in an idempotent, reversible manner. Builds a complete ruleset, swaps it in atomically via `iptables-restore` (per family), and supports automated rollback on failure.\n\n## Inputs\n\n| Input | Source | Required | Default | Notes |\n|---|---|---|---|---|\n| `--policy-dir <dir>` | CLI | No | `./policy.d` | Policy fragments applied in lexical order |\n| `--approved-plan <token>` | CLI | **Yes** | — | Hash from `firewall-plan.sh --json`. Reject if missing/mismatch (exit 41) |\n| `--dry-run` | CLI | No | off | Render + diff only; zero kernel changes |\n| `--family <v4\\|v6\\|both>` | CLI | No | `both` | Which address families to apply |\n| `POLICY_DIR` | env | No | `./policy.d` | Override policy directory |\n| `LOG_LEVEL` | env | No | `info` | `debug` emits full generated ruleset |\n| `LOCK_PATH` | env | No | `/run/fw.lock` | Advisory lock file |\n| stdin | pipe | No | — | If policy piped, `POLICY_DIR` is ignored |\n\nAll inputs validated before any mutation. Unknown flags → exit code 2, zero changes.\n\n## Idempotency Guarantees\n\n- Running N times with same policy produces identical final ruleset (no duplicate rule accumulation)\n- Builds complete ruleset in temp file, swaps via `iptables-restore` (atomic per family)\n- IPv4 and IPv6 generated from same policy source → both families stay in sync\n- No-op run (live state = desired state) makes zero kernel changes, exits 0\n- `--dry-run` exits 0 when no diff, 10 when diff exists\n- Advisory lock prevents interleaved concurrent invocations\n\n## Exit Codes\n\n| Code | Meaning | Changes Applied? | Operator Action |\n|---|---|---|---|\n| 0 | Success (applied or already OK) | Maybe | None |\n| 2 | Usage / invalid argument | No | Fix invocation |\n| 3 | Policy validation failed | No | Correct policy fragment |\n| 4 | Lock acquisition failed | No | Retry after current run completes |\n| 5 | Ruleset generation failed | No | Inspect logs; likely template/syntax error |\n| 6 | Apply failed, rollback succeeded | No (reverted) | Investigate apply error; host on prior ruleset |\n| 7 | Apply failed, rollback FAILED | Partial/unknown | **Manual intervention required** |\n| 10 | `--dry-run`: drift detected | No | Review diff; re-run without `--dry-run` |\n| 41 | Plan approval token missing/mismatch | No | Re-run PLAN to get current token |\n\nCodes 0–5: pre-mutation, host untouched. Codes 6–7: live state may have been altered.\n\n## Rollback Contract\n\n1. Before applying, snapshots current live ruleset via `iptables-save` / `ip6tables-save` to `${BACKUP_DIR}/pre-apply-<timestamp>.{v4,v6}`\n2. Swap is atomic per family. If IPv4 apply succeeds but IPv6 fails → restores IPv4 from snapshot before exiting. Host is never left in mixed-family state.\n3. On apply failure:\n   - Restore succeeds → exit 6, host on exact prior ruleset\n   - Restore fails → exit 7. Snapshot paths printed to stderr. Operator restores manually: `iptables-restore < <snapshot>.v4`\n4. Snapshots retained for `BACKUP_RETENTION` runs (default 10), pruned oldest-first. Failed-run snapshots exempt from pruning until manually cleared.\n5. Script never deletes active ruleset without verified replacement. No window where host has no firewall loaded.\n\n## Side Effects\n\n- Creates `${BACKUP_DIR}/pre-apply-*.{v4,v6}` backup files\n- Writes to `${LOCK_PATH}` advisory lock\n- Modifies kernel netfilter state (iptables/ip6tables)\n- May trigger systemd-run for auto-rollback timer\n\nFile v2.7.0:references/k8s-policy.md\n\n# Kubernetes Node Firewall Policy\n\n**CRITICAL**: Kubernetes nodes manage their own netfilter rules through the CNI plugin and kube-proxy. Manual host firewall changes can break pod networking, Service load balancing, and NetworkPolicy enforcement.\n\n## Detection\n\n```bash\n# Is this a K8s node?\n[[ -f /etc/kubernetes/kubelet.conf ]] && echo \"Control plane or worker node\"\n[[ -d /etc/cni/net.d ]] && echo \"CNI configuration present\"\n\n# Detect CNI plugin\nls /etc/cni/net.d/ 2>/dev/null\nps aux | grep -E \"cilium|calico|flannel|kube-proxy\" | grep -v grep\n\n# Check kube-proxy mode\nkubectl get configmap kube-proxy -n kube-system -o yaml 2>/dev/null | grep -i mode\ncurl -s http://localhost:10249/proxyMode 2>/dev/null\n```\n\n## CNI-Specific Guidance\n\n| CNI | Dataplane | Action |\n|-----|----------|--------|\n| **Cilium** | eBPF (below netfilter) | Do NOT modify host netfilter. Use CiliumNetworkPolicy. |\n| **Calico (eBPF)** | eBPF | Do NOT modify. Use Calico GlobalNetworkPolicy. |\n| **Calico (iptables)** | iptables | Never flush. Use NetworkPolicy. |\n| **Flannel** | iptables MASQUERADE | Never flush. Preserve MASQUERADE rules. |\n| **kube-proxy (iptables)** | iptables Service DNAT | Never flush. |\n| **kube-proxy (ipvs)** | IPVS + auxiliary iptables | Never flush iptables. |\n| **kube-proxy (nftables)** | nftables Services | Never flush ruleset. |\n\n## Required Policy: AUDIT-ONLY Mode\n\nOn any detected Kubernetes node, default to audit-only:\n```bash\nbash scripts/audit-firewall.sh --json\n```\n\nIf `k8s_node: true`, do NOT proceed with host firewall changes. Recommend:\n1. **NetworkPolicies** for pod-to-pod traffic\n2. **Cloud security groups / VPC firewall** for node-level access\n3. Narrow host-level changes ONLY for kubelet API (port 10250) and SSH — and only after staging cluster testing.\n\n## Host Firewall vs NetworkPolicy\n\n| Layer | Managed By | Controls |\n|-------|-----------|---------|\n| Host firewall (this skill) | ufw, firewalld, nftables, iptables | Traffic to the node's own IP |\n| NetworkPolicy | CNI plugin | Traffic between pods |\n| Cloud firewall / SG | Cloud provider API | Traffic entering/exiting VPC |\n\nA properly secured K8s cluster uses **all three layers**.\n\nFile v2.7.0:references/observability.md\n\n# Observability & Performance\n\n## nftables Counters\n\n```bash\n# Per-rule counters and handles\nsudo nft list ruleset -a\nsudo nft list chain inet filter input\n```\n\n## Connection Tracking\n\n```bash\ncat /proc/sys/net/netfilter/nf_conntrack_count\ncat /proc/sys/net/netfilter/nf_conntrack_max\n\nsudo conntrack -L 2>/dev/null | wc -l\n\necho \"Conntrack usage: $(cat /proc/sys/net/netfilter/nf_conntrack_count) / $(cat /proc/sys/net/netfilter/nf_conntrack_max)\"\n```\n\n## Dropped Packet Monitoring\n\n```bash\n# ufw\nsudo tail -f /var/log/ufw.log\n\n# iptables LOG target\nsudo dmesg | grep -i \"iptables\\\\|nf_log\"\n\n# nftables log prefix\nsudo journalctl -k -f | grep \"nft-drop\"\n```\n\n## fail2ban Metrics\n\n```bash\nsudo fail2ban-client status\nsudo fail2ban-client status sshd\ngrep \"Ban\" /var/log/fail2ban.log 2>/dev/null | tail -20\n```\n\n## Recommended Baselines\n\nRecord after hardening:\n- `nf_conntrack_count` at idle and peak load\n- `nft list ruleset` counter values after 24h\n- UFW log volume per hour\n- fail2ban ban rate per day\n\n## Performance Risks\n\n| Risk | Symptom | Mitigation |\n|------|---------|------------|\n| **conntrack exhaustion** | `nf_conntrack: table full` | Increase max or use stateless rules |\n| **logging flood** | syslog high CPU, disk full | Always `limit rate` on log rules |\n| **huge ipsets** | Slow rule evaluation | Use `flags interval`, split sets |\n| **SYN flood** | High half-open connections | Enable `tcp_syncookies`, use SYNPROXY |\n| **rule evaluation order** | High CPU per packet | Place most-hit rules early |\n\n## Kernel Tuning\n\n```bash\n# Enable reverse path filtering\nsudo sysctl -w net.ipv4.conf.all.rp_filter=1\n# Enable SYN cookies\nsudo sysctl -w net.ipv4.tcp_syncookies=1\n# Ignore redirects\nsudo sysctl -w net.ipv4.conf.all.accept_redirects=0\nsudo sysctl -w net.ipv6.conf.all.accept_redirects=0\n# Persist\nsudo tee -a /etc/sysctl.d/99-firewall-hardening.conf << 'EOF'\nnet.ipv4.conf.all.rp_filter=1\nnet.ipv4.conf.default.rp_filter=1\nnet.ipv4.tcp_syncookies=1\nnet.ipv4.conf.all.accept_redirects=0\nnet.ipv6.conf.all.accept_redirects=0\nnet.ipv4.icmp_echo_ignore_broadcasts=1\nEOF\nsudo sysctl --system\n```\n\n> **Warning**: sysctl changes affect the entire TCP/IP stack. Test on non-production first. Never change sysctl hardening values on K8s/CNI hosts without explicit CNI profile support.\n\nArchive v2.5.0: 22 files, 53936 bytes\n\nFiles: PUBLISH.md (17600b), references/backend-firewalld.md (2505b), references/backend-iptables.md (2294b), references/backend-nftables.md (2614b), references/backend-ufw.md (2130b), references/compliance.md (1281b), references/declarative-policy.md (15127b), references/docker-hardening.md (2578b), references/k8s-policy.md (2181b), references/observability.md (2299b), references/policy-schema.json (4066b), references/recovery.md (2031b), references/remaining-improvements.md (7873b), references/security-profiles.md (11431b), references/special-environments.md (6529b), scripts/audit-firewall.sh (12604b), scripts/firewall-apply.sh (6793b), scripts/firewall-plan.sh (6922b), scripts/firewall-verify.sh (4386b), skill-card.md (3413b), SKILL.md (20198b), _meta.json (143b)\n\nFile v2.5.0:SKILL.md\n\n---\nname: linux-firewall-hardening\ntitle: Linux Firewall Hardening\ndescription: 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.\nlicense: Dual MIT / Apache-2.0\nskill_version: 2.5.0\nschema_version: 2\ntags: [security, firewall, ufw, iptables, nftables, firewalld, hardening, docker, fail2ban, policy-as-code, devsecops, ipv6]\n---\n\n# Linux Firewall Hardening\n\n## When to Use\n\n- Check if a Linux server has active firewall protection.\n- Enable and configure a firewall without locking yourself out of SSH.\n- Audit existing rules, troubleshoot connectivity, or apply a security profile.\n- Automate firewall hardening via an AI agent or CI/CD pipeline.\n\n## When NOT to Use\n\n| Condition | Alternative |\n|-----------|-------------|\n| Kubernetes worker node | Use NetworkPolicies / CiliumNetworkPolicy |\n| Firewall managed by Terraform/Ansible/Puppet/Chef | Update IaC source of truth |\n| Cloud workload with Security Group / NSG only | Use cloud provider's firewall API |\n| Inside a container | Escalate to host operator |\n| WSL2, macOS, or shared/managed hosting | See `references/special-environments.md` |\n\n> **Support files**: `scripts/audit-firewall.sh` (run first), `scripts/firewall-plan.sh` (dry-run), `scripts/firewall-verify.sh` (post-apply).\n> Detailed backend guides, Docker/K8s policies, observability, compliance, and recovery are in `references/`.\n\n## 🚨 Emergency: I'm Locked Out — What Now?\n\nIf you just applied firewall rules and lost SSH connectivity:\n\n1. **Wait 5 minutes** — the auto-rollback timer (scheduled during VALIDATE) will restore access. Don't panic and don't take destructive actions.\n2. **Use your second SSH session** — if you opened one (pre-flight checklist), switch to it and fix the rules manually.\n3. **Cloud serial console** — AWS EC2 Serial Console, GCP Serial Port, Azure Serial Console, or hypervisor VNC/IPMI/iDRAC.\n4. **Restore from backup via console** — once connected: `sudo iptables-restore < ~/firewall-backup-*/iptables-v4.rules`\n5. **Emergency ACCEPT (LAST RESORT)** — `sudo iptables -P INPUT ACCEPT; sudo iptables -F; sudo ufw disable`. This exposes the host completely. Re-harden immediately.\n\nFull procedures: `references/recovery.md`.\n\n---\n\n## Prerequisites\n\n- Root or sudo access.\n- An active SSH session (risk of lockout).\n- Know which ports your services use.\n\n---\n\n## NEVER DO (14 Rules)\n\n1. **Never flush iptables/nftables on Kubernetes nodes.** CNI plugins manage netfilter.\n2. **Never run `iptables -F` or `nft flush ruleset` without a verified backup.** Docker/K8s networking will break.\n3. **Never disable firewalld and use raw iptables simultaneously.** Undefined behavior.\n4. **Never set `DROP` policy on INPUT before allowing your current SSH port.** Immediate lockout.\n5. **Never disable Docker's `iptables` management without replacement NAT/routing rules.**\n6. **Never restart `networking.service` or `NetworkManager` remotely without console access.**\n7. **Never apply cloud SG and host firewall changes simultaneously without testing.**\n8. **Never enable logging on high-traffic DROP rules without `limit rate`.** Disk flood.\n9. **Never manage nftables/iptables directly when ufw or firewalld owns the policy.** Split-brain state.\n10. **Never apply outbound default-deny without explicitly allowing DNS, NTP, package mirrors.**\n11. **Never restore firewall backups from a different host, kernel version, or backend mode.**\n12. **Never assume IPv4 rules protect IPv6.** Verify both stacks separately.\n13. **Never change sysctl hardening values on K8s/CNI hosts without explicit CNI profile support.**\n14. **Never enable verbose packet logging without rate limits and log rotation.**\n\n---\n\n## State Machine\n\nFollow states in order. Do not skip.\n\n```\nDETECT → SELECT → PLAN → VALIDATE → APPLY → VERIFY\n```\n\n### State: DETECT\n\nRun the audit script:\n\n```bash\nbash scripts/audit-firewall.sh           # Human-readable\nbash scripts/audit-firewall.sh --json    # Machine-readable\n```\n\n**Key outputs**: `confidence`, `risk_tier`, `recommended_backend`, `halt_reasons`, `k8s_node`, `iac_owner`.\n\n### Risk Tiers & Confidence Gating\n\n| Tier | Confidence | Agent Behavior |\n|------|-----------|----------------|\n| `auto` | ≥ 90% | Proceed automatically to PLAN |\n| `confirmed` | 70–89% | Proceed but require human confirmation before APPLY |\n| `manual` | 50–69% | Audit-only mode. Generate recommendations, do not apply. |\n| `halt` | < 50% | Stop immediately. Escalate findings to operator. |\n\n**Additional halt triggers** (regardless of confidence): containerized, K8s node, IaC managed, no rollback mechanism available.\n\n### Decision Tree\n\n| Condition | Path | Detail |\n|-----------|------|--------|\n| Risk tier = `halt` | **STOP** | Resolve blockers first |\n| Inside container | **STOP** | Escalate to host operator |\n| K8s node detected | **STOP** | `references/k8s-policy.md` |\n| Ubuntu/Debian + ufw active | **Phase: UFW** | `references/backend-ufw.md` |\n| ufw + firewalld both active | **STOP** | Resolve conflict |\n| RHEL/Rocky/Alma + firewalld active | **Phase: firewalld** | `references/backend-firewalld.md` |\n| nftables active, no frontend | **Phase: nftables** | `references/backend-nftables.md` |\n| iptables only | **Phase: iptables** | `references/backend-iptables.md` |\n| Docker host | Apply **Docker Hardening** after phase above | `references/docker-hardening.md` |\n\n### Ownership Boundary\n\nBefore 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`.\n\n---\n\n### State: SELECT\n\nOptionally load a pre-built security profile (`references/security-profiles.md`):\n\n| Profile | Use Case |\n|---------|----------|\n| `public-web-server` | Open 22, 80, 443. Rate-limit SSH. |\n| `internal-database` | SSH from mgmt subnet only. DB port from app subnet only. |\n| `bastion-host` | SSH only. Aggressive rate limiting. |\n| `zero-trust-node` | Default deny all inbound and outbound. |\n\nOr use declarative YAML (`references/declarative-policy.md`):\n\n```\nImperative (state machine) → Ad-hoc hardening, incident response\nDeclarative (YAML)        → GitOps, multi-host, reproducible\nMixed                     → YAML as source-of-truth, state machine for verification\n```\n\n---\n\n### State: PLAN\n\nGenerate a dry-run diff before applying:\n\n```bash\nbash scripts/firewall-plan.sh --profile public-web-server\nbash scripts/firewall-plan.sh --ports 22,80,443\nbash scripts/firewall-plan.sh --json     # Machine-readable diff with approval_token\nbash scripts/firewall-plan.sh --refresh-audit --json  # Force re-audit + plan\n```\n\nReview the output. If `risk_tier` is `confirmed`, present the plan and wait for human confirmation before APPLY.\n\n**Plan JSON schema** (matches `firewall-plan.sh --json` output):\n\n```json\n{\n  \"backend\": \"ufw\",\n  \"active_frontend\": \"ufw\",\n  \"profile\": \"public-web-server\",\n  \"target_ports\": [22, 80, 443],\n  \"diff\": {\n    \"add\":    [{\"port\": 80, \"proto\": \"tcp\", \"source\": \"any\"}],\n    \"skip\":   [{\"port\": 22, \"proto\": \"tcp\", \"reason\": \"already_exists\"}],\n    \"remove\": []\n  },\n  \"risk_assessment\": \"low\",\n  \"estimated_disruption\": \"none\",\n  \"approval_token\": \"sha256:abc123...\",\n  \"audit_cached\": false,\n  \"audit_cache_file\": \"/tmp/firewall-audit.json\"\n}\n```\n\n**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.\n\n**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.\n\n---\n\n### State: VALIDATE\n\n#### 1. Create Backup (Mandatory)\n\n```bash\nBACKUP_DIR=\"$HOME/firewall-backup-$(date +%Y%m%d-%H%M%S)\"\nmkdir -p \"$BACKUP_DIR\"\n\nsudo iptables-save > \"$BACKUP_DIR/iptables-v4.rules\" 2>/dev/null || true\nsudo ip6tables-save > \"$BACKUP_DIR/iptables-v6.rules\" 2>/dev/null || true\nsudo nft list ruleset > \"$BACKUP_DIR/nftables.rules\" 2>/dev/null || true\nsudo ufw status verbose > \"$BACKUP_DIR/ufw-status.txt\" 2>/dev/null || true\nsudo firewall-cmd --list-all --zone=$(sudo firewall-cmd --get-default-zone) > \"$BACKUP_DIR/firewalld-default.txt\" 2>/dev/null || true\n\necho \"Backup saved to $BACKUP_DIR\"\n```\n\n#### 2. Schedule Rollback (Mandatory for Remote)\n\nThe 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.\n\n```bash\n# Build rollback script from backup dir\nROLLBACK_SCRIPT=$(cat <<'RB'\n#!/bin/bash\nBACKUP_DIR=\"REPLACE_ME\"\n[ -f \"$BACKUP_DIR/iptables-v4.rules\" ] && sudo iptables-restore < \"$BACKUP_DIR/iptables-v4.rules\" || { sudo iptables -P INPUT ACCEPT; sudo iptables -F; }\n[ -f \"$BACKUP_DIR/iptables-v6.rules\" ] && sudo ip6tables-restore < \"$BACKUP_DIR/iptables-v6.rules\" || { sudo ip6tables -P INPUT ACCEPT; sudo ip6tables -F; }\n[ -f \"$BACKUP_DIR/nftables.rules\" ] && sudo nft -f \"$BACKUP_DIR/nftables.rules\" || sudo nft flush ruleset\nsystemctl is-active ufw &>/dev/null && sudo ufw disable\nsudo firewall-cmd --panic-off 2>/dev/null\nRB\n)\nROLLBACK_SCRIPT=\"${ROLLBACK_SCRIPT/REPLACE_ME/$BACKUP_DIR}\"\n\n# Schedule (at preferred, systemd-run fallback)\nif command -v at &>/dev/null; then\n    ROLLBACK_JOB_ID=$(echo \"sudo bash -c '$ROLLBACK_SCRIPT'\" | at now + 5 minutes 2>&1 | grep -oP 'job \\K\\d+')\n    echo \"Rollback scheduled: at job $ROLLBACK_JOB_ID (cancel with: atrm $ROLLBACK_JOB_ID)\"\nelif command -v systemd-run &>/dev/null; then\n    UNIT_NAME=\"firewall-rollback-$$\"\n    echo \"$ROLLBACK_SCRIPT\" > /tmp/firewall-rollback-$$.sh\n    chmod +x /tmp/firewall-rollback-$$.sh\n    systemd-run --on-active=5m --unit=\"$UNIT_NAME\" --user /tmp/firewall-rollback-$$.sh\n    echo \"Rollback scheduled: systemd unit $UNIT_NAME (cancel with: systemctl --user stop $UNIT_NAME)\"\nfi\n```\n\nSee `references/recovery.md` for advanced recovery scenarios.\n\n#### 3. Pre-Flight Checklist\n\n- [ ] Backup created successfully\n- [ ] Rollback scheduled (verify with `atq` or `systemctl --user list-units`)\n- [ ] **Second SSH session open and tested** — open a second terminal, SSH in, and confirm you can run `sudo whoami`. This is your emergency console if the primary session loses connectivity. Keep it open until VERIFY passes. **Why**: existing ESTABLISHED conntrack entries usually keep your current session alive, but if conntrack is flushed or the policy change drops your session silently, this second session is your only way back in.\n- [ ] Real SSH port identified (not assumed to be 22)\n- [ ] Confidence ≥ 70% and risk_tier is `auto` or `confirmed`\n- [ ] Ownership verified — no IaC managing firewall\n- [ ] Change window appropriate (maintenance window or low traffic)\n- [ ] PLAN output reviewed and approved\n\n---\n\n### State: APPLY\n\nApply firewall rules using the approved plan from PLAN state.\n\n```bash\n# Get the approval_token from firewall-plan.sh --json output\nbash scripts/firewall-apply.sh --approved-plan=sha256:abc123...\nbash scripts/firewall-apply.sh --approved-plan=sha256:abc123... --dry-run\n```\n\n**Apply behavior:**\n- Verifies `approval_token` matches current plan (exit 41 on mismatch — plan changed since approval)\n- Checks for active rollback timer (exit 40 if VALIDATE was skipped)\n- Supports all backends: ufw, firewalld, nftables, iptables\n- Automatically runs `firewall-verify.sh` after applying\n- Passes `--dry-run` for preview-only mode\n\n**Idempotent inline commands** (for manual/scriptless use):\n\n| Backend | Pattern |\n|---------|---------|\n| ufw | `sudo ufw status \\| awk '{print $1}' \\| grep -qx \"22/tcp\" \\|\\| sudo ufw allow 22/tcp` |\n| firewalld | `sudo firewall-cmd --query-service=ssh \\|\\| sudo firewall-cmd --permanent --add-service=ssh` |\n| iptables | `sudo iptables -C INPUT -p tcp --dport 22 -j ACCEPT 2>/dev/null \\|\\| sudo iptables -A ...` |\n| nftables | Atomic ruleset: `nft -c -f /etc/nftables.conf.new && nft -f /etc/nftables.conf.new` |\n\n#### Docker Hosts\n\nDocker bypasses ufw by default. Use DOCKER-USER chain. Full guide: `references/docker-hardening.md`.\n\n#### Kubernetes Nodes\n\n**Default: AUDIT-ONLY**. Never modify host firewall. Full policy: `references/k8s-policy.md`.\n\n---\n\n### State: VERIFY\n\nRun post-hardening checks:\n\n```bash\nbash scripts/firewall-verify.sh\n```\n\n**Success criteria** (all must pass):\n1. SSH remains reachable from current and second session\n2. Only intended ports are externally reachable\n3. Rules survive reboot (verified via service persistence)\n4. IPv6 exposure matches IPv4 policy\n5. Docker-published ports are intentional (no accidental `0.0.0.0`)\n6. fail2ban jails active (if installed) with correct backend\n7. Rollback timer cancelled after successful verification\n\n**Verify behavior contract:**\n- Verify MUST complete within the rollback timer window (default 5 min)\n- If verify times out before completion → timer auto-fires rollback (system-level protection)\n- If verify FAILS but timer was already cancelled → manual rollback from the backup directory. Restore commands (in priority order):\n  1. `sudo iptables-restore < \"$BACKUP_DIR/iptables-v4.rules\"`\n  2. `sudo ip6tables-restore < \"$BACKUP_DIR/iptables-v6.rules\"`\n  3. `sudo nft -f \"$BACKUP_DIR/nftables.rules\"`\n  4. `sudo ufw reset && sudo ufw disable`\n  See `references/recovery.md` for full recovery procedures including emergency ACCEPT fallback.\n- 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\n\n## Exit Codes (Core Contract)\n\n| Code | Meaning | Agent Action |\n|------|---------|-------------|\n| 0 | Success | Continue |\n| 10 | Backend conflict | Halt; resolve manually |\n| 11 | Backend detection failed | Halt; check firewall stack |\n| 12 | Multiple backends active | Halt; resolve conflict |\n| 20 | IaC-managed | Halt; update IaC source |\n| 21 | Inside container | Halt; escalate to host operator |\n| 22 | K8s node detected | Halt; audit-only mode |\n| 30 | Low confidence (<70%) | Drop to audit-only mode |\n| 31 | No rollback capability | Halt; ensure at or systemd-run |\n| 40 | Preflight failed | Halt; check prerequisites |\n| 41 | Plan approval mismatch | Halt; re-run PLAN with approval |\n| 42 | RESERVED (Backup failed) | Halt; resolve disk/permissions |\n| 50 | RESERVED (Apply failed) | Auto-rollback triggered |\n| 51 | Apply partial | Auto-rollback triggered; verify backup |\n| 60 | Verify failed | Auto-rollback triggered |\n| 61 | RESERVED (State file conflict) | Abort; resolve stale state |\n\n---\n\n## fail2ban Integration\n\nIf fail2ban is installed:\n\n| Host Firewall | Recommended `backend` |\n|--------------|----------------------|\n| ufw | `ufw` or `systemd` |\n| firewalld | `firewalld` |\n| nftables | `nftables` |\n| iptables | `auto` (default) |\n\nAfter changing backend: `sudo fail2ban-client restart && sudo fail2ban-client status sshd`.\n\n---\n\n## Recovery\n\nIf you lose connectivity, priority order:\n1. Wait for auto-rollback (scheduled during VALIDATE)\n2. Use second SSH session\n3. Cloud serial console / hypervisor console\n4. Restore from backup\n5. Emergency ACCEPT (last resort — exposes host completely)\n\nFull procedures: `references/recovery.md`.\n\n## State Persistence & Interrupt-Resume\n\nFor 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:\n\n```bash\nSTATE_DIR=\"$HOME/.firewall-hardening\"\nSTATE_FILE=\"$STATE_DIR/state.json\"\n```\n\n**State file structure:**\n\n```json\n{\n  \"state\": \"validate\",\n  \"started_at\": \"2026-05-11T16:00:00Z\",\n  \"backend\": \"ufw\",\n  \"risk_tier\": \"auto\",\n  \"backup_dir\": \"/home/user/firewall-backup-20260511-160000\",\n  \"rollback_timer_id\": \"firewall-rollback-12345\",\n  \"plan_hash\": \"sha256:abc123...\"\n}\n```\n\n**Resume logic:**\n- If `state.json` exists and `started_at` is within 1 hour → resume from that state\n- If `state.json` is stale (>1 hour) → delete it and start fresh from Detect\n- The file is advisory-only; agent can always restart from Detect\n\n> State persistence is optional. The skill defaults to restarting from Detect each run. Enable by creating `$STATE_DIR` before starting.\n\n---\n\n## Cloud Security Group Reminder\n\nThe host firewall is your **second** layer. Verify cloud SGs are aligned:\n\n| Cloud | Outer Firewall |\n|-------|---------------|\n| AWS | Security Groups |\n| GCP | VPC Firewall Rules |\n| Azure | Network Security Groups |\n| DigitalOcean/Linode/Vultr | Cloud Firewall |\n\n## Compatibility Matrix\n\n| Distro/Env | ufw | firewalld | nftables | iptables | Coverage |\n|------------|-----|-----------|----------|----------|----------|\n| Ubuntu 22.04/24.04 | Primary | — | Backend | Fallback | Full |\n| Debian 12 | Primary | — | Backend | Fallback | Full |\n| RHEL 9 | — | Primary | Native | Backend | Full |\n| Rocky/Alma 9 | — | Primary | Native | Backend | Full |\n| Fedora 40+ | — | Primary | Native | Backend | Partial |\n| Alpine 3.18+ | — | — | Native | Fallback | Partial |\n| Arch | — | — | Native | Fallback | Community |\n| Docker host | ✅ DOCKER-USER chain | ✅ `docker-hardening.md` | ✅ `docker-hardening.md` | ✅ `docker-hardening.md` | Full |\n| LXC/LXD container | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | Partial |\n| systemd-nspawn | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | Partial |\n| WSL2 | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | None |\n\n> 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`.\n\n---\n\n## Observability\n\nEstablish baselines after hardening: conntrack usage, dropped packet rates, fail2ban ban rate. Monitor for anomalies. Full guide: `references/observability.md`.\n\n## Compliance\n\nPractices map to CIS, PCI-DSS, and SOC2 controls. Full mapping: `references/compliance.md`.\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Audit environment | `bash scripts/audit-firewall.sh --json` |\n| Plan changes | `bash scripts/firewall-plan.sh --profile web` |\n| Verify after apply | `bash scripts/firewall-verify.sh` |\n| Allow port (ufw, idempotent) | `sudo ufw status \\| awk '{print $1}' \\| grep -qx \"80/tcp\" \\|\\| sudo ufw allow 80/tcp` |\n| View ufw rules | `sudo ufw status numbered` |\n| View nft rules | `sudo nft list ruleset` |\n| View iptables rules | `sudo iptables -L -n -v` |\n| View ip6tables rules | `sudo ip6tables -L -n -v` |\n| Atomic iptables replace | `sudo iptables-restore < /tmp/rules.v4` |\n| Dry-run nftables | `sudo nft -c -f /etc/nftables.conf` |\n| Backup rules | `sudo iptables-save > ~/iptables.backup` |\n| fail2ban status | `sudo fail2ban-client status sshd` |\n| Cancel rollback (at) | `atrm <jobid>` |\n| Cancel rollback (systemd-run) | `systemctl --user stop firewall-rollback-<pid>` |\n\n## See Also\n\n- `references/backend-ufw.md` — Full UFW phase\n- `references/backend-firewalld.md` — Full firewalld phase\n- `references/backend-nftables.md` — Full nftables phase\n- `references/backend-iptables.md` — Full iptables phase\n- `references/docker-hardening.md` — Docker firewall hardening\n- `references/k8s-policy.md` — Kubernetes node policy\n- `references/security-profiles.md` — Pre-built configurations\n- `references/declarative-policy.md` — YAML policy schema\n- `references/observability.md` — Monitoring and baselines\n- `references/compliance.md` — CIS/PCI-DSS/SOC2 mapping\n- `references/recovery.md` — Recovery procedures\n- `references/special-environments.md` — WSL2, containers, exit codes\n- `scripts/audit-firewall.sh` — Environment detection\n- `scripts/firewall-plan.sh` — Dry-run diff\n- `scripts/firewall-verify.sh` — Post-apply verification\n\nFile v2.5.0:_meta.json\n\n{\n  \"ownerId\": \"kn7e8vz4v0f8vr8dh2yr78fjkd86jgk3\",\n  \"slug\": \"linux-firewall-hardening\",\n  \"version\": \"2.5.0\",\n  \"publishedAt\": 1780220102375\n}\n\nFile v2.5.0:references/backend-firewalld.md\n\n# Backend: firewalld (RHEL / Rocky / Alma / Fedora)\n\nfirewalld is zone-aware. Always specify the zone. Default on most servers is `public`.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo firewall-cmd --state\n# \"running\" or \"not running\"\n```\n\n## Prerequisites\n\n- firewalld must be active but with known rules.\n- No other frontend (ufw) must be active.\n- Never modify iptables/nftables directly when firewalld owns the policy.\n\n## Apply: Idempotent Zone Rules\n\n### Step 1: Identify Active Zone\n\n```bash\nDEFAULT_ZONE=$(sudo firewall-cmd --get-default-zone)\necho \"Default zone: $DEFAULT_ZONE\"\nsudo firewall-cmd --get-active-zones\n```\n\n### Step 2: Add Rules\n\n```bash\nZONE=\"${DEFAULT_ZONE:-public}\"\nSSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk -F: '{print $NF}' | head -1)\nSSH_PORT=${SSH_PORT:-22}\n\n# SSH (service definition)\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=ssh >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=ssh\n\n# HTTP / HTTPS\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=http >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=http\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=https >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=https\n\n# Custom port\n# sudo firewall-cmd --zone=\"$ZONE\" --query-port=8080/tcp >/dev/null 2>&1 || #   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-port=8080/tcp\n\n# Rate-limit SSH (rich rule)\nsudo firewall-cmd --zone=\"$ZONE\" --query-rich-rule='rule service name=ssh limit value=3/m accept' >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule service name=ssh limit value=3/m accept'\n\n# Apply\nsudo firewall-cmd --reload\n```\n\n### Step 3: Verify\n\n```bash\nsudo firewall-cmd --list-all --zone=\"$ZONE\"\n```\n\n## Zone Commands Quick Reference\n\n```bash\n# List all zones\nsudo firewall-cmd --get-zones\n\n# List all zones with rules\nsudo firewall-cmd --list-all-zones\n\n# Change default zone\nsudo firewall-cmd --set-default-zone=drop\n\n# Move interface to different zone\nsudo firewall-cmd --zone=internal --change-interface=eth1\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built firewalld configurations\n- `references/declarative-policy.md` — YAML-to-firewalld rendering\n\nFile v2.5.0:references/backend-iptables.md\n\n# Backend: iptables (Legacy Fallback)\n\nUse atomic `iptables-restore` instead of `-F` followed by individual `-A` commands. Build a complete ruleset file, then swap it in one operation. Always manage IPv4 (`iptables`) and IPv6 (`ip6tables`) separately.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. If your SSH runs on a different port, replace `22` with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> echo \"Detected SSH port: $SSH_PORT\"\n> ```\n> Using the wrong SSH port in the ruleset below will lock you out.\n\n## Key Principle: Atomic Restore\n\n```bash\n# Validate syntax first\nsudo iptables-restore --test /tmp/iptables-v4.rules\nsudo ip6tables-restore --test /tmp/iptables-v6.rules\n\n# Apply atomically\nsudo iptables-restore /tmp/iptables-v4.rules\nsudo ip6tables-restore /tmp/iptables-v6.rules\n```\n\n## Apply: Atomic Rulesets\n\n### Step 1: Build IPv4 Ruleset (`/tmp/iptables-v4.rules`)\n\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n### Step 2: Build IPv6 Ruleset (`/tmp/iptables-v6.rules`)\n\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p icmpv6 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n### Step 3: Persist\n\n- **Debian/Ubuntu**: `sudo apt install iptables-persistent`, then `sudo netfilter-persistent save`\n- **RHEL/CentOS**: `sudo service iptables save` or migrate to `firewalld`\n\n## Idempotent Single-Rule Pattern\n\nIf adding a single rule instead of full restore:\n\n```bash\n# Check if rule exists before adding\nsudo iptables -C INPUT -p tcp --dport 8080 -j ACCEPT 2>/dev/null ||   sudo iptables -A INPUT -p tcp --dport 8080 -j ACCEPT\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built iptables configurations\n- `references/declarative-policy.md` — YAML-to-iptables rendering\n\nFile v2.5.0:references/backend-nftables.md\n\n# Backend: nftables (Modern Dual-Stack)\n\nnftables is the modern replacement for iptables. It supports IPv4 and IPv6 in a single `inet` table, has atomic ruleset replacement, and uses a cleaner syntax.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo nft list ruleset\n# Shows current rules if active\n```\n\n## Key Principle: Atomic Replacement\n\nBuild a new ruleset file, validate with `nft -c`, then apply in one shot. **Never `flush ruleset` manually** on a production host without a backup.\n\n## Apply: Atomic Ruleset\n\n### Step 1: Build Ruleset File\n\n```bash\nsudo tee /etc/nftables.conf.new << 'EOF'\n#!/usr/sbin/nft -f\n\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 80, 443 }\n    }\n\n    chain input {\n        type filter hook input priority 0; policy drop;\n\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n\n        tcp dport @allowed_tcp_ports accept\n\n        # Rate limit new SSH connections\n        tcp dport 22 ct state new limit rate 10/second burst 20 packets accept\n\n        # Log with rate limit to prevent syslog flood\n        log prefix \"nft-drop: \" limit rate 5/second\n        drop\n    }\n\n    chain forward {\n        type filter hook forward priority 0; policy drop;\n    }\n\n    chain output {\n        type filter hook output priority 0; policy accept;\n    }\n}\nEOF\n```\n\n> **Warning**: Excessive logging can overwhelm syslog/journald on high-traffic systems. Always use `limit rate` on log rules and monitor after enabling.\n\n### Step 2: Dry-Run (Validate Syntax)\n\n```bash\nsudo nft -c -f /etc/nftables.conf.new\n```\n\nIf this returns errors, fix the file and re-validate. **Do not proceed until dry-run passes.**\n\n### Step 3: Atomic Apply\n\n```bash\n# Backup current ruleset (belt-and-suspenders)\nsudo nft list ruleset > \"$BACKUP_DIR/nftables-pre-apply.rules\" 2>/dev/null || true\n\n# Atomic replace\nsudo nft -f /etc/nftables.conf.new\nsudo mv /etc/nftables.conf.new /etc/nftables.conf\n\n# Enable persistence\nsudo systemctl enable nftables\nsudo systemctl restart nftables\n```\n\n### Step 4: Verify\n\n```bash\nsudo nft list ruleset\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built nftables configurations\n- `references/declarative-policy.md` — YAML-to-nftables rendering\n\nFile v2.5.0:references/backend-ufw.md\n\n# Backend: UFW (Recommended for Ubuntu / Debian)\n\nUFW (Uncomplicated Firewall) is the easiest path for Ubuntu and Debian. It handles IPv4 and IPv6 together, uses simple commands, and is managed by a systemd service.\n\n## Detection\n\n```bash\nsudo ufw status\n# \"Status: active\" or \"Status: inactive\"\n```\n\n## Prerequisites\n\n- UFW must be installed but inactive (or with known rules).\n- No other frontend (firewalld) must be active.\n- Never modify iptables/nftables directly when ufw owns the policy.\n\n## Apply: Idempotent Rules\n\n### Step 1: Default Policies\n\n```bash\n# Idempotent — safe to repeat\nsudo ufw --dry-run default deny incoming\nsudo ufw --dry-run default allow outgoing\n\n# Apply\nsudo ufw default deny incoming\nsudo ufw default allow outgoing\n```\n\n### Step 2: SSH (Detect Real Port)\n\n```bash\nSSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk -F: '{print $NF}' | head -1)\nSSH_PORT=${SSH_PORT:-22}\n\n# Idempotent — check before add, exact port match\nsudo ufw status | awk '{print $1}' | grep -qx \"${SSH_PORT}/tcp\" || sudo ufw allow \"${SSH_PORT}/tcp\"\n```\n\n### Step 3: Service Rules\n\n```bash\n# HTTP / HTTPS (uses exact port match to avoid matching 8080 when checking 80)\nsudo ufw status | awk '{print $1}' | grep -qx \"80/tcp\"  || sudo ufw allow 80/tcp\nsudo ufw status | awk '{print $1}' | grep -qx \"443/tcp\" || sudo ufw allow 443/tcp\n\n# Custom ports (same pattern)\n# sudo ufw status | awk '{print $1}' | grep -qx \"8080/tcp\" || sudo ufw allow 8080/tcp\n```\n\n### Step 4: Enable\n\n```bash\n# --force prevents interactive prompt during automation\nsudo ufw --force enable\n```\n\n### Step 5: Verify\n\n```bash\nsudo ufw status verbose\nsudo ufw status numbered\n```\n\n## Outbound Policy\n\nUFW defaults to `allow outgoing`. For high-security environments:\n\n```bash\n# WARNING: restrict OUTPUT only after inventorying all outbound dependencies\nsudo ufw default deny outgoing\nsudo ufw allow out 53/udp      # DNS\nsudo ufw allow out 123/udp     # NTP\nsudo ufw allow out 80/tcp\nsudo ufw allow out 443/tcp\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built UFW configurations\n- `references/declarative-policy.md` — YAML-to-UFW rendering\n\nFile v2.5.0:references/compliance.md\n\n# Compliance Mapping\n\nThis skill's hardening practices map to common security standards. Use for audit evidence.\n\n| Control | CIS Benchmark | PCI-DSS | SOC2 | How This Skill Relates |\n|---------|--------------|---------|------|--------------------------|\n| Default deny inbound | CIS 3.5.1.x | Req 1.3 | CC6.1 | Automated detection and enforcement |\n| Restrict outbound | CIS 3.5.2.x | Req 1.3 | CC6.1 | Supports evidence collection and rule generation |\n| Disable unused ports | CIS 2.1.x | Req 2.2 | CC6.2 | Exposure analysis mapping |\n| Rate-limit SSH | CIS 5.2.x | Req 1.2 | CC6.1 | Idempotent rate-limit patterns |\n| Backup before change | CIS 3.4 | Req 12.10 | CC7.2 | Automated backup and rollback |\n| IPv6 symmetric policy | CIS 3.5.3.x | Req 1.3 | CC6.1 | IPv4/IPv6 symmetry validation |\n| Log dropped packets | CIS 3.5.1.x | Req 10.2 | CC7.2 | Rate-limited logging |\n| fail2ban / intrusion prev | CIS 5.2.x | Req 1.2 | CC6.1 | Backend alignment and verification |\n| Docker port exposure | CIS 4.1.x | Req 1.3 | CC6.1 | DOCKER-USER chain enforcement |\n| Kernel hardening | CIS 3.3.x | Req 2.2 | CC6.1 | Guard against CNI mutations |\n\n**Note**: Specific control numbers vary by benchmark version (CIS v2.x, v3.x). Always reference the latest version applicable to your OS.\n\nFile v2.5.0:references/declarative-policy.md\n\n# Declarative Firewall Policy\n\nThis document defines a machine-readable YAML schema for expressing firewall policy independent of the backend implementation. AI agents and CI/CD pipelines can use this schema to generate backend-specific configurations for ufw, firewalld, nftables, and iptables.\n\n## Design Principles\n\n1. **Backend-agnostic**: The policy describes intent, not implementation.\n2. **Idempotent by design**: Rendering the same policy twice produces the same ruleset.\n3. **Atomic application**: Rendered output uses atomic replace (`iptables-restore`, `nft -f`) where possible.\n4. **Validation-integrated**: The policy includes success criteria that the verifier uses.\n5. **Fail hard on unknown fields**: Unrecognized schema keys or unsupported feature/backend combinations produce an error, not silent ignorance. This prevents the dangerous assumption that a field was processed when it wasn't.\n\n## Schema Version\n\nCurrent version: **2.0**. Renderers must refuse to process policies with a higher major version.\n\n| Version | Changes |\n|---------|---------|\n| 1.0 | Initial schema (deprecated) |\n| 2.0 | Added `schema_version`, `metadata` block. Hard-fail on unknown fields. Added `backend_compat` field-level hints. |\n\n## JSON Schema\n\nA formal JSON Schema for validation is available at `references/policy-schema.json`. Renderers should validate policy input against this schema before rendering.\n\n## Schema\n\n```yaml\nschema_version: \"2.0\"          # Required. Must be exactly \"2.0\".\nmetadata:                       # Optional. Tagging and auditing.\n  name: \"web-server-policy\"\n  description: \"Public web server firewall policy\"\n  author: \"ops-team\"\n  last_modified: \"2026-05-11\"\n\nbackend: auto                   # Optional. auto | ufw | firewalld | nftables | iptables\n                                # \"auto\" detects and picks the best available backend.\n                                # Explicit backend restricts rendering to that target.\n\ninbound:\n  default: deny                 # Required. deny | accept\n  rules:\n    - proto: tcp                # tcp | udp | icmp | icmpv6 | any\n      port: 22                  # integer or \"any\"\n      action: accept            # accept | drop | reject\n      source: any               # any | CIDR | [CIDR, CIDR]\n      rate_limit: \"10/min\"      # Optional. e.g. \"10/min\", \"3/sec\"\n      comment: \"SSH access\"     # Optional. Maps to log prefix or rule comment\n      backend_compat:           # Optional. Override which backends support this rule.\n        ufw: true               # Default: true for all. Set false to skip.\n        firewalld: true\n        nftables: true\n        iptables: true\n\n    - proto: tcp\n      port: 443\n      action: accept\n      source: any\n\n    - proto: udp\n      port: 53\n      action: accept\n      source: \"10.0.0.0/8\"\n\noutbound:\n  default: accept               # Required. deny | accept\n  rules: []                     # Optional. Same structure as inbound\n\nforward:\n  default: deny                 # Required. deny | accept\n\nicmp:\n  ipv4: accept                  # accept | deny\n  ipv6: accept                  # accept | deny\n\nlogging:\n  dropped: true                 # Log dropped packets\n  rate_limit: \"5/sec\"           # Prevent log flood\n  prefix: \"fw-drop\"             # Log prefix string\n\nvalidation:\n  ssh_must_reachable: true\n  intended_ports_only: true\n  ipv6_symmetric: true\n  rules_persist_after_reboot: true\n```\n\n## Field Reference\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `schema_version` | string | Yes | Schema version. Current: `\"2.0\"` |\n| `metadata` | map | No | Human-readable tagging (name, description, author, last_modified) |\n| `backend` | enum | No | Target backend: `auto`, `ufw`, `firewalld`, `nftables`, `iptables`. Default: `auto` |\n| `inbound.default` | enum | Yes | `deny` or `accept` |\n| `inbound.rules` | list | No | Ordered list of allow/deny rules |\n| `outbound.default` | enum | Yes | `deny` or `accept` |\n| `outbound.rules` | list | No | Ordered list of allow/deny rules |\n| `forward.default` | enum | Yes | `deny` or `accept` |\n| `icmp.ipv4` | enum | Yes | `accept` or `deny` |\n| `icmp.ipv6` | enum | Yes | `accept` or `deny` |\n| `logging.dropped` | bool | No | Whether to log dropped packets |\n| `logging.rate_limit` | string | No | Rate limit for log entries |\n| `logging.prefix` | string | No | Prefix string for log lines |\n| `validation` | map | No | Success criteria flags |\n\n## Rule Object\n\n```yaml\n- proto: tcp              # Required: tcp | udp | icmp | icmpv6 | any\n  port: 80                # Required for tcp/udp: integer or \"any\"\n  action: accept          # Required: accept | drop | reject\n  source: any             # Optional: any (default) | CIDR | list of CIDRs\n  rate_limit: \"10/min\"    # Optional: rate string\n  comment: \"HTTP\"         # Optional: human description\n  backend_compat:         # Optional: per-backend enable/disable\n    ufw: true\n    firewalld: false      # Skip this rule on firewalld\n    nftables: true\n    iptables: true\n```\n\n## Backend Feature Differences\n\nNot all features are available on all backends. The renderer must hard-fail when a requested feature is unsupported for the target backend, rather than silently ignoring it.\n\n| Feature | ufw | firewalld | nftables | iptables |\n|---------|-----|-----------|----------|----------|\n| Rate limiting | Yes (ufw limit) | Yes (rich rule) | Yes (limit rate) | Yes (recent + hashlimit) |\n| Source CIDR restriction | Yes | Yes | Yes | Yes |\n| Source range (list of CIDRs) | Yes | Yes (multiple rich rules) | Yes (concatenated) | Yes (multiple rules) |\n| Logging dropped packets | Yes (built-in) | Yes (--set-log-denied) | Yes (log statement) | Yes (LOG target) |\n| Custom log prefix | No | No | Yes | Yes |\n| IPv6 (same ruleset as IPv4) | Yes | Yes (separate zone) | Yes (inet family) | No (separate ip6tables) |\n| Sets/maps for port groups | No | No | Yes | No (use multiport) |\n| Connection tracking state | No (implicit) | Yes (rich rule) | Yes (ct state) | Yes (conntrack) |\n| Atomic ruleset replacement | No | Yes (--reload) | Yes (nft -f) | Yes (iptables-restore) |\n| Zone-based rules | No | Yes | No | No |\n\n## Validation Rules\n\nRenderers MUST apply these validation rules before rendering:\n\n1. **Unknown top-level keys**: If the policy contains keys not in the schema, fail with error listing the unknown keys.\n2. **Unknown rule fields**: If a rule object contains fields not in the rule schema, fail.\n3. **Unsupported feature for target backend**: If a rule uses a feature (e.g., custom log prefix) and `backend_compat` is not explicitly set to `false` for that backend, fail with a message like: \"log_prefix is not supported on ufw. Set `backend_compat.ufw: false` to skip this rule, or remove the unsupported field.\"\n4. **Missing required fields**: Fail if `inbound.default`, `outbound.default`, `forward.default`, `icmp.ipv4`, or `icmp.ipv6` are missing.\n5. **Invalid action values**: Fail if `action` is not one of `accept`, `drop`, `reject`.\n6. **Schema version mismatch**: Fail if `schema_version` major version is higher than the renderer supports.\n\n## Rendering to Backends\n\n### ufw Renderer\n\n```bash\n# Generated from policy inbound.default = deny\nsudo ufw --force default deny incoming\nsudo ufw --force default allow outgoing\n\n# Rule: proto=tcp port=22 action=accept\nsudo ufw allow 22/tcp\n\n# Rule: proto=tcp port=22 rate_limit=\"10/min\"\nsudo ufw limit 22/tcp\n\n# Rule: proto=tcp port=443 action=accept\nsudo ufw allow 443/tcp\n\n# Rule: proto=udp port=53 source=\"10.0.0.0/8\"\nsudo ufw allow from 10.0.0.0/8 to any port 53 proto udp\n```\n\n### firewalld Renderer\n\n```bash\nZONE=$(sudo firewall-cmd --get-default-zone)\n\n# Service rules map to --add-service if known, else --add-port\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=ssh\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=https\n\n# Rate limit maps to rich rule\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule service name=ssh limit value=10/min accept'\n\n# Source restriction maps to rich rule\nsudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule family=ipv4 source address=10.0.0.0/8 port port=53 protocol=udp accept'\n\nsudo firewall-cmd --reload\n```\n\n### nftables Renderer\n\n```nft\n#!/usr/sbin/nft -f\n\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 443 }\n    }\n\n    chain input {\n        type filter hook input priority 0; policy drop;\n\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n\n        # Source-restricted UDP\n        ip saddr 10.0.0.0/8 udp dport 53 accept\n\n        # Allowed TCP ports from set\n        tcp dport @allowed_tcp_ports accept\n\n        # Rate-limited SSH\n        tcp dport 22 ct state new limit rate 10/minute accept\n\n        # Logging\n        log prefix \"fw-drop: \" limit rate 5/second\n        drop\n    }\n\n    chain forward { type filter hook forward priority 0; policy drop; }\n    chain output  { type filter hook output  priority 0; policy accept; }\n}\n```\n\n### iptables Renderer\n\n**IPv4** (`iptables-v4.rules`):\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -s 10.0.0.0/8 -p udp --dport 53 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n**IPv6** (`iptables-v6.rules`):\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p icmpv6 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\nApply:\n```bash\nsudo iptables-restore --test /tmp/iptables-v4.rules && sudo iptables-restore /tmp/iptables-v4.rules\nsudo ip6tables-restore --test /tmp/iptables-v6.rules && sudo ip6tables-restore /tmp/iptables-v6.rules\n```\n\n## Golden Test Fixtures\n\nThese test fixtures verify that the policy renderer produces the expected output for each backend. Run these after any renderer change.\n\n### Fixture: public-web-server -> ufw\n\n**Input** (`fixtures/public-web-server.yaml`):\n```yaml\nschema_version: \"2.0\"\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"10/min\"\n    - proto: tcp\n      port: 80\n      action: accept\n    - proto: tcp\n      port: 443\n      action: accept\noutbound:\n  default: accept\nforward:\n  default: deny\nicmp:\n  ipv4: accept\n  ipv6: accept\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n```\n\n**Expected ufw output** (`fixtures/public-web-server.ufw.expected`):\n```\nsudo ufw --force default deny incoming\nsudo ufw --force default allow outgoing\nsudo ufw limit 22/tcp\nsudo ufw allow 80/tcp\nsudo ufw allow 443/tcp\nsudo ufw --force enable\n```\n\n### Fixture: public-web-server -> nftables\n\n**Expected nftables output** (`fixtures/public-web-server.nftables.expected`):\n```nft\n#!/usr/sbin/nft -f\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 80, 443 }\n    }\n    chain input {\n        type filter hook input priority 0; policy drop\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n        tcp dport @allowed_tcp_ports accept\n        tcp dport 22 ct state new limit rate 10/minute accept\n        log prefix \"fw-drop: \" limit rate 5/second\n        drop\n    }\n    chain forward { type filter hook forward priority 0; policy drop }\n    chain output { type filter hook output priority 0; policy accept }\n}\n```\n\n### Fixture: bastion-host -> iptables (v4)\n\n**Input** (`fixtures/bastion-host.yaml`):\n```yaml\nschema_version: \"2.0\"\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"5/min\"\noutbound:\n  default: accept\nforward:\n  default: deny\nicmp:\n  ipv4: accept\n  ipv6: accept\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n  prefix: \"bastion-drop\"\n```\n\n**Expected iptables v4 output** (`fixtures/bastion-host.iptables-v4.expected`):\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set\n-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 6 -j DROP\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -m limit --limit 5/sec -j LOG --log-prefix \"bastion-drop: \"\nCOMMIT\n```\n\n## Example Policies\n\n### Public Web Server (v2.0)\n\n```yaml\nschema_version: \"2.0\"\nmetadata:\n  name: \"public-web-server\"\n  description: \"Standard web server with SSH, HTTP, HTTPS\"\n\nbackend: auto\n\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"10/min\"\n      comment: \"SSH rate-limited\"\n    - proto: tcp\n      port: 80\n      action: accept\n    - proto: tcp\n      port: 443\n      action: accept\n\noutbound:\n  default: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n  prefix: \"fw-drop\"\n```\n\n### Bastion Host (v2.0)\n\n```yaml\nschema_version: \"2.0\"\nmetadata:\n  name: \"bastion-host\"\n  description: \"SSH-only jump box with aggressive rate limiting\"\n\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      rate_limit: \"5/min\"\n\noutbound:\n  default: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n\nlogging:\n  dropped: true\n  rate_limit: \"5/sec\"\n```\n\n### Internal Database (v2.0)\n\n```yaml\nschema_version: \"2.0\"\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      source: \"10.0.1.0/24\"\n    - proto: tcp\n      port: 3306\n      action: accept\n      source: \"10.0.2.0/24\"\n\noutbound:\n  default: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n```\n\n### Zero Trust Node (v2.0)\n\n```yaml\nschema_version: \"2.0\"\nmetadata:\n  name: \"zero-trust-node\"\n  description: \"Default deny all inbound and outbound with explicit allowlist\"\n\ninbound:\n  default: deny\n  rules:\n    - proto: tcp\n      port: 22\n      action: accept\n      source: \"10.0.1.0/24\"\n\noutbound:\n  default: deny\n  rules:\n    - proto: udp\n      port: 53\n      action: accept\n    - proto: udp\n      port: 123\n      action: accept\n    - proto: tcp\n      port: 80\n      action: accept\n    - proto: tcp\n      port: 443\n      action: accept\n\nforward:\n  default: deny\n\nicmp:\n  ipv4: accept\n  ipv6: accept\n```\n\n## Future Enhancements\n\n- GeoIP restrictions (`source_geo: [\"US\", \"CA\"]`)\n- Time-based rules (`time_range: \"09:00-17:00\"`)\n- Connection limits (`max_connections: 100`)\n- Custom chains and forwarding rules for complex topologies\n- Integration with cloud security group APIs (AWS, GCP, Azure)\n- Policy diff tool (compare two policies, show backend-specific rule differences)\n- `hermes skill render-policy` CLI command to validate and render a policy file\n\nFile v2.5.0:references/docker-hardening.md\n\n# Docker Firewall Hardening\n\nDocker manipulates iptables/nftables directly. By default, `dockerd` inserts rules **above** ufw in the FORWARD chain, meaning `docker run -p` exposes containers to the world even when ufw INPUT policy is DROP.\n\n## Detection\n\n```bash\ndocker ps --format \"table {{.Names}}\\t{{.Ports}}\"\nsudo iptables -L DOCKER -n -v 2>/dev/null     # Docker-managed NAT rules\nsudo iptables -L DOCKER-USER -n -v 2>/dev/null # User chain (may be empty)\n\n# Check for unintended 0.0.0.0 exposure\ndocker inspect $(docker ps -q) --format '{{.Name}}: {{range $p, $c := .NetworkSettings.Ports}}{{$p}} -> {{(index $c 0).HostIp}}:{{(index $c 0).HostPort}} {{end}}' 2>/dev/null | grep \"0.0.0.0\"\n```\n\n## Key Principle\n\nDocker manages its own chains (DOCKER, DOCKER-ISOLATION-STAGE-\\*) for container NAT and inter-container communication. **Do NOT modify those chains directly.** Use the DOCKER-USER chain which Docker guarantees it will never touch.\n\n## Mitigation Options\n\n### Option A: DOCKER-USER Chain (RECOMMENDED)\n\nThe DOCKER-USER chain is evaluated BEFORE Docker's own rules but only affects the FORWARD chain. Use it to restrict which external interfaces can reach published ports:\n\n```bash\n# Block all external access to published ports on eth0 (public interface)\nsudo iptables -C DOCKER-USER -i eth0 -j DROP 2>/dev/null || \\\n  sudo iptables -I DOCKER-USER 1 -i eth0 -j DROP\n\n# Allow external HTTPS to reach containers\nsudo iptables -C DOCKER-USER -i eth0 -p tcp --dport 443 -j ACCEPT 2>/dev/null || \\\n  sudo iptables -I DOCKER-USER 2 -i eth0 -p tcp --dport 443 -j ACCEPT\n\n# Persist DOCKER-USER rules\nsudo iptables-save | grep DOCKER-USER | sudo tee /etc/iptables/docker-user.rules\n```\n\n### Option B: Bind Published Ports to Specific IPs\n\nInstead of `-p 8080:8080` (binds `0.0.0.0`):\n```bash\ndocker run -p 127.0.0.1:8080:8080 myapp   # Loopback only\ndocker run -p 10.0.1.10:8080:8080 myapp    # Internal IP only\n```\n\n### Option C: Disable Docker's iptables Management\n\nEdit `/etc/docker/daemon.json`:\n```json\n{ \"iptables\": false }\n```\nThen `sudo systemctl restart docker`.\n\n**WARNING**: Makes you fully responsible for all NAT, port mapping, inter-container communication, and outbound masquerading rules.\n\n## Docker + nftables Hosts\n\nOn hosts where nftables is the primary backend but Docker still uses iptables (legacy mode), the two systems coexist but do not share state. Docker's iptables rules are invisible to `nft list ruleset`. Always check both:\n```bash\nsudo iptables -L -n -v | grep -i docker\nsudo nft list ruleset | grep -i docker  # will likely be empty\n```\n\nFile v2.5.0:references/k8s-policy.md\n\n# Kubernetes Node Firewall Policy\n\n**CRITICAL**: Kubernetes nodes manage their own netfilter rules through the CNI plugin and kube-proxy. Manual host firewall changes can break pod networking, Service load balancing, and NetworkPolicy enforcement.\n\n## Detection\n\n```bash\n# Is this a K8s node?\n[[ -f /etc/kubernetes/kubelet.conf ]] && echo \"Control plane or worker node\"\n[[ -d /etc/cni/net.d ]] && echo \"CNI configuration present\"\n\n# Detect CNI plugin\nls /etc/cni/net.d/ 2>/dev/null\nps aux | grep -E \"cilium|calico|flannel|kube-proxy\" | grep -v grep\n\n# Check kube-proxy mode\nkubectl get configmap kube-proxy -n kube-system -o yaml 2>/dev/null | grep -i mode\ncurl -s http://localhost:10249/proxyMode 2>/dev/null\n```\n\n## CNI-Specific Guidance\n\n| CNI | Dataplane | Action |\n|-----|----------|--------|\n| **Cilium** | eBPF (below netfilter) | Do NOT modify host netfilter. Use CiliumNetworkPolicy. |\n| **Calico (eBPF)** | eBPF | Do NOT modify. Use Calico GlobalNetworkPolicy. |\n| **Calico (iptables)** | iptables | Never flush. Use NetworkPolicy. |\n| **Flannel** | iptables MASQUERADE | Never flush. Preserve MASQUERADE rules. |\n| **kube-proxy (iptables)** | iptables Service DNAT | Never flush. |\n| **kube-proxy (ipvs)** | IPVS + auxiliary iptables | Never flush iptables. |\n| **kube-proxy (nftables)** | nftables Services | Never flush ruleset. |\n\n## Required Policy: AUDIT-ONLY Mode\n\nOn any detected Kubernetes node, default to audit-only:\n```bash\nbash scripts/audit-firewall.sh --json\n```\n\nIf `k8s_node: true`, do NOT proceed with host firewall changes. Recommend:\n1. **NetworkPolicies** for pod-to-pod traffic\n2. **Cloud security groups / VPC firewall** for node-level access\n3. Narrow host-level changes ONLY for kubelet API (port 10250) and SSH — and only after staging cluster testing.\n\n## Host Firewall vs NetworkPolicy\n\n| Layer | Managed By | Controls |\n|-------|-----------|---------|\n| Host firewall (this skill) | ufw, firewalld, nftables, iptables | Traffic to the node's own IP |\n| NetworkPolicy | CNI plugin | Traffic between pods |\n| Cloud firewall / SG | Cloud provider API | Traffic entering/exiting VPC |\n\nA properly secured K8s cluster uses **all three layers**.\n\nFile v2.5.0:references/observability.md\n\n# Observability & Performance\n\n## nftables Counters\n\n```bash\n# Per-rule counters and handles\nsudo nft list ruleset -a\nsudo nft list chain inet filter input\n```\n\n## Connection Tracking\n\n```bash\ncat /proc/sys/net/netfilter/nf_conntrack_count\ncat /proc/sys/net/netfilter/nf_conntrack_max\n\nsudo conntrack -L 2>/dev/null | wc -l\n\necho \"Conntrack usage: $(cat /proc/sys/net/netfilter/nf_conntrack_count) / $(cat /proc/sys/net/netfilter/nf_conntrack_max)\"\n```\n\n## Dropped Packet Monitoring\n\n```bash\n# ufw\nsudo tail -f /var/log/ufw.log\n\n# iptables LOG target\nsudo dmesg | grep -i \"iptables\\\\|nf_log\"\n\n# nftables log prefix\nsudo journalctl -k -f | grep \"nft-drop\"\n```\n\n## fail2ban Metrics\n\n```bash\nsudo fail2ban-client status\nsudo fail2ban-client status sshd\ngrep \"Ban\" /var/log/fail2ban.log 2>/dev/null | tail -20\n```\n\n## Recommended Baselines\n\nRecord after hardening:\n- `nf_conntrack_count` at idle and peak load\n- `nft list ruleset` counter values after 24h\n- UFW log volume per hour\n- fail2ban ban rate per day\n\n## Performance Risks\n\n| Risk | Symptom | Mitigation |\n|------|---------|------------|\n| **conntrack exhaustion** | `nf_conntrack: table full` | Increase max or use stateless rules |\n| **logging flood** | syslog high CPU, disk full | Always `limit rate` on log rules |\n| **huge ipsets** | Slow rule evaluation | Use `flags interval`, split sets |\n| **SYN flood** | High half-open connections | Enable `tcp_syncookies`, use SYNPROXY |\n| **rule evaluation order** | High CPU per packet | Place most-hit rules early |\n\n## Kernel Tuning\n\n```bash\n# Enable reverse path filtering\nsudo sysctl -w net.ipv4.conf.all.rp_filter=1\n# Enable SYN cookies\nsudo sysctl -w net.ipv4.tcp_syncookies=1\n# Ignore redirects\nsudo sysctl -w net.ipv4.conf.all.accept_redirects=0\nsudo sysctl -w net.ipv6.conf.all.accept_redirects=0\n# Persist\nsudo tee -a /etc/sysctl.d/99-firewall-hardening.conf << 'EOF'\nnet.ipv4.conf.all.rp_filter=1\nnet.ipv4.conf.default.rp_filter=1\nnet.ipv4.tcp_syncookies=1\nnet.ipv4.conf.all.accept_redirects=0\nnet.ipv6.conf.all.accept_redirects=0\nnet.ipv4.icmp_echo_ignore_broadcasts=1\nEOF\nsudo sysctl --system\n```\n\n> **Warning**: sysctl changes affect the entire TCP/IP stack. Test on non-production first. Never change sysctl hardening values on K8s/CNI hosts without explicit CNI profile support.\n\nFile v2.5.0:references/policy-schema.json\n\n{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"https://hermes-agent.nousresearch.com/skills/linux-firewall-hardening/policy-schema.json\",\n  \"title\": \"Declarative Firewall Policy\",\n  \"description\": \"JSON Schema for the declarative firewall policy YAML format (v2.0). Renderers should validate policy input against this schema before rendering.\",\n  \"type\": \"object\",\n  \"required\": [\"schema_version\", \"inbound\", \"outbound\", \"forward\", \"icmp\"],\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"schema_version\": {\n      \"type\": \"string\",\n      \"enum\": [\"2.0\"],\n      \"description\": \"Schema version. Must be exactly '2.0'.\"\n    },\n    \"metadata\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"name\": { \"type\": \"string\" },\n        \"description\": { \"type\": \"string\" },\n        \"author\": { \"type\": \"string\" },\n        \"last_modified\": { \"type\": \"string\", \"format\": \"date\" }\n      }\n    },\n    \"backend\": {\n      \"type\": \"string\",\n      \"enum\": [\"auto\", \"ufw\", \"firewalld\", \"nftables\", \"iptables\"],\n      \"default\": \"auto\"\n    },\n    \"inbound\": {\n      \"$ref\": \"#/$defs/chainPolicy\"\n    },\n    \"outbound\": {\n      \"$ref\": \"#/$defs/chainPolicy\"\n    },\n    \"forward\": {\n      \"type\": \"object\",\n      \"required\": [\"default\"],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"default\": { \"type\": \"string\", \"enum\": [\"deny\", \"accept\"] }\n      }\n    },\n    \"icmp\": {\n      \"type\": \"object\",\n      \"required\": [\"ipv4\", \"ipv6\"],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"ipv4\": { \"type\": \"string\", \"enum\": [\"accept\", \"deny\"] },\n        \"ipv6\": { \"type\": \"string\", \"enum\": [\"accept\", \"deny\"] }\n      }\n    },\n    \"logging\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"dropped\": { \"type\": \"boolean\" },\n        \"rate_limit\": { \"type\": \"string\", \"pattern\": \"^\\\\d+/sec(ond)?$|^\\\\d+/min(ute)?$\" },\n        \"prefix\": { \"type\": \"string\", \"maxLength\": 32 }\n      }\n    },\n    \"validation\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"ssh_must_reachable\": { \"type\": \"boolean\" },\n        \"intended_ports_only\": { \"type\": \"boolean\" },\n        \"ipv6_symmetric\": { \"type\": \"boolean\" },\n        \"rules_persist_after_reboot\": { \"type\": \"boolean\" }\n      }\n    }\n  },\n  \"$defs\": {\n    \"chainPolicy\": {\n      \"type\": \"object\",\n      \"required\": [\"default\"],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"default\": { \"type\": \"string\", \"enum\": [\"deny\", \"accept\"] },\n        \"rules\": {\n          \"type\": \"array\",\n          \"items\": { \"$ref\": \"#/$defs/rule\" }\n        }\n      }\n    },\n    \"rule\": {\n      \"type\": \"object\",\n      \"required\": [\"proto\", \"port\", \"action\"],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"proto\": {\n          \"type\": \"string\",\n          \"enum\": [\"tcp\", \"udp\", \"icmp\", \"icmpv6\", \"any\"]\n        },\n        \"port\": {\n          \"oneOf\": [\n            { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 65535 },\n            { \"type\": \"string\", \"const\": \"any\" }\n          ]\n        },\n        \"action\": {\n          \"type\": \"string\",\n          \"enum\": [\"accept\", \"drop\", \"reject\"]\n        },\n        \"source\": {\n          \"oneOf\": [\n            { \"type\": \"string\", \"const\": \"any\" },\n            { \"type\": \"string\", \"format\": \"cidr\" },\n            { \"type\": \"array\", \"items\": { \"type\": \"string\", \"format\": \"cidr\" } }\n          ],\n          \"default\": \"any\"\n        },\n        \"rate_limit\": {\n          \"type\": \"string\",\n          \"pattern\": \"^\\\\d+/(sec(ond)?|min(ute)?|hour)$\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"maxLength\": 256\n        },\n        \"backend_compat\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"ufw\": { \"type\": \"boolean\" },\n            \"firewalld\": { \"type\": \"boolean\" },\n            \"nftables\": { \"type\": \"boolean\" },\n            \"iptables\": { \"type\": \"boolean\" }\n          }\n        }\n      }\n    }\n  }\n}\n\nArchive v2.2.0: 21 files, 48663 bytes\n\nFiles: PUBLISH.md (17600b), references/backend-firewalld.md (2505b), references/backend-iptables.md (2294b), references/backend-nftables.md (2614b), references/backend-ufw.md (2130b), references/compliance.md (1281b), references/declarative-policy.md (15127b), references/docker-hardening.md (2578b), references/k8s-policy.md (2181b), references/observability.md (2299b), references/policy-schema.json (4066b), references/recovery.md (2031b), references/remaining-improvements.md (6206b), references/security-profiles.md (11431b), references/special-environments.md (6529b), scripts/audit-firewall.sh (11284b), scripts/firewall-plan.sh (4795b), scripts/firewall-verify.sh (2915b), skill-card.md (3185b), SKILL.md (19478b), _meta.json (143b)\n\nFile v2.2.0:SKILL.md\n\n---\nname: linux-firewall-hardening\ntitle: Linux Firewall Hardening\ndescription: 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.\nlicense: Dual MIT / Apache-2.0\nskill_version: 2.2.0\nschema_version: 2\ntags: [security, firewall, ufw, iptables, nftables, firewalld, hardening, docker, fail2ban, policy-as-code, devsecops, ipv6]\n---\n\n# Linux Firewall Hardening\n\n## When to Use\n\n- Check if a Linux server has active firewall protection.\n- Enable and configure a firewall without locking yourself out of SSH.\n- Audit existing rules, troubleshoot connectivity, or apply a security profile.\n- Automate firewall hardening via an AI agent or CI/CD pipeline.\n\n## When NOT to Use\n\n| Condition | Alternative |\n|-----------|-------------|\n| Kubernetes worker node | Use NetworkPolicies / CiliumNetworkPolicy |\n| Firewall managed by Terraform/Ansible/Puppet/Chef | Update IaC source of truth |\n| Cloud workload with Security Group / NSG only | Use cloud provider's firewall API |\n| Inside a container | Escalate to host operator |\n| WSL2, macOS, or shared/managed hosting | See `references/special-environments.md` |\n\n> **Support files**: `scripts/audit-firewall.sh` (run first), `scripts/firewall-plan.sh` (dry-run), `scripts/firewall-verify.sh` (post-apply).\n> Detailed backend guides, Docker/K8s policies, observability, compliance, and recovery are in `references/`.\n\n## 🚨 Emergency: I'm Locked Out — What Now?\n\nIf you just applied firewall rules and lost SSH connectivity:\n\n1. **Wait 5 minutes** — the auto-rollback timer (scheduled during VALIDATE) will restore access. Don't panic and don't take destructive actions.\n2. **Use your second SSH session** — if you opened one (pre-flight checklist), switch to it and fix the rules manually.\n3. **Cloud serial console** — AWS EC2 Serial Console, GCP Serial Port, Azure Serial Console, or hypervisor VNC/IPMI/iDRAC.\n4. **Restore from backup via console** — once connected: `sudo iptables-restore < ~/firewall-backup-*/iptables-v4.rules`\n5. **Emergency ACCEPT (LAST RESORT)** — `sudo iptables -P INPUT ACCEPT; sudo iptables -F; sudo ufw disable`. This exposes the host completely. Re-harden immediately.\n\nFull procedures: `references/recovery.md`.\n\n---\n\n## Prerequisites\n\n- Root or sudo access.\n- An active SSH session (risk of lockout).\n- Know which ports your services use.\n\n---\n\n## NEVER DO (14 Rules)\n\n1. **Never flush iptables/nftables on Kubernetes nodes.** CNI plugins manage netfilter.\n2. **Never run `iptables -F` or `nft flush ruleset` without a verified backup.** Docker/K8s networking will break.\n3. **Never disable firewalld and use raw iptables simultaneously.** Undefined behavior.\n4. **Never set `DROP` policy on INPUT before allowing your current SSH port.** Immediate lockout.\n5. **Never disable Docker's `iptables` management without replacement NAT/routing rules.**\n6. **Never restart `networking.service` or `NetworkManager` remotely without console access.**\n7. **Never apply cloud SG and host firewall changes simultaneously without testing.**\n8. **Never enable logging on high-traffic DROP rules without `limit rate`.** Disk flood.\n9. **Never manage nftables/iptables directly when ufw or firewalld owns the policy.** Split-brain state.\n10. **Never apply outbound default-deny without explicitly allowing DNS, NTP, package mirrors.**\n11. **Never restore firewall backups from a different host, kernel version, or backend mode.**\n12. **Never assume IPv4 rules protect IPv6.** Verify both stacks separately.\n13. **Never change sysctl hardening values on K8s/CNI hosts without explicit CNI profile support.**\n14. **Never enable verbose packet logging without rate limits and log rotation.**\n\n---\n\n## State Machine\n\nFollow states in order. Do not skip.\n\n```\nDETECT → SELECT → PLAN → VALIDATE → APPLY → VERIFY\n```\n\n### State: DETECT\n\nRun the audit script:\n\n```bash\nbash scripts/audit-firewall.sh           # Human-readable\nbash scripts/audit-firewall.sh --json    # Machine-readable\n```\n\n**Key outputs**: `confidence`, `risk_tier`, `recommended_backend`, `halt_reasons`, `k8s_node`, `iac_owner`.\n\n### Risk Tiers & Confidence Gating\n\n| Tier | Confidence | Agent Behavior |\n|------|-----------|----------------|\n| `auto` | ≥ 90% | Proceed automatically to PLAN |\n| `confirmed` | 70–89% | Proceed but require human confirmation before APPLY |\n| `manual` | 50–69% | Audit-only mode. Generate recommendations, do not apply. |\n| `halt` | < 50% | Stop immediately. Escalate findings to operator. |\n\n**Additional halt triggers** (regardless of confidence): containerized, K8s node, IaC managed, no rollback mechanism available.\n\n### Decision Tree\n\n| Condition | Path | Detail |\n|-----------|------|--------|\n| Risk tier = `halt` | **STOP** | Resolve blockers first |\n| Inside container | **STOP** | Escalate to host operator |\n| K8s node detected | **STOP** | `references/k8s-policy.md` |\n| Ubuntu/Debian + ufw active | **Phase: UFW** | `references/backend-ufw.md` |\n| ufw + firewalld both active | **STOP** | Resolve conflict |\n| RHEL/Rocky/Alma + firewalld active | **Phase: firewalld** | `references/backend-firewalld.md` |\n| nftables active, no frontend | **Phase: nftables** | `references/backend-nftables.md` |\n| iptables only | **Phase: iptables** | `references/backend-iptables.md` |\n| Docker host | Apply **Docker Hardening** after phase above | `references/docker-hardening.md` |\n\n### Ownership Boundary\n\nBefore 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`.\n\n---\n\n### State: SELECT\n\nOptionally load a pre-built security profile (`references/security-profiles.md`):\n\n| Profile | Use Case |\n|---------|----------|\n| `public-web-server` | Open 22, 80, 443. Rate-limit SSH. |\n| `internal-database` | SSH from mgmt subnet only. DB port from app subnet only. |\n| `bastion-host` | SSH only. Aggressive rate limiting. |\n| `zero-trust-node` | Default deny all inbound and outbound. |\n\nOr use declarative YAML (`references/declarative-policy.md`):\n\n```\nImperative (state machine) → Ad-hoc hardening, incident response\nDeclarative (YAML)        → GitOps, multi-host, reproducible\nMixed                     → YAML as source-of-truth, state machine for verification\n```\n\n---\n\n### State: PLAN\n\nGenerate a dry-run diff before applying:\n\n```bash\nbash scripts/firewall-plan.sh --profile public-web-server\nbash scripts/firewall-plan.sh --ports 22,80,443\nbash scripts/firewall-plan.sh --json     # Machine-readable diff\n```\n\nReview the output. If `risk_tier` is `confirmed`, present the plan and wait for human confirmation before APPLY.\n\n**Plan JSON schema** (machine-readable output):\n\n```json\n{\n  \"backend\": \"ufw\",\n  \"current_state\": \"active\",\n  \"diff\": {\n    \"add\":    [{\"port\": 80, \"proto\": \"tcp\", \"source\": \"any\"}],\n    \"skip\":   [{\"port\": 22, \"proto\": \"tcp\", \"reason\": \"already_exists\"}],\n    \"remove\": []\n  },\n  \"risk_assessment\": \"low\",\n  \"estimated_disruption\": \"none\",\n  \"approval_token\": \"sha256:abc123...\"\n}\n```\n\n**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.\n\n**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.\n\n---\n\n### State: VALIDATE\n\n#### 1. Create Backup (Mandatory)\n\n```bash\nBACKUP_DIR=\"$HOME/firewall-backup-$(date +%Y%m%d-%H%M%S)\"\nmkdir -p \"$BACKUP_DIR\"\n\nsudo iptables-save > \"$BACKUP_DIR/iptables-v4.rules\" 2>/dev/null || true\nsudo ip6tables-save > \"$BACKUP_DIR/iptables-v6.rules\" 2>/dev/null || true\nsudo nft list ruleset > \"$BACKUP_DIR/nftables.rules\" 2>/dev/null || true\nsudo ufw status verbose > \"$BACKUP_DIR/ufw-status.txt\" 2>/dev/null || true\nsudo firewall-cmd --list-all --zone=$(sudo firewall-cmd --get-default-zone) > \"$BACKUP_DIR/firewalld-default.txt\" 2>/dev/null || true\n\necho \"Backup saved to $BACKUP_DIR\"\n```\n\n#### 2. Schedule Rollback (Mandatory for Remote)\n\nThe 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.\n\n```bash\n# Build rollback script from backup dir\nROLLBACK_SCRIPT=$(cat <<'RB'\n#!/bin/bash\nBACKUP_DIR=\"REPLACE_ME\"\n[ -f \"$BACKUP_DIR/iptables-v4.rules\" ] && sudo iptables-restore < \"$BACKUP_DIR/iptables-v4.rules\" || { sudo iptables -P INPUT ACCEPT; sudo iptables -F; }\n[ -f \"$BACKUP_DIR/iptables-v6.rules\" ] && sudo ip6tables-restore < \"$BACKUP_DIR/iptables-v6.rules\" || { sudo ip6tables -P INPUT ACCEPT; sudo ip6tables -F; }\n[ -f \"$BACKUP_DIR/nftables.rules\" ] && sudo nft -f \"$BACKUP_DIR/nftables.rules\" || sudo nft flush ruleset\nsystemctl is-active ufw &>/dev/null && sudo ufw disable\nsudo firewall-cmd --panic-off 2>/dev/null\nRB\n)\nROLLBACK_SCRIPT=\"${ROLLBACK_SCRIPT/REPLACE_ME/$BACKUP_DIR}\"\n\n# Schedule (at preferred, systemd-run fallback)\nif command -v at &>/dev/null; then\n    ROLLBACK_JOB_ID=$(echo \"sudo bash -c '$ROLLBACK_SCRIPT'\" | at now + 5 minutes 2>&1 | grep -oP 'job \\K\\d+')\n    echo \"Rollback scheduled: at job $ROLLBACK_JOB_ID (cancel with: atrm $ROLLBACK_JOB_ID)\"\nelif command -v systemd-run &>/dev/null; then\n    UNIT_NAME=\"firewall-rollback-$$\"\n    echo \"$ROLLBACK_SCRIPT\" > /tmp/firewall-rollback-$$.sh\n    chmod +x /tmp/firewall-rollback-$$.sh\n    systemd-run --on-active=5m --unit=\"$UNIT_NAME\" --user /tmp/firewall-rollback-$$.sh\n    echo \"Rollback scheduled: systemd unit $UNIT_NAME (cancel with: systemctl --user stop $UNIT_NAME)\"\nfi\n```\n\nSee `references/recovery.md` for advanced recovery scenarios.\n\n#### 3. Pre-Flight Checklist\n\n- [ ] Backup created successfully\n- [ ] Rollback scheduled (verify with `atq` or `systemctl --user list-units`)\n- [ ] **Second SSH session open and tested** — open a second terminal, SSH in, and confirm you can run `sudo whoami`. This is your emergency console if the primary session loses connectivity. Keep it open until VERIFY passes. **Why**: existing ESTABLISHED conntrack entries usually keep your current session alive, but if conntrack is flushed or the policy change drops your session silently, this second session is your only way back in.\n- [ ] Real SSH port identified (not assumed to be 22)\n- [ ] Confidence ≥ 70% and risk_tier is `auto` or `confirmed`\n- [ ] Ownership verified — no IaC managing firewall\n- [ ] Change window appropriate (maintenance window or low traffic)\n- [ ] PLAN output reviewed and approved\n\n---\n\n### State: APPLY\n\nAll commands use **idempotent patterns**: check-before-set. Safe to run repeatedly.\n\n| Backend | Pattern |\n|---------|---------|\n| ufw | `sudo ufw status \\| awk '{print $1}' \\| grep -qx \"22/tcp\" \\|\\| sudo ufw allow 22/tcp` |\n| firewalld | `sudo firewall-cmd --query-service=ssh \\|\\| sudo firewall-cmd --permanent --add-service=ssh` |\n| iptables | `sudo iptables -C INPUT -p tcp --dport 22 -j ACCEPT 2>/dev/null \\|\\| sudo iptables -A ...` |\n| nftables | Atomic ruleset: `nft -c -f /etc/nftables.conf.new && nft -f /etc/nftables.conf.new` |\n\nFull step-by-step commands per backend: `references/backend-ufw.md`, `references/backend-firewalld.md`, `references/backend-nftables.md`, `references/backend-iptables.md`.\n\n#### Docker Hosts\n\nDocker bypasses ufw by default. Use DOCKER-USER chain. Full guide: `references/docker-hardening.md`.\n\n#### Kubernetes Nodes\n\n**Default: AUDIT-ONLY**. Never modify host firewall. Full policy: `references/k8s-policy.md`.\n\n---\n\n### State: VERIFY\n\nRun post-hardening checks:\n\n```bash\nbash scripts/firewall-verify.sh\n```\n\n**Success criteria** (all must pass):\n1. SSH remains reachable from current and second session\n2. Only intended ports are externally reachable\n3. Rules survive reboot (verified via service persistence)\n4. IPv6 exposure matches IPv4 policy\n5. Docker-published ports are intentional (no accidental `0.0.0.0`)\n6. fail2ban jails active (if installed) with correct backend\n7. Rollback timer cancelled after successful verification\n\n**Verify behavior contract:**\n- Verify MUST complete within the rollback timer window (default 5 min)\n- If verify times out before completion → timer auto-fires rollback (system-level protection)\n- If verify FAILS but timer was already cancelled → manual rollback from the backup directory. Restore commands (in priority order):\n  1. `sudo iptables-restore < \"$BACKUP_DIR/iptables-v4.rules\"`\n  2. `sudo ip6tables-restore < \"$BACKUP_DIR/iptables-v6.rules\"`\n  3. `sudo nft -f \"$BACKUP_DIR/nftables.rules\"`\n  4. `sudo ufw reset && sudo ufw disable`\n  See `references/recovery.md` for full recovery procedures including emergency ACCEPT fallback.\n- 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\n\n## Exit Codes (Core Contract)\n\n| Code | Meaning | Agent Action |\n|------|---------|-------------|\n| 0 | Success | Continue |\n| 10 | Backend conflict | Halt; resolve manually |\n| 11 | Backend detection failed | Halt; check firewall stack |\n| 12 | Multiple backends active | Halt; resolve conflict |\n| 20 | IaC-managed | Halt; update IaC source |\n| 21 | Inside container | Halt; escalate to host operator |\n| 22 | K8s node detected | Halt; audit-only mode |\n| 30 | Low confidence (<70%) | Drop to audit-only mode |\n| 31 | No rollback capability | Halt; ensure at or systemd-run |\n| 40 | Preflight failed | Halt; check prerequisites |\n| 41 | Plan approval mismatch | Halt; re-run PLAN with approval |\n| 42 | Backup failed | Halt; resolve disk/permissions |\n| 50 | Apply failed | Auto-rollback triggered |\n| 51 | Apply partial | Auto-rollback triggered; verify backup |\n| 60 | Verify failed | Auto-rollback triggered |\n| 61 | State file conflict | Abort; resolve stale state |\n\n---\n\n## fail2ban Integration\n\nIf fail2ban is installed:\n\n| Host Firewall | Recommended `backend` |\n|--------------|----------------------|\n| ufw | `ufw` or `systemd` |\n| firewalld | `firewalld` |\n| nftables | `nftables` |\n| iptables | `auto` (default) |\n\nAfter changing backend: `sudo fail2ban-client restart && sudo fail2ban-client status sshd`.\n\n---\n\n## Recovery\n\nIf you lose connectivity, priority order:\n1. Wait for auto-rollback (scheduled during VALIDATE)\n2. Use second SSH session\n3. Cloud serial console / hypervisor console\n4. Restore from backup\n5. Emergency ACCEPT (last resort — exposes host completely)\n\nFull procedures: `references/recovery.md`.\n\n## State Persistence & Interrupt-Resume\n\nFor 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:\n\n```bash\nSTATE_DIR=\"$HOME/.firewall-hardening\"\nSTATE_FILE=\"$STATE_DIR/state.json\"\n```\n\n**State file structure:**\n\n```json\n{\n  \"state\": \"validate\",\n  \"started_at\": \"2026-05-11T16:00:00Z\",\n  \"backend\": \"ufw\",\n  \"risk_tier\": \"auto\",\n  \"backup_dir\": \"/home/user/firewall-backup-20260511-160000\",\n  \"rollback_timer_id\": \"firewall-rollback-12345\",\n  \"plan_hash\": \"sha256:abc123...\"\n}\n```\n\n**Resume logic:**\n- If `state.json` exists and `started_at` is within 1 hour → resume from that state\n- If `state.json` is stale (>1 hour) → delete it and start fresh from Detect\n- The file is advisory-only; agent can always restart from Detect\n\n> State persistence is optional. The skill defaults to restarting from Detect each run. Enable by creating `$STATE_DIR` before starting.\n\n---\n\n## Cloud Security Group Reminder\n\nThe host firewall is your **second** layer. Verify cloud SGs are aligned:\n\n| Cloud | Outer Firewall |\n|-------|---------------|\n| AWS | Security Groups |\n| GCP | VPC Firewall Rules |\n| Azure | Network Security Groups |\n| DigitalOcean/Linode/Vultr | Cloud Firewall |\n\n## Compatibility Matrix\n\n| Distro/Env | ufw | firewalld | nftables | iptables | Coverage |\n|------------|-----|-----------|----------|----------|----------|\n| Ubuntu 22.04/24.04 | Primary | — | Backend | Fallback | Full |\n| Debian 12 | Primary | — | Backend | Fallback | Full |\n| RHEL 9 | — | Primary | Native | Backend | Full |\n| Rocky/Alma 9 | — | Primary | Native | Backend | Full |\n| Fedora 40+ | — | Primary | Native | Backend | Partial |\n| Alpine 3.18+ | — | — | Native | Fallback | Partial |\n| Arch | — | — | Native | Fallback | Community |\n| Docker host | ✅ DOCKER-USER chain | ✅ `docker-hardening.md` | ✅ `docker-hardening.md` | ✅ `docker-hardening.md` | Full |\n| LXC/LXD container | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | Partial |\n| systemd-nspawn | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | Partial |\n| WSL2 | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | None |\n\n> 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`.\n\n---\n\n## Observability\n\nEstablish baselines after hardening: conntrack usage, dropped packet rates, fail2ban ban rate. Monitor for anomalies. Full guide: `references/observability.md`.\n\n## Compliance\n\nPractices map to CIS, PCI-DSS, and SOC2 controls. Full mapping: `references/compliance.md`.\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Audit environment | `bash scripts/audit-firewall.sh --json` |\n| Plan changes | `bash scripts/firewall-plan.sh --profile web` |\n| Verify after apply | `bash scripts/firewall-verify.sh` |\n| Allow port (ufw, idempotent) | `sudo ufw status \\| awk '{print $1}' \\| grep -qx \"80/tcp\" \\|\\| sudo ufw allow 80/tcp` |\n| View ufw rules | `sudo ufw status numbered` |\n| View nft rules | `sudo nft list ruleset` |\n| View iptables rules | `sudo iptables -L -n -v` |\n| View ip6tables rules | `sudo ip6tables -L -n -v` |\n| Atomic iptables replace | `sudo iptables-restore < /tmp/rules.v4` |\n| Dry-run nftables | `sudo nft -c -f /etc/nftables.conf` |\n| Backup rules | `sudo iptables-save > ~/iptables.backup` |\n| fail2ban status | `sudo fail2ban-client status sshd` |\n| Cancel rollback (at) | `atrm <jobid>` |\n| Cancel rollback (systemd-run) | `systemctl --user stop firewall-rollback-<pid>` |\n\n## See Also\n\n- `references/backend-ufw.md` — Full UFW phase\n- `references/backend-firewalld.md` — Full firewalld phase\n- `references/backend-nftables.md` — Full nftables phase\n- `references/backend-iptables.md` — Full iptables phase\n- `references/docker-hardening.md` — Docker firewall hardening\n- `references/k8s-policy.md` — Kubernetes node policy\n- `references/security-profiles.md` — Pre-built configurations\n- `references/declarative-policy.md` — YAML policy schema\n- `references/observability.md` — Monitoring and baselines\n- `references/compliance.md` — CIS/PCI-DSS/SOC2 mapping\n- `references/recovery.md` — Recovery procedures\n- `references/special-environments.md` — WSL2, containers, exit codes\n- `scripts/audit-firewall.sh` — Environment detection\n- `scripts/firewall-plan.sh` — Dry-run diff\n- `scripts/firewall-verify.sh` — Post-apply verification\n\nFile v2.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn7e8vz4v0f8vr8dh2yr78fjkd86jgk3\",\n  \"slug\": \"linux-firewall-hardening\",\n  \"version\": \"2.2.0\",\n  \"publishedAt\": 1779873705313\n}\n\nFile v2.2.0:references/backend-firewalld.md\n\n# Backend: firewalld (RHEL / Rocky / Alma / Fedora)\n\nfirewalld is zone-aware. Always specify the zone. Default on most servers is `public`.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo firewall-cmd --state\n# \"running\" or \"not running\"\n```\n\n## Prerequisites\n\n- firewalld must be active but with known rules.\n- No other frontend (ufw) must be active.\n- Never modify iptables/nftables directly when firewalld owns the policy.\n\n## Apply: Idempotent Zone Rules\n\n### Step 1: Identify Active Zone\n\n```bash\nDEFAULT_ZONE=$(sudo firewall-cmd --get-default-zone)\necho \"Default zone: $DEFAULT_ZONE\"\nsudo firewall-cmd --get-active-zones\n```\n\n### Step 2: Add Rules\n\n```bash\nZONE=\"${DEFAULT_ZONE:-public}\"\nSSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk -F: '{print $NF}' | head -1)\nSSH_PORT=${SSH_PORT:-22}\n\n# SSH (service definition)\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=ssh >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=ssh\n\n# HTTP / HTTPS\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=http >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=http\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=https >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=https\n\n# Custom port\n# sudo firewall-cmd --zone=\"$ZONE\" --query-port=8080/tcp >/dev/null 2>&1 || #   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-port=8080/tcp\n\n# Rate-limit SSH (rich rule)\nsudo firewall-cmd --zone=\"$ZONE\" --query-rich-rule='rule service name=ssh limit value=3/m accept' >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule service name=ssh limit value=3/m accept'\n\n# Apply\nsudo firewall-cmd --reload\n```\n\n### Step 3: Verify\n\n```bash\nsudo firewall-cmd --list-all --zone=\"$ZONE\"\n```\n\n## Zone Commands Quick Reference\n\n```bash\n# List all zones\nsudo\n\nArchive v2.1.0: 20 files, 43710 bytes\n\nFiles: PUBLISH.md (14954b), references/backend-firewalld.md (2244b), references/backend-iptables.md (1885b), references/backend-nftables.md (2353b), references/backend-ufw.md (2130b), references/compliance.md (1281b), references/declarative-policy.md (15127b), references/docker-hardening.md (2578b), references/k8s-policy.md (2181b), references/observability.md (2299b), references/policy-schema.json (4066b), references/recovery.md (2031b), references/remaining-improvements.md (5957b), references/security-profiles.md (8960b), references/special-environments.md (6369b), scripts/audit-firewall.sh (11284b), scripts/firewall-plan.sh (4795b), scripts/firewall-verify.sh (2744b), SKILL.md (16569b), _meta.json (143b)\n\nArchive v1.0.0: 21 files, 41576 bytes\n\nFiles: problems.txt (949b), PUBLISH.md (12785b), references/backend-firewalld.md (2244b), references/backend-iptables.md (1885b), references/backend-nftables.md (2353b), references/backend-ufw.md (2130b), references/compliance.md (1281b), references/declarative-policy.md (15127b), references/docker-hardening.md (2578b), references/k8s-policy.md (2181b), references/observability.md (2299b), references/policy-schema.json (4066b), references/recovery.md (2031b), references/remaining-improvements.md (4548b), references/security-profiles.md (8960b), references/special-environments.md (6369b), scripts/audit-firewall.sh (11284b), scripts/firewall-plan.sh (4795b), scripts/firewall-verify.sh (2744b), SKILL.md (12004b), _meta.json (143b)","readmeExcerpt":"Skill: linux-firewall-hardening Owner: discovery219 Summary: 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. Tags: devsecops:2.1.0, docker:2.1.0, fail2ban:2.1.0, firewall:2.1.0, firewalld:2.","codeSnippets":[],"executableExamples":[{"language":"text","snippet":"DETECT → SELECT → PLAN → CONFIRM → VALIDATE → APPLY → VERIFY → (COMMIT | ROLLBACK)"},{"language":"bash","snippet":"bash scripts/audit-firewall.sh           # Human-readable\nbash scripts/audit-firewall.sh --json    # Machine-readable"},{"language":"text","snippet":"Imperative (state machine) → Ad-hoc hardening, incident response\nDeclarative (YAML)        → GitOps, multi-host, reproducible\nMixed                     → YAML as source-of-truth, state machine for verification"},{"language":"bash","snippet":"bash scripts/firewall-plan.sh --profile public-web-server\nbash scripts/firewall-plan.sh --ports 22,80,443\nbash scripts/firewall-plan.sh --json     # Machine-readable diff with approval_token\nbash scripts/firewall-plan.sh --refresh-audit --json  # Force re-audit + plan"},{"language":"json","snippet":"{\n  \"backend\": \"ufw\",\n  \"active_frontend\": \"ufw\",\n  \"profile\": \"public-web-server\",\n  \"target_ports\": [22, 80, 443],\n  \"diff\": {\n    \"add\":    [{\"port\": 80, \"proto\": \"tcp\", \"source\": \"any\"}],\n    \"skip\":   [{\"port\": 22, \"proto\": \"tcp\", \"reason\": \"already_exists\"}],\n    \"remove\": []\n  },\n  \"risk_assessment\": \"low\",\n  \"estimated_disruption\": \"none\",\n  \"approval_token\": \"sha256:abc123...\",\n  \"audit_cached\": false,\n  \"audit_cache_file\": \"/tmp/firewall-audit.json\"\n}"},{"language":"text","snippet":"Agent: PLAN output → shows rule diff + PLAN-TOKEN: a1b2c3d4e5f6\nAgent: STOPS. Waits.\nUser:  \"Approved. a1b2c3d4e5f6\"\nAgent: Runs APPLY with --approved-plan=a1b2c3d4e5f6\nAgent: After APPLY → runs VERIFY\nAgent: STOPS. Shows VERIFY results.\nUser:  \"VERIFY passed. Commit.\"\nAgent: Runs COMMIT (disarms rollback timer)"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: linux-firewall-hardening\ntitle: Linux Firewall Hardening\ndescription: 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.\nlicense: Dual MIT / Apache-2.0\nskill_version: 2.7.0\nschema_version: 2\ntags: [security, firewall, ufw, iptables, nftables, firewalld, hardening, docker, fail2ban, policy-as-code, devsecops, ipv6]\n---\n\n# Linux Firewall Hardening\n\n## When to Use\n\n- Check if a Linux server has active firewall protection.\n- Enable and configure a firewall without locking yourself out of SSH.\n- Audit existing rules, troubleshoot connectivity, or apply a security profile.\n- Automate firewall hardening via an AI agent or CI/CD pipeline.\n\n## When NOT to Use\n\n| Condition | Alternative |\n|-----------|-------------|\n| Kubernetes worker node | Use NetworkPolicies / CiliumNetworkPolicy |\n| Firewall managed by Terraform/Ansible/Puppet/Chef | Update IaC source of truth |\n| Cloud workload with Security Group / NSG only | Use cloud provider's firewall API |\n| Inside a container | Escalate to host operator |\n| WSL2, macOS, or shared/managed hosting | See `references/special-environments.md` |\n\n> **Support files**: `scripts/audit-firewall.sh` (run first), `scripts/firewall-plan.sh` (dry-run), `scripts/firewall-verify.sh` (post-apply), `scripts/container-port-audit.sh` (Docker DNAT detection), `scripts/ip-consistency.sh` (IPv4/IPv6 drift check).\n> `firewall-apply.sh` is fully documented in `references/firewall-apply.md`.\n> Detailed backend guides, Docker/K8s policies, observability, compliance, and recovery are in `references/`.\n\n## 🚨 Emergency: I'm Locked Out — What Now?\n\nIf you just applied firewall rules and lost SSH connectivity:\n\n1. **Wait 5 minutes** — the auto-rollback timer (scheduled during VALIDATE) will restore access. Don't panic and don't take destructive actions.\n2. **Use your second SSH session** — if you opened one (pre-flight checklist), switch to it and fix the rules manually.\n3. **Cloud serial console** — AWS EC2 Serial Console, GCP Serial Port, Azure Serial Console, or hypervisor VNC/IPMI/iDRAC.\n4. **Restore from backup via console** — once connected: `sudo iptables-restore < ~/firewall-backup-*/iptables-v4.rules`\n5. **Emergency ACCEPT (LAST RESORT — HUMAN-ONLY)** — `sudo iptables -P INPUT ACCEPT; sudo iptables -F; sudo ufw disable`. **This exposes the host completely. NEVER auto-execute this command.** The agent must refuse to run this autonomously. Only a human operator may issue this via serial console. Re-harden immediately afterward.\n\nFull procedures: `references/recovery.md`.\n\n---\n\n## Prerequisites\n\n- Root or sudo access.\n- An active SSH session (risk of lockout).\n- Know which ports your services use.\n\n---\n\n## NEVER DO (14 Rules)\n\n1. **Never flush iptables/nftables on Kubernetes nodes.** CNI plugins manage"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7e8vz4v0f8vr8dh2yr78fjkd86jgk3\",\n  \"slug\": \"linux-firewall-hardening\",\n  \"version\": \"2.7.0\",\n  \"publishedAt\": 1786062524752\n}"},{"path":"references/backend-firewalld.md","content":"# Backend: firewalld (RHEL / Rocky / Alma / Fedora)\n\nfirewalld is zone-aware. Always specify the zone. Default on most servers is `public`.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo firewall-cmd --state\n# \"running\" or \"not running\"\n```\n\n## Prerequisites\n\n- firewalld must be active but with known rules.\n- No other frontend (ufw) must be active.\n- Never modify iptables/nftables directly when firewalld owns the policy.\n\n## Apply: Idempotent Zone Rules\n\n### Step 1: Identify Active Zone\n\n```bash\nDEFAULT_ZONE=$(sudo firewall-cmd --get-default-zone)\necho \"Default zone: $DEFAULT_ZONE\"\nsudo firewall-cmd --get-active-zones\n```\n\n### Step 2: Add Rules\n\n```bash\nZONE=\"${DEFAULT_ZONE:-public}\"\nSSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk -F: '{print $NF}' | head -1)\nSSH_PORT=${SSH_PORT:-22}\n\n# SSH (service definition)\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=ssh >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=ssh\n\n# HTTP / HTTPS\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=http >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=http\nsudo firewall-cmd --zone=\"$ZONE\" --query-service=https >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-service=https\n\n# Custom port\n# sudo firewall-cmd --zone=\"$ZONE\" --query-port=8080/tcp >/dev/null 2>&1 || #   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-port=8080/tcp\n\n# Rate-limit SSH (rich rule)\nsudo firewall-cmd --zone=\"$ZONE\" --query-rich-rule='rule service name=ssh limit value=3/m accept' >/dev/null 2>&1 ||   sudo firewall-cmd --permanent --zone=\"$ZONE\" --add-rich-rule='rule service name=ssh limit value=3/m accept'\n\n# Apply\nsudo firewall-cmd --reload\n```\n\n### Step 3: Verify\n\n```bash\nsudo firewall-cmd --list-all --zone=\"$ZONE\"\n```\n\n## Zone Commands Quick Reference\n\n```bash\n# List all zones\nsudo firewall-cmd --get-zones\n\n# List all zones with rules\nsudo firewall-cmd --list-all-zones\n\n# Change default zone\nsudo firewall-cmd --set-default-zone=drop\n\n# Move interface to different zone\nsudo firewall-cmd --zone=internal --change-interface=eth1\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built firewalld configurations\n- `references/declarative-policy.md` — YAML-to-firewalld rendering"},{"path":"references/backend-iptables.md","content":"# Backend: iptables (Legacy Fallback)\n\nUse atomic `iptables-restore` instead of `-F` followed by individual `-A` commands. Build a complete ruleset file, then swap it in one operation. Always manage IPv4 (`iptables`) and IPv6 (`ip6tables`) separately.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. If your SSH runs on a different port, replace `22` with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> echo \"Detected SSH port: $SSH_PORT\"\n> ```\n> Using the wrong SSH port in the ruleset below will lock you out.\n\n## Key Principle: Atomic Restore\n\n```bash\n# Validate syntax first\nsudo iptables-restore --test /tmp/iptables-v4.rules\nsudo ip6tables-restore --test /tmp/iptables-v6.rules\n\n# Apply atomically\nsudo iptables-restore /tmp/iptables-v4.rules\nsudo ip6tables-restore /tmp/iptables-v6.rules\n```\n\n## Apply: Atomic Rulesets\n\n### Step 1: Build IPv4 Ruleset (`/tmp/iptables-v4.rules`)\n\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n### Step 2: Build IPv6 Ruleset (`/tmp/iptables-v6.rules`)\n\n```\n*filter\n:INPUT DROP [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n-A INPUT -i lo -j ACCEPT\n-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n-A INPUT -p icmpv6 -j ACCEPT\n-A INPUT -p tcp --dport 22 -j ACCEPT\n-A INPUT -p tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp --dport 443 -j ACCEPT\nCOMMIT\n```\n\n### Step 3: Persist\n\n- **Debian/Ubuntu**: `sudo apt install iptables-persistent`, then `sudo netfilter-persistent save`\n- **RHEL/CentOS**: `sudo service iptables save` or migrate to `firewalld`\n\n## Idempotent Single-Rule Pattern\n\nIf adding a single rule instead of full restore:\n\n```bash\n# Check if rule exists before adding\nsudo iptables -C INPUT -p tcp --dport 8080 -j ACCEPT 2>/dev/null ||   sudo iptables -A INPUT -p tcp --dport 8080 -j ACCEPT\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built iptables configurations\n- `references/declarative-policy.md` — YAML-to-iptables rendering"},{"path":"references/backend-nftables.md","content":"# Backend: nftables (Modern Dual-Stack)\n\nnftables is the modern replacement for iptables. It supports IPv4 and IPv6 in a single `inet` table, has atomic ruleset replacement, and uses a cleaner syntax.\n\n> **Detect your real SSH port first:** The examples below use port 22 for illustration. Replace with your actual port:\n> ```bash\n> SSH_PORT=$(ss -tlnp | grep -E \"sshd|ssh\" | awk '{print $NF}' | awk -F: '{print $NF}' | head -1)\n> SSH_PORT=${SSH_PORT:-22}\n> ```\n\n## Detection\n\n```bash\nsudo nft list ruleset\n# Shows current rules if active\n```\n\n## Key Principle: Atomic Replacement\n\nBuild a new ruleset file, validate with `nft -c`, then apply in one shot. **Never `flush ruleset` manually** on a production host without a backup.\n\n## Apply: Atomic Ruleset\n\n### Step 1: Build Ruleset File\n\n```bash\nsudo tee /etc/nftables.conf.new << 'EOF'\n#!/usr/sbin/nft -f\n\ntable inet filter {\n    set allowed_tcp_ports {\n        type inet_service\n        flags interval\n        elements = { 22, 80, 443 }\n    }\n\n    chain input {\n        type filter hook input priority 0; policy drop;\n\n        iif lo accept\n        ct state established,related accept\n        ct state invalid drop\n\n        ip protocol icmp accept\n        ip6 nexthdr icmpv6 accept\n\n        tcp dport @allowed_tcp_ports accept\n\n        # Rate limit new SSH connections\n        tcp dport 22 ct state new limit rate 10/second burst 20 packets accept\n\n        # Log with rate limit to prevent syslog flood\n        log prefix \"nft-drop: \" limit rate 5/second\n        drop\n    }\n\n    chain forward {\n        type filter hook forward priority 0; policy drop;\n    }\n\n    chain output {\n        type filter hook output priority 0; policy accept;\n    }\n}\nEOF\n```\n\n> **Warning**: Excessive logging can overwhelm syslog/journald on high-traffic systems. Always use `limit rate` on log rules and monitor after enabling.\n\n### Step 2: Dry-Run (Validate Syntax)\n\n```bash\nsudo nft -c -f /etc/nftables.conf.new\n```\n\nIf this returns errors, fix the file and re-validate. **Do not proceed until dry-run passes.**\n\n### Step 3: Atomic Apply\n\n```bash\n# Backup current ruleset (belt-and-suspenders)\nsudo nft list ruleset > \"$BACKUP_DIR/nftables-pre-apply.rules\" 2>/dev/null || true\n\n# Atomic replace\nsudo nft -f /etc/nftables.conf.new\nsudo mv /etc/nftables.conf.new /etc/nftables.conf\n\n# Enable persistence\nsudo systemctl enable nftables\nsudo systemctl restart nftables\n```\n\n### Step 4: Verify\n\n```bash\nsudo nft list ruleset\n```\n\n## Related\n\n- `references/security-profiles.md` — Pre-built nftables configurations\n- `references/declarative-policy.md` — YAML-to-nftables rendering"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"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. Skill: linux-firewall-hardening Owner: discovery219 Summary: 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. Tags: devsecops:2.1.0, docker:2.1.0, fail2ban:2.1.0, firewall:2.1.0, firewalld:2.","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1565,"uniquenessScore":47,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T17:35:08.257Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T21:48:01.355Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}