Linux Security
Linux security is a discipline of subtraction: every running service is an attack surface, every world-writable file a liability, every setuid binary a privilege. This chapter covers the layers — permissions and least privilege, SSH hardening, the firewall, update hygiene, the audit commands that show you your own exposure, and the SELinux/AppArmor machinery underneath.
The foundation — least privilege
# every service runs as its own user — never root
id nginx # nginx's uid — not root
ps -eo user,comm | grep -v ' root ' | sort -u # who runs what
Principles: run daemons as dedicated users, grant only the rights they need, NOPASSWD: only for specific commands, sudo over su, key-only ssh. The model from chapter 12 — owner/group/other — is the wall; this chapter is keeping the wall maintained.
SSH — the exposed door
# /etc/ssh/sshd_config — the three lines that matter
PermitRootLogin no
PasswordAuthentication no
Port 2222 # obscurity tax, not security — but reduces noise
sshd -t validates before reload. Key-only auth (PasswordAuthentication no) is the single largest hardening win — brute force becomes impossible. fail2ban rate-limits attempts; AllowUsers whitelists.
The firewall — default deny
nft list ruleset # the current policy
# the canonical minimal policy:
iptables -P INPUT DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
Default-deny is the posture; allow what's needed. ss -tlnp tells you what's actually listening — the firewall audit always starts there.
The audit commands — your own exposure
ss -tlnp # every listener — attack surface inventory
find / -perm -4000 -type f # every setuid binary
find / -perm -0002 -type d # world-writable dirs
find / -nouser -o -nogroup # orphaned files
ls -la /etc/sudoers.d/ # the delegation surface
cat /etc/passwd | awk -F: '$3==0' # everyone with uid 0 — only root
last -20 # login history
grep 'Failed password' /var/log/auth.log | tail # brute-force traffic
ss -tlnp, the setuid inventory, last, and the auth log — the four commands that see a system's real exposure.
Updates — the unglamorous hardening
apt update && apt upgrade # Debian
dnf upgrade # Fedora
slackpkg update && slackpkg upgrade-all # Slackware
unattended-upgrades # auto security updates — Debian
needs-restarting -r # does it need a reboot?
Most real-world compromises are unpatched software, not clever attacks — the update habit is the highest-value security control.
The capability grammar — root's powers unbundled
getcap /usr/bin/ping # what does it carry?
# /usr/bin/ping cap_net_raw=ep — raw sockets, no setuid needed
setcap cap_net_bind_service=+ep /usr/bin/myd # bind low ports, not root
setcap -r /path # strip them
getcap -r / 2>/dev/null # every capability file on the system
Capabilities split root into ~40 named powers — cap_net_bind_service (bind <1024), cap_net_raw (raw sockets), cap_sys_admin (the dangerous one — mounts, namespaces), cap_chown, cap_dac_override, cap_kill, cap_setuid, cap_sys_time. Modern daemons carry the one they need instead of full setuid — ping carrying cap_net_raw alone is the proof.
The PAM layer — where auth actually happens
/etc/pam.d/sshd — sshd's auth stack:
auth required pam_env.so
auth required pam_unix.so — the password check
auth required pam_nologin.so
account required pam_unix.so — expiry, policy
password required pam_unix.so — password changes
session optional pam_motd.so
session required pam_limits.so
session optional pam_systemd.so
Four phases — auth/account/password/session; four controls — required/requisite/sufficient/optional. PAM stacks modules — add pam_google_authenticator for TOTP, pam_faillock for lockout — without touching sshd's code.
The hardening checklist — the working sequence
# 1. Accounts
passwd -l root # or keep root-key-only
awk -F: '$2 ~ /^[!*]/ {print $1}' /etc/shadow # locked/no-login accounts
find / -nouser -o -nogroup 2>/dev/null # orphans
# 2. SSH — the door (in /etc/ssh/sshd_config):
# PermitRootLogin no · PasswordAuthentication no · AllowUsers me
sshd -t && systemctl reload sshd
# fail2ban or sshguard # rate-limit attempts
ssh-keygen -t ed25519; ssh-copy-id
# 3. Firewall — default deny
iptables -P INPUT DROP; iptables -P FORWARD DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j LOG --log-prefix "DROP: " # watch what hits the wall
# 4. Services — kill the surface
ss -tlnp # inventory
systemctl disable unused-svc # off
apt remove unused # gone
# 5. Updates — the unglamorous wall
apt update && apt upgrade # the habit
unattended-upgrades # auto security
# 6. The audit
find / -perm -4000 -type f # setuid inventory
find / -perm -0002 -type d # world-writable
last -20; faillog -a # who's tried in
The service hardening — the per-daemon grammar
# systemd's sandboxing — the modern confinement:
[Service]
ProtectSystem=strict # / read-only — services can't touch it
ProtectHome=true # /home invisible
PrivateTmp=true # own /tmp
NoNewPrivileges=true # no privilege escalation
PrivateDevices=true # no /dev
ProtectKernelTunables/Modules/Logs=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE # only this cap
RestrictAddressFamilies=AF_INET AF_INET6 # only these sockets
SystemCallFilter=@system-service # syscall allowlist
ReadWritePaths=/var/lib/myapp # the one writable dir
MemoryMax=512M; CPUQuota=50% # resource caps
The kernel-level machinery
- SELinux — mandatory access control — policies that confine processes beyond file permissions (
sestatus,setenforce,ausearch) — Fedora's default - AppArmor — the simpler MAC — profiles per program (
aa-status) — Debian/Ubuntu's - seccomp — syscall filtering — restricts which syscalls a process may make
- capabilities — root unbundled (chapter 12)
- cgroups + namespaces — the isolation primitives containers ride
Crypto at rest — LUKS + the secrets files
cryptsetup luksFormat /dev/sda3 # encrypt the partition
cryptsetup luksOpen /dev/sda3 crypt
mkfs.ext4 /dev/mapper/crypt
Full-disk encryption (LUKS) protects at rest — a stolen disk yields nothing without the passphrase. /etc/shadow's 600 perms, ~/.ssh's 700, private keys' 600 — the permission layer is the crypto's keeper.
The operational hygiene
umask 027 # default-private for a server
passwd -l dormantacct # lock unused accounts
chmod 600 ~/.ssh/id_* # private keys private
sudo -l # YOUR sudo rights — know them
find . -name '*.key' -perm -o+r # world-readable private keys — fix now
Where security goes wrong
| Symptom | Cause |
|---|---|
| Password auth still on after "hardening" | sshd -t passed, systemctl reload didn't happen |
Permission denied on a legit file | SELinux context — restorecon or chcon |
| Root login still open | PermitRootLogin not actually no |
| A daemon runs as root "for simplicity" | The classic — dedicated user now |
sudo -l shows (ALL) NOPASSWD: ALL | The sudoers gift that defeats sudo |
| A world-writable setuid binary | find it — the audit commands above |
Exercises
ss -tlnpyour box — justify every listener.find / -perm -4000— inventory setuid; research one you don't recognize.sshd -ta hardened sshd_config — verify before reload.ausearch -m avcoraa-status— see your MAC layer.- Write the minimal iptables policy from memory.