Lecture 2
01 / 00

Page ↑ / Page ↓ to move · scroll works too

Linux & DevOps — Lecture 2 of 8

Hands on the Keyboard

The mental model is in place.
Today it’s the shell and SSH —
the two tools you’ll touch in every lecture after this one.

All slides available at
slides.raafat.io

Everything else sits on top of these two

Every later lecture assumes you’re already fluent here: writing a systemd unit, running dnf, editing a config file, deploying a container — all of it happens inside a shell, usually once you’ve reached over SSH. This lecture is the floor everything else stands on.

bash ssh grep sed awk cron
01

Shell Fundamentals

  • The shell as a program, not a metaphor
  • Navigating the filesystem
  • Wildcards and expansion
  • Variables, quoting, and environment
  • Startup files and aliases

A shell is just another program

It reads text you type, parses it into a command plus arguments, and asks the kernel to run it. Nothing more mystical than that — it happens to be the program that starts every other program you run.

bash sh zsh
bash — the Bourne Again Shell — is the default login shell here, and the one this whole lecture is written for.

Absolute vs. relative, and reading ls -la

An absolute path starts with / and means the same thing regardless of where you’re standing; a relative path is resolved against your current directory.

~
$ ls -la /etc/ssh
drwxr-xr-x. 2 root root 4096 Sep 2 09:14 .
drwxr-xr-x. 88 root root 12288 Sep 2 09:14 ..
-rw-r--r--. 1 root root 577 Aug 14 03:22 ssh_config
-rw-------. 1 root root 3487 Aug 14 03:22 sshd_config

Wildcards: the shell expands, before the command sees it

~
$ ls *.log
$ ls app-?.log
$ ls backup-{jan,feb,mar}.tar.gz
$ ls /etc/[a-h]*.conf

Critical distinction: the shell expands wildcards and brace lists into a list of matching filenames before the command ever runs — ls never sees the wildcard itself, it sees a list of real filenames.

Variables, and why quoting matters

~
$ name="prod-web-01"
$ echo $name
prod-web-01
$ echo "Deploying to $name"
Deploying to prod-web-01
$ echo 'Deploying to $name'
Deploying to $name

Double quotes still expand variables; single quotes take everything literally. A bare, unquoted variable also gets word-split and glob-expanded — the reason experienced sysadmins quote almost everything.

Shell variable

  • Exists only in this shell
  • Not visible to child processes
  • name="prod-web-01"

Environment variable

  • Inherited by every child process
  • Promoted with export
  • export PATH="$PATH:/opt/app/bin"
env or printenv lists the environment a process would inherit; PATH — the list of directories searched for commands — is the one you'll edit most.

