The 81 commands used most for host triage and day-to-day administration, grouped by the question you are trying to answer. Practise them for real in the Linux Terminal Lab.
grep finds it, cut/awk pull the column, sort | uniq -c counts it. That pipeline answers most triage questions.
grep -n 'Failed password' /var/log/auth.log-n line numbers, -i ignore case, -v invert, -c count only
grep -rni 'password' /etc/ 2>/dev/nullSearch every file under a directory; -l lists only filenames
grep -E 'Accepted (password|publickey)' /var/log/auth.log-E enables | ( ) + ? without escaping; -o prints only the match
cut -d: -f1,3,7 /etc/passwd-d sets the delimiter, -f picks fields; the standard way to read /etc/passwd
awk '{print $1, $11}' /var/log/auth.log$N is the Nth whitespace-separated field, $NF the last; works on any log
sort | uniq -c | sort -rn | headAppend to any pipeline to turn a list into a ranked count
grep 'Failed password' /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | headFailed logins per source IP; the pipeline above applied to the question you ask most
sed -n '100,120p' file.log; sed 's/192.0.2.10/REDACTED/g' report.txt-n with p prints a line range; s/// substitutes (never edit evidence in place)
strings -n 8 /tmp/.x | head -40URLs, IPs and paths inside an unknown file, without running it
echo 'aGVsbG8=' | base64 -dEncoded cron lines and one-liners usually decode to the real command
diff -u /etc/passwd /backup/etc/passwdUnified diff against a known-good copy shows exactly what was added
Who can read, write and run what. Wrong answers here are how attackers stay.
chmod 600 ~/.ssh/id_ed25519; chmod +x script.shOctal: r=4 w=2 x=1 per owner/group/other; keys must be 600
chown -R www-data:www-data /var/www/app-R recurses; a root-owned file in a web root is a tell
find / -perm /6000 -type f 2>/dev/null | xargs ls -laSet-uid binaries run as their owner; an unexpected one in /tmp or /usr/local is a backdoor
find / -writable -type f -not -path '/proc/*' 2>/dev/nullAnything writable by everyone in a system path is a privilege-escalation path
lsattr /etc/passwd /etc/shadow /etc/crontab /root/.bash_historyThe i (immutable) or a (append-only) flag you did not set is a tamper tell; chattr changes it
sudo -lLists this account's sudo rights; NOPASSWD entries deserve a second look
cat /etc/hosts && sudo cat /etc/sudoers /etc/sudoers.d/* 2>/dev/nullRedirected hostnames and new NOPASSWD grants are quiet persistence and pivot tells
Who exists, who is in, who was in, and how they got there.
id && whoami && groupsuid, gid and group membership; uid 0 is root whatever the name says
grep -vE 'nologin|false' /etc/passwd | cut -d: -f1,3,6,7Accounts with a shell; a new uid-0 user or a service account with bash is a tell
sudo cat /etc/shadow | cut -d: -f1,2 | headField 2: ! or * means locked, $6$ is SHA-512; an unlocked service account is a tell
w && who -aLive sessions with source address and idle time
last -a -n 20 && sudo lastb -a -n 10last reads wtmp (successes), lastb reads btmp (failures); gaps mean log tampering
lastlog | grep -v 'Never'One line per user; a service account that logged in is a tell
grep -E 'Accepted (password|publickey)' /var/log/auth.log | tail -20Who got in, from where, and how; compare against the brute-force sources
for h in /root /home/*; do echo "== $h"; cat $h/.ssh/authorized_keys 2>/dev/null; doneAudit every account's authorized_keys for a key nobody recognises
for h in /root /home/*; do echo "== $h"; tail -50 $h/.bash_history 2>/dev/null; doneWhat each account typed; an emptied or truncated history is itself evidence
sudo usermod -L attacker && sudo pkill -KILL -u attackerLock the password and end the sessions; do this after collecting evidence, not before
What is running, who started it, and whether it should be.
ps aux --sort=-%cpu | head -20User, PID, CPU, start time and full command line; --forest shows the parent tree
ps -ef --forest | lessA shell whose parent is a web server or cron is the shape of a reverse shell
top -o %CPU # or: htopPress k to kill, q to quit; a miner shows as sustained CPU under an odd name
ls -la /proc/*/exe 2>/dev/null | grep '(deleted)'A process whose binary is gone from disk is a classic in-memory implant
cat /proc/1234/cmdline | tr '\0' ' '; ls -la /proc/1234/cwdArguments and working directory straight from the kernel, even if ps was tampered with
kill -15 1234; kill -9 123415 (TERM) asks politely, 9 (KILL) does not; capture memory first if you need it
systemctl status nginx; systemctl list-units --type=service --state=runningRunning services and their recent log lines
systemctl list-unit-files --state=enabled && ls -la /etc/systemd/system /home/*/.config/systemd/user 2>/dev/nullEnabled units and their files; a recently added unit with a /tmp ExecStart is persistence
cat /etc/crontab; ls -la /etc/cron.*; for u in $(cut -f1 -d: /etc/passwd); do sudo crontab -l -u $u 2>/dev/null; doneSystem and per-user crontabs; curl | sh in a crontab is the most common persistence
lsof -p 1234; lsof -i :4444What a process has open, or which process owns a port
Listeners, connections, resolution and reachability.
ip addr && ip routeInterfaces, IPs and the default gateway (ifconfig is deprecated)
ss -tulpnTCP/UDP listeners with the owning process; an unknown high port is a bind shell
ss -antup | grep ESTABWho this host is talking to right now, with the process
dig +short api.vendor.test; getent hosts api.vendor.testgetent honours /etc/hosts, dig asks DNS; a mismatch means /etc/hosts was edited
curl -sI https://example.test; curl -s -o /dev/null -w '%{http_code}\n' https://example.test-I headers only, -w prints the status code
ping -c 4 198.51.100.7; traceroute 198.51.100.7Is it up, and which hops are in the way
sudo tcpdump -i eth0 -nn -w /tmp/capture.pcap host 203.0.113.5-nn skips name resolution, -w writes a pcap for Wireshark
sudo iptables -L -n -v --line-numbers # or: sudo nft list rulesetList rules with hit counters; an ACCEPT rule you did not write is a tell
ssh -i key.pem admin@198.51.100.7; scp admin@198.51.100.7:/var/log/auth.log ./evidence/Pull evidence off a host over SSH; -r copies directories
Where Linux writes what happened, and how to read a window of it.
ls -la /var/log/ && tail -n 5 /var/log/auth.log /var/log/syslogauth.log logins and sudo, syslog everything else, kern.log kernel, apache2/nginx web
journalctl --since '2026-10-10 02:00' --until '2026-10-10 03:00' -o short-isoEvery journald entry in the incident window; -u ssh filters one unit, -b this boot
journalctl -u ssh --since yesterday | grep -E 'Failed|Accepted'Distributions without auth.log keep the same lines in the journal
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | headTop source IPs; swap $1 for $7 to rank requested paths and find the web shell
awk '{print $1, $2, substr($3,1,2)}' /var/log/auth.log | uniq -cEntries per hour; a missing hour where there should be traffic means lines were deleted
sudo dmesg -T | tail -50Segfaults, OOM kills, USB devices and module loads with human timestamps
date -u && timedatectlKnow the host's timezone before correlating its logs with anything else
Identify a file without running it, and prove what you collected.
file /tmp/.xELF, script, archive or data from the magic bytes, whatever the extension says
sha256sum /tmp/.x; md5sum /tmp/.xLook the hash up in threat intel; record it before the file is touched
sha256sum -c SHA256SUMS 2>/dev/null | grep -v OKPrints only the files whose hash does not match
file /tmp/.x && strings -n 8 /tmp/.x | head -40 && sha256sum /tmp/.xType, readable strings and a hash in one line, without executing it
sudo debsums -c 2>/dev/null # RPM: rpm -VaLists installed files whose contents no longer match the package: a replaced ls or ps
sudo tar -czvf /tmp/evidence-$(hostname)-$(date +%F).tgz /var/log /etc/crontab /home/*/.bash_historyOne archive of the files you will need; hash it afterwards
sudo dd if=/dev/sdb of=/mnt/case/sdb.img bs=4M status=progress conv=noerror,syncBit-for-bit copy; hash the image and the source to prove they match
What this machine is and what is installed on it.
uname -a && cat /etc/os-release && hostname -fVersion numbers you need before looking up a CVE
uptimeA recent reboot you did not do, or load that does not match the role
env | sort && cat /etc/environmentA changed PATH or LD_PRELOAD is how a shell gets hijacked
dpkg -l | less # RPM: rpm -qa | sortWhat is installed; grep for tools that should not be on a server (nmap, nc, socat)
sudo apt update && sudo apt install -y package # RPM: sudo dnf install packagePackage manager basics; apt list --upgradable shows pending security updates
which ls; type ls; ls -la $(which ls)An alias or a binary in a user-writable path ahead of /bin is a tell
lsmod | head; sudo modinfo module_nameA loaded module with no package and no signature is a rootkit candidate
history | tail -30What you ran in this session; keep it, it is part of the investigation record
man grep; grep --help | less; tldr tarThe manual is on the host; / searches inside man, q quits