core.firewall
Manage a single firewall rule: port, protocol, source, action.
core.firewall brings a single firewall rule (port/proto/source/action) to the present or absent state. The backend is chosen automatically from the installed management binary — ufw (checked first) or firewalld; the states are idempotent, and reconciliation is done by parsing CLI output. The module works with a single rule only and never touches the firewall's default policy or its enabled/disabled state. Requires root.
Requirements
- Rootrequired
- Side
soul-side - Collection
soulstack.network - Category
network
run_as_rootexec_subprocessAuto from the installed management binary (not from Soulprint): ufw is checked first (more common on Debian-family souls), then firewalld (firewall-cmd, the Red Hat family). If neither is found, the step fails (no supported firewall detected). iptables is deliberately deferred: it needs chain semantics and ip(6)tables-save, which an add/delete pair for a single rule does not cover.
States
core.firewall.present — The firewall rule exists (port/proto/source/action).
The rule did not exist and got added (reconciled against ufw status / firewall-cmd --list-ports / --list-rich-rules).
The rule is already present.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
port | int | required | Port 1..65535. |
proto | stringtcp | udp | optional | Protocol tcp|udp (default tcp). |
action | stringallow | deny | optional | Action allow|deny (default allow). |
source | string | optional | IPv4 CIDR or a single IPv4 (default any). IPv6 is not supported in the MVP. |
zone | string | optional | firewalld zone (default — the default zone). |
Example — Allow PostgreSQL from internal subnet
- name: Allow PostgreSQL from internal subnet module: core.firewall.present params: port: 5432 proto: tcp action: allow source: 10.0.0.0/8Output
| Field | Type | Description |
|---|---|---|
changed | bool | |
backend | ufw | firewalld | |
port | int | |
proto | string | |
action | string | |
source | string (only if set, in normalized form) | |
zone | string (only if set) |
core.firewall.absent — The rule is removed.
The rule existed and got removed.
The rule does not exist.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
port | int | required | Port 1..65535. |
proto | stringtcp | udp | optional | Protocol tcp|udp (default tcp). |
action | stringallow | deny | optional | Action allow|deny (default allow). |
source | string | optional | IPv4 CIDR or a single IPv4. |
zone | string | optional | firewalld zone. |
Example — Remove a previously added rule
- name: Remove the PostgreSQL rule module: core.firewall.absent params: port: 5432 proto: tcp source: 10.0.0.0/8Output
| Field | Type | Description |
|---|---|---|
changed | bool | |
backend | ufw | firewalld | |
port | int | |
proto | string | |
action | string |
Reference
Backend semantics
The same rule is translated differently into ufw and firewalld; in both cases idempotency is verified by parsing CLI output (fragile across versions, covered by strict tests).
| Backend | How the rule is translated | Idempotency check |
|---|---|---|
| ufw | present/absent → ufw allow|deny … / ufw delete …. Without source — the short form (80/tcp); with source — the expanded form (proto tcp from <src> to any port <n>). | Parsing the tabular ufw status (direction tokens IN/OUT are taken into account; IPv6 mirror rules (v6) are ignored). |
| firewalld | A simple allow without source → --add-port / --remove-port. A rule with source or action: deny → a rich-rule (deny → reject). Mutations go through --permanent + an explicit firewall-cmd --reload. | A simple rule — in --list-ports, a rich-rule — in --list-rich-rules. |
Notes
- IPv6 is unsupported in the MVP — it is rejected at Validate: both backends work with IPv4 only, and silently accepting IPv6 would give a looping add/drift.
- source is normalized to a canonical form: a single IP → /32, a CIDR with host bits is collapsed to the network address.
- Idempotency is a parse of CLI output (ufw status / firewall-cmd --list-*), which is fragile across tool versions; it is covered by strict unit tests on fixed samples.
Security & defaults
- Works with a single rule only (add/delete): it never touches the default policy and never enables the firewall as a whole — no ufw enable, systemctl start firewalld, or edits to ufw default / the zone target. Enabling a firewall with a default deny policy on a remote host would instantly cut off SSH and lose control of the host.
- The invariant is covered by a unit test: Apply generates no enable/default command at all.
- firewalld: mutations go through --permanent + an explicit firewall-cmd --reload — the rule survives a restart; --reload applies the permanent config at runtime, does not restart the service, and does not change the default policy.
- Privileges are a declaration, not a runtime elevation: the manifest declares run_as_root + exec_subprocess for soul-lint's static check against the host's allowed_capabilities; backend calls run with the soul agent process's privileges (as root), and there is no privilege elevation inside the module.