Where bash reads its own configuration

  • /etc/profile and /etc/profile.d/*.sh — system-wide, read once at login
  • ~/.bash_profile — per-user, read once at login
  • ~/.bashrc — per-user, read for every new interactive shell
  • alias and function definitions belong in ~/.bashrc, not the login files
~/.bashrc
alias ll='ls -la'
alias grep='grep --color=auto'
export EDITOR=vim
02

Redirection, Pipelines & Text Tools

  • Three standard streams
  • Redirecting them
  • Pipes: chaining small tools
  • grep, sed, awk
  • cut, sort, uniq, find, xargs

Every process gets three open streams

stdin fd 0 — input
stdout fd 1 — normal output
stderr fd 2 — errors

stdout and stderr are separate streams for a reason — a script can log errors while still piping clean data downstream.

Sending streams somewhere else

~
$ dnf list installed > packages.txt
$ echo "deploy started" >> deploy.log
$ ping -c1 db01 2> errors.log
$ ./build.sh > build.log 2>&1
$ ./build.sh &> build.log
$ grep -c error < build.log
> overwrites, >> appends. 2>&1 means "point stream 2 at wherever stream 1 currently points" — order matters, so it goes after the stdout redirect.

Pipes chain small tools into one job

~
$ ps aux | grep nginx
$ journalctl -u sshd | tail -n 50
$ cat access.log | grep 500 | wc -l

Each command’s stdout becomes the next one’s stdin — the Unix philosophy in one keystroke: small tools, each doing one thing, composed on demand.

grep — find lines that match

~
$ grep "Failed password" /var/log/secure
$ grep -i error app.log
$ grep -v "^#" /etc/ssh/sshd_config
$ grep -rn "TODO" src/
$ grep -E "^(GET|POST) " access.log
  • -i ignore case   -v invert match   -n show line numbers
  • -r recurse into directories   -c count matches
  • -E extended regular expressions

sed & awk: edit and extract, one line at a time

sed — stream editor

~
$ sed 's/staging/production/' app.conf
$ sed -i 's/foo/bar/g' file.txt
$ sed -n '10,20p' access.log

awk — pattern + action

~
$ awk '{print $1}' access.log
$ awk -F: '{print $1}' /etc/passwd
$ awk '$9 == 500 {print $7}' access.log
sed -i edits the file in place — always safe to try a command without -i first, sanity-check the output, then add it back. awk splits each line into fields ($1, $2, …) on whitespace, or on -F.

cut, sort, uniq, wc, tr — the small, composable ones

~
$ cut -d: -f1 /etc/passwd
$ sort access.log | uniq -c | sort -rn | head
$ wc -l /etc/passwd
$ cat names.txt | tr 'a-z' 'A-Z'

That second line is a classic: sort so identical lines are adjacent, count consecutive duplicates, sort the counts descending, keep the top few — a five-second “what’s most common” report.

find & xargs — locate things, then act on them

~
$ find /var/log -name "*.log" -mtime +30
$ find /tmp -type f -size +100M
$ find . -name "*.tmp" -delete
$ find /var/log -name "*.log" -mtime +30 | xargs rm
$ find . -name "*.txt" | xargs grep -l "error"
xargs takes lines on stdin and turns them into arguments for another command — how find's results get handed off to rm, grep, or anything else.
03

Bash Scripting for Automation

  • Anatomy of a script
  • Arguments, exit codes, conditionals
  • Loops, functions, arrays
  • Scheduling with cron
  • Writing scripts that are safe to re-run

From one-liner to reusable script

backup.sh
#!/usr/bin/env bash
set -euo pipefail
tar -czf "/backups/etc-$(date +%F).tar.gz" /etc
echo "Backup complete"
~
$ chmod +x backup.sh
$ ./backup.sh
$ echo $?
0

The shebang picks the interpreter; chmod +x makes it executable. set -euo pipefail is close to a standard header: exit on error, exit on an unset variable, and fail the whole pipeline if any stage of it fails.

Arguments in, an exit code out

deploy.sh
#!/usr/bin/env bash
echo "Script name: $0"
echo "First arg: $1"
echo "All args: $@"
echo "Arg count: $#"
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <environment>" >&2
exit 1
fi

Every command leaves an exit code behind in $? — 0 means success, anything else means failure. Scripts are expected to honor the same convention.

Conditionals: if, test, and [[ ]]

~
if [[ -f /etc/app.conf ]]; then
echo "config exists"
elif [[ -d /etc/app ]]; then
echo "directory exists instead"
else
echo "nothing found"
fi
if [[ "$env" == "prod" && -n "$token" ]]; then
deploy
fi
  • -f regular file   -d directory   -z/-n empty/non-empty string
  • -eq -ne -gt -lt -ge -le for integers   == != for strings
  • Prefer [[ ]] over [ ] — fewer quoting surprises

Loops: for, while, until

~
for host in web01 web02 web03; do
ssh "$host" "systemctl is-active nginx"
done
while read -r line; do
echo "processing: $line"
done < hosts.txt
until ping -c1 db01 &>/dev/null; do
echo "waiting for db01..."
sleep 5
done

Functions and arrays

~
log() {
echo "[$(date +%T)] $1"
}
hosts=(web01 web02 web03)
echo "First host: ${hosts[0]}"
echo "All hosts: ${hosts[@]}"
echo "Count: ${#hosts[@]}"
for h in "${hosts[@]}"; do
log "checking $h"
done

Scheduling with cron

~
$ crontab -e
$ crontab -l
# minute hour day-of-month month day-of-week command
0 2 * * * /opt/scripts/backup.sh
*/15 * * * * /opt/scripts/health-check.sh
0 0 * * 0 /opt/scripts/weekly-report.sh

Each user has their own crontab; system-wide jobs also live under /etc/cron.d/. Always log cron output somewhere — no terminal is watching when it runs.

Scripts should be safe to run twice

An automation script that runs on a schedule, or gets re-triggered after a failure, will eventually run more than once against the same state. Design for that from the start:

  • Check before you act — if [[ ! -d /opt/app ]]; then mkdir /opt/app; fi instead of assuming it doesn't exist yet
  • Prefer commands that are naturally idempotent — systemctl enable, mkdir -p, rsync
  • Make failure loud and early — set -e, meaningful exit codes, and logs a human can read afterward
04

Processes & System Resources

  • Inspecting what is running
  • Controlling processes and signals
  • A quick read on system health

What’s actually running

~
$ ps aux
$ ps aux | grep nginx
$ pgrep -a nginx
$ top
$ pidof sshd
ps aux is a snapshot; top is the live, refreshing view — press q to quit, k to kill a PID from inside it.

Signals, jobs, and keeping things running after logout

~
$ kill -15 4821
$ kill -9 4821
$ ./long-task.sh &
$ jobs
$ fg %1
$ nohup ./long-task.sh &
$ disown
  • -15 (SIGTERM) asks nicely, -9 (SIGKILL) does not — reach for -9 only when -15 is ignored
  • & backgrounds a job; nohup keeps it running after the shell exits

A thirty-second health check

