2 of 2

FirewalldLesson 2 of 2

Sources, rich rules and port forwarding

Allow or block traffic by the address it comes from, write rich rules with logging and rate limits, and forward one port to another.

  • --add-source
  • --add-rich-rule
  • --list-rich-rules
  • --remove-rich-rule
  • --add-forward-port
  • log prefix
  • reject
  • drop

50 min5 exercises

After this lesson you can

  • Assign a source address or network to a zone so its traffic gets different rules.
  • Write rich rules in the correct syntax to accept, reject, drop or log traffic from specific sources to a service or port.
  • Restrict a service to one network, forward a port, and test the result from the allowed and from the blocked host.

The idea

Allowing a service in a zone lets in everyone that zone covers. To say who may reach it, firewalld gives you two tools.

Sources

Assign an address or network to a zone with --add-source, and traffic from it uses that zone’s rules, instead of the zone for the interface. This is the simplest way to handle “this host is trusted” or “this network can use NFS”:

sudo firewall-cmd --permanent --zone=trusted --add-source=192.168.122.51
sudo firewall-cmd --permanent --zone=internal --add-source=192.168.122.0/24

Rich rules

A rich rule is a one-line sentence with four parts:

rule  family="ipv4"  source address="192.168.122.0/24"  service name="http"  accept
      │              │                                  │                   └─ action
      │              │                                  └─ what: service or port
      │              └─ who: source or destination address
      └─ IP version
PartOptions
Familyipv4, ipv6 (required when you give an address)
Sourcesource address="ADDR[/MASK]", can be negated with invert="True"
Targetservice name="ssh", or port port="8081" protocol="tcp"
Actionaccept, reject, drop, or mark
Logginglog prefix="TEXT " level="info" limit value="3/m" before the action

reject answers the sender with an error so they know at once; drop is silent, so they wait for a timeout. Use reject inside a trusted environment and drop towards the internet.

Manage them with --add-rich-rule='...', --remove-rich-rule='...' (give the identical text) and --list-rich-rules. Wrap the whole rule in single quotes so the double quotes inside survive the shell.

The common trap

If you add service name="http" accept for a network while the plain http service is still allowed in the zone, nothing is restricted: the service rule already lets everyone in. To restrict, remove the zone-wide service and keep only the rich rule.

Forwarding

--add-forward-port=port=80:proto=tcp:toport=8080 redirects incoming connections on port 80 to 8080 on the same machine (add :toaddr=ADDRESS to send them elsewhere, which also needs masquerading).

Try it

sudo firewall-cmd --permanent --zone=trusted --add-source=192.168.122.51
sudo firewall-cmd --reload
firewall-cmd --zone=trusted --list-sources
firewall-cmd --get-active-zones
success
success
192.168.122.51
trusted
  sources: 192.168.122.51
public
  interfaces: eth0

Restrict a service to a network

sudo firewall-cmd --permanent --remove-service=http
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.122.0/24" service name="http" accept'
sudo firewall-cmd --reload
firewall-cmd --list-services
firewall-cmd --list-rich-rules
success
success
success
cockpit dhcpv6-client ssh
rule family="ipv4" source address="192.168.122.0/24" service name="http" accept

Test both sides: from a host in 192.168.122.0/24 the page loads, from a host outside it curl -m3 times out. A rule is proven only when the unwanted access fails too.

Reject and log

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.122.99" service name="ssh" reject'
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.122.0/24" service name="ssh" log prefix="ssh-from-lab " level="info" limit value="3/m" accept'
sudo firewall-cmd --reload
ssh node1 true      # from node2
sudo journalctl -k --grep ssh-from-lab --no-pager | tail -1
Nov 03 14:02:11 node1 kernel: ssh-from-lab IN=eth0 OUT= MAC=52:54:00:12:34:56:52:54:00:aa:bb:cc:08:00 SRC=192.168.122.51 DST=192.168.122.50 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=24680 DF PROTO=TCP SPT=52114 DPT=22 WINDOW=64240 SYN

Rich rules take precedence over the zone’s allowed services in the order log, then deny, then allow. A reject for one address therefore works even though ssh is allowed for everyone.

Forward a port

sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
sudo firewall-cmd --reload
firewall-cmd --list-forward-ports
curl -s http://node1/           # from node2
success
success
port=80:proto=tcp:toport=8080:toaddr=
app

Going further

