Back to skills
SKILL.md
Server Hardening
FSecurityLinux server security hardening covering CIS Benchmark areas: automatic security updates, AppArmor/SELinux MAC, auditd, intrusion detection, login security (PAM), network hardening sysctl, filesystem security, and service minimization. USE WHEN: - Hardening a new Ubuntu/Debian or RHEL/CentOS server before production use - Configuring automatic security updates with unattended-upgrades - Setting up auditd to track privilege escalation and file modifications - Enabling AppArmor enforcement or ...
- 31 stars
- 0 votes
- 0 copies
- 0 views
- Added September 8, 2026
Security analysis
23/100- Accesses sensitive system or user directories
- Accesses sensitive system or user directories
- Accesses sensitive system or user directories
- Creates or modifies cron jobs for persistent execution
- Creates or modifies cron jobs for persistent execution
- Modifies startup scripts or system services for persistence
npx -y skills add claude-dev-suite/claude-dev-suite --skill server-hardening --agent claude-codeAre you the author of Server Hardening?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-server-hardening)---
name: server-hardening
description: |
Linux server security hardening covering CIS Benchmark areas: automatic security
updates, AppArmor/SELinux MAC, auditd, intrusion detection, login security (PAM),
network hardening sysctl, filesystem security, and service minimization.
USE WHEN:
- Hardening a new Ubuntu/Debian or RHEL/CentOS server before production use
- Configuring automatic security updates with unattended-upgrades
- Setting up auditd to track privilege escalation and file modifications
- Enabling AppArmor enforcement or writing custom profiles
- Running rkhunter or Wazuh for intrusion detection
- Locking down PAM login policies (account lockout, password quality)
- Auditing SUID/SGID binaries and open ports
DO NOT USE FOR:
- Application-layer WAF rules (use the waf skill instead)
- Network firewall and UFW/iptables rules (use the firewall skill instead)
- SSL/TLS certificate management (use the ssl-tls skill instead)
- Container security (seccomp profiles, AppArmor with Docker — use the docker skill)
allowed-tools: Read, Grep, Glob, Write, Edit, Bash
---
# Server Hardening — Production Linux Security
## Hardening Philosophy
Apply the **principle of least privilege** at every layer:
- Services run as dedicated non-root users
- Only required ports are open
- Only required packages are installed
- All privilege changes are audited
- System is kept patched automatically
---
## Automatic Security Updates
### unattended-upgrades (Ubuntu/Debian)
```bash
sudo apt install -y unattended-upgrades update-notifier-common apt-listchanges
sudo dpkg-reconfigure --priority=low unattended-upgrades
```
`/etc/apt/apt.conf.d/50unattended-upgrades`:
```
Unattended-Upgrade::Allowed-Origins {
// Apply security updates from Ubuntu's security channel
"${distro_id}:${distro_codename}-security";
// Optional: ESM security updates (Ubuntu Pro)
"UbuntuESMApps:${distro_codename}-apps-security";
"UbuntuESM:${distro_codename}-infra-security";
};
// Automatically remove obsolete packages after upgrade
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
// Reboot automatically if required (kernel/libc updates)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
// Email notification on errors
Unattended-Upgrade::Mail "ops@example.com";
Unattended-Upgrade::MailReport "on-change";
// Block packages that have failed for too long
Unattended-Upgrade::SyslogEnable "true";
Unattended-Upgrade::SyslogFacility "daemon";
// Split the upgrade into smaller batches to reduce memory pressure
Unattended-Upgrade::MinimalSteps "true";
```
`/etc/apt/apt.conf.d/20auto-upgrades`:
```
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
```
```bash
# Test dry run — verify which packages would be upgraded
sudo unattended-upgrades --dry-run --debug
# Check service status
sudo systemctl status unattended-upgrades
# View upgrade log
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
```
`needrestart` — prompts (or automatically restarts) services after library upgrades:
```bash
sudo apt install -y needrestart
# Configure automatic restart mode (no prompt in CI/automated contexts):
sudo sed -i "s/#\$nrconf{restart} = 'i';/\$nrconf{restart} = 'a';/" /etc/needrestart/needrestart.conf
```
---
## SSH Hardening
`/etc/ssh/sshd_config.d/99-hardening.conf` (drop-in, overrides defaults):
```
# Disable root login entirely — use sudo from a named user account
PermitRootLogin no
# Disable password authentication — keys only
PasswordAuthentication no
ChallengeResponseAuthentication no
# Disable X11 forwarding unless required
X11Forwarding no
# Disable agent forwarding (prevents lateral movement via forwarded SSH agent)
AllowAgentForwarding no
# Limit login window (default 120s is excessive)
LoginGraceTime 30
# Maximum authentication attempts before disconnection
MaxAuthTries 3
# Restrict SSH to specific users or groups
AllowUsers deploy admin
# AllowGroups sshusers
# Use only strong algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Disable SSH protocol 1 (already default, but explicit is better)
Protocol 2
# Enable strict mode checking on key file permissions
StrictModes yes
# Log logins at verbose level (captures key fingerprints)
LogLevel VERBOSE
# Disable TCP port forwarding unless required
AllowTcpForwarding no
```
```bash
sudo sshd -t # Test config before reloading
sudo systemctl reload sshd
```
---
## AppArmor (Ubuntu/Debian)
```bash
# Check status
sudo aa-status
# Put a profile in complain mode (log violations, do not enforce)
sudo aa-complain /usr/sbin/nginx
# Enforce a profile
sudo aa-enforce /usr/sbin/nginx
# Load all profiles in /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/
# View recent violations
sudo journalctl -k | grep apparmor | tail -30
# OR
sudo cat /var/log/kern.log | grep apparmor | tail -30
```
Custom AppArmor profile skeleton (`/etc/apparmor.d/usr.local.bin.myapp`):
```
#include <tunables/global>
/usr/local/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice>
# Read app files
/opt/myapp/** r,
# Write to log directory
/var/log/myapp/ rw,
/var/log/myapp/** rw,
# Read config
/etc/myapp/** r,
# Network access (outbound only)
network inet stream,
# Deny everything else
deny /** w,
}
```
---
## SELinux Basics (RHEL/CentOS/Fedora)
```bash
# Check enforcement status
getenforce # Enforcing / Permissive / Disabled
sestatus # Detailed status
# Temporarily set permissive (testing — does not persist reboot)
sudo setenforce 0
# Re-enable enforcement
sudo setenforce 1
# Persistent mode in /etc/selinux/config:
# SELINUX=enforcing
# Fix wrong file context (e.g., after moving files)
sudo restorecon -Rv /var/www/html/
# Generate allow rules from audit denials
sudo ausearch -m avc -ts recent | audit2allow -M mypolicy
sudo semodule -i mypolicy.pp
# View denials
sudo ausearch -m avc -ts recent
```
---
## auditd: System Call Auditing
```bash
sudo apt install -y auditd audispd-plugins
sudo systemctl enable --now auditd
```
`/etc/audit/rules.d/99-production.rules`:
```bash
# Delete all existing rules and start fresh
-D
# Set buffer size (increase if losing events during heavy load)
-b 8192
# Failure mode: 1=print to syslog, 2=panic
-f 1
# ── Authentication and Session Events ─────────────────────────────────────────
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# Track sudo usage (execve of sudo binary)
-a always,exit -F arch=b64 -F path=/usr/bin/sudo -F perm=x -k sudo_usage
-a always,exit -F arch=b64 -F path=/usr/bin/su -F perm=x -k su_usage
# SSH login events
-w /var/log/auth.log -p wa -k auth_log
-w /etc/ssh/sshd_config -p wa -k sshd_config
# ── Privilege Escalation ───────────────────────────────────────────────────────
# Track setuid/setgid program execution
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root_commands
# ── File System Changes ────────────────────────────────────────────────────────
# Monitor critical system files
-w /etc/cron.d/ -p wa -k cron
-w /etc/crontab -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
-w /etc/hosts -p wa -k hosts_file
-w /etc/hostname -p wa -k hostname_file
# Monitor module loading
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k kernel_modules
# Monitor mount operations
-a always,exit -F arch=b64 -S mount -k mounts
# ── Network Connections ────────────────────────────────────────────────────────
-a always,exit -F arch=b64 -S socket -F a0=2 -k network_socket_ipv4
-a always,exit -F arch=b64 -S socket -F a0=10 -k network_socket_ipv6
# ── Make Rules Immutable (requires reboot to change) ─────────────────────────
-e 2
```
```bash
sudo augenrules --load
# Verify rules loaded
sudo auditctl -l
# Search audit log
sudo ausearch -k sudo_usage -ts today
sudo ausearch -k identity -ts recent
# Generate summary report
sudo aureport --summary
sudo aureport --failed --summary
sudo aureport -au --summary # Authentication failures
```
---
## PAM Login Security
### Account Lockout After Failed Attempts
```bash
sudo apt install -y libpam-faillock
```
`/etc/security/faillock.conf`:
```ini
# Lock account after 5 failed attempts
deny = 5
# Unlock after 15 minutes (900 seconds)
unlock_time = 900
# Count failures for this many seconds
fail_interval = 900
# Also lock root account
even_deny_root = true
# Ignore users with UID below this (system accounts)
admin_group = wheel
```
Add to `/etc/pam.d/common-auth` (before `pam_unix.so`):
```
auth required pam_faillock.so preauth silent
auth [success=1 default=bad] pam_unix.so
auth [default=die] pam_faillock.so authfail
auth sufficient pam_faillock.so authsucc
```
Manage locked accounts:
```bash
# Check failed attempts for a user
faillock --user alice
# Reset (unlock) a user
faillock --user alice --reset
```
### Password Quality: pam_pwquality
```bash
sudo apt install -y libpam-pwquality
```
`/etc/security/pwquality.conf`:
```ini
minlen = 14
dcredit = -1 # Require at least 1 digit
ucredit = -1 # Require at least 1 uppercase
lcredit = -1 # Require at least 1 lowercase
ocredit = -1 # Require at least 1 special character
maxrepeat = 3 # Maximum 3 consecutive identical characters
gecoscheck = 1 # Disallow words from GECOS field (user info)
badwords = company myapp admin root
```
---
## Network Hardening Sysctl
Add to `/etc/sysctl.d/99-network-security.conf`:
```ini
# ── IP Forwarding ─────────────────────────────────────────────────────────────
# Disable if this server is NOT a router or VPN gateway
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# ── ICMP Redirects ────────────────────────────────────────────────────────────
# Disable accepting ICMP redirect messages (routing change injection)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Disable sending ICMP redirects (not a router)
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# ── Source Routing ────────────────────────────────────────────────────────────
# Disable source routing (IP options spoofing)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# ── Reverse Path Filtering ────────────────────────────────────────────────────
# Strict mode: drop packets with unexpected source addresses (anti-spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# ── SYN Cookies ──────────────────────────────────────────────────────────────
# Protect against SYN flood attacks
net.ipv4.tcp_syncookies = 1
# ── ICMP ─────────────────────────────────────────────────────────────────────
# Log suspicious (martian) packets
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Ignore ICMP broadcast packets
net.ipv4.icmp_echo_ignore_broadcasts = 1
# ── Disable IPv6 (if not used) ────────────────────────────────────────────────
# net.ipv6.conf.all.disable_ipv6 = 1
# net.ipv6.conf.default.disable_ipv6 = 1
```
Apply: `sudo sysctl -p /etc/sysctl.d/99-network-security.conf`
---
## Filesystem Security
```bash
# Find all SUID/SGID binaries (audit regularly, especially after package installs)
sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -ls 2>/dev/null \
| tee /root/suid_sgid_baseline.txt
# Compare against baseline next week:
sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -ls 2>/dev/null \
| diff /root/suid_sgid_baseline.txt -
# Find world-writable files (excluding /proc, /sys, /tmp)
sudo find / -xdev -not \( -path /proc -prune \) -not \( -path /sys -prune \) \
-perm -o+w -type f -ls 2>/dev/null
# Find unowned files (orphaned after package removal)
sudo find / -xdev \( -nouser -o -nogroup \) -ls 2>/dev/null
# Make critical system files immutable (even root cannot modify without removing flag)
sudo chattr +i /etc/passwd /etc/shadow /etc/group /etc/gshadow /etc/sudoers
# Remove immutable flag to make changes:
sudo chattr -i /etc/sudoers
# edit...
sudo chattr +i /etc/sudoers
```
Mount options for sensitive filesystems (`/etc/fstab`):
```fstab
# /tmp: no executables, no setuid, no device files
tmpfs /tmp tmpfs defaults,nosuid,noexec,nodev,size=2G 0 0
# /var: no setuid, no device files
UUID=xxx /var ext4 defaults,nosuid,nodev 0 2
# /home: no executables, no setuid, no device files
UUID=xxx /home ext4 defaults,nosuid,noexec,nodev 0 2
```
---
## rkhunter — Rootkit Detection
```bash
sudo apt install -y rkhunter
sudo rkhunter --update # Update signatures
sudo rkhunter --propupd # Record current file properties as baseline
sudo rkhunter --check --skip-keypress # Run check
sudo cat /var/log/rkhunter.log | grep -E 'Warning|Error'
```
Cron script `/etc/cron.weekly/rkhunter`:
```bash
#!/bin/bash
REPORT_EMAIL="ops@example.com"
LOGFILE="/var/log/rkhunter.log"
/usr/bin/rkhunter --update --nocolors --quiet
/usr/bin/rkhunter --check --nocolors --skip-keypress \
--report-warnings-only \
--logfile "$LOGFILE"
EXIT_CODE=$?
if [[ $EXIT_CODE -ne 0 ]]; then
WARNINGS=$(grep -E 'Warning|Error' "$LOGFILE" | tail -30)
echo -e "Subject: [ALERT] rkhunter warnings on $(hostname)\n\n$WARNINGS" \
| sendmail "$REPORT_EMAIL"
fi
```
```bash
sudo chmod +x /etc/cron.weekly/rkhunter
```
`/etc/rkhunter.conf` — suppress known false positives:
```
PKGMGR=DPKG
SCRIPTWHITELIST=/usr/bin/lwp-request
ALLOWHIDDENDIR=/dev/.udev
ALLOWHIDDENFILE=/dev/.mdadm
```
---
## Service Minimization
```bash
# List all running services
systemctl list-units --type=service --state=running
# List all enabled services (start at boot)
systemctl list-unit-files --type=service --state=enabled
# Disable and mask services not needed
sudo systemctl disable --now bluetooth.service
sudo systemctl mask bluetooth.service # Prevents re-enable
sudo systemctl disable --now avahi-daemon.service
sudo systemctl mask avahi-daemon.service
sudo systemctl disable --now cups.service
sudo systemctl mask cups.service
# Port audit
sudo ss -tlnp # TCP listening ports + owning process
sudo ss -ulnp # UDP listening ports + owning process
# Nmap self-scan (from same host)
sudo nmap -sV -O localhost
```
---
## Hardening Checklist (20 Points)
- [ ] 1. SSH: `PermitRootLogin no`, `PasswordAuthentication no`
- [ ] 2. SSH: `AllowUsers` / `AllowGroups` restricted to named accounts
- [ ] 3. SSH: Weak ciphers/MACs removed from sshd_config
- [ ] 4. Automatic security updates enabled (unattended-upgrades)
- [ ] 5. Automatic reboot scheduled (off-peak hours) for kernel updates
- [ ] 6. AppArmor enforcing for all installed profiles (`aa-status | grep processes in enforce`)
- [ ] 7. auditd installed and running with production rules
- [ ] 8. Sudo usage logged in auditd and/or `/var/log/auth.log`
- [ ] 9. PAM faillock: account lockout after 5 failed login attempts
- [ ] 10. PAM pwquality: minimum 14-character password with complexity
- [ ] 11. Network sysctl hardening applied (redirects, source routing, SYN cookies)
- [ ] 12. IP forwarding disabled (unless this is a router/VPN gateway)
- [ ] 13. `/tmp` mounted `nosuid,noexec,nodev`
- [ ] 14. `/home` mounted `nosuid,noexec,nodev`
- [ ] 15. SUID/SGID binary baseline recorded; unexpected additions alert
- [ ] 16. No world-writable files outside of `/tmp` and `/var/tmp`
- [ ] 17. rkhunter installed, baseline recorded, weekly cron configured
- [ ] 18. Unnecessary services disabled and masked
- [ ] 19. Port audit: only expected ports open (`ss -tlnp`)
- [ ] 20. All services run as dedicated non-root users with minimal directory access
---
## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|--------------|---------|-----|
| Running application services as root | Exploited app grants full system access | Create dedicated user per service: `useradd --system --no-create-home myapp` |
| No automatic security updates | Known CVEs unpatched for months | Enable `unattended-upgrades` with security channel on day one |
| World-writable directories outside /tmp | Any process can drop files (local privilege escalation vector) | `find / -xdev -perm -o+w -type d` — remove write from others |
| No auditd | Breach investigation has no evidence trail | Install auditd with privilege escalation rules before going live |
| SUID binaries not inventoried | Attacker installs SUID shell backdoor undetected | Record baseline with `find / -perm -4000`; compare weekly |
| SSH password auth enabled on internet-facing server | Brute-force attacks possible | `PasswordAuthentication no` — key-only access |
| AppArmor in complain mode permanently | Complain mode logs but does not block attacks | Move to `aa-enforce` after testing period; use `aa-complain` only during development |
| Skipping `rkhunter --propupd` after package install | Every package upgrade triggers "Warning: file changed" false positives | Run `rkhunter --propupd` after every `apt upgrade` |
| `unattended-upgrades` set to upgrade all packages | Non-security updates can break application compatibility | Limit to `-security` origin only |
---
## Troubleshooting
| Symptom | Likely Cause | Diagnostic & Fix |
|---------|--------------|------------------|
| AppArmor blocking legitimate app | App accesses path not in profile | `journalctl -k | grep apparmor` → add rule to profile; reload with `apparmor_parser -r` |
| auditd causing high I/O / CPU | Too many audit rules or high syscall rate | Reduce rules scope; raise `-b` buffer; check `auditctl -s` for lost events |
| rkhunter false positives after upgrades | File hashes changed due to package update | Run `rkhunter --propupd` after each `apt upgrade` |
| `unattended-upgrades` breaks a package | Package update has incompatible change | Add to `Unattended-Upgrade::Package-Blacklist` in 50unattended-upgrades; pin version |
| Account not locking after failed logins | faillock not in PAM stack or wrong config file | `faillock --user alice` to check counters; verify PAM stack order in common-auth |
| SSH login blocked for valid user | AllowUsers doesn't include that user, or key not accepted | `sshd -T | grep allowusers`; check `~/.ssh/authorized_keys` permissions (must be 600) |
| `chattr +i` prevents sudo from editing `/etc/sudoers` | Immutable flag blocks all writes including root | `chattr -i /etc/sudoers`; edit; `chattr +i /etc/sudoers` |
| rkhunter reports hidden processes | Kernel module using unhide technique, or false positive from docker | `ps auxf` + `rkhunter --list hidden`; whitelist known false positive in `rkhunter.conf` |
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.