Skip to content

Integration with OS Running on the Firewall Machine

FirewallFabrik generates firewall scripts for iptables and nftables tailored for integration with modern Linux systems running systemd. The generated script supports command-line arguments start, stop, status, block, reload, interfaces, and test_interfaces. The script can be integrated with systemd using a custom service unit.

The iptables script is assembled from configlets located in resources/configlets/linux24/ (starting from script_skeleton). The nftables script is rendered from the Jinja2 template resources/templates/nftables/script.sh.j2. You can modify both following the instructions in 13 - Configlets.

Activating the Firewall Policy at Boot

The recommended way to activate the FirewallFabrik-generated policy at boot is to create a systemd service unit. This applies to both iptables and nftables firewalls.

Creating a systemd Service Unit

Create a service unit file that runs the generated firewall script at boot. The default output file name is fwf.sh, stored in /etc/. You can change both in the firewall settings dialog (Compiler > Output file name and Installer > Directory on firewall).

sudo $EDITOR /etc/systemd/system/firewallfabrik.service

Add the following content (adjust the script path if you changed the defaults):

[Unit]
Description=FirewallFabrik firewall policy
DefaultDependencies=no
Before=network-pre.target
Wants=network-pre.target
After=local-fs.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/etc/fwf.sh start
ExecStop=/etc/fwf.sh stop
ExecReload=/etc/fwf.sh reload

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable firewallfabrik.service
sudo systemctl start firewallfabrik.service

Disabling Conflicting Services

When using FirewallFabrik to manage the firewall, disable any distribution-provided firewall services to avoid conflicts:

# RHEL / Fedora / CentOS -- disable firewalld
sudo systemctl disable --now firewalld

# Debian / Ubuntu -- disable ufw
sudo systemctl disable --now ufw

# If nftables.service is active and you use FirewallFabrik for nftables
sudo systemctl disable --now nftables

[!NOTE] The script generated by FirewallFabrik does more than just set iptables or nftables rules. It also adds virtual IP addresses to the interfaces of the firewall, configures kernel parameters, and can check whether interfaces are present and up. Distribution-provided iptables-save/iptables-restore services only manage rules; other tasks performed by the FirewallFabrik-generated script will not be done upon reboot if you rely on those services.

Coexistence with Docker, CrowdSec, fail2ban and Other Tools

By default, the generated firewall script flushes all rules before loading the new policy. This guarantees a clean, deterministic firewall state in which FirewallFabrik controls the entire host firewall.

On servers running Docker, CrowdSec, fail2ban or similar tools, this full flush destroys the rules those tools manage. To avoid this, disable the "Flush entire ruleset" option in the firewall settings dialog. FirewallFabrik then only manages its own tables and chains, leaving everything else untouched.

Both platforms support this coexistence mode:

  • nftables: FirewallFabrik creates named tables (e.g. fwf_filter, fwf_nat). Only these tables are deleted and recreated. Other tools' tables (e.g. Docker's table ip docker, CrowdSec's table inet crowdsec) remain untouched. Untouched is not the same as unaffected: the FirewallFabrik chains keep filtering the same packets, see Forwarded Traffic Needs Rules in the Policy.
  • iptables: FirewallFabrik creates prefixed chains (e.g. fwf_INPUT, fwf_FORWARD, fwf_OUTPUT) and inserts jump rules into the built-in chains. Only the prefixed chains are flushed on reload.

The table/chain prefix is configurable via "Table name" in the firewall settings dialog (default: fwf).

How nftables Coexistence Works

FirewallFabrik creates two named nftables tables (e.g. fwf_filter and fwf_nat). When "Flush entire ruleset" is disabled, the generated script only deletes and recreates these two tables; other tools' tables are never touched. The nft rules file uses an atomic create-then-delete pattern:

table ip fwf_filter {}
delete table ip fwf_filter

table ip fwf_filter {
    chain input { type filter hook input priority filter; policy drop; ... }
    chain forward { ... }
    chain output { ... }
}

This is safe because nft -f processes the entire file atomically.

How iptables Coexistence Works

Since iptables has no table namespace mechanism, FirewallFabrik uses prefixed user chains. When "Flush entire ruleset" is disabled, the generated script:

  1. Removes any existing fwf_* chains and their jump rules from the built-in chains (reset_fwf_chains).
  2. Creates new prefixed chains (fwf_INPUT, fwf_FORWARD, fwf_OUTPUT) and inserts jump rules at position 1 in the built-in chains (setup_fwf_jumps).
  3. All firewall rules target the prefixed chains instead of the built-in chains.
Chain INPUT (policy DROP)
 fwf_INPUT    all  --  0.0.0.0/0  0.0.0.0/0    ← FWF jump rule
 CROWDSEC_CHAIN ...                               ← other tool
 f2b-sshd ...                                     ← other tool

