Linux hardening: passwords, faillock, fail2ban, auditd and OpenSCAP
-
Phase 0: Before you start
-
Phase 1: The origins and OS architecture
-
Phase 2: Day-to-day administration and the command builder
-
Phase 3: Enterprise infrastructure and storage
-
Phase 4: Troubleshooting step by step
-
Phase 5: Final project and career
Linux hardening means removing the easy ways in, making the remaining doors hard to force, and recording who did what, so that an attacker (or a careless colleague) has less to work with and you can prove what happened afterwards. It is never one setting. It is a set of small, boring controls that together make a server pass an audit and survive a bad day.
You already did two big pieces: SSH keys only with root login off in Lesson 33, and a firewall that allows only what you serve in Lesson 34. This lesson adds the rest on rocky01: password quality rules, account lockout with faillock, fail2ban to ban addresses that keep failing, auditd rules that record changes to users and sudo, a look at crypto policies, and an OpenSCAP scan against the CIS benchmark that tells you what is still missing. ubu01 differences are covered at the end of the lab.
Where this lesson fits
Lesson 55 of 82 in Linux Administration · Phase 3: Enterprise infrastructure and storage · Before this: Lesson 54, join Linux to Active Directory · Next: Lesson 56, Nginx and MariaDB
| Time | You need | By the end you can |
|---|---|---|
| About 90 minutes | rocky01 and ubu01 in the state Lesson 54 left them (keys only, firewalls on, joined to corp.saatlab.test); priya from Lesson 15 | Set password and lockout policy, ban SSH attackers, audit changes to identity files, run a CIS scan, read it and fix a finding |
What it is, in one line
Hardening is closing what you don’t need, protecting what you keep with strong authentication and limits, recording important changes, and checking the result against a published benchmark.
In Simple Words
Locking up a house in a busy colony
When a family moves into a new house, they first close the back door nobody uses and fix a proper lock on the front door. They tell the children not to use 1234 as the code for the almirah, and the watchman at the gate is told to stop anyone who tries five wrong keys in a row.
Then they put a camera that records who opens the almirah and when, and once a year a friend who is a police officer walks around with a checklist and tells them what they missed. The checklist (CIS benchmark) and the walk-around (OpenSCAP) don’t make the house safe by themselves; they show you what’s still open.
Engineer's View
The layers on rocky01 and the tool for each
| Layer | Tool | What it does | Where it lives on rocky01 |
|---|---|---|---|
| Remote login | sshd (Lesson 33) | Keys only, no root login | /etc/ssh/sshd_config.d/00-saat-hardening.conf |
| Network | firewalld (Lesson 34) | Only the ports you serve | firewall-cmd --list-all |
| Password quality | pam_pwquality | Rejects short, simple or dictionary passwords for local users | /etc/security/pwquality.conf.d/50-saatlab.conf |
| Lockout | pam_faillock | Locks a local account after N failures for a time | /etc/security/faillock.conf and authselect with-faillock |
| Network bans | fail2ban | Reads the sshd log and adds a firewall rule against an address that keeps failing | /etc/fail2ban/jail.d/sshd.local |
| Audit trail | auditd | The kernel records reads, writes and system calls you choose, with the original user (auid) | /etc/audit/rules.d/50-saatlab.rules |
| Crypto | crypto-policies | One system-wide setting for allowed TLS, SSH and Kerberos algorithms | update-crypto-policies --show |
| Checking | OpenSCAP + SCAP Security Guide | Tests hundreds of rules from a benchmark and writes an HTML report | oscap xccdf eval |
The CIS benchmarks (from the Center for Internet Security) are the checklists most companies and auditors use. Each one has Level 1 (sensible for almost every server, low risk of breaking things) and Level 2 (stricter, for sensitive systems, more likely to affect applications), with server and workstation variants. The SCAP Security Guide turns them, and others such as DISA STIG, PCI-DSS and ANSSI, into machine-checkable profiles that the oscap scanner runs.
pam_pwquality and pam_faillock protect local accounts. AD users from Lesson 54 get their password and lockout rules from the domain, which is why RHEL’s PAM line for pwquality says local_users_only.
How it works, step by step
Hardening is a loop, not a one-off job, and the diagram below shows the loop most teams follow.
1Start from a benchmark and measure first
Pick a target, usually CIS Level 1 for servers, and scan before you change anything. The first report on a fresh install is long and a little alarming, and that’s normal: some findings are quick fixes, some need a design change (separate partitions), and some don’t apply to your environment and become documented exceptions.
2Remove what you don’t need
Every listening port, enabled service and installed package is something to patch and something that can be attacked. ss -tulpn shows what listens, systemctl list-unit-files --state=enabled shows what starts at boot, and the firewall should allow only what you deliberately serve. Ask “who needs this?” for each one before you disable it, because a service another team depends on looks unused to you.
3Make passwords hard to guess
pam_pwquality runs when a password is set or changed. It checks length (minlen), how many character classes are used (minclass: lower, upper, digits, others), repeated characters and a cracklib dictionary. Root is warned but not stopped unless you add enforce_for_root.
4Slow down guessing, locally and from the network
pam_faillock counts failed logins per account and locks it for a while after a set number; this protects console, su and sudo logins. fail2ban works per IP address: it watches the sshd log and, after a few failures from one address, adds a firewall rule that blocks it for a while. With SSH keys only, fail2ban matters less for break-ins, but it still cuts the noise and the load from scanners.
5Record who changed what
auditd collects events from the kernel’s audit system and writes them to /var/log/audit/audit.log. A watch rule like -w /etc/passwd -p wa -k identity records every write or attribute change to that file, with the process, the user it ran as and the audit user (auid), the person who logged in, which stays the same through sudo and su. The picture below shows which layer stops which kind of attempt on rocky01.
6Check the result and keep checking
Run the scanner again after changes, keep the HTML report as evidence, and schedule scans so drift (someone “temporarily” turning something off) shows up. Some rules are fixed better by your build process, such as Kickstart or Ansible (Lesson 64), than by hand on each server.
Crypto policies belong in this step too. On the RHEL family, update-crypto-policies sets the allowed algorithms for OpenSSL, GnuTLS, OpenSSH, Kerberos and Java at once. DEFAULT is right for most servers; FUTURE is stricter and on RHEL 10 can break connections to older services, LEGACY loosens things for old peers, and FIPS is set at install time.
Hands-on lab: harden rocky01 and scan it against CIS
Before you run any command, double-check it
Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.
Steps 1 to 9 run on rocky01 as student (step 5 also uses ubu01 as the “attacker”), step 10 runs on ubu01. Take snapshots of both first (VBoxManage snapshot rocky01 take before-l55 and the same for ubu01), and keep a VirtualBox console window open on rocky01 logged in as root or student, so a lockout mistake can’t shut you out. Start with mkdir -p ~/lab55.
1See what Lessons 33 and 34 already did
Run this on rocky01:
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|pubkeyauthentication) '
sudo firewall-cmd --list-services
sudo ss -tulpn
systemctl list-unit-files --type=service --state=enabled --no-legend | awk '{print $1}' | paste -sd ' 'You should see something like this:
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
cockpit dhcpv6-client ntp ssh
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.1:323 0.0.0.0:* users:(("chronyd",pid=845,fd=5))
udp UNCONN 0 0 [::1]:323 [::]:* users:(("chronyd",pid=845,fd=6))
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1012,fd=7))
tcp LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1012,fd=8))
auditd.service chronyd.service crond.service firewalld.service getty@.service irqbalance.service NetworkManager-dispatcher.service NetworkManager-wait-online.service NetworkManager.service oddjobd.service rsyslog.service selinux-autorelabel-mark.service sshd.service sssd.service sysstat.service tuned.serviceSSH is key-only with root login off, as Lesson 33 left it, so this lesson doesn’t touch sshd.
Only sshd listens on the network; chronyd’s port 323 is its local control socket. Every enabled service has a reason to be there: sssd and oddjobd for AD logins, sysstat for sar. The firewall still allows cockpit, the web console, which isn’t even installed on this minimal server.
✅ Checkpoint: You can name every listening port on rocky01 and the service behind it.
2Close what you don’t use
Remove the unused web console from the firewall zone:
sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --reload
sudo firewall-cmd --list-servicesYou should see something like this:
success
success
dhcpv6-client ntp sshThe rule opened port 9090 for a service that doesn’t exist here, so nothing broke, but the day someone installs cockpit for a quick look it would be reachable from the whole network without anyone deciding that. ntp stays because ubu01 takes its time from rocky01 (Lesson 40). dhcpv6-client stays too: it only lets DHCPv6 replies in, and removing it on a server that uses IPv6 autoconfiguration causes odd problems later. On a real server you go through the enabled services the same way and write down why each one stays.
✅ Checkpoint: firewall-cmd --list-services shows only dhcpv6-client, ntp and ssh.
3Set password quality rules
Create a drop-in file, then test some passwords against it:
sudo mkdir -p /etc/security/pwquality.conf.d
sudo tee /etc/security/pwquality.conf.d/50-saatlab.conf >/dev/null <<'EOF'
# SaatVedha Linux course, Lesson 55: password quality (CIS Level 1 values)
minlen = 14
minclass = 4
maxrepeat = 3
dictcheck = 1
enforce_for_root
EOF
grep pam_pwquality /etc/pam.d/system-auth
for p in 'Saatvedha2026' 'password12345678' 'Administrator1!' 'Coffee-Filter-Mysuru-42'; do echo "$p" | pwscore; doneYou should see something like this:
password requisite pam_pwquality.so local_users_only
Password quality check failed:
The password is shorter than 14 characters
Password quality check failed:
The password contains less than 4 character classes
Password quality check failed:
The password fails the dictionary check - it is based on a dictionary word
100These are the CIS Level 1 values for RHEL 10: at least 14 characters, all four character classes, no more than 3 identical characters in a row, and a dictionary check. pwscore (from libpwquality) applies the same rules as PAM and prints a score from 0 to 100 for a password that passes. The drop-in folder keeps your settings separate from the package’s pwquality.conf, the same idea as sshd_config.d.
Now set a new password for priya as root, first a short one, then a good one you’ll remember:
sudo passwd priyaYou should see something like this:
Changing password for user priya.
New password:
BAD PASSWORD: The password is shorter than 14 characters
New password:
Retype new password:
passwd: all authentication tokens updated successfully.Because of enforce_for_root, even root got a second prompt instead of only a warning. Existing passwords are not checked until they are changed, so a new policy takes effect gradually; that’s why auditors also ask for password ageing (chage, Lesson 15).
✅ Checkpoint: pwscore rejects the first three test passwords, and priya now has a password of 14 or more characters.
4Lock accounts after failed logins
Switch on faillock in the PAM profile and set the CIS values:
authselect current
sudo authselect enable-feature with-faillock
printf '# SaatVedha Lesson 55 (CIS Level 1)\ndeny = 5\nunlock_time = 900\n' | sudo tee -a /etc/security/faillock.conf >/dev/null
grep -E '^(deny|unlock_time)' /etc/security/faillock.conf
grep -c pam_faillock /etc/pam.d/system-auth /etc/pam.d/password-auth
authselect currentYou should see something like this:
Profile ID: sssd
Enabled features:
- with-mkhomedir
deny = 5
unlock_time = 900
/etc/pam.d/system-auth:3
/etc/pam.d/password-auth:3
Profile ID: sssd
Enabled features:
- with-mkhomedir
- with-faillockThe profile is sssd with mkhomedir because realm join set it in Lesson 54 (a fresh Rocky 10 install starts with the “local” profile). Never edit /etc/pam.d/system-auth by hand on the RHEL family: authselect owns it and would overwrite your change. The defaults in faillock.conf are 3 failures and a 10-minute lock; you set 5 failures and 15 minutes (unlock_time is in seconds).
Test it: as student, try su to priya twice with a wrong password, look at the count, then log in correctly:
su - priya
su - priya
sudo faillock --user priya
su - priya -c 'echo logged in'
sudo faillock --user priyaYou should see something like this:
Password:
su: Authentication failure
Password:
su: Authentication failure
priya:
When Type Source Valid
2026-10-27 15:32:10 TTY pts/0 V
2026-10-27 15:32:18 TTY pts/0 V
Password:
logged in
priya:
When Type Source ValidEach failure is recorded with time, type (TTY for a local terminal, RHOST for a network login) and source. A successful login clears the list, so only failures in a row count, within fail_interval (15 minutes by default). The counts live in /var/run/faillock, which is in RAM, so a reboot clears them too.
✅ Checkpoint: faillock showed two failures for priya, and the list was empty after her successful login.

