Cheat sheet
Everything from the lessons on one page. Rows marked “same on both” work everywhere. Use the Show switch at the top to hide the family you don't use.
Where am I & what’s here
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Print current folder | pwd same on both | |
| List files | ls same on both | |
| List with details / hidden files | ls -l ls -a ls -la same on both | |
| Shortcut “ll” | ls -l | ls -alF |
| Which distro is this? | cat /etc/os-release same on both | |
| Who am I? / my groups | whoami id same on both | |
| Machine name / IP address | hostname hostname -I same on both | |
Moving around
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Go into a folder | cd Documents same on both | |
| Up one level / home / back | cd .. cd cd - same on both | |
| Absolute path | cd /var/log same on both | |
Files & folders
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Make folder / nested folders | mkdir NAME mkdir -p a/b/c same on both | |
| Make empty file | touch FILE same on both | |
| Write / append text | echo "hi" > FILE echo "hi" >> FILE same on both | |
| Read a file | cat FILE less FILE head FILE tail FILE same on both | |
| Copy / copy folder | cp A B cp -r DIR1 DIR2 same on both | |
| Move / rename | mv OLD NEW same on both | |
| Delete file / folder | rm FILE rm -r DIR rmdir EMPTYDIR same on both | |
| Friendly text editor | sudo dnf install nano → nano FILE | nano FILE (pre-installed) |
| Write to a root-owned file | echo "hi" | sudo tee /etc/FILE same on both | |
| Search inside text | grep WORD FILE COMMAND | grep WORD same on both | |
| File details / type | stat FILE file FILE same on both | |
The filesystem
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Settings / logs & data / programs | /etc /var /usr/bin same on both | |
| Your own scripts (everyone) | /usr/local/bin (never /usr/bin) same on both | |
| Old folders merged into /usr | /bin → usr/bin /sbin → usr/sbin /lib → usr/lib same on both | |
| Kernel & bootloader | /boot/vmlinuz-* /boot/grub2/ | /boot/vmlinuz-* /boot/grub/ |
| Extra service settings | /etc/sysconfig/ | /etc/default/ |
| Installed-package database | /var/lib/rpm/ | /var/lib/dpkg/ |
| Repo list | /etc/yum.repos.d/ | /etc/apt/sources.list.d/ |
| Kernel info (virtual) | cat /proc/cpuinfo /proc/meminfo /proc/1/comm /sys/class/net/ same on both | |
| Devices | ls -l /dev (b = block/disk, c = character) /dev/null same on both | |
| sudo finds /usr/local/bin? | No: use the full path | Yes |
| The official map | man hier same on both | |
Finding files & links
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Find by name / type / size | find ~ -name "*.txt" find / -type d -name NAME find ~ -size +100M same on both | |
| Changed recently | find ~ -mtime -1 (last day) find ~ -mmin -30 same on both | |
| Act on results | find … -exec COMMAND {} \; find … -delete (look first!) same on both | |
| Instant search | sudo dnf install mlocate → sudo updatedb → locate NAME | sudo apt install plocate → locate NAME |
| Refresh locate | sudo updatedb same on both | |
| Where is a command? | which CMD whereis CMD type CMD same on both | |
| Shortcut (symlink) | ln -s TARGET LINKNAME readlink LINK same on both | |
| Hard link / inode numbers | ln FILE NEWNAME ls -li same on both | |
| Find broken links | find ~ -xtype l same on both | |
Installing software
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Refresh package list | (automatic) dnf check-update | sudo apt update |
| Install | sudo dnf install NAME | sudo apt install NAME |
| Remove | sudo dnf remove NAME | sudo apt remove NAME |
| Update everything | sudo dnf upgrade | sudo apt update && sudo apt upgrade |
| Search | dnf search WORD | apt search WORD |
| Package details | dnf info NAME | apt show NAME |
| List installed | dnf list installed rpm -qa | apt list --installed dpkg -l |
| Extra repository | sudo dnf install epel-release | (universe is on by default) |
| Which package gives a command? | dnf provides '*/bin/NAME' | apt-file search bin/NAME |
| Which package owns a file? | rpm -qf /usr/bin/ls | dpkg -S /usr/bin/ls |
| Version & repo / dependencies | dnf info NAME dnf deplist NAME | apt-cache policy NAME apt-cache depends NAME |
| Package file type | .rpm | .deb |
| Old command name | yum | apt-get |
Users & sudo
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Run one command as root | sudo COMMAND same on both | |
| Admin group | wheel | sudo |
| Add a user | sudo useradd NAME → sudo passwd NAME | sudo adduser NAME |
| Make a user an admin | sudo usermod -aG wheel NAME | sudo usermod -aG sudo NAME |
| Change your password | passwd same on both | |
| Root shell (then exit!) | sudo -i sudo su - same on both | |
| Switch user | su - NAME (the dash = full login) same on both | |
| Is root locked? | sudo passwd -S root → LK | sudo passwd -S root → L (always) |
| Who is logged in? | who w last same on both | |
vim
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Get it | sudo dnf install vim-enhanced (vi is always there) | vim is pre-installed |
| Modes | i a o A → insert Esc → normal v V → visual : → command line same on both | |
| Save & quit | :w :q :wq (ZZ) :q! (discard) same on both | |
| Move | h j k l w b e 0 ^ $ gg G :42 Ctrl+D / Ctrl+U same on both | |
| Edit | x dd dw cw ciw D yy p P u Ctrl+R . 3dd same on both | |
| Search & replace | /word n N * :%s/old/new/g :g/DEBUG/d :v/ERROR/d same on both | |
| Settings | :set number ~/.vimrc vimtutor same on both | |
| Default editor | echo 'export EDITOR=vim' >> ~/.bashrc sudoedit /etc/FILE same on both | |
Wildcards & braces
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Match | * anything ? one character [abc] [0-9] [!x] one of / not same on both | |
| Make lists | mkdir -p proj/{src,docs} touch f{1..5}.txt cp a.conf{,.bak} same on both | |
| Look first | echo rm *.log → rm *.log same on both | |
| Quote to stop it | find . -name '*.png' echo '*' same on both | |
Jobs
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Stop / pause | Ctrl+C stop Ctrl+Z pause same on both | |
| Background | cmd & bg jobs [-l] same on both | |
| Foreground | fg fg %2 same on both | |
| End a job | kill %1 kill PID same on both | |
| Survive logout | nohup cmd & disown %1 (forever? use a systemd service) same on both | |
Your hardware
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Linux & kernel | cat /etc/os-release uname -r hostnamectl same on both | |
| CPU & memory | lscpu nproc free -h /proc/cpuinfo /proc/meminfo same on both | |
| Disks | lsblk (devices) df -h (space) same on both | |
| Devices | sudo dnf install pciutils usbutils → lspci lsusb | lspci lspci -k lsusb |
| Model & VM? | sudo dmidecode -s system-product-name systemd-detect-virt same on both | |
Archives & compression
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Compress one file | gzip -k F xz -k F bzip2 -k F → gunzip / unxz / bunzip2 same on both | |
| Read without unpacking | zcat xzcat bzcat same on both | |
| tar | tar -czf a.tar.gz DIR tar -cJf a.tar.xz DIR tar -tf a.tar.gz tar -xf a.tar.gz -C DIR same on both | |
| zip | sudo dnf install zip unzip → zip -r a.zip DIR unzip -l a.zip unzip a.zip -d DIR | sudo apt install zip unzip → zip -r a.zip DIR unzip -l a.zip unzip a.zip -d DIR |
| What is it? | file NAME same on both | |
Updates
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| What’s waiting | dnf check-update dnf updateinfo list --security | sudo apt update apt list --upgradable |
| Security only | sudo dnf upgrade --security | sudo unattended-upgrade -v |
| Reboot needed? | dnf needs-restarting -r sudo dnf needs-restarting -s | ls /var/run/reboot-required cat /var/run/reboot-required.pkgs |
| Kernels | rpm -q kernel | dpkg -l 'linux-image*' |
| Automatic | sudo dnf install dnf-automatic /etc/dnf/automatic.conf sudo systemctl enable --now dnf-automatic.timer | /etc/apt/apt.conf.d/20auto-upgrades 50unattended-upgrades |
More ways to install
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Vendor repo | sudo dnf config-manager --add-repo URL.repo → sudo dnf install PKG | curl -fsSL KEY | sudo gpg --dearmor -o /usr/share/keyrings/X.gpg deb [signed-by=…] URL noble main → sudo apt update |
| Snap | EPEL: sudo dnf install snapd sudo systemctl enable --now snapd.socket | snap find X sudo snap install X snap list sudo snap remove X |
| Flatpak | sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install flathub ID flatpak list same on both | |
| AppImage | chmod u+x X.AppImage ./X.AppImage (needs fuse-libs) | chmod u+x X.AppImage ./X.AppImage (needs libfuse2t64) |
SELinux & AppArmor
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Is it on? | getenforce sestatus | sudo aa-status |
| Labels / profiles | ls -Z ps -eZ id -Z | ls /etc/apparmor.d ps auxZ |
| What was blocked | sudo ausearch -m avc -ts recent | sudo journalctl -k | grep DENIED |
| Test briefly | sudo setenforce 0 … sudo setenforce 1 | sudo aa-complain PROG … sudo aa-enforce PROG |
| Fix for real | sudo semanage fcontext -a -t TYPE "/path(/.*)?" sudo restorecon -Rv /path semanage port -a setsebool -P | edit /etc/apparmor.d/PROFILE sudo apparmor_parser -r PROFILE |
Networking
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| My IP addresses | ip a ip -br a (old: ifconfig · Windows: ipconfig) same on both | |
| Gateway / routes | ip route (old: route -n) same on both | |
| Neighbours on the LAN | ip neigh (old: arp -a) same on both | |
| Listening ports | ss -tlnp (old: netstat -tlnp) same on both | |
| Get ifconfig/route/netstat | sudo dnf install net-tools | sudo apt install net-tools |
| Test the path | ping -c 3 GATEWAY ping -c 3 8.8.8.8 ping -c 3 google.com tracepath 8.8.8.8 same on both | |
| DNS servers in use | cat /etc/resolv.conf | resolvectl status (resolv.conf says 127.0.0.53) |
| DNS lookup tools | sudo dnf install bind-utils → dig NAME | dig NAME (pre-installed) |
| Local name overrides | /etc/hosts getent hosts NAME same on both | |
| Permanent config | NetworkManager: nmcli con mod … → nmcli con up … (or nmtui) | Netplan: /etc/netplan/*.yaml → sudo netplan try |
| Config files | /etc/NetworkManager/system-connections/*.nmconnection | /etc/netplan/*.yaml (spaces, not tabs; chmod 600) |
| Temporary changes | sudo ip addr add IP/24 dev CARD sudo ip route add default via GW same on both | |
| Rename the machine | sudo hostnamectl set-hostname NAME same on both | |
Remote access (SSH)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Connect / disconnect | ssh USER@HOST exit same on both | |
| Make a key pair | ssh-keygen -t ed25519 same on both | |
| Copy key to server | ssh-copy-id USER@HOST same on both | |
| Copy key without ssh-copy-id | cat ~/.ssh/id_ed25519.pub | ssh USER@HOST "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" same on both | |
| Key permissions (server) | chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys same on both | |
| Loose key permissions | refused (see /var/log/secure) | group-write allowed if the group is yours |
| Nickname a server | ~/.ssh/config: Host NAME / HostName IP / User USER → ssh NAME same on both | |
| Apps | Windows Terminal · PuTTY (+PuTTYgen, .ppk keys) · MobaXterm · Mac Terminal · iTerm2 · Termius same on both | |
| PuTTY copy / paste | select = copy right-click = paste same on both | |
| Web console | Cockpit: sudo systemctl enable --now cockpit.socket → https://IP:9090 | sudo apt install cockpit → https://IP:9090 |
| Install SSH server | sudo dnf install openssh-server | sudo apt install openssh-server |
| SSH service name | sshd | ssh |
Services
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Status / start / stop / restart | systemctl status NAME sudo systemctl start|stop|restart NAME same on both | |
| Start now + at every boot | sudo systemctl enable --now NAME same on both | |
| Auto-starts after install? | No | Yes |
| Everything in the logs | journalctl -u NAME journalctl -f same on both | |
| PID 1 (the init system) | systemd on both ps -p 1 -o comm same on both | |
| Old-style commands | service NAME restart (“Redirecting to /bin/systemctl…”) | service NAME restart /etc/init.d/NAME restart |
| /etc/init.d | just a README | compatibility scripts (“via systemctl”) |
| Old boot on/off tool | chkconfig NAME on | update-rc.d NAME enable |
| Read / edit a unit | systemctl cat NAME sudo systemctl edit NAME (drop-in) same on both | |
| Unit file folders | /usr/lib/systemd/system (packages) /etc/systemd/system (yours, wins) same on both | |
| After changing a unit file | sudo systemctl daemon-reload same on both | |
| Block / unblock a service | sudo systemctl mask NAME sudo systemctl unmask NAME same on both | |
| Installed units & state | systemctl list-unit-files systemctl --failed same on both | |
| Runlevel → target | runlevel systemctl get-default 3 = multi-user.target 5 = graphical.target same on both | |
| Custom service script location | /usr/local/bin (SELinux blocks /home) | /usr/local/bin |
| Kernel settings (not systemctl!) | sysctl KEY sudo sysctl -w KEY=VAL /etc/sysctl.d/*.conf sudo sysctl --system same on both | |
Web server (Apache)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Package & service | httpd | apache2 |
| Install | sudo dnf install httpd | sudo apt install apache2 |
| Config folder | /etc/httpd/ | /etc/apache2/ |
| Logs | /var/log/httpd/ | /var/log/apache2/ |
| Runs as user | apache | www-data |
| Web pages go in | /var/www/html/ same on both | |
| Test it | curl localhost same on both | |
Firewall
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Tool | firewalld (firewall-cmd) | ufw |
| On by default? | Yes (SSH allowed) | No |
| Show rules | sudo firewall-cmd --list-services | sudo ufw status |
| Allow web traffic | sudo firewall-cmd --permanent --add-service=http → sudo firewall-cmd --reload | sudo ufw allow 'Apache' |
| Allow SSH | (already allowed) | sudo ufw allow OpenSSH ← before enabling! |
| Turn on | sudo systemctl enable --now firewalld | sudo ufw enable |
| Security guard | SELinux | AppArmor |
| Low-level rules | sudo nft list ruleset | sudo iptables -L -n |
| iptables command | sudo dnf install iptables-nft | (pre-installed) |
Network & security tools
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Fetch a page / headers | curl URL curl -I URL curl -O URL same on both | |
| Download a file | sudo dnf install wget → wget URL | wget URL |
| Is a port open? | sudo dnf install nmap-ncat → nc -zv HOST PORT | nc -zv HOST PORT |
| What is listening? | ss -tlnp sudo lsof -i :PORT same on both | |
| Watch packets | sudo dnf install tcpdump → sudo tcpdump -i enp0s3 -c 5 icmp | sudo tcpdump -i enp0s3 -c 5 icmp |
| Random secret | openssl rand -base64 24 same on both | |
| Self-signed certificate | openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 30 -subj "/CN=NAME" same on both | |
| Check a certificate | openssl x509 -in cert.pem -noout -dates openssl s_client -connect HOST:443 same on both | |
| Checksum | sha256sum FILE sha256sum -c SHA256SUMS same on both | |
| My public IP | curl ifconfig.me same on both | |
Logs
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Read the journal | journalctl -u NAME -n 50 journalctl -p err journalctl -xeu NAME same on both | |
| Who can read log files | root (use sudo) | root + adm group |
| System messages | /var/log/messages | /var/log/syslog |
| Logins & sudo | /var/log/secure | /var/log/auth.log |
| Package history | /var/log/dnf.log | /var/log/apt/history.log |
| Watch a log live | sudo tail -f FILE (Ctrl+C to stop) same on both | |
| Kernel messages | dmesg -T journalctl -k | sudo dmesg -T journalctl -k |
Text power tools
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Find lines | grep -i -n -c -v -r -E "a|b" same on both | |
| Count | wc -l FILE same on both | |
| Sort / unique / count | sort sort -n sort -rn sort | uniq -c same on both | |
| Columns | cut -d, -f1,3 FILE awk -F, '{print $1}' FILE same on both | |
| Filter by column | awk -F, '$3 > 85 {print $1}' FILE same on both | |
| awk flavor | gawk (awk --version) | mawk (awk -W version) |
| Find & replace | sed 's/old/new/g' FILE (then -i to save) same on both | |
| Swap characters | tr a-z A-Z tr , '\n' tr -d CHARS same on both | |
| Show and save | COMMAND | tee FILE same on both | |
| List → arguments | find … | xargs COMMAND same on both | |
| Compare files | diff OLD NEW same on both | |
Permissions
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Read permissions | ls -l stat FILE same on both | |
| Make a script runnable | chmod +x script.sh same on both | |
| Private file / normal file / folder | chmod 600 FILE chmod 644 FILE chmod 755 DIR same on both | |
| Change owner / group | sudo chown USER:GROUP FILE sudo chgrp GROUP FILE same on both | |
| Default permissions | umask same on both | |
| Home folder default | 700 (drwx------) | 750 (drwxr-x---) |
| /etc/shadow | 000, root only | 640, root:shadow |
| Extra security layer | SELinux (ls -Z) | AppArmor |
Users & groups
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Create a user | sudo useradd NAME → sudo passwd NAME | sudo adduser NAME |
| useradd makes a home folder? | Yes | Only with -m |
| Add to a group (keep others!) | sudo usermod -aG GROUP USER sudo gpasswd -a USER GROUP same on both | |
| New group | sudo groupadd NAME same on both | |
| Lock / delete user | sudo usermod -L USER sudo userdel -r USER same on both | |
| Look up | id USER getent passwd USER who last same on both | |
Cron (scheduling)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Package / service | cronie / crond | cron / cron |
| Edit / list / remove your jobs | crontab -e crontab -l crontab -r (no undo!) same on both | |
| Time fields | minute hour day-of-month month day-of-week command same on both | |
| Every 5 min / 2:30am weekdays | */5 * * * * 30 2 * * 1-5 same on both | |
| System jobs (with a user field) | /etc/crontab /etc/cron.d/NAME same on both | |
| Drop-in folders | /etc/cron.hourly cron.daily cron.weekly cron.monthly same on both | |
| Default crontab editor | vi (EDITOR=nano crontab -e) | asks: 1 = nano |
| cron.d file names with a dot | work | IGNORED |
| Cron log | /var/log/cron | grep CRON /var/log/syslog |
| In a crontab, write % as | \% (or use a script) same on both | |
Processes & disk
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| List processes | ps aux pgrep -l NAME top htop same on both | |
| htop | EPEL: sudo dnf install htop | pre-installed |
| Stop a process | kill PID kill -9 PID pkill NAME same on both | |
| Load / cores / memory | uptime nproc free -h same on both | |
| Disk full? | df -h sudo du -h -d 1 / | sort -h sudo find / -size +1G same on both | |
| Filesystem type | xfs | ext4 |
| Empty a big log safely | sudo truncate -s 0 FILE same on both | |
| Deleted but still full? | sudo lsof +L1 → restart / stop that program same on both | |
| Stop by exact name | killall NAME same on both | |
Disks & mounting
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| See disks | lsblk sudo fdisk -l df -hT same on both | |
| Partition a disk | sudo fdisk /dev/sdb (n … w) same on both | |
| Format | sudo mkfs.xfs /dev/sdb1 | sudo mkfs.ext4 /dev/sdb1 |
| Mount / unmount | sudo mount /dev/sdb1 /data sudo umount /data same on both | |
| “target is busy” | cd ~ lsof /data same on both | |
| Disk ID for fstab | sudo blkid /dev/sdb1 same on both | |
| Mount at boot | UUID=… /data TYPE defaults 0 0 in /etc/fstab same on both | |
| Test fstab (always!) | sudo systemctl daemon-reload sudo mount -a findmnt /data same on both | |
Backups
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Create / list / extract archive | tar -czf A.tar.gz DIR tar -tzf A.tar.gz tar -xzf A.tar.gz -C DEST same on both | |
| Date in a file name | backup-$(date +%F).tar.gz same on both | |
| Mirror a folder | rsync -av SRC/ DEST/ (dry run: -n) same on both | |
| Copy to a server | rsync -av DIR user@host:path/ scp FILE user@host: same on both | |
| Install rsync | sudo dnf install rsync (if missing) | (pre-installed) |
| Compress one file | gzip FILE gunzip FILE.gz zcat FILE.gz same on both | |
| Zip files | sudo dnf install zip unzip | sudo apt install zip unzip |
| Make / list / extract zip | zip -r A.zip DIR unzip -l A.zip unzip A.zip -d DEST same on both | |
| Verify backups | sha256sum *.tar.gz > SHA256SUMS sha256sum -c SHA256SUMS same on both | |
Your company’s app
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Program / settings / data | /opt/cht/ourapp /etc/opt/cht/ourapp /var/opt/cht/ourapp same on both | |
| Service account | sudo useradd --system --shell /sbin/nologin ourapp | sudo adduser --system --group ourapp |
| Owners | program root:root 755 config root:ourapp 640 data ourapp:ourapp 750 same on both | |
| Switch versions | sudo ln -sfn /opt/cht/ourapp/releases/ourapp-X /opt/cht/ourapp/current → sudo systemctl restart ourapp same on both | |
| Test as the app user | sudo -u ourapp /opt/cht/ourapp/current/bin/ourapp --check-config | sudo -u ourapp ourapp --check-config |
| SELinux labels | ls -Z sudo ausearch -m avc -ts recent sudo restorecon -Rv /opt/cht | (AppArmor: no file labels, ls -Z shows ?) |
| Open the port | sudo firewall-cmd --permanent --add-port=8080/tcp → --reload | sudo ufw allow 8080/tcp |
Shell skills
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Repeat last command (as root) | !! sudo !! same on both | |
| Search history | history | grep WORD Ctrl+R same on both | |
| Variables | NAME=value echo "$NAME" export NAME env same on both | |
| Shortcuts | alias NAME='COMMAND' (keep it: add to ~/.bashrc) same on both | |
| Rerun every 2 s | watch -n 2 COMMAND same on both | |
| Session that survives disconnects | sudo dnf install tmux → tmux new -s NAME | tmux new -s NAME |
| Detach / reattach | Ctrl+B then D tmux ls tmux attach -t NAME same on both | |
| screen | EPEL only: sudo dnf install screen | screen -S NAME Ctrl+A then D screen -r |
| Time zone | timedatectl sudo timedatectl set-timezone America/Chicago same on both | |
| Clock sync service | chronyd | systemd-timesyncd |
| Rename the server | sudo hostnamectl set-hostname NAME | sudo hostnamectl set-hostname NAME (+ fix 127.0.1.1 in /etc/hosts) |
Capturing output
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Save results / add to a file | cmd > file cmd >> file same on both | |
| Errors only / both / silence | cmd 2> err.txt cmd > all.txt 2>&1 cmd >/dev/null 2>&1 same on both | |
| Order matters | > file 2>&1 ✔ 2>&1 > file ✘ (errors still on screen) same on both | |
| See it and save it | cmd | tee file cmd 2>&1 | tee -a log same on both | |
| Write a whole file | cat > file <<EOF … EOF <<'EOF' = no $ expansion same on both | |
| Error message from a script | echo "oops" >&2 same on both | |
| Record a session | script session.log … exit same on both | |
| To the system log | logger -t TAG "msg" → /var/log/messages | logger -t TAG "msg" → /var/log/syslog |
| Log a whole script | exec >> /path/script.log 2>&1 (near the top) same on both | |
Substitution & subshells
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Insert a command’s output | echo "Today: $(date +%A)" old style: `date +%A` same on both | |
| Save output in a variable | n=$(ls | wc -l) x=$(cmd 2>&1) (include errors) same on both | |
| Keep lines & spaces | "$(cmd)" (quote it!) same on both | |
| Math | $(( 6 * 7 )) $(( n + 1 )) $(( 7 % 2 )) same on both | |
| Variable tricks | ${f##*/} ${f%/*} ${f%.txt} ${#f} ${X:-default} same on both | |
| Do it “over there” | (cd /var/log && ls) (subshell: you don’t move) same on both | |
| Group outputs | { date; uptime; } > report.txt same on both | |
| Output as a file | diff <(ls dir1) <(ls dir2) same on both | |
| /bin/sh is… | bash (everything works) | dash (no <( ), [[ ]], ${x//a/b}) |
| Force bash | #!/bin/bash bash -c '…' SHELL=/bin/bash in crontab same on both | |
Shell scripting
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| First line | #!/bin/bash (then chmod +x script.sh) same on both | |
| Arguments | $1 $2 … $# "$@" shift ${1:-default} same on both | |
| Tests | [ -f f ] [ -d d ] [ -z "$s" ] [ "$a" = "$b" ] [ "$n" -gt 5 ] same on both | |
| Decide | if …; then …; elif …; else …; fi cmd && ok || fail same on both | |
| Choose | case "$x" in a|b) … ;; *) … ;; esac same on both | |
| Loops | for x in a b c; do …; done while [ … ]; do …; done same on both | |
| File line by line | while IFS=: read -r a b rest; do …; done < file same on both | |
| Functions | name() { local x="$1"; …; return 0; } same on both | |
| Debug / strict | bash -x script.sh set -euo pipefail same on both | |
| Both families in one script | . /etc/os-release; case "$ID" in rocky|rhel) dnf … ;; ubuntu|debian) apt … ;; esac same on both | |
POSIX & portability
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| /bin/sh is… | bash (lenient) | dash (strict) |
| Instead of [[ ]] / == / source | [ ] · = · . same on both | |
| Instead of echo -e / -n | printf 'text\n' same on both | |
| Instead of {1..5} | for i in 1 2 3 4 5 $(seq 5) same on both | |
| Instead of &> | > file 2>&1 same on both | |
| Check a script | sudo dnf install ShellCheck (EPEL) → shellcheck s.sh | sudo apt install shellcheck → shellcheck s.sh checkbashisms s.sh |
| POSIX version / limits | getconf _POSIX_VERSION getconf ARG_MAX same on both | |
Distro cousins
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Rocky’s twin | AlmaLinux: same commands, ID="almalinux" same on both | |
| Amazon Linux 2023 | Red Hat family (from Fedora): dnf, ec2-user, no EPEL, no firewalld, no cron by default same on both | |
| Which family? | grep -E '^(ID|ID_LIKE)=' /etc/os-release (check ID_LIKE) same on both | |
| Container images | rockylinux/rockylinux:9 · almalinux:9 · amazonlinux:2023 · ubuntu:24.04 · debian:13 · alpine same on both | |
| Alpine | apk add PKG no bash (BusyBox sh) same on both | |
Alpine & containers
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Get podman | sudo dnf install podman | sudo apt install podman (or docker.io, with sudo) |
| Run Alpine | podman run -it docker.io/library/alpine:3.22 (exit to leave) same on both | |
| One command, then clean up | podman run --rm IMAGE COMMAND same on both | |
| Containers / images | podman ps -a podman images podman rm NAME same on both | |
| Alpine packages | apk update apk add PKG apk del PKG apk search WORD apk add --no-cache PKG same on both | |
| Alpine basics | shell = ash (apk add bash) tools = BusyBox libc = musl services = OpenRC same on both | |
| OpenRC (real Alpine) | rc-service NAME start rc-update add NAME default rc-status same on both | |
Build & ship images (DevOps)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Build | podman build -t NAME:TAG . (the dot = build context) sudo docker build -t NAME . same on both | |
| Containerfile | FROM image:tag RUN cmd COPY src dest WORKDIR dir ENV K=V EXPOSE port CMD ["prog", "arg"] same on both | |
| Run with a port | podman run -d --name web -p HOST:CONTAINER IMAGE (rootless: HOST ≥ 1024) same on both | |
| Look after it | podman ps -a podman logs NAME podman exec -it NAME sh podman port NAME same on both | |
| Replace it | podman stop NAME podman rm NAME run the new tag podman rmi IMAGE same on both | |
| Open the port | sudo firewall-cmd --add-port=8080/tcp --permanent ; sudo firewall-cmd --reload | sudo ufw allow 8080/tcp |
| Share it | podman login REGISTRY podman tag IMG REGISTRY/you/IMG:TAG podman push REGISTRY/you/IMG:TAG same on both | |
Reliability numbers (SRE)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| The words | SLI = what you measure SLO = your target SLA = a promise with a penalty KPI = a business number same on both | |
| Nines per 30 days | 99% = 7h12m 99.5% = 3h36m 99.9% = 43m 99.99% = 4m19s 99.999% = 26s same on both | |
| Availability from a log | awk '$9 < 500 {g++} END {printf "%.2f%%\n", 100*g/NR}' access.log same on both | |
| Percentiles | awk '{print $NF}' access.log | sort -n | awk '{t[NR]=$1} END {print t[int(NR*0.95)]}' same on both | |
| Error budget | budget = 1 − SLO burn rate 1 = spends it in exactly 30 days same on both | |
Monitoring (Prometheus)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Ports | node_exporter :9100/metrics Prometheus :9090 Alertmanager :9093 same on both | |
| Check before (re)loading | promtool check config /etc/prometheus/prometheus.yml promtool check rules FILE sudo systemctl reload prometheus same on both | |
| Query | curl -s 'localhost:9090/api/v1/query?query=up' | jq promtool query instant http://localhost:9090 'EXPR' same on both | |
| PromQL | up rate(node_cpu_seconds_total{mode="idle"}[5m]) avg by (instance) (…) predict_linear(x[6h], 86400) same on both | |
| Alerts | curl -s localhost:9090/api/v1/alerts | jq pending → firing after for: same on both | |
| See the UI safely | ssh -L 9090:localhost:9090 you@server → http://localhost:9090 same on both | |
Incidents
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Order | acknowledge → confirm impact → mitigate (roll back!) → communicate → resolve → postmortem same on both | |
| What changed? | sudo journalctl -t sudo --since "1 hour ago" ls -lt /etc/httpd/conf.d | sudo journalctl -t sudo --since "1 hour ago" ls -lt /etc/apache2/sites-available |
| Config test | sudo apachectl configtest | sudo apache2ctl configtest |
| Update format | time (UTC) · what users see · what we know · what we’re doing · next update at same on both | |
Limits & OOM
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| OOM evidence | dmesg | grep -i -E "oom|killed process" | sudo dmesg | grep -i -E "oom|killed process" |
| Per-service usage | systemd-cgtop -m systemctl status NAME (Memory:) systemctl show NAME -p MemoryCurrent same on both | |
| Set limits now + saved | sudo systemctl set-property NAME MemoryMax=300M CPUQuota=50% same on both | |
| Other settings | sudo systemctl edit NAME → [Service] OOMScoreAdjust=-500 / LimitNOFILE=65536 same on both | |
| cgroup files | cat /sys/fs/cgroup/system.slice/NAME.service/memory.max same on both | |
Capacity & load testing
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Get ab | sudo dnf install httpd-tools | sudo apt install apache2-utils |
| Load test (your own server only!) | ab -n 2000 -c 50 http://localhost/ (note the trailing /) | grep -E 'Requests per second|Failed| 95%' same on both | |
| Little’s Law | in flight = throughput × latency same on both | |
| Growth | awk 'BEGIN { print log(target/now) / log(1 + rate) }' → months same on both | |
Performance triage (SRE)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Get the tools | sudo dnf install sysstat | sudo apt install sysstat |
| The checklist | uptime dmesg | tail vmstat 1 5 mpstat -P ALL 1 3 pidstat 1 3 iostat -xz 1 3 free -m sar -n DEV 1 3 sar -n TCP,ETCP 1 3 top same on both | |
| Kernel messages | dmesg | tail | sudo dmesg | tail |
| Disk hogs | pidstat -d 1 3 iostat -xz 1 3 (w_await, aqu-sz, %util) same on both | |
| Signs of trouble | load > CPUs vmstat b > 0 or wa high si/so ≠ 0 %util ≈ 100 state D in top same on both | |
| Which service owns a PID? | systemctl status PID ps -o pid,ppid,stat,cmd -p PID same on both | |
| Timers | systemctl list-timers systemctl cat NAME.timer OnCalendar=*-*-* 02:00:00 sudo systemctl daemon-reload same on both | |
Compose
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Get it | sudo dnf install epel-release ; sudo dnf install podman-compose | sudo apt install podman-compose (or docker-compose-v2 → docker compose) |
| Run | podman-compose up -d ps logs [SERVICE] exec SERVICE sh restart SERVICE same on both | |
| Change | up -d (recreates what changed) up -d --build (new code) same on both | |
| Stop | down (keeps volumes) down -v (deletes volumes = data!) same on both | |
| Networking | containers reach each other by SERVICE NAME localhost = the container itself same on both | |
CI (GitHub Actions)
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Where | .github/workflows/NAME.yml → on: [push] jobs: runs-on: steps: uses / run same on both | |
| Pass or fail | exit code 0 = pass anything else = fail same on both | |
| From the terminal | gh run list gh run view gh run view ID --log-failed same on both | |
| Lint scripts | sudo dnf install ShellCheck (EPEL) ; shellcheck *.sh | sudo apt install shellcheck ; shellcheck *.sh |
Ansible
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Install | sudo dnf install epel-release ; sudo dnf install ansible | sudo apt install ansible |
| Inventory | [web] web1 ansible_host=192.168.1.61 ansible-inventory --graph same on both | |
| Ad-hoc | ansible all -m ping ansible web -a 'uptime' ansible web -b -m package -a 'name=tree' same on both | |
| Playbooks | ansible-playbook site.yml --check --diff ansible-playbook site.yml -l web1 same on both | |
| Both families | when: ansible_facts['os_family'] == "RedHat" / "Debian" or [rocky]/[ubuntu] groups + group_vars same on both | |
| Variables | group_vars/GROUP.yml host_vars/HOST.yml ansible-inventory --host HOST -e var=value (wins) same on both | |
| Loops & handlers | loop: "{{ list }}" + {{ item }} notify: Name → handlers: - name: Name same on both | |
| Roles | ansible-galaxy role init --init-path roles NAME roles: [NAME] defaults/ (low) vs vars/ (high) same on both | |
| Vault | ansible-vault encrypt|view|edit|decrypt FILE --ask-vault-pass vault_password_file = ~/.vault_pass same on both | |
| Rolling updates | serial: 1 max_fail_percentage: 0 uri + assert health check -l web1 -i inventories/production.ini same on both | |
| Checks | ansible-playbook X --syntax-check ansible-lint run twice → changed=0 --list-tasks --tags TAG same on both | |
Secrets & config
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| .env | echo "DB_PASSWORD=$(openssl rand -hex 16)" > .env ; chmod 600 .env ; echo .env >> .gitignore same on both | |
| Compose | REDIS_PASSWORD: ${DB_PASSWORD:?set it in .env} same on both | |
| Services | /etc/sysconfig/NAME (600) + EnvironmentFile=/etc/sysconfig/NAME | /etc/default/NAME (600) + EnvironmentFile=/etc/default/NAME |
| Find leaks | git log -p | grep -i pass gitleaks trufflehog same on both | |
| After a leak | ROTATE: new secret everywhere, old one must stop working same on both | |
Deploys
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Proxy switch | edit ProxyPass port ; sudo apachectl configtest ; sudo systemctl reload httpd | edit ProxyPass port ; sudo apache2ctl configtest ; sudo systemctl reload apache2 |
| Smoke test green first | curl localhost:8082/health curl localhost:8082/api/… same on both | |
| Strategies | recreate · rolling · blue-green (instant rollback) · canary (small slice first) same on both | |
| Rocky + proxy | sudo setsebool -P httpd_can_network_connect 1 same on both | |
AWS: basics
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Install CLI v2 | sudo dnf install unzip ; curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip ; unzip awscliv2.zip ; sudo ./aws/install | sudo apt install unzip ; curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip ; unzip awscliv2.zip ; sudo ./aws/install |
| Sign in / who am I | aws configure aws configure --profile NAME aws sts get-caller-identity same on both | |
| Pick fields | --query 'Items[].Field' --output text|table|json X=$(aws … --query Id --output text) same on both | |
| Region | --region us-east-1 export AWS_REGION=… aws configure get region same on both | |
| IAM | aws iam create-user / create-group / add-user-to-group / attach-group-policy --policy-arn … create-access-key same on both | |
| Test a policy | aws iam simulate-principal-policy --policy-source-arn USER-ARN --action-names s3:PutObject same on both | |
AWS: network & servers
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| VPC | aws ec2 create-vpc --cidr-block 10.0.0.0/16 create-subnet --vpc-id … --cidr-block 10.0.1.0/24 --availability-zone us-east-1a same on both | |
| Public subnet | create-internet-gateway + attach-internet-gateway create-route-table + create-route 0.0.0.0/0 → igw + associate-route-table same on both | |
| Security group | create-security-group authorize-security-group-ingress --protocol tcp --port 22 --cidr MY-IP/32 --source-group SG same on both | |
| Latest AMI | aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 | aws ssm get-parameter --name /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id |
| Launch | aws ec2 run-instances --image-id $AMI --instance-type t3.micro --key-name K --subnet-id … --security-group-ids … --user-data file://web.sh same on both | |
| Wait & find | aws ec2 wait instance-running --instance-ids $ID describe-instances --query 'Reservations[].Instances[].PublicIpAddress' same on both | |
| Log in | ssh -i key.pem ec2-user@IP (Amazon Linux) | ssh -i key.pem ubuntu@IP |
| First-boot log | tail /var/log/cloud-init-output.log same on both | |
| Lifecycle | stop-instances (new IP on start) · start-instances · terminate-instances (gone) same on both | |
AWS: S3
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Buckets & files | aws s3 mb s3://UNIQUE-NAME aws s3 cp FILE s3://B/ aws s3 ls s3://B --recursive aws s3 sync DIR s3://B same on both | |
| Delete | aws s3 rm s3://B/KEY aws s3 rb s3://B --force same on both | |
| Website | aws s3 website s3://B --index-document index.html --error-document error.html → http://B.s3-website-REGION.amazonaws.com same on both | |
| Go public (careful!) | aws s3api put-public-access-block … BlockPublicPolicy=false,RestrictPublicBuckets=false put-bucket-policy --policy file://p.json same on both | |
| Share one file | aws s3 presign s3://B/KEY --expires-in 300 same on both | |
| Versions | aws s3api put-bucket-versioning --bucket B --versioning-configuration Status=Enabled list-object-versions same on both | |
AWS: scale, DNS, alarms
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Load balancer | elbv2 create-target-group · register-targets · create-load-balancer --subnets A B · create-listener --default-actions Type=forward,TargetGroupArn=… same on both | |
| Why unhealthy? | aws elbv2 describe-target-health --target-group-arn $TG (Timeout = security group) same on both | |
| Auto Scaling | create-launch-template (UserData = base64 -w0) · autoscaling create-auto-scaling-group --min-size --max-size --desired-capacity same on both | |
| Scale on CPU | put-scaling-policy --policy-type TargetTrackingScaling … 'TargetValue':50 describe-scaling-activities same on both | |
| DNS records | aws route53 change-resource-record-sets --hosted-zone-id Z --change-batch file://c.json (CREATE · UPSERT · DELETE) same on both | |
| Check DNS | dig +short NAME dig NS DOMAIN dig @ns-X.awsdns-Y.com NAME same on both | |
| Alarm → email | sns create-topic · sns subscribe --protocol email · cloudwatch put-metric-alarm … --alarm-actions TOPIC-ARN same on both | |
| Guard the bill | billing alarm in us-east-1 (AWS/Billing EstimatedCharges) aws budgets create-budget same on both | |
AWS: costs & clean-up
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| This month | aws ce get-cost-and-usage --time-period Start=…,End=… --granularity MONTHLY --metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE same on both | |
| Find by tag | aws resourcegroupstaggingapi get-resources --tag-filters Key=Project,Values=NAME same on both | |
| Every region | for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do aws ec2 describe-instances --region $r …; done same on both | |
| Usual leftovers | describe-addresses 'Addresses[?AssociationId==null]' · describe-volumes status=available · load balancers · hosted zones · buckets same on both | |
| Delete order | ASGs → instances → load balancers → target groups → IPs/volumes → buckets → DNS → network same on both | |
Git
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Install | sudo dnf install git | (preinstalled) sudo apt install git |
| Who am I | git config --global user.name "Name" user.email you@example.com same on both | |
| Start / copy a repo | git init git clone URL same on both | |
| Save a snapshot | git status git add FILE git commit -m "message" git commit -am "…" same on both | |
| Look back | git log --oneline git diff git diff --staged git show HASH same on both | |
| Undo | git restore FILE git restore --staged FILE git restore --source=HASH FILE same on both | |
| Branches | git branch git switch -c NAME git switch main git merge NAME same on both | |
| Conflict | edit the file (remove <<< === >>>) → git add FILE → git commit same on both | |
| Share | git remote add origin URL git push -u origin main git pull same on both | |
| Never commit | passwords & keys → list them in .gitignore same on both | |
Python & AI
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Python tools | sudo dnf install python3-pip | sudo apt install python3-venv python3-pip |
| Make & use a venv | python3 -m venv ~/ai-env source ~/ai-env/bin/activate deactivate same on both | |
| Install Ollama | curl -fsSL https://ollama.com/install.sh | sh same on both | |
| Chat with a model | ollama run llama3.2 (/bye to quit) same on both | |
| Models: list / remove | ollama list ollama rm NAME same on both | |
Help!
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Manual page | man COMMAND (q to quit) same on both | |
| Quick help | COMMAND --help same on both | |
| Which command does…? | man -k WORD apropos WORD same on both | |
| Cancel / clear screen | Ctrl+C clear (or Ctrl+L) same on both | |
| Previous commands | ↑ history same on both | |
| Auto-complete | Tab same on both | |
Attack surface
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| What's listening (0.0.0.0 = everyone) | sudo ss -tlnp same on both | |
| Which services run | systemctl list-units --type=service --state=running same on both | |
| Accounts that can log in | getent passwd | awk -F: '$7 !~ /(nologin|false)$/ {print $1}' same on both | |
| Who is an admin | getent group wheel | getent group sudo |
| Turn a service off for good | sudo systemctl disable --now NAME same on both | |
| Lock an account (keep it for now) | sudo usermod -L NAME sudo usermod -s /sbin/nologin NAME sudo gpasswd -d NAME wheel | sudo usermod -L NAME sudo usermod -s /sbin/nologin NAME sudo gpasswd -d NAME sudo |
SSH hardening
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| Where the SSH server settings live | /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf (read first; first value wins) same on both | |
| Give a key a passphrase (new or existing) | ssh-keygen -t ed25519 ssh-keygen -p -f ~/.ssh/id_ed25519 same on both | |
| Unlock the key once per session | ssh-add ssh-add -l same on both | |
| Keys only, no root login | printf 'PasswordAuthentication no\nPermitRootLogin no\n' | sudo tee /etc/ssh/sshd_config.d/00-hardening.conf same on both | |
| Test the config (silence = OK) | sudo sshd -t same on both | |
| What sshd really uses | sudo sshd -T | grep -iE 'passwordauth|permitroot' same on both | |
| Apply it | sudo systemctl reload sshd | sudo systemctl reload ssh |
| Prove passwords are off (from the laptop) | ssh -o PubkeyAuthentication=no user@server (must be refused) same on both | |
| Only these users may log in | AllowUsers student maria (in the drop-in) same on both | |
| The drop-in that overrides you | 01-permitrootlogin.conf (PermitRootLogin yes) | 50-cloud-init.conf (PasswordAuthentication yes) |
Firewalls in depth
| Task | Rocky / RHEL | Ubuntu / Debian |
|---|---|---|
| See the rules | sudo firewall-cmd --get-active-zones sudo firewall-cmd --list-all | sudo ufw status verbose sudo ufw status numbered |
| Refuse everything by default | (the public zone already does) | sudo ufw default deny incoming |
| SSH only from your network | sudo firewall-cmd --permanent --zone=internal --add-source=192.168.1.0/24 sudo firewall-cmd --permanent --zone=public --remove-service=ssh sudo firewall-cmd --reload | sudo ufw allow from 192.168.1.0/24 to any port 22 |
| Block one address | sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.45" drop' | sudo ufw insert 1 deny from 203.0.113.45 |
| Remove a rule | --remove-service=NAME --remove-rich-rule='…' | sudo ufw delete NUMBER |
| Save what's running now | sudo firewall-cmd --runtime-to-permanent | (ufw saves every change) |
| Rate-limit SSH | rich rule: rule service name="ssh" limit value="3/m" accept | sudo ufw limit OpenSSH |
| Log what's blocked | sudo firewall-cmd --set-log-denied=all sudo journalctl -k | grep REJECT | sudo ufw logging on sudo journalctl -k | grep 'UFW BLOCK' |