Rich rules also accept destination address=, protocol value="icmp", masquerade, forward-port and source-port. --add-icmp-block=echo-request stops ping replies. firewall-cmd --get-policies lists policies (firewalld 1.0+), which filter traffic between zones and are the right tool for forwarding or routing setups; rich rules remain the standard answer for exam-style “allow from this network” tasks. firewall-cmd --query-service=http and --query-port=8080/tcp return yes or no with an exit status, useful in scripts and checks.

Practice

0 of 5 done

Do these on your own system. They get harder. Check each result, then tick it off.

  1. Warm-up

    Trust one host completely

    Make everything from node2 (192.168.122.51) accepted by node1, permanently, by assigning that address to the trusted zone. Prove it by reaching a port that is not open for anyone else, then undo it.

    Check your result

    firewall-cmd --zone=trusted --list-sources shows 192.168.122.51. node2 can connect to a closed port, e.g. 8081, while another host cannot.

    Hint

    --permanent --zone=trusted --add-source=192.168.122.51, then --reload. A source is matched before the interface.

    Solution
    (cd /tmp && python3 -m http.server 8081 >/dev/null 2>&1 &)
    sudo firewall-cmd --permanent --zone=trusted --add-source=192.168.122.51
    sudo firewall-cmd --reload
    firewall-cmd --zone=trusted --list-sources
    # from node2: curl -sI http://node1:8081 | head -1
    sudo firewall-cmd --permanent --zone=trusted --remove-source=192.168.122.51
    sudo firewall-cmd --reload
  2. Practice

    Web server for one network only

    httpd must be reachable only from 192.168.122.0/24, and from nowhere else. Currently http is allowed in the public zone for everyone. Fix this permanently using a rich rule, and make sure the plain http service is no longer open to all.

    Check your result

    --list-services no longer lists http. --list-rich-rules shows a rule with source address="192.168.122.0/24" and service name="http" and accept. node2 (in that network) gets the page; a host outside it times out.

    Hint

    Allowing a service in the zone applies to everyone; a rich rule with a source limits it. So remove the service first, then add the rich rule.

    Solution
    sudo firewall-cmd --permanent --remove-service=http
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.122.0/24" service name="http" accept'
    sudo firewall-cmd --reload
    firewall-cmd --list-services
    firewall-cmd --list-rich-rules
  3. Practice

    Block one troublesome host

    Make node1 refuse SSH connections from 192.168.122.99 only, with an explicit rejection (so the client sees the refusal immediately), while others can still use SSH. Verify, then remove the rule.

    Check your result

    --list-rich-rules shows source address="192.168.122.99" service name="ssh" reject. That host receives Connection refused; node2 still connects.

    Hint

    reject sends an ICMP error back; drop discards silently. Rich rules are evaluated before the zone's allowed services.

    Solution
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.122.99" service name="ssh" reject'
    sudo firewall-cmd --reload
    firewall-cmd --list-rich-rules
    sudo firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="192.168.122.99" service name="ssh" reject'
    sudo firewall-cmd --reload
  4. Practice

    Allow, but log and limit

    Allow SSH from 192.168.122.0/24 with a log entry for each new connection, limited to 3 log entries per minute. Make a connection from node2 and find the log line.

    Check your result

    journalctl -k --grep 'ssh-from-lab' (or dmesg) shows a line starting with your prefix. The rule includes log prefix="ssh-from-lab " level="info" limit value="3/m".

    Hint

    rule family="ipv4" source address="..." service name="ssh" log prefix="ssh-from-lab " level="info" limit value="3/m" accept. Logging goes to the kernel log.

    Solution
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.122.0/24" service name="ssh" log prefix="ssh-from-lab " level="info" limit value="3/m" accept'
    sudo firewall-cmd --reload
    # from node2: ssh node1 true
    sudo journalctl -k --grep ssh-from-lab --no-pager | tail -2
  5. Challenge

    Forward port 80 to 8080

    An application listens on TCP 8080, but clients must use port 80. Forward incoming TCP 80 to 8080 on the same host permanently, and test it from node2 with the port 80 URL. State what you must also check about the application, and the zone.

    Check your result

    --list-forward-ports shows port=80:proto=tcp:toport=8080:toaddr=. curl http://node1/ from node2 returns the application's page.

    Hint

    --add-forward-port=port=80:proto=tcp:toport=8080. The forwarded packets still pass the zone's rules for port 8080 after translation.

    Solution
    (cd /tmp && echo app > index.html && python3 -m http.server 8080 >/dev/null 2>&1 &)
    sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
    sudo firewall-cmd --reload
    firewall-cmd --list-forward-ports
    # from node2: curl -s http://node1/