Skip to content

UFW Parser ​

UFW Parser collects UFW firewall rules from many Linux servers — over SSH or from local copies — and shows them as one table, or exports them to CSV, Excel or Ansible host_vars YAML.

Source on GitHub · Summary on chaintools.tech · MIT licence

This page describes version 0.2.0 (ufw_parser --version).

Regenerate exports made with 0.1.0

Version 0.1.0 exported deny and limit rules as allow, port lists and ranges as all ports, and dropped source prefixes and IPv6 sources. Don't apply Ansible files made with it; export again with 0.2.0. The changelog lists the upgrade steps.

Requirements ​

  • Python 3.10 or newer
  • For remote servers: SSH access, the server's host key in known_hosts, and sudo without a password for reading the two rule files (see Remote servers)

Install ​

bash
git clone https://github.com/qf3l3k/ufw_parser.git && cd ufw_parser
python3 -m venv .venv && . .venv/bin/activate
pip install .

Configuration ​

By default the configuration is read from ~/.config/ufw_parser/hosts.yml; -c selects another file.

yaml
servers:
  node-1:
    hostname: node-1.example.com
    processing: remote
    ssh_user: admin
    use_ssh_agent: true
    rule_path: /etc/ufw
  node-2:
    hostname: node-2.example.com
    processing: local
    rule_path: /srv/backups/202610090200/node-2.example.com/etc/ufw
KeyMeaning
hostnameServer name; used to connect in remote mode and as the hostname in the output
processingremote: read the rules over SSH; local: read rule files on this machine
rule_pathlocal: folder searched recursively for *.rules files; remote: folder with user.rules and user6.rules, usually /etc/ufw
ssh_userSSH user (remote); default: your local user name
use_ssh_agenttrue (default): use the SSH agent and your default keys; false: use ssh_pass
ssh_passSSH password, only with use_ssh_agent: false
ssh_portSSH port (remote); default 22
known_hosts_fileExtra known_hosts file, checked in addition to the default ones (remote)
use_sudotrue (default): read the files with sudo; set false when ssh_user is root

The file is checked before any server is contacted. Missing keys, an unknown processing value or a wrong type (for example ssh_port: '22') are all reported at once, and the run stops with exit code 2.

local mode works well on backups made with Rsync Backup Tool.

Remote servers ​

In remote mode, UFW Parser connects over SSH and runs two commands:

bash
sudo -n cat -- /etc/ufw/user.rules
sudo -n cat -- /etc/ufw/user6.rules
  • Host keys are checked. The server's key must be in ~/.ssh/known_hosts, /etc/ssh/ssh_known_hosts or the server's known_hosts_file, under the same name as hostname ([hostname]:port for a port other than 22). Unknown servers are refused, and a changed key fails with a warning about a possible man-in-the-middle attack. To add a server, connect to it once with ssh and check the fingerprint.

  • sudo must not ask for a password (-n). You can allow only these two reads, in a file created with sudo visudo -f /etc/sudoers.d/ufw_parser on each server:

    admin ALL=(root) NOPASSWD: /usr/bin/cat -- /etc/ufw/user.rules, /usr/bin/cat -- /etc/ufw/user6.rules
  • If user6.rules doesn't exist, only the IPv4 rules are read and a warning is logged.

  • Connecting, logging in and each command time out after 15 seconds (--ssh-timeout).

  • Settings from ~/.ssh/config (host aliases, ProxyJump, IdentityFile) are not used.

Usage ​

bash
ufw_parser                                               # table on screen, ~/.config/ufw_parser/hosts.yml
ufw_parser -c hosts.yml -o csv -of ~/exports/            # CSV file
ufw_parser -c hosts.yml -o ansible_yaml -of ~/ansible/host_vars/
OptionDefaultMeaning
-c, --config~/.config/ufw_parser/hosts.ymlConfiguration file
-o, --outputscreenscreen, csv, excel or ansible_yaml
-of, --output_folder~/.config/ufw_parser/Where exports are written
-l, --log~/.config/ufw_parser/Folder for the log file
-j, --jobs8Servers read in parallel
--ssh-timeout15Seconds for SSH connect, login and each remote command
-d, --debugoffDebug logging
-v, --versionShow the version