5Ban repeated SSH failures with fail2ban
Install fail2ban from EPEL and give the sshd jail your settings:
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
# SaatVedha Linux course, Lesson 55
[sshd]
enabled = true
backend = systemd
maxretry = 3
findtime = 10m
bantime = 15m
ignoreip = 127.0.0.1/8 ::1 10.10.10.1
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshdYou should see something like this (the dnf part is cut):
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-session
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:fail2ban on Rocky comes from EPEL with a firewalld action already set (/etc/fail2ban/jail.d/00-firewalld.conf), so bans become firewalld rich rules. Never edit jail.conf itself: put your settings in a .local file, which wins and survives package updates. The jail reads the journal, matching both the sshd and the sshd-session process names that OpenSSH 10 uses. ignoreip keeps 10.10.10.1 safe, the address your laptop’s port-forwarded connections arrive from (check yours with who).
On ubu01, try to log in four times as a user that doesn’t exist:
for i in 1 2 3 4; do ssh -o BatchMode=yes -o ConnectTimeout=5 nosuch@10.10.10.11 true; doneYou should see something like this:
nosuch@10.10.10.11: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
nosuch@10.10.10.11: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
nosuch@10.10.10.11: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
ssh: connect to host 10.10.10.11 port 22: Connection refusedBack on rocky01, see the ban, the firewall rule behind it, and lift it:
sudo fail2ban-client status sshd
sudo firewall-cmd --list-rich-rules
sudo fail2ban-client set sshd unbanip 10.10.10.21You should see something like this:
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 3
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-session
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 10.10.10.21
rule family="ipv4" source address="10.10.10.21" port port="ssh" protocol="tcp" reject type="icmp-port-unreachable"
1Each attempt logged an “Invalid user nosuch from 10.10.10.21” line, which the sshd filter counts as one failure. After the third, fail2ban added a rich rule that rejects SSH from ubu01, so the fourth try was refused at the firewall before sshd saw it.
The ban would end by itself after 15 minutes; unbanip ends it now (the 1 means one address was unbanned). Lift it before you go on, because later lessons SSH from ubu01 to rocky01.
✅ Checkpoint: ubu01 can SSH to rocky01 again (ssh rocky01 hostname from ubu01 prints rocky01) and the banned list is empty.
6Record changes to users, sudo and SSH settings with auditd
Add watch rules, load them and list what the kernel now has:
systemctl is-active auditd
sudo tee /etc/audit/rules.d/50-saatlab.rules >/dev/null <<'EOF'
## SaatVedha Linux course, Lesson 55: watch identity, sudo and SSH settings
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
EOF
sudo augenrules --check
sudo augenrules --load >/dev/null
sudo auditctl -lYou should see something like this:
active
/usr/sbin/augenrules: Rules have changed and should be updated
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d -p wa -k sudoers
-w /etc/ssh/sshd_config.d -p wa -k sshd_configauditd runs by default on the RHEL family. augenrules joins every .rules file in /etc/audit/rules.d into /etc/audit/audit.rules and loads it with auditctl, so you never need to restart auditd (on Rocky, systemctl restart auditd is refused on purpose). -p wa means writes and attribute changes, and -k is a key you search by later.
Make a change through sudo and find out who did it:
sudo useradd -M -s /sbin/nologin auditdemo
sudo userdel auditdemo
sudo ausearch -k identity --start recent -i | grep '^type=SYSCALL' | tail -n 1
sudo aureport -k --summaryYou should see something like this:
type=SYSCALL msg=audit(10/27/2026 15:48:21.304:1342) : arch=x86_64 syscall=rename success=yes exit=0 a0=0x7ffd3c5e8a10 a1=0x55e1b0c4f2a0 a2=0x0 a3=0x0 items=5 ppid=4102 pid=4105 auid=student uid=root gid=root euid=root suid=root fsuid=root egid=root sgid=root fsgid=root tty=pts0 ses=4 comm=userdel exe=/usr/sbin/userdel subj=unconfined_u:unconfined_r:useradd_t:s0-s0:c0.c1023 key=identity
Key Summary Report
===========================
total key
===========================
8 identity-i turns numbers into names. The command ran as uid=root, but auid=student shows the person who logged in and used sudo, and that survives su and sudo, which is what an auditor asks for. comm and exe say which program changed the file, and key=identity ties it to your rule. At work these logs are forwarded off the server (Lesson 29), so someone with root can’t quietly delete them.
✅ Checkpoint: auditctl -l lists seven rules and ausearch shows an event with auid=student and key=identity.
7Read the system crypto policy
Run this:
update-crypto-policies --show
ls /usr/share/crypto-policies/policies/You should see something like this:
DEFAULT
DEFAULT.pol FIPS.pol FUTURE.pol LEGACY.pol modulesrocky01 uses DEFAULT, which on RHEL 10 already drops TLS RSA key exchange and SHA-1 signatures in TLS. Leave it there unless a benchmark or customer requires something else: sudo update-crypto-policies --set FUTURE and a reboot would tighten every library at once, but on RHEL 10 FUTURE allows only post-quantum hybrid key exchange and can break connections to older systems. The modules folder holds sub-policies you can add with a colon, for example to switch off one algorithm; ls it to see which your version ships.
✅ Checkpoint: update-crypto-policies --show prints DEFAULT.
8Scan rocky01 against CIS Level 1 with OpenSCAP
Install the scanner and the content, find the data stream file and list its profiles:
sudo dnf install -y openscap-scanner scap-security-guide
ls /usr/share/xml/scap/ssg/content/ | grep -E 'ds\.xml$'
DS=$(ls /usr/share/xml/scap/ssg/content/ssg-rl10-ds.xml 2>/dev/null || ls /usr/share/xml/scap/ssg/content/ssg-rhel10-ds.xml)
echo "$DS"
oscap info --profiles "$DS"You should see something like this:
ssg-rl10-ds.xml
/usr/share/xml/scap/ssg/content/ssg-rl10-ds.xml
xccdf_org.ssgproject.content_profile_anssi_bp28_enhanced:ANSSI-BP-028 (enhanced)
xccdf_org.ssgproject.content_profile_anssi_bp28_high:ANSSI-BP-028 (high)
xccdf_org.ssgproject.content_profile_anssi_bp28_intermediary:ANSSI-BP-028 (intermediary)
xccdf_org.ssgproject.content_profile_anssi_bp28_minimal:ANSSI-BP-028 (minimal)
xccdf_org.ssgproject.content_profile_cis:CIS Red Hat Enterprise Linux 10 Benchmark for Level 2 - Server
xccdf_org.ssgproject.content_profile_cis_server_l1:CIS Red Hat Enterprise Linux 10 Benchmark for Level 1 - Server
xccdf_org.ssgproject.content_profile_cis_workstation_l1:CIS Red Hat Enterprise Linux 10 Benchmark for Level 1 - Workstation
xccdf_org.ssgproject.content_profile_cis_workstation_l2:CIS Red Hat Enterprise Linux 10 Benchmark for Level 2 - Workstation
xccdf_org.ssgproject.content_profile_e8:Australian Cyber Security Centre (ACSC) Essential Eight
xccdf_org.ssgproject.content_profile_hipaa:Health Insurance Portability and Accountability Act (HIPAA)
xccdf_org.ssgproject.content_profile_ism_o:Australian Cyber Security Centre (ACSC) ISM Official - Base
xccdf_org.ssgproject.content_profile_ism_o_secret:Australian Cyber Security Centre (ACSC) ISM Official - Secret
xccdf_org.ssgproject.content_profile_ism_o_top_secret:Australian Cyber Security Centre (ACSC) ISM Official - Top Secret
xccdf_org.ssgproject.content_profile_pci-dss:PCI-DSS v4.0.1 Control Baseline for Red Hat Enterprise Linux 10
xccdf_org.ssgproject.content_profile_stig:Red Hat STIG for Red Hat Enterprise Linux 10
xccdf_org.ssgproject.content_profile_stig_gui:Red Hat STIG with GUI for Red Hat Enterprise Linux 10Rocky’s package normally ships its own data stream (ssg-rl10-ds.xml); the line with || falls back to the RHEL file if yours is named that way. Profile IDs are long, and the list changes between versions: Red Hat dropped the OSPP profile from RHEL 10, for example, so always copy the ID from oscap info on your own server.
Run the CIS Level 1 server scan (it takes a few minutes) and count the results:
sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis_server_l1 --results ~/lab55/cis-results.xml --report ~/lab55/cis-report.html "$DS" > ~/lab55/cis-scan.txt
echo "exit code: $?"
grep -E '^Result' ~/lab55/cis-scan.txt | sort | uniq -c
grep -B3 -E '^Result\s+fail' ~/lab55/cis-scan.txt | grep '^Title' | head -n 8You should see something like this (your counts will differ):
exit code: 2
197 Result pass
98 Result fail
19 Result notapplicable
7 Result notchecked
Title Ensure /tmp Located On Separate Partition
Title Set Boot Loader Password in grub2
Title Install AIDE
Title Disable Modprobe Loading of USB Storage Driver
Title Set SSH Client Alive Interval
Title Set SSH authentication attempt limit
Title Set Password Maximum Age
Title Set Existing Passwords Maximum AgeExit code 2 means “the scan worked and at least one rule failed” (1 would be an error). Each rule prints four lines (Title, Rule, Ident with its CCE number, Result) and the counts come from those.
The failures sort into kinds: a design choice made at install time (/tmp on its own partition), quick fixes (the USB storage module), settings that need a decision with the app team (SSH idle timeout) and policy items (password ageing). The HTML report has the same list with a description and a fix script per rule; copy it to the laptop with scp -P 2211 student@127.0.0.1:lab55/cis-report.html . and open it in a browser.
[Add your screenshot here]: the top of cis-report.html in a browser, showing the score and the pass and fail counts for rocky01.
✅ Checkpoint: The scan finished with exit code 2, and you have cis-report.html, cis-results.xml and cis-scan.txt in ~/lab55.
9Fix one finding and check it again
Block the USB storage driver, then re-check that rule and two you already fixed:
printf 'install usb-storage /bin/false\nblacklist usb-storage\n' | sudo tee /etc/modprobe.d/usb-storage.conf
sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis_server_l1 --rule xccdf_org.ssgproject.content_rule_kernel_module_usb-storage_disabled --rule xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny --rule xccdf_org.ssgproject.content_rule_service_firewalld_enabled "$DS"You should see something like this:
install usb-storage /bin/false
blacklist usb-storage
--- Starting Evaluation ---
Title Verify firewalld Enabled
Rule xccdf_org.ssgproject.content_rule_service_firewalld_enabled
Ident CCE-88110-2
Result pass
Title Lock Accounts After Failed Password Attempts
Rule xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny
Ident CCE-87388-5
Result pass
Title Disable Modprobe Loading of USB Storage Driver
Rule xccdf_org.ssgproject.content_rule_kernel_module_usb-storage_disabled
Ident CCE-89301-6
Result pass--rule runs only the rules you name, which takes seconds instead of minutes, so you can fix and check one item at a time.
The faillock rule passes because of step 4 (deny 5 and the with-faillock feature), and the firewalld one because of Lesson 34. A USB stick plugged into a server is a common way data leaves a building, and on a VM nothing needs the module. The full re-scan for the evidence file comes after a batch of changes, through your change process.
✅ Checkpoint: All three rules show Result pass.
10Do the same on ubu01
On ubu01, install the Ubuntu packages and look at what they set up:
sudo apt install -y libpam-pwquality fail2ban auditd
grep pam_pwquality /etc/pam.d/common-password
sudo fail2ban-client status sshd | head -n 4
systemctl is-active auditdYou should see something like this:
password requisite pam_pwquality.so retry=3
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
activeOn Ubuntu, installing libpam-pwquality adds it to the PAM stack through pam-auth-update, and the same 50-saatlab.conf drop-in works. fail2ban’s sshd jail is on as soon as it’s installed (systemd backend, nftables ban action), so copy your sshd.local with the same ignoreip. The auditd rules file is the same too. The differences that matter are in the table:
| Control | Rocky Linux 10 (rocky01) | Ubuntu 26.04 (ubu01) |
|---|---|---|
| Password quality | pam_pwquality already in the authselect profile | Install libpam-pwquality; pam-auth-update adds it |
| faillock | authselect enable-feature with-faillock | Not wired in by default; add pam_faillock lines to /etc/pam.d/common-auth and common-account by hand (see man pam_faillock), with a root shell open |
| fail2ban | EPEL, bans through firewalld rich rules | Ubuntu archive, sshd jail on by default, bans through nftables |
| auditd | Installed and running | Install auditd; same rules.d files |
| Crypto policies | update-crypto-policies | No system-wide equivalent; set algorithms per service |
| CIS scanning | openscap-scanner + scap-security-guide (CIS profiles for RHEL 10) | Ubuntu Security Guide (usg) with Ubuntu Pro; the archive’s ssg-debderived content on 26.04 covers 22.04 and 24.04 only, so check whether 26.04 profiles exist before you plan on them |
✅ Checkpoint: On ubu01, pam_pwquality is in common-password, the sshd jail is running and auditd is active.
Break It, Then Fix It
Take a snapshot of rocky01 first (VBoxManage snapshot rocky01 take before-l55-break) and keep the VirtualBox console logged in. Both failures are ones that hardening itself causes, which is why every hardening change needs a way back.
1The right password stops working
As student, fail su to priya five times in a row, then type her correct password:
su - priya
su - priya
su - priya
su - priya
su - priya
su - priyaYou’ll get this, even on the sixth try with the right password:
Password:
su: Authentication failure
(five times with wrong passwords, then the right one)
Password:
su: Authentication failureThe account is locked for 15 minutes, and because authselect calls faillock with silent, nothing tells priya why; to her it looks like the password changed. This is the single most common ticket after faillock is turned on, and it gets worse when an old saved password in a script or a mapped drive keeps failing in the background and locks the account again and again.
Fix: confirm the lock in the log and the tally, then clear it for that user only.
sudo journalctl --since "10 min ago" --no-pager | grep -i 'temporarily locked'
sudo faillock --user priya | tail -n 2
sudo faillock --user priya --reset
su - priya -c 'echo logged in'You should see something like this:
Oct 27 16:05:41 rocky01 su[4388]: pam_faillock(su-l:auth): Consecutive login failures for user priya account temporarily locked
2026-10-27 16:05:27 TTY pts/0 V
2026-10-27 16:05:33 TTY pts/0 V
Password:
logged infaillock --user priya --reset clears only priya’s count. If the same user keeps getting locked, find the source before you reset again: the Source column shows the terminal or the remote address. Root is never locked unless you add even_deny_root, and you should be careful with it; if your own admin account gets locked, the console session you kept open is how you get back in.
✅ Checkpoint: sudo faillock --user priya shows an empty list and priya can log in.
2One typo hides half the audit rules
Remove the dash in front of k on the sudoers.d line, the kind of slip that happens when a rule is typed from a PDF:
sudo sed -i 's|^-w /etc/sudoers.d/ -p wa -k sudoers|-w /etc/sudoers.d/ -p wa k sudoers|' /etc/audit/rules.d/50-saatlab.rules
sudo augenrules --load
echo "exit code: $?"
sudo auditctl -lYou’ll get this error, and a shorter rule list:
No rules
enabled 1
failure 1
pid 868
rate_limit 0
backlog_limit 8192
lost 0
backlog 0
backlog_wait_time 60000
backlog_wait_time_actual 0
(the same status block is printed a second time, cut here)
parameter passed without an option given
There was an error in line 10 of /etc/audit/audit.rules
exit code: 1
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoersLine 10 is counted in the generated /etc/audit/audit.rules, not in your file: augenrules puts a header, -D, -b and -f first, then the rules from rules.d in name order. auditctl loads lines one by one and stops at the first bad one, so the five rules above it are active and everything after it, the sudoers.d and SSH watches and even the backlog_wait_time setting from audit.rules, is silently missing. Nothing else complains, and on a reboot the same thing happens again.
Fix: correct the line, reload, and always check the count after a change.
sudo sed -i 's|^-w /etc/sudoers.d/ -p wa k sudoers|-w /etc/sudoers.d/ -p wa -k sudoers|' /etc/audit/rules.d/50-saatlab.rules
sudo augenrules --load >/dev/null && echo loaded
sudo auditctl -l | wc -lYou should see this:
loaded
7Make “the rule count is what we expect” part of your monitoring or your Ansible role. A quiet audit failure is worse than a loud one, because everyone believes the trail is complete until the day they need it.
✅ Checkpoint: sudo auditctl -l lists all seven rules again, including the sudoers.d and sshd_config.d watches.
This happened at work
The firm that banned itself on deadline day
I was looking after the servers of a chartered accountancy firm in Mysuru the year this happened, on the last day for filing audit reports. Around 10 a.m. the office manager called: nobody in the office could reach the firm’s SFTP server, where staff uploaded clients’ signed documents, while partners working from home could. Forty people, one deadline, no uploads.
The junior admin on site had already restarted sshd and rebooted the office router, and both changed nothing.
I logged in from home and ran sudo fail2ban-client status sshd. The banned list had one address: the office’s own public IP, which every desk shared behind the router. A ban for an hour had been added a few minutes after nine, and it kept coming back.
The cause was an old laptop in the records room with a sync tool still holding a staff member’s expired password. It retried every minute, three failures put the whole office behind a ban, and each time the ban ended the laptop started again. Somebody had set up fail2ban months earlier as part of a hardening checklist and nobody remembered it was there.
I unbanned the office IP, the manager unplugged the laptop, and uploads restarted by 10:40. Afterwards we added the office’s fixed IP to ignoreip with a note in the jail file, moved staff accounts to SSH keys, set up an email whenever fail2ban banned anything, and wrote fail2ban and faillock into the “access problems” runbook, right at the top.
Common mistakes and troubleshooting
| Mistake | What happens | What to do instead |
|---|---|---|
| Applying a whole benchmark at once with its fix script | Something the application needs breaks, and nobody knows which of 200 changes did it | Scan first, fix in small batches through change control, re-scan each time |
| Editing /etc/pam.d/system-auth by hand on RHEL | authselect overwrites it, or a typo locks everyone out | Use authselect enable-feature and the .conf files under /etc/security |
| Turning on faillock without a runbook | Users locked by an old saved password call it a “password problem” for days | Teach the helpdesk faillock --user NAME and --reset, and look at the Source column |
| Editing jail.conf for fail2ban | A package update replaces your settings | Put settings in /etc/fail2ban/jail.d/*.local |
| No ignoreip for your own management path | fail2ban bans your jump host or office NAT and you lose access | Add the fixed management addresses to ignoreip, and keep console access |
| Not checking audit rules after loading | A syntax error leaves half the rules unloaded, silently | Check augenrules --load exit code and the auditctl -l count |
| Treating a passed scan as “secure” | Things the benchmark doesn’t cover (app config, patches, weak keys) stay open | Use the scan as one input, along with patching (Lesson 49), monitoring and reviews |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. You’ve just been given a new Linux server. What would you do first to harden it?
Why they ask this: They want a sensible order and to hear that you check before you change.
How you might answer: First I’d patch it and look at what’s exposed: ss -tulpn, the enabled services and the firewall rules, and remove anything nobody needs. Then SSH: keys only, no root login, through a drop-in file in sshd_config.d.
After that I’d set password quality and faillock, make sure auditd and log forwarding are on, and run an OpenSCAP scan against CIS Level 1 to see what’s left, fixing it in small batches.
2. What’s the difference between faillock and fail2ban?
Why they ask this: A basic question that checks you know which layer each one works on.
How you might answer: faillock is a PAM module that counts failed logins per account and locks that account for a while, for local logins, su and sudo. fail2ban reads logs, counts failures per IP address and adds a firewall rule that blocks that address.
So faillock protects an account from someone guessing its password anywhere, and fail2ban protects the server from one noisy address. I’d use both, with fail2ban’s ignoreip covering my management addresses.
2–5 years experience
3. An auditor asks who changed /etc/sudoers on a server last week. How do you answer?
Why they ask this: They’re checking whether you know auditd and the difference between uid and auid.
How you might answer: If there’s an audit watch on the file, I’d run ausearch -k sudoers -i --start with the date range. The auid field shows the person who logged in even if they used sudo, and comm and exe show which program changed the file.
If there was no watch, I’d look at sudo’s own log lines in the journal or the central log server for visudo or editor commands, but that’s weaker evidence. Then I’d add the watch rule to the standard build so the question is easy next time.
4. Your first OpenSCAP CIS scan shows about 100 failures. How do you handle it?
Why they ask this: They want a practical plan, not “run the fix script”.
How you might answer: I’d sort them: quick, low-risk fixes like module blacklists and file permissions; items that need a decision with app owners like SSH timeouts or password ageing for service accounts; and design items like separate partitions that go into the next build. Each fix gets tested and re-checked with oscap xccdf eval --rule.
Anything we won’t fix gets a written exception with a reason, which auditors accept. Then we put the fixes into the build (Kickstart or Ansible) and schedule scans to catch drift.
Senior level
5. How would you roll out CIS hardening across 300 RHEL and Ubuntu servers without breaking applications?
Why they ask this: Seniors should talk about staging, tailoring, automation and evidence.
How you might answer: I’d start with a tailored profile per OS (CIS Level 1 with documented exceptions), apply it with Ansible in stages: lab, then a few non-critical servers per app team, then the rest, with a rollback for each change. Each stage runs the app’s own tests and is watched for a few days.
Compliance is measured with scheduled OpenSCAP scans on RHEL and usg or another scanner on Ubuntu, results collected centrally, and drift raised as tickets. New servers come out of the build already compliant, so the work doesn’t pile up again.
6. SSH passwords are already off on your servers. Is fail2ban still worth running?
Why they ask this: They want reasoning about risk and trade-offs, not a yes or no.
How you might answer: With key-only SSH, brute force can’t succeed, so fail2ban isn’t protecting passwords any more. It still cuts log noise and load from scanners, slows down probes for vulnerable SSH versions, and can protect other services like web logins with their own filters.
The cost is the risk of banning yourselves, like an office NAT or a jump host, so I’d run it with ignoreip for management addresses, short bans and alerts on each ban. On internet-facing servers I’d keep it; on internal ones behind a jump host I might rely on the firewall alone.
Quick quiz
Q1. Which file should you use for your own password quality settings on Rocky Linux 10?
- /etc/pam.d/system-auth
- /etc/security/pwquality.conf.d/50-saatlab.conf
- /etc/login.defs
- /etc/shadow
Show answer
B — A drop-in under pwquality.conf.d keeps your settings separate; system-auth is owned by authselect.
Q2. priya types the correct password but su says “Authentication failure” after five earlier mistakes. What fixes it?
- Reset her password
sudo faillock --user priya --reset- Restart sshd
- Reboot the firewall
Show answer
B — faillock locked the account; resetting the tally releases it without changing the password.
Q3. What does auid=student in an audit record mean when uid=root?
- student owns the file
- student is the person who logged in, then ran the command as root
- root logged in as student
- The record is corrupt
Show answer
B — The audit user ID is set at login and survives sudo and su.
Q4. augenrules --load reports an error in line 10. Which rules are active?
- None
- All of them
- Only those before line 10
- Only those after line 10
Show answer
C — auditctl loads rules line by line and stops at the first bad one.
Q5. What does exit code 2 from oscap xccdf eval mean?
- The scanner crashed
- The scan ran and at least one rule failed
- The profile was not found
- Every rule passed
Show answer
B — 0 means all passed, 1 an error, 2 that the evaluation finished with failing rules.
Quick recap
- SSH (Lesson 33) and the firewall (Lesson 34) are layers one and two; close unused ports and services next, after asking who needs them.
- Password quality: a drop-in in /etc/security/pwquality.conf.d with minlen, minclass, maxrepeat, dictcheck and
enforce_for_root; test with pwscore. - faillock:
authselect enable-feature with-faillock, deny andunlock_timein faillock.conf;faillock --user NAME --resetreleases a lock. - fail2ban: settings in jail.d/*.local, bans as firewalld rich rules on Rocky, ignoreip for your management path.
- auditd: watch rules in /etc/audit/rules.d,
augenrules --load,ausearch -k; auid shows the real person. Check the rule count after every change. - OpenSCAP:
oscap info --profilesfor the IDs,oscap xccdf eval --profile ... --reportfor the scan,--ruleto re-check one fix.
Next lesson
Lesson 56: Host a web app: Nginx and MariaDB → You’ll build the SaatShop app on rocky01 with Nginx, PHP-FPM and MariaDB on its own logical volume, the app later lessons back up and break.
Further reading: Linux hardening
Want to go deeper into Linux hardening? Read Red Hat’s RHEL 10 documentation on security hardening and compliance scanning, the CIS Benchmarks page, the OpenSCAP oscap documentation, the ComplianceAsCode project behind the SCAP Security Guide, and the man7.org page for pam_faillock(8). You can also see the full plan for this course on the Linux Administration course page.
