Rocky Linux 9 Server Hardening: Security Baseline from First Boot
Rocky Linux 9 Ships Opinionated Security Defaults
Rocky Linux 9 (based on RHEL 9) has better out-of-the-box security than most distributions. SELinux is enforcing, firewalld is enabled, and password policies are set. But "better than default" isn't the same as "production-ready."
This guide takes you from the Rocky Linux 9 first boot to a hardened baseline aligned with DISA STIG and CIS Benchmark Level 1 recommendations.
1. Initial Setup: Non-Root User and SSH Keys
# Create admin user
useradd -m -G wheel adminuser
passwd adminuser
# Add SSH key for the admin user
mkdir -p /home/adminuser/.ssh
chmod 700 /home/adminuser/.ssh
echo "ssh-ed25519 AAAA... your-key" >> /home/adminuser/.ssh/authorized_keys
chmod 600 /home/adminuser/.ssh/authorized_keys
chown -R adminuser:adminuser /home/adminuser/.ssh
# Lock root login
passwd -l root
2. SSH Hardening
Edit /etc/ssh/sshd_config:
Port 2222
AddressFamily inet
ListenAddress 0.0.0.0
PermitRootLogin no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
AllowTcpForwarding no
GatewayPorts no
PermitTunnel no
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
Banner /etc/ssh/banner.txt
# Restrict to specific algorithms
HostKeyAlgorithms ssh-ed25519,rsa-sha2-256,rsa-sha2-512
KexAlgorithms curve25519-sha256,ecdh-sha2-nistp256
Ciphers [email protected],[email protected],aes256-ctr
MACs [email protected],[email protected]
Create the login banner:
cat > /etc/ssh/banner.txt << 'EOF'
*******************************************************************
AUTHORIZED ACCESS ONLY. All activity is monitored and logged.
Unauthorized access is prohibited and will be prosecuted.
*******************************************************************
EOF
# Validate and restart
sudo sshd -t
sudo systemctl restart sshd
3. SELinux: Keep It Enforcing
Rocky Linux 9 defaults to SELinux enforcing. Keep it there.
# Verify status
getenforce # Should output: Enforcing
sestatus
# Never set to Disabled (requires reboot to re-enable)
# Never set to Permissive in production
# Check for SELinux denials
sudo ausearch -m AVC,USER_AVC -ts recent
sudo journalctl -t setroubleshoot --since "1 hour ago"
# If a legitimate service is being denied, create a policy module
# rather than setting permissive
sudo ausearch -c 'nginx' --raw | audit2allow -M mynginx
sudo semodule -i mynginx.pp
# Common boolean flags
getsebool -a | grep httpd
sudo setsebool -P httpd_can_network_connect on # allow Nginx to proxy
sudo setsebool -P httpd_use_nfs on # allow NFS-served content
# Restore correct file contexts after moving files
sudo restorecon -Rv /var/www/html/
4. firewalld Configuration
# Check status
sudo firewall-cmd --state
# Default zone
sudo firewall-cmd --get-default-zone
# Set default zone to drop (deny everything not explicitly allowed)
sudo firewall-cmd --set-default-zone=drop
# Allow your SSH port (do this BEFORE enabling drop zone if SSH is on non-standard port)
sudo firewall-cmd --zone=drop --permanent --add-port=2222/tcp
# Allow HTTP/HTTPS for web servers
sudo firewall-cmd --zone=drop --permanent --add-service=http
sudo firewall-cmd --zone=drop --permanent --add-service=https
# Apply changes
sudo firewall-cmd --reload
# Verify
sudo firewall-cmd --zone=drop --list-all
Rate-Limit SSH with firewalld
# Limit SSH to 3 connections per minute per IP
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4"
service name="ssh"
limit value="3/m"
accept'
sudo firewall-cmd --reload
5. PAM and Password Policy
# Install password quality library
sudo dnf install libpwquality -y
# Edit /etc/security/pwquality.conf
cat >> /etc/security/pwquality.conf << 'EOF'
minlen = 14
minclass = 3
maxrepeat = 3
maxsequence = 4
gecoscheck = 1
EOF
# Account lockout policy — edit /etc/security/faillock.conf
cat > /etc/security/faillock.conf << 'EOF'
deny = 5
unlock_time = 900
fail_interval = 900
EOF
# Password aging (for local accounts)
# Set in /etc/login.defs
sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/' /etc/login.defs
sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS 1/' /etc/login.defs
sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE 14/' /etc/login.defs
6. auditd: System Call Auditing
Rocky Linux 9 ships auditd and enables it by default. Extend it with security-relevant rules:
# Check auditd status
sudo systemctl status auditd
sudo auditctl -l # list active rules
# Add comprehensive audit rules
# File: /etc/audit/rules.d/99-hardening.rules
cat > /etc/audit/rules.d/99-hardening.rules << 'EOF'
# Immutable (must be last) — prevents rule modification at runtime
# -e 2
# Monitor authentication files
-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
# Monitor SSH config
-w /etc/ssh/sshd_config -p wa -k sshd_config
# Monitor cron
-w /etc/cron.d/ -p wa -k cron
-w /etc/crontab -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
# Privileged command execution
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands
# System calls related to time changes
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time_change
# Network configuration changes
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k system_locale
-w /etc/hosts -p wa -k hosts_file
-w /etc/sysconfig/network -p wa -k network_config
# Login/logout events
-w /var/log/lastlog -p wa -k logins
-w /var/run/faillock -p wa -k logins
EOF
# Load the rules
sudo augenrules --load
sudo systemctl restart auditd
# Search audit logs
sudo ausearch -k identity -ts today
sudo ausearch -k sudoers --since yesterday
7. AIDE: File Integrity Monitoring
AIDE (Advanced Intrusion Detection Environment) detects unauthorized changes to system files.
# Install AIDE
sudo dnf install aide -y
# Initialize the baseline database (takes a few minutes)
sudo aide --init
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# Run a check (compare current state to baseline)
sudo aide --check
# Schedule daily checks via cron
cat > /etc/cron.daily/aide-check << 'EOF'
#!/bin/bash
/usr/sbin/aide --check 2>&1 | mail -s "AIDE report - $(hostname)" root
EOF
chmod 700 /etc/cron.daily/aide-check
# Update database after intentional changes (e.g., after package updates)
sudo aide --update
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
8. Kernel Hardening via sysctl
Create /etc/sysctl.d/99-hardening.conf:
# Disable kernel pointer exposure
kernel.kptr_restrict = 2
# Restrict dmesg to root
kernel.dmesg_restrict = 1
# Enable ASLR
kernel.randomize_va_space = 2
# Disable core dumps for setuid programs
fs.suid_dumpable = 0
# Prevent unprivileged users from reading /proc/PID/
kernel.yama.ptrace_scope = 1
# Network hardening
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.tcp_syncookies = 1
# Disable IPv6 if unused
# net.ipv6.conf.all.disable_ipv6 = 1
sudo sysctl --system
9. CIS Benchmark Check with OpenSCAP
Rocky Linux 9 ships with OpenSCAP for automated compliance checking:
# Install OpenSCAP and SCAP Security Guide
sudo dnf install openscap-scanner scap-security-guide -y
# List available profiles
oscap info /usr/share/xml/scap/ssg/content/ssg-rl9-ds.xml | grep Profile
# Run CIS Level 1 assessment
sudo oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_cis \
--report /tmp/cis-report.html \
/usr/share/xml/scap/ssg/content/ssg-rl9-ds.xml
# View the report
# Open /tmp/cis-report.html in a browser
# It shows pass/fail for each CIS control with remediation guidance
# Generate remediation script
sudo oscap xccdf generate fix \
--profile xccdf_org.ssgproject.content_profile_cis \
--fix-type bash \
/usr/share/xml/scap/ssg/content/ssg-rl9-ds.xml > /tmp/cis-remediation.sh
Hardening Checklist
| Control | Command to Verify |
|---|---|
| SELinux enforcing | getenforce |
| Root login disabled | grep PermitRootLogin /etc/ssh/sshd_config |
| SSH password auth off | grep PasswordAuthentication /etc/ssh/sshd_config |
| firewalld active | firewall-cmd --state |
| auditd running | systemctl is-active auditd |
| AIDE initialized | ls /var/lib/aide/aide.db.gz |
| No unneeded services | systemctl list-units --type=service --state=running |
| Password policy set | chage -l root |
| Kernel hardening | sysctl kernel.randomize_va_space |
This baseline covers the most impactful controls. Run the OpenSCAP assessment after applying these to identify remaining gaps — the report includes remediation scripts for each failed check.
Was this article helpful?
Linux Systems Engineer
Everything runs on Linux — I make sure it runs well. From kernel tuning to systemd debugging, I live in the terminal. If your server is misbehaving, I've probably seen that exact dmesg output before.
Related Articles
Ubuntu 24.04 LTS Server Setup for Production: From Bare Metal to Hardened Baseline
A complete walkthrough for turning a fresh Ubuntu 24.04 LTS installation into a production-ready server — from initial SSH lockdown to swap configuration and kernel parameter tuning.
Systemd Service Hardening: Sandbox Your Daemons
How to use systemd's built-in sandboxing directives to isolate services, restrict filesystem access, and harden daemons without containers.
Migrating from CentOS to Rocky Linux: A Practical Conversion Guide
How to migrate existing CentOS 7/8 servers to Rocky Linux 8 or 9 using the migrate2rocky script — including pre-migration checks, the migration process itself, and post-migration validation steps.
Ubuntu Zero-Downtime Patching: Unattended Upgrades, Livepatch, and USN Tracking
How to keep Ubuntu servers patched automatically without unplanned reboots — combining unattended-upgrades, Canonical Livepatch, and USN monitoring into a practical patching strategy.
Fix SSH 'Connection Refused' After Server Reboot
Diagnose and fix SSH connection refused errors after a Linux server reboot, covering sshd service, firewall rules, port configuration, and host key issues.
Linux Networking & Firewall: Configuration and Troubleshooting
Configure Linux network interfaces, set up iptables/nftables/firewalld rules, troubleshoot connectivity with ss, ip, dig, and tcpdump, and secure your servers.
More in Linux
View all →Linux Control Groups (cgroups V2): Limiting CPU And Memory For Production Workloads
If you're running production workloads without cgroup constraints, you're one runaway process away from a bad day. Here's everything you need to configure...
Alpine Linux apk Reference: Package Management, Repositories, and System Administration
A complete reference for Alpine Linux's apk package manager — adding packages, managing repositories, pinning versions, building custom packages, and running Alpine as a minimal server OS.
Alpine Linux for Docker: Building Minimal, Secure Container Images
How to use Alpine Linux as a Docker base image effectively — understanding musl libc compatibility, multi-stage builds, layer optimization, and the security trade-offs versus glibc-based distributions.
openSUSE MicroOS and Transactional Updates: Atomic System Updates with Btrfs Snapshots
How openSUSE's transactional-update and Btrfs snapshot integration enables atomic, rollback-safe system updates — and how MicroOS takes this to a fully immutable, container-optimized OS.