Linux Cheat Sheet
Production Linux reference for platform, DevOps, and SRE engineers. Covers day-to-day operations and incident work: permissions, users and sudo, processes, systemd and journald, packages, networking, firewalls, SSH, storage, performance triage, and security hardening - plus a condensed workstation/WSL2 setup section at the end.
Versions: Ubuntu 22.04+ / Debian 12+ (apt,
systemd) and Fedora 38+ / RHEL 9+ (dnf,firewalld, SELinux). Networking examples useiproute2(ip,ss), not the deprecatednet-tools(ifconfig,netstat). For shell scripting idioms (strict mode, traps, arg parsing) see the Bash cheatsheet; for Docker/Podman see Containers.Last reviewed: May 2026
System & Distro Info
uname -a # kernel, arch, hostname
cat /etc/os-release # distro id/version (scriptable: $ID, $VERSION_ID)
hostnamectl # hostname, OS, kernel, virtualization
uptime # load averages + how long up
who -b ; last -x | head # last boot; login/reboot history
lscpu ; nproc # CPU topology; logical core count
free -h # memory + swap
timedatectl # time, timezone, NTP sync status✅ Branch scripts on
/etc/os-release(. /etc/os-release; echo "$ID"), not on the presence ofapt/dnf- it’s the stable, machine-readable source of truth.
Files, Directories & Permissions
Inspect
ls -lah # long, human sizes, hidden
stat file # perms, owner, timestamps, inode
file binary # detect type
find /var/log -type f -name '*.log' -mtime -1 # modified in last 24h
find . -type f -size +100M -exec ls -lh {} \; # large files
du -sh ./* | sort -h # dir sizes, sortedPermissions (numeric + symbolic)
chmod 640 file # rw- r-- --- (owner rw, group r)
chmod u+x,g-w file # symbolic
chmod -R o-rwx dir # strip "other" recursively
chown user:group file # change owner + group
chown -R app: /srv/app # group defaults to user's primary
umask 027 # new files 640, dirs 750 (set in shell profile)| Octal | Symbolic | Means |
|---|---|---|
| 644 | rw-r--r-- | files: owner writes, others read |
| 755 | rwxr-xr-x | dirs/binaries: owner full, others traverse/run |
| 640 | rw-r----- | secrets readable by owner + group only |
| 600 | rw------- | private (SSH keys, tokens) |
Special bits & ACLs
chmod u+s /usr/bin/foo # setuid (runs as file owner) - audit these
chmod g+s /srv/shared # setgid on dir: new files inherit group
chmod +t /tmp # sticky bit: only owner can delete their files
find / -perm -4000 -type f 2>/dev/null # audit all setuid binaries
# POSIX ACLs (finer-grained than owner/group/other)
setfacl -m u:deploy:rx /srv/app
getfacl /srv/app⚠️
chmod 777is almost never correct. It makes a path world-writable - any user or compromised process can replace its contents. Grant the narrowest owner/group + ACL that works.
Users, Groups & sudo
id alice ; groups alice # uid/gid/groups
sudo useradd -m -s /bin/bash -G sudo alice # create with home + shell + group
sudo usermod -aG docker alice # add to a group (-a is mandatory, else replaces!)
sudo passwd alice # set/reset password
sudo chage -l alice # password aging info
sudo userdel -r alice # delete + home
getent passwd alice # resolves local + LDAP/SSSD
sudo deluser alice docker # remove from a group (Debian)sudo, done safely
sudo visudo # edit /etc/sudoers (validates before save)
sudo visudo -f /etc/sudoers.d/deploy # prefer drop-in files over editing the main file# /etc/sudoers.d/deploy - least privilege, not blanket ALL
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp, /usr/bin/journalctl -u myapp⚠️
usermod -Gwithout-areplaces all of a user’s supplementary groups. Always useusermod -aG. ✅ Scope sudo to specific commands in/etc/sudoers.d/*rather than grantingALL. Validate withvisudoso a syntax error can’t lock you out of root.
Processes & Signals
ps aux --sort=-%mem | head # top memory consumers
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head
pgrep -af nginx # PIDs + full cmdline by name
pstree -ap | less # process tree with PIDs
top # or: htop # interactive; htop is friendlierSignals & control
kill -TERM <pid> # 15: polite stop (default); let it clean up
kill -HUP <pid> # 1: reload config (many daemons)
kill -KILL <pid> # 9: last resort, no cleanup
pkill -f 'python app.py' # by cmdline pattern
kill -0 <pid> # test existence without signalling
nice -n 10 ./batch.sh # start low-priority
renice -n 5 -p <pid> # adjust running priority
nohup ./long.sh >out.log 2>&1 & # survive logout (prefer a systemd unit)✅ Reach for
SIGTERM(the default) and give the process time to shut down cleanly; only escalate toSIGKILL(-9) when it’s truly stuck.-9can’t be trapped, so buffers and locks may be left dirty.
systemd: Services, Units & Timers
systemctl status myapp # state, recent logs, main PID
systemctl start|stop|restart myapp
systemctl reload myapp # reload config without restart (if supported)
systemctl enable --now myapp # start now + on boot
systemctl disable --now myapp
systemctl mask myapp # forcibly prevent start (stronger than disable)
systemctl list-units --type=service --state=running
systemctl list-unit-files --state=enabled
systemctl is-active myapp ; systemctl is-enabled myapp
systemctl daemon-reload # after editing unit files
systemd-analyze blame # slowest units at bootA minimal service unit
# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=app
ExecStart=/srv/app/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp
[Install]
WantedBy=multi-user.targetsudo systemctl edit myapp # creates a drop-in override (preferred over editing the unit)
sudo systemctl daemon-reload && sudo systemctl restart myappTimers (the modern cron)
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true # run on boot if a scheduled run was missed
[Install]
WantedBy=timers.targetsystemctl enable --now backup.timer
systemctl list-timers --all✅ Prefer a systemd service over
nohup ... &, and a systemd timer over a crontab for anything important - you get logging, restart policy, dependency ordering, sandboxing, andPersistent=truecatch-up.
Logs & journald
journalctl -u myapp -f # follow a unit (like tail -f)
journalctl -u myapp --since "1 hour ago" --no-pager
journalctl -u myapp -p err -b # priority err+ for current boot
journalctl -b -1 # previous boot
journalctl -k # kernel ring buffer (dmesg)
journalctl --disk-usage
sudo journalctl --vacuum-time=7d # prune older than 7 days
dmesg -T --level=err,warn # kernel errors with human timestamps
tail -F /var/log/syslog /var/log/auth.log # -F survives logrotate✅ Use
tail -F(capital F) on log files - it re-opens the file acrosslogrotate, unlike-fwhich keeps following the now-rotated inode.
Package Management
Debian / Ubuntu (apt / dpkg)
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx
sudo apt remove --purge nginx # remove config too
sudo apt autoremove --purge # drop orphaned deps
apt policy nginx # installed + candidate versions
apt-mark hold nginx # pin: don't auto-upgrade
dpkg -l | grep nginx ; dpkg -L nginx # installed packages; files in a package
dpkg -S /usr/sbin/nginx # which package owns a fileFedora / RHEL (dnf / rpm)
sudo dnf upgrade --refresh -y
sudo dnf install -y nginx
sudo dnf remove nginx
dnf repoquery --installed
dnf versionlock add nginx # pin (needs python3-dnf-plugin-versionlock)
rpm -qa | grep nginx ; rpm -ql nginx # installed; files
rpm -qf /usr/sbin/nginx # which package owns a file✅ For unattended security patching use
unattended-upgrades(Debian/Ubuntu) ordnf-automatic(Fedora/RHEL), scoped to the security repo, with automatic reboots only inside a maintenance window.
Networking
ip addr ; ip -br a # interfaces/addresses (brief)
ip route ; ip route get 1.1.1.1 # routing table; chosen route to a target
ip link set eth0 up|down
ss -tulpn # listening TCP/UDP sockets + owning process
ss -tnp state established # active connections
resolvectl status # DNS servers (systemd-resolved)
dig +short example.com ; dig -x 1.1.1.1 # forward; reverse lookup
getent hosts example.com # resolves via nsswitch (hosts file + DNS)
curl -fsSL https://api.example.com/health # -f fail on HTTP error, -sS quiet but show errors
curl -o /dev/null -s -w '%{http_code} %{time_total}s\n' https://example.com
nc -zv host 443 # TCP port reachability✅
ssreplacesnetstat,ipreplacesifconfig/route. Thenet-toolscommands are deprecated and often not installed on modern minimal images.
Firewalls
ufw (Ubuntu)
sudo ufw default deny incoming ; sudo ufw default allow outgoing
sudo ufw allow 22/tcp ; sudo ufw allow from 10.0.0.0/8 to any port 5432
sudo ufw enable ; sudo ufw status numberedfirewalld (Fedora/RHEL)
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload ; sudo firewall-cmd --list-allnftables (the modern backend)
sudo nft list ruleset
sudo nft add rule inet filter input tcp dport 22 accept⚠️ Before tightening firewall rules on a remote box, stage a rollback (e.g. an
at-scheduledufw disable/firewall-cmd --reload) so a mistaken rule that drops your SSH session doesn’t lock you out permanently.
SSH & Remote Access
ssh-keygen -t ed25519 -C "you@host" # modern key (not RSA)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
ssh -i ~/.ssh/id_ed25519 user@host
scp file user@host:/path/ # or use rsync for anything non-trivial
ssh -J bastion user@private-host # jump host (ProxyJump)
ssh -L 5432:db.internal:5432 user@bastion # local port-forward to a private service# ~/.ssh/config - stop typing flags
Host prod-bastion
HostName bastion.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Host prod-*
ProxyJump prod-bastionHarden sshd
# /etc/ssh/sshd_config.d/hardening.conf (drop-in)
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yessudo sshd -t && sudo systemctl reload ssh # validate config BEFORE reloading✅ Always
sshd -tbefore reloading sshd. A config typo that fails to start the daemon will lock you out of a remote machine.
Disks, Filesystems & LVM
df -hT # usage + filesystem type
du -xsh /var/* 2>/dev/null | sort -h # -x stays on one filesystem
lsblk -f # block devices + filesystems + mountpoints
mount | column -t ; findmnt # current mounts (findmnt is clearer)
blkid # UUIDs for /etc/fstab
# Mount + persist
sudo mount /dev/sdb1 /mnt/data
# /etc/fstab (use UUID, not /dev/sdX which can reorder across boots)
# UUID=xxxx-xxxx /mnt/data ext4 defaults,noatime 0 2
sudo systemctl daemon-reload && sudo mount -a # test fstab without rebootingLVM essentials
pvcreate /dev/sdb ; vgcreate data /dev/sdb
lvcreate -L 50G -n app data
mkfs.ext4 /dev/data/app
lvextend -r -L +20G /dev/data/app # grow LV + resize filesystem in one step⚠️ Reference filesystems by
UUID=in/etc/fstab, never/dev/sdb1. Device names are not stable across reboots or disk additions, and a wrong mount can fail boot. Addnofailfor non-critical mounts.
Performance & Troubleshooting
uptime # load avg - compare against nproc
vmstat 1 5 # CPU/mem/IO/swap over 5 samples
iostat -xz 1 # per-disk IO (sysstat package)
free -h ; cat /proc/meminfo
top -o %MEM # sort by memory
lsof -p <pid> # files/sockets a process holds
lsof -i :443 # what's on a port
fuser -k 8080/tcp # kill whatever holds a TCP port
strace -fp <pid> # syscalls of a running process
strace -f -e trace=openat ./app # which files it opens
# Disk filling up? find the biggest dirs fast:
sudo du -xh / 2>/dev/null | sort -h | tail -20
# Deleted-but-held files still consuming space:
sudo lsof +L1🔬 High load average with low CPU usually means IO wait or uninterruptible-sleep processes (
ps -eo state,cmd | grep '^D'), not a CPU bottleneck. Checkiostat -xz 1before scaling cores.
Archives, Transfer & Sync
tar czf out.tgz dir/ # create gzip
tar xzf out.tgz -C /target # extract to dir
tar tzf out.tgz | head # list without extracting
tar caf out.tar.zst dir/ # -a picks compressor by extension (zstd)
rsync -avz --progress src/ user@host:/dst/ # archive, compress, show progress
rsync -avz --delete --dry-run src/ /dst/ # mirror; ALWAYS dry-run --delete first
rsync -e 'ssh -i ~/.ssh/id_ed25519' -avz src/ user@host:/dst/⚠️
rsync --deleteremoves anything in the destination not in the source. Always run it with--dry-runfirst, and mind the trailing slash:src/copies contents,srccopies the directory itself.
Scheduling
crontab -e ; crontab -l # per-user cron
# m h dom mon dow command (always use absolute paths; cron has a minimal PATH/env)
# 0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1For anything important, prefer a systemd timer (see the systemd section): logging via journald, restart/retry policy, and Persistent=true to catch up missed runs after downtime. Use OnCalendar= syntax; validate with systemd-analyze calendar "*-*-* 02:00:00".
Security Hardening
# Keep security patches current
sudo apt install -y unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades
# Brute-force protection for SSH and more
sudo apt install -y fail2ban && sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
# Mandatory access control - know which one is active and keep it on
getenforce # SELinux (RHEL/Fedora): Enforcing
sudo aa-status # AppArmor (Ubuntu/Debian)
# Kernel/network sysctls (persist in /etc/sysctl.d/*.conf)
sudo sysctl -w net.ipv4.conf.all.rp_filter=1
sudo sysctl --system # reload all *.conf
# Audit who did what
sudo journalctl -u ssh --since today | grep -i 'accepted\|failed'
sudo ausearch -m USER_LOGIN --start today # if auditd is installed✅ Baseline for any internet-facing host: key-only SSH with no root login,
unattended-upgrades/dnf-automaticfor security patches,fail2ban, a default-deny firewall, and SELinux/AppArmor left in enforcing mode. Disabling MAC “to make it work” is a classic regression.
Workstation & Environment Setup
Condensed setup snippets for a dev workstation or WSL2. These are convenience helpers, not server-hardening steps.
Version managers (pyenv / tfenv / goenv)
# Build deps for pyenv (Ubuntu/Debian)
sudo apt-get install -y git build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev libncursesw5-dev xz-utils tk-dev libffi-dev \
liblzma-dev curl
# Fedora/RHEL: sudo dnf install -y git gcc make zlib-devel bzip2-devel readline-devel \
# sqlite-devel openssl-devel tk-devel libffi-devel xz-devel ncurses-devel
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
cat >> ~/.bashrc <<'EOF'
export PYENV_ROOT="$HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
EOF
git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
git clone https://github.com/syndbg/goenv.git ~/.goenv
cat >> ~/.bashrc <<'EOF'
export GOENV_ROOT="$HOME/.goenv"
export PATH="$GOENV_ROOT/bin:$PATH"
eval "$(goenv init -)"
EOF✅ Pin versions explicitly (
pyenv install 3.12.6 && pyenv global 3.12.6,tfenv install 1.9.8 && tfenv use 1.9.8).tfenv install latestresolves at runtime and silently drifts between machines.
CLI tools (apt repos, keyed correctly)
# GitHub CLI
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
sudo apt update && sudo apt install -y gh
# PowerShell Core
wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb && sudo apt-get update && sudo apt-get install -y powershell
# Python CLI tools - isolated, not system-wide
sudo apt-get install -y pipx && pipx ensurepath
pipx install checkov ; pipx install black ; pipx install pipenv✅ Install apt repo keys into
/usr/share/keyrings/and reference them withsigned-by=in the source list. The legacyapt-key addis deprecated and trusts the key for all repos.
CA certificates (corporate TLS interception)
# Debian/Ubuntu
sudo cp custom-ca.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates
# Fedora/RHEL
sudo cp custom-ca.crt /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trust
# Point common tools at the system bundle
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Python requests
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt # NodeWSL2
ip addr ; cat /etc/resolv.conf # WSL2 gets a virtual NIC + generated resolv.conf# /etc/wsl.conf (inside the distro) - persist mounts, hostname, systemd
[boot]
systemd=true
[network]
generateResolvConf=true# %USERPROFILE%\.wslconfig (on Windows) - cap resources for all distros
@"
[wsl2]
memory=8GB
processors=4
"@ | Set-Content -Path "$Env:USERPROFILE\.wslconfig"
# Apply: run `wsl --shutdown` in PowerShell, then reopen the distro# Corporate VPN compatibility (wsl-vpnkit)
wsl --import wsl-vpnkit "$env:USERPROFILE\wsl-vpnkit" wsl-vpnkit.tar.gz --version 2
wsl -d wsl-vpnkit --cd /app service wsl-vpnkit startSee also: Windows - WSL2 for the Windows-side configuration (
.wslconfig, networking mode).
Anti-patterns
-
🚨
curl | bashas root without reviewing the script - it executes arbitrary network-fetched code at root privilege. Download to a file, read it, verify a checksum, then run; or install from a signed package repo. -
🚨
chmod -R 777to “fix” a permissions problem - it makes paths world-writable, so any user or compromised process can tamper with them. Set the narrowest owner/group plus an ACL that actually works. -
🚨
sudo pip installsystem-wide - it corrupts the OS Python managed by apt/dnf and fights the package manager. Usepipxfor CLI tools andpython -m venv/uv venvfor projects. -
🚨 Disabling SELinux/AppArmor or the firewall to make something work - that trades a config fix for a permanent security regression. Fix the policy/port instead and keep enforcement on.
-
⚠️
usermod -Gwithout-a- it replaces the user’s supplementary groups instead of appending, silently dropping them from (for example)dockerorsudo. Alwaysusermod -aG. -
⚠️ Referencing disks by
/dev/sdXin/etc/fstab- device names reorder across reboots and disk additions; a wrong mount can fail boot. UseUUID=and addnofailfor non-critical mounts. -
⚠️ Pinning nothing in setup scripts (
install latest) -tfenv install latest/goenv install latestresolve at runtime, so a later run silently installs a different version. Pin explicit versions for reproducibility. -
⚠️
nohup ... &/ hand-rolled init for long-running services - you lose logging, restart policy, and ordering. Write a systemd unit (or timer) instead. -
⚠️ Sourcing
~/.bashrcinside non-interactive scripts - it pulls in aliases,PS1, and interactive-only settings that cause surprising behaviour. Set the env vars the script needs explicitly. -
🔬
tail -fon files that get rotated - it keeps following the rotated-away inode and goes silent. Usetail -F, orjournalctl -u <unit> -ffor systemd services. -
🔬 Hardcoding one user’s home path in shared scripts -
/home/alice/...breaks for everyone else. Use$HOME,~, or"$(cd "$(dirname "$0")" && pwd)".
References
- Arch Wiki - distro-agnostic, deepest practical Linux reference
- systemd documentation - units,
systemctl,journalctl, timers - Ubuntu Server Guide - apt, networking, storage on Debian/Ubuntu
- RHEL Documentation - dnf, SELinux, firewalld on RHEL/Fedora
- OpenSSH manual -
ssh,sshd_config, key management - WSL documentation -
.wslconfig,wsl.conf, networking