Executive Summary
A security baseline is the foundation: OS-hardened, patched, with restricted access and audit trails. This guide covers minimal-install servers with hardened SSH, firewall (default-deny), LSM enforcement, least-privilege sudo, audit logging, and systemd hardening.
Goal: Reduce attack surface, detect breaches, and enforce privilege boundaries.
1. Minimal Install & Patching
Minimal Install
What it is:
- Install only required packages (base + SSH + monitoring agent)
- No GUI, X11, unnecessary daemons
- Reduces vulnerabilities (fewer packages = fewer CVEs)
Install steps (Ubuntu/Debian):
# Netboot minimal install (server profile)
# Or, after full install, remove unnecessary packages
apt remove --purge ubuntu-desktop xserver-xorg* snapd apport cups
apt autoremove --purge
apt clean
# Keep core utilities
apt install --no-install-recommends \
build-essential curl wget git tmux htop vim \
openssh-server openssh-client openssh-sftp-server \
ca-certificates apt-listchanges needrestart
# Disable swap (if not needed and running on SSD)
swapoff -a
Install steps (RHEL/CentOS/Fedora):
# Minimal server group
dnf groupinstall "Minimal Install" "Standard"
# Install hardening packages
dnf install --best \
openssh openssh-clients \
aide fail2ban unattended-upgrades \
audit selinux-policy selinux-policy-devel
Automated Patching
What it is:
- unattended-upgrades (Debian/Ubuntu): Auto-patch security updates
- dnf-automatic (RHEL/Fedora): Auto-patch on schedule
- Reduces window for known exploits; critical for compliance
Setup (Ubuntu/Debian):
apt install unattended-upgrades apt-listchanges needrestart
# Configure: /etc/apt/apt.conf.d/50unattended-upgrades
sudo tee /etc/apt/apt.conf.d/50unattended-upgrades > /dev/null << 'UNATTEND'
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}:${distro_codename}-updates";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailOnlyOnError "true";
Unattended-Upgrade::SyslogEnable "true";
Unattended-Upgrade::SyslogFacility "daemon";
UNATTEND
# Enable daily runs
sudo systemctl enable unattended-upgrades
sudo systemctl restart unattended-upgrades
Setup (RHEL/Fedora):
dnf install dnf-automatic
# Configure: /etc/dnf/automatic.conf
sudo tee /etc/dnf/automatic.conf > /dev/null << 'DNFAUTO'
[commands]
upgrade_type = security
apply_updates = yes
random_sleep = 0
[emitters]
system_mailer = yes
email_from = root@localhost
email_to = root
[base]
assumeyes = true
DNFAUTO
# Enable weekly patch (non-rebooting)
sudo systemctl enable dnf-automatic.timer
sudo systemctl start dnf-automatic.timer
# Optional: Add monthly reboot job
cat << 'REBOOT' | sudo tee /etc/cron.d/monthly-reboot
0 2 1 * * root /sbin/shutdown -r +60 "Monthly scheduled reboot"
REBOOT
Verify patching:
# Debian/Ubuntu
apt list --upgradable
sudo unattended-upgrade -d # Dry-run
# RHEL/Fedora
dnf check-update
sudo dnf-automatic
2. SSH Hardening
Key-Based Authentication Only
What it is:
- Disable password auth (no brute-force vulnerability)
- Use SSH keys (Ed25519 or RSA 4096-bit) or FIDO2 hardware keys
- Each admin/service gets unique key
Generate SSH keys (user local machine):
# Ed25519 (recommended, 256-bit security)
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519 -N "passphrase"
# Or RSA 4096-bit (if Ed25519 not supported)
ssh-keygen -t rsa -b 4096 -C "[email protected]" -f ~/.ssh/id_rsa -N "passphrase"
# FIDO2 hardware key (e.g., YubiKey)
# Install: https://github.com/duo-labs/duo_unix
ssh-keygen -t ecdsa-sk -O resident -f ~/.ssh/id_ecdsa_sk -C "user@yubikey"
Copy public key to server:
# From local machine (one-time)
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
# Manually if ssh-copy-id unavailable:
# cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Harden SSH config on server (/etc/ssh/sshd_config
):
# Disable password authentication
PasswordAuthentication no
PubkeyAuthentication yes
# Disable root login (use sudo instead)
PermitRootLogin no
# Allow specific users (explicit allowlist)
AllowUsers user1 [email protected]/24 # user2 only from this subnet
DenyUsers nobody
# Disable tunneling, X11 forwarding (reduce attack surface)
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
PermitTunnel no
# SSH protocol 2 only
Protocol 2
# Strong ciphers (default often sufficient in modern OpenSSH)
Ciphers [email protected],[email protected],[email protected]
KexAlgorithms curve25519-sha256,[email protected]
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
MACs [email protected],[email protected]
# Restrict to specific ports (non-standard optional for obscurity)
Port 22
# Timeouts (limit lingering connections)
ClientAliveInterval 300 # Ping client every 5 min
ClientAliveCountMax 2 # Close if no response after 2 pings (10 min total)
LoginGraceTime 30
# Strict modes (check permissions on ~/.ssh, keys)
StrictModes yes
# Disable empty passwords
PermitEmptyPasswords no
# Limit authentication attempts
MaxAuthTries 3
MaxSessions 10
# Disable host-based authentication
HostbasedAuthentication no
Apply and verify:
sudo sshd -t # Syntax check
sudo systemctl restart ssh # Apply changes
ssh-keyscan -t ed25519 server.example.com >> ~/.ssh/known_hosts # Add to known_hosts
ssh [email protected] # Should connect without password
Fail2Ban (Optional, for exposed servers)
What it is:
- Monitors logs for failed logins
- Auto-blocks IPs with
iptables
/nftables
after N failures - Reduces brute-force attacks
Install & configure:
# Ubuntu/Debian
apt install fail2ban
# RHEL/Fedora
dnf install fail2ban python3-requests
# Create local config (don't edit fail2ban.conf directly)
sudo tee /etc/fail2ban/jail.local > /dev/null << 'FAIL2BAN'
[DEFAULT]
bantime = 3600 # Ban for 1 hour
findtime = 600 # Within last 10 minutes
maxretry = 3 # After 3 failures
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log # or /var/log/secure on RHEL
maxretry = 3
FAIL2BAN
# Start & enable
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban
# Monitor bans
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 192.168.1.100 # Unban an IP
3. Firewall (nftables or ufw)
nftables (Modern, Recommended)
What it is:
- Kernel packet filtering (replaces iptables)
- Rules in simple, efficient syntax
- Default-deny inbound (explicit allowlist)
Install & basic config:
apt install nftables
# Create ruleset: /etc/nftables.conf
sudo tee /etc/nftables.conf > /dev/null << 'NFT'
#!/usr/bin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop; # Default-deny
# Accept loopback (localhost)
iif "lo" accept
# Accept established/related connections
ct state established,related accept
# Accept SSH from anywhere (or restrict to trusted subnet)
# Restrict: iif "eth0" ip saddr 10.0.0.0/8 tcp dport 22 accept
tcp dport { 22 } accept comment "SSH"
# Accept HTTP/HTTPS (if web server)
tcp dport { 80, 443 } accept comment "Web"
# Accept DNS (if running DNS server)
udp dport { 53 } accept comment "DNS"
tcp dport { 53 } accept comment "DNS-TCP"
# Log dropped packets (for debugging)
limit rate 5/minute log prefix "nft_drop: "
# Drop everything else
counter drop
}
chain forward {
type filter hook forward priority 0; policy drop; # Disable forwarding
}
chain output {
type filter hook output priority 0; policy accept; # Allow outbound
}
}
NFT
# Validate
sudo nft -c -f /etc/nftables.conf
# Load rules
sudo systemctl restart nftables
# Verify
sudo nft list ruleset
Update firewall rules at runtime:
# Reload from file
sudo nftables reload
# Or, direct commands
sudo nft add rule inet filter input tcp dport 8080 accept comment "Custom app"
sudo nft delete rule inet filter input tcp dport 8080 # Remove
# Monitor packet counts
sudo nft list ruleset # Shows counters
ufw (Simpler Alternative)
What it is:
- Frontend to
iptables
/nftables
- Simpler syntax, good for beginners
Install & basic config:
apt install ufw
# Enable and set defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (before enabling!)
sudo ufw allow 22/tcp comment "SSH"
# Allow HTTP/HTTPS
sudo ufw allow 80/tcp comment "HTTP"
sudo ufw allow 443/tcp comment "HTTPS"
# Enable firewall
sudo ufw enable
# Verify
sudo ufw status verbose
4. Linux Security Modules (LSM)
SELinux (RHEL/Fedora/CentOS)
What it is:
- Type enforcement (TE): Security contexts on files, processes, ports
- Mandatory Access Control (MAC) prevents even root from unauthorized actions
- Policy:
targeted
(default, confines daemons) orstrict
(all processes)
Check status:
getenforce
sestatus -v
Configure:
# Edit policy: /etc/selinux/config
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
sudo sed -i 's/^SELINUXTYPE=.*/SELINUXTYPE=targeted/' /etc/selinux/config
# Apply (requires reboot)
sudo reboot
# After reboot, verify
getenforce # Should output: Enforcing
# Monitor denials
sudo tail -f /var/log/audit/audit.log | grep denied
Generate policy rules from denials:
# Find denials
sudo grep denied /var/log/audit/audit.log | audit2why
# Generate allow rules (review before applying!)
sudo grep denied /var/log/audit/audit.log | audit2allow -a
# Apply custom policy
sudo audit2allow -a -M custom_policy
sudo semodule -i custom_policy.pp
AppArmor (Ubuntu/Debian)
What it is:
- Path-based access control (simpler than SELinux)
- Policy for each application
- Modes:
complain
(log only),enforce
(block)
Check status:
aa-status
apparmor_status
Configure:
# Most Ubuntu packages come with AppArmor profiles
# Verify key profiles are enabled
sudo aa-enforce /etc/apparmor.d/usr.sbin.sshd
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
# Check denials
sudo tail -f /var/log/audit/audit.log | grep apparmor | grep denied
Generate profile for custom app:
# Put profile in complain mode (log violations, don't block)
sudo aa-complain /path/to/app
# Run app, generate profile
# ... run your app normally for 10+ minutes
# Generate/update profile from logs
sudo aa-logprof
# Switch to enforce mode
sudo aa-enforce /etc/apparmor.d/path.to.app
5. Least-Privilege Access (sudo)
sudo Without Full TTY Bypass
What it is:
- sudo requires password/key (authenticate each use)
NOPASSWD
only for non-interactive commands- Separate admin & app service accounts
- Audit all sudo usage
Configure (/etc/sudoers
):
# Edit safely using visudo
sudo visudo
# Add these lines:
# ===== Users =====
# Standard admin: requires password for all commands
%sudo ALL=(ALL:ALL) ALL
# Service account: limited sudo, no password (non-interactive only)
app-user ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp, /bin/systemctl status myapp
# Require password for all others
Defaults use_pty # Use pseudo-terminal (prevent TTY bypass)
Defaults log_input, log_output # Log all input/output to /var/log/sudo-io/
Defaults requiretty # Require terminal
Defaults env_reset # Clear environment variables
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Remove NOPASSWD if possible; only for non-interactive services
Verify configuration:
sudo -l # List your sudo permissions
sudo -i whoami # Should return: root (and prompt for password)
# Check sudo logs
grep sudo /var/log/auth.log # Ubuntu/Debian
grep sudo /var/log/secure # RHEL/Fedora
6. Audit Logging (auditd)
Basic Audit Rules
What it is:
- Kernel audit subsystem; logs sensitive system calls
- Enables forensics (who accessed what file, when)
- Compliance (PCI-DSS, HIPAA require audit trails)
Install & configure:
apt install auditd audispd-plugins
# Create rules file: /etc/audit/rules.d/custom.rules
sudo tee /etc/audit/rules.d/custom.rules > /dev/null << 'AUDIT'
# Remove any existing rules
-D
# Buffer Size
-b 8192
# Failure Mode (2 = kernel panic on write failure)
-f 1
# Audit the audit logs (meta-audit)
-w /var/log/audit/ -k auditlog
# Monitor sudo usage
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# Monitor SSH configuration
-w /etc/ssh/sshd_config -p wa -k sshd_config
# Monitor system calls: execve (command execution)
-a always,exit -F arch=b64 -S execve -F uid>=1000 -F auid!=-1 -k exec
# Monitor user/group modifications
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
# Monitor network configuration
-w /etc/hosts -p wa -k network
-w /etc/hostname -p wa -k network
-w /etc/sysconfig/network -p wa -k network
# Make configuration immutable (can't be changed until reboot)
-e 2
AUDIT
# Load rules
sudo service auditd restart
# Verify
sudo auditctl -l # List loaded rules
Query audit logs:
# View all audit logs
sudo ausearch -k sudoers # Find sudoers access
sudo ausearch -k exec -ts today # Commands executed today
sudo ausearch -m EXECVE -ts 10:00:00 # Commands at specific time
sudo aureport -au --summary # User activity summary
7. systemd Unit Hardening
Secure Service Unit Template
What it is:
NoNewPrivileges=yes
: Prevent privilege escalation via setuidPrivateTmp=yes
: /tmp isolated per serviceProtectSystem=strict
: Mount /usr, /boot read-onlyProtectHome=read-only
: Home directories read-onlyCapabilityBoundingSet
: Drop unnecessary capabilities (least privilege)
Example hardened service (/etc/systemd/system/myapp.service
):
[Unit]
Description=My Secure Application
Documentation=https://docs.example.com/myapp
After=network-online.target
Wants=network-online.target
[Service]
# Basic config
Type=simple
User=app-user
Group=app-group
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp-server --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5
# ===== SECURITY HARDENING =====
# Privilege & capabilities
NoNewPrivileges=yes # No setuid escalation
PrivateUsers=yes # User namespace isolation
CapabilityBoundingSet=CAP_NET_BIND_SERVICE # Only bind ports <1024
AmbientCapabilities=CAP_NET_BIND_SERVICE # Ambient caps (inherited by children)
# Filesystem restrictions
PrivateTmp=yes # Isolated /tmp
ProtectSystem=strict # /usr, /boot, /etc read-only
ProtectHome=yes # Home dirs inaccessible
ProtectClock=yes # No clock modification
ProtectHostname=yes # No hostname change
ProtectKernelLogs=yes # No kernel logs
ProtectKernelModules=yes # No module loading
ProtectKernelTunables=yes # No sysctl changes
PrivateDevices=yes # Private /dev
# Networking restrictions
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 # Allowed address families
RestrictRealtime=yes # No realtime scheduling
RestrictNamespaces=yes # No namespace changes
# Syscall restrictions (optional, for high-security apps)
SystemCallFilter=@system-service # Allow common syscalls
SystemCallFilter=~@privileged @resources # Deny privileged syscalls
# Seccomp (advanced: filter specific syscalls)
# SeccompFilter=/etc/apparmor.d/seccomp.bpf # Custom seccomp BPF
# Resource limits
CPUQuota=50% # Max 50% of 1 core
MemoryLimit=1G # Max 1GB RAM
TasksMax=1000 # Max 1000 processes/threads
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
Load and test:
sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
# Verify hardening
sudo systemd-analyze security myapp.service # Show hardening score
sudo journalctl -u myapp.service -f # Monitor logs
Security Baseline Checklist
Pre-Deployment
- Minimal install (no GUI, unnecessary packages removed)
- Automated patching enabled (unattended-upgrades/dnf-automatic)
- SSH keys generated (Ed25519 or RSA 4096-bit, passphrase protected)
- SSH public keys copied to
~/.ssh/authorized_keys
-
PermitRootLogin no
enforced -
AllowUsers
whitelist configured -
PasswordAuthentication no
enabled - Firewall rules validated (default-deny inbound)
- Required ports only (SSH, HTTP/HTTPS, services)
- LSM policy loaded (SELinux enforcing or AppArmor enforce)
- Policy exceptions documented
- auditd rules loaded
- sudo configured (NOPASSWD only for specific commands)
- systemd hardening applied to critical services
Post-Deployment
- SSH key login works without password
- SSH password auth fails
- Firewall rules working (
nft list ruleset
/ufw status
) - Blocked ports reject connections
- Audit logs being written (
ausearch
queries work) - Sudo usage logged (
grep sudo /var/log/auth.log
) - Automatic patches applied (check
/var/log/unattended-upgrades/
) - LSM denials monitored (no unexpected blocks)
- Fail2ban blocks repeated failed logins (if enabled)
Ongoing
- Weekly: Review audit logs for anomalies
- Monthly: Test SSH key rotation
- Monthly: Verify patch status (
apt list --upgradable
) - Quarterly: Test service restart (ensure hardening holds)
- Quarterly: Review & update firewall rules
Common Pitfalls
Pitfall 1: Lockout Without Backdoor
Problem: Disable SSH password auth โ forget to add SSH keys โ locked out
Fix: Always copy public key BEFORE disabling passwords; test login before reboot
Pitfall 2: Firewall Blocks Legitimate Traffic
Problem: Default-deny firewall โ forget to open required port โ service unreachable
Fix: Document all open ports; test each port before deploying to production
Pitfall 3: SELinux Overly Restrictive
Problem: Policy too strict โ all app requests denied โ cryptic errors
Fix: Start in permissive
mode, monitor denials, gradually move to enforce
Pitfall 4: sudo Misconfiguration
Problem: NOPASSWD
for interactive commands โ privilege escalation
Fix: Use NOPASSWD
only for non-interactive, single-purpose commands (e.g., systemctl restart)
Pitfall 5: Audit Logging Disabled
Problem: Security incident โ no logs to investigate โ unable to respond
Fix: Enable auditd on day 1; test log queries regularly
Quick Hardening Checklist (One-Liner Setup)
#!/bin/bash
# Emergency hardening script (review before running!)
# 1. Update system
apt update && apt upgrade -y
# 2. Remove unnecessary packages
apt remove --purge xserver-xorg* snapd ubuntu-desktop apport cups -y
apt autoremove --purge -y
# 3. Install security tools
apt install -y openssh-server openssh-client fail2ban nftables auditd sudo
# 4. Enable unattended upgrades
apt install -y unattended-upgrades
systemctl enable unattended-upgrades
# 5. SSH hardening (backup first!)
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sshd -t && systemctl restart ssh
# 6. Enable firewall (default-deny)
systemctl enable nftables
systemctl start nftables
# 7. Enable auditd
systemctl enable auditd
systemctl start auditd
# 8. Verify
echo "=== Security Status ==="
getenforce 2>/dev/null || apparmor_status | head -5
nft list ruleset | head -5
auditctl -l | head -5