Using Linux Terminal, Ubuntu Desktop and Ubuntu Server labs #5
No files matched your search
@@ -46,6 +46,8 @@ with `set -e`. Read `lib/lab.sh` for the helpers; the ones most labs use:
|
||||
`10.99.0.2` (name `peer`), linked to the VM's `10.99.0.1`
|
||||
- `hosts_add LINE`: an `/etc/hosts` line reset takes out again
|
||||
- `state_set KEY VALUE`, `state_get KEY`: notes for check and reset
|
||||
- `sshd_restart`: restart sshd after reset puts its settings back, and
|
||||
check port 22 answers (on Ubuntu, never restart `ssh.service` yourself)
|
||||
|
||||
The peer shares the VM's files, users and `/etc`, but has its own
|
||||
addresses, routes, firewall and ports. It has no route beyond the VM, so
|
||||
|
||||
@@ -9,12 +9,7 @@ rm -f /root/hash-me.txt /root/secret.txt /root/secret.txt.gpg
|
||||
if [ -f "$LAB_STATE/ssh.tar" ]; then
|
||||
rm -f /etc/ssh/sshd_config.d/*
|
||||
tar -C /etc -xf "$LAB_STATE/ssh.tar"
|
||||
if is_rhel; then
|
||||
systemctl restart sshd
|
||||
else
|
||||
systemctl restart ssh.socket 2>/dev/null
|
||||
systemctl restart ssh.service 2>/dev/null
|
||||
fi
|
||||
sshd_restart
|
||||
fi
|
||||
# Root's new keys and gpg's files.
|
||||
for k in /root/.ssh/id_*; do
|
||||
|
||||
@@ -16,12 +16,7 @@ for f in /etc/systemd/system/ssh.socket.d/*; do
|
||||
grep -qxF "${f##*/}" "$LAB_STATE/socket-d" 2>/dev/null || rm -f "$f"
|
||||
done
|
||||
systemctl daemon-reload
|
||||
if is_rhel; then
|
||||
systemctl restart sshd
|
||||
else
|
||||
systemctl restart ssh.socket 2>/dev/null
|
||||
systemctl restart ssh.service 2>/dev/null
|
||||
fi
|
||||
sshd_restart
|
||||
|
||||
# The client side.
|
||||
if [ -f "$LAB_STATE/root-ssh-config" ]; then
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Using Linux Terminal 1.1: Files and directories
|
||||
|
||||
Making a directory tree in one command, copying with hidden files and
|
||||
dates kept, hard and symbolic links, and removing directories.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab creates user `student` (password `Lab-Pass-1`). Work as `student`
|
||||
(`sudo -iu student`), so `~` is `/home/student`. The lab only checks that
|
||||
`~/lab` doesn't exist yet; `le-lab reset` removes it.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Create `~/lab/app/config`, `~/lab/app/logs` and `~/lab/app/data` in one
|
||||
command, and copy `/etc/skel` to `~/lab/skel`, keeping its hidden files
|
||||
and timestamps.
|
||||
2. In `~/lab`, create `notes.txt` containing the word `hello`, a hard link
|
||||
`notes.hard` and a symbolic link `notes.link` to it. Then rename
|
||||
`notes.txt` to `notes.old`, and see what `cat` makes of each link.
|
||||
3. Remove `~/lab/app/logs` with `rmdir`. Put a file in `~/lab/app/data`,
|
||||
try `rmdir` on it, then remove that directory too. Only `config` is left
|
||||
in `~/lab/app`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `mkdir -p` with a brace list: `{config,logs,data}`.
|
||||
- `cp -a`; compare `ls -la ~/lab/skel` with `ls -la /etc/skel`.
|
||||
- `ln`, `ln -s`, `mv`; `ls -li` shows inode numbers and `stat -c %h` the
|
||||
link count.
|
||||
- `rmdir` only removes empty directories: "Directory not empty". `rm -r`.
|
||||
@@ -0,0 +1,29 @@
|
||||
H=$(state_get home)
|
||||
L=$H/lab
|
||||
# skel_kept: every file under /etc/skel is in ~/lab/skel, same type and
|
||||
# modification time.
|
||||
skel_kept() {
|
||||
local f
|
||||
while read -r f; do
|
||||
[ "$(stat -c '%F %Y' "/etc/skel/$f")" = "$(stat -c '%F %Y' "$L/skel/$f" 2>/dev/null)" ] || return 1
|
||||
done < <(cd /etc/skel && find . -mindepth 1)
|
||||
}
|
||||
|
||||
task 1 "~/lab/app/config, and /etc/skel copied to ~/lab/skel with hidden files and dates"
|
||||
need "$L/app/config is a directory" '[ -d "$L/app/config" ]'
|
||||
need "$L/skel holds the hidden files, such as .bashrc" '[ -f "$L/skel/.bashrc" ]'
|
||||
need "every file in ~/lab/skel has the same date as in /etc/skel" skel_kept
|
||||
|
||||
task 2 "notes.hard and notes.link made, then notes.txt renamed to notes.old"
|
||||
need "$L/notes.old contains hello" 'grep -qw hello "$L/notes.old"'
|
||||
need "$L/notes.txt is gone (renamed)" '[ ! -e "$L/notes.txt" ] && [ ! -L "$L/notes.txt" ]'
|
||||
need "notes.hard is a hard link to notes.old (same inode)" '[ ! -L "$L/notes.hard" ] && [ "$L/notes.hard" -ef "$L/notes.old" ]'
|
||||
need "stat -c %h notes.old prints 2" '[ "$(stat -c %h "$L/notes.old")" = 2 ]'
|
||||
need "notes.link is a symbolic link to notes.txt" '[ -L "$L/notes.link" ] && [ "$(basename "$(readlink "$L/notes.link")")" = notes.txt ]'
|
||||
need "notes.link is broken now" '[ ! -e "$L/notes.link" ]'
|
||||
|
||||
task 3 "logs and data removed: only config left in ~/lab/app"
|
||||
need "$L/app exists" '[ -d "$L/app" ]'
|
||||
need "$L/app/logs is gone" '[ ! -e "$L/app/logs" ]'
|
||||
need "$L/app/data is gone" '[ ! -e "$L/app/data" ]'
|
||||
need "ls ~/lab/app lists only config" '[ "$(ls -A "$L/app")" = config ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.1 Files and directories"
|
||||
CHAPTER=/linux-terminal/1-files-and-filesystems/1-1-files-and-directories/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,3 @@
|
||||
H=$(state_get home)
|
||||
[ -n "$H" ] && rm -rf "$H/lab"
|
||||
true
|
||||
@@ -0,0 +1,5 @@
|
||||
user_add student
|
||||
H=$(getent passwd student | cut -d: -f6)
|
||||
[ -e "$H/lab" ] && refuse "$H/lab already exists; the tasks create it. Remove it and start the lab again."
|
||||
state_set home "$H"
|
||||
true
|
||||
@@ -0,0 +1,28 @@
|
||||
# Using Linux Terminal 1.2: Searching, comparing and changing files
|
||||
|
||||
grep and regular expressions, find with errors thrown away, sed with a
|
||||
backup, and diff.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab creates user `student` (password `Lab-Pass-1`). Work as `student`
|
||||
(`sudo -iu student`), so `~` is `/home/student`. The lab removes the files
|
||||
the tasks create if they exist, and `le-lab reset` removes them again.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write the names of all users whose shell is `/bin/bash` to
|
||||
`~/bash-users.txt`, one per line, and nothing else.
|
||||
2. As `student`, find every regular file under `/etc` bigger than 100 KiB
|
||||
and save the list in `~/big-etc.txt`, with any permission errors thrown
|
||||
away: none on the screen and none in the file.
|
||||
3. Copy `/etc/ssh/sshd_config` to `~/sshd.copy`, then use `sed` to change
|
||||
`#Port 22` to `Port 2222` in the copy, keeping a `.bak` of it. `diff
|
||||
~/sshd.copy.bak ~/sshd.copy` shows just that one line changed.
|
||||
|
||||
## Hints
|
||||
|
||||
- `grep '/bin/bash$' /etc/passwd`, then `cut -d: -f1`; compare `wc -l`
|
||||
with `grep -c`.
|
||||
- `find /etc -type f -size +100k`, and `2>/dev/null` for the errors.
|
||||
- `sed -i.bak 's/^#Port 22$/Port 2222/'`; diff shows a `c` change.
|
||||
@@ -0,0 +1,35 @@
|
||||
H=$(state_get home)
|
||||
B=$H/big-etc.txt
|
||||
norm() { sed 's#//*#/#g; s/[[:space:]]*$//' "$@"; }
|
||||
# Each listed path is a regular file under /etc bigger than 100 KiB.
|
||||
all_big() {
|
||||
local p
|
||||
while read -r p; do
|
||||
case $p in /etc/*) ;; *) return 1 ;; esac
|
||||
[ -n "$(find "$p" -maxdepth 0 -type f -size +100k 2>/dev/null)" ] || return 1
|
||||
done < <(norm "$B")
|
||||
}
|
||||
# Every such file student can find is listed.
|
||||
none_missing() {
|
||||
local p
|
||||
while read -r p; do
|
||||
norm "$B" | grep -qxF "$p" || return 1
|
||||
done < <(runuser -u student -- find /etc -type f -size +100k 2>/dev/null | norm)
|
||||
}
|
||||
|
||||
task 1 "~/bash-users.txt lists exactly the users with shell /bin/bash"
|
||||
need "$H/bash-users.txt exists" '[ -f "$H/bash-users.txt" ]'
|
||||
need "it holds the names of the users whose shell is /bin/bash, one per line, and nothing else" \
|
||||
'[ "$(norm "$H/bash-users.txt" | sort)" = "$(awk -F: "\$7 == \"/bin/bash\" {print \$1}" /etc/passwd | sort)" ]'
|
||||
|
||||
task 2 "~/big-etc.txt: regular files under /etc over 100 KiB, no errors"
|
||||
need "$B exists and lists something" '[ -s "$B" ]'
|
||||
need "it has no Permission denied lines" '! grep -qi "permission denied" "$B"'
|
||||
need "every line is a regular file under /etc bigger than 100 KiB" all_big
|
||||
need "it lists every such file student can find" none_missing
|
||||
|
||||
task 3 "sed changed #Port 22 to Port 2222 in ~/sshd.copy, with a .bak"
|
||||
need "$H/sshd.copy.bak is an unchanged copy of /etc/ssh/sshd_config" 'cmp -s "$H/sshd.copy.bak" "$LAB_STATE/sshd_config" || cmp -s "$H/sshd.copy.bak" /etc/ssh/sshd_config'
|
||||
need "$H/sshd.copy has Port 2222 and no #Port 22" 'grep -qx "Port 2222" "$H/sshd.copy" && ! grep -qx "#Port 22" "$H/sshd.copy"'
|
||||
need "diff ~/sshd.copy.bak ~/sshd.copy shows one line changed, as a c change" \
|
||||
'd=$(diff "$H/sshd.copy.bak" "$H/sshd.copy"); [ "$(wc -l <<<"$d")" = 4 ] && head -n1 <<<"$d" | grep -Eq "^[0-9]+c[0-9]+$"'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.2 Searching, comparing and changing files"
|
||||
CHAPTER=/linux-terminal/1-files-and-filesystems/1-2-search-compare-modify/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,3 @@
|
||||
H=$(state_get home)
|
||||
[ -n "$H" ] && rm -f "$H/bash-users.txt" "$H/big-etc.txt" "$H/sshd.copy" "$H/sshd.copy.bak"
|
||||
true
|
||||
@@ -0,0 +1,8 @@
|
||||
grep -qx '#Port 22' /etc/ssh/sshd_config ||
|
||||
refuse "/etc/ssh/sshd_config has no line '#Port 22' for task 3; put it back as Ubuntu ships it and start the lab again."
|
||||
user_add student
|
||||
H=$(getent passwd student | cut -d: -f6)
|
||||
state_set home "$H"
|
||||
rm -f "$H/bash-users.txt" "$H/big-etc.txt" "$H/sshd.copy" "$H/sshd.copy.bak"
|
||||
cp /etc/ssh/sshd_config "$LAB_STATE/sshd_config"
|
||||
true
|
||||
@@ -0,0 +1,34 @@
|
||||
# Using Linux Terminal 2.1: Logs and log rotation
|
||||
|
||||
Reading `/var/log/auth.log` and the journal, rotating a log of your own
|
||||
with logrotate, and filtering the journal by priority.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`), and write answers to the files in
|
||||
`/root/lab-answers/`. The lab creates user `alice`, who then runs four
|
||||
commands with `sudo`, and it logs a few messages tagged `webapp` at
|
||||
different priorities. It only checks that `/var/log/webapp` doesn't exist
|
||||
yet; `le-lab reset` removes it and your logrotate file for it.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Find the last three commands `alice` ran with `sudo`, from
|
||||
`/var/log/auth.log` and from the journal, and write them to
|
||||
`/root/lab-answers/1`, one per line. Both should show the same commands
|
||||
at the same times.
|
||||
2. Create `/var/log/webapp/access.log` with some lines in it, and a
|
||||
logrotate configuration that rotates it weekly, keeps 4 compressed
|
||||
copies, and creates a new empty log owned by `root:adm` with mode 640.
|
||||
Force one rotation.
|
||||
3. Show only error messages (and worse) from the current boot, and write
|
||||
the `webapp` messages among them to `/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `grep 'COMMAND=' /var/log/auth.log`, `journalctl -t sudo`; `tail -n 3`.
|
||||
Pasting whole log lines into the answer file is fine.
|
||||
- A file in `/etc/logrotate.d/` with `weekly`, `rotate 4`, `compress` and
|
||||
`create 0640 root adm`; `logrotate -d` to test it, `logrotate -f` to
|
||||
force it. `zcat` reads the old copy.
|
||||
- `journalctl -p err -b`; `-t webapp` narrows it to one tag.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Answer lines, lowercase.
|
||||
ans() { tr 'A-Z' 'a-z' <"/root/lab-answers/$1" 2>/dev/null; }
|
||||
LR=$(grep -lE '/var/log/webapp/' /etc/logrotate.d/* 2>/dev/null | head -n1)
|
||||
W=/var/log/webapp
|
||||
|
||||
task 1 "alice's last three sudo commands in /root/lab-answers/1"
|
||||
need "/root/lab-answers/1 has cat /etc/hostname" 'ans 1 | grep -qF "cat /etc/hostname"'
|
||||
need "/root/lab-answers/1 has du -sh /var/log" 'ans 1 | grep -qF "du -sh /var/log"'
|
||||
need "/root/lab-answers/1 has tail -n 5 /var/log/dpkg.log" 'ans 1 | grep -qF "tail -n 5 /var/log/dpkg.log"'
|
||||
need "/root/lab-answers/1 has only the last three (not ls /etc/apt, the one before)" '! ans 1 | grep -qF "ls /etc/apt"'
|
||||
|
||||
task 2 "logrotate rotates /var/log/webapp/access.log weekly, 4 compressed, create 640 root adm"
|
||||
need "a file in /etc/logrotate.d names /var/log/webapp/access.log" '[ -n "$LR" ]'
|
||||
need "logrotate -d shows it rotated weekly, keeping 4" 'logrotate -d /etc/logrotate.conf 2>&1 | grep -E "^rotating pattern: .*/var/log/webapp/" | grep -q "weekly (4 rotations)"'
|
||||
need "logrotate -d reports no errors for it" '! logrotate -d "$LR" 2>&1 | grep -qi "^error"'
|
||||
need "$LR compresses old copies" 'grep -Eq "^[[:space:]]*compress([[:space:]]|$)" "$LR"'
|
||||
need "$LR creates the new log with 0640 root adm" 'grep -Eq "^[[:space:]]*create[[:space:]]+0?640[[:space:]]+root[[:space:]]+adm([[:space:]]|$)" "$LR"'
|
||||
need "$W/access.log is a new empty log" '[ -f $W/access.log ] && [ ! -s $W/access.log ]'
|
||||
need "$W/access.log is -rw-r----- root adm" '[ "$(stat -c "%a %U %G" $W/access.log)" = "640 root adm" ]'
|
||||
need "the old lines are in access.log.1.gz (or access.log.1)" '{ zcat $W/access.log.1.gz 2>/dev/null || cat $W/access.log.1 2>/dev/null; } | grep -q .'
|
||||
|
||||
task 3 "The webapp messages of priority err and worse in /root/lab-answers/3"
|
||||
need "/root/lab-answers/3 has the error: cannot connect to database" 'ans 3 | grep -qF "cannot connect to database"'
|
||||
need "/root/lab-answers/3 has the critical message: worker threads stuck" 'ans 3 | grep -qF "worker threads stuck"'
|
||||
need "/root/lab-answers/3 has nothing below err (no warning, no notice)" '! ans 3 | grep -qE "cache warmed|disk use at"'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.1 Logs and log rotation"
|
||||
CHAPTER=/linux-terminal/2-system-resources/2-1-logs-and-log-rotation/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,12 @@
|
||||
rm -f /etc/sudoers.d/le-lab-alice
|
||||
rm -rf /var/log/webapp
|
||||
for f in /etc/logrotate.d/*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qxF "${f##*/}" "$LAB_STATE/logrotate-d" 2>/dev/null && continue
|
||||
grep -q '/var/log/webapp' "$f" && rm -f "$f"
|
||||
done
|
||||
sed -i '\#^"/var/log/webapp/#d' /var/lib/logrotate/status 2>/dev/null
|
||||
if [ -n "$(state_get installed-rsyslog)" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge rsyslog >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,27 @@
|
||||
[ -e /var/log/webapp ] && refuse "/var/log/webapp already exists; task 2 creates it. Remove it and start the lab again."
|
||||
if ! pkg_installed rsyslog; then
|
||||
pkg_install rsyslog
|
||||
state_set installed-rsyslog 1
|
||||
fi
|
||||
systemctl enable --now --quiet rsyslog
|
||||
ls /etc/logrotate.d >"$LAB_STATE/logrotate-d"
|
||||
user_add alice
|
||||
|
||||
# Four commands as alice with sudo, a second apart; the last three are the
|
||||
# answer to task 1.
|
||||
cat >/etc/sudoers.d/le-lab-alice <<'X'
|
||||
alice ALL=(root) NOPASSWD: /usr/bin/ls /etc/apt, /usr/bin/cat /etc/hostname, /usr/bin/du -sh /var/log, /usr/bin/tail -n 5 /var/log/dpkg.log
|
||||
X
|
||||
chmod 440 /etc/sudoers.d/le-lab-alice
|
||||
for c in "/usr/bin/ls /etc/apt" "/usr/bin/cat /etc/hostname" "/usr/bin/du -sh /var/log" "/usr/bin/tail -n 5 /var/log/dpkg.log"; do
|
||||
runuser -u alice -- sudo -n $c >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
done
|
||||
rm -f /etc/sudoers.d/le-lab-alice
|
||||
|
||||
# Messages for task 3, from notice to critical.
|
||||
logger -t webapp -p user.notice "cache warmed in 840 ms"
|
||||
logger -t webapp -p user.warning "disk use at 85 percent on /var"
|
||||
logger -t webapp -p user.err "cannot connect to database at 10.0.0.5:5432"
|
||||
logger -t webapp -p user.crit "all worker threads stuck, giving up"
|
||||
true
|
||||
@@ -0,0 +1,35 @@
|
||||
# Using Linux Terminal 2.2: Partitions and filesystems
|
||||
|
||||
A GPT partition with an ext4 filesystem mounted at boot by UUID, getting a
|
||||
deleted partition back, and a read-only filesystem check.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`), and write answers to the files in
|
||||
`/root/lab-answers/`. The lab adds a blank 2 GiB spare disk,
|
||||
`/dev/loop40`, which stays attached across reboots; its first partition is
|
||||
`/dev/loop40p1` (the guide's `/dev/vdb` and `/dev/vdb1`). It saves
|
||||
`/etc/fstab` and puts it back on reset, which also removes the disk.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Give `/dev/loop40` a GPT partition table with one 1 GiB partition, make
|
||||
an ext4 filesystem labeled `data` on it, and mount it at `/mnt/data` at
|
||||
every boot, by UUID. `findmnt --verify` reports no errors or warnings.
|
||||
2. Save a file `/mnt/data/keep.txt` containing `keep me`. Unmount the
|
||||
filesystem, write its partition's start sector to
|
||||
`/root/lab-answers/2`, delete the partition with fdisk, then recreate it
|
||||
so that `keep.txt` comes back unchanged. Mount it again.
|
||||
3. Check the filesystem fully while it's unmounted, without letting fsck
|
||||
change anything, and save fsck's output in `/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `fdisk /dev/loop40` (`g`, `n`, `+1G`, `w`) or `parted`; `mkfs.ext4 -L`;
|
||||
`blkid` for the UUID; `systemctl daemon-reload` after editing fstab, then
|
||||
`mount -a`.
|
||||
- `fdisk -l /dev/loop40` shows the start; recreate with the same start and
|
||||
answer **N** to "remove the signature". fdisk asks that only at a
|
||||
terminal: fed from a pipe, it keeps the signature without asking.
|
||||
`blkid` shows the same UUID.
|
||||
- `fsck -fn /dev/loop40p1 >/root/lab-answers/3 2>&1`.
|
||||
@@ -0,0 +1,25 @@
|
||||
D=$(state_get dev)
|
||||
P=${D}p1
|
||||
U=$(blkid -o value -s UUID "$P" 2>/dev/null)
|
||||
# Partition size in bytes.
|
||||
psize() { blockdev --getsize64 "$P" 2>/dev/null || echo 0; }
|
||||
|
||||
task 1 "GPT, one 1 GiB ext4 partition labeled data, mounted at /mnt/data by UUID"
|
||||
need "$D has a GPT partition table" '[ "$(blkid -o value -s PTTYPE "$D")" = gpt ]'
|
||||
need "$D has one partition, $P" '[ -b "$P" ] && [ "$(lsblk -nr -o TYPE "$D" | grep -c part)" = 1 ]'
|
||||
need "$P is 1 GiB" 's=$(psize); [ "$s" -ge $((1024 * 1024 * 1016)) ] && [ "$s" -le $((1024 * 1024 * 1032)) ]'
|
||||
need "$P holds ext4 labeled data" '[ "$(blkid -o value -s TYPE "$P")" = ext4 ] && [ "$(blkid -o value -s LABEL "$P")" = data ]'
|
||||
need "/etc/fstab mounts UUID=$U at /mnt/data" '[ -n "$U" ] && awk -v u="UUID=$U" "\$1 == u && \$2 == \"/mnt/data\" && \$3 == \"ext4\"" /etc/fstab | grep -q .'
|
||||
need "findmnt --verify reports no errors or warnings" 'findmnt --verify 2>&1 | grep -q "no errors or warnings"'
|
||||
need "/mnt/data is mounted from it now" '[ -n "$U" ] && [ "$(findmnt -n -o UUID /mnt/data)" = "$U" ]'
|
||||
|
||||
task 2 "Partition deleted and recreated at the same start; keep.txt is back"
|
||||
need "/root/lab-answers/2 holds $P's start sector" '[ "$(answer 2 | grep -o "[0-9]\+" | head -n1)" = "$(cat /sys/class/block/${P#/dev/}/start)" ]'
|
||||
need "/mnt/data is mounted from $P" '[ -n "$U" ] && [ "$(findmnt -n -o UUID /mnt/data)" = "$U" ]'
|
||||
need "/mnt/data/keep.txt contains keep me" '[ "$(cat /mnt/data/keep.txt)" = "keep me" ]'
|
||||
|
||||
task 3 "A full, read-only fsck, its output in /root/lab-answers/3"
|
||||
need "/root/lab-answers/3 has fsck's output through pass 5" 'grep -q "Pass 5" /root/lab-answers/3'
|
||||
need "it ends with the file and block counts" 'grep -Eq "[0-9]+/[0-9]+ files.*, [0-9]+/[0-9]+ blocks" /root/lab-answers/3'
|
||||
need "fsck ran on the unmounted filesystem" '! grep -qi "is mounted" /root/lab-answers/3'
|
||||
need "fsck changed nothing: no FILE SYSTEM WAS MODIFIED" '! grep -q "MODIFIED" /root/lab-answers/3'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.2 Partitions and filesystems"
|
||||
CHAPTER=/linux-terminal/2-system-resources/2-2-partitions-and-filesystems/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,7 @@
|
||||
umount /mnt/data 2>/dev/null
|
||||
if [ -f "$LAB_STATE/fstab" ]; then
|
||||
cp -a "$LAB_STATE/fstab" /etc/fstab
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
[ -n "$(state_get had-mnt-data)" ] || rmdir /mnt/data 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,10 @@
|
||||
if mountpoint -q /mnt/data || [ -n "$(ls -A /mnt/data 2>/dev/null)" ]; then
|
||||
refuse "/mnt/data is in use; unmount or empty it and start the lab again."
|
||||
fi
|
||||
grep -Eq '^[^#]*[[:space:]]/mnt/data[[:space:]]' /etc/fstab &&
|
||||
refuse "/etc/fstab already mounts something at /mnt/data; remove that line and start the lab again."
|
||||
cp -a /etc/fstab "$LAB_STATE/fstab"
|
||||
[ -d /mnt/data ] && state_set had-mnt-data 1
|
||||
dev=$(disk_add 0 2G)
|
||||
state_set dev "$dev"
|
||||
true
|
||||
@@ -0,0 +1,32 @@
|
||||
# Using Linux Terminal 2.3: Crontab format and location
|
||||
|
||||
A user crontab with a repeating job and a boot job, a system job in
|
||||
`/etc/cron.d`, and keeping one user away from crontab.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab creates users `student` and `alice` (password `Lab-Pass-1`), puts
|
||||
a harmless script at `/usr/local/bin/cleanup`, and makes sure cron is
|
||||
running. It saves `/etc/cron.allow` and `/etc/cron.deny` (if they exist)
|
||||
and puts them back on reset. Task 1 needs a few minutes of waiting and a
|
||||
reboot.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. As `student` (`sudo -iu student`), add a cron job that appends the date
|
||||
to `~/ticks.log` every two minutes, and one that writes `booted` to
|
||||
`~/boot.log` at every boot. Wait until `~/ticks.log` has a date or two,
|
||||
then reboot and look at `~/boot.log`.
|
||||
2. Make a system job that runs `/usr/local/bin/cleanup` as root at 3:15 am
|
||||
on the first day of every month, in its own file under `/etc/cron.d`.
|
||||
3. Stop `alice` from using crontab, without touching anyone else: her
|
||||
`crontab -l` says she isn't allowed, and `student`'s still works.
|
||||
|
||||
## Hints
|
||||
|
||||
- `crontab -e`, `crontab -l`; `*/2 * * * *` and `@reboot`; `>>` appends,
|
||||
`>` overwrites.
|
||||
- Files in `/etc/cron.d` have a user field after the five time fields, and
|
||||
cron skips files whose names contain a dot.
|
||||
- `/etc/cron.deny` lists users who may not use crontab; a `cron.allow`
|
||||
would shut out everyone it doesn't list.
|
||||
@@ -0,0 +1,39 @@
|
||||
H=$(state_get home)
|
||||
CT=$(crontab -l -u student 2>/dev/null | grep -Ev '^[[:space:]]*(#|$)')
|
||||
# The cron.d file with the cleanup job.
|
||||
CD=$(grep -lE '^[[:space:]]*15[[:space:]]+0?3[[:space:]]+1[[:space:]]+\*[[:space:]]+\*[[:space:]]+root[[:space:]]+/usr/local/bin/cleanup[[:space:]]*$' /etc/cron.d/* 2>/dev/null | head -n1)
|
||||
refused() { runuser -u "$1" -- crontab -l 2>&1 | grep -q "not allowed"; }
|
||||
others_ok() {
|
||||
local u
|
||||
refused root && return 1
|
||||
for u in $(awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd); do
|
||||
[ "$u" = alice ] && continue
|
||||
refused "$u" && return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
boot_log() {
|
||||
local i
|
||||
for i in $(seq 15); do
|
||||
grep -qx booted "$H/boot.log" 2>/dev/null && return 0
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
task 1 "student's crontab: date to ~/ticks.log every two minutes, booted to ~/boot.log at boot"
|
||||
need "student's crontab has a line that appends date to ~/ticks.log every two minutes" \
|
||||
'grep -E "^(\*/2|0-59/2|0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58)[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]" <<<"$CT" | grep "date" | grep -q ">>.*ticks\.log"'
|
||||
need "student's crontab has an @reboot line that writes booted to ~/boot.log" 'grep -E "^@reboot[[:space:]]" <<<"$CT" | grep -q "boot\.log"'
|
||||
need "$H/ticks.log has a date in it" 'grep -Eq "20[0-9][0-9]" "$H/ticks.log"'
|
||||
need "$H/boot.log contains booted (after a reboot)" boot_log
|
||||
|
||||
task 2 "/etc/cron.d job: /usr/local/bin/cleanup as root at 3:15 on the 1st"
|
||||
need "a file in /etc/cron.d holds 15 3 1 * * root /usr/local/bin/cleanup" '[ -n "$CD" ]'
|
||||
need "its name has no dot in it" '[[ "${CD##*/}" =~ ^[A-Za-z0-9_-]+$ ]]'
|
||||
need "it's owned by root and not writable by group or others" '[ "$(stat -c %U "$CD")" = root ] && [ -z "$(find "$CD" -perm /022)" ]'
|
||||
|
||||
task 3 "alice can't use crontab; everyone else still can"
|
||||
need "alice's crontab -l says she isn't allowed" 'refused alice'
|
||||
need "student's crontab -l still works" '! refused student'
|
||||
need "no other user (or root) is refused" others_ok
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.3 Crontab format and location"
|
||||
CHAPTER=/linux-terminal/2-system-resources/2-3-crontab/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,17 @@
|
||||
crontab -r -u student 2>/dev/null
|
||||
crontab -r -u alice 2>/dev/null
|
||||
H=$(state_get home)
|
||||
[ -n "$H" ] && rm -f "$H/ticks.log" "$H/boot.log"
|
||||
for f in cron.allow cron.deny; do
|
||||
if [ -f "$LAB_STATE/$f" ]; then cp -a "$LAB_STATE/$f" /etc/$f; else rm -f /etc/$f; fi
|
||||
done
|
||||
for f in /etc/cron.d/*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qxF "${f##*/}" "$LAB_STATE/cron-d" 2>/dev/null && continue
|
||||
grep -q /usr/local/bin/cleanup "$f" && rm -f "$f"
|
||||
done
|
||||
[ -n "$(state_get made-cleanup)" ] && rm -f /usr/local/bin/cleanup
|
||||
if [ -n "$(state_get installed-cron)" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge cron >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,25 @@
|
||||
if ! pkg_installed cron; then
|
||||
pkg_install cron
|
||||
state_set installed-cron 1
|
||||
fi
|
||||
systemctl enable --now --quiet cron
|
||||
user_add student
|
||||
user_add alice
|
||||
crontab -r -u student 2>/dev/null || true
|
||||
for f in cron.allow cron.deny; do
|
||||
[ -f /etc/$f ] && cp -a /etc/$f "$LAB_STATE/$f"
|
||||
done
|
||||
ls /etc/cron.d >"$LAB_STATE/cron-d"
|
||||
if [ ! -e /usr/local/bin/cleanup ]; then
|
||||
cat >/usr/local/bin/cleanup <<'X'
|
||||
#!/bin/sh
|
||||
# Practice lab: stands in for a monthly cleanup job.
|
||||
find /tmp -maxdepth 1 -name 'cleanup-test-*' -mtime +30 -delete
|
||||
X
|
||||
chmod 755 /usr/local/bin/cleanup
|
||||
state_set made-cleanup 1
|
||||
fi
|
||||
H=$(getent passwd student | cut -d: -f6)
|
||||
state_set home "$H"
|
||||
rm -f "$H/ticks.log" "$H/boot.log"
|
||||
true
|
||||
@@ -0,0 +1,34 @@
|
||||
# Using Linux Terminal 2.4: Troubleshooting with logs
|
||||
|
||||
Finding why a service failed, spotting a failed SSH login, and filtering
|
||||
the journal by priority and by source.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`), and write answers to the files in
|
||||
`/root/lab-answers/`. The peer, at `10.99.0.2`, stands in for another
|
||||
machine: get a shell there with `ip netns exec lepeer bash`, and this VM
|
||||
is `10.99.0.1` from it. The lab also logs two error messages, one from a
|
||||
program and one from the kernel. Do task 3 within an hour of starting the
|
||||
lab (or reset and start again).
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Create `/etc/systemd/system/broken.service` with
|
||||
`ExecStart=/usr/local/bin/missing`, run `daemon-reload` and start it.
|
||||
Find the reason it failed using only systemctl and journalctl, and write
|
||||
the exit status `systemctl status` shows (such as `status=...`) to
|
||||
`/root/lab-answers/1`.
|
||||
2. From the peer, try to SSH to this VM (`10.99.0.1`) as a user that
|
||||
doesn't exist, and write the name you used to `/root/lab-answers/2`.
|
||||
Find the attempt in `/var/log/auth.log` and in the journal.
|
||||
3. List every message of priority err or worse from the last hour, then
|
||||
only the kernel's, and save that kernel-only list in
|
||||
`/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `systemctl status broken`, `journalctl -u broken`.
|
||||
- `ip netns exec lepeer ssh [email protected]`; `grep 'Invalid user'
|
||||
/var/log/auth.log`; `journalctl -u ssh --since "5 min ago"`.
|
||||
- `journalctl -p err --since "1 hour ago"`; `-k` keeps only the kernel's.
|
||||
@@ -0,0 +1,21 @@
|
||||
START=$(state_get start)
|
||||
ans() { tr 'A-Z' 'a-z' <"/root/lab-answers/$1" 2>/dev/null; }
|
||||
NAME=$(head -n1 /root/lab-answers/2 2>/dev/null | tr -d '[:space:]')
|
||||
invalid_in_authlog() { grep -h sshd /var/log/auth.log /var/log/auth.log.1 2>/dev/null | grep -qF "Invalid user $NAME from 10.99.0.2 "; }
|
||||
invalid_in_journal() { journalctl -q -u ssh -u ssh.socket --since "$START" --no-pager | grep -qF "Invalid user $NAME from 10.99.0.2 "; }
|
||||
|
||||
task 1 "broken.service started, and its failure found: 203/EXEC"
|
||||
need "/etc/systemd/system/broken.service has ExecStart=/usr/local/bin/missing" 'grep -Eq "^ExecStart=/usr/local/bin/missing[[:space:]]*$" /etc/systemd/system/broken.service'
|
||||
need "systemd has loaded it (daemon-reload)" '[ "$(systemctl show -p LoadState --value broken.service)" = loaded ] && [ "$(systemctl show -p NeedDaemonReload --value broken.service)" = no ]'
|
||||
need "it was started and failed: journalctl -u broken shows status=203/EXEC" 'journalctl -q -u broken --since "$START" --no-pager | grep -q "status=203/EXEC"'
|
||||
need "/root/lab-answers/1 has the exit status, 203/EXEC" 'ans 1 | grep -q "203"'
|
||||
|
||||
task 2 "A failed SSH login from the peer as a user that doesn't exist"
|
||||
need "/root/lab-answers/2 holds a user name" '[ -n "$NAME" ]'
|
||||
need "$NAME isn't a user on this system" '! getent passwd "$NAME"'
|
||||
need "/var/log/auth.log has Invalid user $NAME from 10.99.0.2" invalid_in_authlog
|
||||
need "journalctl -u ssh shows the same line" invalid_in_journal
|
||||
|
||||
task 3 "The kernel's err-or-worse messages from the last hour in /root/lab-answers/3"
|
||||
need "/root/lab-answers/3 has the kernel's medium error on sector 4242" 'ans 3 | grep -qF "medium error on sector 4242"'
|
||||
need "/root/lab-answers/3 has only the kernel's messages (not the backup error)" '! ans 3 | grep -qF "backup of /srv failed"'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.4 Troubleshooting with logs"
|
||||
CHAPTER=/linux-terminal/2-system-resources/2-4-troubleshooting-with-logs/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,8 @@
|
||||
systemctl stop broken.service 2>/dev/null
|
||||
rm -f /etc/systemd/system/broken.service
|
||||
rm -rf /etc/systemd/system/broken.service.d
|
||||
systemctl daemon-reload
|
||||
systemctl reset-failed broken.service 2>/dev/null
|
||||
[ -n "$(state_get had-known-host)" ] || ssh-keygen -q -R 10.99.0.1 -f /root/.ssh/known_hosts >/dev/null 2>&1
|
||||
rm -f /root/.ssh/known_hosts.old
|
||||
true
|
||||
@@ -0,0 +1,10 @@
|
||||
for f in /etc/systemd/system/broken.service /usr/local/bin/missing; do
|
||||
[ -e "$f" ] && refuse "$f already exists; the lab needs it absent. Remove it and start the lab again."
|
||||
done
|
||||
state_set start "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
grep -qs '10\.99\.0\.1' /root/.ssh/known_hosts && state_set had-known-host 1
|
||||
peer_up
|
||||
# One error from a program and one from the kernel, for task 3.
|
||||
logger -t backup -p user.err "backup of /srv failed: no space left on device"
|
||||
echo '<3>kernel: lab-disk: medium error on sector 4242 (practice lab message)' >/dev/kmsg
|
||||
true
|
||||
@@ -0,0 +1,36 @@
|
||||
# Using Linux Terminal 3.1: SSH keys
|
||||
|
||||
An Ed25519 key with a passphrase, installing it for another user by hand,
|
||||
and what sshd does when a home directory is too open.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab creates user `student` (password `Lab-Pass-1`). Make the key as
|
||||
`student` (`sudo -iu student`); use `sudo` for the steps that need root.
|
||||
Give the key the passphrase `Lab-Phrase-1`: `le-lab check` needs it to
|
||||
test the login. The lab checks that user `bob` doesn't exist yet, and
|
||||
`le-lab reset` removes him and `~/.ssh/lab_key`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Make an Ed25519 key pair with a passphrase and the comment `lab-key`,
|
||||
saved as `~/.ssh/lab_key`.
|
||||
2. Create a user `bob` with a password, then install your lab key for him
|
||||
so you can log in to `bob@localhost` with it. Ubuntu cloud images turn
|
||||
off password logins over SSH, so ssh-copy-id can't log in there; use
|
||||
the manual steps instead. After `eval "$(ssh-agent)"; ssh-add
|
||||
~/.ssh/lab_key`, `ssh -o BatchMode=yes -o
|
||||
StrictHostKeyChecking=accept-new -i ~/.ssh/lab_key bob@localhost
|
||||
whoami` prints `bob`.
|
||||
3. Run `sudo chmod 777 /home/bob`, try the login again, and find out why it
|
||||
fails. Then fix it.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ssh-keygen -t ed25519 -C lab-key -f ~/.ssh/lab_key`; `ssh-keygen -l -f`.
|
||||
- `useradd -m`, `passwd`; then `/home/bob/.ssh` (mode 700) and
|
||||
`authorized_keys` (mode 600), both owned by `bob`, holding the `.pub`
|
||||
line. BatchMode doesn't ask about a new host key either: without
|
||||
`accept-new`, the first try fails with "Host key verification failed."
|
||||
- `journalctl -u ssh -n 5` or `/var/log/auth.log`: "bad ownership or
|
||||
modes"; `chmod 750 /home/bob`.
|
||||
@@ -0,0 +1,38 @@
|
||||
H=$(state_get home)
|
||||
K=$H/.ssh/lab_key
|
||||
BH=$(getent passwd bob | cut -d: -f6)
|
||||
# key_login: log in as bob with the lab key, unlocked into a temporary copy.
|
||||
key_login() {
|
||||
local d r=1
|
||||
d=$(mktemp -d)
|
||||
cp "$K" "$d/k" && chmod 600 "$d/k" && ssh-keygen -q -p -P Lab-Phrase-1 -N '' -f "$d/k" >/dev/null 2>&1 &&
|
||||
[ "$(timeout 15 ssh -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i "$d/k" bob@localhost whoami </dev/null 2>/dev/null)" = bob ] && r=0
|
||||
rm -rf "$d"
|
||||
return $r
|
||||
}
|
||||
bad_modes() {
|
||||
local n m="bad ownership or modes for directory $BH"
|
||||
journalctl -q -u ssh --since "$(state_get start)" --no-pager 2>/dev/null | grep -qF "$m" && return 0
|
||||
n=$(cat "$LAB_STATE/auth-size" 2>/dev/null); n=${n:-0}
|
||||
[ "$(stat -c %s /var/log/auth.log)" -ge "$n" ] || n=0
|
||||
tail -c +$((n + 1)) /var/log/auth.log | grep -qF "$m"
|
||||
}
|
||||
|
||||
task 1 "~/.ssh/lab_key: Ed25519, comment lab-key, with a passphrase"
|
||||
need "$K and $K.pub exist" '[ -f "$K" ] && [ -f "$K.pub" ]'
|
||||
need "ssh-keygen -l -f $K.pub ends with lab-key (ED25519)" 'ssh-keygen -l -f "$K.pub" | grep -q "lab-key (ED25519)$"'
|
||||
need "$K is -rw------- and owned by student" '[ "$(stat -c "%a %U" "$K")" = "600 student" ]'
|
||||
need "$K has a passphrase" '! ssh-keygen -y -P "" -f "$K"'
|
||||
need "its passphrase is Lab-Phrase-1 (le-lab check needs it)" 'ssh-keygen -y -P Lab-Phrase-1 -f "$K"'
|
||||
|
||||
task 2 "User bob with a password, and the lab key logs in as him"
|
||||
need "user bob exists" 'id bob'
|
||||
need "bob has a password: passwd -S bob shows P" '[ "$(passwd -S bob | cut -d" " -f2)" = P ]'
|
||||
need "bob's authorized_keys holds the lab key" 'grep -qF "$(cut -d" " -f2 "$K.pub")" "$BH/.ssh/authorized_keys"'
|
||||
need "ssh -i ~/.ssh/lab_key bob@localhost whoami prints bob" key_login
|
||||
|
||||
task 3 "Home directory too open: found in sshd's log, then fixed"
|
||||
need "sshd logged bad ownership or modes for directory $BH" bad_modes
|
||||
need "$BH isn't writable by group or others" '[ -d "$BH" ] && [ -z "$(find "$BH" -maxdepth 0 -perm /022)" ]'
|
||||
need "the key login works again" key_login
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.1 SSH keys"
|
||||
CHAPTER=/linux-terminal/3-securing-access/3-1-ssh-keys/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,7 @@
|
||||
if id bob >/dev/null 2>&1; then
|
||||
pkill -KILL -u bob 2>/dev/null
|
||||
userdel -r bob 2>/dev/null
|
||||
fi
|
||||
H=$(state_get home)
|
||||
[ -n "$H" ] && rm -f "$H/.ssh/lab_key" "$H/.ssh/lab_key.pub"
|
||||
true
|
||||
@@ -0,0 +1,8 @@
|
||||
id bob >/dev/null 2>&1 && refuse "user bob already exists; task 2 creates him. Remove him (userdel -r bob) and start the lab again."
|
||||
user_add student
|
||||
H=$(getent passwd student | cut -d: -f6)
|
||||
state_set home "$H"
|
||||
rm -f "$H/.ssh/lab_key" "$H/.ssh/lab_key.pub"
|
||||
state_set start "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
wc -c </var/log/auth.log >"$LAB_STATE/auth-size" 2>/dev/null || true
|
||||
true
|
||||
@@ -0,0 +1,31 @@
|
||||
# Using Linux Terminal 3.2: System-wide and per-user security settings
|
||||
|
||||
A process limit for one user, a message for every login shell, and a
|
||||
stricter umask for one user.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab creates users `alice` and `student` (password `Lab-Pass-1`).
|
||||
Work as root (`sudo -i`) for tasks 1 and 2, and as `student` (`sudo -iu
|
||||
student`) for task 3. The lab saves `/etc/profile`, `/etc/bash.bashrc` and
|
||||
the limits files, and puts them back on reset; it also removes the files
|
||||
you add to `/etc/profile.d` and `/etc/security/limits.d`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Limit the user `alice` to 50 processes, as both soft and hard limit, in
|
||||
a file of its own. `su - alice -c 'ulimit -u; ulimit -Hu'` prints `50`
|
||||
twice, and other users' limits are unchanged.
|
||||
2. Make every login shell print `Authorized use only` before the prompt,
|
||||
without editing `/etc/profile`. `su - alice -c true` prints the message.
|
||||
3. Give `student` a umask of 077 in every interactive shell: in a new
|
||||
shell, `umask` prints `0077`, and `touch ~/x; ls -l ~/x` shows
|
||||
`-rw-------`. Other users keep their umask.
|
||||
|
||||
## Hints
|
||||
|
||||
- A file in `/etc/security/limits.d/` with `alice`, `-` (or a `soft` and a
|
||||
`hard` line), `nproc` and `50`. Limits apply at the next login.
|
||||
- A `.sh` file in `/etc/profile.d/` with an `echo`.
|
||||
- `~/.bashrc` runs in every interactive bash (and Ubuntu's `~/.profile`
|
||||
runs it for login shells); `bash` starts a new one to test.
|
||||
@@ -0,0 +1,32 @@
|
||||
H=$(state_get home)
|
||||
saved=$LAB_STATE/saved
|
||||
login_run() { su - "$1" -c "$2" </dev/null 2>/dev/null; }
|
||||
# An interactive, non-login bash of USER's, started clean.
|
||||
interactive_umask() {
|
||||
local h; h=$(getent passwd "$1" | cut -d: -f6)
|
||||
runuser -u "$1" -- env -i HOME="$h" USER="$1" LOGNAME="$1" PATH=/usr/bin:/bin TERM=dumb bash -i -c umask </dev/null 2>/dev/null | tail -n1
|
||||
}
|
||||
new_files() { # DIR LIST: files in DIR that weren't there at setup
|
||||
local f
|
||||
for f in "$1"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qxF "${f##*/}" "$2" 2>/dev/null || printf '%s\n' "$f"
|
||||
done
|
||||
}
|
||||
|
||||
task 1 "alice limited to 50 processes, soft and hard, in a file of its own"
|
||||
need "su - alice -c 'ulimit -u; ulimit -Hu' prints 50 twice" '[ "$(login_run alice "ulimit -u; ulimit -Hu" | tail -n2 | paste -sd " ")" = "50 50" ]'
|
||||
need "a new file in /etc/security/limits.d sets alice's nproc" 'new_files /etc/security/limits.d "$LAB_STATE/limits-d" | xargs -r grep -hEv "^[[:space:]]*#" | grep -Eq "^[[:space:]]*alice[[:space:]].*nproc"'
|
||||
need "/etc/security/limits.conf is unchanged" 'cmp -s /etc/security/limits.conf "$saved/limits.conf"'
|
||||
need "other users are unchanged: student's ulimit -u isn't 50" '[ "$(login_run student "ulimit -u" | tail -n1)" != 50 ]'
|
||||
|
||||
task 2 "Every login shell prints Authorized use only, from /etc/profile.d"
|
||||
need "su - alice -c true prints Authorized use only" 'login_run alice true | grep -q "Authorized use only"'
|
||||
need "a new .sh file in /etc/profile.d prints it" 'new_files /etc/profile.d "$LAB_STATE/profile-d" | grep "\.sh$" | xargs -r grep -l "Authorized use only" | grep -q .'
|
||||
need "/etc/profile is unchanged" 'cmp -s /etc/profile "$saved/profile"'
|
||||
|
||||
task 3 "student's interactive shells have umask 077"
|
||||
need "a new interactive bash of student's prints umask 0077" '[ "$(interactive_umask student)" = 0077 ]'
|
||||
need "a login shell of student's has umask 0077 too" '[ "$(su - student -c "bash -i -c umask" </dev/null 2>/dev/null | tail -n1)" = 0077 ]'
|
||||
need "a file student touches is -rw-------" 'f=$H/.le-check-$$; runuser -u student -- env -i HOME="$H" PATH=/usr/bin:/bin bash -i -c "touch $f" </dev/null 2>/dev/null; m=$(stat -c %a "$f"); rm -f "$f"; [ "$m" = 600 ]'
|
||||
need "other users keep their umask: alice's isn't 0077" '[ "$(interactive_umask alice)" != 0077 ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.2 System-wide and per-user security settings"
|
||||
CHAPTER=/linux-terminal/3-securing-access/3-2-security-settings/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,14 @@
|
||||
saved=$LAB_STATE/saved
|
||||
for f in profile bash.bashrc; do
|
||||
[ -f "$saved/$f" ] && cp -a "$saved/$f" /etc/$f
|
||||
done
|
||||
[ -f "$saved/limits.conf" ] && cp -a "$saved/limits.conf" /etc/security/limits.conf
|
||||
for pair in "/etc/profile.d profile-d" "/etc/security/limits.d limits-d"; do
|
||||
set -- $pair
|
||||
[ -f "$LAB_STATE/$2" ] || continue
|
||||
for f in "$1"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qxF "${f##*/}" "$LAB_STATE/$2" || rm -f "$f"
|
||||
done
|
||||
done
|
||||
true
|
||||
@@ -0,0 +1,9 @@
|
||||
user_add alice
|
||||
user_add student
|
||||
state_set home "$(getent passwd student | cut -d: -f6)"
|
||||
saved=$LAB_STATE/saved
|
||||
mkdir -p "$saved"
|
||||
cp -a /etc/profile /etc/bash.bashrc /etc/security/limits.conf "$saved/"
|
||||
ls /etc/profile.d >"$LAB_STATE/profile-d"
|
||||
ls /etc/security/limits.d >"$LAB_STATE/limits-d"
|
||||
true
|
||||
@@ -0,0 +1,37 @@
|
||||
# Using Linux Terminal 3.3: Password complexity and expiry
|
||||
|
||||
Password rules with pam_pwquality, aging and account expiry for one user,
|
||||
and aging defaults for new accounts.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`). The lab creates users `alice` and `pwtest`
|
||||
(password `Lab-Pass-1`); try passwords out on `pwtest`, for example `echo
|
||||
'pwtest:lowercase12' | chpasswd`, so alice's settings from task 2 stay as
|
||||
you made them. The rules only apply when a password is set, so they can't
|
||||
lock anyone out of logging in. The lab saves `/etc/pam.d/common-password`,
|
||||
the pwquality settings and `/etc/login.defs`, and puts them back on reset;
|
||||
reset also removes `libpam-pwquality` again if the lab started without it,
|
||||
and user `dave` if you made him.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Install pam_pwquality and require passwords of at least 10 characters
|
||||
with at least one digit and one uppercase letter, for every user
|
||||
including root. `echo 'pwtest:lowercase12' | chpasswd` fails with "BAD
|
||||
PASSWORD: The password contains less than 1 uppercase letters", and
|
||||
`echo 'pwtest:Kettle7-Harbor' | chpasswd` succeeds.
|
||||
2. Make alice's password expire every 60 days with 10 days' warning,
|
||||
disable her account on the last day of next year, and make her choose a
|
||||
new password at her next login.
|
||||
3. Make new accounts' passwords expire after 120 days, without changing
|
||||
existing users. After `useradd -m dave`, `chage -l dave` shows maximum
|
||||
120, and `chage -l alice` is unchanged.
|
||||
|
||||
## Hints
|
||||
|
||||
- `apt install libpam-pwquality`; a file in
|
||||
`/etc/security/pwquality.conf.d/` (make the directory) with `minlen`,
|
||||
`dcredit = -1`, `ucredit = -1` and `enforce_for_root`.
|
||||
- `chage -M -W -E -d 0`; check with `chage -l alice`.
|
||||
- `PASS_MAX_DAYS` in `/etc/login.defs`.
|
||||
@@ -0,0 +1,32 @@
|
||||
sh_field() { getent shadow "$1" | cut -d: -f"$2"; }
|
||||
# try PASSWORD: root's chpasswd sets it for pwtest.
|
||||
try() { echo "pwtest:$1" | chpasswd >/dev/null 2>&1; }
|
||||
NEXT_END=$(( $(date -ud "$(( $(date +%Y) + 1 ))-12-31" +%s) / 86400 ))
|
||||
new_user_max() {
|
||||
local u=lecheck$$ m
|
||||
useradd -M -s /usr/sbin/nologin "$u" >/dev/null 2>&1 || return 1
|
||||
m=$(sh_field "$u" 5)
|
||||
userdel "$u" >/dev/null 2>&1
|
||||
[ "$m" = 120 ]
|
||||
}
|
||||
|
||||
task 1 "pam_pwquality: 10 characters, a digit and an uppercase letter, for root too"
|
||||
need "libpam-pwquality is installed" 'pkg_installed libpam-pwquality'
|
||||
need "/etc/pam.d/common-password uses pam_pwquality" 'grep -Eq "^[[:space:]]*password[[:space:]].*pam_pwquality\.so" /etc/pam.d/common-password'
|
||||
need "even root can't set xq7#lvm-pztw (no uppercase letter)" '! try "xq7#lvm-pztw"'
|
||||
need "even root can't set Xq#Lvm-PzTw (no digit)" '! try "Xq#Lvm-PzTw"'
|
||||
need "even root can't set Xq7#Lvm2Z (only 9 characters)" '! try "Xq7#Lvm2Z"'
|
||||
need "echo pwtest:lowercase12 | chpasswd fails" '! try lowercase12'
|
||||
need "echo pwtest:Kettle7-Harbor | chpasswd succeeds" 'try Kettle7-Harbor'
|
||||
|
||||
task 2 "alice: maximum 60, warning 10, account expires Dec 31 next year, change at next login"
|
||||
need "alice's maximum password age is 60 days" '[ "$(sh_field alice 5)" = 60 ]'
|
||||
need "alice is warned 10 days before" '[ "$(sh_field alice 6)" = 10 ]'
|
||||
need "alice's account expires on the last day of next year" '[ "$(sh_field alice 8)" = "$NEXT_END" ]'
|
||||
need "alice must change her password at next login" '[ "$(sh_field alice 3)" = 0 ]'
|
||||
|
||||
task 3 "New accounts' passwords expire after 120 days; existing users unchanged"
|
||||
need "/etc/login.defs sets PASS_MAX_DAYS 120" '[ "$(awk "\$1 == \"PASS_MAX_DAYS\" {v = \$2} END {print v}" /etc/login.defs)" = 120 ]'
|
||||
need "a new account gets maximum 120" new_user_max
|
||||
need "existing users are unchanged: root's and pwtest's maximum are as before" '[ "$(sh_field root 5)" = "$(cat "$LAB_STATE/max-root")" ] && [ "$(sh_field pwtest 5)" = "$(cat "$LAB_STATE/max-pwtest")" ]'
|
||||
need "alice's maximum isn't 120" '[ "$(sh_field alice 5)" != 120 ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.3 Password complexity and expiry"
|
||||
CHAPTER=/linux-terminal/3-securing-access/3-3-password-rules/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,19 @@
|
||||
saved=$LAB_STATE/saved
|
||||
if id dave >/dev/null 2>&1; then
|
||||
pkill -KILL -u dave 2>/dev/null
|
||||
userdel -r dave 2>/dev/null
|
||||
fi
|
||||
# Packages the task installed (pam-auth-update takes the line out again).
|
||||
if [ -f "$LAB_STATE/packages" ]; then
|
||||
new=$(dpkg-query -W -f '${Package}\n' | grep -Fxv -f "$LAB_STATE/packages" |
|
||||
grep -E 'pwquality|crack|^wamerican$')
|
||||
[ -n "$new" ] && DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $new >/dev/null 2>&1
|
||||
fi
|
||||
if [ -f "$saved/common-password" ]; then
|
||||
cp -a "$saved/common-password" /etc/pam.d/common-password
|
||||
cp -a "$saved/login.defs" /etc/login.defs
|
||||
if [ -f "$saved/pwquality.conf" ]; then cp -a "$saved/pwquality.conf" /etc/security/pwquality.conf; fi
|
||||
rm -rf /etc/security/pwquality.conf.d
|
||||
[ -d "$saved/pwquality.conf.d" ] && cp -a "$saved/pwquality.conf.d" /etc/security/
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,12 @@
|
||||
id dave >/dev/null 2>&1 && refuse "user dave already exists; task 3 creates him. Remove him (userdel -r dave) and start the lab again."
|
||||
user_add alice
|
||||
user_add pwtest
|
||||
saved=$LAB_STATE/saved
|
||||
mkdir -p "$saved"
|
||||
cp -a /etc/pam.d/common-password /etc/login.defs "$saved/"
|
||||
[ -f /etc/security/pwquality.conf ] && cp -a /etc/security/pwquality.conf "$saved/"
|
||||
[ -d /etc/security/pwquality.conf.d ] && cp -a /etc/security/pwquality.conf.d "$saved/"
|
||||
dpkg-query -W -f '${Package}\n' >"$LAB_STATE/packages"
|
||||
# Aging of the existing accounts, for task 3.
|
||||
for u in root pwtest; do getent shadow $u | cut -d: -f5 >"$LAB_STATE/max-$u"; done
|
||||
true
|
||||
@@ -0,0 +1,34 @@
|
||||
# Using Linux Terminal 3.4: sudo policies
|
||||
|
||||
A narrow passwordless rule in a drop-in file, an administrator by group
|
||||
membership, and the file names sudo skips.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`), and write answers to the files in
|
||||
`/root/lab-answers/`. The lab creates users `alice` and `bob` (password
|
||||
`Lab-Pass-1`). Edit sudoers files only with `visudo` (`visudo -f
|
||||
/etc/sudoers.d/NAME`): it refuses to save a file with a mistake in it, and
|
||||
a broken sudoers file stops sudo working for everyone. Keep the root shell
|
||||
open while you work. The lab saves `/etc/sudoers` and puts it back on
|
||||
reset, and removes the files you add to `/etc/sudoers.d`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Let `alice` restart and check the status of the ssh service as root,
|
||||
with no password, and nothing else. Use a drop-in file. `visudo -c`
|
||||
reports every file "parsed OK"; as alice, `sudo systemctl restart ssh`
|
||||
works, and `sudo id` asks for her password and then says she isn't
|
||||
allowed.
|
||||
2. Make `bob` an administrator who can run anything with his password,
|
||||
without editing any sudoers file.
|
||||
3. Name your drop-in `alice.conf` instead, and see what `sudo -l -U alice`
|
||||
says. Write that to `/root/lab-answers/3`, then rename the file back.
|
||||
|
||||
## Hints
|
||||
|
||||
- `alice ALL=(root) NOPASSWD: /usr/bin/systemctl restart ssh,
|
||||
/usr/bin/systemctl status ssh`; `sudo -l -U alice`; test as alice with
|
||||
`su - alice`.
|
||||
- Ubuntu's `/etc/sudoers` gives group `sudo` everything; `usermod -aG`.
|
||||
- `@includedir /etc/sudoers.d` skips names with a dot or ending in `~`.
|
||||
@@ -0,0 +1,42 @@
|
||||
SC=/usr/bin/systemctl
|
||||
may() { local u=$1; shift; sudo -l -U "$u" "$@" >/dev/null 2>&1; }
|
||||
# Drop-ins sudo reads (no dot, no ~) that weren't there at setup and name alice.
|
||||
alice_dropin() {
|
||||
local f
|
||||
for f in /etc/sudoers.d/*; do
|
||||
[ -f "$f" ] || continue
|
||||
case ${f##*/} in *.* | *~) continue ;; esac
|
||||
grep -qxF "${f##*/}" "$LAB_STATE/sudoers-d" && continue
|
||||
grep -qw alice "$f" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
only_ssh() {
|
||||
! may alice /usr/bin/id && ! may alice /bin/bash && ! may alice $SC stop ssh &&
|
||||
! may alice $SC restart cron && ! may alice $SC restart ssh.socket && ! may alice $SC edit ssh
|
||||
}
|
||||
no_password() {
|
||||
local l
|
||||
l=$(sudo -l -U alice | sed -n '/may run the following/,$p')
|
||||
grep -q NOPASSWD <<<"$l" && ! grep -Eq '(^|[^O])PASSWD:' <<<"$l" &&
|
||||
! runuser -u alice -- sudo -n $SC status ssh 2>&1 >/dev/null </dev/null | grep -qi "password is required"
|
||||
}
|
||||
|
||||
task 1 "alice may restart and check ssh as root, no password, nothing else"
|
||||
need "visudo -c reports every file parsed OK" 'visudo -c'
|
||||
need "a drop-in in /etc/sudoers.d (with no dot in its name) names alice" alice_dropin
|
||||
need "alice may run systemctl restart ssh as root" 'may alice $SC restart ssh'
|
||||
need "alice may run systemctl status ssh as root" 'may alice $SC status ssh'
|
||||
need "alice needs no password for them (NOPASSWD)" no_password
|
||||
need "alice may run nothing else (not id, a shell, or other systemctl commands)" only_ssh
|
||||
|
||||
task 2 "bob is an administrator through group sudo"
|
||||
need "groups bob includes sudo" 'id -nG bob | grep -qw sudo'
|
||||
need "sudo -l -U bob shows (ALL : ALL) ALL" 'sudo -l -U bob | grep -qF "(ALL : ALL) ALL"'
|
||||
need "bob still needs his password (no NOPASSWD)" '! sudo -l -U bob | grep -q NOPASSWD'
|
||||
need "no sudoers file names bob" '! grep -rqw bob /etc/sudoers /etc/sudoers.d'
|
||||
|
||||
task 3 "sudo skips alice.conf: its answer saved, file renamed back"
|
||||
need "/root/lab-answers/3 has what sudo said: alice is not allowed to run sudo" 'grep -qi "not allowed to run sudo" /root/lab-answers/3'
|
||||
need "no file with a dot (or ending in ~) in /etc/sudoers.d names alice" '! find /etc/sudoers.d -type f \( -name "*.*" -o -name "*~" \) -exec grep -qw alice {} \; -print | grep -q .'
|
||||
need "alice's drop-in is back in use" 'alice_dropin && may alice $SC restart ssh'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.4 sudo policies"
|
||||
CHAPTER=/linux-terminal/3-securing-access/3-4-sudo-policies/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,6 @@
|
||||
for f in /etc/sudoers.d/* /etc/sudoers.d/.*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qxF "${f##*/}" "$LAB_STATE/sudoers-d" 2>/dev/null || rm -f "$f"
|
||||
done
|
||||
[ -f "$LAB_STATE/sudoers" ] && cp -a "$LAB_STATE/sudoers" /etc/sudoers
|
||||
true
|
||||
@@ -0,0 +1,6 @@
|
||||
visudo -c >/dev/null 2>&1 || refuse "visudo -c finds a problem in the sudoers files; fix it and start the lab again."
|
||||
user_add alice
|
||||
user_add bob
|
||||
cp -a /etc/sudoers "$LAB_STATE/sudoers"
|
||||
ls -A /etc/sudoers.d >"$LAB_STATE/sudoers-d"
|
||||
true
|
||||
@@ -0,0 +1,33 @@
|
||||
# Using Linux Terminal 3.5: Users and groups
|
||||
|
||||
Creating a user with useradd, adding groups, locking an account, and
|
||||
removing one and finding what it left behind.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`), and write answers to the files in
|
||||
`/root/lab-answers/`. The lab checks that user `hana`, UID 1500 and group
|
||||
`qa` don't exist yet. It creates user `lou`, who has files outside his
|
||||
home directory. Task 4 removes `lou` rather than `hana`, so that hana stays
|
||||
for grading. `le-lab reset` removes hana, `qa` and lou's files.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. With useradd, create `hana` with a home directory, bash, the comment
|
||||
"Hana Ito" and UID 1500, in a new group `qa` as well as her own. Set her
|
||||
a password.
|
||||
2. Add hana to `adm` without removing her from `qa`.
|
||||
3. Stop hana logging in by any means, without deleting anything: `passwd
|
||||
-S hana` shows `L`, `chage -l hana` shows an account expiry date in the
|
||||
past, and `su - hana` fails.
|
||||
4. Remove `lou`'s account and home directory, then find anything he left
|
||||
on the root filesystem, and write the paths to `/root/lab-answers/4`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `groupadd`; `useradd -m -s -c -u -G`; `passwd`; check with `getent
|
||||
passwd hana`, `id hana`, `passwd -S hana`.
|
||||
- `usermod -aG`: without `-a`, `-G` replaces the list.
|
||||
- `usermod -L` (or `passwd -l`) locks the password only; `usermod -e 1` or
|
||||
`chage -E 0` expires the account, which also stops SSH keys.
|
||||
- `userdel -r`, then `find / -xdev -nouser`.
|
||||
@@ -0,0 +1,29 @@
|
||||
pw() { getent passwd hana | cut -d: -f"$1"; }
|
||||
today=$(( $(date -u +%s) / 86400 ))
|
||||
ans4() { cat /root/lab-answers/4 2>/dev/null; }
|
||||
|
||||
task 1 "hana: UID 1500, Hana Ito, /home/hana, bash, in qa, with a password"
|
||||
need "user hana exists" 'id hana'
|
||||
need "getent passwd hana shows UID 1500" '[ "$(pw 3)" = 1500 ]'
|
||||
need "her comment is Hana Ito" '[ "$(pw 5 | cut -d, -f1)" = "Hana Ito" ]'
|
||||
need "her home is /home/hana, and it exists" '[ "$(pw 6)" = /home/hana ] && [ -d /home/hana ] && [ "$(stat -c %U /home/hana)" = hana ]'
|
||||
need "her shell is /bin/bash" '[ "$(pw 7)" = /bin/bash ]'
|
||||
need "she has a group of her own" 'getent group hana | grep -q . && [ "$(id -gn hana)" = hana ]'
|
||||
need "id hana lists qa" 'id -nG hana | grep -qw qa'
|
||||
need "she has a password set" 'getent shadow hana | cut -d: -f2 | grep -Eq "^!?\\$"'
|
||||
|
||||
task 2 "hana in adm as well as qa"
|
||||
need "id hana lists adm" 'id -nG hana | grep -qw adm'
|
||||
need "id hana still lists qa" 'id -nG hana | grep -qw qa'
|
||||
|
||||
task 3 "hana can't log in by any means; nothing deleted"
|
||||
need "user hana and /home/hana still exist" 'id hana && [ -d /home/hana ]'
|
||||
need "passwd -S hana shows L" '[ "$(passwd -S hana | cut -d" " -f2)" = L ]'
|
||||
need "chage -l hana shows an account expiry date in the past" 'e=$(getent shadow hana | cut -d: -f8); [ -n "$e" ] && [ "$e" -le "$today" ]'
|
||||
need "su - hana fails" '! su - hana -c true </dev/null'
|
||||
|
||||
task 4 "lou removed with his home; what he left in /root/lab-answers/4"
|
||||
need "lou's account is gone" '! getent passwd lou'
|
||||
need "/home/lou is gone" '[ ! -e /home/lou ]'
|
||||
need "/root/lab-answers/4 lists /var/tmp/lou-build.log" 'ans4 | grep -qF /var/tmp/lou-build.log'
|
||||
need "/root/lab-answers/4 lists /srv/lou-reports" 'ans4 | grep -qF /srv/lou-reports'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.5 Users and groups"
|
||||
CHAPTER=/linux-terminal/3-securing-access/3-5-users-and-groups/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,7 @@
|
||||
if id hana >/dev/null 2>&1; then
|
||||
pkill -KILL -u hana 2>/dev/null
|
||||
userdel -r hana 2>/dev/null
|
||||
fi
|
||||
groupdel qa 2>/dev/null
|
||||
rm -rf /var/tmp/lou-build.log /srv/lou-reports
|
||||
true
|
||||
@@ -0,0 +1,14 @@
|
||||
id hana >/dev/null 2>&1 && refuse "user hana already exists; task 1 creates her. Remove her (userdel -r hana) and start the lab again."
|
||||
getent passwd 1500 >/dev/null && refuse "UID 1500 is already taken (by $(getent passwd 1500 | cut -d: -f1)); task 1 needs it."
|
||||
getent group qa >/dev/null && refuse "group qa already exists; task 1 creates it. Remove it and start the lab again."
|
||||
id lou >/dev/null 2>&1 && refuse "user lou already exists; the lab creates him. Remove him and start the lab again."
|
||||
user_add lou -c "Lou Park"
|
||||
# Files lou leaves outside his home directory. Written by root first,
|
||||
# then given to lou: root can't write to another user's file in a sticky
|
||||
# directory such as /var/tmp (fs.protected_regular).
|
||||
rm -rf /var/tmp/lou-build.log /srv/lou-reports
|
||||
echo "build 41 finished" >/var/tmp/lou-build.log
|
||||
mkdir -p /srv/lou-reports
|
||||
echo "quarter,total" >/srv/lou-reports/q3.csv
|
||||
chown lou:lou /var/tmp/lou-build.log /srv/lou-reports /srv/lou-reports/q3.csv
|
||||
true
|
||||
@@ -0,0 +1,33 @@
|
||||
# Using Linux Terminal 3.6: Ownership and permissions
|
||||
|
||||
A shared group directory with setgid and the sticky bit, private files in
|
||||
octal, and finding setuid programs.
|
||||
|
||||
## Before you start
|
||||
|
||||
Work as root (`sudo -i`) for tasks 1 and 3, and as `student` (`sudo -iu
|
||||
student`) for task 2. Write answers to the files in `/root/lab-answers/`.
|
||||
The lab creates users `ana`, `ben` and `student` (password `Lab-Pass-1`),
|
||||
and checks that `/srv/finance` and group `finance` don't exist yet;
|
||||
`le-lab reset` removes them.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Create `/srv/finance` owned by root and the group `finance` (create the
|
||||
group, with `ana` and `ben` as its members), where members can create
|
||||
and edit files, new files belong to `finance`, members can't delete each
|
||||
other's files, and others have no access. `ls -ld /srv/finance` shows
|
||||
`drwxrws--T root finance` (mode 3770).
|
||||
2. As `student`, make `~/private` a directory only you can enter, and
|
||||
`~/private/notes.txt` a file only you can read and write, using octal.
|
||||
3. List every setuid file on the root filesystem, find the one that lets
|
||||
users change their own password, and write its full path to
|
||||
`/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `groupadd`, `usermod -aG` (or `gpasswd -a`); `chown root:finance`,
|
||||
`chmod 3770`. Test with `su - ana -c 'touch /srv/finance/a'` and `su -
|
||||
ben -c 'rm /srv/finance/a'`.
|
||||
- `chmod 700`, `chmod 600`; `stat -c '%a %n'`.
|
||||
- `find / -xdev -perm -4000 -type f 2>/dev/null`; `ls -l` shows the `s`.
|
||||
@@ -0,0 +1,27 @@
|
||||
H=$(state_get home)
|
||||
F=/srv/finance
|
||||
# shared_dir_works: ana's new file gets group finance, ben can create one
|
||||
# too but can't remove ana's.
|
||||
shared_dir_works() {
|
||||
local a=$F/.le-check-ana-$$ b=$F/.le-check-ben-$$ r=1
|
||||
runuser -u ana -- touch "$a" 2>/dev/null && [ "$(stat -c %G "$a")" = finance ] &&
|
||||
runuser -u ben -- touch "$b" 2>/dev/null &&
|
||||
! runuser -u ben -- rm -f "$a" 2>/dev/null && [ -e "$a" ] && r=0
|
||||
rm -f "$a" "$b"
|
||||
return $r
|
||||
}
|
||||
|
||||
task 1 "/srv/finance: root:finance, mode 3770, shared by ana and ben"
|
||||
need "group finance exists" 'getent group finance'
|
||||
need "ana and ben are members of finance" 'id -nG ana | grep -qw finance && id -nG ben | grep -qw finance'
|
||||
need "$F is a directory owned by root and group finance" '[ -d $F ] && [ "$(stat -c "%U %G" $F)" = "root finance" ]'
|
||||
need "ls -ld $F shows drwxrws--T (mode 3770)" '[ "$(stat -c %a $F)" = 3770 ]'
|
||||
need "a member's new file belongs to finance, and the other member can't delete it" shared_dir_works
|
||||
|
||||
task 2 "~/private is 700 and ~/private/notes.txt is 600, owned by student"
|
||||
need "$H/private is a directory with mode 700" '[ -d "$H/private" ] && [ "$(stat -c %a "$H/private")" = 700 ]'
|
||||
need "$H/private/notes.txt is a file with mode 600" '[ -f "$H/private/notes.txt" ] && [ "$(stat -c %a "$H/private/notes.txt")" = 600 ]'
|
||||
need "both belong to student" '[ "$(stat -c %U "$H/private" "$H/private/notes.txt" | sort -u)" = student ]'
|
||||
|
||||
task 3 "The setuid program for changing passwords in /root/lab-answers/3"
|
||||
need "/root/lab-answers/3 holds /usr/bin/passwd" '[ "$(answer 3)" = /usr/bin/passwd ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.6 Ownership and permissions"
|
||||
CHAPTER=/linux-terminal/3-securing-access/3-6-ownership-and-permissions/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,6 @@
|
||||
rm -rf /srv/finance
|
||||
H=$(state_get home)
|
||||
[ -n "$H" ] && rm -rf "$H/private"
|
||||
for u in ana ben; do gpasswd -d "$u" finance >/dev/null 2>&1; done
|
||||
groupdel finance 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,9 @@
|
||||
[ -e /srv/finance ] && refuse "/srv/finance already exists; task 1 creates it. Remove it and start the lab again."
|
||||
getent group finance >/dev/null && refuse "group finance already exists; task 1 creates it. Remove it and start the lab again."
|
||||
user_add ana
|
||||
user_add ben
|
||||
user_add student
|
||||
H=$(getent passwd student | cut -d: -f6)
|
||||
state_set home "$H"
|
||||
rm -rf "$H/private"
|
||||
true
|
||||
@@ -1,12 +1,7 @@
|
||||
if [ -f "$LAB_STATE/ssh.tar" ]; then
|
||||
rm -f /etc/ssh/sshd_config.d/* /etc/ssh/revoked.krl /etc/ssh/*-cert.pub
|
||||
tar -C /etc -xf "$LAB_STATE/ssh.tar"
|
||||
if is_rhel; then
|
||||
systemctl restart sshd
|
||||
else
|
||||
systemctl restart ssh.socket 2>/dev/null
|
||||
systemctl restart ssh.service 2>/dev/null
|
||||
fi
|
||||
sshd_restart
|
||||
fi
|
||||
rm -rf /root/sshca /srv/jail /usr/local/bin/lab-python
|
||||
pkg_installed usbguard && [ ! -f "$LAB_STATE/usbguard.tar" ] && systemctl disable --now --quiet usbguard 2>/dev/null
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Ubuntu Desktop 1.1: Network connections
|
||||
|
||||
Making a NetworkManager profile with a fixed address, switching it to
|
||||
DHCP while keeping a DNS server, and seeing where Ubuntu stores it. The
|
||||
lab works on a spare interface, never on the one you're connected over.
|
||||
The chapter's Wi-Fi task needs Wi-Fi hardware and isn't part of the lab.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab adds a spare wired interface `lab0` (a dummy interface that
|
||||
comes back at every boot). If NetworkManager isn't installed, as on
|
||||
Ubuntu Server, the lab installs it; it leaves the interfaces it already
|
||||
finds alone, and reset removes it again. Don't activate the profile.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Create a profile `office` for interface `lab0` with the fixed address
|
||||
10.10.10.50/24, gateway 10.10.10.1 and DNS 10.10.10.1, without
|
||||
activating it.
|
||||
2. Make the profile use DHCP for its address but keep the DNS server
|
||||
10.10.10.1. Task 1 stays passed once you've done this.
|
||||
|
||||
When `le-lab check` passes, remove the profile as the chapter shows, or
|
||||
leave that to `le-lab reset`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `nmcli connection add type ethernet ifname lab0 con-name office
|
||||
ipv4.method manual ipv4.addresses ... ipv4.gateway ... ipv4.dns ...
|
||||
connection.autoconnect no`. `nmcli -g ipv4.method,ipv4.addresses
|
||||
connection show office` prints `manual` and `10.10.10.50/24`, and `sudo
|
||||
ls /etc/netplan/` shows a new `90-NM-` file.
|
||||
- `nmcli connection modify office ipv4.method auto`, clearing the old
|
||||
address with `ipv4.addresses "" ipv4.gateway ""`, and
|
||||
`ipv4.ignore-auto-dns yes` so DHCP's DNS servers aren't used.
|
||||
- `nmcli connection delete office`.
|
||||
@@ -0,0 +1,18 @@
|
||||
get() { nmcli -g "$1" connection show office 2>/dev/null; }
|
||||
uuid=$(get connection.uuid)
|
||||
is_task2() { [ "$(get ipv4.method)" = auto ] && [ -z "$(get ipv4.addresses)" ] && [ -z "$(get ipv4.gateway)" ] && [ "$(get ipv4.ignore-auto-dns)" = yes ]; }
|
||||
|
||||
task 1 "Profile office for lab0: 10.10.10.50/24, not active"
|
||||
need "a profile office exists (nmcli connection show)" '[ -n "$uuid" ]'
|
||||
need "it's for interface lab0" '[ "$(get connection.interface-name)" = lab0 ]'
|
||||
need "it has the address 10.10.10.50/24 and gateway 10.10.10.1 (or DHCP after task 2)" '{ [ "$(get ipv4.method)" = manual ] && [ "$(get ipv4.addresses)" = 10.10.10.50/24 ] && [ "$(get ipv4.gateway)" = 10.10.10.1 ]; } || is_task2'
|
||||
need "its DNS server is 10.10.10.1" '[ "$(get ipv4.dns)" = 10.10.10.1 ]'
|
||||
need "it isn't active" '! nmcli -g NAME connection show --active | grep -qx office'
|
||||
need "it's stored as /etc/netplan/90-NM-$uuid.yaml" '[ -f "/etc/netplan/90-NM-$uuid.yaml" ]'
|
||||
|
||||
task 2 "office uses DHCP but keeps DNS 10.10.10.1"
|
||||
need "a profile office exists" '[ -n "$uuid" ]'
|
||||
need "ipv4.method is auto" '[ "$(get ipv4.method)" = auto ]'
|
||||
need "the old address and gateway are cleared" '[ -z "$(get ipv4.addresses)" ] && [ -z "$(get ipv4.gateway)" ]'
|
||||
need "DNS servers from DHCP are ignored (ipv4.ignore-auto-dns yes)" '[ "$(get ipv4.ignore-auto-dns)" = yes ]'
|
||||
need "the DNS server is still 10.10.10.1" '[ "$(get ipv4.dns)" = 10.10.10.1 ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.1 Network connections"
|
||||
CHAPTER=/ubuntu-desktop/1-networking/1-1-network-connections/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,17 @@
|
||||
if command -v nmcli >/dev/null; then
|
||||
nmcli -g UUID,NAME connection show 2>/dev/null | while IFS=: read -r uuid name; do
|
||||
[ "$name" = office ] || [ "$(nmcli -g connection.interface-name connection show "$uuid" 2>/dev/null)" = lab0 ] || continue
|
||||
nmcli connection delete "$uuid" >/dev/null 2>&1
|
||||
rm -f "/etc/netplan/90-NM-$uuid.yaml"
|
||||
done
|
||||
fi
|
||||
if [ -f /etc/systemd/system/le-lab-lab0.service ]; then
|
||||
systemctl disable --now --quiet le-lab-lab0.service 2>/dev/null
|
||||
rm -f /etc/systemd/system/le-lab-lab0.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
ip link del lab0 2>/dev/null
|
||||
if [ -s "$LAB_STATE/pkgs-added" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $(cat "$LAB_STATE/pkgs-added") >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,34 @@
|
||||
installed_pkgs() { dpkg-query -W -f '${db:Status-Abbrev} ${Package}\n' 2>/dev/null | awk '$1 ~ /^.i/ {print $2}' | sort -u; }
|
||||
[ -e /sys/class/net/lab0 ] && refuse "an interface lab0 already exists; remove it first"
|
||||
if command -v nmcli >/dev/null; then
|
||||
systemctl is-active --quiet NetworkManager || refuse "NetworkManager is installed but not running; start it first"
|
||||
nmcli -g NAME connection show | grep -qx office && refuse "a NetworkManager profile office already exists; delete it first"
|
||||
nmcli -g connection.interface-name connection show 2>/dev/null | grep -qx lab0 && refuse "a NetworkManager profile for lab0 already exists; delete it first"
|
||||
else
|
||||
# Ubuntu Server: NetworkManager ships with every wired interface
|
||||
# unmanaged unless netplan hands it over, so the connection stays
|
||||
# with systemd-networkd.
|
||||
installed_pkgs >"$LAB_STATE/pkgs-before"
|
||||
pkg_install network-manager
|
||||
comm -13 "$LAB_STATE/pkgs-before" <(installed_pkgs) >"$LAB_STATE/pkgs-added"
|
||||
systemctl enable --now --quiet NetworkManager
|
||||
for i in $(seq 20); do nmcli general >/dev/null 2>&1 && break; sleep 0.5; done
|
||||
fi
|
||||
|
||||
cat >/etc/systemd/system/le-lab-lab0.service <<'UNIT'
|
||||
[Unit]
|
||||
Description=Spare interface lab0 for a LINUXexpert practice lab
|
||||
Before=network-pre.target NetworkManager.service
|
||||
Wants=network-pre.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/sh -c 'ip link show lab0 >/dev/null 2>&1 || ip link add lab0 type dummy'
|
||||
ExecStop=/bin/sh -c 'ip link del lab0 2>/dev/null || true'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now --quiet le-lab-lab0.service
|
||||
@@ -0,0 +1,38 @@
|
||||
# Ubuntu Desktop 1.2: Layer 2: MAC addresses, ARP and broadcast
|
||||
|
||||
Finding MAC addresses and the maker's prefix, watching an ARP exchange,
|
||||
and checking that an address is free before you use it.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab sets up a second machine, the peer, at `10.99.0.2` (name `peer`).
|
||||
It's on the same network as this VM's interface `le-host` (`10.99.0.1`),
|
||||
so the two can talk directly over Ethernet. Two of the addresses
|
||||
`10.99.0.21`, `10.99.0.22` and `10.99.0.23` are also in use on that
|
||||
network; one is free. `iputils-arping` isn't installed. Write answers to
|
||||
the files in `/root/lab-answers/`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Find your machine's MAC address and the maker's prefix in it, and your
|
||||
default gateway's MAC address. Use the interface your default route
|
||||
goes out of. Write your MAC on line 1 of `/root/lab-answers/1`, the
|
||||
prefix (its first three bytes) on line 2, and the gateway's MAC on
|
||||
line 3.
|
||||
2. Empty the neighbor table, then watch the ARP exchange as you ping the
|
||||
peer. Write the peer's MAC address, from its reply, to
|
||||
`/root/lab-answers/2`.
|
||||
3. Check which of `10.99.0.21`, `10.99.0.22` and `10.99.0.23` is free
|
||||
before giving it to a new machine (install `iputils-arping` first).
|
||||
Write the free address to `/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ip -br link`; `ip route | grep default` gives the gateway's IP; after
|
||||
`ping -c1` to it, `ip neigh show` lists it with `lladdr` and its MAC.
|
||||
- `sudo ip neigh flush dev le-host`, then `sudo tcpdump -i le-host -e -n
|
||||
arp` in one terminal and `ping -c1 10.99.0.2` in another: look for
|
||||
`Request who-has` to `ff:ff:ff:ff:ff:ff` and `Reply ... is-at`.
|
||||
- `sudo arping -D -c 2 -I le-host ADDRESS; echo $?` prints `Received 0
|
||||
response(s)` and `0` for a free address, and a reply and `1` for one in
|
||||
use.
|
||||
@@ -0,0 +1,21 @@
|
||||
norm() { tr A-Z a-z | tr -d '[:space:]' | tr '-' ':'; }
|
||||
line() { sed -n "${2}p" "/root/lab-answers/$1" 2>/dev/null | norm; }
|
||||
dev=$(ip route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") {print $(i + 1); exit}}')
|
||||
gw=$(ip route show default | awk '{print $3; exit}')
|
||||
mac=$(cat "/sys/class/net/$dev/address" 2>/dev/null)
|
||||
gw_mac() {
|
||||
ping -c1 -W2 "$gw" >/dev/null 2>&1
|
||||
ip neigh show "$gw" dev "$dev" | awk '{for (i = 1; i < NF; i++) if ($i == "lladdr") print $(i + 1)}' | head -n1
|
||||
}
|
||||
|
||||
task 1 "Your MAC, its maker's prefix, and the gateway's MAC"
|
||||
need "line 1 of /root/lab-answers/1 is the MAC of $dev" '[ -n "$mac" ] && [ "$(line 1 1)" = "$mac" ]'
|
||||
need "line 2 is the maker's prefix (its first three bytes)" '[ "$(line 1 2 | tr -d :)" = "$(printf %s "$mac" | cut -c1-8 | tr -d :)" ]'
|
||||
need "line 3 is the MAC of the gateway $gw" 'g=$(gw_mac); [ -n "$g" ] && [ "$(line 1 3)" = "$g" ]'
|
||||
|
||||
task 2 "The peer's MAC from its ARP reply"
|
||||
need "/root/lab-answers/2 is the peer's MAC" '[ "$(line 2 1)" = "$(in_peer cat /sys/class/net/le-peer/address)" ]'
|
||||
|
||||
task 3 "The free address found with arping"
|
||||
need "iputils-arping is installed" 'pkg_installed iputils-arping'
|
||||
need "/root/lab-answers/3 is the free address" '[ "$(line 3 1)" = "$(state_get free)" ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.2 Layer 2: MAC addresses, ARP and broadcast"
|
||||
CHAPTER=/ubuntu-desktop/1-networking/1-2-layer-2/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,11 @@
|
||||
if [ -f /etc/systemd/system/le-lab-l2.service ]; then
|
||||
systemctl disable --quiet le-lab-l2.service 2>/dev/null
|
||||
rm -f /etc/systemd/system/le-lab-l2.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
if [ -n "$(state_get had-arping)" ]; then
|
||||
pkg_installed iputils-arping || DEBIAN_FRONTEND=noninteractive apt-get -y -qq install iputils-arping >/dev/null 2>&1
|
||||
elif pkg_installed iputils-arping; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge iputils-arping >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,28 @@
|
||||
[ -e /etc/systemd/system/le-lab-l2.service ] && refuse "le-lab-l2.service exists; run le-lab reset first"
|
||||
ip route show default | grep -q . || refuse "this VM has no default route; task 1 needs one"
|
||||
if pkg_installed iputils-arping; then
|
||||
state_set had-arping 1
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge iputils-arping >/dev/null
|
||||
fi
|
||||
peer_up
|
||||
# The peer gets a fixed MAC address and two of the three candidate
|
||||
# addresses, again at every boot.
|
||||
free=$((21 + RANDOM % 3))
|
||||
state_set free "10.99.0.$free"
|
||||
used=$(for n in 21 22 23; do [ "$n" = "$free" ] || printf ' 10.99.0.%s/24' "$n"; done)
|
||||
cat >/etc/systemd/system/le-lab-l2.service <<UNIT
|
||||
[Unit]
|
||||
Description=LINUXexpert practice lab: addresses on the peer
|
||||
After=le-lab-peer.service
|
||||
Requires=le-lab-peer.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/sh -c 'ip -n lepeer link set le-peer address 02:4c:45:00:00:02; for a in$used; do ip -n lepeer addr add \$\$a dev le-peer 2>/dev/null; done; true'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now --quiet le-lab-l2.service
|
||||
@@ -0,0 +1,31 @@
|
||||
# Ubuntu Desktop 2.1: LTS releases and version numbers
|
||||
|
||||
Finding this system's release and point release, when its support ends,
|
||||
and which Ubuntu releases are still supported.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab installs `distro-info` if it's missing and changes nothing else.
|
||||
Write your answers to the files in `/root/lab-answers/`, one item per
|
||||
line.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Find this system's release, code name and point release. Write the
|
||||
point release (such as `24.04.5`) on line 1 of `/root/lab-answers/1`
|
||||
and the code name on line 2.
|
||||
2. Find the date standard support ends for this release, and for the
|
||||
latest LTS. Write them on lines 1 and 2 of `/root/lab-answers/2`, as
|
||||
`YYYY-MM-DD` (or at least the month, such as `May 2029`).
|
||||
3. List every Ubuntu release still supported today, and say which of them
|
||||
are LTS releases. Write one release per line to `/root/lab-answers/3`
|
||||
as its version number, followed by `LTS` for the LTS ones (such as
|
||||
`24.04 LTS`). Leave out the release still in development.
|
||||
|
||||
## Hints
|
||||
|
||||
- `lsb_release -a`, `/etc/os-release`.
|
||||
- The `eol` column of `/usr/share/distro-info/ubuntu.csv`;
|
||||
`hwe-support-status --verbose`; `ubuntu-distro-info --lts`.
|
||||
- `ubuntu-distro-info --supported --fullname`, and `--devel` for the
|
||||
release still being developed.
|
||||
@@ -0,0 +1,36 @@
|
||||
line() { sed -n "${2}p" "/root/lab-answers/$1" 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; }
|
||||
csv=/usr/share/distro-info/ubuntu.csv
|
||||
. /etc/os-release
|
||||
eol_of() { awk -F, -v s="$1" '$3 == s {print $6}' "$csv"; }
|
||||
# month LINE: YYYY-MM from an answer such as 2029-05-31 or May 2029.
|
||||
month() {
|
||||
local a=$1
|
||||
if [[ $a =~ ^([0-9]{4}-[0-9]{2}) ]]; then printf '%s' "${BASH_REMATCH[1]}"; return; fi
|
||||
date -d "1 $a" +%Y-%m 2>/dev/null
|
||||
}
|
||||
# The supported releases as "VERSION" or "VERSION LTS", sorted.
|
||||
expected() {
|
||||
local skip=${1:-}
|
||||
ubuntu-distro-info --supported --fullname 2>/dev/null | while read -r _ ver rest; do
|
||||
[ -n "$skip" ] && [ "$(ubuntu-distro-info --series="$skip" -r 2>/dev/null | cut -d' ' -f1)" = "$ver" ] && continue
|
||||
case $rest in LTS*) echo "$ver LTS" ;; *) echo "$ver" ;; esac
|
||||
done | sort
|
||||
}
|
||||
given() {
|
||||
grep -v '^[[:space:]]*$' /root/lab-answers/3 2>/dev/null | while read -r l; do
|
||||
v=$(grep -Eo '[0-9]{2}\.[0-9]{2}' <<<"$l" | head -n1)
|
||||
[ -n "$v" ] || continue
|
||||
if grep -qi 'lts' <<<"$l"; then echo "$v LTS"; else echo "$v"; fi
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
task 1 "This system's point release and code name"
|
||||
need "line 1 of /root/lab-answers/1 is the point release ($VERSION_ID.x)" '[ "$(line 1 1)" = "${VERSION%% *}" ]'
|
||||
need "line 2 is the code name" 'line 1 2 | tr A-Z a-z | grep -qw "$VERSION_CODENAME"'
|
||||
|
||||
task 2 "End of standard support: this release and the latest LTS"
|
||||
need "line 1 of /root/lab-answers/2 is this release's end of support" '[ "$(month "$(line 2 1)")" = "$(eol_of "$VERSION_CODENAME" | cut -c1-7)" ]'
|
||||
need "line 2 is the latest LTS's end of support" '[ "$(month "$(line 2 2)")" = "$(eol_of "$(ubuntu-distro-info --lts)" | cut -c1-7)" ]'
|
||||
|
||||
task 3 "Every supported release, LTS ones marked"
|
||||
need "/root/lab-answers/3 lists exactly the supported releases, with LTS marked and the development release left out" '[ "$(given)" = "$(expected "$(ubuntu-distro-info --devel 2>/dev/null)")" ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.1 LTS releases and version numbers"
|
||||
CHAPTER=/ubuntu-desktop/2-open-source-community/2-1-lts-releases/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,4 @@
|
||||
if [ -n "$(state_get added-distro-info)" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge distro-info >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,4 @@
|
||||
if ! command -v ubuntu-distro-info >/dev/null; then
|
||||
pkg_install distro-info
|
||||
state_set added-distro-info 1
|
||||
fi
|
||||
@@ -0,0 +1,30 @@
|
||||
# Ubuntu Desktop 2.3: Open source licenses
|
||||
|
||||
Reading the licenses of installed packages, telling copyleft from
|
||||
permissive, and finding which archive component a package comes from.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab changes nothing. Write your answers to the files in
|
||||
`/root/lab-answers/`, one item per line.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Find the license of the `bash` package and of the `openssl` package,
|
||||
and say which of them could be built into a closed-source product.
|
||||
Write bash's main license on line 1 of `/root/lab-answers/1`,
|
||||
openssl's on line 2, and the package that could be built in on line 3.
|
||||
2. Find which component `ubuntu-drivers-common` comes from, and which
|
||||
component NVIDIA's driver `nvidia-driver-535` is in. Write them on
|
||||
lines 1 and 2 of `/root/lab-answers/2`.
|
||||
3. Count how many installed packages carry a GPL license of any version,
|
||||
and write the number to `/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `grep '^License:' /usr/share/doc/PACKAGE/copyright`; a few files in a
|
||||
package can have other licenses than the main one.
|
||||
- `apt-cache show PACKAGE | grep -m1 Section`: a section with no prefix
|
||||
is in `main`, otherwise the prefix is the component.
|
||||
- `grep -l` over `/usr/share/doc/*/copyright`, then `wc -l`. Rough
|
||||
counts are fine.
|
||||
@@ -0,0 +1,15 @@
|
||||
line() { sed -n "${2}p" "/root/lab-answers/$1" 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | tr A-Z a-z; }
|
||||
component() { local s; s=$(apt-cache show "$1" 2>/dev/null | awk '$1 == "Section:" {print $2; exit}'); [ -n "$s" ] || return; case $s in */*) printf '%s' "${s%%/*}" ;; *) printf main ;; esac; }
|
||||
gpl_count() { grep -l '^License: .*GPL' /usr/share/doc/*/copyright 2>/dev/null | wc -l; }
|
||||
|
||||
task 1 "Licenses of bash and openssl"
|
||||
need "line 1 of /root/lab-answers/1 is bash's license (GPL version 3 or later)" 'line 1 1 | grep -Eq "gpl.?(v)?3"'
|
||||
need "line 2 is openssl's license (Apache 2.0)" 'line 1 2 | grep -Eq "apache"'
|
||||
need "line 3 is the package that could be built into a closed-source product" '[ "$(line 1 3)" = openssl ]'
|
||||
|
||||
task 2 "Components of ubuntu-drivers-common and nvidia-driver-535"
|
||||
need "line 1 of /root/lab-answers/2 is ubuntu-drivers-common's component" 'c=$(component ubuntu-drivers-common); [ -n "$c" ] && [ "$(line 2 1)" = "$c" ]'
|
||||
need "line 2 is nvidia-driver-535's component" 'c=$(component nvidia-driver-535); [ -n "$c" ] && [ "$(line 2 2)" = "$c" ]'
|
||||
|
||||
task 3 "Number of installed packages under a GPL license"
|
||||
need "/root/lab-answers/3 has the count (within 10%)" 'a=$(line 3 1); n=$(gpl_count); [[ $a =~ ^[0-9]+$ ]] && [ $((a * 10)) -ge $((n * 9)) ] && [ $((a * 10)) -le $((n * 11)) ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.3 Open source licenses"
|
||||
CHAPTER=/ubuntu-desktop/2-open-source-community/2-3-open-source-licenses/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1 @@
|
||||
true
|
||||
@@ -0,0 +1,2 @@
|
||||
# Nothing to set up: every task is a finding.
|
||||
true
|
||||
@@ -0,0 +1,45 @@
|
||||
# Ubuntu Desktop 3.1: Boot problems: GRUB 2 and the initramfs
|
||||
|
||||
Making the GRUB menu show at every boot with kernel messages instead of
|
||||
the logo, recovery mode, getting a system back from `grub rescue>`, and
|
||||
rebuilding the initramfs. Tasks 2 and 3 need the VM's console.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab saves `/etc/default/grub` and `/etc/default/grub.d/` so reset can
|
||||
put them back (and runs `update-grub` if they changed). It changes
|
||||
nothing else. On cloud images,
|
||||
`/etc/default/grub.d/50-cloudimg-settings.cfg` is read after
|
||||
`/etc/default/grub` and sets `GRUB_TIMEOUT=0`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Make the GRUB menu appear for 10 seconds at every boot, and make every
|
||||
boot show kernel messages instead of the logo (no `quiet` or `splash`
|
||||
on the kernel command line). Reboot to see it.
|
||||
2. Boot once into recovery mode, get a root shell, and make sure `/` is
|
||||
writable. Not graded: in that shell, `cat /proc/cmdline` contains
|
||||
`recovery`, `systemctl is-active friendly-recovery.service` prints
|
||||
`active`, and `touch /root/recovery-test` succeeds.
|
||||
3. Rename `/boot/grub` to `/boot/grub.bak`, reboot, and get the system
|
||||
booted from `grub rescue>`. Then repair it for good. Not graded: after
|
||||
`sudo mv /boot/grub.bak /boot/grub`, `sudo grub-install DISK` prints
|
||||
`Installation finished. No error reported.`, and the next reboot shows
|
||||
the normal menu. Take a snapshot of the VM before this one.
|
||||
4. Rebuild the initramfs of every installed kernel.
|
||||
|
||||
## Hints
|
||||
|
||||
- `GRUB_TIMEOUT_STYLE=menu`, `GRUB_TIMEOUT=10` and
|
||||
`GRUB_CMDLINE_LINUX_DEFAULT`, in `/etc/default/grub` or a file in
|
||||
`/etc/default/grub.d/` that sorts after the cloud image's; then `sudo
|
||||
update-grub`. After a reboot, `cat /proc/cmdline`.
|
||||
- In the menu: Advanced options, then the `(recovery mode)` entry, then
|
||||
`root`. `mount -o remount,rw /` if `/` is read-only.
|
||||
- At `grub rescue>`: `set` shows the prefix GRUB looked in; `ls
|
||||
(hd0,gpt16)/` finds `grub.bak` (on cloud images `/boot` is its own
|
||||
partition). Then `set prefix=(hd0,gpt16)/grub.bak`, `set
|
||||
root=(hd0,gpt16)`, `insmod normal`, `normal`. `lsblk` shows the disk
|
||||
for `grub-install`.
|
||||
- `sudo update-initramfs -u -k all` prints a `Generating` line for each
|
||||
kernel in `ls /boot/vmlinuz-*`.
|
||||
@@ -0,0 +1,35 @@
|
||||
cfg=/boot/grub/grub.cfg
|
||||
# Effective settings: /etc/default/grub, then grub.d/*.cfg, as
|
||||
# grub-mkconfig reads them.
|
||||
setting() {
|
||||
(
|
||||
. /etc/default/grub >/dev/null 2>&1
|
||||
for f in /etc/default/grub.d/*.cfg; do [ -e "$f" ] && . "$f" >/dev/null 2>&1; done
|
||||
eval "printf '%s' \"\${$1:-}\""
|
||||
)
|
||||
}
|
||||
first_linux() { awk '$1 == "linux" {print; exit}' "$cfg"; }
|
||||
kernels_rebuilt() {
|
||||
local k v n=0
|
||||
for k in /boot/vmlinuz-*; do
|
||||
[ -e "$k" ] || continue
|
||||
v=${k#/boot/vmlinuz-}
|
||||
[ "/boot/initrd.img-$v" -nt "$LAB_STATE/since" ] || return 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
[ "$n" -gt 0 ]
|
||||
}
|
||||
|
||||
task 1 "GRUB menu for 10 seconds, no quiet or splash"
|
||||
need "the settings read by update-grub give GRUB_TIMEOUT_STYLE=menu" '[ "$(setting GRUB_TIMEOUT_STYLE)" = menu ] || [ -z "$(setting GRUB_TIMEOUT_STYLE)" ]'
|
||||
need "the settings read by update-grub give GRUB_TIMEOUT=10" '[ "$(setting GRUB_TIMEOUT)" = 10 ]'
|
||||
need "the settings have no quiet or splash on the command line" '! printf "%s %s" "$(setting GRUB_CMDLINE_LINUX_DEFAULT)" "$(setting GRUB_CMDLINE_LINUX)" | grep -Ewq "quiet|splash"'
|
||||
need "grub.cfg shows the menu for 10 seconds (update-grub)" 'grep -Eq "^[[:space:]]*set timeout=10$" $cfg && ! grep -Eq "timeout_style=(hidden|countdown)" $cfg'
|
||||
need "grub.cfg boots the default entry without quiet or splash" '[ -n "$(first_linux)" ] && ! first_linux | grep -Ewq "quiet|splash"'
|
||||
need "this boot ran without quiet or splash (/proc/cmdline; reboot)" '! grep -Ewq "quiet|splash" /proc/cmdline'
|
||||
|
||||
task 2 "Boot into recovery mode and get a writable root shell" self
|
||||
task 3 "Boot from grub rescue>, then repair GRUB for good" self
|
||||
|
||||
task 4 "The initramfs of every installed kernel rebuilt"
|
||||
need "every /boot/initrd.img-VERSION is newer than the lab's start (update-initramfs)" kernels_rebuilt
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.1 Boot problems: GRUB 2 and the initramfs"
|
||||
CHAPTER=/ubuntu-desktop/3-installing/3-1-boot-problems/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,19 @@
|
||||
# GRUB's files back where task 3 found them.
|
||||
[ ! -e /boot/grub ] && [ -d /boot/grub.bak ] && mv /boot/grub.bak /boot/grub
|
||||
rm -f /root/recovery-test
|
||||
if [ -f "$LAB_STATE/default/grub" ]; then
|
||||
changed=
|
||||
cmp -s "$LAB_STATE/default/grub" /etc/default/grub || changed=1
|
||||
if [ -d "$LAB_STATE/default/grub.d" ]; then
|
||||
diff -r "$LAB_STATE/default/grub.d" /etc/default/grub.d >/dev/null 2>&1 || changed=1
|
||||
elif [ -e /etc/default/grub.d ]; then
|
||||
changed=1
|
||||
fi
|
||||
if [ -n "$changed" ]; then
|
||||
cp -a "$LAB_STATE/default/grub" /etc/default/grub
|
||||
rm -rf /etc/default/grub.d
|
||||
[ -d "$LAB_STATE/default/grub.d" ] && cp -a "$LAB_STATE/default/grub.d" /etc/default/grub.d
|
||||
update-grub >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,10 @@
|
||||
command -v update-grub >/dev/null || refuse "this lab needs GRUB 2 with update-grub"
|
||||
[ -f /boot/grub/grub.cfg ] || refuse "/boot/grub/grub.cfg is missing; this lab needs a system that boots with GRUB 2 from /boot/grub"
|
||||
[ -e /boot/grub.bak ] && refuse "/boot/grub.bak exists; put it back or remove it first"
|
||||
command -v update-initramfs >/dev/null || refuse "this lab needs initramfs-tools (update-initramfs)"
|
||||
mkdir -p "$LAB_STATE/default"
|
||||
cp -a /etc/default/grub "$LAB_STATE/default/"
|
||||
[ -d /etc/default/grub.d ] && cp -a /etc/default/grub.d "$LAB_STATE/default/"
|
||||
# Task 4 looks for initramfs images newer than this.
|
||||
sleep 1
|
||||
touch "$LAB_STATE/since"
|
||||
@@ -0,0 +1,32 @@
|
||||
# Ubuntu Desktop 3.3: A bootable USB drive
|
||||
|
||||
Checking the signature on Ubuntu's checksum file, seeing a changed copy
|
||||
fail, and writing an ISO image to a drive with `dd`. A spare disk stands
|
||||
in for the USB drive.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab adds an empty spare disk, `/dev/loop40` (64 MiB), and an empty
|
||||
folder `/root/usb` to work in. The VM needs to reach
|
||||
`releases.ubuntu.com`. `xorriso` isn't installed.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. In `/root/usb`, download `SHA256SUMS` and `SHA256SUMS.gpg` for Ubuntu
|
||||
24.04 (`wget https://releases.ubuntu.com/24.04/SHA256SUMS
|
||||
https://releases.ubuntu.com/24.04/SHA256SUMS.gpg`) and check the
|
||||
signature.
|
||||
2. Copy `SHA256SUMS` to `/root/usb/SHA256SUMS.copy`, change one character
|
||||
of the copy, and check its signature again.
|
||||
3. Make a small ISO with the volume label `TEST` (install `xorriso`, then
|
||||
`xorriso -as mkisofs -V TEST -o test.iso SOMEDIR`) and write it to the
|
||||
spare disk `/dev/loop40` with `dd`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `gpgv --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
SHA256SUMS.gpg SHA256SUMS` should print `Good signature`.
|
||||
- Change a single letter or digit, for example with `sed -i '1s/^./0/'`
|
||||
or in an editor; gpgv should now print `BAD signature`.
|
||||
- `dd if=test.iso of=/dev/loop40 bs=4M conv=fsync`, then `blkid
|
||||
/dev/loop40` shows `LABEL="TEST"` and `TYPE="iso9660"`.
|
||||
@@ -0,0 +1,17 @@
|
||||
K=/usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
U=/root/usb
|
||||
|
||||
task 1 "SHA256SUMS downloaded and its signature good"
|
||||
need "/root/usb/SHA256SUMS and SHA256SUMS.gpg exist" '[ -s $U/SHA256SUMS ] && [ -s $U/SHA256SUMS.gpg ]'
|
||||
need "SHA256SUMS lists the 24.04 images" 'grep -q "ubuntu-24\.04.*\.iso" $U/SHA256SUMS'
|
||||
need "gpgv says the signature is good" 'gpgv --keyring $K $U/SHA256SUMS.gpg $U/SHA256SUMS'
|
||||
|
||||
task 2 "A copy with one character changed fails the check"
|
||||
need "/root/usb/SHA256SUMS.copy exists" '[ -f $U/SHA256SUMS.copy ]'
|
||||
need "it differs from SHA256SUMS in exactly one character" '[ "$(stat -c %s $U/SHA256SUMS.copy)" = "$(stat -c %s $U/SHA256SUMS)" ] && [ "$(cmp -l $U/SHA256SUMS $U/SHA256SUMS.copy | wc -l)" = 1 ]'
|
||||
need "gpgv reports a bad signature for it" 'gpgv --keyring $K $U/SHA256SUMS.gpg $U/SHA256SUMS.copy 2>&1 | grep -q "BAD signature"'
|
||||
|
||||
task 3 "An ISO labeled TEST written to /dev/loop40"
|
||||
need "xorriso is installed" 'command -v xorriso'
|
||||
need "/dev/loop40 holds an ISO 9660 filesystem" '[ "$(blkid -p -s TYPE -o value /dev/loop40)" = iso9660 ]'
|
||||
need "its label is TEST" '[ "$(blkid -p -s LABEL -o value /dev/loop40)" = TEST ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.3 A bootable USB drive"
|
||||
CHAPTER=/ubuntu-desktop/3-installing/3-3-bootable-usb/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,8 @@
|
||||
installed_pkgs() { dpkg-query -W -f '${db:Status-Abbrev} ${Package}\n' 2>/dev/null | awk '$1 ~ /^.i/ {print $2}' | sort -u; }
|
||||
rm -rf /root/usb /root/test.iso
|
||||
if [ -z "$(state_get had-xorriso)" ] && [ -f "$LAB_STATE/pkgs-before" ]; then
|
||||
# xorriso and the libraries it brought in.
|
||||
added=$(comm -13 "$LAB_STATE/pkgs-before" <(installed_pkgs) | grep -E '^(xorriso|libburn|libisofs|libisoburn|libjte)' || true)
|
||||
[ -n "$added" ] && DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $added >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,8 @@
|
||||
installed_pkgs() { dpkg-query -W -f '${db:Status-Abbrev} ${Package}\n' 2>/dev/null | awk '$1 ~ /^.i/ {print $2}' | sort -u; }
|
||||
[ -e /root/usb ] && refuse "/root/usb already exists; move it away first"
|
||||
command -v gpgv >/dev/null || pkg_install gpgv
|
||||
[ -f /usr/share/keyrings/ubuntu-archive-keyring.gpg ] || refuse "this lab needs /usr/share/keyrings/ubuntu-archive-keyring.gpg (package ubuntu-keyring)"
|
||||
pkg_installed xorriso && state_set had-xorriso 1
|
||||
installed_pkgs >"$LAB_STATE/pkgs-before"
|
||||
disk_add 0 64M >/dev/null
|
||||
mkdir /root/usb
|
||||
@@ -0,0 +1,32 @@
|
||||
# Ubuntu Desktop 3.4: Proprietary GPU and Wi-Fi drivers
|
||||
|
||||
Finding out whether a machine needs a proprietary driver, where those
|
||||
drivers live in the archive, the Secure Boot state, and which kernel
|
||||
driver each network device uses. Nothing gets installed: the lab runs
|
||||
on a VM, which has no hardware that needs these drivers.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab installs `ubuntu-drivers-common` (the `ubuntu-drivers` command)
|
||||
if it's missing; reset removes it again. Write your answers to the files
|
||||
in `/root/lab-answers/`, one item per line.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Find out whether this machine needs any proprietary driver. Write
|
||||
`yes` or `no` to `/root/lab-answers/1`.
|
||||
2. Find which component the NVIDIA 580 driver (`nvidia-driver-580`) and
|
||||
the Broadcom STA Wi-Fi driver (`bcmwl-kernel-source`) are in, and what
|
||||
Secure Boot state this machine is in. Write the two components on
|
||||
lines 1 and 2 of `/root/lab-answers/2`, and on line 3 `enabled`,
|
||||
`disabled` or `no EFI`.
|
||||
3. For each network device, find the kernel driver in use. Write the
|
||||
drivers to `/root/lab-answers/3`, one line per network device.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ubuntu-drivers devices` prints nothing when every device works with
|
||||
open source drivers.
|
||||
- `apt-cache show PACKAGE | grep -m1 Section`; `mokutil --sb-state`.
|
||||
- `lspci -k` shows a `Kernel driver in use:` line under each network
|
||||
controller.
|
||||
@@ -0,0 +1,31 @@
|
||||
line() { sed -n "${2}p" "/root/lab-answers/$1" 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | tr A-Z a-z; }
|
||||
component() { local s; s=$(apt-cache show "$1" 2>/dev/null | awk '$1 == "Section:" {print $2; exit}'); [ -n "$s" ] || return; case $s in */*) printf '%s' "${s%%/*}" ;; *) printf main ;; esac; }
|
||||
needs_driver() { [ -n "$(timeout 120 ubuntu-drivers devices 2>/dev/null | grep -v '^[[:space:]]*$')" ]; }
|
||||
sb_ok() {
|
||||
local a; a=$(line 2 3); s=$(mokutil --sb-state 2>&1)
|
||||
case $s in
|
||||
*"SecureBoot enabled"*) [ "$a" = enabled ] ;;
|
||||
*"SecureBoot disabled"*) [ "$a" = disabled ] ;;
|
||||
*) [[ $a =~ (no|not|bios|legacy|unsupported|efi) ]] && [ "$a" != enabled ] && [ "$a" != disabled ] ;;
|
||||
esac
|
||||
}
|
||||
# PCI network controllers and the driver each uses, as lspci -k shows it.
|
||||
net_drivers() { lspci -k 2>/dev/null | awk '/^[^[:space:]]/ {net = ($0 ~ /(Ethernet|Network) controller/)} net && /Kernel driver in use:/ {print $NF}'; }
|
||||
# Accepted names: lspci's, and the network interfaces' own drivers.
|
||||
ok_drivers() {
|
||||
net_drivers
|
||||
for d in /sys/class/net/*/device/driver; do [ -e "$d" ] && basename "$(readlink -f "$d")"; done
|
||||
}
|
||||
given() { grep -v '^[[:space:]]*$' /root/lab-answers/3 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/.*:[[:space:]]*//'; }
|
||||
|
||||
task 1 "Whether this machine needs a proprietary driver"
|
||||
need "/root/lab-answers/1 says yes or no, matching ubuntu-drivers devices" 'a=$(line 1 1); if needs_driver; then [ "$a" = yes ]; else [ "$a" = no ]; fi'
|
||||
|
||||
task 2 "Components of the NVIDIA and Broadcom drivers, Secure Boot state"
|
||||
need "line 1 of /root/lab-answers/2 is nvidia-driver-580's component" 'c=$(component nvidia-driver-580); [ -n "$c" ] && [ "$(line 2 1)" = "$c" ]'
|
||||
need "line 2 is bcmwl-kernel-source's component" 'c=$(component bcmwl-kernel-source); [ -n "$c" ] && [ "$(line 2 2)" = "$c" ]'
|
||||
need "line 3 is this machine's Secure Boot state (mokutil --sb-state)" sb_ok
|
||||
|
||||
task 3 "Kernel driver of each network device"
|
||||
need "/root/lab-answers/3 has a line for each network device" '[ "$(given | wc -l)" -ge "$(net_drivers | wc -l)" ] && [ "$(given | wc -l)" -ge 1 ]'
|
||||
need "every line is a network device's driver in use" '[ -z "$(comm -23 <(given | sort -u) <(ok_drivers | sort -u))" ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.4 Proprietary GPU and Wi-Fi drivers"
|
||||
CHAPTER=/ubuntu-desktop/3-installing/3-4-proprietary-drivers/
|
||||
SYSTEMS="debian"
|
||||
@@ -0,0 +1,4 @@
|
||||
if [ -s "$LAB_STATE/pkgs-added" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $(cat "$LAB_STATE/pkgs-added") >/dev/null 2>&1
|
||||
fi
|
||||
true
|
||||
@@ -0,0 +1,6 @@
|
||||
installed_pkgs() { dpkg-query -W -f '${db:Status-Abbrev} ${Package}\n' 2>/dev/null | awk '$1 ~ /^.i/ {print $2}' | sort -u; }
|
||||
if ! command -v ubuntu-drivers >/dev/null; then
|
||||
installed_pkgs >"$LAB_STATE/pkgs-before"
|
||||
pkg_install ubuntu-drivers-common
|
||||
comm -13 "$LAB_STATE/pkgs-before" <(installed_pkgs) >"$LAB_STATE/pkgs-added"
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
# Ubuntu Desktop 3.5: LVM volumes, filesystems and snapshots
|
||||
|
||||
A volume group on a spare disk, an ext4 logical volume mounted at boot,
|
||||
growing it onto a second disk while it's mounted, and getting a deleted
|
||||
file back by merging a snapshot.
|
||||
|
||||
## Before you start
|
||||
|
||||
The lab adds two empty spare disks: `/dev/loop40` (1 GiB, the first) and
|
||||
`/dev/loop41` (2 GiB, the second). They come back at every boot. The LVM
|
||||
tools are installed.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Make a volume group `vg1` from the first spare disk and an 800 MiB
|
||||
logical volume `home2` with ext4, mounted at `/mnt/home2` at every
|
||||
boot.
|
||||
2. Grow `home2` by 1.5 GiB, adding the second disk to the group first if
|
||||
needed, without unmounting it.
|
||||
3. Create a file `/mnt/home2/notes.txt`, snapshot `home2`, delete
|
||||
`notes.txt`, and get it back by merging the snapshot.
|
||||
|
||||
## Hints
|
||||
|
||||
- `pvcreate`, `vgcreate`, `lvcreate -L 800M -n`, `mkfs.ext4`; an
|
||||
`/etc/fstab` line for `/dev/mapper/vg1-home2` (or its UUID), then
|
||||
`systemctl daemon-reload` and `mount -a`.
|
||||
- `vgs vg1` shows the free space; `vgextend`, then `lvextend -r -L +1.5G`
|
||||
grows the filesystem too. `df -h /mnt/home2` should show about 2.3G.
|
||||
- `lvcreate -s -L 200M -n NAME vg1/home2`; unmount, `lvconvert --merge
|
||||
vg1/NAME`, mount again. `lvs vg1` no longer lists the snapshot.
|
||||
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user