Skip to content

6. Firewalls, SELinux, and bounded SSH protection

Supported Versions: Ubuntu Server 24.04 LTS (primary), Rocky Linux 9 (alternate) Last Updated: September 15, 2026

Previous: SSH | Course | Quiz | Next: Monitoring

An application can be listening while a firewall rejects a connection, or it can accept a connection but lack permission to read a file. This lesson separates network policy, process/file policy, and reactions to repeated authentication failures.

Prerequisites and outcomes

Use the two console-accessible disposable guests from lessons 1–5: client 192.0.2.10/24, server 192.0.2.20/24, internal-only lab NICs without DHCP/uplink, and separate unchanged NAT/DHCP management NICs. If lesson 5 sent you here because TCP 22 is blocked, complete only the selected firewall branch and return there; full SSH hardening is not a prerequisite for that branch.

You will allow the intended IPv4 client to server TCP 22 and 8000, inspect which policy actually applies, distinguish saved/runtime rules, restore only your changes, repair a deliberately wrong SELinux label on one scratch file, and optionally configure a bounded Fail2ban jail. Use the server console for changes and the client for connection tests. All command/output examples are illustrative, not live-tested VM results.

Take a server snapshot before-host-security. Record existing firewall/runtime state and any management SSH path. Choose UFW on the Ubuntu path OR firewalld on the Rocky path, according to the existing owner. Do not install/enable a competing manager, flush rules, or disable security services to pass a test. This is an IPv4 lab policy, not a complete policy for IPv6 listeners or a production hardening checklist.

1. Understand direction and identify ownership

Inbound means traffic destined for this server, such as a new SSH connection. Outbound is traffic generated by this server, including its requests to package repositories. Forwarded traffic passes through the server to another host. Our server is not a router: no forwarding, NAT, or default-route changes are needed.

Stateful firewalls track conversations and commonly allow replies to established connections. Consequently, a surviving old SSH session does not prove that new inbound sessions are allowed. A firewall allow rule also does not create a listening program.

Server, normal user; inspect only:

bash
hostname
cat /etc/os-release
ip -br link
ip -4 -br address
ip -4 route
systemctl is-active ufw firewalld

A missing/inactive unit can give a nonzero status without being a fault. For UFW, its own status reports whether enforcement is active; the systemd unit alone is insufficient. If Docker, Kubernetes, custom nftables/iptables rules, or another policy manager is present, use a clean course VM rather than assuming this simplified policy owns the whole machine.

Server, normal user; enter names only after matching hypervisor MACs:

bash
read -r -p 'Server lab NIC verified by MAC: ' LAB_IF
read -r -p 'Server management NIC verified by MAC: ' MGMT_IF
ip -br link show dev "$LAB_IF"
ip -br link show dev "$MGMT_IF"
FW_NOTE=$(mktemp -d "$HOME/network-beginner-firewall.XXXXXX")
printf '%s\n' "$FW_NOTE"

Stop if the directory was not created, names are empty/equal, or the MACs do not match. Record these values outside the shell. Confirm .20/24 belongs to LAB_IF, then save a management baseline:

bash
ip -4 -br address show dev "$MGMT_IF" > "$FW_NOTE/management.before"
ip -4 route > "$FW_NOTE/routes.before"

2A. Ubuntu: UFW

Use this branch only for the UFW-owned guest. Have ufw installed through the guest package preparation if absent. Record whether it was active or inactive.

Ubuntu server, normal user invoking sudo:

bash
sudo ufw status verbose
sudo ufw status numbered
sudo ufw show added
sudo ufw show raw
sudo cat /etc/default/ufw
sudo ufw show added > "$FW_NOTE/ufw-added.before"

Inspect any local /etc/ufw/before.rules, after.rules, and corresponding IPv6 files. An earlier custom accept can bypass the user-rule restriction below. Continue only with understood stock rules and no conflicting custom policy. A general allow 22/tcp allows more sources than our objective; inserting a narrow allow alone does not revoke that broader permission.

If UFW is inactive, this first activation path requires its expected default incoming deny/outgoing allow policy, stock DHCP/reply handling, and no existing inbound management services that depend on an open firewall. Console plus outbound NAT/DHCP management satisfies that path. If existing management SSH/inbound services must remain reachable, retain their already approved scoped policy before activation; do not invent a broad management allow or assume the console test verifies SSH. If these conditions are unclear, keep UFW inactive while resolving the policy.