Chain fwf_INPUT (1 references)
 ... FirewallFabrik rules ...

On stop, only the fwf_* chains and their jump rules are removed. The built-in chain policies are reset to ACCEPT so the machine stays reachable, and other tools' chains are untouched.

[!WARNING] Disabling "Flush entire ruleset" means FirewallFabrik no longer controls the entire firewall. On iptables, a rule another tool inserts above the FirewallFabrik jump rule decides on its own and the policy never sees the packet. On nftables the opposite holds: the FirewallFabrik chains keep filtering everything the other tools forward, so that traffic needs rules in the policy. Only disable this if you need coexistence with other tools on the same machine.

Controlling Rule Evaluation Order (iptables)

When multiple tools manage iptables chains, the evaluation order depends on the systemd startup order. Each tool inserts its jump rules at position 1 (top of the built-in chain) using iptables -I. Tools that start later end up at the top, so their rules are evaluated first.

For a typical setup with FirewallFabrik, Docker, CrowdSec and fail2ban, configure the systemd unit to start FirewallFabrik before the other tools:

[Unit]
Description=FirewallFabrik firewall policy
DefaultDependencies=no
Wants=network-pre.target
After=local-fs.target

# Start before network and before other tools so the machine
# is protected as early as possible. Tools that start later
# insert their rules at position 1 (evaluated first).
Before=network-pre.target docker.service crowdsec.service fail2ban.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/etc/fwf.sh start
ExecStop=/etc/fwf.sh stop
ExecReload=/etc/fwf.sh reload

[Install]
WantedBy=multi-user.target

This produces the following evaluation order in the INPUT chain:

Position 1:  -j f2b-sshd         (fail2ban - started last)
Position 2:  -j CROWDSEC_CHAIN   (CrowdSec)
Position 3:  -j fwf_INPUT        (FirewallFabrik - started first)
Built-in policy: DROP

Packets flow top-to-bottom. CrowdSec and fail2ban see each packet before FirewallFabrik's rules. If neither blocks the packet, FirewallFabrik's policy decides. If none of the chains match, the built-in DROP policy applies.

This is typically the desired order: CrowdSec blocklists and fail2ban bans are checked first, then the firewall policy applies.

[!NOTE] nftables uses table priorities instead of insertion order, so the systemd ordering is less critical there. FirewallFabrik's named tables use the default filter priority.

Forwarded Traffic Needs Rules in the Policy

Keeping the rules of another tool alive is not the same as letting them decide. FirewallFabrik still filters forwarded traffic, and the policy has to permit whatever Docker, libvirt or a container runtime forwards. Otherwise that traffic is dropped, no matter which rules those tools installed for it.

On iptables all tools share the built-in FORWARD chain, and the first terminal verdict wins. A tool whose jump rule sits above the FirewallFabrik one accepts the packet on its own, and the rest of the chain never sees it.

nftables works differently. Every table brings its own base chain, and the kernel runs all base chains attached to a hook. An accept ends processing only within its own table, while a drop in any of them is final for the packet. A forwarded packet therefore has to be accepted by every base chain on the forward hook, and the strictest one decides. There is no position to insert at and no start order that changes this.

The FirewallFabrik forward chain is created with policy drop and usually ends in a catch-all deny, so on a host running Docker the symptoms are:

  • published container ports answer nothing from other machines, although the DNAT rule of the container runtime allows (and counts) the packets
  • containers reach nothing outside the host, although the masquerade rule is in place

The same applies to every other tool that filters forwarded traffic in a table of its own.

Two policy rules permit that traffic. container-networks is an object group holding the networks the containers or guests live on:

Rule Interface Direction Source Destination Service Action
To the containers Any Inbound Any container-networks Any Accept
From the containers Any Outbound container-networks Any Any Accept

The Direction field is what keeps both rules in the forward chain. With "Assume firewall is part of 'any'" enabled, a rule whose Source is Any also produces a copy in the output chain, and a rule whose Destination is Any also produces one in the input chain. The second copy of the first rule would permit the firewall to reach anything, and the second copy of the second rule would open every port of the firewall to the containers.

For Docker: Match on addresses, not on interfaces. A bridge created by Docker Compose is named after a project hash (br-72389e00bbd6) and is called something else as soon as the project is recreated, while the subnet it serves is predictable. Pin the pool the runtime allocates from, so that one group covers every network it will ever create:

{
  "default-address-pools": [
    { "base": "172.18.0.0/16", "size": 24 }
  ]
}

[!NOTE] A rule permitting a whole container network does not expose unpublished ports. Docker keeps its own FORWARD chain at policy drop and only accepts the ports actually published, and it drops packets addressed straight to a container address from outside. The rule moves the per-port decision to the container runtime, it does not remove it.

Published Ports Are Not Visible in the Policy

