# 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](https://www.atomai.click/kubernetes-docs/llms/en/networking/beginner/05-ssh-access.md) | [Course](https://www.atomai.click/kubernetes-docs/llms/en/networking/beginner/README.md) | [Quiz](https://www.atomai.click/kubernetes-docs/en/quizzes/networking/beginner/06-firewalls-host-security-quiz) | [Next: Monitoring](https://www.atomai.click/kubernetes-docs/llms/en/networking/beginner/07-monitoring-performance.md)

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](https://www.atomai.click/kubernetes-docs/llms/en/networking/beginner/08-container-cloud-capstone.md#firewall-fault) 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 {#epel-prerequisites}

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](https://www.atomai.click/kubernetes-docs/llms/en/networking/beginner/07-monitoring-performance.md). 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:

- [Ubuntu UFW manual](https://manpages.ubuntu.com/manpages/noble/man8/ufw.8.html) — rule direction, interfaces, ordering, persistence, and exact deletion.
- [firewalld CLI](https://firewalld.org/documentation/man-pages/firewall-cmd.html), [rich language](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html), and [zones](https://firewalld.org/documentation/zone/connections-interfaces-and-sources.html) — timeouts, reload, source/destination rules, and classification.
- [NetworkManager connection settings](https://networkmanager.dev/docs/api/latest/settings-connection.html) — immediate active-connection `zone` changes and empty/default behavior.
- [Rocky Linux 9 SELinux guide](https://docs.rockylinux.org/9/guides/security/learning_selinux/) and [SELinux labeling tools](https://github.com/SELinuxProject/selinux/tree/main/policycoreutils/setfiles) — AVC diagnosis and targeted context restoration.
- [Fedora EPEL getting started](https://docs.fedoraproject.org/en-US/epel/getting-started/), [Rocky repository documentation](https://wiki.rockylinux.org/rocky/repo/), and [EPEL Fail2ban packages](https://packages.fedoraproject.org/pkgs/fail2ban/fail2ban-server/) — optional package sources and prerequisites.
- [Fail2ban jail configuration](https://github.com/fail2ban/fail2ban/blob/master/config/jail.conf), [systemd backend](https://github.com/fail2ban/fail2ban/blob/master/fail2ban/server/filtersystemd.py), and [client manual](https://manpages.ubuntu.com/manpages/noble/man1/fail2ban-client.1.html) — filter/backend/action separation, configuration tests, status, and scoped unban.

[Quiz](https://www.atomai.click/kubernetes-docs/en/quizzes/networking/beginner/06-firewalls-host-security-quiz) | [Continue to monitoring](https://www.atomai.click/kubernetes-docs/llms/en/networking/beginner/07-monitoring-performance.md)