~
$ df -h
$ du -sh /var/log/*
$ free -h
$ uptime
$ cat /proc/loadavg
df is disk space by filesystem, du is disk space by directory — the two questions "am I out of space" and "what's using it" need different commands. /proc is a live window into kernel state, readable like any other file.
05

SSH Fundamentals

How you'll reach every one of these machines from now on.

  • What SSH actually is
  • First connection and host keys
  • Key-based authentication
  • The client config file
  • Hardening the server side

An encrypted remote shell, client to server

SSH (Secure Shell) opens an encrypted connection to a remote machine's sshd daemon, listening on port 22, and hands you a shell on the other end — everything from that point on is just the shell you already know, running somewhere else.
Your laptop ssh client
→
Encrypted tunnel port 22
→
Remote host sshd

The first connection asks you to trust a fingerprint

~
$ ssh student@203.0.113.42
The authenticity of host '203.0.113.42' can't be established.
ED25519 key fingerprint is SHA256:9fK3...
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '203.0.113.42' to the list of known hosts.
student@203.0.113.42's password:

That fingerprint gets written to ~/.ssh/known_hosts. If it ever changes unexpectedly on a later connection, SSH refuses outright — that’s not a bug, it’s the whole point: someone or something on the network changed.

Key-based auth: what you have, not what you remember

~
$ ssh-keygen -t ed25519 -C "student@laptop"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/student/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
$ ssh-copy-id student@203.0.113.42
$ ssh student@203.0.113.42
ssh-copy-id appends your public key to the remote ~/.ssh/authorized_keys — the private key never leaves your machine, and never should.

Stop typing the same flags every time

~/.ssh/config
Host web01
HostName 203.0.113.42
User student
Port 22
IdentityFile ~/.ssh/id_ed25519
Host bastion
HostName 198.51.100.10
User ops
~
$ ssh web01

Every flag you’d otherwise type on the command line can live in a Host block — including jump-host and forwarding settings covered later in this lecture.

Hardening the server side

/etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
AllowUsers student ops
ClientAliveInterval 300
ClientAliveCountMax 2
~
$ sudo systemctl reload sshd
$ sudo firewall-cmd --add-service=ssh --permanent
$ sudo firewall-cmd --reload

Turn off password auth after confirming key-based login works — locking yourself out of a remote VM is a genuinely bad afternoon. firewalld, not the SSH config, is what decides whether port 22 is reachable at all.

06

SSH for Sysadmin Workflows

The same tunnel, put to work: files, commands, and traffic.

  • Moving files: scp and rsync
  • Running remote commands from a script
  • Port forwarding
  • ssh-agent and connection reuse

Moving files over the same encrypted channel

~
$ scp app.tar.gz web01:/opt/releases/
$ scp web01:/var/log/app.log ./local-app.log
$ rsync -avz --delete ./dist/ web01:/opt/app/
scp is a straightforward copy; rsync compares source and destination and transfers only what changed — the one to reach for on anything larger than a single small file, or anything you'll sync repeatedly.

SSH runs one command just as easily as a shell

~
$ ssh web01 "systemctl is-active nginx"
active
$ ssh web01 "df -h" > web01-disk.txt
$ for h in web01 web02 web03; do
ssh "$h" "uptime"
done

This is the shape every remote-automation script takes — a webhook listener, a deploy script, a monitoring check — a local Bash script that runs a handful of commands on a remote box, one ssh call at a time.

Tunneling a port through SSH

~
$ ssh -L 8080:localhost:80 web01
$ ssh -R 9000:localhost:3000 bastion
$ ssh -D 1080 bastion
  • Local -L — reach a remote-only port from your laptop, e.g. an internal dashboard
  • Remote -R — expose something on your side to the remote host
  • Dynamic -D — a SOCKS proxy through the tunnel for anything, not just one port

Fewer passphrases, faster reconnects

~
$ eval "$(ssh-agent -s)"
$ ssh-add ~/.ssh/id_ed25519
$ ssh -A bastion
~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 10m
ssh-agent unlocks your key once per session instead of per connection; -A forwards that agent through a jump host so you don't copy private keys around. ControlMaster reuses one TCP connection for every subsequent SSH to the same host — later connections are near-instant.

Bash + SSH is already most of a deploy script

rollout.sh
#!/usr/bin/env bash
set -euo pipefail
hosts=(web01 web02 web03)
for host in "${hosts[@]}"; do
echo "Deploying to $host..."
ssh "$host" "cd /opt/app && git pull && systemctl restart app"
ssh "$host" "systemctl is-active --quiet app" \
&& echo "$host OK" \
|| { echo "$host FAILED" >&2; exit 1; }
done

Nothing here is new — a loop, an if-style short circuit, and ssh calls. This exact shape, scaled up, is what the capstone’s deployment pipeline is built from.

07

Lab & Logistics

  • Where to find these slides again

Fin

All slides available at
slides.raafat.io