Add separate client allows for SSH and HTTP, followed by a deny for the same lab endpoints. The order is SSH allow, HTTP allow, lab deny, ahead of ordinary broad user rules. All three target only the lab NIC. Separate allows let the capstone firewall exercise remove HTTP permission without removing SSH permission.

Ubuntu server, normal user invoking sudo:

bash
sudo ufw insert 1 allow in on "$LAB_IF" proto tcp \
  from 192.0.2.10 to 192.0.2.20 port 22 comment 'network-beginner-ssh'
sudo ufw insert 2 allow in on "$LAB_IF" proto tcp \
  from 192.0.2.10 to 192.0.2.20 port 8000 comment 'network-beginner-http'
sudo ufw insert 3 deny in on "$LAB_IF" proto tcp \
  from any to 192.0.2.20 port 22,8000 comment 'network-beginner-other'
sudo ufw show added

Stop if any command fails and use the exact removal below for whichever rules you successfully added. Do not duplicate an existing identical rule: record whether it predated this exercise and leave it owned by its original configuration.

If UFW was inactive and the activation gate above passed, run on the server console sudo ufw enable; read the warning rather than using --force. If already active, rule changes are applied immediately.

Ubuntu server, normal user invoking sudo:

bash
sudo ufw status numbered
sudo ufw status verbose
sudo ufw reload
sudo ufw status numbered

Expect the two source-specific allows before the same-interface/destination deny, in the order above, with preservation after reload. UFW rules added this way are saved as well as applied; it has no firewalld-style per-rule runtime/--permanent split or timeout in this workflow. A rule number is only the current list position, so later cleanup uses the original rule specification.

Perform the shared tests below. To undo only these rules, from the Ubuntu server console:

bash
sudo ufw delete allow in on "$LAB_IF" proto tcp \
  from 192.0.2.10 to 192.0.2.20 port 22
sudo ufw delete allow in on "$LAB_IF" proto tcp \
  from 192.0.2.10 to 192.0.2.20 port 8000
sudo ufw delete deny in on "$LAB_IF" proto tcp \
  from any to 192.0.2.20 port 22,8000
sudo ufw show added

These are the three inverses for full lesson cleanup, not three commands to run for an HTTP-only fault. Confirm only your rules. If you enabled an originally inactive UFW solely for this exercise and are restoring its baseline, run sudo ufw disable after removing your saved rules. Do not disable an originally active firewall. Compare with ufw-added.before. To keep the course policy for lessons 7–8, reapply the three rules in their original order and reverify instead of leaving it cleaned up.

2B. Rocky: firewalld and a dedicated lab zone

Use this branch only when firewalld is the existing active owner. A zone groups policy for associated interfaces/sources. An unspecified NetworkManager connection zone falls back to firewalld's default zone, which may already allow SSH broadly. Adding a narrow allow to that zone would not restrict existing allows.

Rocky server, normal user invoking sudo for firewall inspection:

bash
sudo firewall-cmd --state
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all-zones
sudo firewall-cmd --permanent --list-all-zones
sudo firewall-cmd --list-all-policies
sudo firewall-cmd --permanent --list-all-policies
sudo firewall-cmd --direct --get-all-rules
sudo firewall-cmd --list-all-zones > "$FW_NOTE/firewalld-runtime.before"
sudo firewall-cmd --permanent --list-all-zones > "$FW_NOTE/firewalld-permanent.before"
nmcli -f NAME,UUID,TYPE,DEVICE connection show --active

Record the effective zones of both NICs and the default zone. Inspect source bindings, policies, rich rules, and custom direct rules (the direct query is inspection of legacy state, not a recommendation to create it). They can change classification or acceptance beyond what the interface's zone suggests. This fresh-VM procedure requires no competing source bindings/policies and no unrelated runtime-only changes that a reload would discard. Do not use --runtime-to-permanent to save unknown changes.

Find the UUID of the active lab-only NetworkManager profile from lesson 3. Never choose the management profile or a shared profile.

Rocky server, normal user:

bash
read -r -p 'Active lab-only connection UUID: ' LAB_UUID
nmcli connection show uuid "$LAB_UUID"
LAB_OLD_ZONE=$(nmcli -g connection.zone connection show uuid "$LAB_UUID")
printf '%s\n' "$LAB_OLD_ZONE" > "$FW_NOTE/lab-zone.before"

Verify the profile's NIC/MAC and .20/24. The old zone may legitimately be empty; record that exact value. Check both runtime and permanent zone inventories for network-beginner. If it already exists, stop rather than reusing or deleting someone else's zone.

