Skip to content

Firewall Configuration

The configure_firewall task uses the linux-system-roles.firewall role to manage firewalld on both RHEL and Ubuntu hosts. On Ubuntu, UFW is automatically disabled in favor of firewalld.

Zone design

Two custom zones are created:

Zone Services Purpose
perimeter HTTPS (443), SSH (22), 7327/tcp Default zone — handles all traffic
perimeter-mgmt Reserved for source-restricted SSH overrides

perimeter uses target: DROP — traffic not explicitly allowed is silently dropped. It is set as the default zone, so it handles all interface traffic not matched by a more specific source binding.

perimeter-mgmt exists as an empty zone. To restrict SSH to a management subnet, override the firewall variable in host_vars and move SSH there with a source binding (see below).

Source bindings in firewalld are routing rules: traffic from a bound source is directed to that zone, and the most specific match wins. Binding 0.0.0.0/0 is not valid — the default zone already covers all unmatched traffic without a source entry.

Default configuration (open to all)

The role's default allows all sources to reach all services. SSH and HTTPS are both in the perimeter zone with no source binding:

firewall:
  - zone: perimeter
    state: present
  - zone: perimeter
    target: DROP
    service:
      - https
      - ssh
    port:
      - 7327/tcp
    state: enabled
  - set_default_zone: perimeter
    state: enabled

Restricting access by source

To restrict SSH to specific subnets, override the firewall variable in host_vars and move SSH from perimeter into perimeter-mgmt with a source binding:

# host_vars/epic-pauth-sapph.sapphire.dev.yml
firewall:
  - zone: perimeter
    state: present
  - zone: perimeter
    target: DROP
    service:
      - https
    port:
      - 7327/tcp
    state: enabled
  - zone: perimeter-mgmt
    state: present
  - zone: perimeter-mgmt
    target: DROP
    service:
      - ssh
    source:
      - 10.197.0.0/24
      - 10.198.0.0/24
    state: enabled
  - set_default_zone: perimeter
    state: enabled

source accepts a list of CIDR ranges. Traffic from those subnets is routed to perimeter-mgmt; everything else hits the default perimeter zone (which no longer allows SSH).

Verifying firewall state

Run these on the nginx host to inspect the active configuration:

# Show the default zone
firewall-cmd --get-default-zone

# List all zones that have any configuration
firewall-cmd --list-all-zones

# Show active rules for a specific zone
firewall-cmd --zone=perimeter --list-all
firewall-cmd --zone=perimeter-mgmt --list-all

# Show which zones are active (have an interface or source binding)
firewall-cmd --get-active-zones

Example output for --zone=perimeter --list-all on a correctly configured host:

perimeter (default)
  target: DROP
  interfaces:
  sources:
  services: https ssh
  ports: 7327/tcp
  ...

Known limitations

TODO: Firewall rules are additive

The current implementation uses linux-system-roles.firewall, which only adds rules — it does not remove services that are no longer in the variable list. If a service (e.g. http) is added and later removed from the configuration, it will remain enabled in the zone until manually removed with firewall-cmd --zone=perimeter --remove-service=http --permanent && firewall-cmd --reload.

The planned fix is to manage the firewalld zone XML file directly via a Jinja2 template, which is fully declarative — only services present in the template exist in the zone.

Variables

Variable Default Description
firewall_disable_conflicting_services true Disables UFW on Ubuntu before enabling firewalld.
firewall See configure_firewall.yml Full zone configuration passed to linux-system-roles.firewall. Override in host_vars or group_vars to customize per host.