Destination NAT runs in the prerouting hook, ahead of the forward hook. A policy rule therefore sees the container address and the container port, never the published one. A container started with -p 32990:80 is matched as its container address on port 80, and a rule written for port 32990 never matches.

Where the published port has to appear in the policy, a Custom Service carrying the nftables code below matches the port the client connected to, read from the original direction of the conntrack entry:

ct original proto-dst 32990

Traffic from the Firewall to a Container

Traffic the firewall itself sends to a container is not forwarded, it is local output, so the rules above do not cover it. This affects the userland proxy that serves published ports for connections originating on the host, monitoring plugins, and a reverse proxy running outside the container.

Cover it deliberately with a rule naming the firewall object as Source. The reverse direction, a container connecting to a service on the firewall, needs a rule of its own:

Rule Interface Direction Source Destination Service Action
From the firewall Any Outbound the firewall object container-networks Any Accept
To the firewall Any Inbound container-networks the firewall object the ports in question Accept

The first rule is compiled into the output chain, the second into the input chain. Name the services in the second one rather than leaving them at Any, otherwise every port of the firewall is open to whatever runs in a container.

Restarting the Policy when an Interface Address Changes

The firewall policy script generated by FirewallFabrik determines the IP addresses of all dynamic interfaces and assigns them to variables, which it then uses in the policy rules. If an interface's address changes after the policy has been loaded, the firewall script needs to be restarted.

On modern systems using NetworkManager, you can use a dispatcher script:

sudo $EDITOR /etc/NetworkManager/dispatcher.d/99-firewallfabrik

Add the following content:

#!/bin/bash
# Restart FirewallFabrik policy when an interface gets a new address
if [ "$2" = "up" ] || [ "$2" = "dhcp4-change" ] || [ "$2" = "dhcp6-change" ]; then
    systemctl reload firewallfabrik.service 2>/dev/null || true
fi

Make the script executable:

sudo chmod +x /etc/NetworkManager/dispatcher.d/99-firewallfabrik

For systems using systemd-networkd instead of NetworkManager, create a networkd-dispatcher script in /etc/networkd-dispatcher/routable.d/ with similar content.

For PPP connections (e.g. PPPoE), the /etc/ppp/ip-up script can be used to restart the firewall:

#!/bin/bash
systemctl reload firewallfabrik.service

[!NOTE] Replace firewallfabrik.service with the actual service name if you chose a different name for the unit file.

Deploying with Configuration Management

In modern infrastructure, firewall policies are often deployed as part of an automated workflow rather than manually. The generated .fw script fits naturally into configuration management tools.

Ansible

An Ansible playbook to deploy a FirewallFabrik-generated policy might look like this (following the Linuxfabrik Ansible Development Guidelines):

- name: 'Playbook linuxfabrik.lfops.firewallfabrik'
  hosts: 'firewalls'
  become: true

  tasks:

    - name: 'Deploy firewall script'
      ansible.builtin.template:
        src: 'output/{{ inventory_hostname }}.fw'
        dest: '/etc/fwf.sh'
        owner: 'root'
        group: 'root'
        mode: 0o0700
        backup: true
      notify: 'firewallfabrik: activate firewall'

    - name: 'Deploy systemd service'
      ansible.builtin.template:
        src: 'firewallfabrik.service.j2'
        dest: '/etc/systemd/system/firewallfabrik.service'
        owner: 'root'
        group: 'root'
        mode: 0o0644
        backup: true
      notify:
        - 'firewallfabrik: reload systemd'
        - 'firewallfabrik: activate firewall'

    - name: 'Enable firewall service'
      ansible.builtin.service:
        name: 'firewallfabrik'
        enabled: true

  handlers:

    - name: 'firewallfabrik: reload systemd'
      ansible.builtin.systemd:
        daemon_reload: true

    - name: 'firewallfabrik: activate firewall'
      ansible.builtin.service:
        name: 'firewallfabrik'
        state: 'reloaded'

This approach ensures that the firewall script is deployed consistently across all machines and activated in a controlled manner.

CI/CD Integration

For teams using a CI/CD pipeline, the typical workflow is:

  1. Edit the firewall policy, either in the FirewallFabrik GUI or by modifying the .fwf YAML file directly (e.g. via scripts or other automation tools to add rules, objects and attributes).
  2. Save and compile, either via the GUI (Rules > Compile) or on the command line (fwf compile myfile.fwf).
  3. Commit the .fwf source file and the generated .fw script(s) to Git.
  4. A CI/CD pipeline (GitLab CI, GitHub Actions, Jenkins, etc.) picks up the change and runs the deployment playbook or script.

Example GitLab CI stage:

deploy-firewall:
  stage: deploy
  script:
    - ansible-playbook -i inventory deploy-firewall.yml
  only:
    changes:
      - output/*.fw
  when: manual

The when: manual gate ensures that firewall deployments are always explicitly triggered by an operator.

See 10 - Compiling and Installing a Policy for details on compiling policies.