Rocky server, normal user invoking sudo:

bash
sudo firewall-cmd --permanent --new-zone=network-beginner
sudo firewall-cmd --check-config
sudo firewall-cmd --reload

A new zone must be defined permanently and loaded before this workflow can use it at runtime. Reload replaces runtime policy with permanent policy; this is why unrelated runtime changes must be resolved first. No interface has moved yet.

Set two exact rich-rule strings. They select IPv4, the source, the destination, and the TCP port; association of the zone with LAB_IF supplies the interface boundary.

Rocky server, normal user:

bash
RULE22='rule family="ipv4" source address="192.0.2.10/32" destination address="192.0.2.20/32" port port="22" protocol="tcp" accept'
RULE8000='rule family="ipv4" source address="192.0.2.10/32" destination address="192.0.2.20/32" port port="8000" protocol="tcp" accept'

Rocky server, normal user invoking sudo; ten-minute runtime trial:

bash
sudo firewall-cmd --zone=network-beginner --add-rich-rule="$RULE22" --timeout=600
sudo firewall-cmd --zone=network-beginner --add-rich-rule="$RULE8000" --timeout=600
sudo nmcli connection modify uuid "$LAB_UUID" connection.zone network-beginner
sudo firewall-cmd --get-zone-of-interface="$LAB_IF"
sudo firewall-cmd --get-zone-of-interface="$MGMT_IF"
sudo firewall-cmd --zone=network-beginner --list-all
sudo firewall-cmd --permanent --zone=network-beginner --list-all

NetworkManager's connection.zone change takes effect immediately for an active connection. It changes only the selected profile's firewall zone, not its IP/DHCP/gateway. Expect LAB_IF in the new empty zone with just these two rich accepts, and the management NIC in its original zone. The new zone has no broad ssh service or trusted/ACCEPT target. Its default handling rejects unaccepted new TCP connections, while other protocol handling such as ICMP is a separate policy matter.

Only runtime should contain the trial rules. They expire after 600 seconds and are also lost on reload. This does not remove the zone/profile binding: console recovery remains essential. Do not combine --timeout with --permanent. Run the shared tests during the trial.

To retain the policy, Rocky server, normal user invoking sudo:

bash
sudo firewall-cmd --permanent --zone=network-beginner --add-rich-rule="$RULE22"
sudo firewall-cmd --permanent --zone=network-beginner --add-rich-rule="$RULE8000"
sudo firewall-cmd --check-config
sudo firewall-cmd --reload
sudo firewall-cmd --zone=network-beginner --list-rich-rules
sudo firewall-cmd --permanent --zone=network-beginner --list-rich-rules

Continue only if no unrelated runtime changes were introduced meanwhile. Expect identical saved/runtime rule strings after reload; test a new SSH/HTTP connection again.

To restore, first finish/stop the optional Fail2ban jail below because it may add rules to this zone. Then, on the Rocky server console, restore the recorded profile zone (including an empty value):

bash
sudo nmcli connection modify uuid "$LAB_UUID" connection.zone "$LAB_OLD_ZONE"
sudo firewall-cmd --get-zone-of-interface="$LAB_IF"
sudo firewall-cmd --get-zone-of-interface="$MGMT_IF"

Inspect which of your successful additions remain. Rocky server console, sudo:

bash
sudo firewall-cmd --zone=network-beginner --query-rich-rule="$RULE22"
sudo firewall-cmd --zone=network-beginner --query-rich-rule="$RULE8000"
sudo firewall-cmd --permanent --zone=network-beginner --query-rich-rule="$RULE22"
sudo firewall-cmd --permanent --zone=network-beginner --query-rich-rule="$RULE8000"

yes means present; no and a nonzero exit status mean absent, as after a trial timeout. Run only the matching removal for each present rule that you added:

bash
sudo firewall-cmd --zone=network-beginner --remove-rich-rule="$RULE22"
sudo firewall-cmd --zone=network-beginner --remove-rich-rule="$RULE8000"
sudo firewall-cmd --permanent --zone=network-beginner --remove-rich-rule="$RULE22"
sudo firewall-cmd --permanent --zone=network-beginner --remove-rich-rule="$RULE8000"

Finally:

bash
sudo firewall-cmd --permanent --delete-zone=network-beginner
sudo firewall-cmd --check-config
sudo firewall-cmd --reload

