Skip to content

core.firewall

← Module catalog

soul-sidesoulstack.networknetworkroot required

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
  • Sidesoul-side
  • Collectionsoulstack.network
  • Categorynetwork
Capabilitiesrun_as_rootexec_subprocess
Backend detection

Auto 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).

Changes when

The rule did not exist and got added (reconciled against ufw status / firewall-cmd --list-ports / --list-rich-rules).

Stays unchanged when

The rule is already present.

Parameters

ParamTypeRequired / defaultDescription
portintrequiredPort 1..65535.
protostring
tcp | udp
optionalProtocol tcp|udp (default tcp).
actionstring
allow | deny
optionalAction allow|deny (default allow).
sourcestringoptionalIPv4 CIDR or a single IPv4 (default any). IPv6 is not supported in the MVP.
zonestringoptionalfirewalld 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/8

Output

FieldTypeDescription
changedbool
backendufw | firewalld
portint
protostring
actionstring
sourcestring (only if set, in normalized form)
zonestring (only if set)

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).

BackendHow the rule is translatedIdempotency check
ufwpresent/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).
firewalldA 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.

See also