Linux Security Fundamentals
Linux powers most servers, containers, and cloud workloads, so securing it is a core defensive skill. This guide focuses on hardening: understanding the permission model, locking down remote access, enforcing mandatory access control, and detecting the signs of privilege escalation. Everything here should be practiced on systems you own, such as a local VM or a home lab.
The Permission Model
Every file has an owner, a group, and three permission triads: user, group, and other. Each triad has read (r=4), write (w=2), and execute (x=1) bits, combined into an octal value.
| Octal | Symbolic | Meaning |
|---|---|---|
| 644 | rw-r--r-- | Owner writes, all read |
| 600 | rw------- | Private (keys, secrets) |
| 755 | rwxr-xr-x | Executable/dir, all traverse |
| 700 | rwx------ | Owner-only directory |
# Set permissions and ownership
chmod 600 ~/.ssh/id_ed25519 # private key: owner read/write only
chmod 755 /usr/local/bin/tool # executable, world-readable
chown alice:developers report.txt # change owner and group
chmod u+x,g-w,o= script.sh # symbolic: add exec, drop group write, clear other
Never 777
A chmod 777 makes a file world-writable, letting any user or compromised process modify it. If you think you need it, you almost certainly need correct ownership instead.
Users, Groups, and sudo
Users are defined in /etc/passwd, password hashes in /etc/shadow, and groups in /etc/group. sudo grants controlled privilege escalation, logged and configured in /etc/sudoers. Grant the narrowest command set, not blanket root.
# Edit sudoers safely (validates syntax on save)
visudo
# Least-privilege sudo entry: allow only service restarts
alice ALL=(root) /usr/bin/systemctl restart nginx
# Review who can do what
sudo -l -U alice
SUID, SGID, and the Sticky Bit
The SUID bit makes an executable run as its owner (often root) regardless of who launches it. This is why passwd can update /etc/shadow. But SUID is a classic privilege-escalation vector: a misconfigured or vulnerable SUID binary can hand an attacker root. SGID applies the same idea to groups, and the sticky bit on a directory (like /tmp) stops users deleting each other's files.
# Audit all SUID binaries on the system
find / -perm -4000 -type f 2>/dev/null
# Find SGID binaries
find / -perm -2000 -type f 2>/dev/null
# Remove SUID from a binary that does not need it
chmod u-s /path/to/binary
Baseline your SUID set
Record the expected SUID binaries on a clean system. Any new SUID binary appearing later is a strong indicator of tampering. Cross-reference unfamiliar entries against the GTFOBins project to understand escalation risk.
SSH Hardening
SSH is the primary remote-access surface. Prefer key-based authentication (Ed25519), disable password login and direct root login, and limit who may connect.
# /etc/ssh/sshd_config (hardened)
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
AllowUsers alice bob
MaxAuthTries 3
LoginGraceTime 20
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
# Generate a modern key and reload the daemon
ssh-keygen -t ed25519 -a 100 -C "alice@laptop"
sudo systemctl reload ssh
Host Firewall with ufw
ufw is a friendly front end to nftables. Default to deny inbound, allow outbound, then open only required ports.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp # rate-limit SSH against brute force
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Mandatory Access Control: SELinux vs AppArmor
Standard permissions are discretionary. MAC systems enforce policy the user cannot override, confining processes to only what they need even if compromised.
| SELinux | AppArmor | |
|---|---|---|
| Model | Label-based | Path-based |
| Default on | RHEL/Fedora | Ubuntu/SUSE |
| Modes | enforcing/permissive/disabled | enforce/complain |
# SELinux: check and set enforcing
getenforce
sudo setenforce 1 # enforcing until reboot
# persist in /etc/selinux/config: SELINUX=enforcing
# AppArmor: check status and enforce a profile
sudo aa-status
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
Auditing and Logging
auditd records security-relevant events; journald aggregates systemd logs. Ship logs off-host so an attacker cannot erase their tracks.
| Path | Contents |
|---|---|
| /var/log/auth.log | Auth, sudo, SSH (Debian/Ubuntu) |
| /var/log/secure | Auth events (RHEL) |
| /var/log/syslog | General system messages |
| /var/log/audit/audit.log | auditd records |
# Failed SSH logins
sudo grep "Failed password" /var/log/auth.log
sudo journalctl -u ssh --since "1 hour ago"
# Watch a sensitive file with auditd
sudo auditctl -w /etc/passwd -p wa -k passwd_changes
sudo ausearch -k passwd_changes
Detecting Privilege Escalation
Attackers who gain a foothold hunt for a path to root. Regularly review the classic vectors: unexpected SUID binaries, world-writable files in PATH, writable cron jobs, and overly broad sudoers rules.
# Review scheduled tasks
cat /etc/crontab; ls -la /etc/cron.*
# World-writable files (dangerous if in a privileged path)
find / -xdev -type f -perm -0002 2>/dev/null
# Capabilities that grant elevated powers
getcap -r / 2>/dev/null
File Integrity Monitoring
AIDE (Advanced Intrusion Detection Environment) builds a database of file hashes and attributes, then reports any change. Store the baseline database on read-only or off-host media so it cannot be silently rewritten.
sudo aideinit # build baseline
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
sudo aide --check # compare current state to baseline
Least Privilege
The unifying principle: every user, process, and service should have only the access it needs and nothing more. Run services as dedicated non-root accounts, drop Linux capabilities you do not use, isolate workloads with containers or namespaces, and remove packages you do not run. A smaller footprint means fewer things to patch and fewer paths to abuse.
Practice Exercises
- On a Linux VM, audit permissions in a web root: find any world-writable files with
find . -perm -0002and correct ownership and modes to least privilege. - Harden
sshdon the VM: switch to Ed25519 key auth, setPermitRootLogin noandPasswordAuthentication no, reload, and confirm password login is refused. - Enable
ufwwith default-deny inbound, allow only SSH (rate-limited) and your app port, and verify withufw status verbose. - Set SELinux to enforcing (or put an AppArmor profile into enforce mode) and confirm services still start; investigate and resolve any denials in the audit log.
- Enumerate all SUID binaries with
find / -perm -4000 -type f, compare against a documented baseline, and note any you cannot account for. - Review
/var/log/auth.log(orjournalctl -u ssh) for failed login attempts, then practice this against an intentionally vulnerable box like DVWA or a TryHackMe/HackTheBox Linux room.