Delete only your now-unused zone, and only reload when unrelated runtime state is safe. Compare the profile's original zone, default zone, management zone/address, and baseline policies. Leave the verified saved lab zone in place instead if continuing lessons 7–8; redo the same trial/validation steps after a rollback.

3. Verify from the client and keep the evidence bounded

Use a fresh key-only SSH connection from the client as in lesson 5. Retained established sessions do not test new rules. For TCP 8000, have python3 available from the earlier HTTP lesson and confirm no other service owns the port.

Server console A, normal user:

bash
WEB_DIR=$(mktemp -d "$HOME/network-beginner-fw-web.XXXXXX")
printf '%s\n' "$WEB_DIR"

Record the path; stop if empty/failed. Then:

bash
printf 'firewall lesson\n' > "$WEB_DIR/index.html"
timeout 120 python3 -m http.server 8000 --bind 192.0.2.20 --directory "$WEB_DIR"

This serves only the scratch directory, bound to the lab IPv4 address, for at most two minutes. Do not put secrets or symlinks there. In server console B, sudo ss -ltnp 'sport = :8000' should show .20:8000.

Client, normal user:

bash
curl --disable --noproxy '*' --connect-timeout 2 --max-time 5 \
  -i http://192.0.2.20:8000/

Expect HTTP 200 and firewall lesson in the body. A failed test does not justify a broad allow: check listener, route, zone/interface, source, and logs in that order. Inspect configuration to establish the intended source restriction; success from the allowed client does not test denial from every other source. A localhost/server-self test is not an external denied-client test.

After the server times out (status 124 is normal) or you interrupt it with Ctrl+C, server console A, normal user: rm -i -- "$WEB_DIR/index.html" then rmdir -- "$WEB_DIR". Verify management address/routes against FW_NOTE, plus any required new management SSH login. DHCP renewals may change lease timing independently.

4. Rocky SELinux: repair a label, not the entire policy

SELinux is an additional process/resource policy, separate from Unix owner/mode and the firewall. Enforcing applies policy and logs denials; permissive logs would-be denials without enforcing them; disabled means SELinux enforcement is absent. This lab does not switch modes. Ubuntu commonly uses AppArmor instead; do not install or switch to SELinux just to follow this alternate branch.

Rocky server, normal user; sudo for audit log access:

bash
getenforce
sestatus
ls -Z "$HOME"
sudo ausearch -m AVC,USER_AVC -ts recent -i

Use this exercise on the prepared SELinux-enabled guest. If disabled, return to a correctly prepared snapshot rather than trying a live global relabel. The audit tools must be present to use ausearch. No matches means no matching retained audit records in that time window, not proof that policy can never deny access.

An AVC record identifies the attempted permission, process, object, source context (scontext), and target context (tcontext). Read the executable/path and time together. An SELinux context is often user:role:type:level; the type is central to targeted policy. Do not blindly feed every denial to audit2allow: a wrong label or bad application path is often the cause.

Rocky server, normal user; create one private scratch file:

bash
SEL_DIR=$(mktemp -d "$HOME/network-beginner-selinux.XXXXXX")
printf '%s\n' "$SEL_DIR"

Stop on failure and record the exact path, then:

bash
touch "$SEL_DIR/probe.txt"
ls -Z "$SEL_DIR/probe.txt"
matchpathcon "$SEL_DIR/probe.txt"
sudo restorecon -v "$SEL_DIR/probe.txt"
matchpathcon -V "$SEL_DIR/probe.txt"

The policy lookup supplies this path's expected context. First establish that baseline; do not memorize one type for every home layout. Now deliberately set the file to a different existing type and repair it.

Rocky server, normal user invoking sudo where shown:

bash
sudo chcon -t httpd_sys_content_t "$SEL_DIR/probe.txt"
ls -Z "$SEL_DIR/probe.txt"
sudo restorecon -n -v "$SEL_DIR/probe.txt"
sudo restorecon -v "$SEL_DIR/probe.txt"
matchpathcon -V "$SEL_DIR/probe.txt"

chcon changes this file's label; it does not create a persistent file-context rule. restorecon -n previews without changing, and restorecon restores the policy-defined path label. Expect the final verification to agree with policy. The private parent directory means this is a label exercise, not a web-content deployment; no AVC is promised because no confined application was deliberately made to access it.

