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, andsudowithout a password for reading the two rule files (see Remote servers)
Install
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.
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| Key | Meaning |
|---|---|
hostname | Server name; used to connect in remote mode and as the hostname in the output |
processing | remote: read the rules over SSH; local: read rule files on this machine |
rule_path | local: folder searched recursively for *.rules files; remote: folder with user.rules and user6.rules, usually /etc/ufw |
ssh_user | SSH user (remote); default: your local user name |
use_ssh_agent | true (default): use the SSH agent and your default keys; false: use ssh_pass |
ssh_pass | SSH password, only with use_ssh_agent: false |
ssh_port | SSH port (remote); default 22 |
known_hosts_file | Extra known_hosts file, checked in addition to the default ones (remote) |
use_sudo | true (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:
sudo -n cat -- /etc/ufw/user.rules
sudo -n cat -- /etc/ufw/user6.rulesHost keys are checked. The server's key must be in
~/.ssh/known_hosts,/etc/ssh/ssh_known_hostsor the server'sknown_hosts_file, under the same name ashostname([hostname]:portfor 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 withsshand check the fingerprint.sudomust not ask for a password (-n). You can allow only these two reads, in a file created withsudo visudo -f /etc/sudoers.d/ufw_parseron each server:admin ALL=(root) NOPASSWD: /usr/bin/cat -- /etc/ufw/user.rules, /usr/bin/cat -- /etc/ufw/user6.rulesIf
user6.rulesdoesn'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
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/| Option | Default | Meaning |
|---|---|---|
-c, --config | ~/.config/ufw_parser/hosts.yml | Configuration file |
-o, --output | screen | screen, 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, --jobs | 8 | Servers read in parallel |
--ssh-timeout | 15 | Seconds for SSH connect, login and each remote command |
-d, --debug | off | Debug logging |
-v, --version | Show 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 code | Meaning |
|---|---|
0 | All servers read and exported |
1 | Finished with errors: servers that couldn't be read, skipped rules or a failed export |
2 | Invalid 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 1csv/excel:ufw_rules_<YYYYMMDD_HHMMSS>.csv/.xlsxin the output folder, with all columns.ansible_yaml: one<output_folder>/<hostname>/ufw_rules.ymlper server, in rule order:
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:
- 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.
