- ghost-smtp-mailgun-setup: two-system email config (newsletter API + transactional SMTP) - firewalld-fleet-hardening: Fedora fleet firewall audit-and-harden pattern with Ansible - clamav-fleet-deployment: fleet deployment with nice/ionice throttling + quarantine - ansible-check-mode-false-positives: when: not ansible_check_mode guard for verify/assert tasks - ghost-emailanalytics-lag-warning: submitted status, lag counter, fetchMissing skip explained
174 lines
5.1 KiB
Markdown
174 lines
5.1 KiB
Markdown
---
|
|
title: Firewall Hardening with firewalld on Fedora Fleet
|
|
domain: selfhosting
|
|
category: security
|
|
tags:
|
|
- firewall
|
|
- firewalld
|
|
- iptables
|
|
- fedora
|
|
- ansible
|
|
- security
|
|
- hardening
|
|
status: published
|
|
created: 2026-04-18
|
|
updated: 2026-04-18T11:13
|
|
---
|
|
# Firewall Hardening with firewalld on Fedora Fleet
|
|
|
|
## Overview
|
|
|
|
Fedora and RHEL-based hosts use `firewalld` as the default firewall manager, backed by `nftables`. Over time, firewall rules accumulate stale entries — decommissioned services, old IP allowances, leftover port forwards — that widen the attack surface silently. This article covers the audit-and-harden pattern for Fedora fleet hosts using Ansible.
|
|
|
|
> For Ubuntu/Debian hosts, see [ufw-firewall-management](ufw-firewall-management.md).
|
|
|
|
## The Problem with Accumulated Rules
|
|
|
|
Rules added manually or by service installers (`firewall-cmd --add-port=...`) don't get cleaned up when services are removed. Common sources of stale rules:
|
|
|
|
- Monitoring agents (Zabbix, old Netdata exporters)
|
|
- Media servers moved to another host (Jellyfin, Plex)
|
|
- Development ports left open during testing
|
|
- IP-specific allowances for home IPs that have since changed
|
|
|
|
These stale rules are invisible in day-to-day operation but show up during audits as unnecessary exposure.
|
|
|
|
## Auditing Current Rules
|
|
|
|
```bash
|
|
# Show all active rules (nftables, what firewalld actually uses)
|
|
nft list ruleset
|
|
|
|
# Show firewalld zones and services
|
|
firewall-cmd --list-all-zones
|
|
|
|
# Show permanent config (what survives reboot)
|
|
firewall-cmd --permanent --list-all
|
|
```
|
|
|
|
Cross-reference open ports against running services:
|
|
|
|
```bash
|
|
# What's actually listening?
|
|
ss -tlnp
|
|
|
|
# Match against firewall rules — anything open that has no listener is stale
|
|
```
|
|
|
|
## Ansible Hardening Approach
|
|
|
|
Rather than patching rules incrementally, the cleanest approach is to **flush and rebuild**: remove all non-essential rules and explicitly whitelist only what the host legitimately serves. This avoids drift and makes the resulting ruleset self-documenting.
|
|
|
|
The Ansible playbook uses `ansible.posix.firewalld` to manage rules declaratively and a flush task to clear the slate before applying the desired state.
|
|
|
|
### Pattern: Flush → Rebuild
|
|
|
|
```yaml
|
|
- name: Remove stale firewalld rules
|
|
ansible.posix.firewalld:
|
|
port: "{{ item }}"
|
|
permanent: true
|
|
state: disabled
|
|
loop:
|
|
- 8096/tcp # Jellyfin — decommissioned
|
|
- 10050/tcp # Zabbix agent — removed
|
|
- 10051/tcp # Zabbix server — removed
|
|
ignore_errors: true # OK if rule doesn't exist
|
|
|
|
- name: Apply minimal whitelist
|
|
ansible.posix.firewalld:
|
|
port: "{{ item }}"
|
|
permanent: true
|
|
state: enabled
|
|
loop: "{{ allowed_ports }}"
|
|
notify: Reload firewalld
|
|
```
|
|
|
|
Define `allowed_ports` per host in `host_vars/`:
|
|
|
|
```yaml
|
|
# host_vars/majorlab/firewall.yml
|
|
allowed_ports:
|
|
- 80/tcp # Caddy HTTP
|
|
- 443/tcp # Caddy HTTPS
|
|
- 22/tcp # SSH (public)
|
|
- 2222/tcp # SSH (alt)
|
|
- 3478/tcp # Nextcloud Talk TURN
|
|
```
|
|
|
|
### Tailscale SSH: Restrict to ts-input Zone
|
|
|
|
For hosts where SSH should only be accessible via Tailscale, move the SSH rule from the public zone to the `ts-input` interface:
|
|
|
|
```yaml
|
|
- name: Remove SSH from public zone
|
|
ansible.posix.firewalld:
|
|
zone: public
|
|
service: ssh
|
|
permanent: true
|
|
state: disabled
|
|
|
|
- name: Allow SSH on Tailscale interface only
|
|
ansible.posix.firewalld:
|
|
zone: trusted
|
|
interface: tailscale0
|
|
permanent: true
|
|
state: enabled
|
|
notify: Reload firewalld
|
|
```
|
|
|
|
> **Note:** The Tailscale interface is `tailscale0` unless customized. Confirm with `ip link show` before applying.
|
|
|
|
## Per-Host Hardening Reference
|
|
|
|
Different host roles need different rule sets. These are the minimal whitelists for common MajorsHouse host types:
|
|
|
|
| Host Role | Open Ports | Notes |
|
|
|-----------|-----------|-------|
|
|
| Reverse proxy (Caddy) | 80, 443, 22/2222 | No app ports exposed — Caddy proxies internally |
|
|
| Storage/media (Plex) | 32400 (public), 22 (Tailscale-only) | Plex needs public; SSH Tailscale-only |
|
|
| Bot/Discord host | 25 (Postfix), 25000 (webUI), 6514 (syslog-TLS) | No inbound SSH needed if Tailscale-only |
|
|
| Mail server | 25, 587, 993, 443, 22 | Standard mail ports |
|
|
|
|
## Default Policy
|
|
|
|
Set the default zone policy to `DROP` (not `REJECT`) to make the host non-discoverable:
|
|
|
|
```bash
|
|
firewall-cmd --set-default-zone=drop --permanent
|
|
firewall-cmd --reload
|
|
```
|
|
|
|
`DROP` silently discards packets; `REJECT` sends an ICMP unreachable back, confirming the host exists.
|
|
|
|
## Verifying After Apply
|
|
|
|
```bash
|
|
# Confirm active rules match intent
|
|
firewall-cmd --list-all
|
|
|
|
# Spot-check a port that should be closed
|
|
nmap -p 10050 <host-ip>
|
|
# Expected: filtered (not open, not closed)
|
|
|
|
# Confirm a port that should be open
|
|
nmap -p 443 <host-ip>
|
|
# Expected: open
|
|
```
|
|
|
|
## Ansible Handler
|
|
|
|
```yaml
|
|
handlers:
|
|
- name: Reload firewalld
|
|
ansible.builtin.service:
|
|
name: firewalld
|
|
state: reloaded
|
|
```
|
|
|
|
## See Also
|
|
|
|
- [ufw-firewall-management](ufw-firewall-management.md) — Ubuntu/Debian equivalent
|
|
- [ssh-hardening-ansible-fleet](ssh-hardening-ansible-fleet.md)
|
|
- [linux-server-hardening-checklist](linux-server-hardening-checklist.md)
|