Persistent custom paths may need a deliberately scoped semanage fcontext mapping before restorecon, but none is needed here. Cleanup, Rocky server normal user, is rm -i -- "$SEL_DIR/probe.txt" and rmdir -- "$SEL_DIR". If the exercise stopped after chcon, run the same targeted restorecon before cleanup. Never disable SELinux globally or relabel the whole filesystem for this file.

5. Optional packages and Rocky 9 EPEL/CRB

The core firewall/SELinux lesson is complete without Fail2ban or iftop. This section also supplies the Rocky prerequisites referenced by lesson 7's optional iftop path. Package installation happens only in the disposable guest, through its management connection.

For Ubuntu server, inspect apt-cache policy fail2ban python3-systemd first. When candidates are available from the configured Ubuntu repositories, sudo apt update then sudo apt install fail2ban python3-systemd supplies Fail2ban and its journal backend. If there is no candidate, inspect official repository-component configuration instead of adding a random download source.

For Rocky 9, EPEL is Fedora's additional Enterprise Linux repository; CRB supplies dependencies. It is not a reason to use EPEL Next, a different EL major release, or disable package-signature checking.

Rocky guest, normal user; record original repository/package state:

bash
dnf repolist --all
rpm -q epel-release dnf-plugins-core fail2ban-server python3-systemd

Record whether CRB/EPEL were already enabled and which packages existed; take a snapshot before-optional-packages. Then Rocky guest, normal user invoking sudo:

bash
sudo dnf install dnf-plugins-core
sudo dnf config-manager --set-enabled crb
sudo dnf install epel-release
dnf repolist --enabled
dnf info fail2ban-server python3-systemd iftop

Check that EPEL matches 9, expected repositories/signatures are used, and candidates exist for the guest architecture. For Fail2ban use sudo dnf install fail2ban-server python3-systemd; for the optional lesson 7 tool install only iftop there. Inspect transactions and stop on unresolved dependencies; package versions change, so no exact package build is assumed.

Do not disable CRB/EPEL if they predated the exercise or other installed software needs them. If this was only a temporary experiment, restore before-optional-packages after all dependent lessons. That is the exact rollback for packages/repositories; disabling a repository alone does not undo installed packages.

6. Optional Fail2ban jail with lab-scoped actions

Fail2ban reads authentication failure logs through a filter, counts events within findtime, and invokes an action when maxretry is reached. A jail combines these settings. It cannot fix weak keys, replace the firewall's baseline policy, or stop every distributed/slow attack. A wrong filter/backend can leave a running service that detects nothing.

This optional setup assumes a new Fail2ban installation with no existing custom jails. If it is already in use, inspect its owners/configuration first; do not stop another workload's protection. Record initial service/enablement state. On a new install it may start automatically: use the console to stop only this new service while preparing sudo systemctl stop fail2ban. Review /etc/fail2ban/jail.conf, jail.d, and installed actions. Do not edit packaged jail.conf.

Create one new action file for the selected manager, refusing overwrites.

Server, normal user invoking sudo:

bash
sudo sh -c 'umask 077; set -C; : > /etc/fail2ban/action.d/network-beginner.conf'
sudoedit /etc/fail2ban/action.d/network-beginner.conf

For UFW only, put this INI content in the new file, replacing REPLACE_WITH_LAB_NIC with the MAC-verified server NIC name. Do not leave the placeholder.

ini
[Definition]
actionstart =
actionstop =
actioncheck = LC_ALL=C ufw status | grep -q 'Status: active'
actionban = ufw insert 1 deny in on REPLACE_WITH_LAB_NIC proto tcp from <ip> to 192.0.2.20 port 22 comment 'network-beginner-ban'
actionunban = ufw delete deny in on REPLACE_WITH_LAB_NIC proto tcp from <ip> to 192.0.2.20 port 22

For firewalld only, use this content instead; the course zone must still belong exclusively to the lab NIC:

ini
[Definition]
actionstart =
actionstop =
actioncheck = firewall-cmd --zone=network-beginner --list-all
actionban = firewall-cmd --zone=network-beginner --add-rich-rule='rule family="ipv4" priority="-10" source address="<ip>" destination address="192.0.2.20/32" port port="22" protocol="tcp" reject'
actionunban = firewall-cmd --zone=network-beginner --remove-rich-rule='rule family="ipv4" priority="-10" source address="<ip>" destination address="192.0.2.20/32" port port="22" protocol="tcp" reject'