The table goes to standard output and log messages to standard error, so ufw_parser > rules.txt saves only the table.

A server that can't be read (connection, host key or sudo failure, missing rule folder) doesn't stop the run: the other servers are still exported, and the failures are listed again at the end.

Exit codeMeaning
0All servers read and exported
1Finished with errors: servers that couldn't be read, skipped rules or a failed export
2Invalid command line or configuration file

Output ​

UFW Parser reads the ### tuple ### lines in UFW's user.rules and user6.rules, the lines UFW itself rebuilds its rule list from. So every kind of rule comes through as UFW stores it: allow, deny, reject and limit, logging, incoming, outgoing and routed rules, interfaces, port lists and ranges, IPv4 and IPv6 addresses with prefix length, application profiles and comments. A line that can't be parsed is logged as an error and skipped (exit code 1), never guessed.

Each rule becomes one row per IP version, with the columns Hostname, IPv, Direction, Action, Log, Protocol, Port, Destination, Source port, Source, Interface in, Interface out, Route, App, Source app and Comment. On screen, columns that are empty for every rule are left out:

          Hostname IPv Direction Action Protocol  Port Destination Source port       Source                  Comment
node-1.example.com  v4        in  allow      tcp    22   0.0.0.0/0         any    0.0.0.0/0                      SSH
node-1.example.com  v4        in  allow      tcp 26656   0.0.0.0/0         any    0.0.0.0/0               P2P node 1
node-1.example.com  v4        in  allow      tcp  9100   0.0.0.0/0         any 203.0.113.10 Prometheus Node Exporter
node-1.example.com  v6        in  allow      tcp    22        ::/0         any         ::/0                      SSH
node-1.example.com  v6        in  allow      tcp 26656        ::/0         any         ::/0               P2P node 1
  • csv / excel: ufw_rules_<YYYYMMDD_HHMMSS>.csv / .xlsx in the output folder, with all columns.
  • ansible_yaml: one <output_folder>/<hostname>/ufw_rules.yml per server, in rule order:
yaml
ufw_rules:
- {rule: allow, direction: in, to_port: '22', proto: tcp, comment: SSH}
- {rule: allow, direction: in, to_port: '26656', proto: tcp, comment: P2P node 1}
- {rule: allow, direction: in, from_ip: 203.0.113.10, to_port: '9100', proto: tcp, comment: Prometheus Node Exporter}

The entries use the parameters of the community.general.ufw module. Parameters at the module's default (anywhere, any port, any protocol) are left out, and a rule that UFW stores for both IPv4 and IPv6 appears once. Routed rules use route: true with interface_in / interface_out, and rules for an application profile use name instead of a port and protocol. A task that applies the file:

yaml
- name: Apply UFW rules
  community.general.ufw:
    rule: "{{ item.rule }}"
    route: "{{ item.route | default(omit) }}"
    direction: "{{ item.direction | default(omit) }}"
    interface: "{{ item.interface | default(omit) }}"
    interface_in: "{{ item.interface_in | default(omit) }}"
    interface_out: "{{ item.interface_out | default(omit) }}"
    log: "{{ item.log | default(omit) }}"
    from_ip: "{{ item.from_ip | default(omit) }}"
    from_port: "{{ item.from_port | default(omit) }}"
    to_ip: "{{ item.to_ip | default(omit) }}"
    to_port: "{{ item.to_port | default(omit) }}"
    proto: "{{ item.proto | default(omit) }}"
    name: "{{ item.name | default(omit) }}"
    comment: "{{ item.comment | default(omit) }}"
  loop: "{{ ufw_rules }}"

The module has no log-all and no source application profile: such rules are exported as log: true and as the profile's ports, with a warning in the log.