These actions constrain a detected source to lab interface/zone, destination .20, TCP 22; they do not ban that source from the management address or TCP 8000. UFW insertion before the allow and firewalld's negative priority make the temporary deny precede the lab allow. Fail2ban itself removes the rule after bantime; do not reload the underlying firewall during this optional experiment. This timer belongs to Fail2ban, not to the UFW rule: if the daemon fails, inspect and remove only the leftover course ban using its exact actionunban specification rather than assuming it expired.

Server, normal user invoking sudo; exclusive jail creation:

bash
sudo sh -c 'umask 077; set -C; : > /etc/fail2ban/jail.d/99-network-beginner.local'
sudoedit /etc/fail2ban/jail.d/99-network-beginner.local

Use this INI for Ubuntu. On Rocky, replace only ssh.service with sshd.service in journalmatch.

ini
[sshd]
enabled = false

[network-beginner-sshd]
enabled = true
filter = sshd
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service
usedns = no
ignoreip = 127.0.0.0/8 ::/0
port = 22
maxretry = 3
findtime = 60
bantime = 60
action = network-beginner

The [sshd] setting prevents a package-provided default SSH jail from applying a second, broader action on this fresh install. Confirm no other jail is enabled; do not use this to disable existing custom protection. The named jail explicitly uses the sshd filter and your new action. backend=systemd reads the journal, so do not add logpath. journalmatch must match actual log unit metadata. ::/0 intentionally excludes IPv6 from this IPv4-only demonstration; it is not a production protection policy.

Server, normal user invoking sudo; validate before start:

bash
sudo journalctl -u ssh.service -b -n 10 -o verbose --no-pager
sudo fail2ban-client -t
sudo fail2ban-client -d

On Rocky use -u sshd.service. Inspect _SYSTEMD_UNIT in actual authentication-related records; no matching records means backend coverage remains unproven. -t tests configuration, while -d dumps the interpreted configuration without starting enforcement. Verify jail name, journal backend/match, one expected action, bounded times, and absence of competing enabled jails.

Server, normal user invoking sudo:

bash
sudo systemctl start fail2ban
sudo fail2ban-client status
sudo fail2ban-client status network-beginner-sshd
sudo fail2ban-client get network-beginner-sshd journalmatch
sudo fail2ban-client get network-beginner-sshd actions
sudo fail2ban-client get network-beginner-sshd action network-beginner actionban
sudo journalctl -u fail2ban -b -n 30 --no-pager

Expect the intended jail and expanded, scoped action command, with no startup errors. Zero failures/bans is normal. Do not run a brute-force loop or intentionally ban yourself. These checks establish configuration/startup, not demonstrated attack detection or enforcement. Wrong service metadata, missing Python journal support, a filter mismatch, or action execution errors must be resolved before claiming protection works.

If an accidental ban lists the known lab client, server console, sudo:

bash
sudo fail2ban-client set network-beginner-sshd unbanip 192.0.2.10
sudo fail2ban-client status network-beginner-sshd

This requests an exact single-IP unban. Verify that the corresponding UFW/rich rule is absent and a fresh SSH connection succeeds; an existing session is insufficient. Do not flush all bans or firewall rules.

For cleanup, unban any addresses actually listed in this jail, one exact address at a time, and confirm action removal. Then sudo fail2ban-client stop network-beginner-sshd, stop the service only if it was originally inactive/new, and remove only the two files created here with sudo rm -i -- /etc/fail2ban/jail.d/99-network-beginner.local /etc/fail2ban/action.d/network-beginner.conf. Verify the jail is gone and no course ban remains. Do this before deleting the firewalld zone. Restore recorded service/enablement state; full optional-install rollback uses the snapshot.

Completion and course handoff

You should be able to show the selected manager, correct interface/zone, exact .10.20 TCP 22/8000 rules, new permitted SSH/HTTP connections, unchanged management, and the inverse operations. Explain the difference between UFW saved rules and firewalld runtime/permanent rules, and between a correct file mode and a correct SELinux type. Report optional Fail2ban as “configuration verified” unless you actually have additional validated evidence.

Keep the verified source-scoped saved firewall policy for lesson 7. If you practiced removal, reapply and verify it first. No HTTP process, deliberately wrong label, or temporary Fail2ban jail is required for the next lesson.

When no longer needed, remove the exact baseline files you created in FW_NOTE (management.before, routes.before, and the chosen branch's listed baseline files), then rmdir the empty directory. Restore recorded profile/firewall state only at final course cleanup. Never delete an unknown zone/rule merely because its name looks familiar.

Primary references

Checked September 15, 2026:

Quiz | Continue to monitoring