diff --git a/docs/writing-labs.md b/docs/writing-labs.md index 04b94fc..fdeee56 100644 --- a/docs/writing-labs.md +++ b/docs/writing-labs.md @@ -95,10 +95,18 @@ socket bind instead. Mark a task `self` only when it can't be checked from the running system, such as a boot-loader recovery. Say in its README text how to check it. +Logs need care. AlmaLinux keeps the journal only in memory until someone +makes it persistent, so a message from before a reboot is gone after it; +read `/var/log/messages` or `/var/log/secure` as well, or mark the lab +`noreboot`. rsyslog writes `logger` messages to those files a moment +later, so a check that runs right away can miss them: poll briefly. + **reset.sh** undoes the scenario and the usual results of the tasks (the users, files, units and settings they create), so that `le-lab start` on the -same lab scores 0% again. Lab users, spare disks and the peer are removed -for you after `reset.sh` runs. Don't fail on things that aren't there. +same lab scores 0% again. That includes packages a task installs: note in +setup which ones were missing, and remove them again in reset. Lab users, +spare disks and the peer are removed for you after `reset.sh` runs. Don't +fail on things that aren't there. ## Rules diff --git a/labs/linux-plus/1-1-1-boot-process/README.md b/labs/linux-plus/1-1-1-boot-process/README.md new file mode 100644 index 0000000..3324a70 --- /dev/null +++ b/labs/linux-plus/1-1-1-boot-process/README.md @@ -0,0 +1,39 @@ +# Linux+ 1.1.1: The boot process + +How the system booted, a trip into rescue mode, and a lasting change to the +kernel command line. + +## Before you start + +Take a snapshot first: task 2 and task 3 go through the boot loader. On the +RHEL family the lab adds `quiet` to every kernel's command line if it isn't +there, so that task 3 has something to remove. On Ubuntu it makes sure +`loglevel=7` isn't set. Reset puts the boot loader settings back as they +were. + +## Tasks + +1. Find out how this system booted. Write `UEFI` or `BIOS` on the first + line of `/root/lab-answers/1`, and the default systemd target (such as + `multi-user.target`) on the second. +2. Boot once into rescue mode from the boot loader menu, log in as root, + run `systemctl list-units --state=running`, then `systemctl reboot`. Not + graded: check yourself that only a handful of units were running. Root + needs a password for rescue mode (`passwd root`); lock it again + afterwards with `passwd -l root`. +3. Make a lasting change to the kernel command line, then reboot. RHEL + family: remove `quiet` from every installed kernel with grubby. Ubuntu: + add `loglevel=7` to `GRUB_CMDLINE_LINUX_DEFAULT` and run `update-grub`. + Check `/proc/cmdline` after the reboot. Leave the change in place until + you've run `le-lab check`; reset puts it back. + +## Hints + +- `/sys/firmware/efi` exists only on UEFI systems; `systemctl get-default`. +- In the GRUB menu, **e** edits an entry: add `systemd.unit=rescue.target` + to the end of the `linux` line and press **Ctrl+x**. Press **Esc** or hold + **Shift** if the menu is hidden. +- `grubby --update-kernel=ALL --remove-args=...`, `grubby --info=ALL`. +- On Ubuntu, files in `/etc/default/grub.d/` are read after + `/etc/default/grub` and can set the same variable; cloud images set + `GRUB_CMDLINE_LINUX_DEFAULT` in `50-cloudimg-settings.cfg`. diff --git a/labs/linux-plus/1-1-1-boot-process/check.sh b/labs/linux-plus/1-1-1-boot-process/check.sh new file mode 100644 index 0000000..d1f9255 --- /dev/null +++ b/labs/linux-plus/1-1-1-boot-process/check.sh @@ -0,0 +1,21 @@ +line() { sed -n "${1}p" /root/lab-answers/1 2>/dev/null | tr 'A-Z' 'a-z' | tr -d '[:space:]'; } +fw=bios; [ -d /sys/firmware/efi ] && fw=uefi + +task 1 "UEFI or BIOS, and the default target, in /root/lab-answers/1" +need "the first line says ${fw^^}" '[ "$(line 1)" = "$fw" ]' +need "the second line is the default target, $(systemctl get-default)" '[ "$(line 2)" = "$(systemctl get-default)" ]' + +task 2 "Boot once into rescue mode" self + +if is_rhel; then + task 3 "quiet removed from every kernel's command line" rhel + need "no boot entry has quiet (grubby --info=ALL)" '[ -n "$(grubby --info=ALL | grep "^args=")" ] && ! grubby --info=ALL | grep "^args=" | grep -Eq "[\" ]quiet[\" ]"' + need "the running kernel was booted without quiet (reboot)" '! grep -Eq "(^| )quiet( |$)" /proc/cmdline' +else + task 3 "loglevel=7 added to GRUB_CMDLINE_LINUX_DEFAULT" debian + need "GRUB_CMDLINE_LINUX_DEFAULT ends up with loglevel=7" ' + v=$(bash -c ". /etc/default/grub; for f in /etc/default/grub.d/*.cfg; do [ -f \"\$f\" ] && . \"\$f\"; done; printf %s \"\$GRUB_CMDLINE_LINUX_DEFAULT\"") + grep -Eq "(^| )loglevel=7( |$)" <<<"$v"' + need "/boot/grub/grub.cfg has loglevel=7 (update-grub)" 'awk "\$1 == \"linux\"" /boot/grub/grub.cfg | grep -Eq "[[:space:]]loglevel=7([[:space:]]|$)"' + need "the running kernel was booted with loglevel=7 (reboot)" 'grep -Eq "(^| )loglevel=7( |$)" /proc/cmdline' +fi diff --git a/labs/linux-plus/1-1-1-boot-process/lab.conf b/labs/linux-plus/1-1-1-boot-process/lab.conf new file mode 100644 index 0000000..53ec3e9 --- /dev/null +++ b/labs/linux-plus/1-1-1-boot-process/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.1.1 The boot process" +CHAPTER=/linux-plus/1-system-management/1-1-1-boot-process/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-1-1-boot-process/reset.sh b/labs/linux-plus/1-1-1-boot-process/reset.sh new file mode 100644 index 0000000..04ddbcc --- /dev/null +++ b/labs/linux-plus/1-1-1-boot-process/reset.sh @@ -0,0 +1,12 @@ +if is_rhel; then + if [ -n "$(state_get added-quiet)" ]; then + grubby --update-kernel=ALL --remove-args=quiet 2>/dev/null + elif [ -d "$LAB_STATE" ]; then + grubby --update-kernel=ALL --args=quiet 2>/dev/null + fi +elif [ -f "$LAB_STATE/grub.tar" ]; then + rm -rf /etc/default/grub.d + tar -C /etc/default -xf "$LAB_STATE/grub.tar" + update-grub >/dev/null 2>&1 +fi +true diff --git a/labs/linux-plus/1-1-1-boot-process/setup.sh b/labs/linux-plus/1-1-1-boot-process/setup.sh new file mode 100644 index 0000000..a14b451 --- /dev/null +++ b/labs/linux-plus/1-1-1-boot-process/setup.sh @@ -0,0 +1,16 @@ +if is_rhel; then + command -v grubby >/dev/null || refuse "grubby isn't installed; this lab needs it (dnf install grubby)" + # Something for task 3 to remove: quiet on every kernel. + if grubby --info=ALL | grep '^args=' | grep -Evq '[" ]quiet[" ]'; then + grubby --update-kernel=ALL --args=quiet + state_set added-quiet 1 + fi +else + command -v update-grub >/dev/null || refuse "update-grub isn't installed; this lab needs GRUB" + tar -C /etc/default -cf "$LAB_STATE/grub.tar" grub $(cd /etc/default && ls -d grub.d 2>/dev/null) + # Task 3 adds loglevel=7, so it mustn't be there yet. + if grep -qs 'loglevel=7' /etc/default/grub /etc/default/grub.d/*.cfg; then + sed -i 's/ *loglevel=7//' /etc/default/grub /etc/default/grub.d/*.cfg + update-grub >/dev/null 2>&1 + fi +fi diff --git a/labs/linux-plus/1-1-2-filesystem-hierarchy/README.md b/labs/linux-plus/1-1-2-filesystem-hierarchy/README.md new file mode 100644 index 0000000..8ee2821 --- /dev/null +++ b/labs/linux-plus/1-1-2-filesystem-hierarchy/README.md @@ -0,0 +1,30 @@ +# Linux+ 1.1.2: The filesystem hierarchy + +A tour of the directory tree: links into `/usr`, what uses space under +`/var`, real and virtual filesystems, and which package owns a file. + +## Before you start + +The lab sets nothing up. Each task asks for a finding: write it to the file +in `/root/lab-answers/` that the task names. + +## Tasks + +1. Look at `/bin`, `/sbin`, `/lib` and `/tmp` with `ls -ld`. Write the ones + that are symbolic links, one per line, to `/root/lab-answers/1`. +2. Find the directory under `/var` that uses the most space, and write its + full path (such as `/var/spool`) to `/root/lab-answers/2`. +3. List the mount points of the real filesystems (not virtual ones such as + `/proc`), one per line, in `/root/lab-answers/3`. +4. Ask the package manager which package owns `/usr/bin/ls`, and write the + package name to `/root/lab-answers/4`. Then create `/etc/mynote` and see + what it says about that file; remove `/etc/mynote` again. + +## Hints + +- `ls -ld /bin /sbin /lib /tmp`: links start with `l`. Note the `t` at the + end of `/tmp`'s permissions: the sticky bit. +- `du -sh /var/* 2>/dev/null | sort -h | tail`. +- `findmnt --real`, `df -hT`; `findmnt --real -rn -o TARGET` prints just + the mount points. +- `rpm -qf` on the RHEL family, `dpkg -S` on Debian and Ubuntu. diff --git a/labs/linux-plus/1-1-2-filesystem-hierarchy/check.sh b/labs/linux-plus/1-1-2-filesystem-hierarchy/check.sh new file mode 100644 index 0000000..933d0dd --- /dev/null +++ b/labs/linux-plus/1-1-2-filesystem-hierarchy/check.sh @@ -0,0 +1,26 @@ +# Answers as plain lines, lowercase, without spaces or a trailing slash. +lines() { tr 'A-Z' 'a-z' <"/root/lab-answers/$1" 2>/dev/null | tr -d ' \t\r' | sed 's|\(.\)/*$|\1|' | grep .; } +links=$(for d in /bin /sbin /lib /tmp; do [ -L "$d" ] && echo "$d"; done) +# The two biggest directories under /var: the answer may change a little. +biggest=$(du -s /var/* 2>/dev/null | sort -n | tail -n2 | awk '{print $2}') +real=$(findmnt --real -rn -o TARGET) +if is_rhel; then owner=$(rpm -qf --qf '%{NAME}\n' /usr/bin/ls | head -n1); else owner=$(dpkg -S /usr/bin/ls | head -n1 | cut -d: -f1); fi + +task 1 "The symbolic links among /bin /sbin /lib /tmp" +need "/root/lab-answers/1 lists the links: $(echo $links)" ' + [ "$(lines 1 | sed "s|^\([^/]\)|/\1|" | sort -u)" = "$(sort -u <<<"$links")" ]' + +task 2 "The biggest directory under /var" +need "/root/lab-answers/2 names the biggest directory under /var" ' + a=$(lines 2 | head -n1); a=/var/${a#/var/}; grep -qxF "$a" <<<"$biggest"' + +task 3 "The mount points of the real filesystems" +need "/root/lab-answers/3 lists every real filesystem's mount point" ' + [ -n "$(lines 3)" ] && while read -r m; do lines 3 | grep -qxF "$m" || exit 1; done <<<"$real"' +need "/root/lab-answers/3 lists no virtual filesystems" ' + lines 3 | while read -r m; do grep -qxF "$m" <<<"$real" || exit 1; done' + +task 4 "The package that owns /usr/bin/ls" +need "/root/lab-answers/4 names the package $owner" ' + a=$(lines 4 | head -n1); a=${a%%:*}; [ "$a" = "$owner" ] || [[ $a == "$owner"-[0-9]* ]] || [[ $a == "$owner"_[0-9]* ]]' +need "/etc/mynote is removed again" '[ ! -e /etc/mynote ]' diff --git a/labs/linux-plus/1-1-2-filesystem-hierarchy/lab.conf b/labs/linux-plus/1-1-2-filesystem-hierarchy/lab.conf new file mode 100644 index 0000000..177529b --- /dev/null +++ b/labs/linux-plus/1-1-2-filesystem-hierarchy/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.1.2 The filesystem hierarchy" +CHAPTER=/linux-plus/1-system-management/1-1-2-filesystem-hierarchy/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-1-2-filesystem-hierarchy/reset.sh b/labs/linux-plus/1-1-2-filesystem-hierarchy/reset.sh new file mode 100644 index 0000000..c0efa35 --- /dev/null +++ b/labs/linux-plus/1-1-2-filesystem-hierarchy/reset.sh @@ -0,0 +1,2 @@ +rm -f /etc/mynote +true diff --git a/labs/linux-plus/1-1-2-filesystem-hierarchy/setup.sh b/labs/linux-plus/1-1-2-filesystem-hierarchy/setup.sh new file mode 100644 index 0000000..7ec24c2 --- /dev/null +++ b/labs/linux-plus/1-1-2-filesystem-hierarchy/setup.sh @@ -0,0 +1,2 @@ +# Nothing to set up: the tasks look at the system as it is. +rm -f /etc/mynote diff --git a/labs/linux-plus/1-1-3-architectures-distributions-licensing/README.md b/labs/linux-plus/1-1-3-architectures-distributions-licensing/README.md new file mode 100644 index 0000000..bf6e291 --- /dev/null +++ b/labs/linux-plus/1-1-3-architectures-distributions-licensing/README.md @@ -0,0 +1,28 @@ +# Linux+ 1.1.3: Architectures, distributions and licenses + +Finding the machine architecture and the distribution family, a package's +license, and a package that isn't tied to one architecture. + +## Before you start + +The lab sets nothing up. Each task asks for a finding: write it to the file +in `/root/lab-answers/` that the task names. + +## Tasks + +1. Write the machine architecture on the first line of + `/root/lab-answers/1`, and the distribution family this system says it + belongs to (one of the words in `ID_LIKE`, such as `rhel` or `debian`) on + the second. +2. Find the license of the `bash` package and write it to + `/root/lab-answers/2`. +3. Find a package that isn't architecture-specific and write its name to + `/root/lab-answers/3`. + +## Hints + +- `uname -m`; `grep -E '^(ID|ID_LIKE|VERSION_ID)=' /etc/os-release`. +- `rpm -q --qf '%{NAME}: %{LICENSE}\n' bash` on the RHEL family; + `/usr/share/doc/bash/copyright` on Debian and Ubuntu. +- `rpm -qa --qf '%{ARCH} %{NAME}\n' | grep ^noarch`; + `dpkg -l | awk '$4=="all"{print $2}'`. diff --git a/labs/linux-plus/1-1-3-architectures-distributions-licensing/check.sh b/labs/linux-plus/1-1-3-architectures-distributions-licensing/check.sh new file mode 100644 index 0000000..4ab1325 --- /dev/null +++ b/labs/linux-plus/1-1-3-architectures-distributions-licensing/check.sh @@ -0,0 +1,17 @@ +line() { sed -n "${2}p" "/root/lab-answers/$1" 2>/dev/null | tr 'A-Z' 'a-z' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; } +family=$(. /etc/os-release; echo "${ID_LIKE:-$ID}" | tr 'A-Z' 'a-z') + +task 1 "The architecture and the distribution family" +need "the first line is the architecture, $(uname -m)" '[ "$(line 1 1)" = "$(uname -m)" ]' +need "the second line is a word from ID_LIKE ($family)" 'a=$(line 1 2 | tr -d "\""); [ -n "$a" ] && grep -qw -- "$a" <<<"$family"' + +task 2 "The license of bash" +need "/root/lab-answers/2 names bash's license (a version of the GPL)" 'line 2 1 | grep -Eq "gpl.*3|gnu general public license.*3"' + +task 3 "A package that isn't architecture-specific" +need "/root/lab-answers/3 names an installed package" '[ -n "$(line 3 1)" ]' +if is_rhel; then + need "that package's architecture is noarch" '[ "$(rpm -q --qf "%{ARCH}" "$(line 3 1)" 2>/dev/null)" = noarch ]' +else + need "that package's architecture is all" '[ "$(dpkg-query -W -f "\${Architecture}" "$(line 3 1)" 2>/dev/null)" = all ]' +fi diff --git a/labs/linux-plus/1-1-3-architectures-distributions-licensing/lab.conf b/labs/linux-plus/1-1-3-architectures-distributions-licensing/lab.conf new file mode 100644 index 0000000..5370860 --- /dev/null +++ b/labs/linux-plus/1-1-3-architectures-distributions-licensing/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.1.3 Architectures, distributions and licenses" +CHAPTER=/linux-plus/1-system-management/1-1-3-architectures-distributions-licensing/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-1-3-architectures-distributions-licensing/reset.sh b/labs/linux-plus/1-1-3-architectures-distributions-licensing/reset.sh new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/labs/linux-plus/1-1-3-architectures-distributions-licensing/reset.sh @@ -0,0 +1 @@ +true diff --git a/labs/linux-plus/1-1-3-architectures-distributions-licensing/setup.sh b/labs/linux-plus/1-1-3-architectures-distributions-licensing/setup.sh new file mode 100644 index 0000000..69a091c --- /dev/null +++ b/labs/linux-plus/1-1-3-architectures-distributions-licensing/setup.sh @@ -0,0 +1,2 @@ +# Nothing to set up: the tasks look at the system as it is. +true diff --git a/labs/linux-plus/1-2-1-kernel-modules-and-initrd/README.md b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/README.md new file mode 100644 index 0000000..f002605 --- /dev/null +++ b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/README.md @@ -0,0 +1,37 @@ +# Linux+ 1.2.1: Kernel modules and the initrd + +Looking at modules, loading one with an option, loading it at every boot, +and finding the root disk's driver in the initrd. + +## Before you start + +The lab unloads the `dummy` module, a harmless one that makes network +devices. Reboot when task 3 says so; the check looks at the running system. + +## Tasks + +1. Find the license of the `xfs` module and write it to + `/root/lab-answers/1`. +2. Load `dummy` with the option `numdummies=3`, check that it made three + devices, and unload it again. Not graded: `ip -br link show type dummy | + wc -l` should print `3` before you unload it. +3. Make `dummy` load at every boot with the option `numdummies=1`, then + reboot. After the reboot, `ip -br link show type dummy` shows exactly one + device. Keep your files until you've run `le-lab check`; reset removes + them. +4. Look inside your initrd, and find the driver for your root disk + (`virtio_blk`, `nvme` or `ahci`, for example). Write its name to + `/root/lab-answers/4`. + +## Hints + +- `lsmod`, `modinfo xfs`, `modinfo -F license`. +- `modprobe dummy numdummies=3`, `modprobe -r dummy`. +- `/etc/modules-load.d/` for loading, `/etc/modprobe.d/` for options. Files + in every `modprobe.d` directory are read in name order and the last + setting wins; `modprobe --show-depends dummy` shows what modprobe will + pass. +- `lsinitrd` on the RHEL family, `lsinitramfs /boot/initrd.img-$(uname -r)` + on Ubuntu, which builds `virtio_blk` into the kernel: + `grep virtio_blk /lib/modules/$(uname -r)/modules.builtin`. `lsblk` shows + the root disk. diff --git a/labs/linux-plus/1-2-1-kernel-modules-and-initrd/check.sh b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/check.sh new file mode 100644 index 0000000..82e4947 --- /dev/null +++ b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/check.sh @@ -0,0 +1,26 @@ +# Every driver between the root filesystem's disk and the bus it hangs off. +root_drivers() { + local src disk p + src=$(findmnt -n -o SOURCE / | sed 's/\[.*//') + disk=$(lsblk -nspo NAME "$src" | tail -n1) + p=$(readlink -f "/sys/class/block/${disk##*/}/device") + while [ -n "$p" ] && [ "$p" != /sys/devices ] && [ "$p" != / ]; do + [ -e "$p/driver" ] && basename "$(readlink -f "$p/driver")" + p=${p%/*} + done +} + +task 1 "The license of the xfs module" +need "/root/lab-answers/1 has xfs's license, $(modinfo -F license xfs 2>/dev/null)" '[ "$(answer 1)" = "$(modinfo -F license xfs | tr A-Z a-z)" ]' + +task 2 "dummy loaded with numdummies=3, then unloaded" self + +task 3 "dummy loads at every boot with one device" +need "dummy is set to load at boot" ' + cat /etc/modules-load.d/*.conf /run/modules-load.d/*.conf /usr/lib/modules-load.d/*.conf /etc/modules 2>/dev/null | grep -Eq "^[[:space:]]*dummy[[:space:]]*$"' +need "modprobe gives dummy numdummies=1 (the last setting wins)" 'modprobe --show-depends dummy 2>/dev/null | grep -o "numdummies=[0-9]*" | tail -n1 | grep -qx numdummies=1' +need "the dummy module is loaded" '[ -d /sys/module/dummy ]' +need "ip -br link show type dummy shows one device" '[ "$(ip -br link show type dummy | wc -l)" = 1 ]' + +task 4 "The root disk's driver" +need "/root/lab-answers/4 names a driver of the root disk ($(root_drivers | head -n1))" '[ -n "$(answer 4)" ] && root_drivers | grep -qxF "$(answer 4 | tr - _)"' diff --git a/labs/linux-plus/1-2-1-kernel-modules-and-initrd/lab.conf b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/lab.conf new file mode 100644 index 0000000..93cfff0 --- /dev/null +++ b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.2.1 Kernel modules and the initrd" +CHAPTER=/linux-plus/1-system-management/1-2-1-kernel-modules-and-initrd/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-2-1-kernel-modules-and-initrd/reset.sh b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/reset.sh new file mode 100644 index 0000000..6dfca67 --- /dev/null +++ b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/reset.sh @@ -0,0 +1,8 @@ +for f in /etc/modprobe.d/*.conf /etc/modules-load.d/*.conf; do + [ -f "$f" ] && grep -qw dummy "$f" || continue + sed -i '/\bdummy\b/d' "$f" + grep -q '[^[:space:]]' "$f" || rm -f "$f" +done +[ -f /etc/modules ] && sed -i '/^[[:space:]]*dummy[[:space:]]*$/d' /etc/modules +modprobe -r dummy 2>/dev/null +true diff --git a/labs/linux-plus/1-2-1-kernel-modules-and-initrd/setup.sh b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/setup.sh new file mode 100644 index 0000000..44bf6a5 --- /dev/null +++ b/labs/linux-plus/1-2-1-kernel-modules-and-initrd/setup.sh @@ -0,0 +1,4 @@ +if grep -rqsw dummy /etc/modules-load.d /etc/modprobe.d /etc/modules; then + refuse "/etc/modules-load.d or /etc/modprobe.d already mentions dummy; remove that first" +fi +modprobe -r dummy 2>/dev/null || true diff --git a/labs/linux-plus/1-2-2-hardware-inventory/README.md b/labs/linux-plus/1-2-2-hardware-inventory/README.md new file mode 100644 index 0000000..77846c6 --- /dev/null +++ b/labs/linux-plus/1-2-2-hardware-inventory/README.md @@ -0,0 +1,29 @@ +# Linux+ 1.2.2: Finding out about hardware + +An inventory of the VM's hardware, the kernel's messages as a device +appears, and PCI devices that no driver has claimed. + +## Before you start + +The lab sets nothing up. Tasks 1 and 3 ask for findings: write them to the +files in `/root/lab-answers/` that the tasks name. + +## Tasks + +1. Build a quick inventory of this VM with `lscpu`, `lsmem`, `lspci`, + `lsusb`, `lshw` and `dmidecode`. The "hardware" is the hypervisor's: write + the system manufacturer that `dmidecode` reports (such as `QEMU`) to + `/root/lab-answers/1`. +2. Run `dmesg -w` in one terminal, then attach a USB device or a new + virtual disk to the VM, and watch the kernel's messages. Not graded. +3. Find every PCI device without a driver, and write their slot addresses + (such as `00:00.0`), one per line, to `/root/lab-answers/3`. If there are + none, write `none`. + +## Hints + +- Minimal RHEL-family installs need `pciutils` and `usbutils` for `lspci` + and `lsusb`. +- `dmidecode -t system`, or `dmidecode -s system-manufacturer`. +- A device that a driver has claimed has a `driver` link in sysfs: + `for d in /sys/bus/pci/devices/*; do [ -e "$d/driver" ] || lspci -s "${d##*/}"; done` diff --git a/labs/linux-plus/1-2-2-hardware-inventory/check.sh b/labs/linux-plus/1-2-2-hardware-inventory/check.sh new file mode 100644 index 0000000..7d8e4ae --- /dev/null +++ b/labs/linux-plus/1-2-2-hardware-inventory/check.sh @@ -0,0 +1,19 @@ +maker=$(dmidecode -s system-manufacturer 2>/dev/null | head -n1 | tr 'A-Z' 'a-z') +# PCI slots without a driver, without the leading domain (0000:). +nodriver=$(for d in /sys/bus/pci/devices/*; do [ -e "$d/driver" ] || echo "${d##*/}"; done | sed 's/^0000://') +# The answer's first words, without a leading domain. +slots() { awk 'NF {print tolower($1)}' /root/lab-answers/3 2>/dev/null | sed 's/^0000://'; } + +task 1 "The system manufacturer" +need "/root/lab-answers/1 names the system manufacturer, $maker" ' + a=$(answer 1); [ -n "$a" ] && { [ "$a" = "$maker" ] || [[ $maker == *"$a"* ]] || [[ $a == *"$maker"* ]]; }' + +task 3 "PCI devices without a driver" +if [ -z "$nodriver" ]; then + need "/root/lab-answers/3 says none" '[ "$(answer 3)" = none ]' +else + need "/root/lab-answers/3 lists every PCI device without a driver" ' + [ -n "$(slots)" ] && while read -r s; do slots | grep -qxF "$s" || exit 1; done <<<"$nodriver"' + need "/root/lab-answers/3 lists only devices without a driver" ' + slots | while read -r s; do grep -qxF "$s" <<<"$nodriver" || exit 1; done' +fi diff --git a/labs/linux-plus/1-2-2-hardware-inventory/lab.conf b/labs/linux-plus/1-2-2-hardware-inventory/lab.conf new file mode 100644 index 0000000..575e1e3 --- /dev/null +++ b/labs/linux-plus/1-2-2-hardware-inventory/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.2.2 Finding out about hardware" +CHAPTER=/linux-plus/1-system-management/1-2-2-hardware-inventory/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-2-2-hardware-inventory/reset.sh b/labs/linux-plus/1-2-2-hardware-inventory/reset.sh new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/labs/linux-plus/1-2-2-hardware-inventory/reset.sh @@ -0,0 +1 @@ +true diff --git a/labs/linux-plus/1-2-2-hardware-inventory/setup.sh b/labs/linux-plus/1-2-2-hardware-inventory/setup.sh new file mode 100644 index 0000000..69a091c --- /dev/null +++ b/labs/linux-plus/1-2-2-hardware-inventory/setup.sh @@ -0,0 +1,2 @@ +# Nothing to set up: the tasks look at the system as it is. +true diff --git a/labs/linux-plus/1-3-1-partitions/README.md b/labs/linux-plus/1-3-1-partitions/README.md new file mode 100644 index 0000000..c1e4a67 --- /dev/null +++ b/labs/linux-plus/1-3-1-partitions/README.md @@ -0,0 +1,30 @@ +# Linux+ 1.3.1: Disks and partitions + +GPT partition tables with fdisk and with parted in script mode, and growing +a partition into free space. + +## Before you start + +The lab adds three empty 2 GiB spare disks: `/dev/loop40`, `/dev/loop41` +and `/dev/loop42`. Their partitions show up as `/dev/loop40p1` and so on. +They come back at every boot. Each task has a disk of its own, so that the +check can see all three results. + +## Tasks + +1. On `/dev/loop40`, make a GPT partition table with two partitions using + fdisk: the first 500 MiB, the second filling the rest of the disk. +2. On `/dev/loop41`, do the same with parted in script mode (`parted -s`), + naming the partitions `one` and `two`. +3. On `/dev/loop42`, make the same two partitions, then delete partition 2 + and grow partition 1 into the free space with `growpart`. + +## Hints + +- `lsblk` first, to be sure you're on the right disk. +- In fdisk: `g` (new GPT table), `n` (new partition, size `+500M`), `n` + again (accept the defaults for the rest), `p`, `w`. +- `parted -s DISK mklabel gpt mkpart one 1MiB 500MiB mkpart two 500MiB 100%`; + `parted DISK print`. +- `parted -s DISK rm 2`, then `growpart DISK 1` (in `cloud-utils-growpart` + on the RHEL family, `cloud-guest-utils` on Ubuntu). diff --git a/labs/linux-plus/1-3-1-partitions/check.sh b/labs/linux-plus/1-3-1-partitions/check.sh new file mode 100644 index 0000000..09c6f1e --- /dev/null +++ b/labs/linux-plus/1-3-1-partitions/check.sh @@ -0,0 +1,27 @@ +# parted's machine-readable listing in MiB: NUMBER:START:END:SIZE:FS:NAME:FLAGS +parts() { parted -m -s "$1" unit MiB print 2>/dev/null | grep -E '^[0-9]+:' | tr -d 'MiB;'; } +label() { parted -m -s "$1" print 2>/dev/null | awk -F: 'NR == 2 {print $6}'; } +# field DISK PART N: field N of partition PART, whole MiB. +field() { parts "$1" | awk -F: -v p="$2" -v n="$3" '$1 == p {printf "%d", $n}'; } +count() { parts "$1" | wc -l; } +between() { [ -n "$1" ] && [ "$1" -ge "$2" ] && [ "$1" -le "$3" ]; } + +task 1 "loop40: GPT, 500 MiB and the rest, with fdisk" +need "/dev/loop40 has a GPT partition table" '[ "$(label /dev/loop40)" = gpt ]' +need "/dev/loop40 has two partitions" '[ "$(count /dev/loop40)" = 2 ]' +need "partition 1 is 500 MiB" 'between "$(field /dev/loop40 1 4)" 495 505' +need "partition 2 fills the rest of the disk" 'between "$(field /dev/loop40 2 3)" 2040 2048 && between "$(field /dev/loop40 2 4)" 1500 1550' + +task 2 "loop41: GPT with partitions one and two, with parted" +need "/dev/loop41 has a GPT partition table" '[ "$(label /dev/loop41)" = gpt ]' +need "/dev/loop41 has two partitions" '[ "$(count /dev/loop41)" = 2 ]' +need "they're named one and two" '[ "$(parts /dev/loop41 | cut -d: -f6 | paste -sd,)" = one,two ]' +need "partition 1 is about 500 MiB" 'between "$(field /dev/loop41 1 4)" 490 505' +need "partition 2 fills the rest of the disk" 'between "$(field /dev/loop41 2 3)" 2040 2048 && between "$(field /dev/loop41 2 4)" 1500 1560' + +task 3 "loop42: partition 2 deleted, partition 1 grown into the space" +need "/dev/loop42 has a GPT partition table" '[ "$(label /dev/loop42)" = gpt ]' +need "/dev/loop42 has only partition 1" '[ "$(parts /dev/loop42 | cut -d: -f1 | paste -sd,)" = 1 ]' +need "partition 1 starts where it did (near the start of the disk)" 'between "$(field /dev/loop42 1 2)" 0 2' +need "partition 1 fills the disk" 'between "$(field /dev/loop42 1 3)" 2040 2048' +need "the kernel sees the grown partition" 'between "$(( $(blockdev --getsize64 /dev/loop42p1 2>/dev/null || echo 0) / 1048576 ))" 2030 2048' diff --git a/labs/linux-plus/1-3-1-partitions/lab.conf b/labs/linux-plus/1-3-1-partitions/lab.conf new file mode 100644 index 0000000..0f10f39 --- /dev/null +++ b/labs/linux-plus/1-3-1-partitions/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.3.1 Disks and partitions" +CHAPTER=/linux-plus/1-system-management/1-3-1-partitions/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-3-1-partitions/reset.sh b/labs/linux-plus/1-3-1-partitions/reset.sh new file mode 100644 index 0000000..9b3539a --- /dev/null +++ b/labs/linux-plus/1-3-1-partitions/reset.sh @@ -0,0 +1,2 @@ +# The spare disks, and their partitions, are removed after this. +true diff --git a/labs/linux-plus/1-3-1-partitions/setup.sh b/labs/linux-plus/1-3-1-partitions/setup.sh new file mode 100644 index 0000000..e4f6fdd --- /dev/null +++ b/labs/linux-plus/1-3-1-partitions/setup.sh @@ -0,0 +1,3 @@ +disk_add 0 2G >/dev/null +disk_add 1 2G >/dev/null +disk_add 2 2G >/dev/null diff --git a/labs/linux-plus/1-3-2-lvm/README.md b/labs/linux-plus/1-3-2-lvm/README.md new file mode 100644 index 0000000..dab0e57 --- /dev/null +++ b/labs/linux-plus/1-3-2-lvm/README.md @@ -0,0 +1,30 @@ +# Linux+ 1.3.2: Logical volume management + +A volume group with two logical volumes, growing one while it's mounted, and +emptying a disk to take it out of the group. + +## Before you start + +The lab adds two empty 1 GiB spare disks, `/dev/loop40` and `/dev/loop41` +(the chapter's `vdb` and `vdc`). They come back at every boot. Nothing here +is mounted at boot, so do the tasks in one go, without a reboot, and run +`le-lab check` before you clean up. + +## Tasks + +1. Make both disks physical volumes in a volume group `vglab`. Create a + 600 MiB logical volume `one` and a logical volume `two` from all the rest + of the space. Put ext4 on `one` and mount it at `/mnt/one`. +2. Remove `two`, and grow `one` and its filesystem by 300 MiB while it's + mounted. +3. Move everything off `/dev/loop40` and take it out of `vglab`. + +When `le-lab check` passes, clean up as the chapter shows (unmount, remove +the volume group and the physical volumes), or leave it to `le-lab reset`. + +## Hints + +- `pvcreate`, `vgcreate`, `lvcreate -n -L`, `lvcreate -l 100%FREE`, + `mkfs.ext4`; `pvs`, `vgs`, `lvs`. +- `lvremove`, `lvextend -r -L +300M`; `df -h /mnt/one`. +- `pvs -o+pv_used`, `pvmove`, `vgreduce`. diff --git a/labs/linux-plus/1-3-2-lvm/check.sh b/labs/linux-plus/1-3-2-lvm/check.sh new file mode 100644 index 0000000..50f9ba7 --- /dev/null +++ b/labs/linux-plus/1-3-2-lvm/check.sh @@ -0,0 +1,18 @@ +lv_mib() { lvs --noheadings --units m --nosuffix -o lv_size "vglab/$1" 2>/dev/null | awk '{printf "%d", $1}'; } +pv_vg() { pvs --noheadings -o vg_name "$1" 2>/dev/null | tr -d ' '; } + +task 1 "vglab with an ext4 volume one mounted at /mnt/one" +need "volume group vglab exists" vgs vglab +need "logical volume vglab/one exists" lvs vglab/one +need "one holds an ext4 filesystem" '[ "$(blkid -s TYPE -o value /dev/vglab/one)" = ext4 ]' +need "/mnt/one is mounted from one" '[ "$(findmnt -n -o SOURCE /mnt/one)" = /dev/mapper/vglab-one ]' + +task 2 "two removed, one and its filesystem grown by 300 MiB" +need "logical volume two is gone" '! lvs vglab/two' +need "one is 900 MiB (600 + 300)" 'v=$(lv_mib one); [ -n "$v" ] && [ "$v" -ge 896 ] && [ "$v" -le 904 ]' +need "the filesystem on /mnt/one has grown too" '[ "$(df -BM --output=size /mnt/one | tail -n1 | tr -dc 0-9)" -ge 840 ]' + +task 3 "/dev/loop40 emptied and taken out of vglab" +need "/dev/loop40 is no longer in vglab" '[ "$(pv_vg /dev/loop40)" != vglab ]' +need "/dev/loop41 is still in vglab" '[ "$(pv_vg /dev/loop41)" = vglab ]' +need "/mnt/one is still mounted from one" '[ "$(findmnt -n -o SOURCE /mnt/one)" = /dev/mapper/vglab-one ]' diff --git a/labs/linux-plus/1-3-2-lvm/lab.conf b/labs/linux-plus/1-3-2-lvm/lab.conf new file mode 100644 index 0000000..636b729 --- /dev/null +++ b/labs/linux-plus/1-3-2-lvm/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.3.2 Logical volume management" +CHAPTER=/linux-plus/1-system-management/1-3-2-lvm/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-3-2-lvm/reset.sh b/labs/linux-plus/1-3-2-lvm/reset.sh new file mode 100644 index 0000000..b60796e --- /dev/null +++ b/labs/linux-plus/1-3-2-lvm/reset.sh @@ -0,0 +1,5 @@ +findmnt -rn -o SOURCE /mnt/one 2>/dev/null | grep -q vglab && umount -l /mnt/one +vgchange -an vglab >/dev/null 2>&1 +vgremove -ff vglab >/dev/null 2>&1 +rmdir /mnt/one 2>/dev/null +true diff --git a/labs/linux-plus/1-3-2-lvm/setup.sh b/labs/linux-plus/1-3-2-lvm/setup.sh new file mode 100644 index 0000000..faa06f5 --- /dev/null +++ b/labs/linux-plus/1-3-2-lvm/setup.sh @@ -0,0 +1,4 @@ +vgs vglab >/dev/null 2>&1 && refuse "a volume group vglab already exists; remove it first" +findmnt /mnt/one >/dev/null && refuse "something is mounted at /mnt/one; unmount it first" +disk_add 0 1G >/dev/null +disk_add 1 1G >/dev/null diff --git a/labs/linux-plus/1-3-3-filesystems/README.md b/labs/linux-plus/1-3-3-filesystems/README.md new file mode 100644 index 0000000..ebe7e39 --- /dev/null +++ b/labs/linux-plus/1-3-3-filesystems/README.md @@ -0,0 +1,29 @@ +# Linux+ 1.3.3: Filesystems + +An ext4 and an XFS filesystem, running out of inodes with space to spare, a +filesystem check, and a quick benchmark. + +## Before you start + +The lab adds two empty 1 GiB spare disks, `/dev/loop40` and `/dev/loop41` +(the chapter's `vdb` and `vdc`). They come back at every boot. + +## Tasks + +1. Make an ext4 filesystem with only about 2000 inodes on `/dev/loop40`, + and an XFS filesystem on `/dev/loop41`. Mount them at `/mnt/e` and + `/mnt/x`, and compare their space and inodes. +2. Fill `/mnt/e` with empty files until it runs out of inodes while it + still has space. Write the number of files you managed to create to + `/root/lab-answers/2`, then delete them. +3. Unmount `/mnt/e` and check its filesystem with a forced fsck. Install + fio and benchmark `/mnt/x` (random reads and writes of 4 KiB blocks on a + 200 MiB file, for 15 seconds), then remove the test file and unmount + `/mnt/x`. + +## Hints + +- `mkfs.ext4 -N`, `mkfs.xfs`; `df -hT` and `df -i`. +- `for i in $(seq 1 3000); do touch /mnt/e/f$i || break; done`; `ls /mnt/e | wc -l`. +- `fsck -f`; `dumpe2fs -h` shows when a filesystem was last checked. +- `fio --name=t --filename=/mnt/x/t --size=200M --rw=randrw --bs=4k --direct=1 --runtime=15 --time_based` diff --git a/labs/linux-plus/1-3-3-filesystems/check.sh b/labs/linux-plus/1-3-3-filesystems/check.sh new file mode 100644 index 0000000..1a70315 --- /dev/null +++ b/labs/linux-plus/1-3-3-filesystems/check.sh @@ -0,0 +1,23 @@ +sb() { dumpe2fs -h /dev/loop40 2>/dev/null | awk -F: -v k="$1" '$1 == k {sub(/^[ \t]+/, "", $2); s = $2; for (i = 3; i <= NF; i++) s = s ":" $i; print s}'; } +fstype() { blkid -s TYPE -o value "$1" 2>/dev/null; } +inodes=$(sb 'Inode count') +first=$(sb 'First inode') + +task 1 "ext4 with about 2000 inodes on loop40, XFS on loop41" +need "/dev/loop40 holds ext4" '[ "$(fstype /dev/loop40)" = ext4 ]' +need "the ext4 filesystem has about 2000 inodes (not tens of thousands)" '[ -n "$inodes" ] && [ "$inodes" -ge 1500 ] && [ "$inodes" -le 4096 ]' +need "/dev/loop41 holds XFS" '[ "$(fstype /dev/loop41)" = xfs ]' + +task 2 "/mnt/e run out of inodes, the count noted, the files deleted" +need "/root/lab-answers/2 has the number of files that fit ($((inodes - first)))" ' + a=$(answer 2 | tr -dc 0-9); [ -n "$a" ] && [ "$a" -ge $((inodes - first - 2)) ] && [ "$a" -le $((inodes - first + 1)) ]' +need "the files are deleted again" '[ "$(sb "Free inodes")" -ge $((inodes - first - 2)) ]' + +task 3 "ext4 checked with fsck, XFS benchmarked with fio" +need "/dev/loop40 isn't mounted" '! findmnt -S /dev/loop40' +need "fsck checked /dev/loop40 after it was made" '[ "$(date -d "$(sb "Last checked")" +%s)" -gt "$(date -d "$(sb "Filesystem created")" +%s)" ]' +need "fio is installed" 'command -v fio' +need "/dev/loop41 isn't mounted" '! findmnt -S /dev/loop41' +need "the test file is removed" ' + m=$(mktemp -d); mount -o ro /dev/loop41 "$m" || exit 1 + n=$(find "$m" -mindepth 1 | wc -l); umount "$m"; rmdir "$m"; [ "$n" = 0 ]' diff --git a/labs/linux-plus/1-3-3-filesystems/lab.conf b/labs/linux-plus/1-3-3-filesystems/lab.conf new file mode 100644 index 0000000..f73be3c --- /dev/null +++ b/labs/linux-plus/1-3-3-filesystems/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.3.3 Filesystems" +CHAPTER=/linux-plus/1-system-management/1-3-3-filesystems/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-3-3-filesystems/reset.sh b/labs/linux-plus/1-3-3-filesystems/reset.sh new file mode 100644 index 0000000..9d3a8fb --- /dev/null +++ b/labs/linux-plus/1-3-3-filesystems/reset.sh @@ -0,0 +1,5 @@ +for m in /mnt/e /mnt/x; do + findmnt -rn -o SOURCE "$m" 2>/dev/null | grep -q loop4 && umount -l "$m" + rmdir "$m" 2>/dev/null +done +true diff --git a/labs/linux-plus/1-3-3-filesystems/setup.sh b/labs/linux-plus/1-3-3-filesystems/setup.sh new file mode 100644 index 0000000..e45a06c --- /dev/null +++ b/labs/linux-plus/1-3-3-filesystems/setup.sh @@ -0,0 +1,5 @@ +for m in /mnt/e /mnt/x; do + findmnt "$m" >/dev/null && refuse "something is mounted at $m; unmount it first" +done +disk_add 0 1G >/dev/null +disk_add 1 1G >/dev/null diff --git a/labs/linux-plus/1-3-4-mounting/README.md b/labs/linux-plus/1-3-4-mounting/README.md new file mode 100644 index 0000000..a1798e5 --- /dev/null +++ b/labs/linux-plus/1-3-4-mounting/README.md @@ -0,0 +1,32 @@ +# Linux+ 1.3.4: Mounting filesystems + +An `/etc/fstab` line by UUID with mount options, `noexec` in action, a +remount, and finding what keeps a mount busy. + +## Before you start + +The lab adds a 512 MiB spare disk, `/dev/loop40`, with an ext4 filesystem on +it. It comes back at every boot. Task 2 changes the mount only until it's +mounted again, so do the tasks in one go and run `le-lab check` before you +clean up. + +## Tasks + +1. Add `/dev/loop40` to `/etc/fstab` by its UUID, mounted at `/mnt/lab` with + the options `defaults,noatime,noexec,nofail`, and mount it from fstab. +2. Copy `/usr/bin/true` into `/mnt/lab` and show that it won't run from + there. Then remount `/mnt/lab` so that programs can run from it, without + changing `/etc/fstab`. +3. Make the mount busy (`cd /mnt/lab`), try to unmount it, and find the + culprit with `lsof`. Not graded. + +When `le-lab check` passes, remove your line from `/etc/fstab`, run +`systemctl daemon-reload` and unmount, or leave it to `le-lab reset`. + +## Hints + +- `blkid /dev/loop40`, `blkid -s UUID -o value`. +- After editing fstab: `systemctl daemon-reload`, `mount -a`, `findmnt /mnt/lab`. +- `mount -o remount,exec`. +- `lsof +D /mnt/lab`, `fuser -vm /mnt/lab`. Minimal RHEL-family installs need + the `lsof` package. diff --git a/labs/linux-plus/1-3-4-mounting/check.sh b/labs/linux-plus/1-3-4-mounting/check.sh new file mode 100644 index 0000000..77ab43e --- /dev/null +++ b/labs/linux-plus/1-3-4-mounting/check.sh @@ -0,0 +1,20 @@ +UUID=$(state_get uuid) +# The fstab line for /mnt/lab, comments left out. +line() { awk '$1 !~ /^#/ && $2 == "/mnt/lab"' /etc/fstab; } +has_opt() { line | awk '{print $4}' | tr , '\n' | grep -qx "$1"; } +live_opt() { findmnt -n -o OPTIONS /mnt/lab | tr , '\n' | grep -qx "$1"; } + +task 1 "/dev/loop40 in fstab by UUID at /mnt/lab with noatime,noexec,nofail" +need "/etc/fstab has one line for /mnt/lab" '[ "$(line | wc -l)" = 1 ]' +need "the line names the disk by UUID=$UUID" '[ "$(line | awk "{print \$1}" | tr -d "\"")" = "UUID=$UUID" ]' +need "the line has ext4 and the options noatime, noexec and nofail" '[ "$(line | awk "{print \$3}")" = ext4 ] && has_opt noatime && has_opt noexec && has_opt nofail' +need "/mnt/lab is mounted from /dev/loop40" '[ "$(findmnt -n -o SOURCE /mnt/lab)" = /dev/loop40 ]' +need "the mount uses noatime" 'live_opt noatime' + +task 2 "true copied in; /mnt/lab remounted to allow programs" +need "/mnt/lab/true is there" '[ -f /mnt/lab/true ] && [ -x /mnt/lab/true ]' +need "/mnt/lab is mounted without noexec now" 'findmnt /mnt/lab && ! live_opt noexec' +need "/mnt/lab/true runs" /mnt/lab/true +need "/etc/fstab still says noexec" 'has_opt noexec' + +task 3 "The busy mount's culprit found with lsof" self diff --git a/labs/linux-plus/1-3-4-mounting/lab.conf b/labs/linux-plus/1-3-4-mounting/lab.conf new file mode 100644 index 0000000..a27dac0 --- /dev/null +++ b/labs/linux-plus/1-3-4-mounting/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.3.4 Mounting filesystems" +CHAPTER=/linux-plus/1-system-management/1-3-4-mounting/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-3-4-mounting/reset.sh b/labs/linux-plus/1-3-4-mounting/reset.sh new file mode 100644 index 0000000..d3fba17 --- /dev/null +++ b/labs/linux-plus/1-3-4-mounting/reset.sh @@ -0,0 +1,7 @@ +UUID=$(state_get uuid) +[ -n "$UUID" ] && sed -i "/$UUID/d" /etc/fstab +sed -i -E '/^[^#]*[[:space:]]\/mnt\/lab[[:space:]]/d' /etc/fstab +systemctl daemon-reload +findmnt /mnt/lab >/dev/null && umount -l /mnt/lab +rmdir /mnt/lab 2>/dev/null +true diff --git a/labs/linux-plus/1-3-4-mounting/setup.sh b/labs/linux-plus/1-3-4-mounting/setup.sh new file mode 100644 index 0000000..080fe10 --- /dev/null +++ b/labs/linux-plus/1-3-4-mounting/setup.sh @@ -0,0 +1,5 @@ +findmnt /mnt/lab >/dev/null && refuse "something is mounted at /mnt/lab; unmount it first" +grep -Eq '^[^#]*[[:space:]]/mnt/lab[[:space:]]' /etc/fstab && refuse "/etc/fstab already has a line for /mnt/lab; remove it first" +disk_add 0 512M >/dev/null +mkfs.ext4 -q /dev/loop40 +state_set uuid "$(blkid -s UUID -o value /dev/loop40)" diff --git a/labs/linux-plus/1-3-5-raid/README.md b/labs/linux-plus/1-3-5-raid/README.md new file mode 100644 index 0000000..bf7367e --- /dev/null +++ b/labs/linux-plus/1-3-5-raid/README.md @@ -0,0 +1,33 @@ +# Linux+ 1.3.5: Software RAID + +A RAID 1 mirror with a hot spare, a failed disk, the spare taking over, and +the old disk coming back as the new spare. + +## Before you start + +The lab installs `mdadm` and adds three empty 1 GiB spare disks, +`/dev/loop40`, `/dev/loop41` and `/dev/loop42` (the chapter's `vdb`, `vdc` +and `vdd`). They come back at every boot, but the array isn't set up to +start and mount at boot, so do the tasks in one go and run `le-lab check` +before you clean up. + +## Tasks + +1. Build `/dev/md0`, a RAID 1 of `/dev/loop40` and `/dev/loop41` with + `/dev/loop42` as a spare. Put ext4 on it, mount it at `/mnt/raid`, and + write `hello` to `/mnt/raid/file`. +2. Mark `/dev/loop40` as failed and watch the spare take over. The file is + still readable. +3. Remove the failed disk from the array, wipe its RAID superblock, and add + it back as the new spare. + +When `le-lab check` passes, clean up as the chapter shows (unmount, stop the +array, wipe the superblocks), or leave it to `le-lab reset`. + +## Hints + +- `mdadm --create /dev/md0 --level=1 --raid-devices=2 --spare-devices=1 --metadata=1.2 ...`; + `cat /proc/mdstat`; `mdadm --wait /dev/md0` waits for a sync to finish. +- `mdadm /dev/md0 --fail DISK`; `mdadm --detail /dev/md0`. +- `mdadm /dev/md0 --remove DISK`, `mdadm --zero-superblock DISK`, + `mdadm /dev/md0 --add DISK`. diff --git a/labs/linux-plus/1-3-5-raid/check.sh b/labs/linux-plus/1-3-5-raid/check.sh new file mode 100644 index 0000000..99d9a4a --- /dev/null +++ b/labs/linux-plus/1-3-5-raid/check.sh @@ -0,0 +1,22 @@ +# A disk's role in md0 as mdadm --detail shows it: "active sync", "spare", +# "faulty", or nothing. +role() { mdadm --detail /dev/md0 2>/dev/null | awk -v d="$1" '$NF == d {$1 = $2 = $3 = $4 = $NF = ""; gsub(/^ +| +$/, ""); print}'; } +detail() { mdadm --detail /dev/md0 2>/dev/null | awk -F' : ' -v k="$1" '{gsub(/^ +/, "", $1)} $1 == k {print $2}'; } + +task 1 "md0: RAID 1 with a spare, ext4, mounted at /mnt/raid, file says hello" +need "/dev/md0 is a RAID 1 array" '[ "$(detail "Raid Level")" = raid1 ]' +need "md0 mirrors two disks" '[ "$(detail "Raid Devices")" = 2 ]' +need "md0 holds ext4" '[ "$(blkid -s TYPE -o value /dev/md0)" = ext4 ]' +need "/mnt/raid is mounted from md0" '[ "$(findmnt -n -o SOURCE /mnt/raid)" = /dev/md0 ]' +need "/mnt/raid/file says hello" 'grep -qx hello /mnt/raid/file' + +task 2 "loop40 failed, the spare loop42 took over" +need "/dev/loop42 is an active member of md0" '[[ $(role /dev/loop42) == "active sync"* ]]' +need "/dev/loop41 is still an active member" '[[ $(role /dev/loop41) == "active sync"* ]]' +need "md0 has finished rebuilding (clean, not degraded)" ' + s=$(detail State); [[ $s == clean* || $s == active* ]] && [[ $s != *degraded* && $s != *recovering* && $s != *resyncing* ]]' +need "/mnt/raid/file still says hello" 'grep -qx hello /mnt/raid/file' + +task 3 "loop40 removed, wiped and added back as the spare" +need "/dev/loop40 is md0's spare" '[ "$(role /dev/loop40)" = spare ]' +need "md0 has no failed disks" '[ "$(detail "Failed Devices")" = 0 ]' diff --git a/labs/linux-plus/1-3-5-raid/lab.conf b/labs/linux-plus/1-3-5-raid/lab.conf new file mode 100644 index 0000000..7385398 --- /dev/null +++ b/labs/linux-plus/1-3-5-raid/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.3.5 Software RAID" +CHAPTER=/linux-plus/1-system-management/1-3-5-raid/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-3-5-raid/reset.sh b/labs/linux-plus/1-3-5-raid/reset.sh new file mode 100644 index 0000000..1ec61e1 --- /dev/null +++ b/labs/linux-plus/1-3-5-raid/reset.sh @@ -0,0 +1,11 @@ +findmnt /mnt/raid >/dev/null && umount -l /mnt/raid +# Stop every array built from the spare disks, then wipe their superblocks. +for md in /sys/block/md*; do + [ -e "$md" ] || continue + ls "$md/slaves" 2>/dev/null | grep -q '^loop4[0-9]' && mdadm --stop "/dev/${md##*/}" >/dev/null 2>&1 +done +for dev in /dev/loop40 /dev/loop41 /dev/loop42; do + [ -e "$dev" ] && mdadm --zero-superblock "$dev" >/dev/null 2>&1 +done +rmdir /mnt/raid 2>/dev/null +true diff --git a/labs/linux-plus/1-3-5-raid/setup.sh b/labs/linux-plus/1-3-5-raid/setup.sh new file mode 100644 index 0000000..3289a3e --- /dev/null +++ b/labs/linux-plus/1-3-5-raid/setup.sh @@ -0,0 +1,7 @@ +[ -e /dev/md0 ] && [ -n "$(cat /sys/block/md0/md/array_state 2>/dev/null | grep -v clear)" ] && + refuse "/dev/md0 is already in use; stop it first" +findmnt /mnt/raid >/dev/null && refuse "something is mounted at /mnt/raid; unmount it first" +command -v mdadm >/dev/null || pkg_install mdadm +disk_add 0 1G >/dev/null +disk_add 1 1G >/dev/null +disk_add 2 1G >/dev/null diff --git a/labs/linux-plus/1-4-1-network-configuration/README.md b/labs/linux-plus/1-4-1-network-configuration/README.md new file mode 100644 index 0000000..4f34d89 --- /dev/null +++ b/labs/linux-plus/1-4-1-network-configuration/README.md @@ -0,0 +1,38 @@ +# Linux+ 1.4.1: Network configuration + +How names are resolved, and a second address on an interface: with +NetworkManager on the RHEL family, with a Netplan file on Ubuntu. + +## Before you start + +So that you never touch the interface you're connected over, the lab adds a +practice interface, `lab0` (a dummy device, with nothing on the other end). +On the RHEL family it's NetworkManager connection `lab0`, with the address +`192.168.76.1/24`. On Ubuntu, systemd-networkd creates it with no address. +It comes back at every boot. Use `lab0` wherever the chapter uses your main +connection or interface. + +## Tasks + +1. Make the name `labtest` resolve to `10.99.99.99` without DNS. Check it + with `getent hosts labtest`, and see that `dig +short labtest` (which + only asks DNS) doesn't find it. Leave the line in place until you've run + `le-lab check`. +2. RHEL family: add a second address, `192.168.77.10/24`, to connection + `lab0`, keeping the first, and bring the connection up again. +3. Ubuntu: give `lab0` the address `192.168.77.10/24` with a new Netplan + file, `/etc/netplan/90-lab.yaml`, readable only by root, and apply it + with `netplan try`. + +When `le-lab check` passes, take the line and the address out again as the +chapter shows, or leave it to `le-lab reset`. + +## Hints + +- `/etc/resolv.conf`, the `hosts:` line in `/etc/nsswitch.conf`, + `/etc/hosts`. On the RHEL family, `dig` is in `bind-utils`. +- `nmcli -t -f NAME,DEVICE con show --active`, + `nmcli con mod NAME +ipv4.addresses ...`, `nmcli con up NAME`, + `ip -br addr`. +- An `ethernets:` entry for `lab0` with `addresses: [...]`; `chmod 600`; + `netplan try` (press Enter to keep it), `netplan status`. diff --git a/labs/linux-plus/1-4-1-network-configuration/check.sh b/labs/linux-plus/1-4-1-network-configuration/check.sh new file mode 100644 index 0000000..8079de8 --- /dev/null +++ b/labs/linux-plus/1-4-1-network-configuration/check.sh @@ -0,0 +1,20 @@ +has_live() { ip -o -4 addr show dev lab0 2>/dev/null | grep -qF " $1 "; } + +task 1 "labtest resolves to 10.99.99.99 without DNS" +need "getent hosts labtest prints 10.99.99.99" 'getent hosts labtest | grep -q "^10\.99\.99\.99[[:space:]]"' +need "/etc/hosts has the line" 'grep -Eq "^10\.99\.99\.99[[:space:]].*\blabtest\b" /etc/hosts' + +task 2 "Connection lab0 has 192.168.77.10/24 as well" rhel +if is_rhel; then + need "connection lab0 has 192.168.77.10/24" 'nmcli -g ipv4.addresses connection show lab0 | grep -qF 192.168.77.10/24' + need "connection lab0 keeps 192.168.76.1/24" 'nmcli -g ipv4.addresses connection show lab0 | grep -qF 192.168.76.1/24' + need "lab0 has both addresses now (connection up)" 'has_live 192.168.77.10/24 && has_live 192.168.76.1/24' +fi + +task 3 "lab0 has 192.168.77.10/24 from /etc/netplan/90-lab.yaml" debian +if is_debian; then + need "/etc/netplan/90-lab.yaml exists" '[ -f /etc/netplan/90-lab.yaml ]' + need "only root can read it (mode 600)" '[ "$(stat -c %a /etc/netplan/90-lab.yaml)" = 600 ]' + need "it gives lab0 192.168.77.10/24" 'netplan get ethernets.lab0.addresses 2>/dev/null | grep -qF 192.168.77.10/24' + need "lab0 has 192.168.77.10/24 now (applied)" 'has_live 192.168.77.10/24' +fi diff --git a/labs/linux-plus/1-4-1-network-configuration/lab.conf b/labs/linux-plus/1-4-1-network-configuration/lab.conf new file mode 100644 index 0000000..c03ee91 --- /dev/null +++ b/labs/linux-plus/1-4-1-network-configuration/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.4.1 Network configuration" +CHAPTER=/linux-plus/1-system-management/1-4-1-network-configuration/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-4-1-network-configuration/reset.sh b/labs/linux-plus/1-4-1-network-configuration/reset.sh new file mode 100644 index 0000000..a5b6c5f --- /dev/null +++ b/labs/linux-plus/1-4-1-network-configuration/reset.sh @@ -0,0 +1,10 @@ +sed -i -E '/^[^#]*[[:space:]]labtest([[:space:]]|$)/d' /etc/hosts +if is_rhel; then + nmcli connection delete lab0 >/dev/null 2>&1 +else + rm -f /etc/netplan/90-lab.yaml /etc/systemd/network/10-le-lab0.netdev + netplan generate 2>/dev/null + networkctl reload 2>/dev/null +fi +ip link del lab0 2>/dev/null +true diff --git a/labs/linux-plus/1-4-1-network-configuration/setup.sh b/labs/linux-plus/1-4-1-network-configuration/setup.sh new file mode 100644 index 0000000..e05a101 --- /dev/null +++ b/labs/linux-plus/1-4-1-network-configuration/setup.sh @@ -0,0 +1,23 @@ +[ -e /sys/class/net/lab0 ] && refuse "an interface lab0 already exists; remove it first" +if is_rhel; then + command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager || + refuse "this lab needs NetworkManager running" + nmcli -g NAME connection show | grep -qx lab0 && refuse "a NetworkManager connection lab0 already exists; delete it first" +else + command -v netplan >/dev/null && systemctl is-active --quiet systemd-networkd || + refuse "this lab needs Netplan with systemd-networkd" + [ -e /etc/netplan/90-lab.yaml ] && refuse "/etc/netplan/90-lab.yaml already exists; remove it first" +fi +grep -Eq '^[^#]*[[:space:]]labtest([[:space:]]|$)' /etc/hosts && refuse "/etc/hosts already has labtest; remove that line first" + +if is_rhel; then + nmcli -w 20 connection add type dummy ifname lab0 con-name lab0 \ + ipv4.method manual ipv4.addresses 192.168.76.1/24 ipv6.method disabled >/dev/null + nmcli -w 20 connection up lab0 >/dev/null +else + mkdir -p /etc/systemd/network + printf '[NetDev]\nName=lab0\nKind=dummy\n' >/etc/systemd/network/10-le-lab0.netdev + networkctl reload + for i in $(seq 20); do [ -e /sys/class/net/lab0 ] && break; sleep 0.5; done + [ -e /sys/class/net/lab0 ] +fi diff --git a/labs/linux-plus/1-4-2-network-tools/README.md b/labs/linux-plus/1-4-2-network-tools/README.md new file mode 100644 index 0000000..b53441f --- /dev/null +++ b/labs/linux-plus/1-4-2-network-tools/README.md @@ -0,0 +1,40 @@ +# Linux+ 1.4.2: Network tools + +Mapping the VM's network, following a lookup and a connection, capturing +DNS traffic, and testing a port and throughput between two machines. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the other VM. It sits +on this VM's second network, `10.99.0.0/24`, where this VM is `10.99.0.1`. +It shares this VM's files and installed programs, but has its own network +stack: get a shell there with `sudo ip netns exec lepeer bash`. Tasks 1 and +2 ask for findings: write them to the files in `/root/lab-answers/` that +they name. Task 2 needs internet access. Reset closes the ports again and +removes `iperf3` and `nc` if they weren't installed before the lab. + +## Tasks + +1. Map this VM's network: addresses, routes, neighbors, listening ports. + Write the address of the default gateway to `/root/lab-answers/1`. +2. Look up `www.kernel.org`, trace the path to it, and fetch the headers of + `https://www.kernel.org`. Write the first line of the headers (the + status line, such as `HTTP/1.1 301 Moved Permanently`) to + `/root/lab-answers/2`. +3. Capture your own DNS traffic with `tcpdump` while you look up a name. + Not graded. +4. Test a port and the throughput between this VM (VM A) and the peer + (VM B). Install `iperf3` and `nc`, and if a firewall is running here, + open ports 9000/tcp and 5201/tcp until the next reboot. Run `nc -l 9000` + here and `nc -zv 10.99.0.1 9000` in the peer, then `iperf3 -s` here and + `iperf3 -c 10.99.0.1` in the peer. + +## Hints + +- `ip -br a`, `ip r`, `ip neigh`, `ss -tulnp`, `hostname -I`. +- On the RHEL family, `dig` is in `bind-utils`, and `nc` in `nmap-ncat`. + `dig +short`, `tracepath -n`, `curl -sI URL | head -1`. +- `tcpdump -i any -nn port 53` in one terminal, `dig example.org` in + another; Ctrl+c stops tcpdump. +- `firewall-cmd --add-port=9000/tcp --add-port=5201/tcp` (without + `--permanent`); with ufw, `ufw allow 9000/tcp`. diff --git a/labs/linux-plus/1-4-2-network-tools/check.sh b/labs/linux-plus/1-4-2-network-tools/check.sh new file mode 100644 index 0000000..4e78bd6 --- /dev/null +++ b/labs/linux-plus/1-4-2-network-tools/check.sh @@ -0,0 +1,30 @@ +gw=$(ip -4 route show default | awk '{print $3; exit}') +# reach PORT: the peer can open a TCP connection to PORT here. If nothing +# listens there, a listener runs for a moment so that the firewall is tested. +reach() { + local pid='' ok=1 + if ! listening "$1" tcp; then + python3 -c 'import socket,sys,time +s=socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.bind(("0.0.0.0", int(sys.argv[1]))); s.listen(5); time.sleep(12)' "$1" & + pid=$! + for i in 1 2 3 4 5 6 7 8 9 10; do listening "$1" tcp && break; sleep 0.3; done + fi + in_peer timeout 6 bash -c "/dev/null + return $ok +} + +task 1 "The default gateway's address" +need "/root/lab-answers/1 has the default gateway, $gw" '[ "$(answer 1)" = "$gw" ]' + +task 2 "The status line of https://www.kernel.org" +need "/root/lab-answers/2 has an HTTP status line" 'answer 2 | grep -Eq "^http/[0-9.]+ [1-5][0-9][0-9]"' + +task 3 "DNS traffic captured with tcpdump" self + +task 4 "iperf3 and nc installed; the peer reaches ports 9000 and 5201 here" +need "iperf3 is installed" 'command -v iperf3' +need "nc is installed" 'command -v nc' +need "the peer can connect to port 9000/tcp here" 'reach 9000' +need "the peer can connect to port 5201/tcp here" 'reach 5201' diff --git a/labs/linux-plus/1-4-2-network-tools/lab.conf b/labs/linux-plus/1-4-2-network-tools/lab.conf new file mode 100644 index 0000000..48dda14 --- /dev/null +++ b/labs/linux-plus/1-4-2-network-tools/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.4.2 Network tools" +CHAPTER=/linux-plus/1-system-management/1-4-2-network-tools/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-4-2-network-tools/reset.sh b/labs/linux-plus/1-4-2-network-tools/reset.sh new file mode 100644 index 0000000..8d062fe --- /dev/null +++ b/labs/linux-plus/1-4-2-network-tools/reset.sh @@ -0,0 +1,19 @@ +if systemctl is-active --quiet firewalld; then + for p in 9000 5201; do + [ -n "$(state_get fw-had-$p)" ] || firewall-cmd -q --remove-port=$p/tcp 2>/dev/null + done +elif command -v ufw >/dev/null && ufw status | grep -q 'Status: active'; then + for p in 9000 5201; do + [ -n "$(state_get ufw-had-$p)" ] || { ufw delete allow $p/tcp; ufw delete allow $p; } >/dev/null 2>&1 + done +fi +true +# The packages task 4 installed, if they weren't there before the lab. +if [ -s "$LAB_STATE/not-installed" ]; then + if is_rhel; then + dnf -y -q remove $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1 + else + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1 + fi +fi +true diff --git a/labs/linux-plus/1-4-2-network-tools/setup.sh b/labs/linux-plus/1-4-2-network-tools/setup.sh new file mode 100644 index 0000000..83212e7 --- /dev/null +++ b/labs/linux-plus/1-4-2-network-tools/setup.sh @@ -0,0 +1,14 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +[ -n "$IF" ] || refuse "no default route; this lab needs a working network" +peer_up +# What the firewall allowed before, so that reset takes out only the lab's ports. +if systemctl is-active --quiet firewalld; then + for p in 9000 5201; do firewall-cmd -q --query-port=$p/tcp && state_set fw-had-$p 1; done +elif command -v ufw >/dev/null && ufw status | grep -q 'Status: active'; then + for p in 9000 5201; do ufw status | grep -Eq "^$p(/tcp)?[[:space:]].*ALLOW" && state_set ufw-had-$p 1; done +fi +true +# Reset takes out again what task 4 installs, so that the lab starts over. +for p in iperf3 $(pick nmap-ncat netcat-openbsd); do + pkg_installed "$p" || printf '%s\n' "$p" >>"$LAB_STATE/not-installed" +done diff --git a/labs/linux-plus/1-5-1-shell-environment/README.md b/labs/linux-plus/1-5-1-shell-environment/README.md new file mode 100644 index 0000000..d065cec --- /dev/null +++ b/labs/linux-plus/1-5-1-shell-environment/README.md @@ -0,0 +1,32 @@ +# Linux+ 1.5.1: The shell environment + +A directory of your own on the PATH for good, which startup files bash runs +for login and non-login shells, and the history shortcuts. + +## Before you start + +The lab creates user `alex` (password `Lab-Pass-1`, shell bash). Do the +tasks as `alex`: `sudo -iu alex` or `su - alex`. On the RHEL family alex's +login startup file is `~/.bash_profile`; on Ubuntu it's `~/.profile`. + +## Tasks + +1. Create `~/bin/hi`, a script that prints `hi from` and its own name + (`echo "hi from $0"`), and make it executable. Add `$HOME/bin` to the + front of `PATH` in alex's login startup file, so that `hi` works in + every new login shell. +2. Add the line `echo bashrc ran` to `~/.bashrc` and `echo profile ran` to + the login startup file. Start `bash` (non-login) and `bash -l` (login) + and see which lines each one prints. Leave the lines in place until + you've run `le-lab check`, then remove them. +3. Practice history: run `ls /etc/ssh`, then `cd !$`, then `!!`, then + press Ctrl+r and type `ls` to find the first command. Not graded. + +## Hints + +- `mkdir -p ~/bin`, `printf '#!/bin/bash\necho "hi from $0"\n' > ~/bin/hi`, + `chmod +x`. +- `echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bash_profile` (or + `~/.profile`), then `source` it; `which hi`. +- bash runs only the first of `~/.bash_profile`, `~/.bash_login` and + `~/.profile` that it finds for a login shell. diff --git a/labs/linux-plus/1-5-1-shell-environment/check.sh b/labs/linux-plus/1-5-1-shell-environment/check.sh new file mode 100644 index 0000000..c2afcdc --- /dev/null +++ b/labs/linux-plus/1-5-1-shell-environment/check.sh @@ -0,0 +1,21 @@ +H=$(getent passwd alex | cut -d: -f6) +# The login startup file bash reads for alex: the first that exists. +P='' +for f in .bash_profile .bash_login .profile; do [ -f "$H/$f" ] && { P=$H/$f; break; }; done +# Output of a shell as alex, with no terminal. +as_alex() { runuser -u alex -- env -i HOME="$H" USER=alex LOGNAME=alex SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin TERM=dumb "$@" /dev/null; } + +task 1 "~/bin/hi on alex's PATH in every login shell" +need "$H/bin/hi exists and is executable" '[ -f "$H/bin/hi" ] && [ -x "$H/bin/hi" ]' +need "$H/bin/hi belongs to alex" '[ "$(stat -c %U "$H/bin/hi")" = alex ]' +need "alex's login startup file adds \$HOME/bin to PATH" '[ -n "$P" ] && grep -Eq "^[^#]*PATH=[\"]?(\\\$HOME|~|\\\$\{HOME\}|$H)/bin:" "$P"' +need "hi works in a new login shell and prints \"hi from\"" 'as_alex bash -l -c hi | grep -q "hi from .*hi"' + +task 2 "echo lines in ~/.bashrc and the login startup file" +need "~/.bashrc has echo bashrc ran" 'grep -Eq "^[[:space:]]*echo [\"'"'"']?bashrc ran" "$H/.bashrc"' +need "the login startup file has echo profile ran" '[ -n "$P" ] && grep -Eq "^[[:space:]]*echo [\"'"'"']?profile ran" "$P"' +need "a login shell prints profile ran" 'as_alex bash -l -c true | grep -qx "profile ran"' +need "a non-login interactive shell prints bashrc ran but not profile ran" ' + o=$(as_alex bash -i -c true); grep -qx "bashrc ran" <<<"$o" && ! grep -qx "profile ran" <<<"$o"' + +task 3 "History: !\$, !! and Ctrl+r" self diff --git a/labs/linux-plus/1-5-1-shell-environment/lab.conf b/labs/linux-plus/1-5-1-shell-environment/lab.conf new file mode 100644 index 0000000..6ce6908 --- /dev/null +++ b/labs/linux-plus/1-5-1-shell-environment/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.5.1 The shell environment" +CHAPTER=/linux-plus/1-system-management/1-5-1-shell-environment/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-5-1-shell-environment/reset.sh b/labs/linux-plus/1-5-1-shell-environment/reset.sh new file mode 100644 index 0000000..0ccc652 --- /dev/null +++ b/labs/linux-plus/1-5-1-shell-environment/reset.sh @@ -0,0 +1,2 @@ +# users_remove takes alex and his home directory away after this. +true diff --git a/labs/linux-plus/1-5-1-shell-environment/setup.sh b/labs/linux-plus/1-5-1-shell-environment/setup.sh new file mode 100644 index 0000000..c21d379 --- /dev/null +++ b/labs/linux-plus/1-5-1-shell-environment/setup.sh @@ -0,0 +1 @@ +user_add alex -s /bin/bash diff --git a/labs/linux-plus/1-5-2-redirection-and-pipes/README.md b/labs/linux-plus/1-5-2-redirection-and-pipes/README.md new file mode 100644 index 0000000..c24140c --- /dev/null +++ b/labs/linux-plus/1-5-2-redirection-and-pipes/README.md @@ -0,0 +1,34 @@ +# Linux+ 1.5.2: Redirection and pipes + +Output and errors in separate files, here documents with and without +expansion, `tee`, and file names with spaces through `xargs`. + +## Before you start + +The lab clears away the files the tasks create, if they're there. Several +are in `/tmp`, which Ubuntu empties at boot, so do the tasks without a +reboot and run `le-lab check` before you clean up. + +## Tasks + +1. Make a directory `/tmp/redir` and work in it. Run + `ls /etc /nonexistent` four ways: output to `out.txt` and errors to + `err.txt`; both into `all.txt`; and with `2>&1 > only-out.txt`. Watch + where the error goes in the last one. +2. With here documents, write `user=$USER` to `/tmp/a.txt` with the + variable expanded, and to `/tmp/b.txt` exactly as written. +3. Count the login shells in `/etc/passwd`, most common first. Then save + `getent passwd` to `/tmp/users.txt` and count its lines in the same + pipeline. +4. Make a directory `/tmp/x` with two files, `a b.tmp` and `c d.tmp`. See + `find ... | xargs rm` fail on the spaces, then remove them safely with + `find` and `xargs`. Leave `/tmp/x` itself. + +## Hints + +- `> out.txt 2> err.txt`, `> all.txt 2>&1`; redirections apply left to + right. +- `<< EOF` expands variables; `<< 'EOF'` doesn't. +- `cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn`; + `getent passwd | tee /tmp/users.txt | wc -l`. +- `find /tmp/x -name '*.tmp' -print0 | xargs -0 rm`. diff --git a/labs/linux-plus/1-5-2-redirection-and-pipes/check.sh b/labs/linux-plus/1-5-2-redirection-and-pipes/check.sh new file mode 100644 index 0000000..d71c594 --- /dev/null +++ b/labs/linux-plus/1-5-2-redirection-and-pipes/check.sh @@ -0,0 +1,18 @@ +R=/tmp/redir + +task 1 "Output and errors in out.txt, err.txt, all.txt and only-out.txt" +need "$R/err.txt has the error and no listing" 'grep -q nonexistent $R/err.txt && ! grep -qx passwd $R/err.txt' +need "$R/out.txt has the listing of /etc and no error" 'grep -qx passwd $R/out.txt && ! grep -q nonexistent $R/out.txt' +need "$R/all.txt has both" 'grep -qx passwd $R/all.txt && grep -q nonexistent $R/all.txt' +need "$R/only-out.txt has the listing but not the error" 'grep -qx passwd $R/only-out.txt && ! grep -q nonexistent $R/only-out.txt' + +task 2 "Here documents: /tmp/a.txt expanded, /tmp/b.txt as written" +need "/tmp/a.txt says user= and a user name" 'grep -Eqx "user=[a-z_][a-z0-9_.-]*" /tmp/a.txt' +need "/tmp/b.txt says user=\$USER" 'grep -qxF "user=\$USER" /tmp/b.txt' + +task 3 "getent passwd saved to /tmp/users.txt with tee" +need "/tmp/users.txt is the output of getent passwd" 'getent passwd | cmp -s - /tmp/users.txt' + +task 4 "The files with spaces in /tmp/x removed with find and xargs" +need "/tmp/x is there" '[ -d /tmp/x ]' +need "/tmp/x has no .tmp files left" '[ -z "$(find /tmp/x -name "*.tmp")" ]' diff --git a/labs/linux-plus/1-5-2-redirection-and-pipes/lab.conf b/labs/linux-plus/1-5-2-redirection-and-pipes/lab.conf new file mode 100644 index 0000000..02eb2bb --- /dev/null +++ b/labs/linux-plus/1-5-2-redirection-and-pipes/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.5.2 Redirection and pipes" +CHAPTER=/linux-plus/1-system-management/1-5-2-redirection-and-pipes/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-5-2-redirection-and-pipes/reset.sh b/labs/linux-plus/1-5-2-redirection-and-pipes/reset.sh new file mode 100644 index 0000000..81942a3 --- /dev/null +++ b/labs/linux-plus/1-5-2-redirection-and-pipes/reset.sh @@ -0,0 +1,2 @@ +rm -rf /tmp/redir /tmp/a.txt /tmp/b.txt /tmp/users.txt /tmp/x +true diff --git a/labs/linux-plus/1-5-2-redirection-and-pipes/setup.sh b/labs/linux-plus/1-5-2-redirection-and-pipes/setup.sh new file mode 100644 index 0000000..6668908 --- /dev/null +++ b/labs/linux-plus/1-5-2-redirection-and-pipes/setup.sh @@ -0,0 +1 @@ +rm -rf /tmp/redir /tmp/a.txt /tmp/b.txt /tmp/users.txt /tmp/x diff --git a/labs/linux-plus/1-5-3-text-tools/README.md b/labs/linux-plus/1-5-3-text-tools/README.md new file mode 100644 index 0000000..95c93bc --- /dev/null +++ b/labs/linux-plus/1-5-3-text-tools/README.md @@ -0,0 +1,34 @@ +# Linux+ 1.5.3: Working with text + +grep, cut, awk, sed, tr, sort and uniq on real files: users with a login +shell, a config file without its comments, an edit with sed, and a word +count. + +## Before you start + +The lab clears away the files the tasks create in `/tmp`, if they're there. +Ubuntu empties `/tmp` at boot, so do the tasks without a reboot. Tasks 1, 2 +and 4 ask for findings: write them to the files in `/root/lab-answers/` that +they name. + +## Tasks + +1. Find the users with a UID from 1000 to 59999 (the regular users) with + awk, and write their names, one per line, to `/root/lab-answers/1`. +2. Strip the comments and blank lines from `/etc/ssh/sshd_config`, saving + what's left to `/tmp/sshd.active`, and count its lines in the same + pipeline. Write the count to `/root/lab-answers/2`. +3. Copy `/etc/ssh/sshd_config` to `/tmp/s.conf`. With `sed -i.bak`, change + the `PermitRootLogin` line (commented out or not) to + `PermitRootLogin no`, keeping the original as `/tmp/s.conf.bak`, and + compare the two with `diff`. +4. Find the most common word in the bash man page, and write it to + `/root/lab-answers/4`. + +## Hints + +- `awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd`. +- `grep -v -E '^\s*(#|$)'`, `tee`, `wc -l`. On the RHEL family only root + can read `sshd_config`: `sudo`. +- `sed -i.bak 's/^#\?PermitRootLogin.*/PermitRootLogin no/'`. +- `man bash | tr -cs 'A-Za-z' '\n' | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head`. diff --git a/labs/linux-plus/1-5-3-text-tools/check.sh b/labs/linux-plus/1-5-3-text-tools/check.sh new file mode 100644 index 0000000..c732310 --- /dev/null +++ b/labs/linux-plus/1-5-3-text-tools/check.sh @@ -0,0 +1,22 @@ +regular=$(awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd | sort) +active() { grep -v -E '^\s*(#|$)' /etc/ssh/sshd_config; } +# The three most common words in the bash man page. +top=$(MANWIDTH=80 man bash 2>/dev/null | tr -cs 'A-Za-z' '\n' | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head -n3 | awk '{print $2}') + +task 1 "The regular users (UID 1000 to 59999)" +need "/root/lab-answers/1 lists exactly the regular users: $(echo $regular)" '[ "$(awk "NF {print \$1}" /root/lab-answers/1 | sort)" = "$regular" ]' + +task 2 "sshd_config without comments in /tmp/sshd.active, and its line count" +need "/tmp/sshd.active is sshd_config without comments and blank lines" 'active | cmp -s - /tmp/sshd.active' +need "/root/lab-answers/2 has the count, $(active | wc -l)" '[ "$(answer 2 | tr -dc 0-9)" = "$(active | wc -l)" ]' + +task 3 "PermitRootLogin no in /tmp/s.conf, the original in /tmp/s.conf.bak" +need "/tmp/s.conf.bak is a copy of sshd_config" 'cmp -s /etc/ssh/sshd_config /tmp/s.conf.bak' +need "/tmp/s.conf has the line PermitRootLogin no" 'grep -qx "PermitRootLogin no" /tmp/s.conf' +need "only the PermitRootLogin line changed" ' + [ "$(wc -l ]" | grep -v PermitRootLogin | grep -q .' + +task 4 "The most common word in the bash man page" +need "man bash works" '[ -n "$top" ]' +need "/root/lab-answers/4 has the most common word" '[ -n "$(answer 4)" ] && grep -qxF "$(answer 4)" <<<"$top"' diff --git a/labs/linux-plus/1-5-3-text-tools/lab.conf b/labs/linux-plus/1-5-3-text-tools/lab.conf new file mode 100644 index 0000000..7400ab1 --- /dev/null +++ b/labs/linux-plus/1-5-3-text-tools/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.5.3 Working with text" +CHAPTER=/linux-plus/1-system-management/1-5-3-text-tools/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-5-3-text-tools/reset.sh b/labs/linux-plus/1-5-3-text-tools/reset.sh new file mode 100644 index 0000000..7998753 --- /dev/null +++ b/labs/linux-plus/1-5-3-text-tools/reset.sh @@ -0,0 +1,2 @@ +rm -f /tmp/sshd.active /tmp/s.conf /tmp/s.conf.bak +true diff --git a/labs/linux-plus/1-5-3-text-tools/setup.sh b/labs/linux-plus/1-5-3-text-tools/setup.sh new file mode 100644 index 0000000..7da4f89 --- /dev/null +++ b/labs/linux-plus/1-5-3-text-tools/setup.sh @@ -0,0 +1 @@ +rm -f /tmp/sshd.active /tmp/s.conf /tmp/s.conf.bak diff --git a/labs/linux-plus/1-5-4-text-editors/README.md b/labs/linux-plus/1-5-4-text-editors/README.md new file mode 100644 index 0000000..7c3802a --- /dev/null +++ b/labs/linux-plus/1-5-4-text-editors/README.md @@ -0,0 +1,34 @@ +# Linux+ 1.5.4: Text editors + +Vim's basic moves on a real file, a change in nano, and your preferred +editor for programs such as `crontab -e`. + +## Before you start + +The lab removes `/tmp/s.conf` if it's there, and notes the startup files +task 4 may change so that reset can put them back. Ubuntu empties `/tmp` at +boot, so do task 2 without a reboot. + +## Tasks + +1. Run `vimtutor` and work through lessons 1 and 2 (about 15 minutes). Not + graded. +2. Copy `/etc/ssh/sshd_config` to `/tmp/s.conf` and open it in Vim. Turn on + line numbers, jump to the first line with `Port`, copy it, paste it + below, and change the copy to `Port 2222`. Replace every `yes` with + `YES`, undo that, and save and quit. +3. Make the same kind of change to `/tmp/s.conf` in nano, then quit without + saving. Not graded. +4. Set your preferred editor for good (with `EDITOR` or `VISUAL` in a shell + startup file, or with `select-editor` on Ubuntu), and try it with + `crontab -e`; quit without saving. + +## Hints + +- On the RHEL family `vim` and `vimtutor` are in `vim-enhanced`, and nano + is in `nano`. +- `:set number`, `/Port`, `yy`, `p`, `cw` or `C`, `:%s/yes/YES/g`, `u`, + `:wq`. +- In nano, `Ctrl+x` then `n` quits without saving. +- `export EDITOR=nano` in `~/.bashrc`; `crontab -e` reads `VISUAL`, then + `EDITOR`. diff --git a/labs/linux-plus/1-5-4-text-editors/check.sh b/labs/linux-plus/1-5-4-text-editors/check.sh new file mode 100644 index 0000000..4c26abd --- /dev/null +++ b/labs/linux-plus/1-5-4-text-editors/check.sh @@ -0,0 +1,16 @@ +task 1 "vimtutor lessons 1 and 2" self + +task 2 "/tmp/s.conf: the Port line copied and changed to Port 2222, nothing else" +need "/tmp/s.conf exists" '[ -f /tmp/s.conf ]' +need "/tmp/s.conf has the line Port 2222" 'grep -qx "Port 2222" /tmp/s.conf' +need "the only change from sshd_config is the new Port 2222 line" ' + [ "$(diff /etc/ssh/sshd_config /tmp/s.conf | grep "^[<>]")" = "> Port 2222" ]' +need "the new line is right below a line with Port" ' + grep -B1 -x "Port 2222" /tmp/s.conf | head -n1 | grep -q Port' + +task 3 "A change in nano, quit without saving" self + +task 4 "A preferred editor set for good" +need "EDITOR or VISUAL is set in a startup file, or select-editor was used" ' + grep -Eqs "^[[:space:]]*(export[[:space:]]+)?(EDITOR|VISUAL)=" /etc/environment /etc/profile.d/*.sh /root/.bashrc /root/.bash_profile /root/.profile /home/*/.bashrc /home/*/.bash_profile /home/*/.profile || + ls /root/.selected_editor /home/*/.selected_editor >/dev/null 2>&1' diff --git a/labs/linux-plus/1-5-4-text-editors/lab.conf b/labs/linux-plus/1-5-4-text-editors/lab.conf new file mode 100644 index 0000000..cfba9a9 --- /dev/null +++ b/labs/linux-plus/1-5-4-text-editors/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.5.4 Text editors" +CHAPTER=/linux-plus/1-system-management/1-5-4-text-editors/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-5-4-text-editors/reset.sh b/labs/linux-plus/1-5-4-text-editors/reset.sh new file mode 100644 index 0000000..1cda51f --- /dev/null +++ b/labs/linux-plus/1-5-4-text-editors/reset.sh @@ -0,0 +1,13 @@ +rm -f /tmp/s.conf +# Startup files as they were before the lab. +if [ -f "$LAB_STATE/kept" ]; then + while read -r f; do + [ -e "$LAB_STATE/saved$f" ] || continue + rm -rf "$f"; cp -a "$LAB_STATE/saved$f" "$f" + done <"$LAB_STATE/kept" +fi +if [ -f "$LAB_STATE/absent" ]; then + while read -r f; do case $f in *'*'*) ;; *) rm -rf "$f" ;; esac; done <"$LAB_STATE/absent" +fi +command -v restorecon >/dev/null && restorecon -R /etc/environment /etc/profile.d /root /home 2>/dev/null +true diff --git a/labs/linux-plus/1-5-4-text-editors/setup.sh b/labs/linux-plus/1-5-4-text-editors/setup.sh new file mode 100644 index 0000000..8a86751 --- /dev/null +++ b/labs/linux-plus/1-5-4-text-editors/setup.sh @@ -0,0 +1,13 @@ +rm -f /tmp/s.conf +# The startup files task 4 may change, for reset to put back. +saved=$LAB_STATE/saved +for f in /etc/environment /etc/profile.d /root/.bashrc /root/.bash_profile /root/.profile /root/.selected_editor \ + /home/*/.bashrc /home/*/.bash_profile /home/*/.profile /home/*/.selected_editor; do + if [ -e "$f" ]; then + mkdir -p "$saved${f%/*}" + cp -a "$f" "$saved$f" + printf '%s\n' "$f" >>"$LAB_STATE/kept" + else + printf '%s\n' "$f" >>"$LAB_STATE/absent" + fi +done diff --git a/labs/linux-plus/1-6-1-archives-and-compression/README.md b/labs/linux-plus/1-6-1-archives-and-compression/README.md new file mode 100644 index 0000000..e417635 --- /dev/null +++ b/labs/linux-plus/1-6-1-archives-and-compression/README.md @@ -0,0 +1,36 @@ +# Linux+ 1.6.1: Archives and compression + +tar archives, restoring one file, gzip, bzip2 and xz side by side, reading a +compressed log, and cpio. + +## Before you start + +The lab clears away the files the tasks create in `/tmp`, if they're there. +Ubuntu empties `/tmp` at boot, so do the tasks without a reboot, and run +`le-lab check` before the chapter's clean-up step (`le-lab reset` cleans up +too). Tasks 2 and 3 ask for findings: write them to the files in +`/root/lab-answers/` that they name. + +## Tasks + +1. Archive `/etc` into `/tmp/etc.tar.gz` with gzip, list it to find + `sshd_config`, and restore just `etc/ssh/sshd_config` from it into + `/tmp/restore`. +2. Archive `/etc` without compression into `/tmp/etc.tar`. Copy it to + `/tmp/t-gzip.tar`, `/tmp/t-bzip2.tar` and `/tmp/t-xz.tar`, and compress + each with the tool it's named after, keeping the copies. Write the name + of the tool that made the smallest file to `/root/lab-answers/2`. +3. Save the last 500 lines of the journal, compressed with gzip, as + `/tmp/j.gz`. Without unpacking it, count the lines that mention `error` + in any case, and write the count to `/root/lab-answers/3`. +4. From `/etc`, make a cpio archive of the files in `ssh` as + `/tmp/ssh.cpio`, and unpack it into `/tmp/c`. + +## Hints + +- `tar -czf`, `tar -tzf`, `tar -xzf ARCHIVE -C DIR MEMBER`. +- `gzip -k`, `bzip2 -k`, `xz -k`; `ls -lh /tmp/t-*`. bzip2 may need + installing. The tools skip a file whose name already ends in their own + suffix (`.gz`, `.bz2`, `.xz`). +- `journalctl -n 500 --no-pager | gzip > ...`; `zgrep -c -i`, `zless`. +- `cd /etc && find ssh -type f | cpio -o > ...`; `cpio -idv < ...`. diff --git a/labs/linux-plus/1-6-1-archives-and-compression/check.sh b/labs/linux-plus/1-6-1-archives-and-compression/check.sh new file mode 100644 index 0000000..f62fed8 --- /dev/null +++ b/labs/linux-plus/1-6-1-archives-and-compression/check.sh @@ -0,0 +1,23 @@ +smallest=$(ls -S /tmp/t-gzip.tar.gz /tmp/t-bzip2.tar.bz2 /tmp/t-xz.tar.xz 2>/dev/null | tail -n1 | sed 's|^/tmp/t-||; s|\..*||') + +task 1 "/etc in /tmp/etc.tar.gz; sshd_config restored into /tmp/restore" +need "/tmp/etc.tar.gz is a gzip-compressed tar archive" 'gzip -t /tmp/etc.tar.gz && tar -tzf /tmp/etc.tar.gz >/dev/null' +need "it has etc/ssh/sshd_config" 'tar -tzf /tmp/etc.tar.gz | grep -qx etc/ssh/sshd_config' +need "/tmp/restore/etc/ssh/sshd_config matches /etc/ssh/sshd_config" 'cmp -s /tmp/restore/etc/ssh/sshd_config /etc/ssh/sshd_config' + +task 2 "gzip, bzip2 and xz compared on /tmp/etc.tar" +need "/tmp/etc.tar is a tar archive of /etc" 'tar -tf /tmp/etc.tar | grep -qx etc/ssh/sshd_config' +need "/tmp/t-gzip.tar.gz is valid gzip" 'gzip -t /tmp/t-gzip.tar.gz' +need "/tmp/t-bzip2.tar.bz2 is valid bzip2" 'bzip2 -t /tmp/t-bzip2.tar.bz2' +need "/tmp/t-xz.tar.xz is valid xz" 'xz -t /tmp/t-xz.tar.xz' +need "the uncompressed copies are kept (-k)" '[ -f /tmp/t-gzip.tar ] && [ -f /tmp/t-bzip2.tar ] && [ -f /tmp/t-xz.tar ]' +need "/root/lab-answers/2 names the tool with the smallest file${smallest:+, $smallest}" '[ -n "$smallest" ] && [ "$(answer 2)" = "$smallest" ]' + +task 3 "The journal in /tmp/j.gz, its error lines counted" +need "/tmp/j.gz is valid gzip" 'gzip -t /tmp/j.gz' +need "it holds lines from the journal" '[ "$(zcat /tmp/j.gz | wc -l)" -ge 50 ]' +need "/root/lab-answers/3 has the count of lines with error" '[ "$(answer 3 | tr -dc 0-9)" = "$(zgrep -c -i error /tmp/j.gz)" ]' + +task 4 "ssh in /tmp/ssh.cpio, unpacked in /tmp/c" +need "/tmp/ssh.cpio is a cpio archive with ssh/sshd_config" 'cpio -it /dev/null | grep -qx ssh/sshd_config' +need "/tmp/c/ssh/sshd_config matches /etc/ssh/sshd_config" 'cmp -s /tmp/c/ssh/sshd_config /etc/ssh/sshd_config' diff --git a/labs/linux-plus/1-6-1-archives-and-compression/lab.conf b/labs/linux-plus/1-6-1-archives-and-compression/lab.conf new file mode 100644 index 0000000..543d232 --- /dev/null +++ b/labs/linux-plus/1-6-1-archives-and-compression/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.6.1 Archives and compression" +CHAPTER=/linux-plus/1-system-management/1-6-1-archives-and-compression/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-6-1-archives-and-compression/reset.sh b/labs/linux-plus/1-6-1-archives-and-compression/reset.sh new file mode 100644 index 0000000..97cb578 --- /dev/null +++ b/labs/linux-plus/1-6-1-archives-and-compression/reset.sh @@ -0,0 +1,2 @@ +rm -rf /tmp/etc.tar /tmp/etc.tar.gz /tmp/t-gzip.tar* /tmp/t-bzip2.tar* /tmp/t-xz.tar* /tmp/j.gz /tmp/ssh.cpio /tmp/restore /tmp/c +true diff --git a/labs/linux-plus/1-6-1-archives-and-compression/setup.sh b/labs/linux-plus/1-6-1-archives-and-compression/setup.sh new file mode 100644 index 0000000..05bb1c0 --- /dev/null +++ b/labs/linux-plus/1-6-1-archives-and-compression/setup.sh @@ -0,0 +1 @@ +rm -rf /tmp/etc.tar /tmp/etc.tar.gz /tmp/t-gzip.tar* /tmp/t-bzip2.tar* /tmp/t-xz.tar* /tmp/j.gz /tmp/ssh.cpio /tmp/restore /tmp/c diff --git a/labs/linux-plus/1-6-2-copying-and-imaging/README.md b/labs/linux-plus/1-6-2-copying-and-imaging/README.md new file mode 100644 index 0000000..eed8d2e --- /dev/null +++ b/labs/linux-plus/1-6-2-copying-and-imaging/README.md @@ -0,0 +1,36 @@ +# Linux+ 1.6.2: Copies, syncs and disk images + +Mirroring a directory with rsync, pulling files from another machine over +SSH, and imaging a disk with dd. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the other VM. It runs +its own sshd and shares this VM's files and users, so its `/etc` is this +VM's `/etc`. The lab creates user `alex` (password `Lab-Pass-1`) and adds a +small 64 MiB spare disk, `/dev/loop40` (the chapter's `vdb`), with a +filesystem on it. Several files go in `/tmp`, which Ubuntu empties at boot, +so do the tasks without a reboot and run `le-lab check` before you clean up. + +## Tasks + +1. Copy `/etc/ssh` into `/tmp/src` and mirror `/tmp/src/` to `/tmp/dst/` + with rsync. Add a file `/tmp/src/ssh/new.txt` and delete + `/tmp/src/ssh/moduli`, try a dry run, then mirror again so that + `/tmp/dst` matches. Finally, copy `/tmp/src` (no trailing slash) into + `/tmp/dst2/`, and see where it lands. +2. As `alex`, set up SSH keys so that alex can log in to the peer + (`alex@10.99.0.2`) without a password, then pull a copy of the peer's + `/etc/` into `/tmp/other-etc/` with rsync. Files only root can read are + skipped with "Permission denied". +3. Image `/dev/loop40` into `/tmp/vdb.img` with dd and compare the two with + `cmp`. Keep the image until you've run `le-lab check`. + +## Hints + +- `rsync -av SRC/ DST/`, `rsync -avn --delete`, `rsync -a SRC DST/`. +- `sudo -iu alex`, `ssh-keygen`. The peer shares alex's home directory, so + adding the public key to `~/.ssh/authorized_keys` is enough + (`ssh-copy-id` needs a password login, which may be turned off). + `rsync -a alex@10.99.0.2:/etc/ /tmp/other-etc/`. +- `dd if=DISK of=IMAGE bs=4M status=progress`, `cmp DISK IMAGE && echo identical`. diff --git a/labs/linux-plus/1-6-2-copying-and-imaging/check.sh b/labs/linux-plus/1-6-2-copying-and-imaging/check.sh new file mode 100644 index 0000000..90539c6 --- /dev/null +++ b/labs/linux-plus/1-6-2-copying-and-imaging/check.sh @@ -0,0 +1,16 @@ +task 1 "/tmp/dst mirrors /tmp/src; /tmp/src copied into /tmp/dst2" +need "/tmp/dst/ssh/sshd_config is there" '[ -f /tmp/dst/ssh/sshd_config ]' +need "/tmp/dst/ssh/new.txt is there" '[ -f /tmp/dst/ssh/new.txt ]' +need "/tmp/dst/ssh/moduli is gone (--delete)" '[ ! -e /tmp/dst/ssh/moduli ] && [ ! -e /tmp/src/ssh/moduli ]' +need "/tmp/dst matches /tmp/src" '[ -z "$(rsync -an --delete --out-format=%n /tmp/src/ /tmp/dst/ | grep -v "/$")" ]' +need "/tmp/dst2 has the directory src in it" '[ -d /tmp/dst2/src/ssh ]' + +task 2 "The peer's /etc pulled over SSH into /tmp/other-etc as alex" +need "alex has an SSH key the peer accepts" ' + runuser -u alex -- ssh -n -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 alex@10.99.0.2 true' +need "/tmp/other-etc has the peer's /etc files" '[ -f /tmp/other-etc/passwd ] && [ -d /tmp/other-etc/ssh ]' +need "alex made the copy" '[ "$(stat -c %U /tmp/other-etc/passwd)" = alex ]' +need "files only root can read were skipped" '[ ! -e /tmp/other-etc/shadow ]' + +task 3 "/dev/loop40 imaged into /tmp/vdb.img" +need "/tmp/vdb.img is identical to /dev/loop40" '[ -f /tmp/vdb.img ] && cmp -s /dev/loop40 /tmp/vdb.img' diff --git a/labs/linux-plus/1-6-2-copying-and-imaging/lab.conf b/labs/linux-plus/1-6-2-copying-and-imaging/lab.conf new file mode 100644 index 0000000..ef862bd --- /dev/null +++ b/labs/linux-plus/1-6-2-copying-and-imaging/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.6.2 Copies, syncs and disk images" +CHAPTER=/linux-plus/1-system-management/1-6-2-copying-and-imaging/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-6-2-copying-and-imaging/reset.sh b/labs/linux-plus/1-6-2-copying-and-imaging/reset.sh new file mode 100644 index 0000000..cfda618 --- /dev/null +++ b/labs/linux-plus/1-6-2-copying-and-imaging/reset.sh @@ -0,0 +1,5 @@ +systemctl disable --now --quiet le-lab-peer-sshd.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-peer-sshd.service +systemctl daemon-reload +rm -rf /tmp/src /tmp/dst /tmp/dst2 /tmp/other-etc /tmp/vdb.img +true diff --git a/labs/linux-plus/1-6-2-copying-and-imaging/setup.sh b/labs/linux-plus/1-6-2-copying-and-imaging/setup.sh new file mode 100644 index 0000000..40a41f7 --- /dev/null +++ b/labs/linux-plus/1-6-2-copying-and-imaging/setup.sh @@ -0,0 +1,28 @@ +command -v rsync >/dev/null || refuse "rsync isn't installed; install it first (the chapter uses it on both machines)" +[ -x /usr/sbin/sshd ] || refuse "sshd isn't installed; install openssh-server first" +rm -rf /tmp/src /tmp/dst /tmp/dst2 /tmp/other-etc /tmp/vdb.img +user_add alex -s /bin/bash +peer_up +# The peer's sshd, on the peer's own address. +cat >/etc/systemd/system/le-lab-peer-sshd.service <<'X' +[Unit] +Description=Practice lab sshd on the peer +Requires=le-lab-peer.service +After=le-lab-peer.service network.target + +[Service] +NetworkNamespacePath=/run/netns/lepeer +ExecStartPre=/bin/mkdir -p /run/sshd +ExecStart=/usr/sbin/sshd -D -o ListenAddress=10.99.0.2:22 -o PidFile=none +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet le-lab-peer-sshd.service +# A small disk with something on it to image. +disk_add 0 64M >/dev/null +mkfs.ext4 -q -L labdata /dev/loop40 +m=$(mktemp -d); mount /dev/loop40 "$m"; cp -r /etc/ssh "$m/"; umount "$m"; rmdir "$m" diff --git a/labs/linux-plus/1-7-1-kvm-and-qemu/README.md b/labs/linux-plus/1-7-1-kvm-and-qemu/README.md new file mode 100644 index 0000000..4443639 --- /dev/null +++ b/labs/linux-plus/1-7-1-kvm-and-qemu/README.md @@ -0,0 +1,35 @@ +# Linux+ 1.7.1: KVM, QEMU and virtual disks + +Getting a host ready for KVM, and working with disk images: qcow2 and raw, +resizing, converting, and a backing file. + +## Before you start + +Task 1 is best done on a VM with **nested virtualization** (the CPU flag +`vmx` or `svm` inside it), but nothing here runs a VM, so the lab works +without it too: `virt-host-validate` then reports what's missing. The +images go in `/tmp`, which Ubuntu empties at boot, so do task 2 without a +reboot. Task 3 asks for a finding: write it to `/root/lab-answers/3`. +Reset removes the packages task 1 installs, if they weren't there before. + +## Tasks + +1. Check the host for KVM support, install QEMU, `qemu-img`, libvirt and + `virt-install`, and run `virt-host-validate qemu`. +2. Create a 5 GiB qcow2 image `/tmp/a.qcow2` and grow it by 1 GiB. Convert + it to a raw image, `/tmp/a.raw`, and compare the space each really + uses. Then create `/tmp/b.qcow2`, a qcow2 image with `/tmp/a.qcow2` as + its backing file, and look at the backing chain. +3. Check whether this VM uses VirtIO devices. Write the driver of its main + network interface, as `ethtool -i` reports it, to `/root/lab-answers/3`. + +## Hints + +- `grep -cE 'vmx|svm' /proc/cpuinfo`, `lsmod | grep kvm`, `ls -l /dev/kvm`. +- RHEL family: `qemu-kvm qemu-img libvirt virt-install`. Ubuntu: + `qemu-system-x86 qemu-utils libvirt-daemon-system virtinst`. +- `qemu-img create -f qcow2`, `qemu-img info`, `qemu-img resize +1G`, + `qemu-img convert -f qcow2 -O raw`, `ls -ls`, + `qemu-img create -f qcow2 -F qcow2 -b BACKING NEW`, + `qemu-img info --backing-chain`. +- `lspci | grep -i virtio`; `ip route` shows the main interface. diff --git a/labs/linux-plus/1-7-1-kvm-and-qemu/check.sh b/labs/linux-plus/1-7-1-kvm-and-qemu/check.sh new file mode 100644 index 0000000..f39a527 --- /dev/null +++ b/labs/linux-plus/1-7-1-kvm-and-qemu/check.sh @@ -0,0 +1,23 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +drv=$(basename "$(readlink -f "/sys/class/net/$IF/device/driver" 2>/dev/null)" 2>/dev/null) +command -v ethtool >/dev/null && drv=$(ethtool -i "$IF" 2>/dev/null | awk '$1 == "driver:" {print $2}') +info() { qemu-img info -U --output=json "$1" 2>/dev/null; } +field() { info "$1" | python3 -c 'import json,sys; v = json.load(sys.stdin).get(sys.argv[1]); print("" if v is None else v)' "$2" 2>/dev/null; } + +task 1 "QEMU, qemu-img, libvirt and virt-install installed" +need "qemu-img is installed" 'command -v qemu-img' +need "a QEMU system emulator is installed" 'ls /usr/libexec/qemu-kvm /usr/bin/qemu-system-x86_64 2>/dev/null | grep -q .' +need "libvirt is installed (virsh and the QEMU driver)" 'command -v virsh && ls /usr/sbin/virtqemud /usr/sbin/libvirtd 2>/dev/null | grep -q .' +need "virt-install is installed" 'command -v virt-install' +need "virt-host-validate is installed" 'command -v virt-host-validate' + +task 2 "a.qcow2 grown to 6 GiB, a.raw converted from it, b.qcow2 backed by it" +need "/tmp/a.qcow2 is a qcow2 image" '[ "$(field /tmp/a.qcow2 format)" = qcow2 ]' +need "/tmp/a.qcow2 is 6 GiB (5 + 1)" '[ "$(field /tmp/a.qcow2 virtual-size)" = 6442450944 ]' +need "/tmp/a.raw is a raw image of 6 GiB" '[ "$(field /tmp/a.raw format)" = raw ] && [ "$(stat -c %s /tmp/a.raw)" = 6442450944 ]' +need "/tmp/b.qcow2 is a qcow2 image" '[ "$(field /tmp/b.qcow2 format)" = qcow2 ]' +need "/tmp/b.qcow2's backing file is /tmp/a.qcow2 (qcow2)" ' + [ "$(readlink -f "$(field /tmp/b.qcow2 full-backing-filename)")" = /tmp/a.qcow2 ] && [ "$(field /tmp/b.qcow2 backing-filename-format)" = qcow2 ]' + +task 3 "The main network interface's driver" +need "/root/lab-answers/3 has the driver of $IF${drv:+, $drv}" '[ -n "$drv" ] && [ "$(answer 3)" = "$drv" ]' diff --git a/labs/linux-plus/1-7-1-kvm-and-qemu/lab.conf b/labs/linux-plus/1-7-1-kvm-and-qemu/lab.conf new file mode 100644 index 0000000..ebbe81b --- /dev/null +++ b/labs/linux-plus/1-7-1-kvm-and-qemu/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.7.1 KVM, QEMU and virtual disks" +CHAPTER=/linux-plus/1-system-management/1-7-1-kvm-and-qemu/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-7-1-kvm-and-qemu/reset.sh b/labs/linux-plus/1-7-1-kvm-and-qemu/reset.sh new file mode 100644 index 0000000..dac25ed --- /dev/null +++ b/labs/linux-plus/1-7-1-kvm-and-qemu/reset.sh @@ -0,0 +1,12 @@ +rm -f /tmp/a.qcow2 /tmp/a.raw /tmp/b.qcow2 +true +# The packages task 1 installed, if they weren't there before the lab. +if [ -s "$LAB_STATE/not-installed" ]; then + say "Removing the packages task 1 installed..." + if is_rhel; then + dnf -y -q remove $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1 + else + DEBIAN_FRONTEND=noninteractive apt-get -y -qq --autoremove purge $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1 + fi +fi +true diff --git a/labs/linux-plus/1-7-1-kvm-and-qemu/setup.sh b/labs/linux-plus/1-7-1-kvm-and-qemu/setup.sh new file mode 100644 index 0000000..5acc00a --- /dev/null +++ b/labs/linux-plus/1-7-1-kvm-and-qemu/setup.sh @@ -0,0 +1,6 @@ +grep -qE 'vmx|svm' /proc/cpuinfo || warn "This VM has no nested virtualization (no vmx or svm CPU flag); virt-host-validate will say so." +rm -f /tmp/a.qcow2 /tmp/a.raw /tmp/b.qcow2 +# Reset takes out again what task 1 installs, so that the lab starts over. +for p in $(pick "qemu-kvm qemu-img libvirt virt-install" "qemu-system-x86 qemu-utils libvirt-daemon-system virtinst"); do + pkg_installed "$p" || printf '%s\n' "$p" >>"$LAB_STATE/not-installed" +done diff --git a/labs/linux-plus/1-7-2-managing-vms/README.md b/labs/linux-plus/1-7-2-managing-vms/README.md new file mode 100644 index 0000000..2b62af9 --- /dev/null +++ b/labs/linux-plus/1-7-2-managing-vms/README.md @@ -0,0 +1,45 @@ +# Linux+ 1.7.2: Managing virtual machines + +Creating a VM from a cloud image with virt-install, suspending, resizing +and shutting it down, and a snapshot to undo a mistake. + +## Before you start + +The VM you run this lab on needs **nested virtualization** (the CPU flag +`vmx` or `svm` inside it) and about 6 GiB of memory, plus internet access. +The lab installs QEMU, libvirt and virt-install (the KVM host from chapter +1.7.1) and downloads a small cloud image, CirrOS, to +`/var/lib/le-lab/images/cirros.img`. Use it where the chapter downloads a +Rocky Linux or Ubuntu image (or download one of those yourself). Its user +is `cirros`, password `gocubsgo`. + +## Tasks + +1. Create a VM named `lab1` from the cloud image: copy it to + `/var/lib/libvirt/images/lab1.qcow2`, grow that to 10 GiB, and import it + with virt-install, with 1 GiB of memory, one vCPU, a VirtIO disk, a + VirtIO network card on the `default` network, and a generated root + password from cloud-init. `virsh list --all` shows it, and + `virsh domifaddr lab1` shows its address once it has booted. +2. Suspend and resume `lab1`, give it 768 MiB of memory while it runs, then + shut it down. Not graded. +3. Start `lab1` again, snapshot it as `clean`, break something inside it + (log in on `virsh console lab1` and run `rm -rf /etc/ssh`; Ctrl+] leaves + the console), and revert to the snapshot. + +When `le-lab check` passes, clean up as the chapter shows (task 4: delete +the snapshot, then destroy and undefine `lab1` with its storage), or leave +it to `le-lab reset`. + +## Hints + +- On the RHEL family, start the libvirt sockets first: + `systemctl enable --now virtqemud.socket virtnetworkd.socket virtstoraged.socket`. +- `qemu-img resize`; `virt-install --import --disk PATH,bus=virtio + --network network=default,model=virtio --os-variant detect=on,require=off + --cloud-init root-password-generate=on --noautoconsole`. +- `virsh suspend`, `virsh resume`, `virsh setmem lab1 768M --live`, + `virsh shutdown`, `virsh domstate`; CirrOS may ignore `virsh shutdown` + while it's still booting, and `virsh destroy` powers it off. +- `virsh snapshot-create-as lab1 clean`, `virsh snapshot-list lab1`, + `virsh snapshot-revert lab1 clean`. diff --git a/labs/linux-plus/1-7-2-managing-vms/check.sh b/labs/linux-plus/1-7-2-managing-vms/check.sh new file mode 100644 index 0000000..cdf06c7 --- /dev/null +++ b/labs/linux-plus/1-7-2-managing-vms/check.sh @@ -0,0 +1,19 @@ +export LIBVIRT_DEFAULT_URI=qemu:///system +IMG=/var/lib/libvirt/images/lab1.qcow2 +xml() { virsh dumpxml --inactive lab1 2>/dev/null; } + +task 1 "lab1 imported from lab1.qcow2 (10 GiB), 1 GiB, VirtIO disk and network" +need "virsh list --all works" 'virsh list --all' +need "VM lab1 exists" 'virsh dominfo lab1' +need "lab1's disk is $IMG" 'virsh domblklist lab1 --inactive --details | awk "\$2 == \"disk\" {print \$4}" | grep -qxF "$IMG"' +need "$IMG is 10 GiB" 'qemu-img info -U --output=json "$IMG" | grep -Eq "\"virtual-size\": 10737418240,"' +need "the disk is on the VirtIO bus" 'xml | grep -Eq ""' +need "the network card is VirtIO, on the default network" 'xml | grep -q ""' +need "lab1 has 1 GiB of memory" 'xml | grep -Eq "1048576"' +need "lab1 has one vCPU" 'xml | grep -Eq "]*>1"' + +task 2 "Suspend, resume, setmem and shut down" self + +task 3 "A snapshot clean of lab1, reverted to" +need "lab1 has a snapshot named clean" 'virsh snapshot-list lab1 --name | grep -qx clean' +need "clean is lab1's current snapshot (reverted)" '[ "$(virsh snapshot-current lab1 --name 2>/dev/null)" = clean ]' diff --git a/labs/linux-plus/1-7-2-managing-vms/lab.conf b/labs/linux-plus/1-7-2-managing-vms/lab.conf new file mode 100644 index 0000000..8c0ee80 --- /dev/null +++ b/labs/linux-plus/1-7-2-managing-vms/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.7.2 Managing virtual machines" +CHAPTER=/linux-plus/1-system-management/1-7-2-managing-vms/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/1-7-2-managing-vms/reset.sh b/labs/linux-plus/1-7-2-managing-vms/reset.sh new file mode 100644 index 0000000..4cc9a44 --- /dev/null +++ b/labs/linux-plus/1-7-2-managing-vms/reset.sh @@ -0,0 +1,12 @@ +if command -v virsh >/dev/null; then + export LIBVIRT_DEFAULT_URI=qemu:///system + if virsh dominfo lab1 >/dev/null 2>&1; then + virsh destroy lab1 >/dev/null 2>&1 + for s in $(virsh snapshot-list lab1 --name 2>/dev/null); do virsh snapshot-delete lab1 "$s" >/dev/null 2>&1; done + virsh undefine lab1 --remove-all-storage --snapshots-metadata --nvram >/dev/null 2>&1 || + virsh undefine lab1 --snapshots-metadata >/dev/null 2>&1 + fi + rm -f /var/lib/libvirt/images/lab1.qcow2 +fi +rm -rf /var/lib/le-lab/images +true diff --git a/labs/linux-plus/1-7-2-managing-vms/setup.sh b/labs/linux-plus/1-7-2-managing-vms/setup.sh new file mode 100644 index 0000000..c7c102d --- /dev/null +++ b/labs/linux-plus/1-7-2-managing-vms/setup.sh @@ -0,0 +1,20 @@ +if ! grep -qE 'vmx|svm' /proc/cpuinfo; then + warn "This VM has no nested virtualization (no vmx or svm CPU flag); VMs inside it won't run." +fi +if command -v virsh && virsh -c qemu:///system dominfo lab1; then + refuse "a VM named lab1 already exists; delete it first" +fi >/dev/null 2>&1 +# The KVM host from chapter 1.7.1. +if is_rhel; then + pkg_installed libvirt && pkg_installed virt-install && pkg_installed qemu-kvm || pkg_install qemu-kvm qemu-img libvirt virt-install +else + pkg_installed libvirt-daemon-system && pkg_installed virtinst && pkg_installed qemu-system-x86 || + pkg_install qemu-system-x86 qemu-utils libvirt-daemon-system virtinst +fi +mkdir -p /var/lib/le-lab/images +if [ ! -s /var/lib/le-lab/images/cirros.img ]; then + say "${C_DIM}Downloading the CirrOS cloud image...${C_0}" + curl -fsSL -o /var/lib/le-lab/images/cirros.img.part \ + https://download.cirros-cloud.net/0.6.3/cirros-0.6.3-x86_64-disk.img + mv /var/lib/le-lab/images/cirros.img.part /var/lib/le-lab/images/cirros.img +fi diff --git a/labs/linux-plus/2-1-1-files-and-directories/README.md b/labs/linux-plus/2-1-1-files-and-directories/README.md new file mode 100644 index 0000000..9c77cfe --- /dev/null +++ b/labs/linux-plus/2-1-1-files-and-directories/README.md @@ -0,0 +1,30 @@ +# Linux+ 2.1.1: Files and directories + +Building a directory tree, copying it with and without its attributes, +comparing files, and identifying file types. + +## Before you start + +Work as root, so `~` is `/root`. The lab only checks that `/root/lab` +doesn't exist yet. Leave your work in place for grading: `le-lab reset` +removes `/root/lab`. Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Create `~/lab/app` with the subdirectories `conf`, `logs` and `data`, + and a file `~/lab/app/conf/app.conf` that contains `Listen 80`, with + mode 640. Copy the tree once with `cp -r` to `~/lab/copy-r` and once with + `cp -a` to `~/lab/copy-a`, and compare the modify times of the three + `app.conf` files. +2. Change `80` to `8080` in `~/lab/copy-a/conf/app.conf` only. Save the + unified diff of the original against that copy in + `/root/lab-answers/2`. +3. Find the most common file type among the files in `/usr/bin`, and write + it (as `file` describes it, for example the text before the first comma) + to `/root/lab-answers/3`. + +## Hints + +- `mkdir -p` with a brace list, `chmod`, `stat -c '%a %y %n'`. +- `sed -i`, `diff -u`, `sdiff`, `diff -rq`. +- `file /usr/bin/* | awk -F: '{print $2}' | cut -d, -f1 | sort | uniq -c | sort -rn | head`. diff --git a/labs/linux-plus/2-1-1-files-and-directories/check.sh b/labs/linux-plus/2-1-1-files-and-directories/check.sh new file mode 100644 index 0000000..76863ab --- /dev/null +++ b/labs/linux-plus/2-1-1-files-and-directories/check.sh @@ -0,0 +1,25 @@ +A=/root/lab/app/conf/app.conf R=/root/lab/copy-r/conf/app.conf C=/root/lab/copy-a/conf/app.conf +top_type() { + file /usr/bin/* | awk -F: '{print $2}' | cut -d, -f1 | sort | uniq -c | sort -rn | head -n1 | + sed 's/^[[:space:]]*[0-9]*[[:space:]]*//' +} + +task 1 "~/lab/app built and copied with cp -r and cp -a" +need "~/lab/app has conf, logs and data" '[ -d /root/lab/app/conf ] && [ -d /root/lab/app/logs ] && [ -d /root/lab/app/data ]' +need "~/lab/app/conf/app.conf has mode 640" '[ "$(stat -c %a "$A")" = 640 ]' +need "~/lab/copy-r and ~/lab/copy-a have the same three directories" ' + for c in copy-r copy-a; do for s in conf logs data; do [ -d /root/lab/$c/$s ] || exit 1; done; done' +# app.conf in copy-a changes in task 2, so compare the logs directories. +need "the cp -a copy kept the modify times" 'a=/root/lab/app/logs c=/root/lab/copy-a/logs; [ -d "$c" ] && ! [ "$c" -nt "$a" ] && ! [ "$c" -ot "$a" ]' +need "the cp -a copy kept mode 640" '[ "$(stat -c %a "$C")" = 640 ]' +need "the cp -r copy got new modify times" '[ -f "$R" ] && [ "$R" -nt "$A" ] && [ /root/lab/copy-r/logs -nt /root/lab/app/logs ]' + +task 2 "copy-a changed to 8080, unified diff saved" +need "the original still says Listen 80" 'grep -qx "Listen 80" "$A"' +need "copy-a says Listen 8080" 'grep -qx "Listen 8080" "$C"' +need "/root/lab-answers/2 is a unified diff (-Listen 80, +Listen 8080)" ' + grep -qx -- "-Listen 80" /root/lab-answers/2 && grep -qx -- "+Listen 8080" /root/lab-answers/2 && grep -q "^@@" /root/lab-answers/2' + +task 3 "The most common file type in /usr/bin" +need "/root/lab-answers/3 names the most common type" ' + t=$(top_type | tr "A-Z" "a-z"); [ -n "$t" ] && tr "A-Z" "a-z" /dev/null || pkg_install file +true diff --git a/labs/linux-plus/2-1-2-finding-files-links-devices/README.md b/labs/linux-plus/2-1-2-finding-files-links-devices/README.md new file mode 100644 index 0000000..e47ac90 --- /dev/null +++ b/labs/linux-plus/2-1-2-finding-files-links-devices/README.md @@ -0,0 +1,35 @@ +# Linux+ 2.1.2: Finding files, links and device files + +Finding files by size and permission, the locate database, hard and +symbolic links, and finding which process holds a file open. + +## Before you start + +Work as root. The lab installs `plocate` and `lsof`, leaves a large file +and an extra set-user-ID program somewhere on the root filesystem, and +starts a process that keeps `/tmp/held.txt` open. Write answers to the +files in `/root/lab-answers/`. Don't run the chapter's final clean-up +(`rm -f ... /tmp/h.txt /tmp/s.txt ~/findme-*.txt`): the lab grades those +files, and `le-lab reset` removes them. + +## Tasks + +1. Find every regular file over 50 MiB and every set-user-ID file on the + root filesystem (don't cross into other filesystems). Save their full + paths, one per line, in `/root/lab-answers/1` (`ls -l` style lines are + fine too). +2. Create a file `~/findme-.txt`, then make `locate findme-` + find it. +3. In `/tmp`, create `t.txt` containing `hi`, a hard link `h.txt` to it and + a symbolic link `s.txt` to it. Then delete `t.txt`, and see which of the + two links still works. +4. Find the process that holds `/tmp/held.txt` open, and write its PID to + `/root/lab-answers/4`. + +## Hints + +- `find / -xdev -type f -size +50M`, `find / -xdev -perm -4000 -type f`, + `2>/dev/null`. +- `locate` only knows what the last `updatedb` saw. +- `ln`, `ln -s`, `ls -li`. +- `lsof /tmp/held.txt`, or `fuser`. diff --git a/labs/linux-plus/2-1-2-finding-files-links-devices/check.sh b/labs/linux-plus/2-1-2-finding-files-links-devices/check.sh new file mode 100644 index 0000000..0ded99b --- /dev/null +++ b/labs/linux-plus/2-1-2-finding-files-links-devices/check.sh @@ -0,0 +1,20 @@ +A1=/root/lab-answers/1 +held_pid() { systemctl show -p MainPID --value le-lab-held.service; } + +task 1 "Large and set-user-ID files listed" +need "the list names the file over 50 MiB under /var/lib" 'grep -q /var/lib/le-report/archive.dat "$A1"' +need "the list names the set-user-ID program under /usr/local" 'grep -q /usr/local/sbin/le-backup-helper "$A1"' +need "the list names the usual set-user-ID programs (such as passwd)" 'grep -q /usr/bin/passwd "$A1" && grep -q /usr/bin/su "$A1"' + +task 2 "locate finds ~/findme-*.txt" +need "a file ~/findme-*.txt exists" 'ls /root/findme-*.txt' +need "locate findme- lists it" 'for f in /root/findme-*.txt; do locate findme- | grep -qxF "$f" && exit 0; done; exit 1' + +task 3 "Hard link survives, symbolic link dangles" +need "/tmp/t.txt is deleted" '[ ! -e /tmp/t.txt ] && [ ! -L /tmp/t.txt ]' +need "/tmp/h.txt is a regular file containing hi" '[ -f /tmp/h.txt ] && [ ! -L /tmp/h.txt ] && [ "$(cat /tmp/h.txt)" = hi ]' +need "/tmp/h.txt is the last name of its inode" '[ "$(stat -c %h /tmp/h.txt)" = 1 ]' +need "/tmp/s.txt is a symbolic link to t.txt" '[ -L /tmp/s.txt ] && [ "$(basename "$(readlink /tmp/s.txt)")" = t.txt ]' + +task 4 "The PID holding /tmp/held.txt open" +need "/root/lab-answers/4 has the PID" '[ -n "$(held_pid)" ] && [ "$(answer 4)" = "$(held_pid)" ]' diff --git a/labs/linux-plus/2-1-2-finding-files-links-devices/lab.conf b/labs/linux-plus/2-1-2-finding-files-links-devices/lab.conf new file mode 100644 index 0000000..afcde44 --- /dev/null +++ b/labs/linux-plus/2-1-2-finding-files-links-devices/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.1.2 Finding files, links and device files" +CHAPTER=/linux-plus/2-services-and-user-management/2-1-2-finding-files-links-devices/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-1-2-finding-files-links-devices/reset.sh b/labs/linux-plus/2-1-2-finding-files-links-devices/reset.sh new file mode 100644 index 0000000..aebb1be --- /dev/null +++ b/labs/linux-plus/2-1-2-finding-files-links-devices/reset.sh @@ -0,0 +1,12 @@ +systemctl stop le-lab-held.service 2>/dev/null +systemctl reset-failed le-lab-held.service 2>/dev/null +rm -rf /var/lib/le-report /usr/local/sbin/le-backup-helper +rm -f /tmp/held.txt /tmp/t.txt /tmp/h.txt /tmp/s.txt /root/findme-*.txt +if [ -n "$(state_get plocate)" ]; then + if is_rhel; then dnf -y -q remove plocate >/dev/null; else DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge plocate >/dev/null 2>&1; fi + rm -rf /var/lib/plocate +fi +if [ -n "$(state_get lsof)" ]; then + if is_rhel; then dnf -y -q remove lsof >/dev/null; else DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge lsof >/dev/null 2>&1; fi +fi +true diff --git a/labs/linux-plus/2-1-2-finding-files-links-devices/setup.sh b/labs/linux-plus/2-1-2-finding-files-links-devices/setup.sh new file mode 100644 index 0000000..95f0567 --- /dev/null +++ b/labs/linux-plus/2-1-2-finding-files-links-devices/setup.sh @@ -0,0 +1,11 @@ +for f in /root/findme-*.txt /tmp/t.txt /tmp/h.txt /tmp/s.txt; do + [ ! -e "$f" ] && [ ! -L "$f" ] || refuse "$f already exists; remove it first" +done +pkg_installed plocate || { pkg_install plocate; state_set plocate 1; } +command -v lsof >/dev/null || { pkg_install lsof; state_set lsof 1; } +# Something large and something set-user-ID to find. +mkdir -p /var/lib/le-report +head -c 60M /dev/zero >/var/lib/le-report/archive.dat +install -m 4755 /usr/bin/true /usr/local/sbin/le-backup-helper +# A process holding /tmp/held.txt open. +systemd-run --quiet --unit=le-lab-held -p StandardOutput=file:/tmp/held.txt /usr/bin/sleep infinity diff --git a/labs/linux-plus/2-2-1-users-and-groups/README.md b/labs/linux-plus/2-2-1-users-and-groups/README.md new file mode 100644 index 0000000..46b3421 --- /dev/null +++ b/labs/linux-plus/2-2-1-users-and-groups/README.md @@ -0,0 +1,32 @@ +# Linux+ 2.2.1: Users and groups + +Reading the account files, a skeleton directory, a user, a group and a +service account, and the `usermod -G` trap. + +## Before you start + +The lab only checks that `labuser`, `labsvc`, `labgroup` and +`/etc/skel/bin` don't exist yet; it doesn't create anything. Write answers +to the files in `/root/lab-answers/`. Don't run the chapter's clean-up +step: `le-lab reset` does it. + +## Tasks + +1. Find the lowest UID that `useradd` gives a normal user on this system, + and write it to `/root/lab-answers/1`. +2. Add a directory `bin` to `/etc/skel`. Then create user `labuser` with + the comment `Lab User`, shell `/bin/bash` and a home directory (which + should get the `bin` directory); a group `labgroup` with `labuser` as a + member; and a system account `labsvc` with no home directory that can't + log in. +3. Add `labuser` to the administrators' group (`wheel` on RHEL relatives, + `sudo` on Debian and Ubuntu), then run `usermod -G labgroup labuser` and + see what happens to that membership. Finish with `labuser` in both + groups. + +## Hints + +- `/etc/login.defs`: `UID_MIN`, `SYS_UID_MAX`. +- `useradd -m -c -s`, `groupadd`, `usermod -aG`, `useradd -r -M -s + /sbin/nologin`; `id`, `getent passwd`. +- `-G` without `-a` replaces the list of supplementary groups. diff --git a/labs/linux-plus/2-2-1-users-and-groups/check.sh b/labs/linux-plus/2-2-1-users-and-groups/check.sh new file mode 100644 index 0000000..a9724a5 --- /dev/null +++ b/labs/linux-plus/2-2-1-users-and-groups/check.sh @@ -0,0 +1,22 @@ +uid_min=$(awk '$1 == "UID_MIN" {print $2}' /etc/login.defs) +admin=$(pick wheel sudo) +field() { getent passwd "$1" | cut -d: -f"$2"; } + +task 1 "UID_MIN found" +need "/root/lab-answers/1 has UID_MIN from /etc/login.defs" '[ -n "$uid_min" ] && [ "$(answer 1)" = "$uid_min" ]' + +task 2 "skel bin, labuser, labgroup and service account labsvc" +need "/etc/skel/bin is a directory" '[ -d /etc/skel/bin ]' +need "user labuser exists" id labuser +need "labuser's comment is Lab User" '[ "$(field labuser 5)" = "Lab User" ]' +need "labuser's shell is /bin/bash" '[ "$(field labuser 7)" = /bin/bash ]' +need "labuser's home directory has bin from /etc/skel" '[ -d "$(field labuser 6)/bin" ]' +need "labuser is in group labgroup" 'id -nG labuser | grep -qw labgroup' +need "user labsvc exists" id labsvc +need "labsvc is a system account (UID below UID_MIN)" '[ "$(id -u labsvc)" -lt "$uid_min" ]' +need "labsvc has no home directory" '[ ! -d "$(field labsvc 6)" ]' +need "labsvc can't log in (nologin shell)" '[[ "$(field labsvc 7)" == */nologin ]]' + +task 3 "labuser in labgroup and the administrators' group" +need "labuser is in $admin" 'id -nG labuser | grep -qw "$admin"' +need "labuser is still in labgroup" 'id -nG labuser | grep -qw labgroup' diff --git a/labs/linux-plus/2-2-1-users-and-groups/lab.conf b/labs/linux-plus/2-2-1-users-and-groups/lab.conf new file mode 100644 index 0000000..fea1d0f --- /dev/null +++ b/labs/linux-plus/2-2-1-users-and-groups/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.2.1 Users and groups" +CHAPTER=/linux-plus/2-services-and-user-management/2-2-1-users-and-groups/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-2-1-users-and-groups/reset.sh b/labs/linux-plus/2-2-1-users-and-groups/reset.sh new file mode 100644 index 0000000..ce6107a --- /dev/null +++ b/labs/linux-plus/2-2-1-users-and-groups/reset.sh @@ -0,0 +1,5 @@ +userdel -r labuser 2>/dev/null +userdel -r labsvc 2>/dev/null +groupdel labgroup 2>/dev/null +rm -rf /etc/skel/bin +true diff --git a/labs/linux-plus/2-2-1-users-and-groups/setup.sh b/labs/linux-plus/2-2-1-users-and-groups/setup.sh new file mode 100644 index 0000000..edea14d --- /dev/null +++ b/labs/linux-plus/2-2-1-users-and-groups/setup.sh @@ -0,0 +1,4 @@ +for u in labuser labsvc; do ! id "$u" >/dev/null 2>&1 || refuse "user $u already exists; remove it first"; done +! getent group labgroup >/dev/null || refuse "group labgroup already exists; remove it first" +[ ! -e /etc/skel/bin ] || refuse "/etc/skel/bin already exists; remove it first" +true diff --git a/labs/linux-plus/2-2-2-locking-expiry-and-identity/README.md b/labs/linux-plus/2-2-2-locking-expiry-and-identity/README.md new file mode 100644 index 0000000..5fb6159 --- /dev/null +++ b/labs/linux-plus/2-2-2-locking-expiry-and-identity/README.md @@ -0,0 +1,27 @@ +# Linux+ 2.2.2: Locking, expiry and who's who + +Password aging, forcing a password change, locking and expiring an +account. + +## Before you start + +The lab only checks that user `agetest` doesn't exist yet. Don't remove +`agetest` at the end: the lab grades it, and `le-lab reset` removes it. + +## Tasks + +1. Create user `agetest` with a home directory and the password + `Lab-Pass-123`, and look at its password aging and the defaults in + `/etc/login.defs`. +2. Force `agetest` to change the password at the next login, and see it + happen with `su - agetest` (press Ctrl+C at the new-password prompt). +3. Lock `agetest`'s password and check its status, then unlock it again. + Finally, expire the account itself (expiry day 0). + +## Hints + +- `useradd -m`, `chpasswd`, `chage -l`, `grep '^PASS_' /etc/login.defs`. +- `chage -d 0`. +- `passwd -l`, `passwd -S`, `passwd -u`, `chage -E 0`, `chage -l`. +- The chapter's step 4 (who's who) isn't graded: try `id`, `groups`, `who`, + `w`, `last -n 5` and `lastb -n 5` (or `lslogins --failed`) anyway. diff --git a/labs/linux-plus/2-2-2-locking-expiry-and-identity/check.sh b/labs/linux-plus/2-2-2-locking-expiry-and-identity/check.sh new file mode 100644 index 0000000..1cfc81d --- /dev/null +++ b/labs/linux-plus/2-2-2-locking-expiry-and-identity/check.sh @@ -0,0 +1,13 @@ +sh_field() { getent shadow agetest | cut -d: -f"$1"; } + +task 1 "agetest with a home directory and a password" +need "user agetest exists" id agetest +need "agetest has a home directory" '[ -d "$(getent passwd agetest | cut -d: -f6)" ]' +need "agetest has a password set" 'h=$(sh_field 2); h=${h#!}; [ -n "$h" ] && [ "${h#\*}" = "$h" ]' + +task 2 "agetest must change the password at the next login" +need "the last password change is day 0" '[ "$(sh_field 3)" = 0 ]' + +task 3 "Password unlocked again, account expired" +need "agetest's password isn't locked" '[ -n "$(sh_field 2)" ] && [ "$(sh_field 2 | cut -c1)" != "!" ]' +need "the account expires on day 0" '[ "$(sh_field 8)" = 0 ]' diff --git a/labs/linux-plus/2-2-2-locking-expiry-and-identity/lab.conf b/labs/linux-plus/2-2-2-locking-expiry-and-identity/lab.conf new file mode 100644 index 0000000..7a3f7b8 --- /dev/null +++ b/labs/linux-plus/2-2-2-locking-expiry-and-identity/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.2.2 Locking, expiry and who's who" +CHAPTER=/linux-plus/2-services-and-user-management/2-2-2-locking-expiry-and-identity/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-2-2-locking-expiry-and-identity/reset.sh b/labs/linux-plus/2-2-2-locking-expiry-and-identity/reset.sh new file mode 100644 index 0000000..e391238 --- /dev/null +++ b/labs/linux-plus/2-2-2-locking-expiry-and-identity/reset.sh @@ -0,0 +1,3 @@ +pkill -KILL -u agetest 2>/dev/null +userdel -r agetest 2>/dev/null +true diff --git a/labs/linux-plus/2-2-2-locking-expiry-and-identity/setup.sh b/labs/linux-plus/2-2-2-locking-expiry-and-identity/setup.sh new file mode 100644 index 0000000..00892c9 --- /dev/null +++ b/labs/linux-plus/2-2-2-locking-expiry-and-identity/setup.sh @@ -0,0 +1,2 @@ +! id agetest >/dev/null 2>&1 || refuse "user agetest already exists; remove it first" +true diff --git a/labs/linux-plus/2-3-1-viewing-processes/README.md b/labs/linux-plus/2-3-1-viewing-processes/README.md new file mode 100644 index 0000000..7f6e7c0 --- /dev/null +++ b/labs/linux-plus/2-3-1-viewing-processes/README.md @@ -0,0 +1,31 @@ +# Linux+ 2.3.1: Looking at processes + +Process states, zombies and their parents, finding a busy process with the +sysstat tools, and tracing a program's system calls. + +## Before you start + +The lab installs sysstat and strace, and starts some processes for you to +find: one that is stopped, one whose child is a zombie, and one that keeps +a CPU core busy. It also installs a small program, +`/usr/local/bin/lab-report`. Write answers to the files in +`/root/lab-answers/`. Leave the processes running until you've checked. + +## Tasks + +1. Find the stopped process (state `T`), and write its PID to + `/root/lab-answers/1`. +2. Find the zombie (state `Z`). Write the PID of its parent, the process + that never collected it, to `/root/lab-answers/2`. +3. One process keeps a CPU core busy. Confirm it with `mpstat` and `pidstat` + and write its PID to `/root/lab-answers/3`. +4. Trace `/usr/local/bin/lab-report` with strace to see which files it + opens, and write the full path of the configuration file it reads to + `/root/lab-answers/4`. + +## Hints + +- `ps -eo pid,ppid,stat,%cpu,%mem,cmd`, `pstree -ps PID`. +- `ps -eo pid,ppid,stat,cmd | grep '[d]efunct'`; the parent is the PPID. +- `mpstat -P ALL 1 3`, `pidstat 1 3`, `top`. +- `strace -f -e trace=openat COMMAND`. diff --git a/labs/linux-plus/2-3-1-viewing-processes/check.sh b/labs/linux-plus/2-3-1-viewing-processes/check.sh new file mode 100644 index 0000000..5b63aef --- /dev/null +++ b/labs/linux-plus/2-3-1-viewing-processes/check.sh @@ -0,0 +1,14 @@ +mainpid() { systemctl show -p MainPID --value "$1"; } + +task 1 "The stopped process" +need "the stopped process is still there" '[ "$(ps -o stat= -p "$(mainpid le-lab-stopped.service)" | cut -c1)" = T ]' +need "/root/lab-answers/1 has its PID" '[ "$(answer 1)" = "$(mainpid le-lab-stopped.service)" ]' + +task 2 "The zombie's parent" +need "/root/lab-answers/2 has the PID of the zombie's parent" '[ "$(answer 2)" = "$(mainpid le-lab-zombie.service)" ]' + +task 3 "The busy process" +need "/root/lab-answers/3 has the PID of the process using a core" '[ "$(answer 3)" = "$(mainpid le-lab-busy.service)" ]' + +task 4 "The file lab-report reads" +need "/root/lab-answers/4 has the path of its rules file" '[ "$(answer 4)" = /var/lib/lab-report/rules.conf ]' diff --git a/labs/linux-plus/2-3-1-viewing-processes/lab.conf b/labs/linux-plus/2-3-1-viewing-processes/lab.conf new file mode 100644 index 0000000..dd4a287 --- /dev/null +++ b/labs/linux-plus/2-3-1-viewing-processes/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.3.1 Looking at processes" +CHAPTER=/linux-plus/2-services-and-user-management/2-3-1-viewing-processes/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-3-1-viewing-processes/reset.sh b/labs/linux-plus/2-3-1-viewing-processes/reset.sh new file mode 100644 index 0000000..dc5f3f5 --- /dev/null +++ b/labs/linux-plus/2-3-1-viewing-processes/reset.sh @@ -0,0 +1,8 @@ +for u in le-lab-stopped le-lab-zombie le-lab-busy; do + systemctl kill -s KILL "$u.service" 2>/dev/null + systemctl stop "$u.service" 2>/dev/null + systemctl reset-failed "$u.service" 2>/dev/null +done +rm -rf /var/lib/lab-report /usr/local/bin/lab-report /usr/local/lib/le-lab/indexer +rmdir /usr/local/lib/le-lab 2>/dev/null +true diff --git a/labs/linux-plus/2-3-1-viewing-processes/setup.sh b/labs/linux-plus/2-3-1-viewing-processes/setup.sh new file mode 100644 index 0000000..90b2bc3 --- /dev/null +++ b/labs/linux-plus/2-3-1-viewing-processes/setup.sh @@ -0,0 +1,18 @@ +pkg_install sysstat strace +mkdir -p /var/lib/lab-report /usr/local/lib/le-lab +printf 'level=2\nformat=text\n' >/var/lib/lab-report/rules.conf +cat >/usr/local/bin/lab-report <<'X' +#!/bin/bash +# Writes a short report; its settings come from its rules file. +n=0 +while read -r line; do n=$((n + 1)); done /dev/null + systemctl reset-failed "$u.service" 2>/dev/null +done +true diff --git a/labs/linux-plus/2-3-2-signals-jobs-priority/setup.sh b/labs/linux-plus/2-3-2-signals-jobs-priority/setup.sh new file mode 100644 index 0000000..2f0b029 --- /dev/null +++ b/labs/linux-plus/2-3-2-signals-jobs-priority/setup.sh @@ -0,0 +1,9 @@ +for c in 'sleep 1000' 'sleep 1001'; do + ! pgrep -fx "$c" >/dev/null || refuse "a process '$c' is already running; stop it first" +done +user_add alice +# As if alice had typed them: argv[0] is the bare name, as in a shell. +for n in 1000 1001; do + systemd-run --quiet --uid=alice --unit=le-lab-sleep$n /bin/bash -c "exec -a sleep /usr/bin/sleep $n" +done +systemd-run --quiet --uid=alice --unit=le-lab-yes -p Nice=15 -p StandardOutput=null /bin/bash -c 'exec -a yes /usr/bin/yes' diff --git a/labs/linux-plus/2-3-3-scheduling/README.md b/labs/linux-plus/2-3-3-scheduling/README.md new file mode 100644 index 0000000..c64f81d --- /dev/null +++ b/labs/linux-plus/2-3-3-scheduling/README.md @@ -0,0 +1,32 @@ +# Linux+ 2.3.3: Scheduling jobs + +A cron job that runs every minute, the environment cron jobs get, and a +one-off job with `at`. + +## Before you start + +Work as root. The lab makes sure cron is installed and running, and +removes old copies of the files the tasks create in `/tmp`. Two of the +tasks take a few minutes of waiting. + +## Tasks + +1. Add a job to your crontab that appends the output of `date` to + `/tmp/cron-test.txt` every minute. When it has run at least twice, + remove the job from your crontab again (keep the file). +2. Add a job to your crontab that writes its environment (`env`) to + `/tmp/cron-env.txt` every minute. When it has run, compare that file + with your shell's environment, then remove the job (keep the file). +3. Install `at` if needed and make sure `atd` is running and starts at + boot. Schedule a one-off job that creates `/tmp/at-ran` two minutes + from now, and wait for it. + +## Hints + +- `(crontab -l 2>/dev/null; echo '* * * * * ...') | crontab -`, + `crontab -l | grep -v PATTERN | crontab -`. +- `diff <(sort /tmp/cron-env.txt) <(env | sort)`: cron's `PATH` and + `SHELL` are minimal. +- `systemctl enable --now atd`, `echo 'touch /tmp/at-ran' | at now + 2 + minutes`, `atq`. +- The chapter's step 4 (anacron and `/etc/crontab`) isn't graded. diff --git a/labs/linux-plus/2-3-3-scheduling/check.sh b/labs/linux-plus/2-3-3-scheduling/check.sh new file mode 100644 index 0000000..f9101ea --- /dev/null +++ b/labs/linux-plus/2-3-3-scheduling/check.sh @@ -0,0 +1,16 @@ +# Every user crontab on the system, RHEL or Debian layout. +crontabs() { cat /var/spool/cron/[!c]* /var/spool/cron/crontabs/* 2>/dev/null | grep -v '^[[:space:]]*#'; } + +task 1 "Cron appended the date every minute, then the job was removed" +need "/tmp/cron-test.txt has at least two lines" '[ "$(wc -l /dev/null | grep -vE 'cron-(test|env)' | crontab -u "$u" - + fi +done +if command -v atq >/dev/null; then + atq 2>/dev/null | awk '{print $1}' | xargs -r atrm 2>/dev/null +fi +if [ -z "$(state_get had-at)" ] && pkg_installed at; then + systemctl disable --now --quiet atd 2>/dev/null + if is_rhel; then dnf -y -q remove at >/dev/null; else DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge at >/dev/null 2>&1; fi +elif [ -z "$(state_get atd-active)" ]; then + systemctl disable --now --quiet atd 2>/dev/null +fi +rm -f /tmp/cron-test.txt /tmp/cron-env.txt /tmp/at-ran +true diff --git a/labs/linux-plus/2-3-3-scheduling/setup.sh b/labs/linux-plus/2-3-3-scheduling/setup.sh new file mode 100644 index 0000000..a53d66b --- /dev/null +++ b/labs/linux-plus/2-3-3-scheduling/setup.sh @@ -0,0 +1,6 @@ +cron=$(pick crond cron) +if ! pkg_installed "$(pick cronie cron)"; then pkg_install cronie/cron; fi +systemctl enable --now --quiet "$cron" +pkg_installed at && state_set had-at 1 +if systemctl is-active --quiet atd; then state_set atd-active 1; fi +rm -f /tmp/cron-test.txt /tmp/cron-env.txt /tmp/at-ran diff --git a/labs/linux-plus/2-4-1-package-managers/README.md b/labs/linux-plus/2-4-1-package-managers/README.md new file mode 100644 index 0000000..3419ae8 --- /dev/null +++ b/labs/linux-plus/2-4-1-package-managers/README.md @@ -0,0 +1,29 @@ +# Linux+ 2.4.1: Package managers + +Asking which package owns a file, installing and removing with a history +to show for it, and holding a package back from upgrades. + +## Before you start + +The lab notes where your package history stands, so it can see what you +install and remove from now on. It needs `tree` not to be installed. +Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Find out which package owns `/usr/bin/ssh`, and write the package name + (without version) to `/root/lab-answers/1`. Look at its description and + file list too. +2. Install `tree`, check the package history, and then remove it again (on + RHEL relatives, by undoing the last transaction). +3. Hold `bash` so that upgrades skip it, and check that an upgrade leaves it + alone. Leave the hold in place for grading. + +## Hints + +- `rpm -qf`, `rpm -qi`, `rpm -ql`; `dpkg -S`, `dpkg -s`, `dpkg -L`. +- `dnf history`, `dnf history undo last`; `apt install`, `apt remove`, + `/var/log/apt/history.log`. +- `dnf versionlock add` (package `python3-dnf-plugin-versionlock`), + `dnf upgrade --assumeno`; `apt-mark hold`, `apt-mark showhold`. +- The chapter's step 4 (the signing keys you trust) isn't graded. diff --git a/labs/linux-plus/2-4-1-package-managers/check.sh b/labs/linux-plus/2-4-1-package-managers/check.sh new file mode 100644 index 0000000..b95eb2b --- /dev/null +++ b/labs/linux-plus/2-4-1-package-managers/check.sh @@ -0,0 +1,21 @@ +owner=$(if is_rhel; then rpm -qf --qf '%{NAME}\n' /usr/bin/ssh; else dpkg -S /usr/bin/ssh | cut -d: -f1; fi 2>/dev/null) +h0=$(state_get history) +tree_installed() { + if is_rhel; then dnf history list tree 2>/dev/null | awk -F'|' -v h="${h0:-0}" '$1+0 > h && $4 ~ /Install/' | grep -q . + else tail -n +"$((h0 + 1))" /var/log/apt/history.log | grep -q '^Install: .*\btree:'; fi +} +tree_removed() { + if is_rhel; then dnf history list tree 2>/dev/null | awk -F'|' -v h="${h0:-0}" '$1+0 > h && $4 ~ /Remove|Erase/' | grep -q . + else tail -n +"$((h0 + 1))" /var/log/apt/history.log | grep -Eq '^(Remove|Purge): .*\btree:'; fi +} + +task 1 "The package that owns /usr/bin/ssh" +need "/root/lab-answers/1 names the package" '[ -n "$owner" ] && [ "$(answer 1)" = "$owner" ]' + +task 2 "tree installed, then removed again" +need "the package history shows tree installed" tree_installed +need "the package history shows tree removed" tree_removed +need "tree is no longer installed" '! pkg_installed tree' + +task 3 "bash held back from upgrades" +need "bash is held" 'if is_rhel; then dnf versionlock list 2>/dev/null | grep -q "^bash-"; else apt-mark showhold | grep -qx bash; fi' diff --git a/labs/linux-plus/2-4-1-package-managers/lab.conf b/labs/linux-plus/2-4-1-package-managers/lab.conf new file mode 100644 index 0000000..da80049 --- /dev/null +++ b/labs/linux-plus/2-4-1-package-managers/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.4.1 Package managers" +CHAPTER=/linux-plus/2-services-and-user-management/2-4-1-package-managers/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-4-1-package-managers/reset.sh b/labs/linux-plus/2-4-1-package-managers/reset.sh new file mode 100644 index 0000000..7308ece --- /dev/null +++ b/labs/linux-plus/2-4-1-package-managers/reset.sh @@ -0,0 +1,11 @@ +if is_rhel; then + dnf versionlock delete bash >/dev/null 2>&1 + if [ -z "$(state_get had-versionlock)" ] && pkg_installed python3-dnf-plugin-versionlock; then + dnf -y -q remove python3-dnf-plugin-versionlock >/dev/null + fi + pkg_installed tree && dnf -y -q remove tree >/dev/null +else + apt-mark unhold bash >/dev/null 2>&1 + pkg_installed tree && DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge tree >/dev/null 2>&1 +fi +true diff --git a/labs/linux-plus/2-4-1-package-managers/setup.sh b/labs/linux-plus/2-4-1-package-managers/setup.sh new file mode 100644 index 0000000..2c15a4a --- /dev/null +++ b/labs/linux-plus/2-4-1-package-managers/setup.sh @@ -0,0 +1,10 @@ +! pkg_installed tree || refuse "tree is installed; remove it first (the lab grades installing and removing it)" +if is_rhel; then + state_set history "$(dnf history list 2>/dev/null | awk -F'|' 'NR > 2 {gsub(/ /, "", $1); print $1; exit}')" + pkg_installed python3-dnf-plugin-versionlock && state_set had-versionlock 1 + if dnf versionlock list 2>/dev/null | grep -q '^bash-'; then refuse "bash is already version-locked; run dnf versionlock delete bash first"; fi +else + state_set history "$(cat /var/log/apt/history.log 2>/dev/null | wc -l)" + ! apt-mark showhold | grep -qx bash || refuse "bash is already on hold; run apt-mark unhold bash first" +fi +true diff --git a/labs/linux-plus/2-4-2-repositories-and-other-sources/README.md b/labs/linux-plus/2-4-2-repositories-and-other-sources/README.md new file mode 100644 index 0000000..ef3fe0e --- /dev/null +++ b/labs/linux-plus/2-4-2-repositories-and-other-sources/README.md @@ -0,0 +1,36 @@ +# Linux+ 2.4.2: Repositories and other sources + +An extra repository, alternatives, a Python virtual environment, and +building a small program from source. + +## Before you start + +Work as root, so `~` is `/root`. The VM needs internet access. The lab +sets the `editor` alternative to automatic mode (on Debian and Ubuntu) and +checks that `~/venv` and `~/src` don't exist yet. Leave your work in +place for grading. + +## Tasks + +1. RHEL relatives: add the EPEL repository, enable the CRB repository, and + install `htop` from EPEL. +2. Debian and Ubuntu: look at the APT sources and the alternatives for + `editor`, then choose an editor yourself with `update-alternatives + --config editor`. +3. See what `pip3 install` does outside a virtual environment, then create + the virtual environment `~/venv` and install `requests` into it. +4. In `~/src`, write `hi.c` (a program that prints `built`) and a + `Makefile` with a `hi` target that compiles it and an `install` target + that copies it to `/usr/local/bin/hi`. Build it, install it, and run + `hi`. + +## Hints + +- `dnf repolist --all`, `dnf install epel-release`, `dnf config-manager + --set-enabled crb`, `dnf list --installed htop` (the repo column). +- `cat /etc/apt/sources.list.d/*.sources`, `update-alternatives + --display editor`. +- Ubuntu needs `python3-venv` (and `python3-pip`); `python3 -m venv`, + `~/venv/bin/pip install`, `~/venv/bin/pip list`. +- `gcc` and `make` (`build-essential` on Ubuntu); Makefile recipe lines + start with a tab. diff --git a/labs/linux-plus/2-4-2-repositories-and-other-sources/check.sh b/labs/linux-plus/2-4-2-repositories-and-other-sources/check.sh new file mode 100644 index 0000000..13be10f --- /dev/null +++ b/labs/linux-plus/2-4-2-repositories-and-other-sources/check.sh @@ -0,0 +1,17 @@ +task 1 "EPEL and CRB enabled, htop from EPEL" rhel +need "epel-release is installed" 'pkg_installed epel-release' +need "the crb repository is enabled" 'dnf repolist --enabled 2>/dev/null | grep -q "^crb "' +need "htop is installed from EPEL" 'dnf list --installed htop 2>/dev/null | grep -q epel' + +task 2 "editor chosen with update-alternatives" debian +need "the editor alternative is set by hand (manual mode)" 'update-alternatives --query editor | grep -qx "Status: manual"' + +task 3 "~/venv with requests" +need "~/venv is a virtual environment" '[ -x /root/venv/bin/python3 ] && [ -f /root/venv/pyvenv.cfg ]' +need "requests is installed in it" '/root/venv/bin/python3 -c "import requests" && /root/venv/bin/pip show requests' + +task 4 "hi built from source with make" +need "~/src/hi.c and ~/src/Makefile exist" '[ -f /root/src/hi.c ] && [ -f /root/src/Makefile ]' +need "the Makefile has hi and install targets" 'grep -q "^hi:" /root/src/Makefile && grep -q "^install:" /root/src/Makefile' +need "~/src/hi prints built" '[ "$(/root/src/hi)" = built ]' +need "make install puts it in /usr/local/bin" 'make -C /root/src -n install | grep -q /usr/local/bin' diff --git a/labs/linux-plus/2-4-2-repositories-and-other-sources/lab.conf b/labs/linux-plus/2-4-2-repositories-and-other-sources/lab.conf new file mode 100644 index 0000000..b9c8ae7 --- /dev/null +++ b/labs/linux-plus/2-4-2-repositories-and-other-sources/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.4.2 Repositories and other sources" +CHAPTER=/linux-plus/2-services-and-user-management/2-4-2-repositories-and-other-sources/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-4-2-repositories-and-other-sources/reset.sh b/labs/linux-plus/2-4-2-repositories-and-other-sources/reset.sh new file mode 100644 index 0000000..61e5742 --- /dev/null +++ b/labs/linux-plus/2-4-2-repositories-and-other-sources/reset.sh @@ -0,0 +1,9 @@ +rm -rf /root/venv /root/src /usr/local/bin/hi +if is_rhel; then + pkg_installed htop && dnf -y -q remove htop >/dev/null + if [ -z "$(state_get had-crb)" ]; then dnf config-manager --set-disabled crb >/dev/null 2>&1; fi + if [ -z "$(state_get had-epel)" ] && pkg_installed epel-release; then dnf -y -q remove epel-release >/dev/null; fi +else + update-alternatives --auto editor >/dev/null 2>&1 +fi +true diff --git a/labs/linux-plus/2-4-2-repositories-and-other-sources/setup.sh b/labs/linux-plus/2-4-2-repositories-and-other-sources/setup.sh new file mode 100644 index 0000000..c50f670 --- /dev/null +++ b/labs/linux-plus/2-4-2-repositories-and-other-sources/setup.sh @@ -0,0 +1,10 @@ +for f in /root/venv /root/src; do [ ! -e "$f" ] || refuse "$f already exists; move it away first"; done +if is_rhel; then + pkg_installed htop && refuse "htop is installed; remove it first (task 1 installs it from EPEL)" + pkg_installed epel-release && state_set had-epel 1 + dnf repolist --enabled 2>/dev/null | grep -q '^crb ' && state_set had-crb 1 +else + update-alternatives --query editor >/dev/null 2>&1 || refuse "there is no editor alternative to choose from" + update-alternatives --auto editor >/dev/null 2>&1 +fi +true diff --git a/labs/linux-plus/2-4-3-infrastructure-services/README.md b/labs/linux-plus/2-4-3-infrastructure-services/README.md new file mode 100644 index 0000000..9d106af --- /dev/null +++ b/labs/linux-plus/2-4-3-infrastructure-services/README.md @@ -0,0 +1,34 @@ +# Linux+ 2.4.3: DNS, time and DHCP services + +Which service keeps the clock in sync, a caching DNS server that another +machine can use, and reading a DHCP lease from the client side. + +## Before you start + +The lab sets up a second machine, `peer` (10.99.0.2), linked to this VM's +address 10.99.0.1. It stands in for the chapter's other VM. On RHEL +relatives the lab installs `bind-utils` for `dig`. Write answers to the +files in `/root/lab-answers/`. + +## Tasks + +1. Find out which service keeps this VM's clock in sync, and write its + name (`chronyd` or `systemd-timesyncd`) to `/root/lab-answers/1`. +2. RHEL relatives: build a caching DNS server with Unbound that listens on + all addresses and answers clients from 10.0.0.0/8 and 192.168.0.0/16, + and that starts at boot. Open the firewall for DNS if it's running. + From the peer, `dig @10.99.0.1` should get answers. +3. Find the address of the DHCP server that gave this VM its address, and + write it to `/root/lab-answers/3`. + +## Hints + +- `chronyc sources -v`, `chronyc tracking`, `timedatectl`, + `timedatectl timesync-status`, `systemctl status`. +- A file in `/etc/unbound/conf.d/` with `interface:` and + `access-control:` lines, `unbound-checkconf`, `systemctl enable --now`, + `firewall-cmd --add-service=dns` (and `--permanent`); + `ip netns exec lepeer dig @10.99.0.1 www.kernel.org`. +- `nmcli -f DHCP4 device show DEVICE` (`dhcp_server_identifier`); + `networkctl status`, `journalctl -u systemd-networkd`, + `/run/systemd/netif/leases/`. diff --git a/labs/linux-plus/2-4-3-infrastructure-services/check.sh b/labs/linux-plus/2-4-3-infrastructure-services/check.sh new file mode 100644 index 0000000..d61383c --- /dev/null +++ b/labs/linux-plus/2-4-3-infrastructure-services/check.sh @@ -0,0 +1,21 @@ +timesvc() { for s in chronyd systemd-timesyncd; do active $s && { echo $s; return; }; done; } +dhcp_server() { + if command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager; then + nmcli -t -f DHCP4 device show 2>/dev/null | sed -n 's/.*dhcp_server_identifier = //p' + fi + cat /run/systemd/netif/leases/* 2>/dev/null | sed -n 's/^SERVER_ADDRESS=//p' +} +peer_dig() { in_peer dig +time=2 +tries=2 @10.99.0.1 "$@"; } + +task 1 "The time service" +need "a time service is running" '[ -n "$(timesvc)" ]' +need "/root/lab-answers/1 names it" '[ "$(answer 1 | sed "s/\.service$//")" = "$(timesvc)" ] || { [ "$(timesvc)" = chronyd ] && [ "$(answer 1)" = chrony ]; }' + +task 2 "Unbound answers the peer" rhel +need "unbound is running" active unbound +need "unbound starts at boot" enabled unbound +need "unbound listens on all addresses on port 53" 'ss -Hlnu "sport = :53" | grep -Eq "(0\.0\.0\.0|\*):53"' +need "the peer gets an answer from 10.99.0.1 (firewall open, access allowed)" 'peer_dig localhost | grep -q "status: NOERROR"' + +task 3 "The DHCP server" +need "/root/lab-answers/3 has the DHCP server's address" 'a=$(answer 3); [ -n "$a" ] && dhcp_server | grep -qxF "$a"' diff --git a/labs/linux-plus/2-4-3-infrastructure-services/lab.conf b/labs/linux-plus/2-4-3-infrastructure-services/lab.conf new file mode 100644 index 0000000..b4f6307 --- /dev/null +++ b/labs/linux-plus/2-4-3-infrastructure-services/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.4.3 DNS, time and DHCP services" +CHAPTER=/linux-plus/2-services-and-user-management/2-4-3-infrastructure-services/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-4-3-infrastructure-services/reset.sh b/labs/linux-plus/2-4-3-infrastructure-services/reset.sh new file mode 100644 index 0000000..9e7ee07 --- /dev/null +++ b/labs/linux-plus/2-4-3-infrastructure-services/reset.sh @@ -0,0 +1,16 @@ +if is_rhel; then + if [ -n "$(state_get had-unbound)" ]; then + if [ -f "$LAB_STATE/unbound.tar" ]; then rm -rf /etc/unbound; tar -C /etc -xf "$LAB_STATE/unbound.tar"; fi + systemctl try-restart unbound 2>/dev/null + elif pkg_installed unbound; then + systemctl disable --now --quiet unbound 2>/dev/null + dnf -y -q remove unbound >/dev/null + rm -rf /etc/unbound + fi + if [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" + systemctl is-active --quiet firewalld && firewall-cmd -q --reload + fi +fi +true diff --git a/labs/linux-plus/2-4-3-infrastructure-services/setup.sh b/labs/linux-plus/2-4-3-infrastructure-services/setup.sh new file mode 100644 index 0000000..329c988 --- /dev/null +++ b/labs/linux-plus/2-4-3-infrastructure-services/setup.sh @@ -0,0 +1,9 @@ +if is_rhel; then + pkg_installed unbound && state_set had-unbound 1 + [ -n "$(state_get had-unbound)" ] && [ -d /etc/unbound ] && tar -C /etc -cf "$LAB_STATE/unbound.tar" unbound + command -v dig >/dev/null || pkg_install bind-utils + if systemctl is-active --quiet firewalld; then + tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld + fi +fi +peer_up diff --git a/labs/linux-plus/2-4-4-web-and-mail-services/README.md b/labs/linux-plus/2-4-4-web-and-mail-services/README.md new file mode 100644 index 0000000..86626a4 --- /dev/null +++ b/labs/linux-plus/2-4-4-web-and-mail-services/README.md @@ -0,0 +1,38 @@ +# Linux+ 2.4.4: Web and mail services + +A name-based site with Nginx on Ubuntu, an Apache virtual host with the +right SELinux label on RHEL relatives, and local mail with Postfix. + +## Before you start + +The VM needs internet access to install packages. The lab only checks that +`/srv/www` doesn't exist yet, and notes the time, so that only mail sent +from now on counts. Leave your work in place for grading: `le-lab reset` +removes the packages it finds you installed. + +## Tasks + +1. Debian and Ubuntu: install Nginx and serve `/srv/www/lab` (with an + `index.html` of your own) for the name `lab.test` on port 80, from a file + in `/etc/nginx/conf.d/`. `curl -s -H 'Host: lab.test' http://127.0.0.1/` + returns your page. Break the syntax on purpose once, and read what + `nginx -t` says before you fix it. +2. RHEL relatives: install Apache and serve `/srv/www/shop` (with an + `index.html` of your own) as the virtual host `shop.test` on port 80, + starting at boot. Keep SELinux enforcing: give `/srv/www` the right + label with a rule that survives a relabel. + `curl -s -H 'Host: shop.test' http://127.0.0.1/` returns your page. +3. Install Postfix and a `mail` command, make sure Postfix is running, and + send yourself a local mail with the subject `test`. Read it with `mail`. + +## Hints + +- `apt install nginx`, a `server {}` block with `listen`, `server_name` + and `root`; `nginx -t`, `systemctl reload nginx`. +- `dnf install httpd`, a `` block in + `/etc/httpd/conf.d/`, `apachectl configtest`; a 403 means the label is + wrong. `semanage fcontext -a -t httpd_sys_content_t '/srv/www(/.*)?'` + (package `policycoreutils-python-utils`), `restorecon -Rv /srv/www`. +- `postfix` with `s-nail` (RHEL relatives) or `mailutils` (Ubuntu, + "Local only"); `systemctl enable --now postfix`; `echo hi | mail -s test + $USER`. diff --git a/labs/linux-plus/2-4-4-web-and-mail-services/check.sh b/labs/linux-plus/2-4-4-web-and-mail-services/check.sh new file mode 100644 index 0000000..6a870d6 --- /dev/null +++ b/labs/linux-plus/2-4-4-web-and-mail-services/check.sh @@ -0,0 +1,28 @@ +page() { curl -s --max-time 5 -H "Host: $1" http://127.0.0.1/; } +new_test_mail() { + local f + for f in /var/mail/* /var/spool/mail/* /root/mbox /home/*/mbox; do + [ -f "$f" ] && [ "$f" -nt "$LAB_STATE/started" ] && grep -q '^Subject: test' "$f" && return 0 + done + return 1 +} + +task 1 "Nginx serves lab.test from /srv/www/lab" debian +need "nginx is running" active nginx +need "/srv/www/lab/index.html exists" '[ -s /srv/www/lab/index.html ]' +need "a file in /etc/nginx/conf.d/ sets up lab.test" 'grep -qs "server_name.*lab\.test" /etc/nginx/conf.d/*.conf' +need "nginx -t passes" 'nginx -t' +need "http://127.0.0.1/ with Host lab.test returns /srv/www/lab/index.html" 'page lab.test | cmp -s - /srv/www/lab/index.html' + +task 2 "Apache serves shop.test from /srv/www/shop" rhel +need "httpd is running" active httpd +need "httpd starts at boot" enabled httpd +need "/srv/www/shop/index.html exists" '[ -s /srv/www/shop/index.html ]' +need "SELinux is enforcing" '[ "$(getenforce)" = Enforcing ]' +need "the SELinux rules give /srv/www/shop httpd_sys_content_t" 'matchpathcon /srv/www/shop/index.html | grep -q httpd_sys_content_t' +need "index.html has that label now" 'ls -Z /srv/www/shop/index.html | grep -q httpd_sys_content_t' +need "http://127.0.0.1/ with Host shop.test returns /srv/www/shop/index.html" 'page shop.test | cmp -s - /srv/www/shop/index.html' + +task 3 "Local mail with the subject test delivered" +need "postfix is running" active postfix +need "a mailbox has a new message with the subject test" new_test_mail diff --git a/labs/linux-plus/2-4-4-web-and-mail-services/lab.conf b/labs/linux-plus/2-4-4-web-and-mail-services/lab.conf new file mode 100644 index 0000000..e5ed393 --- /dev/null +++ b/labs/linux-plus/2-4-4-web-and-mail-services/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.4.4 Web and mail services" +CHAPTER=/linux-plus/2-services-and-user-management/2-4-4-web-and-mail-services/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-4-4-web-and-mail-services/reset.sh b/labs/linux-plus/2-4-4-web-and-mail-services/reset.sh new file mode 100644 index 0000000..70f8fbd --- /dev/null +++ b/labs/linux-plus/2-4-4-web-and-mail-services/reset.sh @@ -0,0 +1,19 @@ +rm -f /etc/nginx/conf.d/lab.conf /etc/httpd/conf.d/shop.conf +grep -ls 'lab\.test' /etc/nginx/conf.d/*.conf 2>/dev/null | xargs -r rm -f +grep -ls 'shop\.test' /etc/httpd/conf.d/*.conf 2>/dev/null | xargs -r rm -f +command -v semanage >/dev/null && semanage fcontext -d '/srv/www(/.*)?' 2>/dev/null +rm -rf /srv/www +for p in nginx httpd postfix s-nail mailutils; do + [ -n "$(state_get had-$p)" ] && continue + pkg_installed $p || continue + systemctl disable --now --quiet $p 2>/dev/null + if is_rhel; then + dnf -y -q remove $p >/dev/null + else + pkgs=$p + [ $p = nginx ] && pkgs="nginx nginx-common" + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $pkgs >/dev/null 2>&1 + fi +done +for p in nginx httpd; do [ -n "$(state_get had-$p)" ] && systemctl try-restart $p 2>/dev/null; done +true diff --git a/labs/linux-plus/2-4-4-web-and-mail-services/setup.sh b/labs/linux-plus/2-4-4-web-and-mail-services/setup.sh new file mode 100644 index 0000000..28a974e --- /dev/null +++ b/labs/linux-plus/2-4-4-web-and-mail-services/setup.sh @@ -0,0 +1,3 @@ +[ ! -e /srv/www ] || refuse "/srv/www already exists; move it away first" +for p in nginx httpd postfix s-nail mailutils; do pkg_installed $p && state_set had-$p 1; done +touch "$LAB_STATE/started" diff --git a/labs/linux-plus/2-5-1-systemd-units/README.md b/labs/linux-plus/2-5-1-systemd-units/README.md new file mode 100644 index 0000000..5de999f --- /dev/null +++ b/labs/linux-plus/2-5-1-systemd-units/README.md @@ -0,0 +1,31 @@ +# Linux+ 2.5.1: systemd units + +A service with a timer, masking and unmasking a unit, and overriding a +vendor unit with a drop-in. + +## Before you start + +The lab installs `at` (for `atd.service`) and stops and disables `atd`. +Leave your work in place for grading: `le-lab reset` removes the timer and +reverts `atd`. + +## Tasks + +1. Write `labbeat.service`, a oneshot service that runs `/usr/bin/logger -t + labbeat "still here"`, and `labbeat.timer`, which runs it every 2 + minutes. Enable and start the timer, and watch the messages arrive in + the journal. +2. Mask `atd.service` and see that starting it is refused. Then unmask it, + and make it run now and at every boot. +3. With a drop-in made by `systemctl edit`, run `atd` at nice level 5. + Check it with `systemctl cat` and `systemctl show -p Nice`. (The chapter + then reverts it with `systemctl revert atd`; leave it in place here.) + +## Hints + +- Unit files go in `/etc/systemd/system/`; `OnCalendar=*:0/2`, + `AccuracySec=1s`, `WantedBy=timers.target`; `systemctl daemon-reload`, + `systemctl list-timers`, `journalctl -t labbeat`. +- `systemctl mask`, `systemctl unmask`, `systemctl enable --now`. +- `systemctl edit atd`: `[Service]` and `Nice=5`; restart `atd` so the + running process picks it up. diff --git a/labs/linux-plus/2-5-1-systemd-units/check.sh b/labs/linux-plus/2-5-1-systemd-units/check.sh new file mode 100644 index 0000000..3cad0d5 --- /dev/null +++ b/labs/linux-plus/2-5-1-systemd-units/check.sh @@ -0,0 +1,19 @@ +atd_nice() { ps -o ni= -p "$(systemctl show -p MainPID --value atd)" | tr -d ' '; } + +task 1 "labbeat.timer runs labbeat.service every 2 minutes" +need "labbeat.service runs logger -t labbeat" 'systemctl cat labbeat.service | grep -Eq "^ExecStart=.*logger.*-t labbeat"' +need "labbeat.service is a oneshot service" '[ "$(systemctl show -p Type --value labbeat.service)" = oneshot ]' +need "labbeat.timer is enabled" enabled labbeat.timer +need "labbeat.timer is active" active labbeat.timer +need "it fires every 2 minutes" 'systemctl show -p TimersCalendar --value labbeat.timer | grep -q "\*-\*-\* \*:00/2:00"' +need "the journal has a labbeat message from it" 'journalctl -q -t labbeat --no-pager | grep -q "still here"' + +task 2 "atd unmasked, running and enabled" +need "atd isn't masked" '[ "$(systemctl is-enabled atd 2>/dev/null)" != masked ]' +need "atd is running" active atd +need "atd starts at boot" enabled atd + +task 3 "atd runs at nice 5 from a drop-in" +need "a drop-in for atd sets Nice=5" 'grep -qsx "Nice=5" /etc/systemd/system/atd.service.d/*.conf' +need "systemctl show atd -p Nice says 5" '[ "$(systemctl show -p Nice --value atd)" = 5 ]' +need "the running atd has nice 5" '[ "$(atd_nice)" = 5 ]' diff --git a/labs/linux-plus/2-5-1-systemd-units/lab.conf b/labs/linux-plus/2-5-1-systemd-units/lab.conf new file mode 100644 index 0000000..581efee --- /dev/null +++ b/labs/linux-plus/2-5-1-systemd-units/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.5.1 systemd units" +CHAPTER=/linux-plus/2-services-and-user-management/2-5-1-systemd-units/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-5-1-systemd-units/reset.sh b/labs/linux-plus/2-5-1-systemd-units/reset.sh new file mode 100644 index 0000000..5ac76e1 --- /dev/null +++ b/labs/linux-plus/2-5-1-systemd-units/reset.sh @@ -0,0 +1,14 @@ +systemctl disable --now --quiet labbeat.timer 2>/dev/null +rm -f /etc/systemd/system/labbeat.service /etc/systemd/system/labbeat.timer +systemctl unmask --quiet atd 2>/dev/null +systemctl revert atd >/dev/null 2>&1 +rm -rf /etc/systemd/system/atd.service.d +systemctl daemon-reload +systemctl reset-failed labbeat.service 2>/dev/null +if [ -z "$(state_get had-at)" ]; then + systemctl disable --now --quiet atd 2>/dev/null + if is_rhel; then dnf -y -q remove at >/dev/null; else DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge at >/dev/null 2>&1; fi +elif [ -n "$(state_get atd-enabled)" ]; then + systemctl enable --now --quiet atd 2>/dev/null +fi +true diff --git a/labs/linux-plus/2-5-1-systemd-units/setup.sh b/labs/linux-plus/2-5-1-systemd-units/setup.sh new file mode 100644 index 0000000..6a84225 --- /dev/null +++ b/labs/linux-plus/2-5-1-systemd-units/setup.sh @@ -0,0 +1,8 @@ +for f in /etc/systemd/system/labbeat.service /etc/systemd/system/labbeat.timer /etc/systemd/system/atd.service.d; do + [ ! -e "$f" ] || refuse "$f already exists; remove it first" +done +pkg_installed at && state_set had-at 1 +pkg_installed at || pkg_install at +enabled atd && state_set atd-enabled 1 +systemctl disable --now --quiet atd 2>/dev/null +true diff --git a/labs/linux-plus/2-5-2-systemd-utilities/README.md b/labs/linux-plus/2-5-2-systemd-utilities/README.md new file mode 100644 index 0000000..4bedd7d --- /dev/null +++ b/labs/linux-plus/2-5-2-systemd-utilities/README.md @@ -0,0 +1,29 @@ +# Linux+ 2.5.2: systemd tools + +Setting the hostname and time zone, and changing a kernel parameter now +and for good. + +## Before you start + +The lab notes the hostname, time zone and `vm.swappiness` it finds, so +that `le-lab reset` can put them back. If the time zone is already UTC, +it sets it to America/Phoenix for now, and if `vm.swappiness` is already +20, it sets it to 60. + +## Tasks + +1. Set the hostname to `lab-alma.example.test` (on an Ubuntu VM, + `lab-ubuntu.example.test` is fine too), so that it stays after a + reboot. +2. Set the time zone to UTC. +3. Set the kernel parameter `vm.swappiness` to 20 now, and in a file in + `/etc/sysctl.d/` so that it's 20 after a reboot too. (The chapter then + removes the file again; leave it in place here.) + +## Hints + +- `hostnamectl`, `hostnamectl set-hostname`, `hostname`. +- `timedatectl list-timezones`, `timedatectl set-timezone`. +- `sysctl -n`, `sysctl -w`, `sysctl --system`. +- The chapter's steps on `resolvectl` (Ubuntu) and `systemd-analyze` aren't + graded; try them anyway. diff --git a/labs/linux-plus/2-5-2-systemd-utilities/check.sh b/labs/linux-plus/2-5-2-systemd-utilities/check.sh new file mode 100644 index 0000000..46e32c8 --- /dev/null +++ b/labs/linux-plus/2-5-2-systemd-utilities/check.sh @@ -0,0 +1,12 @@ +target='^lab-(alma|ubuntu)\.example\.test$' + +task 1 "Hostname lab-alma.example.test" +need "the static hostname is lab-alma.example.test" 'hostnamectl --static | grep -Eq "$target"' +need "hostname prints it too" 'hostname | grep -Eq "$target"' + +task 2 "Time zone UTC" +need "timedatectl shows the time zone UTC" 'case $(timedatectl show -p Timezone --value) in UTC | Etc/UTC) true ;; *) false ;; esac' + +task 3 "vm.swappiness 20, now and after a reboot" +need "vm.swappiness is 20 now" '[ "$(sysctl -n vm.swappiness)" = 20 ]' +need "a file in /etc/sysctl.d sets it to 20" 'grep -Eqs "^[[:space:]]*vm[./]swappiness[[:space:]]*=[[:space:]]*20[[:space:]]*$" /etc/sysctl.d/*.conf' diff --git a/labs/linux-plus/2-5-2-systemd-utilities/lab.conf b/labs/linux-plus/2-5-2-systemd-utilities/lab.conf new file mode 100644 index 0000000..c936d8f --- /dev/null +++ b/labs/linux-plus/2-5-2-systemd-utilities/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.5.2 systemd tools" +CHAPTER=/linux-plus/2-services-and-user-management/2-5-2-systemd-utilities/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-5-2-systemd-utilities/reset.sh b/labs/linux-plus/2-5-2-systemd-utilities/reset.sh new file mode 100644 index 0000000..b69b6c3 --- /dev/null +++ b/labs/linux-plus/2-5-2-systemd-utilities/reset.sh @@ -0,0 +1,8 @@ +h=$(state_get hostname); [ -n "$h" ] && hostnamectl set-hostname "$h" +tz=$(state_get timezone); [ -n "$tz" ] && timedatectl set-timezone "$tz" +for f in $(grep -Els '^[[:space:]]*vm[./]swappiness[[:space:]]*=[[:space:]]*20[[:space:]]*$' /etc/sysctl.d/*.conf); do + sed -i -E '/^[[:space:]]*vm[./]swappiness[[:space:]]*=[[:space:]]*20[[:space:]]*$/d' "$f" + grep -Eq '^[[:space:]]*[^#[:space:]]' "$f" || rm -f "$f" +done +s=$(state_get swappiness); [ -n "$s" ] && sysctl -qw vm.swappiness="$s" +true diff --git a/labs/linux-plus/2-5-2-systemd-utilities/setup.sh b/labs/linux-plus/2-5-2-systemd-utilities/setup.sh new file mode 100644 index 0000000..2350a06 --- /dev/null +++ b/labs/linux-plus/2-5-2-systemd-utilities/setup.sh @@ -0,0 +1,9 @@ +state_set hostname "$(hostnamectl --static 2>/dev/null || hostname)" +tz=$(timedatectl show -p Timezone --value) +state_set timezone "$tz" +state_set swappiness "$(sysctl -n vm.swappiness)" +case $(hostnamectl --static) in lab-alma.example.test | lab-ubuntu.example.test) + hostnamectl set-hostname "$(hostnamectl --static | cut -d. -f1)" ;; +esac +case $tz in UTC | Etc/UTC | Etc/UCT | UCT | Universal | Zulu) timedatectl set-timezone America/Phoenix ;; esac +[ "$(sysctl -n vm.swappiness)" != 20 ] || sysctl -qw vm.swappiness=60 diff --git a/labs/linux-plus/2-6-1-runtimes-and-images/README.md b/labs/linux-plus/2-6-1-runtimes-and-images/README.md new file mode 100644 index 0000000..d747bd8 --- /dev/null +++ b/labs/linux-plus/2-6-1-runtimes-and-images/README.md @@ -0,0 +1,38 @@ +# Linux+ 2.6.1: Container runtimes and images + +Pulling and inspecting an image, building one from a Containerfile, +rebuilding with the layer cache, and tagging and pruning, all rootless. + +## Before you start + +The VM needs internet access to pull images. The lab installs Podman and +creates user `alice` (password `Lab-Pass-1`), with lingering turned on so +her user services run. Do every task as alice: log in as her, or use +`sudo -iu alice` and then `export XDG_RUNTIME_DIR=/run/user/$(id -u)`. + +## Tasks + +1. Pull `registry.access.redhat.com/ubi10/ubi-minimal`, list your images, + and show its OS, architecture and default command. +2. In `~/img`, write `app.py` (it prints `hello from` and its arguments) + and a `Containerfile` based on that image that installs `python3` with + `microdnf`, copies `app.py` to `/opt/app.py`, runs as user 1001, has + the entrypoint `python3 /opt/app.py` and the default argument + `default`. Build it as `hello:1.0`, run it with and without arguments, + and look at its history. +3. Change `app.py`, rebuild `hello:1.0`, and notice that the package layer + comes from the cache. +4. Tag `hello:1.0` as `hello:latest`, remove the `latest` tag again, and + prune the dangling images that rebuilding left behind. + +## Hints + +- `podman pull`, `podman images`, `podman image inspect --format + '{{.Os}}/{{.Architecture}} {{.Config.Cmd}}'`. +- `FROM`, `RUN microdnf install -y python3 && microdnf clean all`, `COPY`, + `USER 1001`, `ENTRYPOINT ["python3", "/opt/app.py"]`, `CMD + ["default"]`; `podman build -t hello:1.0 .`, `podman run --rm`, + `podman history`. +- "Using cache" in the build output. +- `podman tag`, `podman rmi`, `podman image prune -f`, + `podman images -f dangling=true`. diff --git a/labs/linux-plus/2-6-1-runtimes-and-images/check.sh b/labs/linux-plus/2-6-1-runtimes-and-images/check.sh new file mode 100644 index 0000000..2ce2806 --- /dev/null +++ b/labs/linux-plus/2-6-1-runtimes-and-images/check.sh @@ -0,0 +1,25 @@ +auid=$(id -u alice 2>/dev/null) +apodman() { (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$auid podman "$@"); } +H=localhost/hello:1.0 +orig=$(printf 'import sys\nprint("hello from", sys.argv[1:])\n') +cfg() { apodman image inspect -f "$1" "$H"; } + +task 1 "ubi10/ubi-minimal pulled" +need "alice has registry.access.redhat.com/ubi10/ubi-minimal" 'apodman images --format "{{.Repository}}" | grep -qx registry.access.redhat.com/ubi10/ubi-minimal' + +task 2 "hello:1.0 built from ~/img" +need "~/img has app.py and a Containerfile" '[ -f /home/alice/img/app.py ] && [ -f /home/alice/img/Containerfile ]' +need "alice has the image localhost/hello:1.0" 'apodman image exists "$H"' +need "its entrypoint is python3 /opt/app.py" '[ "$(cfg "{{json .Config.Entrypoint}}")" = "[\"python3\",\"/opt/app.py\"]" ]' +need "its default argument is default" '[ "$(cfg "{{json .Config.Cmd}}")" = "[\"default\"]" ]' +need "it runs as user 1001" '[ "$(cfg "{{.Config.User}}")" = 1001 ]' +need "podman run --rm hello:1.0 works" 'apodman run --rm "$H"' + +task 3 "hello:1.0 rebuilt with a changed app.py" +need "~/img/app.py has changed" '[ -f /home/alice/img/app.py ] && [ "$(cat /home/alice/img/app.py)" != "$orig" ]' +need "hello:1.0 has the changed app.py" 'apodman run --rm --entrypoint cat "$H" /opt/app.py | cmp -s - /home/alice/img/app.py' + +task 4 "latest tag removed, dangling images pruned" +need "hello:1.0 is still there" 'apodman image exists "$H"' +need "there is no hello:latest" '! apodman image exists localhost/hello:latest' +need "there are no dangling images" '[ -z "$(apodman images -q -f dangling=true)" ]' diff --git a/labs/linux-plus/2-6-1-runtimes-and-images/lab.conf b/labs/linux-plus/2-6-1-runtimes-and-images/lab.conf new file mode 100644 index 0000000..b662339 --- /dev/null +++ b/labs/linux-plus/2-6-1-runtimes-and-images/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.6.1 Container runtimes and images" +CHAPTER=/linux-plus/2-services-and-user-management/2-6-1-runtimes-and-images/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-6-1-runtimes-and-images/reset.sh b/labs/linux-plus/2-6-1-runtimes-and-images/reset.sh new file mode 100644 index 0000000..9bc3a9d --- /dev/null +++ b/labs/linux-plus/2-6-1-runtimes-and-images/reset.sh @@ -0,0 +1,11 @@ +# alice's containers, pods, volumes and networks go with her account; stop +# them cleanly first, then her services. +if id alice >/dev/null 2>&1; then + uid=$(id -u alice) + (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$uid sh -c \ + 'podman pod rm -f -a; podman rm -f -a -t 0; podman volume prune -f; podman network prune -f' >/dev/null 2>&1) + loginctl disable-linger alice 2>/dev/null + loginctl terminate-user alice 2>/dev/null + systemctl stop "user@$uid.service" 2>/dev/null +fi +true diff --git a/labs/linux-plus/2-6-1-runtimes-and-images/setup.sh b/labs/linux-plus/2-6-1-runtimes-and-images/setup.sh new file mode 100644 index 0000000..a8ab647 --- /dev/null +++ b/labs/linux-plus/2-6-1-runtimes-and-images/setup.sh @@ -0,0 +1,5 @@ +pkg_installed podman || pkg_install podman +user_add alice +loginctl enable-linger alice +uid=$(id -u alice) +for i in $(seq 30); do [ -S /run/user/$uid/bus ] && break; sleep 1; done diff --git a/labs/linux-plus/2-6-2-running-containers/README.md b/labs/linux-plus/2-6-2-running-containers/README.md new file mode 100644 index 0000000..26f89f4 --- /dev/null +++ b/labs/linux-plus/2-6-2-running-containers/README.md @@ -0,0 +1,38 @@ +# Linux+ 2.6.2: Running containers + +Running a rootless web server container, looking inside it, and seeing how +rootless containers map users to the host. + +## Before you start + +The VM needs internet access to pull images. The lab installs Podman and +creates user `alice` (password `Lab-Pass-1`), with lingering turned on so +her user services run. Do every task as alice: log in as her, or use +`sudo -iu alice` and then `export XDG_RUNTIME_DIR=/run/user/$(id -u)`. +Write answers to files in alice's `~/answers/` directory (make it first). +Docker Hub limits anonymous pulls; if you hit the limit, +`mirror.gcr.io/library/nginx:1.30` is the same image. + +## Tasks + +1. Run `nginx:1.30` in the background as a container named `web`, with + host port 8080 forwarded to its port 80 and the variable `TZ` set to + `UTC`. Check it with `podman ps`, `curl` and its logs. Leave it running. +2. Get a shell inside `web` and look around (`id`, `/etc/os-release`, + `$TZ`), and look at it with `podman top` and `podman inspect`. Write the + `NAME` from the container's `/etc/os-release` to `~/answers/2`. +3. Find the nginx main process on the host side and see which user it runs + as, then look at the user namespace's UID map and `/etc/subuid`. Write + the host UID that UID 1 inside alice's containers maps to (the start of + her subordinate range) to `~/answers/3`. + +## Hints + +- `podman run -d --name -p 8080:80 -e TZ=UTC docker.io/library/nginx:1.30`, + `curl -sI http://127.0.0.1:8080`, `podman logs`. +- `podman exec -it web sh`, `podman top web`, `podman inspect web --format + '{{.State.Status}} {{.State.Pid}} {{json .Config.Env}}'`. +- `ps -o user,pid,cmd -p PID`, `podman unshare cat /proc/self/uid_map`, + `grep alice /etc/subuid`. +- The chapter's step 4 (stop, start, remove) isn't graded: try it when + you're done. diff --git a/labs/linux-plus/2-6-2-running-containers/check.sh b/labs/linux-plus/2-6-2-running-containers/check.sh new file mode 100644 index 0000000..59fee43 --- /dev/null +++ b/labs/linux-plus/2-6-2-running-containers/check.sh @@ -0,0 +1,25 @@ +auid=$(id -u alice 2>/dev/null) +apodman() { (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$auid podman "$@"); } +aanswer() { head -n1 "/home/alice/answers/$1" 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; } +# NAME= from an nginx image of alice's. +os_name() { + local img + img=$(apodman inspect -f '{{.ImageName}}' web 2>/dev/null) || + img=$(apodman images --format '{{.Repository}}:{{.Tag}}' | grep -m1 nginx) + apodman run --rm --entrypoint sh "$img" -c '. /etc/os-release; echo "$NAME"' +} +subuid_start() { awk -F: -v u=alice '$1 == u {print $2; exit}' /etc/subuid; } +web_up() { for i in $(seq 10); do [ "$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://127.0.0.1:8080/)" = 200 ] && return 0; sleep 1; done; return 1; } + +task 1 "web runs nginx:1.30 on port 8080 with TZ=UTC" +need "alice has a running container named web" '[ "$(apodman inspect -f "{{.State.Status}}" web)" = running ]' +need "it runs nginx 1.30" 'apodman inspect -f "{{.ImageName}}" web | grep -q "nginx:1.30"' +need "it publishes port 80 on host port 8080" 'apodman port web | grep -Eq "^80/tcp -> .*:8080$"' +need "it has TZ=UTC" 'apodman inspect -f "{{range .Config.Env}}{{println .}}{{end}}" web | grep -qx TZ=UTC' +need "http://127.0.0.1:8080 answers 200" web_up + +task 2 "The container's OS name" +need "~/answers/2 has the NAME from the container's /etc/os-release" 'a=$(aanswer 2); [ -n "$a" ] && [ "${a,,}" = "$(os_name | tr A-Z a-z)" ]' + +task 3 "Where UID 1 maps on the host" +need "~/answers/3 has the start of alice's subordinate UID range" '[ -n "$(subuid_start)" ] && [ "$(aanswer 3)" = "$(subuid_start)" ]' diff --git a/labs/linux-plus/2-6-2-running-containers/lab.conf b/labs/linux-plus/2-6-2-running-containers/lab.conf new file mode 100644 index 0000000..f96e2f6 --- /dev/null +++ b/labs/linux-plus/2-6-2-running-containers/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.6.2 Running containers" +CHAPTER=/linux-plus/2-services-and-user-management/2-6-2-running-containers/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-6-2-running-containers/reset.sh b/labs/linux-plus/2-6-2-running-containers/reset.sh new file mode 100644 index 0000000..9bc3a9d --- /dev/null +++ b/labs/linux-plus/2-6-2-running-containers/reset.sh @@ -0,0 +1,11 @@ +# alice's containers, pods, volumes and networks go with her account; stop +# them cleanly first, then her services. +if id alice >/dev/null 2>&1; then + uid=$(id -u alice) + (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$uid sh -c \ + 'podman pod rm -f -a; podman rm -f -a -t 0; podman volume prune -f; podman network prune -f' >/dev/null 2>&1) + loginctl disable-linger alice 2>/dev/null + loginctl terminate-user alice 2>/dev/null + systemctl stop "user@$uid.service" 2>/dev/null +fi +true diff --git a/labs/linux-plus/2-6-2-running-containers/setup.sh b/labs/linux-plus/2-6-2-running-containers/setup.sh new file mode 100644 index 0000000..a8ab647 --- /dev/null +++ b/labs/linux-plus/2-6-2-running-containers/setup.sh @@ -0,0 +1,5 @@ +pkg_installed podman || pkg_install podman +user_add alice +loginctl enable-linger alice +uid=$(id -u alice) +for i in $(seq 30); do [ -S /run/user/$uid/bus ] && break; sleep 1; done diff --git a/labs/linux-plus/2-6-3-container-storage-and-networks/README.md b/labs/linux-plus/2-6-3-container-storage-and-networks/README.md new file mode 100644 index 0000000..c50ea79 --- /dev/null +++ b/labs/linux-plus/2-6-3-container-storage-and-networks/README.md @@ -0,0 +1,35 @@ +# Linux+ 2.6.3: Container storage and networks + +A bind mount that SELinux allows, a named volume that outlives its +containers, and name resolution on a user-defined network, all rootless. + +## Before you start + +The VM needs internet access to pull images. The lab installs Podman and +creates user `alice` (password `Lab-Pass-1`), with lingering turned on so +her user services run. Do every task as alice: log in as her, or use +`sudo -iu alice` and then `export XDG_RUNTIME_DIR=/run/user/$(id -u)`. +Docker Hub limits anonymous pulls; if you hit the limit, use +`mirror.gcr.io/library/...` instead of `docker.io/library/...`. Leave the +containers, the volume and the network in place for grading. + +## Tasks + +1. Put an `index.html` in `~/site`, and run `nginx:1.30` as a container + named `site` on host port 8080 that serves `~/site` read-only from + `/usr/share/nginx/html`. On RHEL relatives, keep SELinux enforcing: run + it once without a label option and see the 403, then fix it. +2. Create the volume `labdata`. Write the date to `first.txt` in it from + one `alpine:3.24` container, and read it back from another. +3. Create the network `labnet` and run `nginx:1.30` on it as a container + named `back`. From another container on `labnet`, fetch + `http://back` by name. + +## Hints + +- `podman run -d --name site -p 8080:80 -v ~/site:/usr/share/nginx/html:ro`, + then `:ro,Z`; `curl -s http://127.0.0.1:8080`, `ls -Zd ~/site`. +- `podman volume create`, `podman run --rm -v labdata:/data + docker.io/library/alpine:3.24 sh -c 'date > /data/first.txt'`. +- `podman network create`, `podman run --network labnet`, `wget -qO- + http://back` from an alpine container. diff --git a/labs/linux-plus/2-6-3-container-storage-and-networks/check.sh b/labs/linux-plus/2-6-3-container-storage-and-networks/check.sh new file mode 100644 index 0000000..8d45696 --- /dev/null +++ b/labs/linux-plus/2-6-3-container-storage-and-networks/check.sh @@ -0,0 +1,27 @@ +auid=$(id -u alice 2>/dev/null) +apodman() { (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$auid podman "$@"); } +site_up() { + for i in $(seq 10); do curl -s --max-time 3 http://127.0.0.1:8080/ | cmp -s - /home/alice/site/index.html && return 0; sleep 1; done + return 1 +} +vol_file() { cat "$(apodman volume inspect -f '{{.Mountpoint}}' labdata)/first.txt"; } +alpine_img() { apodman images --format '{{.Repository}}:{{.Tag}}' | grep -m1 '/alpine:'; } + +task 1 "site serves ~/site read-only on port 8080" +need "~/site/index.html exists" '[ -s /home/alice/site/index.html ]' +need "alice has a running container named site" '[ "$(apodman inspect -f "{{.State.Status}}" site)" = running ]' +need "it mounts ~/site read-only" 'apodman inspect -f "{{range .Mounts}}{{println .Source .Destination .RW}}{{end}}" site | grep -qx "/home/alice/site /usr/share/nginx/html false"' +need "it publishes port 80 on host port 8080" 'apodman port site | grep -Eq "^80/tcp -> .*:8080$"' +need "SELinux is enforcing" '! is_rhel || [ "$(getenforce)" = Enforcing ]' +need "~/site has a container label" '! is_rhel || ls -Zd /home/alice/site | grep -q container_file_t' +need "http://127.0.0.1:8080 returns ~/site/index.html" site_up + +task 2 "Volume labdata holds first.txt" +need "volume labdata exists" 'apodman volume exists labdata' +need "first.txt in it has a date in it" 'vol_file | grep -Eq "[0-9]{2}:[0-9]{2}:[0-9]{2}"' + +task 3 "back answers by name on labnet" +need "network labnet exists" 'apodman network exists labnet' +need "labnet has DNS turned on" '[ "$(apodman network inspect -f "{{.DNSEnabled}}" labnet)" = true ]' +need "back is running on labnet" '[ "$(apodman inspect -f "{{.State.Status}}" back)" = running ] && apodman inspect -f "{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}" back | grep -qw labnet' +need "another container on labnet fetches http://back" '[ -n "$(alpine_img)" ] && apodman run --rm --network labnet "$(alpine_img)" wget -qO- -T 5 http://back | grep -qi nginx' diff --git a/labs/linux-plus/2-6-3-container-storage-and-networks/lab.conf b/labs/linux-plus/2-6-3-container-storage-and-networks/lab.conf new file mode 100644 index 0000000..3369415 --- /dev/null +++ b/labs/linux-plus/2-6-3-container-storage-and-networks/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.6.3 Container storage and networks" +CHAPTER=/linux-plus/2-services-and-user-management/2-6-3-container-storage-and-networks/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/2-6-3-container-storage-and-networks/reset.sh b/labs/linux-plus/2-6-3-container-storage-and-networks/reset.sh new file mode 100644 index 0000000..9bc3a9d --- /dev/null +++ b/labs/linux-plus/2-6-3-container-storage-and-networks/reset.sh @@ -0,0 +1,11 @@ +# alice's containers, pods, volumes and networks go with her account; stop +# them cleanly first, then her services. +if id alice >/dev/null 2>&1; then + uid=$(id -u alice) + (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$uid sh -c \ + 'podman pod rm -f -a; podman rm -f -a -t 0; podman volume prune -f; podman network prune -f' >/dev/null 2>&1) + loginctl disable-linger alice 2>/dev/null + loginctl terminate-user alice 2>/dev/null + systemctl stop "user@$uid.service" 2>/dev/null +fi +true diff --git a/labs/linux-plus/2-6-3-container-storage-and-networks/setup.sh b/labs/linux-plus/2-6-3-container-storage-and-networks/setup.sh new file mode 100644 index 0000000..a8ab647 --- /dev/null +++ b/labs/linux-plus/2-6-3-container-storage-and-networks/setup.sh @@ -0,0 +1,5 @@ +pkg_installed podman || pkg_install podman +user_add alice +loginctl enable-linger alice +uid=$(id -u alice) +for i in $(seq 30); do [ -S /run/user/$uid/bus ] && break; sleep 1; done diff --git a/labs/linux-plus/3-1-1-pam-polkit-directory-services/README.md b/labs/linux-plus/3-1-1-pam-polkit-directory-services/README.md new file mode 100644 index 0000000..d9991ab --- /dev/null +++ b/labs/linux-plus/3-1-1-pam-polkit-directory-services/README.md @@ -0,0 +1,35 @@ +# Linux+ 3.1.1: PAM, polkit and directory services + +Reading a PAM stack, blocking logins with `pam_nologin`, and looking at a +polkit action. + +## Before you start + +The lab creates user `pat` (password `Lab-Pass-1`), a normal account that +can log in over SSH. Write answers to the files in `/root/lab-answers/`. +Task 2 blocks new logins for everyone but root for a while: keep a root +shell open (`sudo -i`) until you've removed the block. + +## Tasks + +1. Read sshd's PAM stack in `/etc/pam.d/sshd`. Its `auth` lines hand over + to a shared file of the system (with `substack` on the RHEL family, + `@include` on Ubuntu). Write that file's name to `/root/lab-answers/1`. + On the RHEL family, also see what `authselect current` says. +2. Block logins for everyone but root with `pam_nologin`, with the message + `Maintenance until 11:00`. Try `ssh pat@localhost` and save what it + prints to `/root/lab-answers/2`. Then remove the block, so that pat can + log in again. +3. Look at the polkit actions with `pkaction`. Find the authorization + that `org.freedesktop.systemd1.manage-units` needs from a user in an + active local session, and write it to `/root/lab-answers/3`. If you + like, try `pkexec id` too (on Ubuntu, install `pkexec` first). +4. If you have an AD or FreeIPA test domain, run `realm discover` and + `realm join` against it, then `id` a domain user. If not, read + `man sssd.conf` and `man realm`. Not graded. + +## Hints + +- `cat /etc/pam.d/sshd`; `authselect current`. +- `/etc/nologin`; `ssh pat@localhost 2>&1 | tee /root/lab-answers/2`. +- `pkaction --verbose --action-id ...`, the `implicit active:` line. diff --git a/labs/linux-plus/3-1-1-pam-polkit-directory-services/check.sh b/labs/linux-plus/3-1-1-pam-polkit-directory-services/check.sh new file mode 100644 index 0000000..52a6d9b --- /dev/null +++ b/labs/linux-plus/3-1-1-pam-polkit-directory-services/check.sh @@ -0,0 +1,11 @@ +task 1 "The shared file sshd's PAM auth lines use" +need "/root/lab-answers/1 names it" '[ "$(answer 1 | sed "s|.*/||")" = "$(pick password-auth common-auth)" ]' + +task 2 "pam_nologin tried with the maintenance message, then removed" +need "/root/lab-answers/2 has the message ssh showed" 'grep -q "Maintenance until 11:00" /root/lab-answers/2' +need "/etc/nologin is gone, so users can log in again" '[ ! -e /etc/nologin ] && [ ! -e /run/nologin ]' + +task 3 "The authorization manage-units needs in an active session" +need "/root/lab-answers/3 has it" '[ -n "$(answer 3)" ] && [ "$(answer 3)" = "$(pkaction --verbose --action-id org.freedesktop.systemd1.manage-units | awk "/implicit active:/ {print \$3}")" ]' + +task 4 "Join a directory domain, or read about it" self diff --git a/labs/linux-plus/3-1-1-pam-polkit-directory-services/lab.conf b/labs/linux-plus/3-1-1-pam-polkit-directory-services/lab.conf new file mode 100644 index 0000000..6659f15 --- /dev/null +++ b/labs/linux-plus/3-1-1-pam-polkit-directory-services/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.1.1 PAM, polkit and directory services" +CHAPTER=/linux-plus/3-security/3-1-1-pam-polkit-directory-services/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-1-1-pam-polkit-directory-services/reset.sh b/labs/linux-plus/3-1-1-pam-polkit-directory-services/reset.sh new file mode 100644 index 0000000..f7fe3a9 --- /dev/null +++ b/labs/linux-plus/3-1-1-pam-polkit-directory-services/reset.sh @@ -0,0 +1,2 @@ +rm -f /etc/nologin +true diff --git a/labs/linux-plus/3-1-1-pam-polkit-directory-services/setup.sh b/labs/linux-plus/3-1-1-pam-polkit-directory-services/setup.sh new file mode 100644 index 0000000..f1eb605 --- /dev/null +++ b/labs/linux-plus/3-1-1-pam-polkit-directory-services/setup.sh @@ -0,0 +1,7 @@ +user_add pat +[ ! -e /etc/nologin ] || refuse "/etc/nologin exists, so only root can log in; remove it and start again" +# pat logs in with a password, so sshd must allow them. +mkdir -p /run/sshd +sshd -T 2>/dev/null | grep -qx 'passwordauthentication yes' || + refuse "sshd doesn't allow password logins, which task 2 uses; allow them and start again" +true diff --git a/labs/linux-plus/3-1-2-logging-and-auditing/README.md b/labs/linux-plus/3-1-2-logging-and-auditing/README.md new file mode 100644 index 0000000..aaa5830 --- /dev/null +++ b/labs/linux-plus/3-1-2-logging-and-auditing/README.md @@ -0,0 +1,36 @@ +# Linux+ 3.1.2: Logging and auditing + +A persistent journal, a tagged log message, a logrotate rule, and an audit +watch on a file. + +## Before you start + +The journal on this VM is kept in memory only, so it's lost at every +reboot (the lab moved any saved journal aside; reset puts it back). The lab +creates `/var/log/labapp/app.log`, a log file of a made-up application. +auditd runs on the RHEL family; on Ubuntu it isn't installed yet. + +## Tasks + +1. Make the journal persistent, so that it keeps messages across reboots. + Check its size and the list of boots. +2. After task 1, log the message `hello journal` with the tag `labtest`, and + find it in the journal by its tag. +3. Write a logrotate rule, `/etc/logrotate.d/labapp`, for + `/var/log/labapp/*.log`: rotate daily, keep 3 old logs, compress them, + and don't complain if a log is missing. Test it with a dry run, then + force a rotation. +4. Watch `/etc/hosts` for writes and attribute changes with the audit key + `hosts-change` (install `auditd` on Ubuntu first; on AlmaLinux 10, + `auditctl` is in the `audit-rules` package). Touch the file, add a + line to it, and find the events by their key. Then take the line out + again and remove the watch. + +## Hints + +- `/var/log/journal`, `systemctl restart systemd-journald`; until the next + boot, journald writes there only after `journalctl --flush`. + `journalctl --disk-usage`, `journalctl --list-boots`. +- `logger -t`, `journalctl -t`. +- `logrotate -d` (dry run), `logrotate -f` (force). +- `auditctl -w FILE -p wa -k KEY`, `ausearch -k KEY -i`, `auditctl -W`. diff --git a/labs/linux-plus/3-1-2-logging-and-auditing/check.sh b/labs/linux-plus/3-1-2-logging-and-auditing/check.sh new file mode 100644 index 0000000..1f7af95 --- /dev/null +++ b/labs/linux-plus/3-1-2-logging-and-auditing/check.sh @@ -0,0 +1,26 @@ +START=$(state_get start) +# events KEY: audit events with this key logged since the lab started. +events() { + ausearch --input-logs -k "$1" --raw 2>/dev/null | grep '^type=SYSCALL' | grep -v 'comm="auditctl"' | grep -o 'msg=audit([0-9]*' | + cut -d'(' -f2 | awk -v s="$START" '$1 > s {n++} END {exit !n}' +} + +task 1 "The journal is persistent" +need "/var/log/journal exists" '[ -d /var/log/journal ]' +need "journald writes its system journal there" 'find /var/log/journal -name "system*.journal" | grep -q .' +need "journald isn't set to keep it in memory" '! grep -rhsEi "^[[:space:]]*Storage[[:space:]]*=[[:space:]]*(volatile|none)" /etc/systemd/journald.conf /etc/systemd/journald.conf.d' + +task 2 "hello journal, tagged labtest, in the journal" +need "journalctl -t labtest shows hello journal" 'journalctl -q --no-pager -t labtest --since "@$START" | grep -q "hello journal"' + +task 3 "logrotate rule for /var/log/labapp, tested and forced" +f=/etc/logrotate.d/labapp +need "$f exists for /var/log/labapp/*.log" 'grep -Eq "^[[:space:]]*/var/log/labapp/\*\.log" '$f +need "it rotates daily, keeps 3, compresses and has missingok" 'for w in "daily" "rotate[[:space:]]+3" "compress" "missingok"; do grep -Eq "^[[:space:]]*$w[[:space:]]*$" '$f' || exit 1; done' +need "logrotate -d accepts it" "logrotate -d $f" +need "a forced rotation left app.log.1.gz" '[ -f /var/log/labapp/app.log.1.gz ]' + +task 4 "Audit watch on /etc/hosts tried, then removed" +need "ausearch -k hosts-change finds events since the lab started" 'events hosts-change' +need "the watch is removed again" '! auditctl -l 2>/dev/null | grep -q hosts-change' +need "the test line is out of /etc/hosts" '! grep -qx "# test" /etc/hosts' diff --git a/labs/linux-plus/3-1-2-logging-and-auditing/lab.conf b/labs/linux-plus/3-1-2-logging-and-auditing/lab.conf new file mode 100644 index 0000000..92b0ef2 --- /dev/null +++ b/labs/linux-plus/3-1-2-logging-and-auditing/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.1.2 Logging and auditing" +CHAPTER=/linux-plus/3-security/3-1-2-logging-and-auditing/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-1-2-logging-and-auditing/reset.sh b/labs/linux-plus/3-1-2-logging-and-auditing/reset.sh new file mode 100644 index 0000000..060707e --- /dev/null +++ b/labs/linux-plus/3-1-2-logging-and-auditing/reset.sh @@ -0,0 +1,11 @@ +command -v auditctl >/dev/null && auditctl -W /etc/hosts -p wa -k hosts-change >/dev/null 2>&1 +sed -i '/^# test$/d' /etc/hosts +rm -rf /var/log/labapp /etc/logrotate.d/labapp +# The journal as it was. +journalctl --relinquish-var 2>/dev/null +rm -rf /var/log/journal +if [ -d "$LAB_STATE/journal.saved" ]; then + mv "$LAB_STATE/journal.saved" /var/log/journal + journalctl --flush +fi +true diff --git a/labs/linux-plus/3-1-2-logging-and-auditing/setup.sh b/labs/linux-plus/3-1-2-logging-and-auditing/setup.sh new file mode 100644 index 0000000..440c8a2 --- /dev/null +++ b/labs/linux-plus/3-1-2-logging-and-auditing/setup.sh @@ -0,0 +1,13 @@ +state_set start "$(date +%s)" +# The journal in memory only: move a saved journal aside. +if [ -d /var/log/journal ]; then + journalctl --relinquish-var + mv /var/log/journal "$LAB_STATE/journal.saved" + journalctl --flush +fi +# The application's log. +if [ ! -d /var/log/labapp ]; then + mkdir -p /var/log/labapp + for i in 1 2 3; do echo "$(date '+%F %T') labapp: request $i served"; done >/var/log/labapp/app.log +fi +rm -f /etc/logrotate.d/labapp diff --git a/labs/linux-plus/3-2-1-firewalld-and-ufw/README.md b/labs/linux-plus/3-2-1-firewalld-and-ufw/README.md new file mode 100644 index 0000000..71828b9 --- /dev/null +++ b/labs/linux-plus/3-2-1-firewalld-and-ufw/README.md @@ -0,0 +1,38 @@ +# Linux+ 3.2.1: firewalld and ufw + +Runtime and permanent firewalld rules, a rich rule for one source address, +and ufw. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the other VM. It sits +on this VM's second network, `10.99.0.0/24` (this VM is `10.99.0.1` there, +on interface `le-host`). It also has a second address, `10.99.0.3`, which +stands in for "anywhere else". Get a shell there with +`sudo ip netns exec lepeer bash`; from it, `curl --interface 10.99.0.3` +sends from the second address. + +On this VM, the lab runs web services on ports 8080 and 9000. On the RHEL +family it installs and starts firewalld, allowing SSH. Keep a console open: +a firewall mistake can cut off SSH. + +## Tasks + +1. RHEL family: allow HTTP in firewalld at runtime, reload and see it gone, + then allow it permanently, in the zone of the main interface. +2. RHEL family: with a rich rule, allow port 8080/tcp only from the peer's + address `10.99.0.2`, persistently. From the peer, `curl + http://10.99.0.1:8080/` works, and from `10.99.0.3` it fails. +3. Ubuntu: turn on ufw, allowing OpenSSH first. +4. Ubuntu: allow port 9000/tcp only from the peer's address `10.99.0.2`. + From the peer, `curl http://10.99.0.1:9000/` works, and from + `10.99.0.3` it fails. Look at the rules with `ufw status numbered`. + +## Hints + +- `firewall-cmd --add-service`, `--permanent`, `--reload`, + `--list-services`, `--get-active-zones`. +- `--add-rich-rule='rule family="ipv4" source address="..." port + port="8080" protocol="tcp" accept'`, `--list-rich-rules`. +- `ufw allow OpenSSH`, `ufw enable`, `ufw status verbose`. +- `ufw allow from ADDRESS to any port PORT proto tcp`, `ufw delete N`. diff --git a/labs/linux-plus/3-2-1-firewalld-and-ufw/check.sh b/labs/linux-plus/3-2-1-firewalld-and-ufw/check.sh new file mode 100644 index 0000000..a444334 --- /dev/null +++ b/labs/linux-plus/3-2-1-firewalld-and-ufw/check.sh @@ -0,0 +1,32 @@ +IF=$(state_get if) +# The peer's second address goes at every reboot. +ip -n lepeer addr add 10.99.0.3/24 dev le-peer 2>/dev/null +# from ADDRESS PORT: curl's exit code from the peer's ADDRESS (0 when it works). +from() { in_peer curl -s -o /dev/null --max-time 4 --interface "$1" "http://10.99.0.1:$2/"; echo $?; } +if is_rhel && systemctl is-active --quiet firewalld; then + Z=$(firewall-cmd --get-zone-of-interface="$IF" 2>/dev/null || firewall-cmd --get-default-zone) +fi +fw() { firewall-cmd "$@" --zone="$Z"; } + +task 1 "firewalld allows HTTP permanently" rhel +need "firewalld is running and enabled" 'active firewalld && enabled firewalld' +need "http is allowed in the permanent configuration" 'fw --permanent --query-service=http' +need "http is allowed now" 'fw --query-service=http' + +task 2 "8080/tcp allowed only from 10.99.0.2, with a rich rule" rhel +need "a rich rule allows 8080/tcp from 10.99.0.2, now and permanently" 'for p in "" --permanent; do fw $p --list-rich-rules | grep -E "10\.99\.0\.2(/32)?\"" | grep "8080" | grep -q accept || exit 1; done' +need "8080/tcp isn't open to everyone" '! fw --query-port=8080/tcp && ! fw --permanent --query-port=8080/tcp' +need "SSH is still allowed" 'fw --query-service=ssh || fw --query-port=22/tcp' +need "port 8080 answers 10.99.0.2" '[ "$(from 10.99.0.2 8080)" = 0 ]' +need "port 8080 is blocked from 10.99.0.3" '[ "$(from 10.99.0.3 8080)" != 0 ]' + +task 3 "ufw on, allowing OpenSSH" debian +need "ufw is active" 'ufw status | grep -q "Status: active"' +need "ufw allows SSH" 'ufw status | grep -Eq "^(22(/tcp)?|OpenSSH)[[:space:]].*ALLOW"' +need "incoming traffic is denied by default" 'ufw status verbose | grep -Eq "Default: (deny|reject) \(incoming\)"' + +task 4 "9000/tcp allowed only from 10.99.0.2" debian +need "a ufw rule allows 9000/tcp from 10.99.0.2" 'ufw status | grep -E "^9000(/tcp)?[[:space:]].*ALLOW.*10\.99\.0\.2([[:space:]]|$)"' +need "ufw doesn't allow 9000 from anywhere" '! ufw status | grep -E "^9000(/tcp)?[[:space:]].*ALLOW.*Anywhere"' +need "port 9000 answers 10.99.0.2" '[ "$(from 10.99.0.2 9000)" = 0 ]' +need "port 9000 is blocked from 10.99.0.3" '[ "$(from 10.99.0.3 9000)" != 0 ]' diff --git a/labs/linux-plus/3-2-1-firewalld-and-ufw/lab.conf b/labs/linux-plus/3-2-1-firewalld-and-ufw/lab.conf new file mode 100644 index 0000000..f848940 --- /dev/null +++ b/labs/linux-plus/3-2-1-firewalld-and-ufw/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.2.1 firewalld and ufw" +CHAPTER=/linux-plus/3-security/3-2-1-firewalld-and-ufw/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-2-1-firewalld-and-ufw/reset.sh b/labs/linux-plus/3-2-1-firewalld-and-ufw/reset.sh new file mode 100644 index 0000000..b590bb2 --- /dev/null +++ b/labs/linux-plus/3-2-1-firewalld-and-ufw/reset.sh @@ -0,0 +1,28 @@ +for u in le-lab-app8080 le-lab-app9000; do + systemctl disable --now --quiet "$u.service" 2>/dev/null + rm -f "/etc/systemd/system/$u.service" +done +systemctl daemon-reload +rm -rf /srv/le-app /usr/local/lib/le-lab/httpd.py +rmdir /usr/local/lib/le-lab 2>/dev/null + +if is_rhel; then + if [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" + command -v restorecon >/dev/null && restorecon -R /etc/firewalld + fi + if [ -n "$(state_get firewalld-was-active)" ]; then + firewall-cmd -q --reload + else + systemctl disable --now --quiet firewalld + fi +else + [ -n "$(state_get ufw-was-active)" ] || ufw --force disable >/dev/null + if [ -f "$LAB_STATE/ufw.tar" ]; then + rm -rf /etc/ufw + tar -C /etc -xf "$LAB_STATE/ufw.tar" + fi + [ -n "$(state_get ufw-was-active)" ] && ufw reload >/dev/null +fi +true diff --git a/labs/linux-plus/3-2-1-firewalld-and-ufw/setup.sh b/labs/linux-plus/3-2-1-firewalld-and-ufw/setup.sh new file mode 100644 index 0000000..30c235f --- /dev/null +++ b/labs/linux-plus/3-2-1-firewalld-and-ufw/setup.sh @@ -0,0 +1,65 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +[ -n "$IF" ] || refuse "no default route; this lab needs a working network" +state_set if "$IF" +peer_up +ip -n lepeer addr add 10.99.0.3/24 dev le-peer 2>/dev/null || true + +if is_rhel; then + pkg_install firewalld nftables + pkg_installed firewalld && systemctl is-active --quiet firewalld && state_set firewalld-was-active 1 + tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld + systemctl enable --now --quiet firewalld + firewall-cmd -q --state + z=$(firewall-cmd --get-zone-of-interface="$IF" 2>/dev/null || firewall-cmd --get-default-zone) + # The starting point: SSH allowed, HTTP not. + firewall-cmd -q --permanent --zone="$z" --query-service=ssh || firewall-cmd -q --permanent --zone="$z" --add-service=ssh + firewall-cmd -q --permanent --zone="$z" --query-service=http && firewall-cmd -q --permanent --zone="$z" --remove-service=http + firewall-cmd -q --reload +else + ufw status | grep -q 'Status: active' && state_set ufw-was-active 1 + tar -C /etc -cf "$LAB_STATE/ufw.tar" ufw default/ufw + if [ -n "$(state_get ufw-was-active)" ]; then + ufw --force reset >/dev/null + ufw --force disable >/dev/null + fi +fi + +# Web services on 8080 and 9000. +mkdir -p /srv/le-app /usr/local/lib/le-lab +echo 'Lab app' >/srv/le-app/index.html +cat >/usr/local/lib/le-lab/httpd.py <<'Y' +# A static web server like "python3 -m http.server PORT --directory DIR", +# without its reverse DNS lookup of the address, which can take seconds. +import argparse, functools, http.server, socketserver + +ap = argparse.ArgumentParser() +ap.add_argument("port", type=int) +ap.add_argument("--bind", default="") +ap.add_argument("--directory", default=".") +a = ap.parse_args() + + +class Server(http.server.ThreadingHTTPServer): + def server_bind(self): + socketserver.TCPServer.server_bind(self) + self.server_name, self.server_port = self.server_address[:2] + + +handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=a.directory) +Server((a.bind, a.port), handler).serve_forever() +Y +for p in 8080 9000; do + cat >/etc/systemd/system/le-lab-app$p.service </dev/null) +# drops: packets counted by the rule that drops @blocked. +drops() { grep -E 'ip saddr @blocked .*counter packets [0-9]+ .*drop' <<<"$T" | grep -oE 'packets [0-9]+' | awk '{print $2}'; } + +task 1 "Table inet lab with set blocked and chain input loaded" +need "table inet lab is loaded" '[ -n "$T" ]' +need "it has a set blocked of type ipv4_addr" 'grep -A2 "set blocked" <<<"$T" | grep -q "type ipv4_addr"' +need "chain input hooks input at priority 0 with policy accept" 'grep -Eq "type filter hook input priority (0|filter); policy accept;" <<<"$T"' +need "a rule counts and drops packets from @blocked" 'grep -Eq "ip saddr @blocked counter packets [0-9]+ bytes [0-9]+ drop" <<<"$T"' +need "a rule counts packets to TCP port 8080" 'grep -Eq "tcp dport 8080 counter packets" <<<"$T"' + +task 2 "The peer blocked, and the drops counted" +need "10.99.0.2 is in the set blocked" 'nft list set inet lab blocked 2>/dev/null | grep -q "10\.99\.0\.2"' +need "the peer's ping gets no reply" '! in_peer ping -c1 -W2 10.99.0.1' +need "the drop rule has counted packets" '[ "$(drops)" -gt 0 ]' diff --git a/labs/linux-plus/3-2-2-netfilter-nftables-iptables/lab.conf b/labs/linux-plus/3-2-2-netfilter-nftables-iptables/lab.conf new file mode 100644 index 0000000..70151c6 --- /dev/null +++ b/labs/linux-plus/3-2-2-netfilter-nftables-iptables/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.2.2 Netfilter, nftables and iptables" +CHAPTER=/linux-plus/3-security/3-2-2-netfilter-nftables-iptables/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-2-2-netfilter-nftables-iptables/reset.sh b/labs/linux-plus/3-2-2-netfilter-nftables-iptables/reset.sh new file mode 100644 index 0000000..34a41f8 --- /dev/null +++ b/labs/linux-plus/3-2-2-netfilter-nftables-iptables/reset.sh @@ -0,0 +1,3 @@ +nft delete table inet lab 2>/dev/null +rm -f /tmp/lab.nft +true diff --git a/labs/linux-plus/3-2-2-netfilter-nftables-iptables/setup.sh b/labs/linux-plus/3-2-2-netfilter-nftables-iptables/setup.sh new file mode 100644 index 0000000..7d25933 --- /dev/null +++ b/labs/linux-plus/3-2-2-netfilter-nftables-iptables/setup.sh @@ -0,0 +1,5 @@ +command -v nft >/dev/null || pkg_install nftables +nft list table inet lab >/dev/null 2>&1 && nft delete table inet lab +rm -f /tmp/lab.nft +peer_up +true diff --git a/labs/linux-plus/3-2-3-nat-and-forwarding/README.md b/labs/linux-plus/3-2-3-nat-and-forwarding/README.md new file mode 100644 index 0000000..50b82e8 --- /dev/null +++ b/labs/linux-plus/3-2-3-nat-and-forwarding/README.md @@ -0,0 +1,37 @@ +# Linux+ 3.2.3: NAT and IP forwarding + +Turning this VM into a router for a private network with firewalld zones, +a policy and masquerading. + +## Before you start + +RHEL family only. The peer, at `10.99.0.2`, stands in for the Ubuntu VM. +It sits on this VM's second network, `10.99.0.0/24` (this VM is +`10.99.0.1` there, on interface `le-host`), and its default route already +points at this VM, but it has no way out yet: from the peer, +`ping -c 2 9.9.9.9` fails. Get a shell there with +`sudo ip netns exec lepeer bash`. + +The lab installs and starts firewalld, allowing SSH. The interface +`le-host` isn't managed by NetworkManager, so set its zone with +`firewall-cmd` rather than `nmcli`. Keep a console open: a firewall mistake +can cut off SSH. + +## Tasks + +1. Put `le-host` in firewalld's `internal` zone, persistently. +2. Create a firewalld policy `int-to-pub` that accepts traffic from zone + `internal` to zone `public`, and turn on masquerading in the zone of + the main interface, both persistently. Check `net.ipv4.ip_forward`. +3. From the peer, `ping -c 2 9.9.9.9` now works. Watch the translation on + this VM with `tcpdump -ni any icmp` while it runs. + +## Hints + +- `firewall-cmd --permanent --zone=internal --change-interface=le-host`, + `--get-active-zones`. +- `firewall-cmd --permanent --new-policy NAME`, then `--policy NAME` with + `--add-ingress-zone`, `--add-egress-zone` and `--set-target=ACCEPT` + (firewalld 2.4 takes one of these per command); `--info-policy NAME`. +- `--zone=public --add-masquerade`, with `--permanent` too; + `--reload`. diff --git a/labs/linux-plus/3-2-3-nat-and-forwarding/check.sh b/labs/linux-plus/3-2-3-nat-and-forwarding/check.sh new file mode 100644 index 0000000..1f29984 --- /dev/null +++ b/labs/linux-plus/3-2-3-nat-and-forwarding/check.sh @@ -0,0 +1,20 @@ +IF=$(state_get if) +Z=$(firewall-cmd --get-zone-of-interface="$IF" 2>/dev/null) + +task 1 "le-host in zone internal, persistently" +need "firewalld is running and enabled" 'active firewalld && enabled firewalld' +need "le-host is in zone internal now" '[ "$(firewall-cmd --get-zone-of-interface=le-host)" = internal ]' +need "le-host is in zone internal permanently" '[ "$(firewall-cmd --permanent --get-zone-of-interface=le-host)" = internal ]' + +task 2 "Policy int-to-pub and masquerading, persistently" +for p in --permanent ""; do + w=permanently; [ -z "$p" ] && w=now + need "policy int-to-pub exists ($w)" "firewall-cmd $p --info-policy int-to-pub" + need "it goes from internal to public ($w)" "firewall-cmd $p --info-policy int-to-pub | grep -Eq '^[[:space:]]*ingress-zones: (.* )?internal( |\$)' && firewall-cmd $p --info-policy int-to-pub | grep -Eq '^[[:space:]]*egress-zones: (.* )?public( |\$)'" + need "its target is ACCEPT ($w)" "firewall-cmd $p --info-policy int-to-pub | grep -Eq '^[[:space:]]*target: ACCEPT'" +done +need "masquerading is on in the main interface's zone, now and permanently" 'firewall-cmd --zone="$Z" --query-masquerade && firewall-cmd --permanent --zone="$Z" --query-masquerade' +need "IPv4 forwarding is on" '[ "$(sysctl -n net.ipv4.ip_forward)" = 1 ]' + +task 3 "The peer reaches the internet through this VM" +need "the peer can ping 9.9.9.9" 'in_peer ping -c1 -W3 9.9.9.9 || in_peer ping -c1 -W3 9.9.9.9' diff --git a/labs/linux-plus/3-2-3-nat-and-forwarding/lab.conf b/labs/linux-plus/3-2-3-nat-and-forwarding/lab.conf new file mode 100644 index 0000000..ca5f37a --- /dev/null +++ b/labs/linux-plus/3-2-3-nat-and-forwarding/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.2.3 NAT and IP forwarding" +CHAPTER=/linux-plus/3-security/3-2-3-nat-and-forwarding/ +SYSTEMS="rhel" diff --git a/labs/linux-plus/3-2-3-nat-and-forwarding/reset.sh b/labs/linux-plus/3-2-3-nat-and-forwarding/reset.sh new file mode 100644 index 0000000..acf01a2 --- /dev/null +++ b/labs/linux-plus/3-2-3-nat-and-forwarding/reset.sh @@ -0,0 +1,19 @@ +if [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" + command -v restorecon >/dev/null && restorecon -R /etc/firewalld +fi +if [ -n "$(state_get firewalld-was-active)" ]; then + firewall-cmd -q --reload +else + systemctl disable --now --quiet firewalld +fi +# Forwarding, as it was. +for f in /etc/sysctl.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/sysctl-d" 2>/dev/null && continue + grep -q ip_forward "$f" && rm -f "$f" +done +v=$(state_get ip-forward) +[ -n "$v" ] && sysctl -qw net.ipv4.ip_forward="$v" +true diff --git a/labs/linux-plus/3-2-3-nat-and-forwarding/setup.sh b/labs/linux-plus/3-2-3-nat-and-forwarding/setup.sh new file mode 100644 index 0000000..5ed149f --- /dev/null +++ b/labs/linux-plus/3-2-3-nat-and-forwarding/setup.sh @@ -0,0 +1,18 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +[ -n "$IF" ] || refuse "no default route; this lab needs a working network" +state_set if "$IF" +state_set ip-forward "$(sysctl -n net.ipv4.ip_forward)" +ls /etc/sysctl.d >"$LAB_STATE/sysctl-d" + +pkg_install firewalld nftables tcpdump +pkg_installed firewalld && systemctl is-active --quiet firewalld && state_set firewalld-was-active 1 +tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld +systemctl enable --now --quiet firewalld +firewall-cmd -q --state +z=$(firewall-cmd --get-zone-of-interface="$IF" 2>/dev/null || firewall-cmd --get-default-zone) +firewall-cmd -q --permanent --zone="$z" --query-service=ssh || firewall-cmd -q --permanent --zone="$z" --add-service=ssh +firewall-cmd -q --permanent --zone="$z" --query-masquerade && firewall-cmd -q --permanent --zone="$z" --remove-masquerade +firewall-cmd -q --permanent --info-policy int-to-pub >/dev/null 2>&1 && firewall-cmd -q --permanent --delete-policy int-to-pub +firewall-cmd -q --reload +peer_up +true diff --git a/labs/linux-plus/3-3-1-sudo-and-su/README.md b/labs/linux-plus/3-3-1-sudo-and-su/README.md new file mode 100644 index 0000000..a8c9938 --- /dev/null +++ b/labs/linux-plus/3-3-1-sudo-and-su/README.md @@ -0,0 +1,32 @@ +# Linux+ 3.3.1: sudo and su + +A sudo rule for one command, finding a refused command in the log, and +closing a shell escape with NOEXEC. + +## Before you start + +The lab makes the journal persistent if it isn't, so the log of task 2 +survives a reboot. User `sudotest` doesn't exist yet: you create it. + +## Tasks + +1. Look at your own rights (`sudo -l`) and at `/etc/sudoers` and what it + includes. Then create user `sudotest` with the password `Lab-Pass-123`, + and a file `/etc/sudoers.d/sudotest` that lets it run, as root, only + `systemctl status sshd` and `systemctl status ssh`. Give the file mode + 440 and check the sudo configuration with `visudo -c`. +2. As `sudotest`, list your rights, check the SSH service's status with + sudo, and try `sudo id`, which is refused. Find the refusal in the + journal. +3. Also let `sudotest` run `/usr/bin/less /etc/shadow` as root, keeping the + rule from task 1. Try it and type `!id` in less: you get a root shell + command. Then change the rule to use `NOEXEC`, so that the shell escape + is refused. + +## Hints + +- `useradd -m`, `chpasswd`; `ALL=(root) /usr/bin/systemctl status sshd, + ...`; `chmod 440`; `visudo -c`. +- `su - sudotest`, `sudo -l`; `journalctl _COMM=sudo --since "10 min ago"`. +- One rule can list several commands, or use one line per command; + `NOEXEC:` goes before the command it applies to. diff --git a/labs/linux-plus/3-3-1-sudo-and-su/check.sh b/labs/linux-plus/3-3-1-sudo-and-su/check.sh new file mode 100644 index 0000000..cd87502 --- /dev/null +++ b/labs/linux-plus/3-3-1-sudo-and-su/check.sh @@ -0,0 +1,18 @@ +START=$(state_get start) +F=/etc/sudoers.d/sudotest +L=$(sudo -l -U sudotest 2>/dev/null) + +task 1 "sudotest may run only systemctl status of the SSH service" +need "user sudotest exists" 'id sudotest' +need "$F has mode 440" '[ "$(stat -c %a $F)" = 440 ]' +need "visudo -c finds no errors" 'visudo -c' +need "sudotest may run systemctl status sshd or ssh" 'sudo -l -U sudotest /usr/bin/systemctl status sshd || sudo -l -U sudotest /usr/bin/systemctl status ssh' +need "sudotest may not run id, or anything else" '! sudo -l -U sudotest /usr/bin/id && ! sudo -l -U sudotest /usr/bin/systemctl restart sshd && ! grep -Eq "\(ALL( : ALL)?\) (NOPASSWD: )?ALL$" <<<"$L"' + +task 2 "The refused sudo id found in the journal" +need "the journal shows sudo refusing a command of sudotest" 'journalctl -q --no-pager _COMM=sudo --since "@$START" | grep -E "sudotest ?:" | grep -q "command not allowed"' + +task 3 "less /etc/shadow allowed with NOEXEC" +need "sudotest may run /usr/bin/less /etc/shadow" 'sudo -l -U sudotest /usr/bin/less /etc/shadow' +need "the rule for less has NOEXEC" 'grep -Eq "NOEXEC: .*/usr/bin/less /etc/shadow" <<<"$L"' +need "the rule from task 1 is still there" 'sudo -l -U sudotest /usr/bin/systemctl status sshd || sudo -l -U sudotest /usr/bin/systemctl status ssh' diff --git a/labs/linux-plus/3-3-1-sudo-and-su/lab.conf b/labs/linux-plus/3-3-1-sudo-and-su/lab.conf new file mode 100644 index 0000000..a4162f6 --- /dev/null +++ b/labs/linux-plus/3-3-1-sudo-and-su/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.3.1 sudo and su" +CHAPTER=/linux-plus/3-security/3-3-1-sudo-and-su/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-3-1-sudo-and-su/reset.sh b/labs/linux-plus/3-3-1-sudo-and-su/reset.sh new file mode 100644 index 0000000..952e3ed --- /dev/null +++ b/labs/linux-plus/3-3-1-sudo-and-su/reset.sh @@ -0,0 +1,11 @@ +rm -f /etc/sudoers.d/sudotest +if id sudotest >/dev/null 2>&1; then + pkill -KILL -u sudotest 2>/dev/null + userdel -r sudotest 2>/dev/null +fi +if [ -n "$(state_get made-journal)" ]; then + journalctl --relinquish-var 2>/dev/null + rm -rf /var/log/journal + systemctl restart systemd-journald +fi +true diff --git a/labs/linux-plus/3-3-1-sudo-and-su/setup.sh b/labs/linux-plus/3-3-1-sudo-and-su/setup.sh new file mode 100644 index 0000000..85ca17b --- /dev/null +++ b/labs/linux-plus/3-3-1-sudo-and-su/setup.sh @@ -0,0 +1,11 @@ +id sudotest >/dev/null 2>&1 && refuse "user sudotest already exists; remove it (userdel -r sudotest) and start again" +[ -e /etc/sudoers.d/sudotest ] && refuse "/etc/sudoers.d/sudotest already exists; remove it and start again" +state_set start "$(date +%s)" +# Task 2's evidence should survive a reboot. +if [ ! -d /var/log/journal ]; then + mkdir -p /var/log/journal + systemd-tmpfiles --create --prefix /var/log/journal + journalctl --flush + state_set made-journal 1 +fi +true diff --git a/labs/linux-plus/3-3-2-permissions-attributes-acls/README.md b/labs/linux-plus/3-3-2-permissions-attributes-acls/README.md new file mode 100644 index 0000000..284cacb --- /dev/null +++ b/labs/linux-plus/3-3-2-permissions-attributes-acls/README.md @@ -0,0 +1,33 @@ +# Linux+ 3.3.2: Permissions, attributes and ACLs + +A team directory with setgid and the sticky bit, an immutable file, and an +ACL that lets one more user read a private file. + +## Before you start + +The lab creates user `pat` (password `Lab-Pass-1`), who stands in for you +in the chapter's commands: where they say `$USER` or `~`, use `pat` and +`/home/pat`. Group `labteam`, `/srv/team` and user `acltest` don't exist +yet: you create them. Try the umask commands of the chapter first if you +like; they aren't graded. + +## Tasks + +1. Create group `labteam`, add `pat` to it, and make `/srv/team` a team + directory: group `labteam`, mode `3770` (setgid and sticky, no access + for others). As `pat`, create a file `/srv/team/mine`; its group should + be `labteam`, not pat's own group. +2. Create `/srv/team/locked` and make it immutable, so that even root + can't delete it. Try, and look at its attributes. Leave it immutable. +3. Create user `acltest`. As `pat`, write `secret` to `/home/pat/acl.txt` + with mode 600, and let others pass through pat's home directory + (`o+x`). `acltest` can't read the file; give it read access with an + ACL, so that it can, and nobody else can. + +## Hints + +- `groupadd`, `usermod -aG`, `chgrp`, `chmod 3770`, `ls -ld`; + `sudo -u pat touch`. +- `chattr +i`, `lsattr`. +- On Ubuntu, `apt install acl` first. `setfacl -m u:NAME:r`, `getfacl`; + `sudo -u acltest cat`. diff --git a/labs/linux-plus/3-3-2-permissions-attributes-acls/check.sh b/labs/linux-plus/3-3-2-permissions-attributes-acls/check.sh new file mode 100644 index 0000000..8e265d2 --- /dev/null +++ b/labs/linux-plus/3-3-2-permissions-attributes-acls/check.sh @@ -0,0 +1,18 @@ +task 1 "Team directory /srv/team for group labteam" +need "group labteam exists, with pat in it" 'id -nG pat | grep -qw labteam' +need "/srv/team belongs to group labteam" '[ "$(stat -c %G /srv/team)" = labteam ]' +need "/srv/team has mode 3770" '[ "$(stat -c %a /srv/team)" = 3770 ]' +need "/srv/team/mine is pat's, in group labteam" '[ "$(stat -c %U:%G /srv/team/mine)" = pat:labteam ]' + +task 2 "/srv/team/locked is immutable" +need "/srv/team/locked exists" '[ -f /srv/team/locked ]' +need "it has the immutable attribute" 'lsattr -d /srv/team/locked | cut -d" " -f1 | grep -q i' + +task 3 "acltest reads pat's acl.txt through an ACL" +F=/home/pat/acl.txt +need "$F is pat's and says secret" '[ "$(stat -c %U $F)" = pat ] && grep -qx secret $F' +need "user acltest exists" 'id acltest' +need "its owner has rw, and others have no access" '[ "$(stat -c %A $F | cut -c2,3,8-10)" = "rw---" ]' +need "getfacl shows user:acltest:r--" 'getfacl -p $F 2>/dev/null | grep -qx "user:acltest:r--"' +need "acltest can read it" 'runuser -u acltest -- cat $F | grep -qx secret' +need "another user can't" '! runuser -u nobody -- cat $F' diff --git a/labs/linux-plus/3-3-2-permissions-attributes-acls/lab.conf b/labs/linux-plus/3-3-2-permissions-attributes-acls/lab.conf new file mode 100644 index 0000000..06e314d --- /dev/null +++ b/labs/linux-plus/3-3-2-permissions-attributes-acls/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.3.2 Permissions, attributes and ACLs" +CHAPTER=/linux-plus/3-security/3-3-2-permissions-attributes-acls/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-3-2-permissions-attributes-acls/reset.sh b/labs/linux-plus/3-3-2-permissions-attributes-acls/reset.sh new file mode 100644 index 0000000..931a316 --- /dev/null +++ b/labs/linux-plus/3-3-2-permissions-attributes-acls/reset.sh @@ -0,0 +1,9 @@ +chattr -i /srv/team/locked 2>/dev/null +rm -rf /srv/team +if id acltest >/dev/null 2>&1; then + pkill -KILL -u acltest 2>/dev/null + userdel -r acltest 2>/dev/null +fi +users_remove +getent group labteam >/dev/null && groupdel labteam +true diff --git a/labs/linux-plus/3-3-2-permissions-attributes-acls/setup.sh b/labs/linux-plus/3-3-2-permissions-attributes-acls/setup.sh new file mode 100644 index 0000000..d445da9 --- /dev/null +++ b/labs/linux-plus/3-3-2-permissions-attributes-acls/setup.sh @@ -0,0 +1,5 @@ +getent group labteam >/dev/null && refuse "group labteam already exists; remove it and start again" +[ -e /srv/team ] && refuse "/srv/team already exists; remove it and start again" +id acltest >/dev/null 2>&1 && refuse "user acltest already exists; remove it and start again" +user_add pat +true diff --git a/labs/linux-plus/3-3-3-selinux/README.md b/labs/linux-plus/3-3-3-selinux/README.md new file mode 100644 index 0000000..c5126a8 --- /dev/null +++ b/labs/linux-plus/3-3-3-selinux/README.md @@ -0,0 +1,34 @@ +# Linux+ 3.3.3: SELinux + +The classic moved-file problem with a web server, reading the denial, and a +boolean. Keep SELinux enforcing throughout. + +## Before you start + +RHEL family only. SELinux must be enforcing. The web server isn't +installed or started by the lab: that's task 1. The lab sets the boolean +`httpd_enable_homedirs` off. Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Look around (`sestatus`, `ls -Zd`, `ps -eZ`, `getsebool -a`). Install + `httpd`, and enable and start it. +2. As your normal user, create `~/moved.html` with the text `moved`, and + `sudo mv` it into `/var/www/html/`. `curl + http://127.0.0.1/moved.html` returns 403. Read the denial, fix the + file's label, and see the page served. Then try `chcon -t user_home_t` + on it and see `restorecon` undo that too. +3. Write the SELinux type the moved file had before you fixed it to + `/root/lab-answers/3`. +4. Turn the boolean `httpd_enable_homedirs` on, persistently. (The + chapter turns it off again afterward; leave it on until you've checked.) + +## Hints + +- `dnf install httpd`, `systemctl enable --now`. +- `ls -Z`, `ausearch -m AVC -ts recent`; `sealert -a + /var/log/audit/audit.log` needs the `setroubleshoot-server` package; + `restorecon -v`. +- The type is the third field of the label. +- `setsebool -P`, `getsebool`. diff --git a/labs/linux-plus/3-3-3-selinux/check.sh b/labs/linux-plus/3-3-3-selinux/check.sh new file mode 100644 index 0000000..539bf1d --- /dev/null +++ b/labs/linux-plus/3-3-3-selinux/check.sh @@ -0,0 +1,19 @@ +F=/var/www/html/moved.html +type_of() { stat -c %C "$1" 2>/dev/null | cut -d: -f3; } + +task 1 "httpd installed, enabled and running" +need "httpd is running" active httpd +need "httpd is enabled" enabled httpd + +task 2 "The moved page labeled right and served" +need "$F exists and says moved" 'grep -qx moved '$F +need "it's labeled httpd_sys_content_t" '[ "$(type_of '$F')" = httpd_sys_content_t ]' +need "curl http://127.0.0.1/moved.html returns it" 'curl -s --max-time 5 http://127.0.0.1/moved.html | grep -qx moved' +need "SELinux is enforcing" '[ "$(getenforce)" = Enforcing ]' + +task 3 "The moved file's type before the fix" +need "/root/lab-answers/3 has a home directory type" 'case $(answer 3) in user_home_t | admin_home_t) exit 0 ;; *) exit 1 ;; esac' + +task 4 "httpd_enable_homedirs on, persistently" +need "httpd_enable_homedirs is on" 'getsebool httpd_enable_homedirs | grep -q -- "--> on$"' +need "it's saved in the policy, not just for this boot" 'grep -qx "httpd_enable_homedirs=1" /var/lib/selinux/targeted/active/booleans.local' diff --git a/labs/linux-plus/3-3-3-selinux/lab.conf b/labs/linux-plus/3-3-3-selinux/lab.conf new file mode 100644 index 0000000..0a2e174 --- /dev/null +++ b/labs/linux-plus/3-3-3-selinux/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.3.3 SELinux" +CHAPTER=/linux-plus/3-security/3-3-3-selinux/ +SYSTEMS="rhel" diff --git a/labs/linux-plus/3-3-3-selinux/reset.sh b/labs/linux-plus/3-3-3-selinux/reset.sh new file mode 100644 index 0000000..b225da9 --- /dev/null +++ b/labs/linux-plus/3-3-3-selinux/reset.sh @@ -0,0 +1,7 @@ +rm -f /var/www/html/moved.html +setsebool -P httpd_enable_homedirs off 2>/dev/null +if [ -z "$(state_get httpd-was-active)" ]; then + systemctl stop --quiet httpd 2>/dev/null +fi +[ -n "$(state_get httpd-was-enabled)" ] || systemctl disable --quiet httpd 2>/dev/null +true diff --git a/labs/linux-plus/3-3-3-selinux/setup.sh b/labs/linux-plus/3-3-3-selinux/setup.sh new file mode 100644 index 0000000..7059453 --- /dev/null +++ b/labs/linux-plus/3-3-3-selinux/setup.sh @@ -0,0 +1,9 @@ +[ "$(getenforce 2>/dev/null)" = Enforcing ] || refuse "SELinux isn't enforcing; set SELINUX=enforcing in /etc/selinux/config, reboot, and start again" +[ -e /var/www/html/moved.html ] && refuse "/var/www/html/moved.html already exists; remove it and start again" +systemctl is-active --quiet httpd && state_set httpd-was-active 1 +systemctl is-enabled --quiet httpd 2>/dev/null && state_set httpd-was-enabled 1 +if [ -z "$(state_get httpd-was-active)" ]; then + systemctl disable --now --quiet httpd 2>/dev/null || true +fi +setsebool -P httpd_enable_homedirs off +true diff --git a/labs/linux-plus/3-3-4-secure-remote-access/README.md b/labs/linux-plus/3-3-4-secure-remote-access/README.md new file mode 100644 index 0000000..6fb0a41 --- /dev/null +++ b/labs/linux-plus/3-3-4-secure-remote-access/README.md @@ -0,0 +1,46 @@ +# Linux+ 3.3.4: Secure remote access with SSH + +A key pair, key-only logins, and a local port forward. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the Ubuntu VM, the +client; this VM, `10.99.0.1` from the peer, is the server. Get a shell +there with `sudo ip netns exec lepeer bash`. The peer shares this VM's +files and users, so a key made there is in the same home directory. + +The lab creates user `kim` (password `Lab-Pass-1`): work as kim on both +sides, for example with `sudo ip netns exec lepeer su - kim`. It also runs +a small web service on this VM's `127.0.0.1:9090` only, standing in for +Cockpit. + +Task 2 turns off password logins on this VM: log in with a key yourself, +and keep a second session or a console open while you change sshd. + +## Tasks + +1. As kim on the peer, make an ed25519 key pair `~/.ssh/lab_ed25519` + without a passphrase, install it for kim on this VM with `ssh-copy-id` + (to `kim@10.99.0.1`), and log in with it. Look at the permissions of + `~/.ssh` and `authorized_keys`. +2. On this VM, turn off password and keyboard-interactive logins and root + logins, in a drop-in that wins over the other files in + `/etc/ssh/sshd_config.d/`. Check the configuration, reload sshd, and + check what sshd uses. +3. From the peer, a password login as kim is now refused, and the key + still works. +4. As kim on the peer, forward the peer's local port 19090 to port 9090 + on this VM's own localhost, through SSH with the key, in the + background. `curl http://localhost:19090/` on the peer returns the + page. + +## Hints + +- `ssh-keygen -t ed25519 -f ~/.ssh/lab_ed25519 -N ''`, `ssh-copy-id -i`, + `ssh -i`. +- Files in `sshd_config.d` are read in name order and the first value + wins: `ls /etc/ssh/sshd_config.d/`. `sshd -t`, `sshd -T | grep -iE + '^(passwordauth|kbdinter|permitroot)'`; `systemctl reload sshd` (`ssh` + on Ubuntu). +- `ssh -o PubkeyAuthentication=no`. +- `ssh -N -f -L 19090:localhost:9090`. diff --git a/labs/linux-plus/3-3-4-secure-remote-access/check.sh b/labs/linux-plus/3-3-4-secure-remote-access/check.sh new file mode 100644 index 0000000..b3a530c --- /dev/null +++ b/labs/linux-plus/3-3-4-secure-remote-access/check.sh @@ -0,0 +1,25 @@ +is_debian && mkdir -p /run/sshd +T=$(sshd -T 2>/dev/null) +K=/home/kim/.ssh/lab_ed25519 +SSHOPT=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR) +key_login() { runuser -u kim -- timeout 15 ssh "${SSHOPT[@]}" -i $K kim@10.99.0.1 'echo le-shell-ok' &1 /dev/null +systemctl disable --now --quiet le-lab-console.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-console.service +rm -rf /srv/le-console /usr/local/lib/le-lab/httpd.py +rmdir /usr/local/lib/le-lab 2>/dev/null +if [ -f "$LAB_STATE/ssh.tar" ]; then + rm -f /etc/ssh/sshd_config.d/* + tar -C /etc -xf "$LAB_STATE/ssh.tar" + command -v restorecon >/dev/null && restorecon -R /etc/ssh +fi +systemctl daemon-reload +if is_rhel; then systemctl restart sshd; else systemctl restart ssh; fi +true diff --git a/labs/linux-plus/3-3-4-secure-remote-access/setup.sh b/labs/linux-plus/3-3-4-secure-remote-access/setup.sh new file mode 100644 index 0000000..d933d60 --- /dev/null +++ b/labs/linux-plus/3-3-4-secure-remote-access/setup.sh @@ -0,0 +1,46 @@ +mkdir -p /run/sshd +sshd -T 2>/dev/null | grep -qx 'passwordauthentication yes' || + refuse "sshd doesn't allow password logins, which task 1 uses; allow them and start again" +listening 9090 tcp && refuse "something already listens on port 9090, which the lab uses; stop it and start again" +user_add kim +tar -C /etc -cf "$LAB_STATE/ssh.tar" ssh +peer_up + +# A web service on 127.0.0.1:9090 only, standing in for Cockpit. +mkdir -p /srv/le-console /usr/local/lib/le-lab +echo 'Lab console on port 9090' >/srv/le-console/index.html +cat >/usr/local/lib/le-lab/httpd.py <<'Y' +# A static web server like "python3 -m http.server PORT --bind ADDRESS +# --directory DIR", without its reverse DNS lookup of the address, which +# can take seconds. +import argparse, functools, http.server, socketserver + +ap = argparse.ArgumentParser() +ap.add_argument("port", type=int) +ap.add_argument("--bind", default="") +ap.add_argument("--directory", default=".") +a = ap.parse_args() + + +class Server(http.server.ThreadingHTTPServer): + def server_bind(self): + socketserver.TCPServer.server_bind(self) + self.server_name, self.server_port = self.server_address[:2] + + +handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=a.directory) +Server((a.bind, a.port), handler).serve_forever() +Y +cat >/etc/systemd/system/le-lab-console.service <<'Y' +[Unit] +Description=Practice lab web service on 127.0.0.1:9090 +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/httpd.py 9090 --bind 127.0.0.1 --directory /srv/le-console + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --now --quiet le-lab-console.service diff --git a/labs/linux-plus/3-3-5-system-hardening/README.md b/labs/linux-plus/3-3-5-system-hardening/README.md new file mode 100644 index 0000000..f573fbd --- /dev/null +++ b/labs/linux-plus/3-3-5-system-hardening/README.md @@ -0,0 +1,37 @@ +# Linux+ 3.3.5: Hardening the system + +Finding what listens, setuid and capability-holding programs, a noexec +mount, and a service's hardening score. + +## Before you start + +The lab has left a few things on this VM for you to find: a service that +listens on the network, a setuid program and a program with a file +capability, none of them installed by a package. Write answers to the +files in `/root/lab-answers/`. Task 4's mount lasts only until a reboot: +do the tasks and check in one go. + +## Tasks + +1. List what listens and what's enabled. One service listens on TCP port + 2323 for no good reason: find it, and stop and disable it. +2. Find the setuid and setgid programs. Write the path of the setuid + program that no package installed to `/root/lab-answers/2`. +3. Find the programs that hold file capabilities. Write the path of the + one that no package installed to `/root/lab-answers/3`. +4. Mount a tmpfs on `/mnt/nx` with `noexec`, `nosuid` and `nodev`. Copy + `/usr/bin/id` there and see that it can't run. Leave it mounted. +5. Check for pending security updates, and find the overall exposure score + that `systemd-analyze security` gives the SSH service (`sshd` on the + RHEL family, `ssh` on Ubuntu). Write the number to + `/root/lab-answers/5`. + +## Hints + +- `ss -tulpn`, `systemctl list-unit-files --state=enabled`, `ps -o + unit= -p PID`; `systemctl disable --now`. +- `find / -xdev -perm -4000 -type f`; `rpm -qf` or `dpkg -S`. +- `getcap -r /usr/bin /usr/sbin`. +- `mount -t tmpfs -o noexec,nosuid,nodev tmpfs /mnt/nx`, `findmnt`. +- `dnf updateinfo list --security` or `apt list --upgradable`; + `systemd-analyze security UNIT | tail -1`. diff --git a/labs/linux-plus/3-3-5-system-hardening/check.sh b/labs/linux-plus/3-3-5-system-hardening/check.sh new file mode 100644 index 0000000..0efcb24 --- /dev/null +++ b/labs/linux-plus/3-3-5-system-hardening/check.sh @@ -0,0 +1,20 @@ +# The overall exposure of the SSH service, as systemd-analyze shows it. +score() { systemd-analyze security "$(pick sshd ssh).service" 2>/dev/null | tail -1 | grep -oE '[0-9]+\.[0-9]+' | head -1; } + +task 1 "The service on TCP port 2323 stopped and disabled" +need "nothing listens on port 2323" '! listening 2323 tcp' +need "its unit is stopped and disabled" '! active legacy-console.service && ! enabled legacy-console.service' + +task 2 "The setuid program no package installed" +need "/root/lab-answers/2 has its path" '[ "$(answer 2)" = /usr/local/bin/sysreport ]' + +task 3 "The capability-holding program no package installed" +need "/root/lab-answers/3 has its path" '[ "$(answer 3)" = /usr/sbin/netprobe ]' + +task 4 "A noexec, nosuid, nodev tmpfs on /mnt/nx" +need "a tmpfs is mounted on /mnt/nx" '[ "$(findmnt -n -o FSTYPE /mnt/nx)" = tmpfs ]' +need "it has noexec, nosuid and nodev" 'o=$(findmnt -n -o OPTIONS /mnt/nx); for w in noexec nosuid nodev; do grep -qw $w <<<"$o" || exit 1; done' +need "/mnt/nx/id is there and can't run" '[ -f /mnt/nx/id ] && ! /mnt/nx/id' + +task 5 "The SSH service's exposure score" +need "/root/lab-answers/5 has the score" 's=$(score); [ -n "$s" ] && grep -qw "$s" /root/lab-answers/5' diff --git a/labs/linux-plus/3-3-5-system-hardening/lab.conf b/labs/linux-plus/3-3-5-system-hardening/lab.conf new file mode 100644 index 0000000..6ee7ff8 --- /dev/null +++ b/labs/linux-plus/3-3-5-system-hardening/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.3.5 Hardening the system" +CHAPTER=/linux-plus/3-security/3-3-5-system-hardening/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-3-5-system-hardening/reset.sh b/labs/linux-plus/3-3-5-system-hardening/reset.sh new file mode 100644 index 0000000..30d32af --- /dev/null +++ b/labs/linux-plus/3-3-5-system-hardening/reset.sh @@ -0,0 +1,8 @@ +systemctl disable --now --quiet legacy-console.service 2>/dev/null +rm -f /etc/systemd/system/legacy-console.service /usr/local/lib/le-lab/legacyd.py +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +rm -f /usr/local/bin/sysreport /usr/sbin/netprobe +umount /mnt/nx 2>/dev/null +rmdir /mnt/nx 2>/dev/null +true diff --git a/labs/linux-plus/3-3-5-system-hardening/setup.sh b/labs/linux-plus/3-3-5-system-hardening/setup.sh new file mode 100644 index 0000000..e9d7269 --- /dev/null +++ b/labs/linux-plus/3-3-5-system-hardening/setup.sh @@ -0,0 +1,39 @@ +listening 2323 tcp && refuse "something already listens on port 2323, which the lab uses; stop it and start again" +findmnt /mnt/nx >/dev/null && refuse "/mnt/nx is already mounted; unmount it and start again" +command -v setcap >/dev/null || pkg_install libcap/libcap2-bin + +# Task 1: a service listening on TCP 2323. +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/legacyd.py <<'Y' +# Accepts connections on TCP port 2323, says hello and hangs up. +import socket +open("/proc/self/comm", "w").write("legacyd") +s = socket.socket() +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.bind(("0.0.0.0", 2323)) +s.listen() +while True: + c, _ = s.accept() + c.sendall(b"legacy remote console\r\n") + c.close() +Y +cat >/etc/systemd/system/legacy-console.service <<'Y' +[Unit] +Description=Legacy remote console +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/legacyd.py + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --now --quiet legacy-console.service + +# Task 2: a setuid program no package installed (a copy of id: harmless). +install -m 4755 -o root -g root /usr/bin/id /usr/local/bin/sysreport +# Task 3: a program with a file capability. +install -m 755 /usr/bin/true /usr/sbin/netprobe +setcap cap_net_raw+ep /usr/sbin/netprobe +true diff --git a/labs/linux-plus/3-4-1-account-hardening/README.md b/labs/linux-plus/3-4-1-account-hardening/README.md new file mode 100644 index 0000000..b0c3a65 --- /dev/null +++ b/labs/linux-plus/3-4-1-account-hardening/README.md @@ -0,0 +1,34 @@ +# Linux+ 3.4.1: Hardening accounts + +Password aging, password quality, locking and expiring an account, and +auditing the accounts of a system. + +## Before you start + +User `hardtest` doesn't exist yet: you create it. The lab has also added +two accounts that an audit should catch. Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Read the current rules in `pwquality.conf`, `login.defs` and + `faillock.conf`. Create user `hardtest`, and set its password aging: + at most 90 days, at least 1 day, a warning 7 days before. Then make it + change its password at the next login. +2. Ubuntu: install `libpam-pwquality`, so that `passwd` judges new + passwords. Try `passwd hardtest` with the password `password1` (Ctrl+C + to stop). On the RHEL family, `passwd` uses pwquality already: just try + it. +3. Lock `hardtest`'s password and expire the account, and look at its + shadow entry and `chage -l`. +4. Audit the system: find the accounts other than root with UID 0, and the + accounts with an empty password. Write their names, one per line, to + `/root/lab-answers/4`. + +## Hints + +- `useradd -m`, `chage -M -m -W`, `chage -d 0`, `chage -l`. +- `/etc/pam.d/common-password`. +- `passwd -l`, `chage -E 0`, `grep hardtest /etc/shadow`. +- `awk -F: '$3 == 0' /etc/passwd`, `awk -F: '$2 == ""' /etc/shadow`, + `lastlog -b 30`. diff --git a/labs/linux-plus/3-4-1-account-hardening/check.sh b/labs/linux-plus/3-4-1-account-hardening/check.sh new file mode 100644 index 0000000..edf47d7 --- /dev/null +++ b/labs/linux-plus/3-4-1-account-hardening/check.sh @@ -0,0 +1,19 @@ +# field N: hardtest's field N in /etc/shadow. +field() { awk -F: -v n="$1" '$1 == "hardtest" {print $n}' /etc/shadow; } + +task 1 "hardtest: aging 90/1/7 and a new password at next login" +need "user hardtest exists" 'id hardtest' +need "maximum 90 days, minimum 1, warning 7" '[ "$(field 5)/$(field 4)/$(field 6)" = 90/1/7 ]' +need "it must change its password at the next login" '[ "$(field 3)" = 0 ]' + +task 2 "passwd uses pwquality on Ubuntu" debian +need "libpam-pwquality is installed" 'pkg_installed libpam-pwquality' +need "/etc/pam.d/common-password uses pam_pwquality" 'conf_has /etc/pam.d/common-password pam_pwquality' + +task 3 "hardtest locked and expired" +need "its password is locked" 'field 2 | grep -q "^!"' +need "the account has expired" '[ "$(field 8)" = 0 ]' + +task 4 "The audit's findings" +need "/root/lab-answers/4 names the other UID 0 account" 'grep -qw toor /root/lab-answers/4' +need "it names the account with an empty password" 'grep -qw guest /root/lab-answers/4' diff --git a/labs/linux-plus/3-4-1-account-hardening/lab.conf b/labs/linux-plus/3-4-1-account-hardening/lab.conf new file mode 100644 index 0000000..7c92afd --- /dev/null +++ b/labs/linux-plus/3-4-1-account-hardening/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.4.1 Hardening accounts" +CHAPTER=/linux-plus/3-security/3-4-1-account-hardening/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-4-1-account-hardening/reset.sh b/labs/linux-plus/3-4-1-account-hardening/reset.sh new file mode 100644 index 0000000..c11e56a --- /dev/null +++ b/labs/linux-plus/3-4-1-account-hardening/reset.sh @@ -0,0 +1,10 @@ +if id hardtest >/dev/null 2>&1; then + pkill -KILL -u hardtest 2>/dev/null + userdel -r hardtest 2>/dev/null +fi +# toor shares root's UID: take its lines out rather than run userdel. +sed -i '/^toor:/d' /etc/passwd /etc/shadow +if is_debian && [ -z "$(state_get had-pwquality)" ] && pkg_installed libpam-pwquality; then + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge libpam-pwquality >/dev/null 2>&1 +fi +true diff --git a/labs/linux-plus/3-4-1-account-hardening/setup.sh b/labs/linux-plus/3-4-1-account-hardening/setup.sh new file mode 100644 index 0000000..3a02157 --- /dev/null +++ b/labs/linux-plus/3-4-1-account-hardening/setup.sh @@ -0,0 +1,10 @@ +id hardtest >/dev/null 2>&1 && refuse "user hardtest already exists; remove it (userdel -r hardtest) and start again" +id toor >/dev/null 2>&1 && refuse "an account toor already exists; remove it and start again" +is_debian && pkg_installed libpam-pwquality && state_set had-pwquality 1 +# For the audit: a second UID 0 account (locked) and one with an empty +# password. +useradd -o -u 0 -g 0 -M -d /root -s /bin/bash -c "Operator" toor +passwd -l toor >/dev/null +user_add guest -c "Guest" +passwd -d guest >/dev/null +true diff --git a/labs/linux-plus/3-5-1-encryption-at-rest/README.md b/labs/linux-plus/3-5-1-encryption-at-rest/README.md new file mode 100644 index 0000000..60707ad --- /dev/null +++ b/labs/linux-plus/3-5-1-encryption-at-rest/README.md @@ -0,0 +1,33 @@ +# Linux+ 3.5.1: Encrypting data at rest + +A LUKS volume on a spare disk, a key file for it, and a file encrypted with +GnuPG. + +## Before you start + +The lab adds a spare disk, `/dev/loop40` (256 MiB, empty), for you to +encrypt. It's an image file under `/var/lib/le-lab/disks/`, attached again +at every boot. Nothing opens the LUKS volume at boot: after a reboot, open +it with the key file and mount it again before you check. + +## Tasks + +1. Install `cryptsetup` if it's missing. Format `/dev/loop40` with LUKS + (choose a passphrase), open it as `lab`, make an ext4 filesystem on it, + mount it on `/mnt/lab`, and write `hidden` to `/mnt/lab/file.txt`. +2. Look at the LUKS header. Make a 64-byte random key file, + `/root/lab.key`, with mode 400, and add it to the volume. Then lock the + volume (unmount and close it), see that the disk can't be mounted + directly, and open it again with the key file and mount it on + `/mnt/lab`. +3. In your home directory, encrypt a file `n.txt` with GnuPG, with a + passphrase (symmetric encryption), to `n.txt.gpg`, and decrypt it + again. + +## Hints + +- `cryptsetup luksFormat`, `cryptsetup open`, `mkfs.ext4`, `lsblk`. +- `cryptsetup luksDump`, `dd if=/dev/urandom bs=64 count=1`, + `cryptsetup luksAddKey DEVICE KEYFILE`; `cryptsetup close`, `blkid`, + `cryptsetup open --key-file`. +- `gpg --pinentry-mode loopback -c`, `gpg --pinentry-mode loopback -d`. diff --git a/labs/linux-plus/3-5-1-encryption-at-rest/check.sh b/labs/linux-plus/3-5-1-encryption-at-rest/check.sh new file mode 100644 index 0000000..467f70c --- /dev/null +++ b/labs/linux-plus/3-5-1-encryption-at-rest/check.sh @@ -0,0 +1,22 @@ +D=/dev/loop40 +# symmetric FILE: FILE starts with a GnuPG passphrase (symmetric key) packet. +symmetric() { + local g; g=$(mktemp -d) + gpg --homedir "$g" --batch --list-packets --pinentry-mode loopback --passphrase le-not-it "$1" 2>/dev/null | grep -q '^:symkey enc packet:' + local r=$?; rm -rf "$g"; return $r +} + +task 1 "LUKS on /dev/loop40, open as lab, ext4 mounted on /mnt/lab" +need "/dev/loop40 is a LUKS volume" '[ "$(blkid -s TYPE -o value $D)" = crypto_LUKS ]' +need "it's open as /dev/mapper/lab" 'cryptsetup status lab | grep -Eq "device:[[:space:]]+$D$"' +need "/mnt/lab is mounted from /dev/mapper/lab, ext4" '[ "$(findmnt -n -o SOURCE,FSTYPE /mnt/lab | tr -s " ")" = "/dev/mapper/lab ext4" ]' +need "/mnt/lab/file.txt says hidden" 'grep -qx hidden /mnt/lab/file.txt' + +task 2 "A key file opens the volume too" +need "/root/lab.key is 64 bytes, mode 400" '[ "$(stat -c %s:%a /root/lab.key)" = 64:400 ]' +need "the key file opens /dev/loop40" 'cryptsetup open --test-passphrase --key-file /root/lab.key $D' +need "two key slots are in use" '[ "$(cryptsetup luksDump $D | grep -cE "^[[:space:]]+[0-9]+: luks2")" -ge 2 ] || [ "$(cryptsetup luksDump $D | grep -c "Key Slot [0-9]: ENABLED")" -ge 2 ]' + +task 3 "n.txt encrypted with a passphrase" +need "n.txt.gpg is in your home directory" 'ls /root/n.txt.gpg /home/*/n.txt.gpg 2>/dev/null | grep -q .' +need "it's encrypted with a passphrase" 'for f in /root/n.txt.gpg /home/*/n.txt.gpg; do [ -f "$f" ] && symmetric "$f" && exit 0; done; exit 1' diff --git a/labs/linux-plus/3-5-1-encryption-at-rest/lab.conf b/labs/linux-plus/3-5-1-encryption-at-rest/lab.conf new file mode 100644 index 0000000..acaffa7 --- /dev/null +++ b/labs/linux-plus/3-5-1-encryption-at-rest/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.5.1 Encrypting data at rest" +CHAPTER=/linux-plus/3-security/3-5-1-encryption-at-rest/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-5-1-encryption-at-rest/reset.sh b/labs/linux-plus/3-5-1-encryption-at-rest/reset.sh new file mode 100644 index 0000000..1678e30 --- /dev/null +++ b/labs/linux-plus/3-5-1-encryption-at-rest/reset.sh @@ -0,0 +1,5 @@ +umount /mnt/lab 2>/dev/null +cryptsetup close lab 2>/dev/null +rmdir /mnt/lab 2>/dev/null +rm -f /root/lab.key /root/n.txt /root/n.txt.gpg /home/*/n.txt /home/*/n.txt.gpg +true diff --git a/labs/linux-plus/3-5-1-encryption-at-rest/setup.sh b/labs/linux-plus/3-5-1-encryption-at-rest/setup.sh new file mode 100644 index 0000000..36769bb --- /dev/null +++ b/labs/linux-plus/3-5-1-encryption-at-rest/setup.sh @@ -0,0 +1,4 @@ +[ -e /dev/mapper/lab ] && refuse "a device-mapper device called lab already exists; close it and start again" +findmnt /mnt/lab >/dev/null && refuse "something is mounted on /mnt/lab; unmount it and start again" +disk_add 0 256M >/dev/null +true diff --git a/labs/linux-plus/3-5-2-encryption-in-transit/README.md b/labs/linux-plus/3-5-2-encryption-in-transit/README.md new file mode 100644 index 0000000..9ebdc89 --- /dev/null +++ b/labs/linux-plus/3-5-2-encryption-in-transit/README.md @@ -0,0 +1,33 @@ +# Linux+ 3.5.2: Encrypting data in transit + +Inspecting a TLS server, the system-wide crypto policy, and seeing a +clear-text protocol on the wire. + +## Before you start + +Task 1 needs the internet. Nothing else is set up: write answers to the +files in `/root/lab-answers/`. + +## Tasks + +1. Connect to `www.kernel.org:443` with `openssl s_client` and look at the + certificate chain, protocol and cipher; then try again allowing only + TLS 1.1, and see it fail. Write the protocol version the first + connection used (as `openssl` shows it) to `/root/lab-answers/1`. +2. RHEL family: look at the system-wide crypto policy and what it allows + in OpenSSL. Write the name of the current policy to + `/root/lab-answers/2`. +3. Run a web server on `127.0.0.1:8000` and capture the traffic on the + loopback interface while you send it a request with the header + `Authorization: Basic c2VjcmV0` with curl. Save the capture to + `/root/lab-answers/3.pcap` and read the header back from it. + +## Hints + +- `openssl s_client -connect HOST:443 -servername HOST /dev/null | grep -q '\.8000'" +need "the Authorization header is readable in it" "tcpdump -n -A -r $P 2>/dev/null | grep -q 'Authorization: Basic c2VjcmV0'" diff --git a/labs/linux-plus/3-5-2-encryption-in-transit/lab.conf b/labs/linux-plus/3-5-2-encryption-in-transit/lab.conf new file mode 100644 index 0000000..742d8c9 --- /dev/null +++ b/labs/linux-plus/3-5-2-encryption-in-transit/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.5.2 Encrypting data in transit" +CHAPTER=/linux-plus/3-security/3-5-2-encryption-in-transit/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-5-2-encryption-in-transit/reset.sh b/labs/linux-plus/3-5-2-encryption-in-transit/reset.sh new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/labs/linux-plus/3-5-2-encryption-in-transit/reset.sh @@ -0,0 +1 @@ +true diff --git a/labs/linux-plus/3-5-2-encryption-in-transit/setup.sh b/labs/linux-plus/3-5-2-encryption-in-transit/setup.sh new file mode 100644 index 0000000..400f3ed --- /dev/null +++ b/labs/linux-plus/3-5-2-encryption-in-transit/setup.sh @@ -0,0 +1,2 @@ +rm -f /root/lab-answers/3.pcap +true diff --git a/labs/linux-plus/3-5-3-hashing-and-certificates/README.md b/labs/linux-plus/3-5-3-hashing-and-certificates/README.md new file mode 100644 index 0000000..e5861b9 --- /dev/null +++ b/labs/linux-plus/3-5-3-hashing-and-certificates/README.md @@ -0,0 +1,34 @@ +# Linux+ 3.5.3: Hashing and certificates + +A checksum that catches a change, a private CA and a server certificate it +signs, and trusting the CA system-wide. + +## Before you start + +Nothing is set up. Work as root (`sudo -i`), in root's home directory +`/root`, so the files are where the check looks: `/root/f.txt`, +`/root/f.sha256` and `/root/pki/`. + +## Tasks + +1. Write `version 1` to `f.txt`, save its SHA-256 checksum in + `f.sha256`, and check it. Then change the file to `version 2` and see + the check fail. Leave it like that. +2. In `/root/pki`, make a private CA (`ca.key`, `ca.crt`, subject + `CN=Lab CA`), and a server key and certificate (`www.key`, `www.crt`) + for `lab.example.test`, with that name in its subject alternative names, + signed by the CA. Verify the certificate against the CA. +3. Serve the certificate with `openssl s_server` on port 8443 and see curl + reject it, then accept it with `--cacert`. Then add the CA to the + system's trust store, and see curl accept it without `--cacert`. + +## Hints + +- `sha256sum FILE > FILE.sha256`, `sha256sum -c`. +- `openssl req -x509 -newkey rsa:3072 -nodes`, `openssl req -new`, + `openssl x509 -req -CA -CAkey -CAcreateserial -extfile` with + `subjectAltName=DNS:...`; `openssl verify -CAfile`. +- `curl --resolve lab.example.test:8443:127.0.0.1`. RHEL family: + `/etc/pki/ca-trust/source/anchors/` and `update-ca-trust`; Ubuntu: + `/usr/local/share/ca-certificates/` (the name must end in `.crt`) and + `update-ca-certificates`. diff --git a/labs/linux-plus/3-5-3-hashing-and-certificates/check.sh b/labs/linux-plus/3-5-3-hashing-and-certificates/check.sh new file mode 100644 index 0000000..85c9bbc --- /dev/null +++ b/labs/linux-plus/3-5-3-hashing-and-certificates/check.sh @@ -0,0 +1,17 @@ +D=/root/pki +x509() { openssl x509 -noout "$@" 2>/dev/null; } + +task 1 "f.sha256 catches the change to f.txt" +need "/root/f.sha256 has the checksum of version 1 for f.txt" 'grep -Eq "^$(printf "version 1\n" | sha256sum | cut -d" " -f1)[[:space:]]+\*?f\.txt$" /root/f.sha256' +need "/root/f.txt now says version 2" 'grep -qx "version 2" /root/f.txt' +need "sha256sum -c reports the change" 'cd /root && ! sha256sum -c f.sha256' + +task 2 "A private CA and a certificate for lab.example.test signed by it" +need "$D/ca.crt is a CA certificate with subject CN = Lab CA" 'x509 -in $D/ca.crt -subject | grep -q "CN *= *Lab CA" && x509 -in $D/ca.crt -ext basicConstraints | grep -q "CA:TRUE"' +need "$D/ca.key is its key" '[ "$(x509 -in $D/ca.crt -pubkey | sha256sum)" = "$(openssl pkey -in $D/ca.key -pubout 2>/dev/null | sha256sum)" ]' +need "$D/www.crt names lab.example.test in its subject alternative names" 'x509 -in $D/www.crt -ext subjectAltName | grep -q "DNS:lab\.example\.test"' +need "$D/www.key is its key" '[ "$(x509 -in $D/www.crt -pubkey | sha256sum)" = "$(openssl pkey -in $D/www.key -pubout 2>/dev/null | sha256sum)" ]' +need "openssl verify -CAfile ca.crt www.crt succeeds" 'openssl verify -CAfile $D/ca.crt $D/www.crt' + +task 3 "The CA trusted system-wide" +need "openssl verify www.crt succeeds without -CAfile" 'openssl verify $D/www.crt' diff --git a/labs/linux-plus/3-5-3-hashing-and-certificates/lab.conf b/labs/linux-plus/3-5-3-hashing-and-certificates/lab.conf new file mode 100644 index 0000000..6ecd924 --- /dev/null +++ b/labs/linux-plus/3-5-3-hashing-and-certificates/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.5.3 Hashing and certificates" +CHAPTER=/linux-plus/3-security/3-5-3-hashing-and-certificates/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-5-3-hashing-and-certificates/reset.sh b/labs/linux-plus/3-5-3-hashing-and-certificates/reset.sh new file mode 100644 index 0000000..5af1d25 --- /dev/null +++ b/labs/linux-plus/3-5-3-hashing-and-certificates/reset.sh @@ -0,0 +1,10 @@ +pkill -f 's_server -accept 8443' 2>/dev/null +rm -rf /root/pki /root/f.txt /root/f.sha256 +if is_rhel; then + rm -f /etc/pki/ca-trust/source/anchors/lab-ca.crt + update-ca-trust +else + rm -f /usr/local/share/ca-certificates/lab-ca.crt + update-ca-certificates --fresh >/dev/null +fi +true diff --git a/labs/linux-plus/3-5-3-hashing-and-certificates/setup.sh b/labs/linux-plus/3-5-3-hashing-and-certificates/setup.sh new file mode 100644 index 0000000..d80ed54 --- /dev/null +++ b/labs/linux-plus/3-5-3-hashing-and-certificates/setup.sh @@ -0,0 +1,3 @@ +[ -e /root/pki ] && refuse "/root/pki already exists; move it away and start again" +rm -f /root/f.txt /root/f.sha256 +true diff --git a/labs/linux-plus/3-6-1-vulnerabilities-and-standards/README.md b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/README.md new file mode 100644 index 0000000..1599f8f --- /dev/null +++ b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/README.md @@ -0,0 +1,34 @@ +# Linux+ 3.6.1: Vulnerabilities, detection and standards + +Scanning your own machine, finding CVEs in a package's changelog, and a +CIS benchmark scan. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the Ubuntu VM: scan +this VM, `10.99.0.1`, from there. Get a shell there with +`sudo ip netns exec lepeer bash`; it shares this VM's programs, so install +`nmap` on this VM. Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. From the peer, scan this VM with `nmap -sV 10.99.0.1` and `nmap -sU + --top-ports 20 10.99.0.1`, and compare with `ss -tulpn` here. Write + the product and version nmap reports for port 22 to + `/root/lab-answers/1`. +2. Find a CVE fixed in the `openssh-server` package, from its changelog, + and write its ID (such as `CVE-2024-6387`) to `/root/lab-answers/2`. + On the RHEL family, also list the security advisories. +3. RHEL family: install the SCAP scanner and content, find a CIS profile in + the data stream for this system, and run a scan with it, writing the + HTML report to `/root/cis.html`. It takes a few minutes. + +## Hints + +- `dnf install nmap` or `apt install nmap`; the `VERSION` column. +- `rpm -q --changelog openssh-server`, `dnf updateinfo list --security`; + `apt changelog openssh-server`, or + `/usr/share/doc/openssh-server/changelog.Debian.gz` (`zgrep`). +- `dnf install openscap-scanner scap-security-guide`, + `/usr/share/xml/scap/ssg/content/`, `oscap info`, `oscap xccdf eval + --profile ID --report FILE DATASTREAM`. diff --git a/labs/linux-plus/3-6-1-vulnerabilities-and-standards/check.sh b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/check.sh new file mode 100644 index 0000000..3f77821 --- /dev/null +++ b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/check.sh @@ -0,0 +1,18 @@ +# The OpenSSH version, as 9.6 or 9.9. +V=$(ssh -V 2>&1 | grep -oE 'OpenSSH_[0-9]+\.[0-9]+' | cut -d_ -f2) +changelog() { + if is_rhel; then rpm -q --changelog openssh-server + elif [ -f /usr/share/doc/openssh-server/changelog.Debian.gz ]; then zcat /usr/share/doc/openssh-server/changelog.Debian.gz + else apt-get changelog openssh-server 2>/dev/null; fi +} + +task 1 "The SSH product and version nmap found" +need "/root/lab-answers/1 names OpenSSH and its version" 'answer 1 | grep -q openssh && answer 1 | grep -qF "$V"' + +task 2 "A CVE fixed in openssh-server" +need "/root/lab-answers/2 has a CVE ID" 'grep -Eqi "CVE-[0-9]{4}-[0-9]{4,}" /root/lab-answers/2' +need "the CVE is in the package's changelog" 'c=$(grep -Eoi "CVE-[0-9]{4}-[0-9]{4,}" /root/lab-answers/2 | head -1); changelog | grep -qiw "$c"' + +task 3 "A CIS scan report in /root/cis.html" rhel +need "/root/cis.html is an OpenSCAP report" 'grep -q "OpenSCAP" /root/cis.html' +need "it's from a CIS profile" 'grep -Eq "content_profile_cis" /root/cis.html' diff --git a/labs/linux-plus/3-6-1-vulnerabilities-and-standards/lab.conf b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/lab.conf new file mode 100644 index 0000000..e0dc8d8 --- /dev/null +++ b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.6.1 Vulnerabilities, detection and standards" +CHAPTER=/linux-plus/3-security/3-6-1-vulnerabilities-and-standards/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/3-6-1-vulnerabilities-and-standards/reset.sh b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/reset.sh new file mode 100644 index 0000000..387b305 --- /dev/null +++ b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/reset.sh @@ -0,0 +1,2 @@ +rm -f /root/cis.html +true diff --git a/labs/linux-plus/3-6-1-vulnerabilities-and-standards/setup.sh b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/setup.sh new file mode 100644 index 0000000..28703df --- /dev/null +++ b/labs/linux-plus/3-6-1-vulnerabilities-and-standards/setup.sh @@ -0,0 +1,3 @@ +rm -f /root/cis.html +peer_up +true diff --git a/labs/linux-plus/3-6-2-integrity-destruction-banners/README.md b/labs/linux-plus/3-6-2-integrity-destruction-banners/README.md new file mode 100644 index 0000000..351a555 --- /dev/null +++ b/labs/linux-plus/3-6-2-integrity-destruction-banners/README.md @@ -0,0 +1,39 @@ +# Linux+ 3.6.2: Integrity, data destruction and banners + +Verifying installed packages, AIDE on a small rule set, a pre-login SSH +banner, and wiping a disk. + +## Before you start + +The lab has changed one file that a package installed, outside `/etc`. It +adds a spare disk, `/dev/loop40` (64 MiB, full of old data), an image file +under `/var/lib/le-lab/disks/`. Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Verify the installed packages, and find the file outside `/etc` whose + contents no longer match its package. Write its path to + `/root/lab-answers/1`, then restore it by reinstalling its package, so + that the package verifies clean again. +2. RHEL family: install AIDE, and write a small configuration, + `/root/lab-aide.conf`, that keeps its database in `/root/lab-aide.db` + (new ones in `/root/lab-aide.db.new`) and watches `/etc/ssh` for + permissions, owner, group, size and SHA-256. Initialize the database, + create `/etc/ssh/lab-new-file`, and run a check that reports it. Leave + the file and the database in place. +3. Show the banner `Authorized use only. Activity is monitored.` before + SSH logins, from `/etc/issue.net`, through a drop-in in + `/etc/ssh/sshd_config.d/`. Connect with `ssh localhost` and see it. +4. Wipe `/dev/loop40`: one pass of random data, then zeros. Look at its + first sector afterward. + +## Hints + +- `rpm -Va` (`5` means the digest differs), `rpm -qf`, `dnf reinstall`; + `dpkg --verify`, `dpkg -S`, `apt install --reinstall`. +- `database_in=file:...`, `database_out=file:...`, `/etc/ssh + p+u+g+s+sha256`; `aide -c FILE --init`, `--check`. +- `Banner /etc/issue.net`; `sshd -t`, `systemctl reload sshd` (`ssh` on + Ubuntu); `sshd -T | grep banner`. +- `shred -v -n 1 -z`, `hexdump -C -n 512`. diff --git a/labs/linux-plus/3-6-2-integrity-destruction-banners/check.sh b/labs/linux-plus/3-6-2-integrity-destruction-banners/check.sh new file mode 100644 index 0000000..5335c9d --- /dev/null +++ b/labs/linux-plus/3-6-2-integrity-destruction-banners/check.sh @@ -0,0 +1,27 @@ +F=/usr/share/zoneinfo/Pacific/Pitcairn +verify_clean() { + if is_rhel; then rpm -V "$(rpm -qf $F)" | grep -q "$F" && return 1; return 0 + else dpkg --verify "$(dpkg -S $F | cut -d: -f1)" | grep -q "$F" && return 1; return 0; fi +} +mkdir -p /run/sshd + +task 1 "The changed file found and restored" +need "/root/lab-answers/1 has its path" '[ "$(answer 1)" = "${F,,}" ]' +need "its package verifies clean again" verify_clean + +task 2 "AIDE reports a new file in /etc/ssh" rhel +C=/root/lab-aide.conf +need "$C keeps the database in /root/lab-aide.db" 'grep -Eq "^database_in=file:/root/lab-aide\.db$" '$C' && grep -Eq "^database_out=file:/root/lab-aide\.db\.new$" '$C +need "it watches /etc/ssh with p+u+g+s+sha256" 'grep -Eq "^/etc/ssh[[:space:]]+p\+u\+g\+s\+sha256$" '$C +need "/root/lab-aide.db exists" '[ -s /root/lab-aide.db ]' +need "/etc/ssh/lab-new-file exists" '[ -e /etc/ssh/lab-new-file ]' +need "aide --check reports it as added" 'aide -c '$C' --check 2>/dev/null | grep -q "/etc/ssh/lab-new-file"' + +task 3 "A pre-login SSH banner from /etc/issue.net" +need "/etc/issue.net has the banner text" 'grep -qx "Authorized use only. Activity is monitored." /etc/issue.net' +need "a drop-in in /etc/ssh/sshd_config.d/ sets it" 'grep -rqsE "^[[:space:]]*Banner[[:space:]]+/etc/issue\.net" /etc/ssh/sshd_config.d/' +need "sshd -T shows banner /etc/issue.net" 'sshd -T 2>/dev/null | grep -qx "banner /etc/issue.net"' +need "ssh localhost shows the banner" 'timeout 10 ssh -o BatchMode=yes -o PreferredAuthentications=none -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null le-nobody@localhost 2>&1 /dev/null && restorecon -R /etc/ssh +fi +if [ -f "$LAB_STATE/issue.net" ]; then + cp -a "$LAB_STATE/issue.net" /etc/issue.net +fi +rm -f /etc/issue.net.orig +if is_rhel; then systemctl reload sshd; else mkdir -p /run/sshd; systemctl reload ssh 2>/dev/null || systemctl restart ssh; fi +true diff --git a/labs/linux-plus/3-6-2-integrity-destruction-banners/setup.sh b/labs/linux-plus/3-6-2-integrity-destruction-banners/setup.sh new file mode 100644 index 0000000..bfde303 --- /dev/null +++ b/labs/linux-plus/3-6-2-integrity-destruction-banners/setup.sh @@ -0,0 +1,14 @@ +F=/usr/share/zoneinfo/Pacific/Pitcairn +[ -f $F ] || refuse "$F is missing; this lab changes it on purpose" +[ -e /etc/ssh/lab-new-file ] && refuse "/etc/ssh/lab-new-file already exists; remove it and start again" +# Task 1: a changed file of a package (a time zone nobody here uses). +cp -a $F "$LAB_STATE/tzfile" +printf 'le-lab\n' >>$F +# Task 3: the SSH configuration and the banner file, for reset. +tar -C /etc -cf "$LAB_STATE/ssh.tar" ssh +[ -f /etc/issue.net ] && cp -a /etc/issue.net "$LAB_STATE/issue.net" +# Task 4: a disk full of old data. +d=$(disk_add 0 64M) +yes "customer record 0042: Jane Example, 555-0100" | head -c 64M >"$d" || true +sync +true diff --git a/labs/linux-plus/4-1-1-ansible/README.md b/labs/linux-plus/4-1-1-ansible/README.md new file mode 100644 index 0000000..89320ab --- /dev/null +++ b/labs/linux-plus/4-1-1-ansible/README.md @@ -0,0 +1,38 @@ +# Linux+ 4.1.1: Ansible + +An inventory, ad hoc commands, and a playbook that installs a package, +manages a file and starts a service, run until it changes nothing. + +## Before you start + +The VM needs internet access. This one VM is both the control node and the +managed host: Ansible reaches it over SSH at `127.0.0.1`. The lab creates +user `alice` (password `Lab-Pass-1`), who may use sudo with her password, +installs `at` and stops and disables `atd`. Work as alice for the +Ansible tasks (`sudo -iu alice` is fine). `le-lab reset` puts `/etc/motd` +back. + +## Tasks + +1. Install Ansible. As alice, set up key-based SSH from alice to + `alice@127.0.0.1`, and write `~/ans/inventory.ini` with a group `lab` + that holds `127.0.0.1` with `ansible_user=alice`. Run the `ping` and + `setup` modules against `lab`. +2. Write `~/ans/site.yml`, a play for `lab` that becomes root and installs + `tree`, writes `/etc/motd` with the content `Managed by Ansible on + ` (from the facts), and makes `atd` run and start at boot. + Run it with `--check --diff` first, then for real, then again to see + everything `ok`. Finally change `/etc/motd` by hand, run the playbook + once more, and watch it put the content back. + +## Hints + +- `ansible-core` on RHEL relatives, `ansible` on Ubuntu. +- `ssh-keygen`; on one machine you can append your public key to + `~/.ssh/authorized_keys` yourself; `ssh 127.0.0.1 true` once to accept the + host key. +- `ansible lab -i inventory.ini -m ping`, `-m setup -a + 'filter=ansible_distribution*'`. +- `ansible.builtin.package`, `ansible.builtin.copy` with `content:`, + `ansible.builtin.service`; `become: true` and `-K` for alice's sudo + password; `ansible-playbook --check --diff`. diff --git a/labs/linux-plus/4-1-1-ansible/check.sh b/labs/linux-plus/4-1-1-ansible/check.sh new file mode 100644 index 0000000..e093e1f --- /dev/null +++ b/labs/linux-plus/4-1-1-ansible/check.sh @@ -0,0 +1,14 @@ +ans() { (cd /home/alice/ans 2>/dev/null && runuser -u alice -- env HOME=/home/alice "$@"); } +distro=$(. /etc/os-release; echo "$NAME" | awk '{print $1}') + +task 1 "Ansible reaches 127.0.0.1 as alice over SSH" +need "ansible is installed" 'command -v ansible && command -v ansible-playbook' +need "~/ans/inventory.ini has group lab with 127.0.0.1" 'grep -qx "\[lab\]" /home/alice/ans/inventory.ini && grep -q "^127\.0\.0\.1" /home/alice/ans/inventory.ini' +need "ansible lab -m ping works as alice" 'ans timeout 60 ansible lab -i inventory.ini -m ping' + +task 2 "site.yml installs tree, manages /etc/motd, runs atd" +need "~/ans/site.yml exists" '[ -s /home/alice/ans/site.yml ]' +need "tree is installed" 'pkg_installed tree' +need "/etc/motd says Managed by Ansible on $distro" 'grep -qx "Managed by Ansible on $distro.*" /etc/motd' +need "atd is running" active atd +need "atd starts at boot" enabled atd diff --git a/labs/linux-plus/4-1-1-ansible/lab.conf b/labs/linux-plus/4-1-1-ansible/lab.conf new file mode 100644 index 0000000..f525c7a --- /dev/null +++ b/labs/linux-plus/4-1-1-ansible/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.1.1 Ansible" +CHAPTER=/linux-plus/4-automation-and-scripting/4-1-1-ansible/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-1-1-ansible/reset.sh b/labs/linux-plus/4-1-1-ansible/reset.sh new file mode 100644 index 0000000..765e4c2 --- /dev/null +++ b/labs/linux-plus/4-1-1-ansible/reset.sh @@ -0,0 +1,13 @@ +if [ -f "$LAB_STATE/motd" ]; then cp -a "$LAB_STATE/motd" /etc/motd; else rm -f /etc/motd; fi +if pkg_installed tree; then + if is_rhel; then dnf -y -q remove tree >/dev/null; else DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge tree >/dev/null 2>&1; fi +fi +if [ -z "$(state_get had-at)" ]; then + systemctl disable --now --quiet atd 2>/dev/null + if is_rhel; then dnf -y -q remove at >/dev/null; else DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge at >/dev/null 2>&1; fi +elif [ -n "$(state_get atd-enabled)" ]; then + systemctl enable --now --quiet atd 2>/dev/null +else + systemctl disable --now --quiet atd 2>/dev/null +fi +true diff --git a/labs/linux-plus/4-1-1-ansible/setup.sh b/labs/linux-plus/4-1-1-ansible/setup.sh new file mode 100644 index 0000000..221efb9 --- /dev/null +++ b/labs/linux-plus/4-1-1-ansible/setup.sh @@ -0,0 +1,8 @@ +pkg_installed tree && refuse "tree is installed; remove it first (the playbook installs it)" +user_add alice -G "$(pick wheel sudo)" +pkg_installed at && state_set had-at 1 +pkg_installed at || pkg_install at +enabled atd && state_set atd-enabled 1 +systemctl disable --now --quiet atd 2>/dev/null +[ -e /etc/motd ] && cp -a /etc/motd "$LAB_STATE/motd" +systemctl enable --now --quiet "$(pick sshd ssh)" 2>/dev/null || true diff --git a/labs/linux-plus/4-1-2-puppet-and-opentofu/README.md b/labs/linux-plus/4-1-2-puppet-and-opentofu/README.md new file mode 100644 index 0000000..dd70511 --- /dev/null +++ b/labs/linux-plus/4-1-2-puppet-and-opentofu/README.md @@ -0,0 +1,35 @@ +# Linux+ 4.1.2: Puppet and OpenTofu + +A Puppet manifest applied without a server, and an OpenTofu configuration +planned, applied and destroyed with a provider that needs no cloud +account. + +## Before you start + +Work as root, so `~` is `/root`. The VM needs internet access to add the +OpenVox and OpenTofu package repositories. The lab only checks that +`~/lab.pp`, `~/tofu` and `/tmp/puppet-lab.txt` don't exist yet. +`le-lab reset` removes those files but leaves the packages installed. + +## Tasks + +1. Add the OpenVox 8 repository for your system (see voxpupuli.org) and + install `openvox-agent`. Write `~/lab.pp`, a manifest that manages + `/tmp/puppet-lab.txt` with mode 0644 and the content `Managed by Puppet + on ` (from the `os.name` fact). Apply it with `--noop` first, + then for real, and ask facter for `os.family`. +2. Install OpenTofu from opentofu.org's packages. In `~/tofu`, write a + `main.tf` with a `terraform_data` resource named `hello` whose input is + `hello from OpenTofu`, and an output `msg` with its output. Run `tofu + init`, `plan` and `apply`, look at the state, and then destroy it. + +## Hints + +- The release package `openvox8-release-el-10.noarch.rpm` (or + `openvox8-release-ubuntu24.04.deb`), then `openvox-agent`; + `/opt/puppetlabs/bin/puppet apply --noop`, `/opt/puppetlabs/bin/facter`. +- A `file { '/tmp/puppet-lab.txt': ... }` resource with `ensure`, + `content` and `mode`; `${facts['os']['name']}` in a double-quoted string. +- The installer script `install-opentofu.sh` with `--install-method rpm` + or `deb`; `tofu init`, `tofu plan`, `tofu apply -auto-approve`, + `tofu state list`, `tofu destroy -auto-approve`. diff --git a/labs/linux-plus/4-1-2-puppet-and-opentofu/check.sh b/labs/linux-plus/4-1-2-puppet-and-opentofu/check.sh new file mode 100644 index 0000000..6522a9a --- /dev/null +++ b/labs/linux-plus/4-1-2-puppet-and-opentofu/check.sh @@ -0,0 +1,15 @@ +F=/tmp/puppet-lab.txt T=/root/tofu +os_name() { /opt/puppetlabs/bin/facter os.name 2>/dev/null; } +tf_resources() { python3 -c 'import json, sys; print(len(json.load(open(sys.argv[1]))["resources"]))' "$1" 2>/dev/null; } + +task 1 "Puppet manages /tmp/puppet-lab.txt" +need "openvox-agent is installed" 'pkg_installed openvox-agent && [ -x /opt/puppetlabs/bin/puppet ]' +need "~/lab.pp manages /tmp/puppet-lab.txt" 'grep -q "file.*/tmp/puppet-lab.txt" /root/lab.pp' +need "/tmp/puppet-lab.txt says Managed by Puppet on the OS name" '[ -n "$(os_name)" ] && [ "$(cat "$F")" = "Managed by Puppet on $(os_name)" ]' +need "/tmp/puppet-lab.txt has mode 644" '[ "$(stat -c %a "$F")" = 644 ]' + +task 2 "OpenTofu applied terraform_data.hello, then destroyed it" +need "tofu is installed" 'command -v tofu' +need "~/tofu/main.tf has terraform_data hello" 'grep -Eq "resource +\"terraform_data\" +\"hello\"" "$T/main.tf"' +need "the state was applied (the backup state has the resource)" '[ "$(tf_resources "$T/terraform.tfstate.backup")" -ge 1 ] || [ "$(tf_resources "$T/terraform.tfstate")" -ge 1 ]' +need "it was destroyed again (the state has no resources)" '[ "$(tf_resources "$T/terraform.tfstate")" = 0 ]' diff --git a/labs/linux-plus/4-1-2-puppet-and-opentofu/lab.conf b/labs/linux-plus/4-1-2-puppet-and-opentofu/lab.conf new file mode 100644 index 0000000..a9289f9 --- /dev/null +++ b/labs/linux-plus/4-1-2-puppet-and-opentofu/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.1.2 Puppet and OpenTofu" +CHAPTER=/linux-plus/4-automation-and-scripting/4-1-2-puppet-and-opentofu/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-1-2-puppet-and-opentofu/reset.sh b/labs/linux-plus/4-1-2-puppet-and-opentofu/reset.sh new file mode 100644 index 0000000..fb60756 --- /dev/null +++ b/labs/linux-plus/4-1-2-puppet-and-opentofu/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/lab.pp /root/tofu /tmp/puppet-lab.txt +true diff --git a/labs/linux-plus/4-1-2-puppet-and-opentofu/setup.sh b/labs/linux-plus/4-1-2-puppet-and-opentofu/setup.sh new file mode 100644 index 0000000..6fc72d5 --- /dev/null +++ b/labs/linux-plus/4-1-2-puppet-and-opentofu/setup.sh @@ -0,0 +1,2 @@ +for f in /root/lab.pp /root/tofu /tmp/puppet-lab.txt; do [ ! -e "$f" ] || refuse "$f already exists; move it away first"; done +true diff --git a/labs/linux-plus/4-1-4-kubernetes-swarm-compose/README.md b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/README.md new file mode 100644 index 0000000..a269a1a --- /dev/null +++ b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/README.md @@ -0,0 +1,35 @@ +# Linux+ 4.1.4: Kubernetes, Swarm and Compose + +A two-service stack with Compose on Podman, and a pod turned into +Kubernetes YAML and played back, all rootless. + +## Before you start + +The VM needs internet access. The lab installs Podman and creates user +`alice` (password `Lab-Pass-1`), with lingering turned on so her user +services run. Install `podman-compose` yourself (from EPEL on RHEL +relatives). Do the container work as alice: log in as her, or use +`sudo -iu alice` and then `export XDG_RUNTIME_DIR=/run/user/$(id -u)`. +Docker Hub limits anonymous pulls; if you hit the limit, use +`mirror.gcr.io/library/...` instead of `docker.io/library/...`. + +## Tasks + +1. In `~/stack`, write a `compose.yaml` with two services: `web` + (`nginx:1.30`, host port 8080 to port 80) and `cache` (`redis:8`). Bring + it up with `podman-compose`, check it with `ps`, `curl` and the logs, + and leave it running. (The chapter then runs `podman-compose down`; + skip that here.) +2. Create a pod `lab` that publishes host port 8081 to port 80, and run + `nginx:1.30` in it. Generate Kubernetes YAML for it as + `~/stack/lab.yaml`, remove the pod, and start it again from the YAML + with `podman kube play`. Leave it running. + +## Hints + +- `dnf install podman-compose` (EPEL), `apt install podman-compose`; + `podman-compose up -d`, `podman-compose ps`, `podman-compose logs web`, + `curl -sI http://127.0.0.1:8080`. +- `podman pod create --name lab -p 8081:80`, `podman run -d --pod lab`, + `podman kube generate lab > lab.yaml`, `podman pod rm -f lab`, + `podman kube play lab.yaml`, `podman pod ps`. diff --git a/labs/linux-plus/4-1-4-kubernetes-swarm-compose/check.sh b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/check.sh new file mode 100644 index 0000000..b0ec681 --- /dev/null +++ b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/check.sh @@ -0,0 +1,19 @@ +auid=$(id -u alice 2>/dev/null) +apodman() { (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$auid podman "$@"); } +http_ok() { for i in $(seq 10); do [ "$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$1")" = 200 ] && return 0; sleep 1; done; return 1; } +svc_running() { + apodman ps --filter label=com.docker.compose.project=stack --filter "label=com.docker.compose.service=$1" --format '{{.Image}}' +} + +task 1 "Compose stack web + cache running from ~/stack" +need "podman-compose is installed" 'command -v podman-compose' +need "~/stack/compose.yaml has services web and cache" 'grep -Eq "^[[:space:]]+web:" /home/alice/stack/compose.yaml && grep -Eq "^[[:space:]]+cache:" /home/alice/stack/compose.yaml' +need "the web service runs nginx" 'svc_running web | grep -q nginx' +need "the cache service runs redis" 'svc_running cache | grep -q redis' +need "http://127.0.0.1:8080 answers 200" 'http_ok http://127.0.0.1:8080/' + +task 2 "Pod lab played from ~/stack/lab.yaml" +need "~/stack/lab.yaml is Kubernetes YAML for a pod with nginx" 'grep -q "^kind: Pod" /home/alice/stack/lab.yaml && grep -q "image: .*nginx" /home/alice/stack/lab.yaml' +need "it publishes port 8081" 'grep -q "hostPort: 8081" /home/alice/stack/lab.yaml' +need "pod lab is running" '[ "$(apodman pod inspect -f "{{.State}}" lab)" = Running ]' +need "http://127.0.0.1:8081 answers 200" 'http_ok http://127.0.0.1:8081/' diff --git a/labs/linux-plus/4-1-4-kubernetes-swarm-compose/lab.conf b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/lab.conf new file mode 100644 index 0000000..b8ec76d --- /dev/null +++ b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.1.4 Kubernetes, Swarm and Compose" +CHAPTER=/linux-plus/4-automation-and-scripting/4-1-4-kubernetes-swarm-compose/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-1-4-kubernetes-swarm-compose/reset.sh b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/reset.sh new file mode 100644 index 0000000..9bc3a9d --- /dev/null +++ b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/reset.sh @@ -0,0 +1,11 @@ +# alice's containers, pods, volumes and networks go with her account; stop +# them cleanly first, then her services. +if id alice >/dev/null 2>&1; then + uid=$(id -u alice) + (cd / && runuser -u alice -- env XDG_RUNTIME_DIR=/run/user/$uid sh -c \ + 'podman pod rm -f -a; podman rm -f -a -t 0; podman volume prune -f; podman network prune -f' >/dev/null 2>&1) + loginctl disable-linger alice 2>/dev/null + loginctl terminate-user alice 2>/dev/null + systemctl stop "user@$uid.service" 2>/dev/null +fi +true diff --git a/labs/linux-plus/4-1-4-kubernetes-swarm-compose/setup.sh b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/setup.sh new file mode 100644 index 0000000..a8ab647 --- /dev/null +++ b/labs/linux-plus/4-1-4-kubernetes-swarm-compose/setup.sh @@ -0,0 +1,5 @@ +pkg_installed podman || pkg_install podman +user_add alice +loginctl enable-linger alice +uid=$(id -u alice) +for i in $(seq 30); do [ -S /run/user/$uid/bus ] && break; sleep 1; done diff --git a/labs/linux-plus/4-2-1-script-basics/README.md b/labs/linux-plus/4-2-1-script-basics/README.md new file mode 100644 index 0000000..c5ec0a3 --- /dev/null +++ b/labs/linux-plus/4-2-1-script-basics/README.md @@ -0,0 +1,31 @@ +# Linux+ 4.2.1: Shell script basics + +A script with arguments, a default value and exit codes, and parameter +expansion. + +## Before you start + +Work as root, so `~` is `/root`. The lab only checks that `~/info.sh` +doesn't exist yet. Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Write the executable script `~/info.sh`: with `set -u`, it takes a host + as its first argument (default: this machine's hostname), prints + `Checking HOST (N args, script NAME, PID PID)`, then pings the host once + with a one-second timeout and prints `up` and exits 0, or prints `down` + and exits 1. Run it with no argument, with `192.0.2.1`, and with + `bash -x` and `localhost`. +2. Set `f=/var/log/nginx/access.log` and, with parameter expansion only + (no external commands), print on one line: the file name, the directory, + the path with `.log` replaced by `.old`, the length of the path, and the + path in capitals, separated by spaces. Save that line in + `/root/lab-answers/2`. + +## Hints + +- `#!/bin/bash`, `${1:-$(hostname)}`, `$#`, `$0`, `$$`, + `if ping -c1 -W1 "$host" >/dev/null 2>&1; then ...`, `chmod +x`, + `echo "exit $?"`. +- `${f##*/}`, `${f%/*}`, `${f%.log}`, `${#f}`, `${f^^}`. +- The chapter's step 2 (export and subshells) isn't graded; try it anyway. diff --git a/labs/linux-plus/4-2-1-script-basics/check.sh b/labs/linux-plus/4-2-1-script-basics/check.sh new file mode 100644 index 0000000..b8e2318 --- /dev/null +++ b/labs/linux-plus/4-2-1-script-basics/check.sh @@ -0,0 +1,13 @@ +I=/root/info.sh + +task 1 "~/info.sh with arguments and exit codes" +need "~/info.sh is executable" '[ -x "$I" ]' +need "it starts with a bash shebang" 'head -n1 "$I" | grep -Eq "^#!(/usr)?/bin/(env )?bash"' +need "it uses set -u" 'grep -Eq "^[[:space:]]*set -[a-z]*u" "$I"' +need "info.sh localhost prints Checking localhost and up, and exits 0" 'out=$("$I" localhost) && grep -q "Checking localhost (1 args" <<<"$out" && grep -qx up <<<"$out"' +need "info.sh 192.0.2.1 prints down and exits 1" 'out=$("$I" 192.0.2.1); [ $? = 1 ] && grep -qx down <<<"$out"' +need "with no argument it checks this machine's hostname" '"$I" | grep -q "Checking $(hostname) (0 args"' + +task 2 "Parameter expansion of the nginx log path" +need "/root/lab-answers/2 has the expanded line" '[ "$(answer 2)" = "access.log /var/log/nginx /var/log/nginx/access.old 25 /var/log/nginx/access.log" ]' +need "the last field is in capitals" 'grep -q " /VAR/LOG/NGINX/ACCESS.LOG$" /root/lab-answers/2' diff --git a/labs/linux-plus/4-2-1-script-basics/lab.conf b/labs/linux-plus/4-2-1-script-basics/lab.conf new file mode 100644 index 0000000..20def86 --- /dev/null +++ b/labs/linux-plus/4-2-1-script-basics/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.2.1 Shell script basics" +CHAPTER=/linux-plus/4-automation-and-scripting/4-2-1-script-basics/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-2-1-script-basics/reset.sh b/labs/linux-plus/4-2-1-script-basics/reset.sh new file mode 100644 index 0000000..e216502 --- /dev/null +++ b/labs/linux-plus/4-2-1-script-basics/reset.sh @@ -0,0 +1,2 @@ +rm -f /root/info.sh +true diff --git a/labs/linux-plus/4-2-1-script-basics/setup.sh b/labs/linux-plus/4-2-1-script-basics/setup.sh new file mode 100644 index 0000000..6d7d503 --- /dev/null +++ b/labs/linux-plus/4-2-1-script-basics/setup.sh @@ -0,0 +1,2 @@ +[ ! -e /root/info.sh ] || refuse "/root/info.sh already exists; move it away first" +true diff --git a/labs/linux-plus/4-2-2-conditionals-and-tests/README.md b/labs/linux-plus/4-2-2-conditionals-and-tests/README.md new file mode 100644 index 0000000..012d4ca --- /dev/null +++ b/labs/linux-plus/4-2-2-conditionals-and-tests/README.md @@ -0,0 +1,39 @@ +# Linux+ 4.2.2: Conditions and tests + +Numeric tests with exit codes, file and string tests, a regular +expression match and a `case` statement. + +## Before you start + +Work as root, so `~` is `/root`. The lab only checks that +`~/diskcheck.sh` doesn't exist yet. Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Write the executable script `~/diskcheck.sh`. It takes a limit in + percent as its first argument (default 80) and reads how full `/` is. + At or above the limit it prints `ALERT: / is N%` and exits 2; at or + above the limit minus 10 it prints `warn: / is N%` and exits 1; + otherwise it prints `ok: / is N%` (exit 0). Run it with no argument and + with `5`. +2. Loop over `/etc/passwd`, `/etc`, `/nope` and `/bin`, and print for each + whether it is a `link`, `dir`, `file` or `missing` (as `PATH: TYPE`, + testing for a link first). Then test an empty variable with `-z` + (printing `empty`), and compare `10` and `9` as strings with `[[ 10 > 9 + ]] || echo "string compare says 10 < 9"`. Save all of the output in + `/root/lab-answers/2`. +3. For `10.0.5.21`, `10.0.5` and `web01`, print `X: ipv4` or `X: not ipv4` + using a regular expression match. Then, for `start`, `stop` and `bogus`, + use a `case` statement to print `X ok` for start and stop and + `X unknown` for anything else. Save all of the output in + `/root/lab-answers/3`. + +## Hints + +- `df --output=pcent / | tail -1 | tr -dc '0-9'`, `[ "$used" -ge "$limit" ]`, + `$((limit - 10))`, `exit 2`. +- `[ -L ]`, `[ -d ]`, `[ -f ]`; `{ ...; } > /root/lab-answers/2` saves the + output of several commands. +- `[[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]`; `case $a in start|stop) ...;; + *) ...;; esac`. diff --git a/labs/linux-plus/4-2-2-conditionals-and-tests/check.sh b/labs/linux-plus/4-2-2-conditionals-and-tests/check.sh new file mode 100644 index 0000000..61b7c7e --- /dev/null +++ b/labs/linux-plus/4-2-2-conditionals-and-tests/check.sh @@ -0,0 +1,27 @@ +D=/root/diskcheck.sh +used=$(df --output=pcent / | tail -1 | tr -dc '0-9') +# run LIMIT: "exit-code first-word" +run() { local out rc; out=$("$D" "$@" 2>/dev/null); rc=$?; echo "$rc ${out%%:*}"; } +has() { grep -qxF -- "$2" "/root/lab-answers/$1"; } + +task 1 "~/diskcheck.sh with limits and exit codes" +need "~/diskcheck.sh is executable" '[ -x "$D" ]' +need "below the limit minus 10 it prints ok and exits 0" '[ "$(run $((used + 11)))" = "0 ok" ]' +need "within 10 of the limit it prints warn and exits 1" '[ "$(run $((used + 5)))" = "1 warn" ]' +need "at the limit it prints ALERT and exits 2" '[ "$(run "$used")" = "2 ALERT" ]' +need "the message names / and the percentage" '"$D" "$used" | grep -qx "ALERT: / is ${used}%"' +need "without an argument the limit is 80" '[ "$(run)" = "$(run 80)" ]' + +task 2 "File and string tests saved" +need "/etc/passwd: file" 'has 2 "/etc/passwd: file"' +need "/etc: dir" 'has 2 "/etc: dir"' +need "/nope: missing" 'has 2 "/nope: missing"' +need "/bin: link" 'has 2 "/bin: link"' +need "empty" 'has 2 empty' +need "string compare says 10 < 9" 'has 2 "string compare says 10 < 9"' + +task 3 "Regex and case output saved" +need "10.0.5.21: ipv4" 'has 3 "10.0.5.21: ipv4"' +need "10.0.5: not ipv4 and web01: not ipv4" 'has 3 "10.0.5: not ipv4" && has 3 "web01: not ipv4"' +need "start ok and stop ok" 'has 3 "start ok" && has 3 "stop ok"' +need "bogus unknown" 'has 3 "bogus unknown"' diff --git a/labs/linux-plus/4-2-2-conditionals-and-tests/lab.conf b/labs/linux-plus/4-2-2-conditionals-and-tests/lab.conf new file mode 100644 index 0000000..3691ad9 --- /dev/null +++ b/labs/linux-plus/4-2-2-conditionals-and-tests/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.2.2 Conditions and tests" +CHAPTER=/linux-plus/4-automation-and-scripting/4-2-2-conditionals-and-tests/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-2-2-conditionals-and-tests/reset.sh b/labs/linux-plus/4-2-2-conditionals-and-tests/reset.sh new file mode 100644 index 0000000..efeb90d --- /dev/null +++ b/labs/linux-plus/4-2-2-conditionals-and-tests/reset.sh @@ -0,0 +1,2 @@ +rm -f /root/diskcheck.sh +true diff --git a/labs/linux-plus/4-2-2-conditionals-and-tests/setup.sh b/labs/linux-plus/4-2-2-conditionals-and-tests/setup.sh new file mode 100644 index 0000000..78501e4 --- /dev/null +++ b/labs/linux-plus/4-2-2-conditionals-and-tests/setup.sh @@ -0,0 +1,2 @@ +[ ! -e /root/diskcheck.sh ] || refuse "/root/diskcheck.sh already exists; move it away first" +true diff --git a/labs/linux-plus/4-2-3-loops-and-functions/README.md b/labs/linux-plus/4-2-3-loops-and-functions/README.md new file mode 100644 index 0000000..232d6a8 --- /dev/null +++ b/labs/linux-plus/4-2-3-loops-and-functions/README.md @@ -0,0 +1,36 @@ +# Linux+ 4.2.3: Loops and functions + +Reading a file line by line into fields, the pipe subshell trap, and a +script with functions and local variables. + +## Before you start + +Work as root, so `~` is `/root`. The lab creates user `alice`, so there is +at least one normal account to find, and checks that `~/hosts.sh` doesn't +exist yet. Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. With a `while IFS=: read -r ...` loop over `/etc/passwd`, print + `USER -> SHELL` for every account with a UID from 1000 up to (not + including) 65534. Save the output in `/root/lab-answers/1`. +2. Count the entries of `ls /etc` twice into a variable `c`: once in a + `while read` loop at the end of a pipe, printing `pipe: $c`, and once + with the loop reading from process substitution, printing + `process substitution: $c`. Save both lines in `/root/lab-answers/2`, + and work out why the first count is 0. +3. Write `~/hosts.sh`: it sets `msg="global"`, has a function `check` with + a local `msg="checking HOST"` that it prints before pinging the host once + (one-second timeout), loops over its arguments printing ` up` or + ` down` after each check, and ends by printing `msg is still: $msg`. Run + it with `localhost 192.0.2.1`. + +## Hints + +- `while IFS=: read -r user _ uid _ _ home shell; do ...; done < /etc/passwd`. +- `ls /etc | while read -r x; do c=$((c+1)); done` runs the loop in a + subshell; `done < <(ls /etc)` doesn't. +- `check() { local msg="checking $1"; ...; }`, `for h in "$@"`, + `check "$h" && echo " up" || echo " down"`. +- The chapter's step 2 (`until` and a second terminal) isn't graded; try + it anyway. diff --git a/labs/linux-plus/4-2-3-loops-and-functions/check.sh b/labs/linux-plus/4-2-3-loops-and-functions/check.sh new file mode 100644 index 0000000..bad00c4 --- /dev/null +++ b/labs/linux-plus/4-2-3-loops-and-functions/check.sh @@ -0,0 +1,17 @@ +expected1() { awk -F: '$3 >= 1000 && $3 < 65534 {print $1 " -> " $7}' /etc/passwd; } +hosts_out() { (cd /tmp && timeout 20 bash /root/hosts.sh localhost 192.0.2.1 2>/dev/null); } + +task 1 "Normal accounts and their shells" +need "/root/lab-answers/1 lists alice -> /bin/bash" 'grep -qx "alice -> /bin/bash" /root/lab-answers/1' +need "it lists every account with a UID from 1000 to 65533, and no others" 'diff <(expected1 | sort) <(grep . /root/lab-answers/1 | sort)' + +task 2 "The pipe subshell trap" +need "/root/lab-answers/2 has pipe: 0" 'grep -qx "pipe: 0" /root/lab-answers/2' +need "it has process substitution: N with the number of entries in /etc" 'n=$(sed -n "s/^process substitution: //p" /root/lab-answers/2); [ -n "$n" ] && [ $((n - $(ls /etc | wc -l))) -le 3 ] && [ $(($(ls /etc | wc -l) - n)) -le 3 ]' + +task 3 "~/hosts.sh with a function and a local variable" +need "~/hosts.sh exists" '[ -f /root/hosts.sh ]' +need "it defines a function with a local variable" 'grep -Eq "local +msg" /root/hosts.sh' +need "it prints checking localhost, then up" 'hosts_out | grep -A1 -x "checking localhost" | grep -qx " up"' +need "it prints checking 192.0.2.1, then down" 'hosts_out | grep -A1 -x "checking 192.0.2.1" | grep -qx " down"' +need "it ends with msg is still: global" '[ "$(hosts_out | tail -n1)" = "msg is still: global" ]' diff --git a/labs/linux-plus/4-2-3-loops-and-functions/lab.conf b/labs/linux-plus/4-2-3-loops-and-functions/lab.conf new file mode 100644 index 0000000..81721af --- /dev/null +++ b/labs/linux-plus/4-2-3-loops-and-functions/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.2.3 Loops and functions" +CHAPTER=/linux-plus/4-automation-and-scripting/4-2-3-loops-and-functions/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-2-3-loops-and-functions/reset.sh b/labs/linux-plus/4-2-3-loops-and-functions/reset.sh new file mode 100644 index 0000000..72aa9b2 --- /dev/null +++ b/labs/linux-plus/4-2-3-loops-and-functions/reset.sh @@ -0,0 +1,2 @@ +rm -f /root/hosts.sh +true diff --git a/labs/linux-plus/4-2-3-loops-and-functions/setup.sh b/labs/linux-plus/4-2-3-loops-and-functions/setup.sh new file mode 100644 index 0000000..0b9b7fb --- /dev/null +++ b/labs/linux-plus/4-2-3-loops-and-functions/setup.sh @@ -0,0 +1,2 @@ +[ ! -e /root/hosts.sh ] || refuse "/root/hosts.sh already exists; move it away first" +user_add alice diff --git a/labs/linux-plus/4-3-1-python-basics/README.md b/labs/linux-plus/4-3-1-python-basics/README.md new file mode 100644 index 0000000..33039aa --- /dev/null +++ b/labs/linux-plus/4-3-1-python-basics/README.md @@ -0,0 +1,36 @@ +# Linux+ 4.3.1: Python for administrators + +A virtual environment with pinned requirements, the chapter's disk report, +running a command from Python and parsing its JSON, and a style check. + +## Before you start + +Work as root, so `~` is `/root`. The VM needs internet access to install +from PyPI. The lab only checks that `~/py` doesn't exist yet. Write answers +to the files in `/root/lab-answers/`. + +## Tasks + +1. In `~/py`, create the virtual environment `.venv` and install `pyyaml` + into it. Load some YAML with it, save the installed packages to + `~/py/requirements.txt` with `pip freeze`, and see that the system's + `python3` can't import `yaml` (unless the distribution's package is + installed). +2. Save the disk report from the chapter as `~/py/disk.py`, run it, and + check its exit code. +3. From Python, run `ip -j addr`, parse the JSON, and print each + interface's name with its list of addresses. Save the output in + `/root/lab-answers/3`. +4. Install `flake8` into the virtual environment and check `~/py/disk.py` + with it until it reports nothing. + +## Hints + +- Ubuntu needs `python3-venv` and `python3-pip` first; `python3 -m venv + .venv && source .venv/bin/activate`, `pip install`, `pip freeze > + requirements.txt`, `deactivate`. +- The report uses `shutil.disk_usage` for `/`, `/var` and `/home`, prints + `MOUNT PERCENT%` lines, and exits 1 if any is at 80% or more. +- `subprocess.run(["ip", "-j", "addr"], capture_output=True, text=True)`, + `json.loads`, `link.get("addr_info", [])`, `a["local"]`. +- `pip install flake8`, `flake8 ~/py/disk.py`. diff --git a/labs/linux-plus/4-3-1-python-basics/check.sh b/labs/linux-plus/4-3-1-python-basics/check.sh new file mode 100644 index 0000000..9b112b7 --- /dev/null +++ b/labs/linux-plus/4-3-1-python-basics/check.sh @@ -0,0 +1,26 @@ +P=/root/py V=/root/py/.venv +# disk.py's exit code should match what the report itself shows. +disk_ok() { + local out rc + out=$(cd /tmp && "$V/bin/python3" "$P/disk.py" 2>/dev/null || python3 "$P/disk.py" 2>/dev/null); rc=$? + grep -Eq '^/ +[0-9]+\.[0-9]%$' <<<"$out" || return 1 + if awk '{sub("%", "", $2); if ($2 + 0 >= 80) bad = 1} END {exit !bad}' <<<"$out"; then [ $rc = 1 ]; else [ $rc = 0 ]; fi +} + +task 1 "~/py/.venv with pyyaml and requirements.txt" +need "~/py/.venv is a virtual environment" '[ -x "$V/bin/python3" ] && [ -f "$V/pyvenv.cfg" ]' +need "yaml imports in it" '"$V/bin/python3" -c "import yaml"' +need "~/py/requirements.txt pins PyYAML" 'grep -qi "^pyyaml==" "$P/requirements.txt"' + +task 2 "~/py/disk.py reports and sets its exit code" +need "~/py/disk.py exists" '[ -f "$P/disk.py" ]' +need "it uses shutil.disk_usage" 'grep -q "disk_usage" "$P/disk.py"' +need "it prints a line for / and exits 0, or 1 when something is 80% full" disk_ok + +task 3 "Interfaces and addresses from ip -j addr" +need "/root/lab-answers/3 lists lo with 127.0.0.1" 'grep -Eq "^lo +\[.*127\.0\.0\.1" /root/lab-answers/3' +need "it lists every interface" 'for i in $(ip -o link show | awk -F": " "{print \$2}" | cut -d@ -f1); do grep -q "^$i " /root/lab-answers/3 || exit 1; done' + +task 4 "disk.py passes flake8" +need "flake8 is installed in ~/py/.venv" '[ -x "$V/bin/flake8" ]' +need "flake8 reports nothing for ~/py/disk.py" '"$V/bin/flake8" "$P/disk.py"' diff --git a/labs/linux-plus/4-3-1-python-basics/lab.conf b/labs/linux-plus/4-3-1-python-basics/lab.conf new file mode 100644 index 0000000..2373574 --- /dev/null +++ b/labs/linux-plus/4-3-1-python-basics/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.3.1 Python for administrators" +CHAPTER=/linux-plus/4-automation-and-scripting/4-3-1-python-basics/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-3-1-python-basics/reset.sh b/labs/linux-plus/4-3-1-python-basics/reset.sh new file mode 100644 index 0000000..5d3eab2 --- /dev/null +++ b/labs/linux-plus/4-3-1-python-basics/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/py +true diff --git a/labs/linux-plus/4-3-1-python-basics/setup.sh b/labs/linux-plus/4-3-1-python-basics/setup.sh new file mode 100644 index 0000000..21673a8 --- /dev/null +++ b/labs/linux-plus/4-3-1-python-basics/setup.sh @@ -0,0 +1,2 @@ +[ ! -e /root/py ] || refuse "/root/py already exists; move it away first" +true diff --git a/labs/linux-plus/4-4-1-git-basics/README.md b/labs/linux-plus/4-4-1-git-basics/README.md new file mode 100644 index 0000000..27dd017 --- /dev/null +++ b/labs/linux-plus/4-4-1-git-basics/README.md @@ -0,0 +1,33 @@ +# Linux+ 4.4.1: Git basics + +Configuring Git, a first repository and commit, ignoring files, staging +and diffs, and a branch. + +## Before you start + +Work as root, so `~` is `/root`. The lab only checks that `~/repo` doesn't +exist yet, and notes whether you already have a `~/.gitconfig` (reset +removes one the lab didn't find). + +## Tasks + +1. Install Git, set your name and email globally and make `main` the + default branch name. Create the repository `~/repo`, and commit an + executable script `hello.sh` that prints `hello` with the message + `Add hello script`. +2. Make Git ignore `*.log` files (create `run.log` to see it), change the + greeting in `hello.sh` to `hello, world`, look at the unstaged and staged + diffs, and commit everything with the message `Greet the world, ignore + logs`. +3. Create a branch `feature`, append a `date` line to `hello.sh` there and + commit it as `Print the date`. Back on `main`, `hello.sh` has no date + line; compare the branches. + +## Hints + +- `git config --global user.name`, `user.email`, `init.defaultBranch`; + `git init`, `git status`, `git add`, `git commit -m`. +- `.gitignore`, `git status --short`, `git diff`, `git add -A`, + `git diff --staged`, `git log --oneline`. +- `git checkout -b feature` (or `git switch -c`), `git commit -am`, + `git log --oneline --graph --all`, `git diff main..feature`. diff --git a/labs/linux-plus/4-4-1-git-basics/check.sh b/labs/linux-plus/4-4-1-git-basics/check.sh new file mode 100644 index 0000000..c0481ff --- /dev/null +++ b/labs/linux-plus/4-4-1-git-basics/check.sh @@ -0,0 +1,23 @@ +g() { git -C /root/repo "$@"; } +gc() { HOME=/root git config --global --get "$1"; } +msg_commit() { g log --format=%H --fixed-strings --grep="$2" "$1" 2>/dev/null | head -n1; } + +task 1 "Git configured, ~/repo with the first commit" +need "git is installed" 'command -v git' +need "your name and email are set globally" '[ -n "$(gc user.name)" ] && [ -n "$(gc user.email)" ]' +need "the default branch name is main" '[ "$(gc init.defaultBranch)" = main ]' +need "~/repo is a repository with a branch main" 'g rev-parse -q --verify refs/heads/main' +need "main has the commit Add hello script" '[ -n "$(msg_commit main "Add hello script")" ]' +need "hello.sh is committed as an executable file" 'g ls-tree "$(msg_commit main "Add hello script")" hello.sh | grep -q "^100755"' + +task 2 "*.log ignored, greeting changed and committed" +need "main has the commit Greet the world, ignore logs" '[ -n "$(msg_commit main "Greet the world, ignore logs")" ]' +need ".gitignore with *.log is committed on main" 'g show main:.gitignore | grep -qxF "*.log"' +need "Git ignores run.log" '[ -e /root/repo/run.log ] && g check-ignore -q run.log && [ -z "$(g ls-files run.log)" ]' +need "hello.sh on main says hello, world" 'g show main:hello.sh | grep -q "hello, world"' + +task 3 "Branch feature prints the date, main doesn't" +need "branch feature exists" 'g rev-parse -q --verify refs/heads/feature' +need "feature has the commit Print the date" '[ -n "$(msg_commit feature "Print the date")" ]' +need "hello.sh on feature has a date line" 'g show feature:hello.sh | grep -qx date' +need "hello.sh on main has no date line" '! g show main:hello.sh | grep -qx date' diff --git a/labs/linux-plus/4-4-1-git-basics/lab.conf b/labs/linux-plus/4-4-1-git-basics/lab.conf new file mode 100644 index 0000000..fcdd071 --- /dev/null +++ b/labs/linux-plus/4-4-1-git-basics/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.4.1 Git basics" +CHAPTER=/linux-plus/4-automation-and-scripting/4-4-1-git-basics/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-4-1-git-basics/reset.sh b/labs/linux-plus/4-4-1-git-basics/reset.sh new file mode 100644 index 0000000..79d227f --- /dev/null +++ b/labs/linux-plus/4-4-1-git-basics/reset.sh @@ -0,0 +1,3 @@ +rm -rf /root/repo +[ -n "$(state_get had-gitconfig)" ] || rm -f /root/.gitconfig +true diff --git a/labs/linux-plus/4-4-1-git-basics/setup.sh b/labs/linux-plus/4-4-1-git-basics/setup.sh new file mode 100644 index 0000000..8d0e144 --- /dev/null +++ b/labs/linux-plus/4-4-1-git-basics/setup.sh @@ -0,0 +1,3 @@ +[ ! -e /root/repo ] || refuse "/root/repo already exists; move it away first" +[ -e /root/.gitconfig ] && state_set had-gitconfig 1 +true diff --git a/labs/linux-plus/4-4-2-git-collaboration/README.md b/labs/linux-plus/4-4-2-git-collaboration/README.md new file mode 100644 index 0000000..f0fe91d --- /dev/null +++ b/labs/linux-plus/4-4-2-git-collaboration/README.md @@ -0,0 +1,42 @@ +# Linux+ 4.4.2: Working with remotes and history + +A bare "server" repository, pushing, cloning, fetching and pulling, a +squash merge, an annotated tag, the stash, and undoing commits. + +## Before you start + +Work as root, so `~` is `/root`. The lab installs Git and creates `~/repo` +as chapter 4.4.1 leaves it: `main` with the commits `Add hello script` +and `Greet the world, ignore logs`, and a branch `feature` with the +commit `Print the date`. If you have no Git identity or default branch +name yet, it sets them in `~/.gitconfig` as chapter 4.4.1 does (reset +removes the file again). It checks that `~/remote.git` and +`~/repo2` don't exist yet. Don't run the chapter's clean-up step: +`le-lab reset` does it. + +## Tasks + +1. Create the bare repository `~/remote.git`, add it to `~/repo` as + `origin`, push `main` and `feature` to it (setting the upstream), and + clone it to `~/repo2`. +2. In `~/repo2`, add a file `README` containing `Lab scripts`, commit it + as `Add a README` and push it. In `~/repo`, fetch, see the new commit + with `git log main..origin/main`, and pull it. +3. In `~/repo` on `main`, squash-merge `feature` and commit it as `Print + the date (squashed)`. Tag that commit as `v1.0` with the message `First + release`. Then append `wip` to `hello.sh`, stash the change, look at the + status, and pop the stash again. +4. Commit everything as `Oops`, then undo that commit with a soft reset + (keeping the change staged), unstage it and throw it away. Finally undo + the squashed commit without rewriting history, with `git revert`. + +## Hints + +- `git init --bare`, `git remote add origin`, `git push -u origin main + feature`, `git clone`. +- `git push`, `git fetch`, `git log --oneline main..origin/main`, + `git pull`. +- `git merge --squash feature`, `git commit -m`, `git tag -a v1.0 -m`, + `git log --oneline --decorate`, `git stash`, `git stash pop`. +- `git commit -am`, `git reset --soft HEAD~1`, `git restore --staged`, + `git restore`, `git revert --no-edit HEAD`. diff --git a/labs/linux-plus/4-4-2-git-collaboration/check.sh b/labs/linux-plus/4-4-2-git-collaboration/check.sh new file mode 100644 index 0000000..dc9687e --- /dev/null +++ b/labs/linux-plus/4-4-2-git-collaboration/check.sh @@ -0,0 +1,32 @@ +g() { git -C /root/repo "$@"; } +r() { git -C /root/remote.git "$@"; } +FEAT=$(state_get feature) +readme_commit() { r log --format=%H --diff-filter=A refs/heads/main -- README 2>/dev/null | tail -n1; } +squash() { g log --format=%H --fixed-strings --grep="Print the date (squashed)" main 2>/dev/null | tail -n1; } + +task 1 "~/remote.git with main and feature, cloned to ~/repo2" +need "~/remote.git is a bare repository" '[ "$(r rev-parse --is-bare-repository)" = true ]' +need "it has branches main and feature" 'r rev-parse -q --verify refs/heads/main && r rev-parse -q --verify refs/heads/feature' +need "feature there is the feature branch from ~/repo" '[ "$(r rev-parse refs/heads/feature)" = "$FEAT" ]' +need "~/repo has origin pointing at ~/remote.git" 'g remote get-url origin | grep -q "remote\.git$"' +need "main in ~/repo tracks origin/main" '[ "$(g rev-parse --abbrev-ref main@{upstream})" = origin/main ]' +need "~/repo2 is a clone of ~/remote.git" 'git -C /root/repo2 remote get-url origin | grep -q "remote\.git$"' + +task 2 "README pushed from ~/repo2 and pulled into ~/repo" +need "~/remote.git's main has a commit adding README" '[ -n "$(readme_commit)" ]' +need "README there says Lab scripts" 'r show refs/heads/main:README | grep -qx "Lab scripts"' +need "main in ~/repo has that commit" 'g merge-base --is-ancestor "$(readme_commit)" main' + +task 3 "Squash-merged feature, tagged v1.0" +need "main has a commit Print the date (squashed)" '[ -n "$(squash)" ]' +need "it's a squash, not a merge (one parent)" '[ "$(g rev-list --parents -n1 "$(squash)" | wc -w)" = 2 ]' +need "it adds the date line" 'g show "$(squash)" -- hello.sh | grep -qx "+date"' +need "feature itself isn't merged into main" '! g merge-base --is-ancestor "$FEAT" main' +need "v1.0 is an annotated tag" '[ "$(g cat-file -t refs/tags/v1.0)" = tag ]' +need "v1.0 points at the squashed commit" '[ "$(g rev-parse "v1.0^{commit}")" = "$(squash)" ]' + +task 4 "Oops undone, squashed commit reverted" +need "main has no commit Oops" '! g log --format=%s main | grep -qx Oops' +need "the latest commit on main reverts the squashed commit" 'g log -1 --format=%B main | grep -q "This reverts commit $(squash)"' +need "hello.sh on main has no date or wip line" '! g show main:hello.sh | grep -Eqx "date|wip"' +need "the working tree is clean" '[ -z "$(g status --porcelain)" ]' diff --git a/labs/linux-plus/4-4-2-git-collaboration/lab.conf b/labs/linux-plus/4-4-2-git-collaboration/lab.conf new file mode 100644 index 0000000..c0371e2 --- /dev/null +++ b/labs/linux-plus/4-4-2-git-collaboration/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.4.2 Working with remotes and history" +CHAPTER=/linux-plus/4-automation-and-scripting/4-4-2-git-collaboration/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-4-2-git-collaboration/reset.sh b/labs/linux-plus/4-4-2-git-collaboration/reset.sh new file mode 100644 index 0000000..254437f --- /dev/null +++ b/labs/linux-plus/4-4-2-git-collaboration/reset.sh @@ -0,0 +1,3 @@ +rm -rf /root/repo /root/repo2 /root/remote.git +[ -n "$(state_get had-gitconfig)" ] || rm -f /root/.gitconfig +true diff --git a/labs/linux-plus/4-4-2-git-collaboration/setup.sh b/labs/linux-plus/4-4-2-git-collaboration/setup.sh new file mode 100644 index 0000000..92fd755 --- /dev/null +++ b/labs/linux-plus/4-4-2-git-collaboration/setup.sh @@ -0,0 +1,22 @@ +for f in /root/repo /root/repo2 /root/remote.git; do [ ! -e "$f" ] || refuse "$f already exists; move it away first"; done +command -v git >/dev/null || pkg_install git +export HOME=/root +[ -e /root/.gitconfig ] && state_set had-gitconfig 1 +git config --global --get user.name >/dev/null || git config --global user.name "Lab User" +git config --global --get user.email >/dev/null || git config --global user.email lab@example.test +# Chapter 4.4.1 sets this too; without it the bare repository's HEAD is +# master, and the clone in task 1 checks out nothing. +git config --global --get init.defaultBranch >/dev/null || git config --global init.defaultBranch main +# ~/repo as chapter 4.4.1 leaves it. +git init -q -b main /root/repo +cd /root/repo +printf '#!/bin/bash\necho hello\n' >hello.sh && chmod +x hello.sh +git add hello.sh && git commit -q -m "Add hello script" +echo '*.log' >.gitignore; touch run.log +sed -i 's/hello/hello, world/' hello.sh +git add -A && git commit -q -m "Greet the world, ignore logs" +git checkout -q -b feature +echo 'date' >>hello.sh && git commit -q -am "Print the date" +state_set feature "$(git rev-parse HEAD)" +git checkout -q main +cd / diff --git a/labs/linux-plus/4-5-1-ai-responsible-use/README.md b/labs/linux-plus/4-5-1-ai-responsible-use/README.md new file mode 100644 index 0000000..f4e22f9 --- /dev/null +++ b/labs/linux-plus/4-5-1-ai-responsible-use/README.md @@ -0,0 +1,33 @@ +# Linux+ 4.5.1: Using AI responsibly + +The review habit, without any AI tool: checking a generated script with +ShellCheck, and rewriting it so it fails safely. + +## Before you start + +Work as root, so `~` is `/root`. The VM needs internet access to install +ShellCheck (from EPEL on RHEL relatives). The lab only checks that +`~/ai-cleanup.sh` and `~/cleanup.sh` don't exist yet. Write answers to the +files in `/root/lab-answers/`. Don't run the chapter's clean-up line: the +lab grades `~/cleanup.sh`, and `le-lab reset` removes the files. + +## Tasks + +1. Save the chapter's "AI-written" `~/ai-cleanup.sh`, and find its problems + before running anything. Install ShellCheck, run it on the script, and + save its report in `/root/lab-answers/1`. +2. Write the safer `~/cleanup.sh`: with `set -euo pipefail`, it takes a + directory and a number of days as required arguments (with a usage + message), refuses a number of days that isn't a number, and deletes + only `*.tar.gz` files directly in that directory that are older than + that many days, printing each one. Test it on throwaway files in + `/tmp/bk`, and make sure ShellCheck has nothing to say about it. + +## Hints + +- `dnf install ShellCheck` (EPEL) or `apt install shellcheck`; + `shellcheck ~/ai-cleanup.sh > /root/lab-answers/1`. +- `${1:?usage: cleanup.sh DIR DAYS}`, `[[ $days =~ ^[0-9]+$ ]]`, + `find "$dir" -maxdepth 1 -type f -name '*.tar.gz' -mtime +"$days" -print + -delete`; `touch -d '20 days ago'`. +- The chapter's step 3 (writing a prompt) isn't graded. diff --git a/labs/linux-plus/4-5-1-ai-responsible-use/check.sh b/labs/linux-plus/4-5-1-ai-responsible-use/check.sh new file mode 100644 index 0000000..7d8f78c --- /dev/null +++ b/labs/linux-plus/4-5-1-ai-responsible-use/check.sh @@ -0,0 +1,24 @@ +C=/root/cleanup.sh +# Run cleanup.sh on a scratch directory: old.tar.gz must go, the rest stay. +trial() { + local t rc + t=$(mktemp -d) + touch -d '20 days ago' "$t/old.tar.gz" "$t/old.txt" + mkdir "$t/sub" && touch -d '20 days ago' "$t/sub/deep.tar.gz" + touch "$t/new.tar.gz" + bash "$C" "$t" 14 >/dev/null 2>&1; rc=$? + [ $rc = 0 ] && [ ! -e "$t/old.tar.gz" ] && [ -e "$t/new.tar.gz" ] && [ -e "$t/old.txt" ] && [ -e "$t/sub/deep.tar.gz" ] + rc=$?; rm -rf "$t"; return $rc +} + +task 1 "ShellCheck's report on ai-cleanup.sh" +need "~/ai-cleanup.sh exists" '[ -f /root/ai-cleanup.sh ] && grep -q "BACKUP_DIR" /root/ai-cleanup.sh' +need "ShellCheck is installed" 'command -v shellcheck' +need "/root/lab-answers/1 has ShellCheck's warnings (unquoted variables, cd without a check)" 'grep -q SC2086 /root/lab-answers/1 && grep -q SC2164 /root/lab-answers/1' + +task 2 "A safe ~/cleanup.sh" +need "~/cleanup.sh exists and uses set -euo pipefail" 'grep -q "set -euo pipefail" "$C"' +need "it deletes only old *.tar.gz files directly in the directory" trial +need "it fails without arguments" '! bash "$C" /dev/null 2>&1' +need "it refuses a number of days that isn't a number" 't=$(mktemp -d); touch -d "20 days ago" "$t/old.tar.gz"; bash "$C" "$t" abc >/dev/null 2>&1; rc=$?; [ -e "$t/old.tar.gz" ]; ok=$?; rm -rf "$t"; [ $rc != 0 ] && [ $ok = 0 ]' +need "ShellCheck has nothing to say about it" 'shellcheck "$C"' diff --git a/labs/linux-plus/4-5-1-ai-responsible-use/lab.conf b/labs/linux-plus/4-5-1-ai-responsible-use/lab.conf new file mode 100644 index 0000000..5054c74 --- /dev/null +++ b/labs/linux-plus/4-5-1-ai-responsible-use/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.5.1 Using AI responsibly" +CHAPTER=/linux-plus/4-automation-and-scripting/4-5-1-ai-responsible-use/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/4-5-1-ai-responsible-use/reset.sh b/labs/linux-plus/4-5-1-ai-responsible-use/reset.sh new file mode 100644 index 0000000..640193b --- /dev/null +++ b/labs/linux-plus/4-5-1-ai-responsible-use/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/ai-cleanup.sh /root/cleanup.sh /tmp/bk +true diff --git a/labs/linux-plus/4-5-1-ai-responsible-use/setup.sh b/labs/linux-plus/4-5-1-ai-responsible-use/setup.sh new file mode 100644 index 0000000..f2722eb --- /dev/null +++ b/labs/linux-plus/4-5-1-ai-responsible-use/setup.sh @@ -0,0 +1,2 @@ +for f in /root/ai-cleanup.sh /root/cleanup.sh; do [ ! -e "$f" ] || refuse "$f already exists; move it away first"; done +true diff --git a/labs/linux-plus/5-1-1-monitoring/README.md b/labs/linux-plus/5-1-1-monitoring/README.md new file mode 100644 index 0000000..01717d0 --- /dev/null +++ b/labs/linux-plus/5-1-1-monitoring/README.md @@ -0,0 +1,35 @@ +# Linux+ 5.1.1: Monitoring + +An SNMP agent queried from another machine, and a health check with a +threshold that logs when it fails. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the Ubuntu VM: query +this VM, `10.99.0.1`, from there. Get a shell there with +`sudo ip netns exec lepeer bash`; it shares this VM's programs, so install +the SNMP client tools on this VM. The lab runs a small web service on +`127.0.0.1:8080`, and makes the journal persistent if it isn't. + +## Tasks + +1. Install and run an SNMP agent on this VM, enabled at boot, with a + read-only community `labro` for the network `10.99.0.0/24`, reachable + from the peer. Open the firewall for it if one is running. From the + peer, walk `1.3.6.1.2.1.1` and get the uptime `1.3.6.1.2.1.1.3.0`. +2. Write the chapter's health-check script as `health.sh` in your home + directory: three tries, `OK` with exit 0 if all succeed, `WARNING` with + exit 1 if some fail, and if all three fail, a `user.crit` log message + `health: URL failing (CODE)`, `CRITICAL` and exit 2. Run it against + `http://127.0.0.1:8080/` (OK) and `http://127.0.0.1:9/` (CRITICAL), + and find the critical message in the journal. + +## Hints + +- RHEL family: `net-snmp`, `net-snmp-utils`, `/etc/snmp/snmpd.conf`. + Ubuntu: `snmpd`, `snmp`; its `snmpd.conf` listens on localhost only + (`agentaddress`). `rocommunity COMMUNITY SOURCE`. +- `firewall-cmd --add-service=snmp` (and `--permanent`), `ufw allow`. +- `snmpwalk -v2c -c labro 10.99.0.1 1.3.6.1.2.1.1`, `snmpget`. +- `curl -s -o /dev/null -w '%{http_code}' --max-time 3`, `logger -p + user.crit`, `journalctl -p crit --since "5 min ago"`. diff --git a/labs/linux-plus/5-1-1-monitoring/check.sh b/labs/linux-plus/5-1-1-monitoring/check.sh new file mode 100644 index 0000000..880bc0b --- /dev/null +++ b/labs/linux-plus/5-1-1-monitoring/check.sh @@ -0,0 +1,25 @@ +START=$(state_get start) +UNIT=$(pick snmpd snmpd) +script() { ls /root/health.sh /home/*/health.sh 2>/dev/null | head -1; } +# run URL: run the student's script, with logger made harmless so the +# check doesn't log a critical message itself; print "EXIT OUTPUT". +run() { + local d o rc; d=$(mktemp -d) + printf '#!/bin/sh\nexit 0\n' >"$d/logger"; chmod 755 "$d/logger" + o=$(PATH="$d:$PATH" timeout 30 bash "$(script)" "$1" 2>/dev/null); rc=$? + rm -rf "$d" + echo "$rc $o" +} +wait_snmp() { local i; for i in 1 2 3 4 5; do in_peer snmpget -v2c -c labro -t 2 -r 0 10.99.0.1 1.3.6.1.2.1.1.3.0 2>/dev/null | grep -qi timeticks && return 0; sleep 2; done; return 1; } + +task 1 "SNMP agent answering the peer with community labro" +need "the SNMP client tools are installed (snmpget)" 'command -v snmpget' +need "snmpd is running and enabled" 'active snmpd && enabled snmpd' +need "snmpd.conf has a read-only community labro for 10.99.0.0/24" 'conf_has /etc/snmp/snmpd.conf "^[[:space:]]*rocommunity[[:space:]]+labro[[:space:]]+10\.99\.0\.0/24"' +need "snmpget from the peer returns the uptime" wait_snmp + +task 2 "health.sh: OK, CRITICAL, and a critical log message" +need "health.sh is in your home directory" '[ -n "$(script)" ]' +need "the journal has its critical message for a failing URL" 'journalctl -q --no-pager -p crit --since "@$START" | grep -q "health: .* failing"' +need "it prints OK and exits 0 for http://127.0.0.1:8080/" '[ "$(run http://127.0.0.1:8080/)" = "0 OK" ]' +need "it prints CRITICAL and exits 2 for http://127.0.0.1:9/" '[ "$(run http://127.0.0.1:9/)" = "2 CRITICAL" ]' diff --git a/labs/linux-plus/5-1-1-monitoring/lab.conf b/labs/linux-plus/5-1-1-monitoring/lab.conf new file mode 100644 index 0000000..3a935e0 --- /dev/null +++ b/labs/linux-plus/5-1-1-monitoring/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.1.1 Monitoring" +CHAPTER=/linux-plus/5-troubleshooting/5-1-1-monitoring/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-1-1-monitoring/reset.sh b/labs/linux-plus/5-1-1-monitoring/reset.sh new file mode 100644 index 0000000..fa7965d --- /dev/null +++ b/labs/linux-plus/5-1-1-monitoring/reset.sh @@ -0,0 +1,23 @@ +systemctl disable --now --quiet le-lab-web.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-web.service +rm -rf /srv/le-web /usr/local/lib/le-lab/httpd.py +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +rm -f /root/health.sh /home/*/health.sh +# SNMP: the agent off and its configuration as it was. +if [ -f "$LAB_STATE/snmpd.conf" ]; then + cp -a "$LAB_STATE/snmpd.conf" /etc/snmp/snmpd.conf +elif [ -f /etc/snmp/snmpd.conf ]; then + sed -i '/labro/d' /etc/snmp/snmpd.conf +fi +[ -n "$(state_get had-snmpd)" ] || systemctl disable --now --quiet snmpd 2>/dev/null +if systemctl is-active --quiet firewalld; then + [ -n "$(state_get snmp-open)" ] || firewall-cmd -q --remove-service=snmp 2>/dev/null + [ -n "$(state_get snmp-open-permanent)" ] || firewall-cmd -q --permanent --remove-service=snmp 2>/dev/null +fi +if [ -n "$(state_get made-journal)" ]; then + journalctl --relinquish-var 2>/dev/null + rm -rf /var/log/journal + systemctl restart systemd-journald +fi +true diff --git a/labs/linux-plus/5-1-1-monitoring/setup.sh b/labs/linux-plus/5-1-1-monitoring/setup.sh new file mode 100644 index 0000000..36b6a12 --- /dev/null +++ b/labs/linux-plus/5-1-1-monitoring/setup.sh @@ -0,0 +1,54 @@ +state_set start "$(date +%s)" +if [ ! -d /var/log/journal ]; then + mkdir -p /var/log/journal + systemd-tmpfiles --create --prefix /var/log/journal + journalctl --flush + state_set made-journal 1 +fi +for f in /etc/snmp/snmpd.conf; do [ -f $f ] && cp -a $f "$LAB_STATE/snmpd.conf"; done +pkg_installed net-snmp && state_set had-snmpd 1 +pkg_installed snmpd && state_set had-snmpd 1 +peer_up + +mkdir -p /srv/le-web /usr/local/lib/le-lab +echo 'Lab web service' >/srv/le-web/index.html +cat >/usr/local/lib/le-lab/httpd.py <<'Y' +# A static web server like "python3 -m http.server PORT --bind ADDRESS +# --directory DIR", without its reverse DNS lookup of the address, which +# can take seconds. +import argparse, functools, http.server, socketserver + +ap = argparse.ArgumentParser() +ap.add_argument("port", type=int) +ap.add_argument("--bind", default="") +ap.add_argument("--directory", default=".") +a = ap.parse_args() + + +class Server(http.server.ThreadingHTTPServer): + def server_bind(self): + socketserver.TCPServer.server_bind(self) + self.server_name, self.server_port = self.server_address[:2] + + +handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=a.directory) +Server((a.bind, a.port), handler).serve_forever() +Y +cat >/etc/systemd/system/le-lab-web.service <<'Y' +[Unit] +Description=Practice lab web service on 127.0.0.1:8080 +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/httpd.py 8080 --bind 127.0.0.1 --directory /srv/le-web + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --now --quiet le-lab-web.service +if systemctl is-active --quiet firewalld; then + firewall-cmd -q --query-service=snmp && state_set snmp-open 1 + firewall-cmd -q --permanent --query-service=snmp && state_set snmp-open-permanent 1 +fi +true diff --git a/labs/linux-plus/5-2-1-boot-and-kernel-issues/README.md b/labs/linux-plus/5-2-1-boot-and-kernel-issues/README.md new file mode 100644 index 0000000..99462c6 --- /dev/null +++ b/labs/linux-plus/5-2-1-boot-and-kernel-issues/README.md @@ -0,0 +1,34 @@ +# Linux+ 5.2.1: Boot, kernel and driver problems + +A service that won't start, a driver that won't load, the GRUB menu, and +the kernels to fall back to. + +## Before you start + +The lab installs nginx and enables it, but its configuration is broken, so +it doesn't start. Something on this VM also stops the `dummy` network +driver from loading. Write answers to the files in `/root/lab-answers/`. +Task 3 needs the VM's console, and a root password for rescue mode. + +## Tasks + +1. nginx failed to start. Find out why from its status, the journal and + nginx's own test, and write the file and line number of the error (as + `FILE:LINE`) to `/root/lab-answers/1`. Fix it so nginx runs. +2. `modprobe dummy` fails. Find what blocks it, write the path of that + file to `/root/lab-answers/2`, and fix it so the driver loads. +3. Practice the GRUB menu: boot once into `rescue.target` by editing the + kernel line, look around with `systemctl list-units --type=target`, + then `systemctl default`. Rescue mode needs a root password: if root is + locked, set one first, and lock it again afterward. Not graded. +4. List the kernels you could fall back to, and write their versions, one + per line, to `/root/lab-answers/4`. + +## Hints + +- `systemctl --failed`, `systemctl status`, `journalctl -u nginx -n 5`, + `nginx -t`. +- `modprobe -v`, `grep -r dummy /etc/modprobe.d/`, `modprobe -c`. +- `e` at the GRUB menu (on Ubuntu, Esc or Shift to show it), + `systemd.unit=rescue.target`, Ctrl+X. +- `grubby --info=ALL | grep ^kernel` (RHEL family) or `ls /boot/vmlinuz-*`. diff --git a/labs/linux-plus/5-2-1-boot-and-kernel-issues/check.sh b/labs/linux-plus/5-2-1-boot-and-kernel-issues/check.sh new file mode 100644 index 0000000..d10311e --- /dev/null +++ b/labs/linux-plus/5-2-1-boot-and-kernel-issues/check.sh @@ -0,0 +1,18 @@ +LINE=$(state_get bad-line) +# The kernels installed, by version. +kernels() { ls /boot/vmlinuz-* | grep -v rescue | sed 's|^/boot/vmlinuz-||'; } + +task 1 "nginx's error found and fixed" +need "/root/lab-answers/1 has the file and line of the error" 'grep -q "nginx\.conf" /root/lab-answers/1 && grep -qE "(:|line )$LINE([^0-9]|$)" /root/lab-answers/1' +need "nginx -t passes" 'nginx -t' +need "nginx is running and enabled" 'active nginx && enabled nginx' + +task 2 "The dummy driver loads again" +need "/root/lab-answers/2 has the file that blocked it" 'grep -q "/etc/modprobe.d/local-hardening.conf" /root/lab-answers/2' +need "no install rule stops it" '! modprobe -c | grep -qE "^install dummy "' +need "modprobe dummy works" 'modprobe dummy' + +task 3 "Booted into rescue.target from the GRUB menu" self + +task 4 "The kernels to fall back to" +need "/root/lab-answers/4 lists every installed kernel" '[ -s /root/lab-answers/4 ] && for k in $(kernels); do grep -qF "$k" /root/lab-answers/4 || exit 1; done' diff --git a/labs/linux-plus/5-2-1-boot-and-kernel-issues/lab.conf b/labs/linux-plus/5-2-1-boot-and-kernel-issues/lab.conf new file mode 100644 index 0000000..5b5150a --- /dev/null +++ b/labs/linux-plus/5-2-1-boot-and-kernel-issues/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.2.1 Boot, kernel and driver problems" +CHAPTER=/linux-plus/5-troubleshooting/5-2-1-boot-and-kernel-issues/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-2-1-boot-and-kernel-issues/reset.sh b/labs/linux-plus/5-2-1-boot-and-kernel-issues/reset.sh new file mode 100644 index 0000000..265b385 --- /dev/null +++ b/labs/linux-plus/5-2-1-boot-and-kernel-issues/reset.sh @@ -0,0 +1,7 @@ +[ -f "$LAB_STATE/nginx.conf" ] && cp -a "$LAB_STATE/nginx.conf" /etc/nginx/nginx.conf +if [ -z "$(state_get nginx-was-active)" ]; then systemctl stop --quiet nginx 2>/dev/null; fi +[ -n "$(state_get nginx-was-enabled)" ] || systemctl disable --quiet nginx 2>/dev/null +systemctl reset-failed nginx 2>/dev/null +rm -f /etc/modprobe.d/local-hardening.conf +modprobe -r dummy 2>/dev/null +true diff --git a/labs/linux-plus/5-2-1-boot-and-kernel-issues/setup.sh b/labs/linux-plus/5-2-1-boot-and-kernel-issues/setup.sh new file mode 100644 index 0000000..e3753df --- /dev/null +++ b/labs/linux-plus/5-2-1-boot-and-kernel-issues/setup.sh @@ -0,0 +1,18 @@ +[ -e /etc/modprobe.d/local-hardening.conf ] && refuse "/etc/modprobe.d/local-hardening.conf already exists; move it away and start again" +pkg_installed nginx && state_set had-nginx 1 +pkg_install nginx +cp -a /etc/nginx/nginx.conf "$LAB_STATE/nginx.conf" +systemctl is-active --quiet nginx && state_set nginx-was-active 1 +systemctl is-enabled --quiet nginx 2>/dev/null && state_set nginx-was-enabled 1 +# Task 1: a bad line. +echo 'this is not valid;' >>/etc/nginx/nginx.conf +state_set bad-line "$(wc -l /dev/null || true +# Task 2: a rule that stops the dummy driver from loading. +modprobe -r dummy 2>/dev/null || true +cat >/etc/modprobe.d/local-hardening.conf <<'Y' +# Local hardening: drivers we don't use. +install dummy /bin/false +Y +true diff --git a/labs/linux-plus/5-2-2-storage-issues/README.md b/labs/linux-plus/5-2-2-storage-issues/README.md new file mode 100644 index 0000000..9504daf --- /dev/null +++ b/labs/linux-plus/5-2-2-storage-issues/README.md @@ -0,0 +1,39 @@ +# Linux+ 5.2.2: Storage problems + +An fstab entry that mounts nothing, a filesystem that's out of inodes, and +a deleted file that still takes up space. + +## Before you start + +The lab adds three spare disks, image files under +`/var/lib/le-lab/disks/` that are attached again at every boot, each with +an ext4 filesystem and a line in `/etc/fstab`: + +- `/dev/loop40`, meant to be mounted on `/mnt/lab`; +- `/dev/loop41`, mounted on `/mnt/inodes`, where an application keeps a + cache (`/mnt/inodes/cache/`, safe to delete) and its data + (`/mnt/inodes/data/`, keep it); +- `/dev/loop42`, mounted on `/mnt/scratch`, used by the service + `report-cache`. + +Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. `/mnt/lab` isn't mounted, though `/etc/fstab` has a line for it. Find + the error, and fix the line so that `mount -a` mounts `/dev/loop40` + there, now and at boot. +2. The application can't create files in `/mnt/inodes/data`, yet `df -h` + shows free space. Find out why, write the resource that ran out to + `/root/lab-answers/2`, and make room without touching `data/`. +3. `/mnt/scratch` is nearly full, but `du` finds almost nothing there. Find + the process that holds the space, write its name to + `/root/lab-answers/3`, and free the space. + +## Hints + +- `findmnt --verify`, `blkid`, `lsblk -f`; `systemctl daemon-reload` + after editing fstab. +- `df -h` versus `df -i`. +- `lsof +L1` (install `lsof` if it's missing), or `ls -l /proc/*/fd` + looking for `(deleted)`; `ps`, `systemctl status`. diff --git a/labs/linux-plus/5-2-2-storage-issues/check.sh b/labs/linux-plus/5-2-2-storage-issues/check.sh new file mode 100644 index 0000000..3fc2b6a --- /dev/null +++ b/labs/linux-plus/5-2-2-storage-issues/check.sh @@ -0,0 +1,17 @@ +U0=$(blkid -s UUID -o value /dev/loop40 2>/dev/null) +held() { find /proc/[0-9]*/fd -lname '/mnt/scratch/*(deleted)' 2>/dev/null | grep -q .; } + +task 1 "/mnt/lab mounted from /dev/loop40 through fstab" +need "fstab's /mnt/lab line uses /dev/loop40's UUID" '[ -n "$U0" ] && findmnt --fstab -n -o SOURCE /mnt/lab | grep -qx "UUID=$U0"' +need "fstab has no line with the bad UUID" '! grep -q "00000000-0000-0000-0000-000000000000" /etc/fstab' +need "/dev/loop40 is mounted on /mnt/lab" '[ "$(findmnt -n -o SOURCE /mnt/lab)" = /dev/loop40 ]' + +task 2 "Room on /mnt/inodes again" +need "/root/lab-answers/2 names the resource" 'grep -qi inode /root/lab-answers/2' +need "/mnt/inodes/data/records.txt is still there" 'grep -q "Customer data" /mnt/inodes/data/records.txt' +need "a new file can be created in /mnt/inodes/data" 'f=/mnt/inodes/data/.le-check; touch $f && rm -f $f' + +task 3 "The space on /mnt/scratch freed" +need "/root/lab-answers/3 names the process" 'grep -Eqi "reportd|report-cache" /root/lab-answers/3' +need "no process holds a deleted file on /mnt/scratch" '! held' +need "/mnt/scratch is mostly free" '[ "$(df --output=pcent /mnt/scratch | tail -1 | tr -dc 0-9)" -lt 50 ]' diff --git a/labs/linux-plus/5-2-2-storage-issues/lab.conf b/labs/linux-plus/5-2-2-storage-issues/lab.conf new file mode 100644 index 0000000..7108d00 --- /dev/null +++ b/labs/linux-plus/5-2-2-storage-issues/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.2.2 Storage problems" +CHAPTER=/linux-plus/5-troubleshooting/5-2-2-storage-issues/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-2-2-storage-issues/reset.sh b/labs/linux-plus/5-2-2-storage-issues/reset.sh new file mode 100644 index 0000000..1c055fc --- /dev/null +++ b/labs/linux-plus/5-2-2-storage-issues/reset.sh @@ -0,0 +1,10 @@ +systemctl stop report-cache.service 2>/dev/null +rm -f /etc/systemd/system/report-cache.service /usr/local/lib/le-lab/reportd.py +rmdir /usr/local/lib/le-lab 2>/dev/null +umount /mnt/lab /mnt/inodes /mnt/scratch 2>/dev/null +# fstab without the lab's three lines. +awk '$2 != "/mnt/lab" && $2 != "/mnt/inodes" && $2 != "/mnt/scratch"' /etc/fstab >"$LAB_STATE/fstab.new" && + cat "$LAB_STATE/fstab.new" >/etc/fstab +systemctl daemon-reload +rmdir /mnt/lab /mnt/inodes /mnt/scratch 2>/dev/null +true diff --git a/labs/linux-plus/5-2-2-storage-issues/setup.sh b/labs/linux-plus/5-2-2-storage-issues/setup.sh new file mode 100644 index 0000000..69fae2c --- /dev/null +++ b/labs/linux-plus/5-2-2-storage-issues/setup.sh @@ -0,0 +1,49 @@ +for m in /mnt/lab /mnt/inodes /mnt/scratch; do + findmnt $m >/dev/null && refuse "something is mounted on $m; unmount it and start again" + findmnt --fstab $m >/dev/null && refuse "/etc/fstab already has a line for $m; remove it and start again" +done +cp -a /etc/fstab "$LAB_STATE/fstab.orig" +d0=$(disk_add 0 256M); d1=$(disk_add 1 64M); d2=$(disk_add 2 256M) +mkfs.ext4 -q -F $d0 +mkfs.ext4 -q -F -N 2048 $d1 +mkfs.ext4 -q -F $d2 +mkdir -p /mnt/lab /mnt/inodes /mnt/scratch +# Task 1: a UUID that doesn't exist. +echo 'UUID=00000000-0000-0000-0000-000000000000 /mnt/lab ext4 defaults,nofail 0 0' >>/etc/fstab +echo "UUID=$(blkid -s UUID -o value $d1) /mnt/inodes ext4 defaults,nofail 0 0" >>/etc/fstab +echo "UUID=$(blkid -s UUID -o value $d2) /mnt/scratch ext4 defaults,nofail 0 0" >>/etc/fstab +systemctl daemon-reload +mount /mnt/inodes; mount /mnt/scratch +[ "$(findmnt -n -o SOURCE /mnt/inodes)" = $d1 ] && [ "$(findmnt -n -o SOURCE /mnt/scratch)" = $d2 ] || + die "the spare disks didn't mount; run le-lab reset" +# Task 2: a cache that uses up every inode. +mkdir -p /mnt/inodes/data /mnt/inodes/cache +echo 'Customer data' >/mnt/inodes/data/records.txt +( cd /mnt/inodes/cache && i=0; while [ $i -lt 5000 ] && touch "c$i" 2>/dev/null; do i=$((i + 1)); done ) || true +# Task 3: a service that holds a deleted 200 MiB file open. +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/reportd.py <<'Y' +# Writes a big cache file, deletes it but keeps it open, and waits. +import os, time +open("/proc/self/comm", "w").write("reportd") +p = "/mnt/scratch/report-cache.bin" +f = open(p, "wb") +for _ in range(200): + f.write(b"\0" * 1048576) +f.flush() +os.unlink(p) +while True: + time.sleep(3600) +Y +cat >/etc/systemd/system/report-cache.service <<'Y' +[Unit] +Description=Report cache +RequiresMountsFor=/mnt/scratch + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/reportd.py +Y +systemctl daemon-reload +systemctl start report-cache.service +sleep 3 +true diff --git a/labs/linux-plus/5-2-3-process-and-system-issues/README.md b/labs/linux-plus/5-2-3-process-and-system-issues/README.md new file mode 100644 index 0000000..559d68f --- /dev/null +++ b/labs/linux-plus/5-2-3-process-and-system-issues/README.md @@ -0,0 +1,38 @@ +# Linux+ 5.2.3: Process, package and access problems + +A segfault and its core dump, the OOM killer in a memory-limited scope, a +stopped process, and the cron PATH problem. + +## Before you start + +The lab makes the journal persistent if it isn't. It starts a service, +`queue-worker`, that adds one to the number in `/var/lib/queue-worker/count` +every second, and creates user `ops`, whose crontab runs a tool from +`/usr/local/sbin` every minute, logging to `/home/ops/labtool.log`. +Something is wrong with both. The stopped process doesn't survive a +reboot: do the tasks and check without rebooting. Write answers to the +files in `/root/lab-answers/`. + +## Tasks + +1. Make a process die of a segmentation fault (`bash -c 'kill -SEGV + $$'`), note its exit code, and find its core dump with `coredumpctl` + (on Ubuntu, install `systemd-coredump` first). +2. Watch the OOM killer act inside a scope limited to 50 MiB of memory + with no swap, and find its message in the kernel log. +3. `queue-worker` is running, but the count doesn't go up. Find out why, + write the state code `ps` shows for its process to + `/root/lab-answers/3`, and get it going again without restarting it. +4. User `ops`'s cron job fails every minute. Find out why from its log, + and fix the crontab entry so that it runs (as user `ops`). Wait for + the next run and see `labtool ran` at the end of the log. + +## Hints + +- `echo $?` (128 + the signal number), `coredumpctl list`, + `coredumpctl info`. +- `systemd-run --scope -p MemoryMax=50M -p MemorySwapMax=0 python3 -c + ...`, `journalctl -k --since "-2min" | grep -i -E 'oom|killed'`. +- `systemctl status`, `ps -o pid,stat,cmd -p PID`, `kill -CONT`. +- `crontab -l -u ops`, `crontab -e -u ops`; cron's `PATH` is short: use + the full path, or set `PATH=` in the crontab. diff --git a/labs/linux-plus/5-2-3-process-and-system-issues/check.sh b/labs/linux-plus/5-2-3-process-and-system-issues/check.sh new file mode 100644 index 0000000..469d3ae --- /dev/null +++ b/labs/linux-plus/5-2-3-process-and-system-issues/check.sh @@ -0,0 +1,19 @@ +START=$(state_get start) +PID=$(state_get worker-pid) +count() { cat /var/lib/queue-worker/count 2>/dev/null; } + +task 1 "A segfault's core dump found" +need "coredumpctl lists a SIGSEGV since the lab started" 'coredumpctl list --no-legend --no-pager --since "@$START" 2>/dev/null | grep -Eq "SEGV|[[:space:]]11[[:space:]]"' + +task 2 "The OOM killer acted in a limited scope" +need "the kernel log shows a process killed for memory since the lab started" 'journalctl -q -k --no-pager --since "@$START" | grep -Eqi "killed process|oom-kill"' + +task 3 "queue-worker going again, without a restart" +need "/root/lab-answers/3 has the state code" 'answer 3 | grep -Eqw "t|stopped"' +need "the worker is the same process the lab started" '[ "$(systemctl show -p MainPID --value queue-worker.service)" = "$PID" ]' +need "it isn't stopped" '! ps -o stat= -p "$PID" | grep -q "^T"' +need "the count goes up" 'a=$(count); sleep 2.5; [ "$(count)" -gt "$a" ]' + +task 4 "ops's cron job runs labtool" +need "ops's crontab still runs labtool every minute" 'crontab -l -u ops 2>/dev/null | grep -Eq "^\* \* \* \* \* .*labtool"' +need "the last line of /home/ops/labtool.log is labtool ran" '[ "$(tail -n1 /home/ops/labtool.log 2>/dev/null)" = "labtool ran" ]' diff --git a/labs/linux-plus/5-2-3-process-and-system-issues/lab.conf b/labs/linux-plus/5-2-3-process-and-system-issues/lab.conf new file mode 100644 index 0000000..45ce1bf --- /dev/null +++ b/labs/linux-plus/5-2-3-process-and-system-issues/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.2.3 Process, package and access problems" +CHAPTER=/linux-plus/5-troubleshooting/5-2-3-process-and-system-issues/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-2-3-process-and-system-issues/reset.sh b/labs/linux-plus/5-2-3-process-and-system-issues/reset.sh new file mode 100644 index 0000000..debb4b7 --- /dev/null +++ b/labs/linux-plus/5-2-3-process-and-system-issues/reset.sh @@ -0,0 +1,15 @@ +pid=$(state_get worker-pid) +[ -n "$pid" ] && kill -CONT "$pid" 2>/dev/null +systemctl stop queue-worker.service 2>/dev/null +rm -f /etc/systemd/system/queue-worker.service /usr/local/lib/le-lab/queue-worker +rm -rf /var/lib/queue-worker +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +crontab -r -u ops 2>/dev/null +rm -f /usr/local/sbin/labtool +if [ -n "$(state_get made-journal)" ]; then + journalctl --relinquish-var 2>/dev/null + rm -rf /var/log/journal + systemctl restart systemd-journald +fi +true diff --git a/labs/linux-plus/5-2-3-process-and-system-issues/setup.sh b/labs/linux-plus/5-2-3-process-and-system-issues/setup.sh new file mode 100644 index 0000000..288bd37 --- /dev/null +++ b/labs/linux-plus/5-2-3-process-and-system-issues/setup.sh @@ -0,0 +1,42 @@ +id ops >/dev/null 2>&1 && refuse "user ops already exists; remove it and start again" +[ -e /usr/local/sbin/labtool ] && refuse "/usr/local/sbin/labtool already exists; remove it and start again" +state_set start "$(date +%s)" +if [ ! -d /var/log/journal ]; then + mkdir -p /var/log/journal + systemd-tmpfiles --create --prefix /var/log/journal + journalctl --flush + state_set made-journal 1 +fi +# Task 3: a worker, stopped. +mkdir -p /usr/local/lib/le-lab /var/lib/queue-worker +cat >/usr/local/lib/le-lab/queue-worker <<'Y' +#!/bin/bash +n=0 +while :; do + n=$((n + 1)) + echo $n >/var/lib/queue-worker/count + sleep 1 +done +Y +chmod 755 /usr/local/lib/le-lab/queue-worker +cat >/etc/systemd/system/queue-worker.service <<'Y' +[Unit] +Description=Queue worker + +[Service] +ExecStart=/usr/local/lib/le-lab/queue-worker +Y +systemctl daemon-reload +systemctl start queue-worker.service +sleep 2 +pid=$(systemctl show -p MainPID --value queue-worker.service) +kill -STOP "$pid" +state_set worker-pid "$pid" +# Task 4: a cron job whose command isn't on cron's PATH. +install -m 755 /dev/stdin /usr/local/sbin/labtool <<'Y' +#!/bin/bash +echo labtool ran +Y +user_add ops +echo '* * * * * labtool >> /home/ops/labtool.log 2>&1' | crontab -u ops - +true diff --git a/labs/linux-plus/5-3-1-interface-and-link-issues/README.md b/labs/linux-plus/5-3-1-interface-and-link-issues/README.md new file mode 100644 index 0000000..f04d0ff --- /dev/null +++ b/labs/linux-plus/5-3-1-interface-and-link-issues/README.md @@ -0,0 +1,37 @@ +# Linux+ 5.3.1: Interface and link problems + +A link that's down, an MTU mismatch, a dummy interface, and an address +conflict. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), is another machine on this VM's +second network, `10.99.0.0/24`; this VM is `10.99.0.1` there, on interface +`le-host`. Get a shell there with `sudo ip netns exec lepeer bash`. The lab +has broken a few things on that network. None of it lasts past a reboot: +do the tasks and check without rebooting. Leave your main interface alone. +Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Read your links' details and counters. This VM can't reach the peer + (`ping 10.99.0.2`): find out why at the link level and fix it. +2. Small pings to the peer work, but big packets never arrive. Find the + largest packet (the MTU, IP header included) that gets from this VM to + the peer with "don't fragment" set, and write it to + `/root/lab-answers/2`. +3. Create a dummy interface `lab0` with address `192.0.2.10/24` and MTU + 1400, bring it up, then take its link down again. Leave it there. +4. You want to add `10.99.0.5` to `le-host`. First check whether another + host on that network already uses it, and write the MAC address of the + host that answers to `/root/lab-answers/4`. + +## Hints + +- `ip -br link`, `ip -br addr`, `ip -s link show`, `ethtool`; `ip link + set DEV up`. +- `ping -c1 -M do -s SIZE` (SIZE + 28 is the packet size), `tracepath -n`. +- `ip link add NAME type dummy`, `ip addr add`, `ip link set NAME up mtu + 1400`, `ip link set NAME down`. +- `arping -D -c 2 -I le-host ADDRESS` (on Ubuntu, install + `iputils-arping`); exit 0 means nobody answered. diff --git a/labs/linux-plus/5-3-1-interface-and-link-issues/check.sh b/labs/linux-plus/5-3-1-interface-and-link-issues/check.sh new file mode 100644 index 0000000..a7010c1 --- /dev/null +++ b/labs/linux-plus/5-3-1-interface-and-link-issues/check.sh @@ -0,0 +1,18 @@ +MAC=$(state_get peer-mac) + +task 1 "le-host up, and the peer reachable" +need "le-host is up" 'ip -o link show le-host | grep -q "<[^>]*\bUP\b"' +need "this VM can ping 10.99.0.2" 'ping -c1 -W2 10.99.0.2' + +task 2 "The path MTU to the peer" +# veth lets frames 4 bytes over the MTU through, so 1400 to 1404 counts. +need "/root/lab-answers/2 has the path MTU" 'n=$(answer 2 | tr -dc 0-9); [ -n "$n" ] && [ "$n" -ge 1400 ] && [ "$n" -le 1404 ]' + +task 3 "Dummy interface lab0, MTU 1400, link down" +need "lab0 exists and is a dummy interface" 'ip -d link show lab0 | grep -qw dummy' +need "it has 192.0.2.10/24" 'ip -4 -o addr show dev lab0 | grep -q " 192\.0\.2\.10/24 "' +need "its MTU is 1400" 'ip -o link show lab0 | grep -q " mtu 1400 "' +need "its link is down" '! ip -o link show lab0 | grep -q "<[^>]*\bUP\b"' + +task 4 "The host that already uses 10.99.0.5" +need "/root/lab-answers/4 has its MAC address" '[ -n "$MAC" ] && grep -qi "$MAC" /root/lab-answers/4' diff --git a/labs/linux-plus/5-3-1-interface-and-link-issues/lab.conf b/labs/linux-plus/5-3-1-interface-and-link-issues/lab.conf new file mode 100644 index 0000000..55b293e --- /dev/null +++ b/labs/linux-plus/5-3-1-interface-and-link-issues/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.3.1 Interface and link problems" +CHAPTER=/linux-plus/5-troubleshooting/5-3-1-interface-and-link-issues/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-3-1-interface-and-link-issues/reset.sh b/labs/linux-plus/5-3-1-interface-and-link-issues/reset.sh new file mode 100644 index 0000000..c692619 --- /dev/null +++ b/labs/linux-plus/5-3-1-interface-and-link-issues/reset.sh @@ -0,0 +1,3 @@ +ip link del lab0 2>/dev/null +ip addr del 10.99.0.5/24 dev le-host 2>/dev/null +true diff --git a/labs/linux-plus/5-3-1-interface-and-link-issues/setup.sh b/labs/linux-plus/5-3-1-interface-and-link-issues/setup.sh new file mode 100644 index 0000000..03bf88a --- /dev/null +++ b/labs/linux-plus/5-3-1-interface-and-link-issues/setup.sh @@ -0,0 +1,10 @@ +ip link show lab0 >/dev/null 2>&1 && refuse "an interface lab0 already exists; delete it and start again" +peer_up +# Task 2: the peer's side takes only 1400-byte packets. +ip -n lepeer link set le-peer mtu 1400 +# Task 4: the peer already uses 10.99.0.5. +ip -n lepeer addr add 10.99.0.5/24 dev le-peer 2>/dev/null || true +state_set peer-mac "$(ip -n lepeer -o link show le-peer | grep -oE 'link/ether [0-9a-f:]+' | cut -d' ' -f2)" +# Task 1: the link is down. +ip link set le-host down +true diff --git a/labs/linux-plus/5-3-2-addressing-routing-dns-issues/README.md b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/README.md new file mode 100644 index 0000000..63a2ab0 --- /dev/null +++ b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/README.md @@ -0,0 +1,38 @@ +# Linux+ 5.3.2: Addressing, routing and DNS problems + +Working through the layers: the route a packet takes, a missing default +route, and a name that resolves to the wrong address. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), is another machine on this VM's +second network, `10.99.0.0/24`; this VM, `10.99.0.1`, is its gateway. Get a +shell there with `sudo ip netns exec lepeer bash`. From the peer, +`198.51.100.9` stands in for an internet address such as `9.9.9.9`. The +peer reads its network settings at boot from `/etc/netns/lepeer/network` +(`ip -batch` commands), so fix that file as well as the running system. +The lab has also changed something on this VM. Task 3 needs the internet. +Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Walk the method on this VM: addresses, routes, the gateway, an internet + address, a name, a port. Write the gateway address and the interface + this VM uses to reach `9.9.9.9` to `/root/lab-answers/1`. +2. The peer can't reach `198.51.100.9`. Find out why from the error, and + fix it, now and at boot. +3. On this VM, `www.kernel.org` resolves to a wrong address for most + programs, but `dig` gets the right one. Find out where the wrong address + comes from, write that source to `/root/lab-answers/3`, and fix it. +4. Compare IPv4 and IPv6 to `www.kernel.org` (`dig AAAA`, `curl -4`, + `curl -6`). Not graded. + +## Hints + +- `ip -br addr`, `ip route`, `ip route get 9.9.9.9`, `ping`, `getent + hosts`, `nc -zv`. +- "Network is unreachable" means no route; `ip route add default via`. + Lines in the peer's file are `ip` commands without the `ip`. +- `getent hosts` follows `/etc/nsswitch.conf`; `dig` asks the DNS server + only (`dnf install bind-utils` or `apt install dnsutils` if it's + missing). diff --git a/labs/linux-plus/5-3-2-addressing-routing-dns-issues/check.sh b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/check.sh new file mode 100644 index 0000000..882f9d2 --- /dev/null +++ b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/check.sh @@ -0,0 +1,18 @@ +R=$(ip -4 route get 9.9.9.9 2>/dev/null) +GW=$(awk '{for (i = 1; i < NF; i++) if ($i == "via") print $(i + 1)}' <<<"$R") +DEV=$(awk '{for (i = 1; i < NF; i++) if ($i == "dev") print $(i + 1)}' <<<"$R") +NET=/etc/netns/lepeer/network + +task 1 "The gateway and interface to 9.9.9.9" +need "/root/lab-answers/1 has the gateway address" '[ -n "$GW" ] && grep -qw "${GW//./\\.}" /root/lab-answers/1' +need "it has the interface" '[ -n "$DEV" ] && grep -qw "$DEV" /root/lab-answers/1' + +task 2 "The peer reaches 198.51.100.9, now and at boot" +need "the peer can ping 198.51.100.9" 'in_peer ping -c1 -W2 198.51.100.9' +need "the peer's default route is via 10.99.0.1" 'in_peer ip route show default | grep -q "via 10\.99\.0\.1 "' +need "$NET adds a default route via 10.99.0.1 and doesn't delete it" 'grep -Eq "^[^#]*route (add|replace|append) default via 10\.99\.0\.1([[:space:]]|$)" $NET && ! grep -Eq "^[^#]*route (del|delete|flush)" $NET' + +task 3 "www.kernel.org resolves correctly again" +need "/root/lab-answers/3 names the source" 'grep -q "/etc/hosts" /root/lab-answers/3' +need "/etc/hosts has no 192.0.2.99 line" '! grep -q "^192\.0\.2\.99[[:space:]]" /etc/hosts' +need "getent hosts www.kernel.org doesn't give 192.0.2.99" '! getent hosts www.kernel.org | grep -q 192\.0\.2\.99' diff --git a/labs/linux-plus/5-3-2-addressing-routing-dns-issues/lab.conf b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/lab.conf new file mode 100644 index 0000000..c73bf85 --- /dev/null +++ b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.3.2 Addressing, routing and DNS problems" +CHAPTER=/linux-plus/5-troubleshooting/5-3-2-addressing-routing-dns-issues/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-3-2-addressing-routing-dns-issues/reset.sh b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/reset.sh new file mode 100644 index 0000000..e8fa43c --- /dev/null +++ b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/reset.sh @@ -0,0 +1,11 @@ +systemctl disable --now --quiet le-lab-peer-net.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-peer-net.service /usr/local/lib/le-lab/peer-net.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +ip link del le-far 2>/dev/null +rm -f /etc/NetworkManager/conf.d/91-le-lab-far.conf +systemctl reload NetworkManager 2>/dev/null +rm -f /etc/netns/lepeer/network +rmdir /etc/netns/lepeer /etc/netns 2>/dev/null +sed -i '/^192\.0\.2\.99[[:space:]].*www\.kernel\.org/d' /etc/hosts +true diff --git a/labs/linux-plus/5-3-2-addressing-routing-dns-issues/setup.sh b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/setup.sh new file mode 100644 index 0000000..2b728cb --- /dev/null +++ b/labs/linux-plus/5-3-2-addressing-routing-dns-issues/setup.sh @@ -0,0 +1,44 @@ +grep -q '^192\.0\.2\.99[[:space:]].*www\.kernel\.org' /etc/hosts && refuse "/etc/hosts already has a 192.0.2.99 line for www.kernel.org; remove it and start again" +peer_up +mkdir -p /etc/netns/lepeer +cat >/etc/netns/lepeer/network <<'Y' +# The peer's network settings, applied inside the peer at every boot with +# "ip -batch" (each line is an ip command without the leading "ip"). +address replace 10.99.0.2/24 dev le-peer +route del default +Y +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/peer-net.sh <<'Y' +#!/bin/sh +# The far side, 198.51.100.9, on this VM; then the peer's own settings. +ip link show le-far >/dev/null 2>&1 || ip link add le-far type dummy +ip addr replace 198.51.100.9/32 dev le-far +ip link set le-far up +ip -n lepeer -force -batch /etc/netns/lepeer/network +true +Y +chmod 755 /usr/local/lib/le-lab/peer-net.sh +if [ -d /etc/NetworkManager/conf.d ]; then + printf '[keyfile]\nunmanaged-devices=interface-name:le-host;interface-name:le-far\n' \ + >/etc/NetworkManager/conf.d/91-le-lab-far.conf + systemctl reload NetworkManager 2>/dev/null || true +fi +cat >/etc/systemd/system/le-lab-peer-net.service <<'Y' +[Unit] +Description=Practice lab: the peer's network settings +Requires=le-lab-peer.service +After=le-lab-peer.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/le-lab/peer-net.sh + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --now --quiet le-lab-peer-net.service +# Task 3: a wrong address for www.kernel.org. +hosts_add "192.0.2.99 www.kernel.org" +true diff --git a/labs/linux-plus/5-4-1-access-issues/README.md b/labs/linux-plus/5-4-1-access-issues/README.md new file mode 100644 index 0000000..b866c35 --- /dev/null +++ b/labs/linux-plus/5-4-1-access-issues/README.md @@ -0,0 +1,36 @@ +# Linux+ 5.4.1: Permission, SELinux and account problems + +A directory that blocks the way, an ACL mask, an attribute that stops +root, and an account that can't log in. + +## Before you start + +The lab creates user `permtest` (password `Lab-Pass-1`) and the files +`/srv/lab/inner/f.txt` and `/srv/lab/inner/old-report.txt`. It installs +the `acl` tools if they're missing. Several things stand in permtest's way. +Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. `permtest` can't read `/srv/lab/inner/f.txt`, although the file itself + is readable by everyone. Find the path component that blocks it, write + it to `/root/lab-answers/1`, and fix it so others can pass through, + without giving them write access. +2. `f.txt` has an ACL entry giving `permtest` read and write access, but + permtest still can't write to it. Find the ACL entry that limits it, + write its name to `/root/lab-answers/2`, and fix it without removing + permtest's entry. +3. Even root can't delete `/srv/lab/inner/old-report.txt`. Find out why, + write the attribute's name to `/root/lab-answers/3`, and delete the + file. +4. `permtest` can't log in at all. Find the two causes (`su - permtest` + and `chage -l permtest` help) and fix them, so that permtest gets a + bash shell again and its account never expires. + +## Hints + +- `namei -l PATH`, `chmod`. +- `getfacl`; the `mask` entry; `setfacl -m m::rw`. +- `lsattr`, `chattr -i`. +- `getent passwd permtest`, `usermod -s /bin/bash`, `chage -l`, + `chage -E -1`. diff --git a/labs/linux-plus/5-4-1-access-issues/check.sh b/labs/linux-plus/5-4-1-access-issues/check.sh new file mode 100644 index 0000000..3a3a329 --- /dev/null +++ b/labs/linux-plus/5-4-1-access-issues/check.sh @@ -0,0 +1,23 @@ +F=/srv/lab/inner/f.txt +as() { runuser -u permtest -- "$@"; } +field() { awk -F: -v n="$1" '$1 == "permtest" {print $n}' /etc/shadow; } + +task 1 "permtest can read f.txt; the blocking directory found" +need "/root/lab-answers/1 names the directory" '[ "$(answer 1 | sed "s|/*$||")" = /srv/lab ]' +need "permtest can read $F" 'as cat '$F' | grep -qx data' +need "others can't write to /srv/lab" '[ $(( 0$(stat -c %a /srv/lab) & 2 )) = 0 ]' + +task 2 "permtest can write to f.txt; the limiting entry found" +need "/root/lab-answers/2 names the entry" 'answer 2 | grep -qw mask' +need "permtest still has its rw ACL entry" 'getfacl -p '$F' 2>/dev/null | grep -q "^user:permtest:rw"' +need "permtest can write to $F" 'as test -w '$F +need "others can't write to it" '[ "$(stat -c %A '$F' | cut -c9)" = - ]' + +task 3 "old-report.txt deleted; the attribute found" +need "/root/lab-answers/3 names the attribute" 'answer 3 | grep -Eqw "immutable|i"' +need "/srv/lab/inner/old-report.txt is gone" '[ ! -e /srv/lab/inner/old-report.txt ]' + +task 4 "permtest can log in again" +need "permtest's shell is bash" '[ "$(getent passwd permtest | cut -d: -f7)" = /bin/bash ] || [ "$(getent passwd permtest | cut -d: -f7)" = /usr/bin/bash ]' +need "its account never expires" '[ -z "$(field 8)" ] || [ "$(field 8)" = -1 ]' +need "su - permtest runs a command" 'su - permtest -c "echo le-ok" /dev/null +rm -rf /srv/lab +true diff --git a/labs/linux-plus/5-4-1-access-issues/setup.sh b/labs/linux-plus/5-4-1-access-issues/setup.sh new file mode 100644 index 0000000..5b1a1ee --- /dev/null +++ b/labs/linux-plus/5-4-1-access-issues/setup.sh @@ -0,0 +1,17 @@ +[ -e /srv/lab ] && refuse "/srv/lab already exists; move it away and start again" +command -v setfacl >/dev/null || pkg_install acl +user_add permtest +mkdir -p /srv/lab/inner +echo data >/srv/lab/inner/f.txt +chmod 644 /srv/lab/inner/f.txt +chmod 700 /srv/lab +# Task 2: rw for permtest, but a mask of r. +setfacl -m u:permtest:rw /srv/lab/inner/f.txt +setfacl -m m::r /srv/lab/inner/f.txt +# Task 3: an immutable file. +echo 'Q3 report, superseded' >/srv/lab/inner/old-report.txt +chattr +i /srv/lab/inner/old-report.txt +# Task 4: no shell, and an expired account. +usermod -s "$(command -v nologin)" permtest +chage -E 0 permtest +true diff --git a/labs/linux-plus/5-4-2-service-and-crypto-issues/README.md b/labs/linux-plus/5-4-2-service-and-crypto-issues/README.md new file mode 100644 index 0000000..a81240d --- /dev/null +++ b/labs/linux-plus/5-4-2-service-and-crypto-issues/README.md @@ -0,0 +1,49 @@ +# Linux+ 5.4.2: Service, certificate and cipher problems + +A service exposed to the network, a certificate for the wrong name, a key +exchange that can't be agreed, and a broken package repository. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for another machine: scan +this VM, `10.99.0.1`, from there (`sudo ip netns exec lepeer bash`; install +`nmap` on this VM). The lab has set up a few broken things: + +- a service, `metrics-exporter`, configured in + `/etc/metrics-exporter.conf`; +- a TLS test server, `le-lab-tls`, on port 8443, with its files in + `/etc/le-lab/tls/`: the lab's own CA (`ca.crt`, `ca.key`) and the + server's certificate and key (`server.crt`, `server.key`); +- something in the SSH client's configuration; +- on the RHEL family, a package repository. + +Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Find what's exposed on this VM, and compare with what the peer sees. + One service listens on all addresses but is only meant for local + monitoring tools: write its TCP port to `/root/lab-answers/1`, and make + it listen on `127.0.0.1` only. +2. `curl --cacert /etc/le-lab/tls/ca.crt --resolve + lab.example.test:8443:127.0.0.1 https://lab.example.test:8443/` fails. + Write curl's exit code and the reason to `/root/lab-answers/2`. Then + issue the server a new certificate for `lab.example.test`, signed by the + lab's CA, and restart `le-lab-tls`, so the command works. +3. `ssh localhost` fails before it even asks how to log in. Find the + setting that causes it, write the file it's in to + `/root/lab-answers/3`, and fix it. Try the chapter's TLS 1.0 test + against `www.kernel.org` too. +4. RHEL family: `dnf makecache` fails. Find the repository that breaks it, + write its ID to `/root/lab-answers/4`, and disable or remove it. + +## Hints + +- `ss -tulpn`; from the peer, `nmap -sV 10.99.0.1`; `systemctl cat`. +- `curl -v`; `openssl x509 -noout -subject -ext subjectAltName`; + `openssl req -new`, `openssl x509 -req -CA -CAkey -CAcreateserial + -extfile` with `subjectAltName=DNS:...`. +- `ssh -v localhost`, `ssh -G localhost | grep kex`, + `/etc/ssh/ssh_config.d/`. +- `dnf repolist`, `/etc/yum.repos.d/`, `enabled=0` or + `dnf config-manager setopt ID.enabled=0`. diff --git a/labs/linux-plus/5-4-2-service-and-crypto-issues/check.sh b/labs/linux-plus/5-4-2-service-and-crypto-issues/check.sh new file mode 100644 index 0000000..86641df --- /dev/null +++ b/labs/linux-plus/5-4-2-service-and-crypto-issues/check.sh @@ -0,0 +1,23 @@ +curl_ok() { curl -s --max-time 5 --cacert /etc/le-lab/tls/ca.crt --resolve lab.example.test:8443:127.0.0.1 https://lab.example.test:8443/ -o /dev/null; } +wait_local() { local i; for i in $(seq 15); do ss -Htln 'sport = :9100' | grep -q '127\.0\.0\.1:9100' && return 0; sleep 1; done; return 1; } + +task 1 "metrics-exporter listens on 127.0.0.1 only" +need "/root/lab-answers/1 has its port" 'grep -qw 9100 /root/lab-answers/1' +need "metrics-exporter is running" active metrics-exporter +need "it listens on 127.0.0.1:9100" wait_local +need "it listens on no other address" '[ "$(ss -Htln "sport = :9100" | grep -vc "127\.0\.0\.1:9100")" = 0 ]' + +task 2 "The server's certificate matches lab.example.test" +need "/root/lab-answers/2 has curl's exit code and the reason" 'grep -qw 60 /root/lab-answers/2 && grep -Eqi "name|subject|match|alt" /root/lab-answers/2' +need "le-lab-tls is running" active le-lab-tls +need "the certificate is signed by the lab's CA" 'openssl verify -CAfile /etc/le-lab/tls/ca.crt /etc/le-lab/tls/server.crt' +need "curl to https://lab.example.test:8443/ with the lab's CA works" 'for i in 1 2 3 4 5; do curl_ok && exit 0; sleep 1; done; exit 1' + +task 3 "ssh localhost agrees on a key exchange again" +need "/root/lab-answers/3 has the file" 'grep -q "/etc/ssh/ssh_config.d/05-legacy-hosts.conf" /root/lab-answers/3' +need "ssh -G localhost doesn't insist on diffie-hellman-group1-sha1" '! ssh -G localhost 2>/dev/null | grep -qx "kexalgorithms diffie-hellman-group1-sha1"' +need "ssh localhost gets past the key exchange" '! timeout 15 ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost true 2>&1 /dev/null | grep -qw internal-tools' diff --git a/labs/linux-plus/5-4-2-service-and-crypto-issues/lab.conf b/labs/linux-plus/5-4-2-service-and-crypto-issues/lab.conf new file mode 100644 index 0000000..9ff74b8 --- /dev/null +++ b/labs/linux-plus/5-4-2-service-and-crypto-issues/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.4.2 Service, certificate and cipher problems" +CHAPTER=/linux-plus/5-troubleshooting/5-4-2-service-and-crypto-issues/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-4-2-service-and-crypto-issues/reset.sh b/labs/linux-plus/5-4-2-service-and-crypto-issues/reset.sh new file mode 100644 index 0000000..92c8d69 --- /dev/null +++ b/labs/linux-plus/5-4-2-service-and-crypto-issues/reset.sh @@ -0,0 +1,11 @@ +systemctl disable --now --quiet metrics-exporter.service le-lab-tls.service 2>/dev/null +rm -f /etc/systemd/system/metrics-exporter.service /etc/systemd/system/le-lab-tls.service +rm -rf /etc/systemd/system/metrics-exporter.service.d /etc/systemd/system/le-lab-tls.service.d +systemctl daemon-reload +rm -f /usr/local/lib/le-lab/metrics-exporter.py /etc/metrics-exporter.conf +rmdir /usr/local/lib/le-lab 2>/dev/null +rm -rf /etc/le-lab/tls +rmdir /etc/le-lab 2>/dev/null +rm -f /etc/ssh/ssh_config.d/05-legacy-hosts.conf +rm -f /etc/yum.repos.d/internal-tools.repo +true diff --git a/labs/linux-plus/5-4-2-service-and-crypto-issues/setup.sh b/labs/linux-plus/5-4-2-service-and-crypto-issues/setup.sh new file mode 100644 index 0000000..4266057 --- /dev/null +++ b/labs/linux-plus/5-4-2-service-and-crypto-issues/setup.sh @@ -0,0 +1,79 @@ +listening 9100 tcp && refuse "something already listens on port 9100, which the lab uses; stop it and start again" +listening 8443 tcp && refuse "something already listens on port 8443, which the lab uses; stop it and start again" +peer_up +mkdir -p /usr/local/lib/le-lab /etc/le-lab/tls + +# Task 1: a service on all addresses. +echo 'LISTEN=0.0.0.0' >/etc/metrics-exporter.conf +cat >/usr/local/lib/le-lab/metrics-exporter.py <<'Y' +# Serves a few made-up metrics on the address in /etc/metrics-exporter.conf. +import http.server, socketserver +addr = "127.0.0.1" +for line in open("/etc/metrics-exporter.conf"): + if line.strip().startswith("LISTEN="): + addr = line.split("=", 1)[1].strip() + + +class H(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.end_headers() + self.wfile.write(b"node_load1 0.42\nnode_users 3\n") + + +class S(http.server.HTTPServer): + allow_reuse_address = True + + def server_bind(self): + socketserver.TCPServer.server_bind(self) + + +S((addr, 9100), H).serve_forever() +Y +cat >/etc/systemd/system/metrics-exporter.service <<'Y' +[Unit] +Description=Metrics exporter for local monitoring +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/metrics-exporter.py + +[Install] +WantedBy=multi-user.target +Y + +# Task 2: a CA, and a server certificate for the wrong name. +cd /etc/le-lab/tls +openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 30 -subj "/CN=Lab Test CA" 2>/dev/null +openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=old.example.test" 2>/dev/null +printf 'subjectAltName=DNS:old.example.test\n' >san.ext +openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 30 -extfile san.ext 2>/dev/null +rm -f server.csr san.ext +chmod 600 ca.key server.key +cd / +cat >/etc/systemd/system/le-lab-tls.service <<'Y' +[Unit] +Description=Practice lab TLS test server on port 8443 +After=network.target + +[Service] +ExecStart=/usr/bin/openssl s_server -accept 8443 -cert /etc/le-lab/tls/server.crt -key /etc/le-lab/tls/server.key -www -quiet + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --now --quiet metrics-exporter.service le-lab-tls.service + +# Task 3: the SSH client insists on a key exchange the server dropped. +cat >/etc/ssh/ssh_config.d/05-legacy-hosts.conf <<'Y' +# Settings for old appliances. +Host localhost 127.0.0.1 + KexAlgorithms diffie-hellman-group1-sha1 +Y + +# Task 4: a repository that doesn't exist. +if is_rhel; then + printf '[internal-tools]\nname=Internal tools\nbaseurl=https://repo.example.invalid/el10/\ngpgcheck=1\n' >/etc/yum.repos.d/internal-tools.repo +fi +true diff --git a/labs/linux-plus/5-5-1-cpu-memory-performance/README.md b/labs/linux-plus/5-5-1-cpu-memory-performance/README.md new file mode 100644 index 0000000..f3d1929 --- /dev/null +++ b/labs/linux-plus/5-5-1-cpu-memory-performance/README.md @@ -0,0 +1,33 @@ +# Linux+ 5.5.1: CPU and memory performance + +A baseline with sar, a CPU bottleneck, a process that switches context all +the time, and a slow boot. + +## Before you start + +The lab installs sysstat with collection off, starts two services that +load this VM in different ways, and adds a unit that slows the boot. +**Reboot once** after setup, so that the last boot includes it. Write +answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Take a baseline (`nproc`, `uptime`, `free -h`, `vmstat 1 3`, `sar -u 1 + 3`), and turn on sysstat's collection so that `sar` has history, now + and at boot. +2. Something keeps a CPU busy. Find the service behind it, write its name + to `/root/lab-answers/2`, and stop it for good. +3. Count context switches per process (`pidstat -w`). One process switches + thousands of times a second while using little CPU: write its name (the + command `pidstat` shows) to `/root/lab-answers/3`. +4. Find the unit that slowed the last boot most, and write its name to + `/root/lab-answers/4`. Look at the critical chain too. + +## Hints + +- `systemctl enable --now sysstat`; on Ubuntu, also `ENABLED="true"` in + `/etc/default/sysstat`. +- `vmstat 1 5`, `uptime`, `mpstat -P ALL 1 1`, `top`; `ps -o unit= -p + PID`; `systemctl disable --now`. +- `pidstat -w 1 3`: the `cswch/s` column. +- `systemd-analyze blame`, `systemd-analyze critical-chain`. diff --git a/labs/linux-plus/5-5-1-cpu-memory-performance/check.sh b/labs/linux-plus/5-5-1-cpu-memory-performance/check.sh new file mode 100644 index 0000000..033b834 --- /dev/null +++ b/labs/linux-plus/5-5-1-cpu-memory-performance/check.sh @@ -0,0 +1,18 @@ +sysstat_on() { + enabled sysstat-collect.timer && active sysstat-collect.timer || return 1 + is_rhel || grep -Eq '^ENABLED="?true' /etc/default/sysstat +} +slowest() { systemd-analyze blame --no-pager 2>/dev/null | awk 'NR == 1 {print $2}'; } + +task 1 "sysstat collection on, now and at boot" +need "sysstat-collect.timer is enabled and running (and ENABLED=true on Ubuntu)" sysstat_on + +task 2 "The CPU hog's service named and stopped" +need "/root/lab-answers/2 names the service" 'answer 2 | grep -q report-builder' +need "report-builder is stopped and disabled" '! active report-builder && ! enabled report-builder' + +task 3 "The process that switches context most" +need "/root/lab-answers/3 names it" 'answer 3 | grep -q msgbroker' + +task 4 "The unit that slowed the last boot most" +need "/root/lab-answers/4 names it" 'a=$(answer 4); s=$(slowest); [ -n "$a" ] && { [[ $a == *cache-prime* ]] || { [ -n "$s" ] && [[ $a == *${s%.service}* ]]; }; }' diff --git a/labs/linux-plus/5-5-1-cpu-memory-performance/lab.conf b/labs/linux-plus/5-5-1-cpu-memory-performance/lab.conf new file mode 100644 index 0000000..601e1fc --- /dev/null +++ b/labs/linux-plus/5-5-1-cpu-memory-performance/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.5.1 CPU and memory performance" +CHAPTER=/linux-plus/5-troubleshooting/5-5-1-cpu-memory-performance/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-5-1-cpu-memory-performance/reset.sh b/labs/linux-plus/5-5-1-cpu-memory-performance/reset.sh new file mode 100644 index 0000000..b89824a --- /dev/null +++ b/labs/linux-plus/5-5-1-cpu-memory-performance/reset.sh @@ -0,0 +1,10 @@ +for u in report-builder message-broker cache-prime; do + systemctl disable --now --quiet "$u.service" 2>/dev/null + rm -rf "/etc/systemd/system/$u.service" "/etc/systemd/system/$u.service.d" +done +systemctl daemon-reload +rm -f /usr/local/lib/le-lab/report-builder /usr/local/lib/le-lab/msgbroker.py +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl disable --quiet --now sysstat.service sysstat-collect.timer sysstat-summary.timer 2>/dev/null +is_debian && sed -i 's/^ENABLED=.*/ENABLED="false"/' /etc/default/sysstat 2>/dev/null +true diff --git a/labs/linux-plus/5-5-1-cpu-memory-performance/setup.sh b/labs/linux-plus/5-5-1-cpu-memory-performance/setup.sh new file mode 100644 index 0000000..7d8d0e7 --- /dev/null +++ b/labs/linux-plus/5-5-1-cpu-memory-performance/setup.sh @@ -0,0 +1,59 @@ +pkg_install sysstat +# Task 1: collection off. +if is_rhel; then + systemctl disable --quiet --now sysstat.service sysstat-collect.timer sysstat-summary.timer 2>/dev/null || true +else + sed -i 's/^ENABLED=.*/ENABLED="false"/' /etc/default/sysstat + systemctl disable --quiet --now sysstat.service sysstat-collect.timer sysstat-summary.timer 2>/dev/null || true +fi +mkdir -p /usr/local/lib/le-lab +# Task 2: a service that keeps one CPU busy. +cat >/usr/local/lib/le-lab/report-builder <<'Y' +#!/bin/bash +while :; do :; done +Y +# Task 3: a process that sleeps very briefly, over and over. +cat >/usr/local/lib/le-lab/msgbroker.py <<'Y' +import time +open("/proc/self/comm", "w").write("msgbroker") +while True: + time.sleep(0.0002) +Y +chmod 755 /usr/local/lib/le-lab/report-builder +cat >/etc/systemd/system/report-builder.service <<'Y' +[Unit] +Description=Nightly report builder + +[Service] +ExecStart=/usr/local/lib/le-lab/report-builder + +[Install] +WantedBy=multi-user.target +Y +cat >/etc/systemd/system/message-broker.service <<'Y' +[Unit] +Description=Message broker + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/msgbroker.py + +[Install] +WantedBy=multi-user.target +Y +# Task 4: a slow unit at boot. +cat >/etc/systemd/system/cache-prime.service <<'Y' +[Unit] +Description=Prime the application cache + +[Service] +Type=oneshot +ExecStart=/bin/sleep 25 +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --quiet cache-prime.service +systemctl enable --quiet --now report-builder.service message-broker.service +true diff --git a/labs/linux-plus/5-5-2-disk-and-network-performance/README.md b/labs/linux-plus/5-5-2-disk-and-network-performance/README.md new file mode 100644 index 0000000..4bb3139 --- /dev/null +++ b/labs/linux-plus/5-5-2-disk-and-network-performance/README.md @@ -0,0 +1,39 @@ +# Linux+ 5.5.2: Disk and network performance + +A process that keeps the disk busy, latency on a link, and throughput +measured with iperf3. + +## Before you start + +The lab installs sysstat and starts a service that writes to disk all the +time. The peer, at `10.99.0.2` (name `peer`), is another machine on this +VM's second network, `10.99.0.0/24`; this VM is `10.99.0.1` there, on +interface `le-host`. Get a shell there with `sudo ip netns exec lepeer +bash`; it shares this VM's programs. The lab has slowed that network down +on purpose. None of the slowdowns lasts past a reboot: do the tasks and +check without rebooting. Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Watch disk latency and utilization with `iostat -xz 1`. Find the + process that keeps writing, write the name of its service to + `/root/lab-answers/1`, and stop it for good. +2. Measure latency and jitter from this VM to the peer with `ping -c 20 + -i 0.2 10.99.0.2`. Write the average round-trip time, in milliseconds, + to `/root/lab-answers/2`. Then find what delays the packets on this + VM's side of the link, and remove it. +3. Install `iperf3`, run a server on this VM (`iperf3 -s`), opening port + 5201/tcp in the firewall if one is running, and measure the throughput + from the peer with `iperf3 -c 10.99.0.1 -t 5`. Write the result, in + Mbit/s, to `/root/lab-answers/3`. Then find what limits it on the + peer's side of the link, remove it, and measure again. + +## Hints + +- `iostat -xz 1` (`%util`, `w_await`), `pidstat -d 1`, `ps -o unit= -p + PID`, `systemctl disable --now`. +- `tc qdisc show dev le-host`, `tc qdisc del dev DEV root`; on the peer, + `tc qdisc show dev le-peer`. +- `firewall-cmd --add-port=5201/tcp` (and `--permanent`), `ufw allow`; + `iperf3 -c ADDRESS -t 5`, `-R` for the other direction. diff --git a/labs/linux-plus/5-5-2-disk-and-network-performance/check.sh b/labs/linux-plus/5-5-2-disk-and-network-performance/check.sh new file mode 100644 index 0000000..2710166 --- /dev/null +++ b/labs/linux-plus/5-5-2-disk-and-network-performance/check.sh @@ -0,0 +1,25 @@ +# mbits: throughput from the peer to an iperf3 server on this VM, in Mbit/s. +mbits() { + local srv='' + if ! listening 5201 tcp; then iperf3 -s -1 -p 5201 >/dev/null 2>&1 & srv=$!; sleep 1; fi + in_peer timeout 15 iperf3 -c 10.99.0.1 -t 2 -J 2>/dev/null | + python3 -c 'import json, sys; print(int(json.load(sys.stdin)["end"]["sum_received"]["bits_per_second"] / 1e6))' + [ -n "$srv" ] && kill $srv 2>/dev/null + true +} +num() { answer "$1" | grep -oE '[0-9]+(\.[0-9]+)?' | head -1 | cut -d. -f1; } + +task 1 "The disk writer's service named and stopped" +need "/root/lab-answers/1 names the service" 'answer 1 | grep -q log-shipper' +need "log-shipper is stopped and disabled" '! active log-shipper && ! enabled log-shipper' + +task 2 "Latency to the peer measured, and the delay removed" +need "/root/lab-answers/2 has the average RTT you measured (70 to 150 ms)" 'n=$(num 2); [ -n "$n" ] && [ "$n" -ge 70 ] && [ "$n" -le 150 ]' +need "le-host has no delay (netem) any more" '! tc qdisc show dev le-host | grep -q netem' +need "ping to the peer takes under 20 ms" 'ping -c3 -i 0.2 -W2 10.99.0.2 | awk -F/ "/^rtt|^round-trip/ {exit !(\$5 < 20)}"' + +task 3 "Throughput measured, and the limit removed" +need "/root/lab-answers/3 has the throughput you measured (20 to 60 Mbit/s)" 'n=$(num 3); [ -n "$n" ] && [ "$n" -ge 20 ] && [ "$n" -le 60 ]' +need "iperf3 is installed" 'command -v iperf3' +need "the peer's le-peer has no rate limit (tbf) any more" '! in_peer tc qdisc show dev le-peer | grep -q tbf' +need "iperf3 from the peer gets over 200 Mbit/s" 'n=$(mbits); [ -n "$n" ] && [ "$n" -gt 200 ]' diff --git a/labs/linux-plus/5-5-2-disk-and-network-performance/lab.conf b/labs/linux-plus/5-5-2-disk-and-network-performance/lab.conf new file mode 100644 index 0000000..17b873f --- /dev/null +++ b/labs/linux-plus/5-5-2-disk-and-network-performance/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.5.2 Disk and network performance" +CHAPTER=/linux-plus/5-troubleshooting/5-5-2-disk-and-network-performance/ +SYSTEMS="rhel debian" diff --git a/labs/linux-plus/5-5-2-disk-and-network-performance/reset.sh b/labs/linux-plus/5-5-2-disk-and-network-performance/reset.sh new file mode 100644 index 0000000..09b28a1 --- /dev/null +++ b/labs/linux-plus/5-5-2-disk-and-network-performance/reset.sh @@ -0,0 +1,12 @@ +systemctl disable --now --quiet log-shipper.service 2>/dev/null +rm -rf /etc/systemd/system/log-shipper.service /etc/systemd/system/log-shipper.service.d /var/lib/log-shipper +rm -f /usr/local/lib/le-lab/log-shipper.py +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +tc qdisc del dev le-host root 2>/dev/null +pkill -x iperf3 2>/dev/null +if systemctl is-active --quiet firewalld; then + [ -n "$(state_get port-open)" ] || firewall-cmd -q --remove-port=5201/tcp 2>/dev/null + [ -n "$(state_get port-open-permanent)" ] || firewall-cmd -q --permanent --remove-port=5201/tcp 2>/dev/null +fi +true diff --git a/labs/linux-plus/5-5-2-disk-and-network-performance/setup.sh b/labs/linux-plus/5-5-2-disk-and-network-performance/setup.sh new file mode 100644 index 0000000..ec4e947 --- /dev/null +++ b/labs/linux-plus/5-5-2-disk-and-network-performance/setup.sh @@ -0,0 +1,35 @@ +pkg_install sysstat +peer_up +mkdir -p /usr/local/lib/le-lab /var/lib/log-shipper +# Task 1: a service that writes small synchronous blocks all the time. +cat >/usr/local/lib/le-lab/log-shipper.py <<'Y' +import os, time +open("/proc/self/comm", "w").write("log-shipper") +fd = os.open("/var/lib/log-shipper/spool.dat", os.O_WRONLY | os.O_CREAT | os.O_DSYNC, 0o600) +block = b"x" * 4096 +n = 0 +while True: + os.pwrite(fd, block, (n % 2048) * 4096) + n += 1 + time.sleep(0.004) +Y +cat >/etc/systemd/system/log-shipper.service <<'Y' +[Unit] +Description=Log shipper + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/log-shipper.py + +[Install] +WantedBy=multi-user.target +Y +systemctl daemon-reload +systemctl enable --now --quiet log-shipper.service +# Task 2: 80 ms of delay on this VM's side; task 3: 40 Mbit/s on the peer's. +tc qdisc replace dev le-host root netem delay 80ms 10ms +ip netns exec lepeer tc qdisc replace dev le-peer root tbf rate 40mbit burst 64kb latency 50ms +if systemctl is-active --quiet firewalld; then + firewall-cmd -q --query-port=5201/tcp && state_set port-open 1 + firewall-cmd -q --permanent --query-port=5201/tcp && state_set port-open-permanent 1 +fi +true diff --git a/labs/lpic-1/101-1-hardware-settings/README.md b/labs/lpic-1/101-1-hardware-settings/README.md new file mode 100644 index 0000000..6d4ea41 --- /dev/null +++ b/labs/lpic-1/101-1-hardware-settings/README.md @@ -0,0 +1,30 @@ +# LPIC-1 101.1: Hardware settings + +Finding the driver behind a device, loading and unloading kernel modules, +and blacklisting a module so it doesn't load at boot. + +This lab runs on the RHEL family only: the Ubuntu cloud kernel doesn't +include the `pcspkr` module, and Ubuntu blacklists it already. + +## Before you start + +The lab installs `pciutils` (for `lspci`) if it's missing, and notes the +current boot so it can tell that you rebooted. Write answers to the files +in `/root/lab-answers/`. + +## Tasks + +1. With `lspci -k`, find the kernel driver in use for the VM's Ethernet + controller, and write its name to `/root/lab-answers/1`. +2. Unload and reload a module that nothing is using, then check `dmesg` + for the messages. `loop` works if `lsmod` lists it with nothing in + "Used by". Check it yourself: the module is back in `lsmod`. +3. Add `blacklist pcspkr` to a file in `/etc/modprobe.d/`, reboot, and + confirm with `lsmod` that it didn't load. Then load it by hand with + `modprobe`. + +## Hints + +- `lspci -k` prints "Kernel driver in use" under each device. +- `lsmod`, `modprobe -r`, `modprobe`, `dmesg | tail`. +- `modprobe --showconfig | grep pcspkr`; `lsmod | grep pcspkr`. diff --git a/labs/lpic-1/101-1-hardware-settings/check.sh b/labs/lpic-1/101-1-hardware-settings/check.sh new file mode 100644 index 0000000..7b76169 --- /dev/null +++ b/labs/lpic-1/101-1-hardware-settings/check.sh @@ -0,0 +1,34 @@ +# The PCI driver and the network driver of the VM's network card. +nic_drivers() { + local i dev + i=$(ip route show default | awk '{for (n = 1; n < NF; n++) if ($n == "dev") {print $(n + 1); exit}}') + [ -n "$i" ] || i=$(ls /sys/class/net | grep -v '^lo$' | head -n1) + dev=$(readlink -f "/sys/class/net/$i/device") + basename "$(readlink -f "$dev/driver")" + basename "$(readlink -f "$dev/../driver")" +} +driver_ok() { + local a d + a=$(answer 1 | tr _ -) + [ -n "$a" ] || return 1 + for d in $(nic_drivers); do [ "$(printf '%s' "$d" | tr _ -)" = "$a" ] && return 0; done + return 1 +} +new_blacklist() { + local f + for f in $(grep -lsE '^[[:space:]]*blacklist[[:space:]]+pcspkr\b' /etc/modprobe.d/*.conf); do + grep -qxF "$f" "$LAB_STATE/old-blacklists" || return 0 + done + return 1 +} + +task 1 "The network card's kernel driver named" +need "/root/lab-answers/1 names the driver lspci -k shows for the Ethernet controller" driver_ok + +task 2 "A module unloaded and loaded again" self + +task 3 "pcspkr blacklisted, the VM rebooted, then pcspkr loaded by hand" +need "a file in /etc/modprobe.d/ blacklists pcspkr" new_blacklist +need "modprobe's configuration includes the blacklist" 'modprobe --showconfig | grep -qx "blacklist pcspkr"' +need "the VM has been rebooted since the lab started" '[ "$(cat /proc/sys/kernel/random/boot_id)" != "$(state_get boot-id)" ]' +need "pcspkr is loaded now (by hand, with modprobe)" 'grep -q "^pcspkr " /proc/modules' diff --git a/labs/lpic-1/101-1-hardware-settings/lab.conf b/labs/lpic-1/101-1-hardware-settings/lab.conf new file mode 100644 index 0000000..4d7416d --- /dev/null +++ b/labs/lpic-1/101-1-hardware-settings/lab.conf @@ -0,0 +1,3 @@ +TITLE="101.1 Hardware settings" +CHAPTER=/lpic-1/101-system-architecture/101-1-hardware-settings/ +SYSTEMS="rhel" diff --git a/labs/lpic-1/101-1-hardware-settings/reset.sh b/labs/lpic-1/101-1-hardware-settings/reset.sh new file mode 100644 index 0000000..78b5873 --- /dev/null +++ b/labs/lpic-1/101-1-hardware-settings/reset.sh @@ -0,0 +1,7 @@ +for f in $(grep -lsE '^[[:space:]]*blacklist[[:space:]]+pcspkr\b' /etc/modprobe.d/*.conf); do + grep -qxF "$f" "$LAB_STATE/old-blacklists" 2>/dev/null && continue + sed -i -E '/^[[:space:]]*blacklist[[:space:]]+pcspkr\b/d' "$f" + [ -s "$f" ] || rm -f "$f" +done +modprobe pcspkr 2>/dev/null +true diff --git a/labs/lpic-1/101-1-hardware-settings/setup.sh b/labs/lpic-1/101-1-hardware-settings/setup.sh new file mode 100644 index 0000000..3884d11 --- /dev/null +++ b/labs/lpic-1/101-1-hardware-settings/setup.sh @@ -0,0 +1,5 @@ +modinfo pcspkr >/dev/null 2>&1 || refuse "this kernel has no pcspkr module, which task 3 needs." +command -v lspci >/dev/null || pkg_install pciutils +state_set boot-id "$(cat /proc/sys/kernel/random/boot_id)" +# Files that already blacklist pcspkr don't count for task 3. +grep -lsE '^[[:space:]]*blacklist[[:space:]]+pcspkr\b' /etc/modprobe.d/*.conf >"$LAB_STATE/old-blacklists" || true diff --git a/labs/lpic-1/101-2-boot-the-system/README.md b/labs/lpic-1/101-2-boot-the-system/README.md new file mode 100644 index 0000000..3c0ae42 --- /dev/null +++ b/labs/lpic-1/101-2-boot-the-system/README.md @@ -0,0 +1,35 @@ +# LPIC-1 101.2: Booting the system + +Booting into a different target from the GRUB menu, keeping the journal +across reboots, and finding out which init the system runs. + +## Before you start + +The lab makes the journal volatile: if `/var/log/journal` exists, it moves +it aside (reset puts it back), so the journal is kept only in memory under +`/run/log/journal` and is lost at every reboot. It also notes the current +boot. Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Boot once with `systemd.unit=rescue.target` added at the GRUB menu, log + in, and check `/proc/cmdline`. Check it yourself: `/proc/cmdline` + showed the option during that boot, and the next normal boot didn't + have it. +2. Make the journal persistent by creating `/var/log/journal`, reboot + twice, then list the boots with `journalctl --list-boots` and read the + previous one with `journalctl -b -1`. +3. Find out which init the VM runs with `ps -p 1 -o comm=`, then look at + `ls -l /sbin/init`. Write the name of PID 1 on line 1 of + `/root/lab-answers/3`, and the file `/sbin/init` points to on line 2. + +## Hints + +- At the GRUB menu, `e` edits the entry for one boot; add the option to + the end of the `linux` line and press Ctrl+X. If the menu doesn't show, + hold Shift or press Esc while the VM starts. +- `mkdir /var/log/journal`, then `systemd-tmpfiles --create --prefix + /var/log/journal` and `journalctl --flush` (or restart + systemd-journald) to start using it without a reboot. +- `journalctl --list-boots`, `journalctl -b -1 -n 20`. +- `ps -p 1 -o comm=`, `ls -l /sbin/init`, `readlink -f /sbin/init`. diff --git a/labs/lpic-1/101-2-boot-the-system/check.sh b/labs/lpic-1/101-2-boot-the-system/check.sh new file mode 100644 index 0000000..20a84a3 --- /dev/null +++ b/labs/lpic-1/101-2-boot-the-system/check.sh @@ -0,0 +1,20 @@ +boots() { journalctl -q --list-boots --no-pager 2>/dev/null | grep -c .; } +init_ok() { + local l1 l2 + l1=$(sed -n 1p /root/lab-answers/3 2>/dev/null | tr -d '[:space:]') + l2=$(sed -n 2p /root/lab-answers/3 2>/dev/null | tr -d '[:space:]') + [ "$l1" = "$(ps -p 1 -o comm= | tr -d ' ')" ] || return 1 + [ -n "$l2" ] && case $(readlink -f /sbin/init) in *"${l2#..}") return 0 ;; esac + [ "$l2" = "$(readlink /sbin/init)" ] +} + +task 1 "Booted once into rescue.target from the GRUB menu" self + +task 2 "The journal kept on disk across reboots" +need "/var/log/journal exists" '[ -d /var/log/journal ]' +need "the journal is written to /var/log/journal" 'ls /var/log/journal/*/system.journal' +need "the VM has been rebooted since the lab started" '[ "$(cat /proc/sys/kernel/random/boot_id)" != "$(state_get boot-id)" ]' +need "journalctl --list-boots shows more than one boot" '[ "$(boots)" -ge 2 ]' + +task 3 "PID 1 and /sbin/init identified" +need "line 1 of /root/lab-answers/3 is PID 1's name, line 2 what /sbin/init points to" init_ok diff --git a/labs/lpic-1/101-2-boot-the-system/lab.conf b/labs/lpic-1/101-2-boot-the-system/lab.conf new file mode 100644 index 0000000..b1c9aa4 --- /dev/null +++ b/labs/lpic-1/101-2-boot-the-system/lab.conf @@ -0,0 +1,3 @@ +TITLE="101.2 Booting the system" +CHAPTER=/lpic-1/101-system-architecture/101-2-boot-the-system/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/101-2-boot-the-system/reset.sh b/labs/lpic-1/101-2-boot-the-system/reset.sh new file mode 100644 index 0000000..c4b9043 --- /dev/null +++ b/labs/lpic-1/101-2-boot-the-system/reset.sh @@ -0,0 +1,21 @@ +journalctl --relinquish-var 2>/dev/null +if [ "$(state_get had-journal)" = yes ] && [ -d "$LAB_STATE/journal-orig" ]; then + # Put the original journal back, keeping what was written since. + mkdir -p /var/log/journal + for m in "$LAB_STATE"/journal-orig/*/; do + [ -d "$m" ] || continue + id=$(basename "$m") + mkdir -p "/var/log/journal/$id" + for f in "$m"*; do + t=/var/log/journal/$id/$(basename "$f") + [ -e "$t" ] && t=/var/log/journal/$id/orig-$(basename "$f") + mv "$f" "$t" + done + done + rm -rf "$LAB_STATE/journal-orig" + systemd-tmpfiles --create --prefix /var/log/journal 2>/dev/null + journalctl --flush 2>/dev/null +elif [ "$(state_get had-journal)" = no ]; then + rm -rf /var/log/journal +fi +true diff --git a/labs/lpic-1/101-2-boot-the-system/setup.sh b/labs/lpic-1/101-2-boot-the-system/setup.sh new file mode 100644 index 0000000..cd8c7c5 --- /dev/null +++ b/labs/lpic-1/101-2-boot-the-system/setup.sh @@ -0,0 +1,13 @@ +case $(systemd-analyze cat-config systemd/journald.conf 2>/dev/null | sed -n 's/^[[:space:]]*Storage=//p' | tail -n1) in +'' | auto) ;; +*) refuse "journald's Storage= is set to something other than auto; set it back to auto (or remove the line), then start the lab again." ;; +esac +state_set boot-id "$(cat /proc/sys/kernel/random/boot_id)" +if [ -d /var/log/journal ]; then + # Stop writing there, then move it aside; the journal stays in /run. + journalctl --relinquish-var + mv /var/log/journal "$LAB_STATE/journal-orig" + state_set had-journal yes +else + state_set had-journal no +fi diff --git a/labs/lpic-1/102-1-disk-layout/README.md b/labs/lpic-1/102-1-disk-layout/README.md new file mode 100644 index 0000000..aa01007 --- /dev/null +++ b/labs/lpic-1/102-1-disk-layout/README.md @@ -0,0 +1,28 @@ +# LPIC-1 102.1: Designing a disk layout + +Building LVM on a spare disk, growing a logical volume and its filesystem +in one step, and adding a swap file. + +## Before you start + +The lab adds an empty 2 GiB spare disk, `/dev/loop40`. It comes back at +every boot, before filesystems are mounted. Before you start, look at the +VM's own layout with `lsblk -f` and `findmnt`, and work out why each +filesystem is where it is. + +## Tasks + +1. On `/dev/loop40`, make a physical volume, a volume group `vg_lab` and a + 1 GiB logical volume `lv_data`, and put a filesystem on it. Then grow + the logical volume and its filesystem by 500 MiB in one step with + `lvextend -r`. +2. Add a 512 MiB swap file, turn it on, and check it with `swapon --show`. + +## Hints + +- `pvcreate`, `vgcreate`, `lvcreate -n lv_data -L 1G vg_lab`, + `mkfs.ext4` or `mkfs.xfs`; `lvs`, `lsblk -f`. +- `lvextend -r -L +500M`. XFS grows only while it's mounted; ext4 grows + either way. +- `fallocate -l 512M` (or `dd`), `chmod 600`, `mkswap`, `swapon`; a line in + `/etc/fstab` keeps it on after a reboot. diff --git a/labs/lpic-1/102-1-disk-layout/check.sh b/labs/lpic-1/102-1-disk-layout/check.sh new file mode 100644 index 0000000..f613265 --- /dev/null +++ b/labs/lpic-1/102-1-disk-layout/check.sh @@ -0,0 +1,29 @@ +lv_mib() { lvs --noheadings --units m --nosuffix -o lv_size vg_lab/lv_data 2>/dev/null | awk '{printf "%d", $1}'; } +# Size of the filesystem on lv_data in MiB: from df if it's mounted, else +# from the superblock. +fs_mib() { + local dev=/dev/mapper/vg_lab-lv_data m b + m=$(findmnt -rn -o TARGET -S "$dev" | head -n1) + if [ -n "$m" ]; then df -BM --output=size "$m" | tail -n1 | tr -dc 0-9; return; fi + b=$(blkid -p -s FSSIZE -o value "$dev" 2>/dev/null) + if [ -z "$b" ] && [ "$(blkid -s TYPE -o value "$dev")" = ext4 ]; then + b=$(dumpe2fs -h "$dev" 2>/dev/null | awk -F: '/^Block count/ {c = $2} /^Block size/ {s = $2} END {print c * s}') + fi + [ -n "$b" ] && echo $((b / 1048576)) +} +# New swap files, with their sizes in bytes. +new_swapfiles() { + swapon --show=NAME,TYPE,SIZE --bytes --noheadings | awk '$2 == "file" {print $1, $3}' | + while read -r f s; do grep -qxF "$f" "$LAB_STATE/old-swaps" || echo "$f $s"; done +} + +task 1 "vg_lab/lv_data on /dev/loop40, grown with its filesystem to 1.5 GiB" +need "/dev/loop40 is a physical volume in vg_lab" '[ "$(pvs --noheadings -o vg_name /dev/loop40 2>/dev/null | tr -d " ")" = vg_lab ]' +need "logical volume vg_lab/lv_data exists" lvs vg_lab/lv_data +need "lv_data holds a filesystem" '[ -n "$(blkid -s TYPE -o value /dev/mapper/vg_lab-lv_data)" ]' +need "lv_data is about 1.5 GiB (1 GiB + 500 MiB)" '[ "$(lv_mib)" -ge 1500 ] && [ "$(lv_mib)" -le 1560 ]' +need "the filesystem has grown with it" '[ "$(fs_mib)" -ge 1400 ]' + +task 2 "A 512 MiB swap file is on" +need "a swap file is on (swapon --show)" '[ -n "$(new_swapfiles)" ]' +need "the swap file is 512 MiB" 'new_swapfiles | awk "\$2 >= 530000000 && \$2 <= 540100000 {ok = 1} END {exit !ok}"' diff --git a/labs/lpic-1/102-1-disk-layout/lab.conf b/labs/lpic-1/102-1-disk-layout/lab.conf new file mode 100644 index 0000000..81fbcb2 --- /dev/null +++ b/labs/lpic-1/102-1-disk-layout/lab.conf @@ -0,0 +1,3 @@ +TITLE="102.1 Designing a disk layout" +CHAPTER=/lpic-1/102-installation-and-packages/102-1-disk-layout/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/102-1-disk-layout/reset.sh b/labs/lpic-1/102-1-disk-layout/reset.sh new file mode 100644 index 0000000..9af220b --- /dev/null +++ b/labs/lpic-1/102-1-disk-layout/reset.sh @@ -0,0 +1,15 @@ +# Swap files the student added: off, out of fstab, deleted. +swapon --show=NAME,TYPE --noheadings | awk '$2 == "file" {print $1}' | while read -r f; do + grep -qxF "$f" "$LAB_STATE/old-swaps" 2>/dev/null && continue + swapoff "$f" + sed -i "\\|^[[:space:]]*$f[[:space:]]|d" /etc/fstab + rm -f "$f" +done +# vg_lab: unmounted and out of fstab; the spare disk goes afterwards. +uuid=$(blkid -s UUID -o value /dev/mapper/vg_lab-lv_data 2>/dev/null) +[ -n "$uuid" ] && sed -i "/$uuid/d" /etc/fstab +sed -i '/vg_lab/d' /etc/fstab +findmnt -rn -o TARGET,SOURCE | awk '$2 ~ /vg_lab/ {print $1}' | sort -r | xargs -r -n1 umount -l +systemctl daemon-reload +vgchange -an vg_lab >/dev/null 2>&1 +true diff --git a/labs/lpic-1/102-1-disk-layout/setup.sh b/labs/lpic-1/102-1-disk-layout/setup.sh new file mode 100644 index 0000000..11b98bc --- /dev/null +++ b/labs/lpic-1/102-1-disk-layout/setup.sh @@ -0,0 +1,2 @@ +disk_add 0 2G >/dev/null +swapon --show=NAME --noheadings >"$LAB_STATE/old-swaps" diff --git a/labs/lpic-1/102-2-boot-manager/README.md b/labs/lpic-1/102-2-boot-manager/README.md new file mode 100644 index 0000000..3ad173b --- /dev/null +++ b/labs/lpic-1/102-2-boot-manager/README.md @@ -0,0 +1,37 @@ +# LPIC-1 102.2: Installing a boot manager + +Changing GRUB's settings, regenerating `grub.cfg`, adding a custom menu +entry, and using the GRUB menu and command line. + +## Before you start + +Take a snapshot first: this lab changes the boot loader's configuration. +The lab saves `/etc/default/grub`, `/etc/default/grub.d/` and +`/etc/grub.d/40_custom`; reset puts them back and regenerates `grub.cfg`. +To see the GRUB menu you need the VM's console, not SSH. + +## Tasks + +1. Change `GRUB_TIMEOUT` to 10, regenerate `grub.cfg`, and reboot to see + it. +2. Add a kernel option at the menu with **e** for one boot, then check + `/proc/cmdline`. Check it yourself: the option shows in `/proc/cmdline` + for that boot only. +3. Copy an entry from `grub.cfg` into `/etc/grub.d/40_custom`, change its + title, regenerate, and boot it. On AlmaLinux, `grub.cfg` has no entries + to copy: write the entry from one of the files in + `/boot/loader/entries/` instead. +4. Open the GRUB command line with **c** and find your kernel with `ls`. + Check it yourself: `ls (hd0,gpt3)/` or similar lists a `vmlinuz` file. + +## Hints + +- `/etc/default/grub`; on Ubuntu cloud images a file in + `/etc/default/grub.d/` sets `GRUB_TIMEOUT` again, after it, and + `GRUB_TIMEOUT_STYLE=hidden` hides the menu. +- `grub2-mkconfig -o /boot/grub2/grub.cfg` on the RHEL family, + `update-grub` on Debian and Ubuntu. +- A `menuentry 'Title' { ... }` block keeps its `linux` and `initrd` + lines; on AlmaLinux they come from the `linux`, `options` and `initrd` + lines of a BLS entry. +- `grub2-reboot` or `grub-reboot` with the title boots an entry once. diff --git a/labs/lpic-1/102-2-boot-manager/check.sh b/labs/lpic-1/102-2-boot-manager/check.sh new file mode 100644 index 0000000..e155753 --- /dev/null +++ b/labs/lpic-1/102-2-boot-manager/check.sh @@ -0,0 +1,33 @@ +cfg=$(pick /boot/grub2/grub.cfg /boot/grub/grub.cfg) +grub_timeout() { + ( + . /etc/default/grub + for f in /etc/default/grub.d/*.cfg; do [ -f "$f" ] && . "$f"; done + printf '%s' "${GRUB_TIMEOUT:-}" + ) 2>/dev/null +} +# Titles of the menu entries in 40_custom. +custom_titles() { sed -n "s/^[[:space:]]*menuentry[[:space:]]*['\"]\\([^'\"]*\\)['\"].*/\\1/p" /etc/grub.d/40_custom 2>/dev/null; } +new_entry() { + local t + while read -r t; do + [ -n "$t" ] || continue + grep -qxF "$t" "$LAB_STATE/old-titles" && continue + grep -qF "menuentry '$t'" "$cfg" || grep -qF "menuentry \"$t\"" "$cfg" || continue + return 0 + done < <(custom_titles) + return 1 +} + +task 1 "GRUB waits 10 seconds" +need "GRUB_TIMEOUT is 10 (after /etc/default/grub.d/ too)" '[ "$(grub_timeout)" = 10 ]' +need "grub.cfg was regenerated with timeout=10" 'grep -Eq "^[[:space:]]*set timeout=10\b" "$cfg"' + +task 2 "A kernel option added for one boot" self + +task 3 "A custom menu entry with a new title" +need "/etc/grub.d/40_custom has a menuentry" '[ -n "$(custom_titles)" ]' +need "the entry has a linux line" 'grep -Eq "^[[:space:]]*linux(16|efi)?[[:space:]]+/" /etc/grub.d/40_custom' +need "grub.cfg was regenerated with the entry under a new title" new_entry + +task 4 "Kernel found from the GRUB command line" self diff --git a/labs/lpic-1/102-2-boot-manager/lab.conf b/labs/lpic-1/102-2-boot-manager/lab.conf new file mode 100644 index 0000000..be57ea6 --- /dev/null +++ b/labs/lpic-1/102-2-boot-manager/lab.conf @@ -0,0 +1,3 @@ +TITLE="102.2 Installing a boot manager" +CHAPTER=/lpic-1/102-installation-and-packages/102-2-boot-manager/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/102-2-boot-manager/reset.sh b/labs/lpic-1/102-2-boot-manager/reset.sh new file mode 100644 index 0000000..d854edc --- /dev/null +++ b/labs/lpic-1/102-2-boot-manager/reset.sh @@ -0,0 +1,11 @@ +if [ -f "$LAB_STATE/grub-config.tar" ]; then + rm -rf /etc/default/grub.d + if tar -tf "$LAB_STATE/grub-config.tar" | grep -q '^etc/grub.d/40_custom$'; then :; else + # 40_custom didn't exist; nothing to restore over it. + rm -f /etc/grub.d/40_custom + fi + tar -C / -xpf "$LAB_STATE/grub-config.tar" + if is_rhel; then grub2-mkconfig -o /boot/grub2/grub.cfg >/dev/null 2>&1 + else update-grub >/dev/null 2>&1; fi +fi +true diff --git a/labs/lpic-1/102-2-boot-manager/setup.sh b/labs/lpic-1/102-2-boot-manager/setup.sh new file mode 100644 index 0000000..0d65111 --- /dev/null +++ b/labs/lpic-1/102-2-boot-manager/setup.sh @@ -0,0 +1,12 @@ +cfg=$(pick /boot/grub2/grub.cfg /boot/grub/grub.cfg) +[ -f "$cfg" ] || refuse "no $cfg; this lab needs GRUB 2." +[ -f /etc/default/grub ] || refuse "no /etc/default/grub; this lab needs GRUB 2." +if grep -Eq '^[[:space:]]*set timeout=10\b' "$cfg"; then + refuse "grub.cfg already waits 10 seconds; set GRUB_TIMEOUT to something else first." +fi +tar -C / -cf "$LAB_STATE/grub-config.tar" etc/default/grub $( [ -d /etc/default/grub.d ] && echo etc/default/grub.d ) $( [ -f /etc/grub.d/40_custom ] && echo etc/grub.d/40_custom ) +# Titles that exist already, from grub.cfg and BLS entries. +{ + sed -n "s/^[[:space:]]*menuentry[[:space:]]*['\"]\\([^'\"]*\\)['\"].*/\\1/p" "$cfg" + sed -n 's/^title[[:space:]]*//p' /boot/loader/entries/*.conf 2>/dev/null || true +} >"$LAB_STATE/old-titles" diff --git a/labs/lpic-1/102-3-shared-libraries/README.md b/labs/lpic-1/102-3-shared-libraries/README.md new file mode 100644 index 0000000..ace33e7 --- /dev/null +++ b/labs/lpic-1/102-3-shared-libraries/README.md @@ -0,0 +1,36 @@ +# LPIC-1 102.3: Shared libraries + +Finding the libraries a program needs and the packages they came from, +the three kinds of library name, and adding a directory to the linker's +search path. + +## Before you start + +The lab installs the SSH client if it's missing. Write answers to the +files in `/root/lab-answers/`. + +## Tasks + +1. Run `ldd` on `/usr/bin/ls`, `/usr/bin/ssh` and `/usr/bin/bash`. Pick + one library from each and find which package installed it. Write one + line per program to `/root/lab-answers/1`: the program, the library + and the package, such as `ls libselinux.so.1 libselinux`. +2. Look at `ls -l` of `libc.so*` and `libz.so*` in your library directory + and name each kind of name. Write zlib's soname on line 1 of + `/root/lab-answers/2`, and its real name (the file the soname points + to) on line 2. +3. Make an empty directory `/opt/test/lib`, add it to a file in + `/etc/ld.so.conf.d/`, run `ldconfig -v | grep /opt` and see it scanned. + Then remove the file and run `ldconfig` again. Check it yourself: + `ldconfig -v 2>/dev/null | grep /opt` prints the directory while the + file is there, and nothing after. + +## Hints + +- `rpm -qf FILE` on the RHEL family, `dpkg -S FILE` on Debian and Ubuntu. + If `dpkg -S` finds nothing for a path under `/lib`, try it under + `/usr/lib` (or the other way round). +- The library directory is `/usr/lib64` on the RHEL family and + `/usr/lib/x86_64-linux-gnu` on Debian and Ubuntu; `ldconfig -p | grep libz`. +- Real name `libz.so.1.3`, soname `libz.so.1`, linker name `libz.so`; + `readlink`, `objdump -p FILE | grep SONAME`. diff --git a/labs/lpic-1/102-3-shared-libraries/check.sh b/labs/lpic-1/102-3-shared-libraries/check.sh new file mode 100644 index 0000000..dfb9886 --- /dev/null +++ b/labs/lpic-1/102-3-shared-libraries/check.sh @@ -0,0 +1,40 @@ +# The package names that own a library file, given as it appears in ldd. +owners() { + local p=$1 c + for c in "$p" "$(readlink -f "$p")" "/usr${p#/usr}" "${p#/usr}"; do + [ -n "$c" ] || continue + if is_rhel; then rpm -qf --qf '%{NAME}\n' "$c" 2>/dev/null | grep -v 'not owned' + else dpkg -S "$c" 2>/dev/null | cut -d: -f1 | tr ',' '\n' | tr -d ' '; fi + done | sort -u +} +# lib_line PROGRAM: answer 1 has a line for it with a library it uses and +# the package that owns that library. +lib_line() { + local prog=$1 line lib pkg path o + while read -r line; do + set -- $line + [ "$(basename "${1:-}")" = "$prog" ] || continue + lib=$(basename "${2:-}") pkg=${3:-} + pkg=${pkg%%:*} + [ -n "$lib" ] && [ -n "$pkg" ] || continue + path=$(ldd "/usr/bin/$prog" | awk -v l="$lib" '$1 == l && $3 ~ /^\// {print $3} $1 ~ "/"l"$" {print $1}' | head -n1) + [ -n "$path" ] || continue + for o in $(owners "$path"); do + case $pkg in "$o" | "$o"-[0-9]*) return 0 ;; esac + done + done /dev/null | xargs -r rm -f +rm -rf /opt/test +ldconfig +true diff --git a/labs/lpic-1/102-3-shared-libraries/setup.sh b/labs/lpic-1/102-3-shared-libraries/setup.sh new file mode 100644 index 0000000..5d5945a --- /dev/null +++ b/labs/lpic-1/102-3-shared-libraries/setup.sh @@ -0,0 +1,2 @@ +command -v ssh >/dev/null || pkg_install openssh-clients/openssh-client +true diff --git a/labs/lpic-1/102-4-debian-packages/README.md b/labs/lpic-1/102-4-debian-packages/README.md new file mode 100644 index 0000000..941ce49 --- /dev/null +++ b/labs/lpic-1/102-4-debian-packages/README.md @@ -0,0 +1,39 @@ +# LPIC-1 102.4: Debian package management + +Installing, inspecting, removing and purging packages, finding which +package owns a file, installing a downloaded `.deb`, and reconfiguring a +package. + +## Before you start + +The lab makes sure `cowsay` and `hello` aren't installed, and sets the +time zone to UTC if it's Europe/Berlin already (reset puts the old one +back). Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Install `cowsay` with `apt-get`, list its files with `dpkg -L`, check + its status with `dpkg -s`, then remove it and purge it, checking + `dpkg -l` after each step. +2. Find which package provides `/usr/bin/ls`, then try `/bin/ls` and see + why it fails. Then use `apt-file` to find which package would provide + the `ab` command, which you don't have yet. Write the package that owns + `/usr/bin/ls` on line 1 of `/root/lab-answers/2`, and the package with + `ab` on line 2. +3. Download the `hello` package with `apt-get download` in `/root`, list + its contents with `dpkg -c`, and install it with `dpkg -i`. +4. Run `dpkg-reconfigure tzdata` and change the time zone to + Europe/Berlin. + +## Hints + +- `dpkg -l cowsay` shows `ii` while it's installed. A package with + configuration files shows `rc` after `apt-get remove` and disappears + with `apt-get purge`; `cowsay` has none (`dpkg -s` lists no + `Conffiles:`), so it's gone after `remove` and `purge` has nothing left + to do. +- `dpkg -S`; `/bin` is a symlink to `usr/bin`, and the package lists the + file only under `/usr/bin`. `apt-get install apt-file`, `apt-file + update`, `apt-file search bin/ab`. +- `apt-get download hello`, `dpkg -c`, `dpkg -i`. +- `timedatectl`, `ls -l /etc/localtime`, `cat /etc/timezone`. diff --git a/labs/lpic-1/102-4-debian-packages/check.sh b/labs/lpic-1/102-4-debian-packages/check.sh new file mode 100644 index 0000000..0afae0e --- /dev/null +++ b/labs/lpic-1/102-4-debian-packages/check.sh @@ -0,0 +1,25 @@ +# dpkg_did ACTION PACKAGE: dpkg's log shows the action since setup. +dpkg_did() { + local since; since=$(state_get since) + cat /var/log/dpkg.log 2>/dev/null | awk -v s="$since" -v a="$1" -v p="$2" \ + '($1 " " $2) >= s && $3 == a && ($4 == p || index($4, p ":") == 1) {ok = 1} END {exit !ok}' +} +gone() { ! dpkg-query -W -f '${Status}' "$1" 2>/dev/null | grep -Eq 'installed|config-files'; } + +task 1 "cowsay installed, then removed (and purged)" +need "dpkg's log shows cowsay installed" 'dpkg_did install cowsay' +need "dpkg's log shows cowsay removed" 'dpkg_did remove cowsay' +need "cowsay is gone, configuration files too (dpkg -l)" 'gone cowsay' + +task 2 "Owner of /usr/bin/ls and provider of ab found" +need "line 1 of /root/lab-answers/2 is the package that owns /usr/bin/ls" '[ "$(sed -n 1p /root/lab-answers/2 | tr -d "[:space:]" | cut -d: -f1)" = "$(dpkg -S /usr/bin/ls | cut -d: -f1)" ]' +need "line 2 is the package that provides the ab command" '[ "$(sed -n 2p /root/lab-answers/2 | tr -d "[:space:]" | cut -d: -f1)" = apache2-utils ]' + +task 3 "hello downloaded and installed with dpkg -i" +need "a hello .deb was downloaded to /root" 'ls /root/hello_*.deb' +need "dpkg -c lists the .deb" 'dpkg -c /root/hello_*.deb | grep -q /usr/bin/hello' +need "hello is installed" 'dpkg-query -W -f "\${Status}" hello | grep -q "ok installed"' + +task 4 "Time zone Europe/Berlin" +need "/etc/localtime points to Europe/Berlin" 'readlink -f /etc/localtime | grep -q "/Europe/Berlin$"' +need "/etc/timezone says Europe/Berlin" 'grep -qx Europe/Berlin /etc/timezone' diff --git a/labs/lpic-1/102-4-debian-packages/lab.conf b/labs/lpic-1/102-4-debian-packages/lab.conf new file mode 100644 index 0000000..25f9fcb --- /dev/null +++ b/labs/lpic-1/102-4-debian-packages/lab.conf @@ -0,0 +1,3 @@ +TITLE="102.4 Debian package management" +CHAPTER=/lpic-1/102-installation-and-packages/102-4-debian-packages/ +SYSTEMS="debian" diff --git a/labs/lpic-1/102-4-debian-packages/reset.sh b/labs/lpic-1/102-4-debian-packages/reset.sh new file mode 100644 index 0000000..9232b15 --- /dev/null +++ b/labs/lpic-1/102-4-debian-packages/reset.sh @@ -0,0 +1,12 @@ +for p in cowsay hello; do + dpkg-query -W -f '${Status}' "$p" 2>/dev/null | grep -Eq 'installed|config-files' && + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge "$p" >/dev/null 2>&1 +done +rm -f /root/hello_*.deb +tz=$(state_get tz) +if [ -n "$tz" ] && [ -e "/usr/share/zoneinfo/$tz" ]; then + ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime + echo "$tz" >/etc/timezone + DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive tzdata >/dev/null 2>&1 +fi +true diff --git a/labs/lpic-1/102-4-debian-packages/setup.sh b/labs/lpic-1/102-4-debian-packages/setup.sh new file mode 100644 index 0000000..21a6dcb --- /dev/null +++ b/labs/lpic-1/102-4-debian-packages/setup.sh @@ -0,0 +1,7 @@ +for p in cowsay hello; do + pkg_installed "$p" && DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge "$p" >/dev/null +done +state_set tz "$(timedatectl show -p Timezone --value)" +[ "$(state_get tz)" = Europe/Berlin ] && timedatectl set-timezone Etc/UTC +# Task 1 and 3 look in dpkg's log from here on. +state_set since "$(date '+%Y-%m-%d %H:%M:%S')" diff --git a/labs/lpic-1/102-5-rpm-and-yum/README.md b/labs/lpic-1/102-5-rpm-and-yum/README.md new file mode 100644 index 0000000..3538005 --- /dev/null +++ b/labs/lpic-1/102-5-rpm-and-yum/README.md @@ -0,0 +1,34 @@ +# LPIC-1 102.5: RPM, YUM and Zypper + +Querying and verifying installed packages, looking inside a downloaded +package, and undoing an install with the DNF history. (The chapter's +Zypper task needs openSUSE, so it isn't part of this lab.) + +## Before you start + +The lab adds a comment line to `/etc/ssh/sshd_config` (reset takes it out +again), and notes where the DNF history stands. Write answers to the files +in `/root/lab-answers/`. + +## Tasks + +1. Find which package owns `/etc/ssh/sshd_config`, list its configuration + files, then verify the package with `rpm -V`. Write the package name on + line 1 of `/root/lab-answers/1`, and below it every file `rpm -V` + reports as changed, one per line. +2. Download the `tree` package with `dnf download` into `/root/rpms`, + query it with `rpm -qpi` and `rpm -qpl`, and extract one file, + `usr/bin/tree`, with `rpm2cpio` and `cpio`, so that it ends up as + `/root/rpms/usr/bin/tree`. +3. Use `dnf provides` to find the package for the `dig` command, which you + don't have, and write its name to `/root/lab-answers/3`. Install it, + then undo the install with `dnf history undo last`. + +## Hints + +- `rpm -qf`, `rpm -qc`, `rpm -V`; the letters say what changed (`5` the + checksum, `S` the size, `T` the time), `c` marks a configuration file. +- `dnf download --destdir`, `rpm -qpi`, `rpm -qpl`; `rpm2cpio FILE.rpm | + cpio -idv ./usr/bin/tree`, run inside `/root/rpms`. +- `dnf provides dig` downloads the file lists first, which can take a + minute. `dnf history`, `dnf history info last`. diff --git a/labs/lpic-1/102-5-rpm-and-yum/check.sh b/labs/lpic-1/102-5-rpm-and-yum/check.sh new file mode 100644 index 0000000..fe6daf6 --- /dev/null +++ b/labs/lpic-1/102-5-rpm-and-yum/check.sh @@ -0,0 +1,37 @@ +verify_listed() { + local f n=0 + for f in $(rpm -V openssh-server 2>/dev/null | awk '{print $NF}'); do + grep -qE "(^|[[:space:]])$f([[:space:]]|$)" /root/lab-answers/1 || return 1 + n=$((n + 1)) + done + [ "$n" -gt 0 ] +} +# Transactions since setup: one installed bind-utils, a later one (an +# undo) removed it. +undone() { + local start id inst='' + start=$(state_get history); start=${start:-0} + for id in $(dnf -q history list 2>/dev/null | awk -v s="$start" '$1 ~ /^[0-9]+$/ && $1 > s {print $1}' | sort -n); do + info=$(LC_ALL=C dnf -q history info "$id" 2>/dev/null) + if [ -z "$inst" ] && printf '%s\n' "$info" | grep -Eq '^[[:space:]]+Install[[:space:]]+bind-utils-'; then + inst=$id + elif [ -n "$inst" ] && printf '%s\n' "$info" | grep -Eq '^[[:space:]]+(Removed|Erase|Remove)[[:space:]]+bind-utils-' && + printf '%s\n' "$info" | grep -Eq '^Command Line[[:space:]]*:.*(undo|rollback)'; then + return 0 + fi + done + return 1 +} + +task 1 "Owner of sshd_config found and verified" +need "line 1 of /root/lab-answers/1 is the package that owns /etc/ssh/sshd_config" '[ "$(sed -n 1p /root/lab-answers/1 | tr -d "[:space:]")" = "$(rpm -qf --qf "%{NAME}" /etc/ssh/sshd_config)" ]' +need "every file rpm -V reports is listed below it" verify_listed + +task 2 "tree downloaded, queried and one file extracted" +need "/root/rpms holds the tree package" 'for f in /root/rpms/tree-*.rpm; do [ "$(rpm -qp --qf "%{NAME}" "$f" 2>/dev/null)" = tree ] && exit 0; done; exit 1' +need "/root/rpms/usr/bin/tree was extracted from it" '[ -x /root/rpms/usr/bin/tree ] && /root/rpms/usr/bin/tree --version' + +task 3 "dig's package found, installed, and the install undone" +need "/root/lab-answers/3 names the package with dig" '[ "$(answer 3)" = bind-utils ]' +need "the DNF history shows bind-utils installed, then the install undone" undone +need "bind-utils isn't installed any more" '! rpm -q bind-utils' diff --git a/labs/lpic-1/102-5-rpm-and-yum/lab.conf b/labs/lpic-1/102-5-rpm-and-yum/lab.conf new file mode 100644 index 0000000..e20561b --- /dev/null +++ b/labs/lpic-1/102-5-rpm-and-yum/lab.conf @@ -0,0 +1,3 @@ +TITLE="102.5 RPM, YUM and Zypper" +CHAPTER=/lpic-1/102-installation-and-packages/102-5-rpm-and-yum/ +SYSTEMS="rhel" diff --git a/labs/lpic-1/102-5-rpm-and-yum/reset.sh b/labs/lpic-1/102-5-rpm-and-yum/reset.sh new file mode 100644 index 0000000..bc1d57d --- /dev/null +++ b/labs/lpic-1/102-5-rpm-and-yum/reset.sh @@ -0,0 +1,4 @@ +sed -i '/^# le-lab 102.5/d' /etc/ssh/sshd_config +rm -rf /root/rpms +rpm -q bind-utils >/dev/null 2>&1 && dnf -y -q remove bind-utils >/dev/null 2>&1 +true diff --git a/labs/lpic-1/102-5-rpm-and-yum/setup.sh b/labs/lpic-1/102-5-rpm-and-yum/setup.sh new file mode 100644 index 0000000..d624a54 --- /dev/null +++ b/labs/lpic-1/102-5-rpm-and-yum/setup.sh @@ -0,0 +1,6 @@ +rpm -q openssh-server >/dev/null || refuse "openssh-server isn't installed; this lab needs it." +if rpm -q bind-utils >/dev/null 2>&1; then + refuse "bind-utils is installed already, and task 3 installs it; remove it with: sudo dnf remove bind-utils, then start the lab again." +fi +grep -q '^# le-lab 102.5' /etc/ssh/sshd_config || echo '# le-lab 102.5: a change for rpm -V to find' >>/etc/ssh/sshd_config +state_set history "$(dnf -q history list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ {print $1; exit}')" diff --git a/labs/lpic-1/102-6-virtualization-guest/README.md b/labs/lpic-1/102-6-virtualization-guest/README.md new file mode 100644 index 0000000..313cfd4 --- /dev/null +++ b/labs/lpic-1/102-6-virtualization-guest/README.md @@ -0,0 +1,35 @@ +# LPIC-1 102.6: Linux as a virtualization guest + +Recognizing a virtual machine from the inside, and fixing what a clone +copies from its template. + +## Before you start + +The lab treats this VM as a fresh clone: it notes its machine ID, SSH host +keys and hostname as the template's, so task 2 can tell whether you +changed them. Reset puts the hostname back, but keeps the new machine ID +and host keys. Write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Run `systemd-detect-virt` and `lscpu` in the VM, and `lsmod | grep + virtio`. Write the hypervisor `systemd-detect-virt` reports to + `/root/lab-answers/1`. +2. A clone comes out with the same `/etc/machine-id`, SSH host keys + (`ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`) and `hostname` as + its template. Fix all three on this VM: a new machine ID, new SSH host + keys and a new hostname, all still in place after a reboot, with SSH + still working. (`ip link` would show the same MAC address too; the + hypervisor sets that one.) + +## Hints + +- `systemd-detect-virt`, `lscpu | grep -i hypervisor`. +- Empty `/etc/machine-id` (don't delete it), then + `systemd-machine-id-setup`; or `systemd-firstboot`. +- Remove `/etc/ssh/ssh_host_*`, then `ssh-keygen -A`, and restart sshd + (`ssh` on Ubuntu). Your SSH client will warn that the host key changed: + remove the old key with `ssh-keygen -R`. +- `hostnamectl set-hostname`. On a cloud image, cloud-init sets the + hostname again at every boot unless `preserve_hostname: true` is in a + file under `/etc/cloud/cloud.cfg.d/`. diff --git a/labs/lpic-1/102-6-virtualization-guest/check.sh b/labs/lpic-1/102-6-virtualization-guest/check.sh new file mode 100644 index 0000000..14b8eaa --- /dev/null +++ b/labs/lpic-1/102-6-virtualization-guest/check.sh @@ -0,0 +1,27 @@ +keys_new() { + local k n=0 + for k in /etc/ssh/ssh_host_*_key.pub; do + [ -f "$k" ] || continue + grep -qxF "$(ssh-keygen -lf "$k" | awk '{print $2}')" "$LAB_STATE/host-keys" && return 1 + n=$((n + 1)) + done + [ "$n" -gt 0 ] +} +sshd_up() { systemctl is-active --quiet sshd || systemctl is-active --quiet ssh || systemctl is-active --quiet ssh.socket; } +# The keys sshd offers match the files. +sshd_serves() { + local f + f=$(ssh-keyscan -t ed25519 127.0.0.1 2>/dev/null | ssh-keygen -lf - 2>/dev/null | awk '{print $2}') + [ -n "$f" ] && [ "$f" = "$(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub | awk '{print $2}')" ] +} + +task 1 "The hypervisor named" +need "/root/lab-answers/1 names what systemd-detect-virt reports" '[ "$(answer 1)" = "$(systemd-detect-virt)" ]' + +task 2 "New machine ID, SSH host keys and hostname" +need "/etc/machine-id is new" '[ -s /etc/machine-id ] && [ "$(cat /etc/machine-id)" != "$(state_get machine-id)" ]' +need "every SSH host key is new" keys_new +need "sshd is running" sshd_up +need "sshd offers the new ed25519 key" sshd_serves +need "the hostname is new" '[ "$(hostnamectl hostname 2>/dev/null || hostname)" != "$(state_get hostname)" ]' +need "the new hostname is in /etc/hostname, so it stays after a reboot" '[ "$(tr -d "[:space:]" /dev/null || hostname)" +for k in /etc/ssh/ssh_host_*_key.pub; do + [ -f "$k" ] && ssh-keygen -lf "$k" | awk '{print $2}' +done >"$LAB_STATE/host-keys" +[ -s "$LAB_STATE/host-keys" ] || refuse "no SSH host keys in /etc/ssh; this lab needs sshd." diff --git a/labs/lpic-1/103-1-command-line/README.md b/labs/lpic-1/103-1-command-line/README.md new file mode 100644 index 0000000..6c6a2cc --- /dev/null +++ b/labs/lpic-1/103-1-command-line/README.md @@ -0,0 +1,37 @@ +# LPIC-1 103.1: Working on the command line + +Quoting, exported variables, running your own scripts, builtins and +programs, and searching the manual. + +## Before you start + +The lab installs `man-db` and builds the manual's search index if needed. +Work as root (`sudo -i`) unless a task says otherwise, and write answers +to the files in `/root/lab-answers/`. + +## Tasks + +1. Predict what each of these prints, then check: + `echo '$HOME'`, `echo "$HOME"`, `echo \$HOME`, `echo "It's $USER"`. + Write the four outputs, as root, one per line in that order, to + `/root/lab-answers/1`. +2. Set a variable, show that `bash -c 'echo $var'` can't see it, export + it, and try again. Check it yourself: the second try prints the value. +3. Write a two-line script, `hello.sh`, in your home directory, make it + executable with `chmod +x`, and run it with `./hello.sh`. Then try + running it by name alone, and write why that fails to + `/root/lab-answers/3`. +4. Run `type` on `cd`, `ls`, `echo` and `ssh`, and compare with `which`. + Write the ones that are shell builtins to `/root/lab-answers/4`. +5. Find the man page for the `crontab` file format (not the command) using + `man -k`, and write how you'd open it (such as `man 1 ls`) to + `/root/lab-answers/5`. + +## Hints + +- Single quotes keep everything literal, double quotes expand `$`, and + `\` protects one character. +- `export var`, `env | grep var`. +- `#!/bin/bash` on line 1; `echo $PATH`. +- `type -a`; `which` only searches `PATH`. +- `man -k crontab` (or `apropos`); the number in brackets is the section. diff --git a/labs/lpic-1/103-1-command-line/check.sh b/labs/lpic-1/103-1-command-line/check.sh new file mode 100644 index 0000000..cab85ab --- /dev/null +++ b/labs/lpic-1/103-1-command-line/check.sh @@ -0,0 +1,32 @@ +line() { sed -n "$1p" /root/lab-answers/1 2>/dev/null | sed 's/[[:space:]]*$//'; } +# hello.sh in root's home or a user's: executable, a #! line, two lines or more. +script_ok() { + local f + for f in /root/hello.sh /home/*/hello.sh; do + [ -f "$f" ] && [ -x "$f" ] && head -n1 "$f" | grep -q '^#!' && [ "$(grep -c . "$f")" -ge 2 ] && return 0 + done + return 1 +} +builtins_ok() { + local a; a=$(tr -s ',[:space:]' '\n\n' /dev/null) + printf '%s\n' "$a" | grep -qx cd && printf '%s\n' "$a" | grep -qx echo && + ! printf '%s\n' "$a" | grep -qxE 'ls|ssh' +} + +task 1 "The four echo outputs" +need "line 1 of /root/lab-answers/1 is what echo '\$HOME' prints" '[ "$(line 1)" = "\$HOME" ]' +need "line 2 is what echo \"\$HOME\" prints for root" '[ "$(line 2)" = /root ]' +need "line 3 is what echo \\\$HOME prints" '[ "$(line 3)" = "\$HOME" ]' +need "line 4 is what echo \"It's \$USER\" prints for root" '[ "$(line 4)" = "It'\''s root" ]' + +task 2 "An exported variable seen by a child shell" self + +task 3 "hello.sh runs with ./; the reason it doesn't run by name" +need "an executable hello.sh with a #! line and two lines or more is in a home directory" script_ok +need "/root/lab-answers/3 explains it with PATH" 'grep -qi path /root/lab-answers/3' + +task 4 "The shell builtins among cd, ls, echo and ssh" +need "/root/lab-answers/4 lists cd and echo, and not ls or ssh" builtins_ok + +task 5 "The man page for the crontab file format" +need "/root/lab-answers/5 opens section 5 of crontab" 'grep -Eq "(^|[^0-9])5([^0-9]|$)" /root/lab-answers/5 && grep -q crontab /root/lab-answers/5' diff --git a/labs/lpic-1/103-1-command-line/lab.conf b/labs/lpic-1/103-1-command-line/lab.conf new file mode 100644 index 0000000..0abbc39 --- /dev/null +++ b/labs/lpic-1/103-1-command-line/lab.conf @@ -0,0 +1,3 @@ +TITLE="103.1 Working on the command line" +CHAPTER=/lpic-1/103-gnu-and-unix-commands/103-1-command-line/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/103-1-command-line/reset.sh b/labs/lpic-1/103-1-command-line/reset.sh new file mode 100644 index 0000000..f076f9f --- /dev/null +++ b/labs/lpic-1/103-1-command-line/reset.sh @@ -0,0 +1,2 @@ +rm -f /root/hello.sh /home/*/hello.sh +true diff --git a/labs/lpic-1/103-1-command-line/setup.sh b/labs/lpic-1/103-1-command-line/setup.sh new file mode 100644 index 0000000..e83ddb2 --- /dev/null +++ b/labs/lpic-1/103-1-command-line/setup.sh @@ -0,0 +1,3 @@ +command -v man >/dev/null || pkg_install man-db +command -v crontab >/dev/null || pkg_install cronie/cron +mandb -q >/dev/null 2>&1 || true diff --git a/labs/lpic-1/103-2-text-filters/README.md b/labs/lpic-1/103-2-text-filters/README.md new file mode 100644 index 0000000..8a41018 --- /dev/null +++ b/labs/lpic-1/103-2-text-filters/README.md @@ -0,0 +1,50 @@ +# LPIC-1 103.2: Processing text with filters + +Counting with `cut`, `sort` and `uniq`, spotting Windows line endings, +splitting and joining files, and following a log. + +## Before you start + +The lab creates `/root/filters/` with three text files, `report1.txt`, +`report2.txt` and `report3.txt`; one of them has Windows line endings. +Work as root (`sudo -i`), and write answers to the files in +`/root/lab-answers/`. Where the journal is kept only in memory (no +`/var/log/journal`), a reboot loses task 5's message, so check before +you reboot. + +## Tasks + +1. Make `/root/filters/fruit.txt` with the five lines from the chapter: + + ``` + apple 3 + banana 12 + cherry 7 + apple 5 + banana 1 + ``` + + Count how many lines mention each fruit with `cut`, `sort` and + `uniq -c`, and write the output to `/root/lab-answers/1`. +2. Count the accounts using each login shell in `/etc/passwd`, most common + first, and write the output to `/root/lab-answers/2`. +3. Use `od -c` to find out which of the three report files uses Windows + line endings (`\r \n`), and write its name to `/root/lab-answers/3`. +4. Split a large file from `/var/log` into 100-line pieces in + `/root/filters/split/`, join them back with `cat` into + `/root/filters/joined.log`, and prove the result is identical with + `sha256sum`. +5. Follow `/var/log/syslog` or `/var/log/messages` with `tail -f` in one + terminal while running `logger hello` in another. If neither file + exists, your system logs only to the journal: use `journalctl -f` + instead. + +## Hints + +- `cut -d' ' -f1 FILE | sort | uniq -c`. +- `cut -d: -f7`, then `sort | uniq -c | sort -rn`. +- `od -c FILE | head`; `file` says "with CRLF line terminators" too. +- `split -l 100 FILE /root/filters/split/part-`, `cat .../part-* > + ...`, `sha256sum`. Pick a file that doesn't change while you work, such + as `cloud-init.log` or an older rotated log. +- `logger hello`; `journalctl -f` or `journalctl -t USER`. diff --git a/labs/lpic-1/103-2-text-filters/check.sh b/labs/lpic-1/103-2-text-filters/check.sh new file mode 100644 index 0000000..fe2f7c4 --- /dev/null +++ b/labs/lpic-1/103-2-text-filters/check.sh @@ -0,0 +1,39 @@ +# Normalized "COUNT WORD" lines, sorted. +norm() { awk 'NF >= 2 {print $1, $2}' "$@" | sort; } +fruit_ok() { + [ "$(norm /root/lab-answers/1 2>/dev/null)" = "$(printf '2 apple\n2 banana\n1 cherry\n' | sort)" ] +} +shells_ok() { + local want + want=$(cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn) + [ "$(norm /root/lab-answers/2 2>/dev/null)" = "$(printf '%s\n' "$want" | norm)" ] || return 1 + # Most common first: the first line has the highest count. + [ "$(awk 'NF {print $1; exit}' /root/lab-answers/2)" = "$(printf '%s\n' "$want" | awk '{print $1; exit}')" ] +} +split_ok() { + local d=/root/filters/split n=0 f last='' + for f in "$d"/*; do + [ -f "$f" ] || continue + [ -n "$last" ] && { [ "$(wc -l <"$last")" -eq 100 ] || return 1; } + last=$f n=$((n + 1)) + done + [ "$n" -ge 2 ] && [ "$(wc -l <"$last")" -le 100 ] || return 1 + cat "$d"/* | cmp -s - /root/filters/joined.log +} + +task 1 "fruit.txt and a count of lines per fruit" +need "/root/filters/fruit.txt has the chapter's five lines" '[ "$(cat /root/filters/fruit.txt)" = "$(printf "apple 3\nbanana 12\ncherry 7\napple 5\nbanana 1")" ]' +need "/root/lab-answers/1 counts 2 apple, 2 banana and 1 cherry" fruit_ok + +task 2 "Accounts per login shell, most common first" +need "/root/lab-answers/2 counts each shell in /etc/passwd, most common first" shells_ok + +task 3 "The file with Windows line endings" +need "/root/lab-answers/3 names the report file with Windows line endings" '[ "$(basename "$(answer 3)")" = "$(state_get crlf)" ]' + +task 4 "A log split into 100-line pieces and joined again" +need "/root/filters/joined.log exists" '[ -s /root/filters/joined.log ]' +need "/root/filters/split/ holds two or more pieces of 100 lines (the last one up to 100), and they join up to joined.log" split_ok + +task 5 "logger hello seen in the log" +need "the log has a hello message from logger since the lab started" 'journalctl -q --no-pager --since "@$(state_get since)" -o cat | grep -qx hello' diff --git a/labs/lpic-1/103-2-text-filters/lab.conf b/labs/lpic-1/103-2-text-filters/lab.conf new file mode 100644 index 0000000..a33d23b --- /dev/null +++ b/labs/lpic-1/103-2-text-filters/lab.conf @@ -0,0 +1,3 @@ +TITLE="103.2 Processing text with filters" +CHAPTER=/lpic-1/103-gnu-and-unix-commands/103-2-text-filters/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/103-2-text-filters/reset.sh b/labs/lpic-1/103-2-text-filters/reset.sh new file mode 100644 index 0000000..b68624d --- /dev/null +++ b/labs/lpic-1/103-2-text-filters/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/filters +true diff --git a/labs/lpic-1/103-2-text-filters/setup.sh b/labs/lpic-1/103-2-text-filters/setup.sh new file mode 100644 index 0000000..678fef5 --- /dev/null +++ b/labs/lpic-1/103-2-text-filters/setup.sh @@ -0,0 +1,8 @@ +mkdir -p /root/filters +crlf=$(( (RANDOM % 3) + 1 )) +for i in 1 2 3; do + printf 'Weekly report %s\nAll systems normal.\nNext check on Monday.\n' "$i" >/root/filters/report$i.txt +done +sed -i 's/$/\r/' /root/filters/report$crlf.txt +state_set crlf "report$crlf.txt" +state_set since "$(date +%s)" diff --git a/labs/lpic-1/103-3-file-management/README.md b/labs/lpic-1/103-3-file-management/README.md new file mode 100644 index 0000000..498eb5d --- /dev/null +++ b/labs/lpic-1/103-3-file-management/README.md @@ -0,0 +1,36 @@ +# LPIC-1 103.3: Basic file management + +Brace expansion and copying trees, finding files by age and size, tar +archives, the three compressors, and `dd`. + +## Before you start + +The lab adds a 64 MiB spare disk, `/dev/loop40`, with a partition table +and a little text near the start, and installs `bzip2` if it's missing. +Work as root (`sudo -i`), and write answers to the files in +`/root/lab-answers/`. Files in `/tmp` don't survive a reboot on every +system, so check task 3 before you reboot. + +## Tasks + +1. Make `/root/practice/a`, `/root/practice/b` and `/root/practice/c` + with one command, create a file in each with `touch`, then copy the + whole tree with `cp -a` and remove the copy with `rm -r`. +2. Find every file under `/etc` changed in the last day, and write the + list to `/root/lab-answers/2-etc`. Find every file over 10 MiB under + `/usr`, and write that list to `/root/lab-answers/2-usr`. +3. Archive `/etc` into `/root/etc.tar.xz`, list it, and extract only + `etc/hostname` into `/tmp`. +4. Compress copies of one log file with `gzip`, `bzip2` and `xz` in + `/root/compress/`, and compare the sizes with `ls -l`. +5. Copy the first megabyte of the spare disk `/dev/loop40` to + `/root/first-mb.img` with `dd`, and look at it with `od -c | head`. + +## Hints + +- `mkdir -p /root/practice/{a,b,c}`; `touch`; `cp -a`; `rm -r`. +- `find /etc -type f -mtime -1`; `find /usr -type f -size +10M`. +- `tar -cJf`, `tar -tJf`, `tar -xJf ARCHIVE -C /tmp etc/hostname`. +- `gzip -k` (or `-c`), `bzip2 -k`, `xz -k`; `ls -l`. +- `dd if=/dev/loop40 of=/root/first-mb.img bs=1M count=1`; `od -c | head`, + or `od -c | grep -v '\\0 \\0'` to skip the zeros. diff --git a/labs/lpic-1/103-3-file-management/check.sh b/labs/lpic-1/103-3-file-management/check.sh new file mode 100644 index 0000000..cd4a018 --- /dev/null +++ b/labs/lpic-1/103-3-file-management/check.sh @@ -0,0 +1,47 @@ +etc_ok() { + local f n=0 now; now=$(date +%s) + [ -s /root/lab-answers/2-etc ] || return 1 + while read -r f; do + [ -n "$f" ] || continue + case $f in /etc/*) ;; *) return 1 ;; esac + [ -e "$f" ] || continue + # Changed within a day of the answer being written (with an hour to spare). + [ $(( $(stat -c %Y /root/lab-answers/2-etc) - $(stat -c %Y "$f") )) -le 90000 ] || return 1 + n=$((n + 1)) + done /dev/null | sort) + got=$(grep -o '/usr/[^[:space:]]*' /root/lab-answers/2-usr 2>/dev/null | sort -u) + [ -n "$want" ] && [ -z "$(comm -23 <(printf '%s\n' "$want") <(printf '%s\n' "$got"))" ] || return 1 + # and nothing much under 10 MiB. + local f + for f in $got; do [ ! -f "$f" ] || [ "$(stat -c %s "$f")" -ge 10000000 ] || return 1; done +} +compress_ok() { + local d=/root/compress g b x + g=$(ls "$d"/*.gz 2>/dev/null | head -n1); b=$(ls "$d"/*.bz2 2>/dev/null | head -n1); x=$(ls "$d"/*.xz 2>/dev/null | head -n1) + [ -n "$g" ] && [ -n "$b" ] && [ -n "$x" ] || return 1 + [ -n "$(gzip -dc "$g" | head -c1)" ] || return 1 + cmp -s <(gzip -dc "$g") <(bzip2 -dc "$b") && cmp -s <(gzip -dc "$g") <(xz -dc "$x") +} + +task 1 "/root/practice/a, b and c, with a file in each" +need "/root/practice/a, b and c each hold a file" 'for x in a b c; do [ -n "$(find /root/practice/$x -mindepth 1 -maxdepth 1 -type f)" ] || exit 1; done' + +task 2 "Recent files under /etc and big files under /usr listed" +need "/root/lab-answers/2-etc lists files under /etc changed in the day before" etc_ok +need "/root/lab-answers/2-usr lists every file over 10 MiB under /usr" usr_ok + +task 3 "/etc archived with xz; etc/hostname extracted into /tmp" +need "/root/etc.tar.xz is an xz-compressed tar of /etc" 'tar -tJf /root/etc.tar.xz | grep -qx "etc/passwd"' +need "/tmp/etc/hostname was extracted from it" '[ -f /tmp/etc/hostname ] && tar -xJOf /root/etc.tar.xz etc/hostname | cmp -s - /tmp/etc/hostname' + +task 4 "One log compressed three ways" +need "/root/compress holds .gz, .bz2 and .xz copies of the same file" compress_ok + +task 5 "The first megabyte of /dev/loop40 copied with dd" +need "/root/first-mb.img is 1 MiB" '[ "$(stat -c %s /root/first-mb.img)" = 1048576 ]' +need "it matches the start of /dev/loop40" 'head -c 1048576 /dev/loop40 | cmp -s - /root/first-mb.img' diff --git a/labs/lpic-1/103-3-file-management/lab.conf b/labs/lpic-1/103-3-file-management/lab.conf new file mode 100644 index 0000000..359cb11 --- /dev/null +++ b/labs/lpic-1/103-3-file-management/lab.conf @@ -0,0 +1,3 @@ +TITLE="103.3 Basic file management" +CHAPTER=/lpic-1/103-gnu-and-unix-commands/103-3-file-management/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/103-3-file-management/reset.sh b/labs/lpic-1/103-3-file-management/reset.sh new file mode 100644 index 0000000..0fc06f2 --- /dev/null +++ b/labs/lpic-1/103-3-file-management/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/practice /root/practice-copy /root/etc.tar.xz /tmp/etc /root/compress /root/first-mb.img +true diff --git a/labs/lpic-1/103-3-file-management/setup.sh b/labs/lpic-1/103-3-file-management/setup.sh new file mode 100644 index 0000000..569d0a8 --- /dev/null +++ b/labs/lpic-1/103-3-file-management/setup.sh @@ -0,0 +1,6 @@ +command -v bzip2 >/dev/null || pkg_install bzip2 +command -v xz >/dev/null || pkg_install xz/xz-utils +dev=$(disk_add 0 64M) +parted -s "$dev" mklabel msdos mkpart primary 1MiB 100% +printf 'LINUXexpert practice disk: you found the text.' | dd of="$dev" bs=512 seek=40 conv=notrunc status=none +sync diff --git a/labs/lpic-1/103-4-streams-pipes-redirects/README.md b/labs/lpic-1/103-4-streams-pipes-redirects/README.md new file mode 100644 index 0000000..1ac50d8 --- /dev/null +++ b/labs/lpic-1/103-4-streams-pipes-redirects/README.md @@ -0,0 +1,32 @@ +# LPIC-1 103.4: Streams, pipes and redirects + +Sending output and errors to files, file names with spaces in pipelines, +here-documents, and `noclobber`. + +## Before you start + +The lab creates `/root/streams/`, and in `/root/streams/spaces/` three +files with spaces in their names. Work as root (`sudo -i`). + +## Tasks + +1. Run `ls /etc /nonexistent` with the output going to + `/root/streams/out.txt` and the errors to `/root/streams/err.txt`, + then both to `/root/streams/both.txt`. Try `2>&1` before and after `>` + and compare. +2. Delete the three files in `/root/streams/spaces/` with `find -print0 | + xargs -0`. Try plain `xargs` first on copies and see it fail. +3. Write a here-document into `/root/streams/heredoc.txt`, and use + `$(date)` inside it, so the file holds the date it was written. +4. Turn on `set -o noclobber`, try to overwrite a file with `>`, then use + `>|`. Check it yourself: the first try says "cannot overwrite existing + file", the second works. + +## Hints + +- `>`, `2>`, `&>` or `> FILE 2>&1`; `2>&1 > FILE` sends errors to the + terminal, because they go where output went before the redirect. +- `cp -r spaces copies`, `find copies -type f | xargs rm`; + `find spaces -type f -print0 | xargs -0 rm`. +- `cat > FILE <"/root/streams/spaces/old report $n.txt"; done diff --git a/labs/lpic-1/103-5-processes/README.md b/labs/lpic-1/103-5-processes/README.md new file mode 100644 index 0000000..8380876 --- /dev/null +++ b/labs/lpic-1/103-5-processes/README.md @@ -0,0 +1,37 @@ +# LPIC-1 103.5: Creating, watching and killing processes + +Job control, a `tmux` session that outlives your login, signalling +processes by name, and finding the biggest memory users. + +## Before you start + +The lab installs `tmux` if it's missing. Write answers to the files in +`/root/lab-answers/`. Running processes don't survive a reboot, so check +before you reboot. + +## Tasks + +1. Start `sleep 600`, suspend it with Ctrl-Z, list it with `jobs`, resume + it with `bg`, bring it back with `fg`, and end it with Ctrl-C. Check it + yourself: `jobs` shows it as Stopped, then Running. +2. Start a `tmux` session, run `top` in it, detach, log out, log back in + and reattach. Leave `top` running in the session. +3. Start `sleep 1000 &` three times, list them with `pgrep -a sleep`, and + end them all with one `pkill`. Check it yourself: `pgrep -a sleep` + prints nothing afterwards. +4. Use `ps -eo pid,%mem,comm --sort=-%mem | head` to find the five + biggest memory users, and compare with `top` sorted by memory. Write + their PIDs and names, biggest first, one per line, to + `/root/lab-answers/4`. +5. Start a CPU-heavy loop in the background (`while :; do :; done &`), + then run `watch -d uptime` and see the load average climb. Leave + `watch` with Ctrl-C and end the loop with `kill %1`. Check it + yourself: `jobs` is empty afterwards. + +## Hints + +- Ctrl-Z, `jobs`, `bg %1`, `fg %1`. +- `tmux`, `top`, Ctrl-B then `d` to detach, `tmux ls`, + `tmux attach`. +- `pgrep -a`, `pkill sleep` (or `pkill -x sleep`). +- In `top`, Shift+M sorts by memory. diff --git a/labs/lpic-1/103-5-processes/check.sh b/labs/lpic-1/103-5-processes/check.sh new file mode 100644 index 0000000..115a365 --- /dev/null +++ b/labs/lpic-1/103-5-processes/check.sh @@ -0,0 +1,33 @@ +# A top process whose ancestors include a tmux server. +top_in_tmux() { + local p q + for p in $(pgrep -x top); do + q=$p + while [ -n "$q" ] && [ "$q" -gt 1 ]; do + case $(ps -o comm= -p "$q") in tmux*) return 0 ;; esac + q=$(ps -o ppid= -p "$q" | tr -d ' ') + done + done + return 1 +} +# The biggest memory user in the answer is one of the biggest three now. +mem_ok() { + local first top3 + [ "$(grep -c '[0-9]' /root/lab-answers/4 2>/dev/null)" -ge 5 ] || return 1 + first=$(grep -m1 -oE '[0-9]+' /root/lab-answers/4) + top3=$(ps -eo pid=,comm= --sort=-rss | head -n 3) + printf '%s\n' "$top3" | awk -v p="$first" '$1 == p {ok = 1} END {exit !ok}' +} + +task 1 "A job stopped, resumed and ended" self + +task 2 "top runs in a detached tmux session" +need "a tmux session is running" 'pgrep -f "^tmux" >/dev/null' +need "top runs inside it" top_in_tmux + +task 3 "Three sleeps ended with one pkill" self + +task 4 "The five biggest memory users" +need "/root/lab-answers/4 lists five processes, the first one among the three biggest now" mem_ok + +task 5 "A CPU loop watched and killed" self diff --git a/labs/lpic-1/103-5-processes/lab.conf b/labs/lpic-1/103-5-processes/lab.conf new file mode 100644 index 0000000..fb16b96 --- /dev/null +++ b/labs/lpic-1/103-5-processes/lab.conf @@ -0,0 +1,3 @@ +TITLE="103.5 Creating, watching and killing processes" +CHAPTER=/lpic-1/103-gnu-and-unix-commands/103-5-processes/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/103-5-processes/reset.sh b/labs/lpic-1/103-5-processes/reset.sh new file mode 100644 index 0000000..6f5b80c --- /dev/null +++ b/labs/lpic-1/103-5-processes/reset.sh @@ -0,0 +1,9 @@ +# tmux servers running a top: end them. +for p in $(pgrep -x top); do + q=$p + while [ -n "$q" ] && [ "$q" -gt 1 ]; do + case $(ps -o comm= -p "$q") in tmux*) kill "$q" 2>/dev/null; break ;; esac + q=$(ps -o ppid= -p "$q" | tr -d ' ') + done +done +true diff --git a/labs/lpic-1/103-5-processes/setup.sh b/labs/lpic-1/103-5-processes/setup.sh new file mode 100644 index 0000000..34dee0d --- /dev/null +++ b/labs/lpic-1/103-5-processes/setup.sh @@ -0,0 +1,2 @@ +command -v tmux >/dev/null || pkg_install tmux +true diff --git a/labs/lpic-1/103-6-process-priorities/README.md b/labs/lpic-1/103-6-process-priorities/README.md new file mode 100644 index 0000000..0b6a630 --- /dev/null +++ b/labs/lpic-1/103-6-process-priorities/README.md @@ -0,0 +1,32 @@ +# LPIC-1 103.6: Process priorities + +Sharing a CPU between niced processes, who may lower a nice value, and how +`nice` passes on to child processes. + +## Before you start + +The lab creates user `dana` (password `Lab-Pass-1`) and starts a long +`sleep` process owned by her at nice value 10. Write answers to the +files in `/root/lab-answers/`. The process doesn't survive a reboot, so +check before you reboot. + +## Tasks + +1. Start two CPU-heavy loops pinned to the same CPU, one at the default + and one niced: + `taskset -c 0 sh -c 'while :; do :; done' &` and + `taskset -c 0 nice -n 19 sh -c 'while :; do :; done' &`. + Watch their CPU share in `top`, then end them with `kill %1 %2`. Check + it yourself: the niced loop gets only a few percent. +2. As `dana`, try to renice her `sleep` process from 10 to 5 and read + the error. Then do it as root. +3. Run `nice` with no arguments, then `nice bash` and run `nice` again + inside the new shell. Write what `nice` printed inside the new shell to + `/root/lab-answers/3`. + +## Hints + +- `top` shows `NI` and `%CPU`; `q` quits. +- `su - dana` (or `sudo -iu dana`), `pgrep -u dana sleep`, + `renice -n 5 -p PID`; `ps -o pid,ni,comm -u dana`. +- `nice` alone prints the current nice value; `nice CMD` adds 10. diff --git a/labs/lpic-1/103-6-process-priorities/check.sh b/labs/lpic-1/103-6-process-priorities/check.sh new file mode 100644 index 0000000..b84c6c2 --- /dev/null +++ b/labs/lpic-1/103-6-process-priorities/check.sh @@ -0,0 +1,9 @@ +pid=$(state_get pid) +task 1 "Two pinned loops compared" self + +task 2 "dana's sleep reniced from 10 to 5" +need "dana's sleep is still running" '[ "$(ps -o user= -p "$pid" | tr -d " ")" = dana ] && [ "$(ps -o comm= -p "$pid")" = sleep ]' +need "it runs at nice 5" '[ "$(ps -o ni= -p "$pid" | tr -d " ")" = 5 ]' + +task 3 "nice inside nice bash" +need "/root/lab-answers/3 has what nice prints inside nice bash" '[ "$(answer 3)" = 10 ]' diff --git a/labs/lpic-1/103-6-process-priorities/lab.conf b/labs/lpic-1/103-6-process-priorities/lab.conf new file mode 100644 index 0000000..fb599c5 --- /dev/null +++ b/labs/lpic-1/103-6-process-priorities/lab.conf @@ -0,0 +1,3 @@ +TITLE="103.6 Process priorities" +CHAPTER=/lpic-1/103-gnu-and-unix-commands/103-6-process-priorities/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/103-6-process-priorities/reset.sh b/labs/lpic-1/103-6-process-priorities/reset.sh new file mode 100644 index 0000000..41afc56 --- /dev/null +++ b/labs/lpic-1/103-6-process-priorities/reset.sh @@ -0,0 +1,3 @@ +systemctl stop le-lab-dana-sleep 2>/dev/null +systemctl reset-failed le-lab-dana-sleep 2>/dev/null +true diff --git a/labs/lpic-1/103-6-process-priorities/setup.sh b/labs/lpic-1/103-6-process-priorities/setup.sh new file mode 100644 index 0000000..2ba96e2 --- /dev/null +++ b/labs/lpic-1/103-6-process-priorities/setup.sh @@ -0,0 +1,8 @@ +user_add dana +systemd-run --quiet --unit=le-lab-dana-sleep --uid=dana --nice=10 sleep 86400 +for _ in 1 2 3 4 5; do + p=$(systemctl show -p MainPID --value le-lab-dana-sleep) + [ "${p:-0}" -gt 0 ] && break + sleep 1 +done +state_set pid "$p" diff --git a/labs/lpic-1/103-7-regular-expressions/README.md b/labs/lpic-1/103-7-regular-expressions/README.md new file mode 100644 index 0000000..3826056 --- /dev/null +++ b/labs/lpic-1/103-7-regular-expressions/README.md @@ -0,0 +1,34 @@ +# LPIC-1 103.7: Searching text with regular expressions + +Filtering out comments, pulling addresses out of a log, editing a copy of +a file with `sed`, and basic against extended regular expressions. + +## Before you start + +The lab creates `/root/regex/access.log`, a log with IPv4 addresses in it. +Work as root (`sudo -i`), and write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Show `/etc/ssh/sshd_config` without comments or blank lines, first with + two `grep -v` commands, then with one `grep -Ev '^(#|$)'`. Save the + result in `/root/regex/sshd-active.txt`. +2. Pull every IPv4 address out of `/root/regex/access.log` with `grep -Eo` + and count each one. Write the counts, as `uniq -c` prints them, to + `/root/lab-answers/2`. +3. Copy `/etc/ssh/sshd_config` to `/root/sshd_config`, then use `sed + -i.bak` on the copy to set the port to 2222, and compare the two files + with `diff`. +4. Write the same pattern as a basic and an extended regex, and check both + give the same matches in `/root/regex/access.log`. Write the basic one + on line 1 of `/root/lab-answers/4` and the extended one on line 2. + +## Hints + +- `grep -v '^#' FILE | grep -v '^$'`. +- `grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' FILE | sort | uniq -c`. +- The port line may be commented out (`#Port 22`): + `sed -i.bak 's/^#\?Port .*/Port 2222/'`. `diff FILE.bak FILE`. +- In a basic regex `\{`, `\(`, `\|` and `\+` do what `{`, `(`, `|` and + `+` do in an extended one. diff --git a/labs/lpic-1/103-7-regular-expressions/check.sh b/labs/lpic-1/103-7-regular-expressions/check.sh new file mode 100644 index 0000000..7c0a0e4 --- /dev/null +++ b/labs/lpic-1/103-7-regular-expressions/check.sh @@ -0,0 +1,25 @@ +counts_ok() { [ "$(awk 'NF >= 2 {print $1, $2}' /root/lab-answers/2 2>/dev/null | sort)" = "$(cat "$LAB_STATE/counts")" ]; } +same_matches() { + local b e + b=$(sed -n 1p /root/lab-answers/4 2>/dev/null) e=$(sed -n 2p /root/lab-answers/4 2>/dev/null) + # Strip quotes the student may have kept. + b=${b#\'}; b=${b%\'}; e=${e#\'}; e=${e%\'} + [ -n "$b" ] && [ -n "$e" ] && [ "$b" != "$e" ] || return 1 + local mb me + mb=$(grep -e "$b" /root/regex/access.log) && me=$(grep -E -e "$e" /root/regex/access.log) || return 1 + [ "$mb" = "$me" ] +} + +task 1 "sshd_config without comments or blank lines" +need "/root/regex/sshd-active.txt has exactly the lines that aren't comments or blank" '[ -s /root/regex/sshd-active.txt ] && grep -Ev "^(#|$)" /etc/ssh/sshd_config | cmp -s - /root/regex/sshd-active.txt' + +task 2 "IPv4 addresses in the log counted" +need "/root/lab-answers/2 has the count of each address" counts_ok + +task 3 "A copy of sshd_config with port 2222, and a .bak of the original" +need "/root/sshd_config.bak is the original" 'cmp -s /root/sshd_config.bak /etc/ssh/sshd_config' +need "/root/sshd_config sets Port 2222" 'grep -Eq "^Port[[:space:]]+2222[[:space:]]*$" /root/sshd_config' +need "only the port line changed" '[ "$(diff /root/sshd_config.bak /root/sshd_config | grep -c "^[<>]")" -le 2 ]' + +task 4 "One pattern as a basic and an extended regex" +need "the two patterns differ and match the same lines of access.log" same_matches diff --git a/labs/lpic-1/103-7-regular-expressions/lab.conf b/labs/lpic-1/103-7-regular-expressions/lab.conf new file mode 100644 index 0000000..a1c1ef5 --- /dev/null +++ b/labs/lpic-1/103-7-regular-expressions/lab.conf @@ -0,0 +1,3 @@ +TITLE="103.7 Searching text with regular expressions" +CHAPTER=/lpic-1/103-gnu-and-unix-commands/103-7-regular-expressions/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/103-7-regular-expressions/reset.sh b/labs/lpic-1/103-7-regular-expressions/reset.sh new file mode 100644 index 0000000..1bcfe31 --- /dev/null +++ b/labs/lpic-1/103-7-regular-expressions/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/regex /root/sshd_config /root/sshd_config.bak +true diff --git a/labs/lpic-1/103-7-regular-expressions/setup.sh b/labs/lpic-1/103-7-regular-expressions/setup.sh new file mode 100644 index 0000000..d319686 --- /dev/null +++ b/labs/lpic-1/103-7-regular-expressions/setup.sh @@ -0,0 +1,11 @@ +mkdir -p /root/regex +awk 'BEGIN { + srand() + split("192.0.2.10 198.51.100.7 203.0.113.45 192.0.2.200 10.20.30.40", ip, " ") + split("GET POST GET GET HEAD", m, " ") + for (i = 1; i <= 60; i++) { + a = ip[int(rand() * 5) + 1] + printf "2026-10-%02d 10:%02d:%02d %s %s /app/v2.%d/index.html 200 %d\n", int(rand() * 28) + 1, i % 60, int(rand() * 60), a, m[int(rand() * 5) + 1], int(rand() * 9), int(rand() * 5000) + } +}' >/root/regex/access.log +grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' /root/regex/access.log | sort | uniq -c | awk '{print $1, $2}' | sort >"$LAB_STATE/counts" diff --git a/labs/lpic-1/104-1-partitions-and-filesystems/README.md b/labs/lpic-1/104-1-partitions-and-filesystems/README.md new file mode 100644 index 0000000..a05b3ce --- /dev/null +++ b/labs/lpic-1/104-1-partitions-and-filesystems/README.md @@ -0,0 +1,39 @@ +# LPIC-1 104.1: Creating partitions and filesystems + +Partitioning with `fdisk` and `parted`, ext4 and XFS with labels, a swap +partition, wiping a disk, and Btrfs subvolumes and snapshots. + +## Before you start + +The lab adds two empty 4 GiB spare disks, `/dev/loop40` and `/dev/loop41` +(the chapter's `/dev/sdb` and `/dev/sdc`). Their partitions show up as +`/dev/loop40p1` and so on. It installs the Btrfs tools if they're missing. +Swap you turn on by hand is off again after a reboot, so check before you +reboot. + +## Tasks + +1. Partition `/dev/loop40` as GPT with `fdisk`: a 1 GiB partition, a + 512 MiB partition with the swap type, and the rest. Print the table + before writing. +2. Put ext4 on the first partition and XFS on the third, each with a + label. Run `lsblk -f` to see them. +3. Turn the second partition into swap, enable it, and check with + `swapon --show` and `free -h`. +4. Give `/dev/loop41` a GPT table and one partition with `parted`, and + notice that nothing asks you to confirm a write. Then clear it with + `wipefs -a /dev/loop41`. Check it yourself: `wipefs /dev/loop41` lists + nothing afterwards. +5. Make a Btrfs filesystem on `/dev/loop41`, create a subvolume, write a + file, snapshot the subvolume, delete the file, and find it again in + the snapshot. + +## Hints + +- In `fdisk`: `g` (new GPT table), `n` (sizes as `+1G`, `+512M`), `t` + (type `swap` or `19`), `p`, `w`. +- `mkfs.ext4 -L`, `mkfs.xfs -L`; `lsblk -f`. +- `mkswap`, `swapon`, `swapon --show`, `free -h`. +- `parted -s /dev/loop41 mklabel gpt mkpart one 1MiB 100%`; `wipefs -a`. +- `mkfs.btrfs`, mount it, `btrfs subvolume create`, `btrfs subvolume + snapshot`, `btrfs subvolume list`. diff --git a/labs/lpic-1/104-1-partitions-and-filesystems/check.sh b/labs/lpic-1/104-1-partitions-and-filesystems/check.sh new file mode 100644 index 0000000..9710f13 --- /dev/null +++ b/labs/lpic-1/104-1-partitions-and-filesystems/check.sh @@ -0,0 +1,51 @@ +sectors() { cat "/sys/class/block/$1/$2" 2>/dev/null || echo 0; } +mib() { echo $(( $(sectors "$1" size) / 2048 )); } +ptype() { lsblk -dno PARTTYPE "/dev/$1" 2>/dev/null | tr 'A-Z' 'a-z' | tr -d ' '; } +SWAP_GUID=0657fd6d-a4ab-43c4-84e5-0933c84b4f4f +# A snapshot on the Btrfs filesystem of /dev/loop41 holds a file that its +# source subvolume no longer has. +btrfs_ok() { + local m rc=1 line uuid puuid path src f + m=$(mktemp -d) + mount -o ro,subvolid=5 /dev/loop41 "$m" 2>/dev/null || { rmdir "$m"; return 1; } + declare -A byuuid + while read -r line; do + uuid=$(sed -n 's/.* uuid \([^ ]*\) .*/\1/p' <<<"$line") + path=$(sed -n 's/.* path \(.*\)$/\1/p' <<<"$line") + byuuid[$uuid]=${path#/} + done < <(btrfs subvolume list -u "$m" 2>/dev/null) + while read -r line; do + puuid=$(sed -n 's/.* parent_uuid \([^ ]*\) .*/\1/p' <<<"$line") + path=$(sed -n 's/.* path \(.*\)$/\1/p' <<<"$line"); path=${path#/} + src=${byuuid[$puuid]:-} + [ -n "$src" ] || continue + while read -r f; do + [ -e "$m/$src/$f" ] || { rc=0; break 2; } + done < <(cd "$m/$path" && find . -xdev -type f) + done < <(btrfs subvolume list -u -q "$m" 2>/dev/null) + umount "$m"; rmdir "$m" + return $rc +} + +task 1 "GPT on /dev/loop40: 1 GiB, 512 MiB swap type, and the rest" +need "/dev/loop40 has a GPT partition table" '[ "$(blkid -p -s PTTYPE -o value /dev/loop40)" = gpt ]' +need "/dev/loop40p1 is 1 GiB" '[ "$(mib loop40p1)" -ge 1000 ] && [ "$(mib loop40p1)" -le 1030 ]' +need "/dev/loop40p2 is 512 MiB" '[ "$(mib loop40p2)" -ge 500 ] && [ "$(mib loop40p2)" -le 520 ]' +need "/dev/loop40p2 has the Linux swap type" '[ "$(ptype loop40p2)" = $SWAP_GUID ]' +need "/dev/loop40p3 takes the rest of the disk" '[ "$(mib loop40p3)" -ge 2400 ]' + +task 2 "ext4 and XFS, each with a label" +need "/dev/loop40p1 holds ext4" '[ "$(blkid -s TYPE -o value /dev/loop40p1)" = ext4 ]' +need "the ext4 filesystem has a label" '[ -n "$(blkid -s LABEL -o value /dev/loop40p1)" ]' +need "/dev/loop40p3 holds XFS" '[ "$(blkid -s TYPE -o value /dev/loop40p3)" = xfs ]' +need "the XFS filesystem has a label" '[ -n "$(blkid -s LABEL -o value /dev/loop40p3)" ]' + +task 3 "Swap on /dev/loop40p2" +need "/dev/loop40p2 is swap" '[ "$(blkid -s TYPE -o value /dev/loop40p2)" = swap ]' +need "it's in use (swapon --show)" 'swapon --show=NAME --noheadings | grep -qx /dev/loop40p2' + +task 4 "A GPT table made with parted, then wiped" self + +task 5 "Btrfs: a subvolume, a snapshot, and a file found again in it" +need "/dev/loop41 holds Btrfs" '[ "$(blkid -s TYPE -o value /dev/loop41)" = btrfs ]' +need "a snapshot holds a file deleted from its subvolume" btrfs_ok diff --git a/labs/lpic-1/104-1-partitions-and-filesystems/lab.conf b/labs/lpic-1/104-1-partitions-and-filesystems/lab.conf new file mode 100644 index 0000000..0d1a153 --- /dev/null +++ b/labs/lpic-1/104-1-partitions-and-filesystems/lab.conf @@ -0,0 +1,3 @@ +TITLE="104.1 Creating partitions and filesystems" +CHAPTER=/lpic-1/104-devices-filesystems-fhs/104-1-partitions-and-filesystems/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/104-1-partitions-and-filesystems/reset.sh b/labs/lpic-1/104-1-partitions-and-filesystems/reset.sh new file mode 100644 index 0000000..a8cec55 --- /dev/null +++ b/labs/lpic-1/104-1-partitions-and-filesystems/reset.sh @@ -0,0 +1,16 @@ +# Lines the student may have added for the spare disks; the disks +# themselves (mounts and swap too) go afterwards. +ids='' +for p in /dev/loop40 /dev/loop40p* /dev/loop41; do + [ -b "$p" ] || continue + u=$(blkid -s UUID -o value "$p" 2>/dev/null); [ -n "$u" ] && ids="$ids UUID=$u " + l=$(blkid -s LABEL -o value "$p" 2>/dev/null); [ -n "$l" ] && ids="$ids LABEL=$l " +done +awk -v ids="$ids" ' + /^[[:space:]]*#/ || NF < 2 { print; next } + { src = $1; gsub(/"/, "", src) } + index(ids, " " src " ") || src ~ /^\/dev\/loop4[01]/ { next } + { print }' /etc/fstab >/etc/fstab.le-lab && cat /etc/fstab.le-lab >/etc/fstab +rm -f /etc/fstab.le-lab +systemctl daemon-reload +true diff --git a/labs/lpic-1/104-1-partitions-and-filesystems/setup.sh b/labs/lpic-1/104-1-partitions-and-filesystems/setup.sh new file mode 100644 index 0000000..8e4be3d --- /dev/null +++ b/labs/lpic-1/104-1-partitions-and-filesystems/setup.sh @@ -0,0 +1,4 @@ +command -v mkfs.btrfs >/dev/null || pkg_install btrfs-progs +modprobe btrfs 2>/dev/null || refuse "this kernel has no Btrfs support, which task 5 needs." +disk_add 0 4G >/dev/null +disk_add 1 4G >/dev/null diff --git a/labs/lpic-1/104-2-filesystem-integrity/README.md b/labs/lpic-1/104-2-filesystem-integrity/README.md new file mode 100644 index 0000000..6b2f3a7 --- /dev/null +++ b/labs/lpic-1/104-2-filesystem-integrity/README.md @@ -0,0 +1,42 @@ +# LPIC-1 104.2: Keeping filesystems healthy + +Running out of inodes, checking ext4 with `e2fsck`, backup superblocks, +`tune2fs`, checking XFS, and space held by deleted files. + +## Before you start + +The lab adds two empty spare disks: `/dev/loop40` (100 MiB) and +`/dev/loop41` (512 MiB). Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Make a 100 MiB ext4 filesystem with few inodes and 1 KiB blocks on + `/dev/loop40` (`mkfs.ext4 -N 1000 -b 1024`), mount it, and create empty + files in a loop until it fails. Compare `df -h` and `df -i`. (With the + 4 KiB blocks Ubuntu picks, a filesystem this small has a single block + group and no backup superblock for task 3.) +2. Unmount it and run `e2fsck`, then `e2fsck -f`, and read the five + passes. +3. Find the backup superblocks with `dumpe2fs`, then run `e2fsck -f -b + BACKUP /dev/loop40` with the first one. Expect it to report something + "FIXED" and "FILE SYSTEM WAS MODIFIED" even on a healthy filesystem: it + rewrites the main superblock from the backup. Write the block number of + the first backup superblock to `/root/lab-answers/3`. +4. Change the filesystem's label to `scratch` and its reserved percentage + to 2 with `tune2fs`, and check both with `tune2fs -l`. +5. Make an XFS filesystem on `/dev/loop41`, run `xfs_repair -n` on it + unmounted, then mount it and run `xfs_info`. +6. Delete a large file while `tail -f` holds it open, and compare `df` and + `du` before and after stopping `tail`. Check it yourself: `df` gets the + space back only when `tail` ends. + +## Hints + +- `for i in $(seq 2000); do touch /mnt/small/f$i || break; done`; + `df -i`. +- `umount`; plain `e2fsck` skips a clean filesystem, `-f` forces it. +- `dumpe2fs /dev/loop40 | grep -i superblock`. +- `tune2fs -L`, `tune2fs -m`, `tune2fs -l | grep -Ei 'volume name|reserved block|block count'`. +- `mkfs.xfs`, `xfs_repair -n`, `xfs_info MOUNTPOINT`. +- `lsof +L1` lists deleted files that are still open. diff --git a/labs/lpic-1/104-2-filesystem-integrity/check.sh b/labs/lpic-1/104-2-filesystem-integrity/check.sh new file mode 100644 index 0000000..bc8e6cb --- /dev/null +++ b/labs/lpic-1/104-2-filesystem-integrity/check.sh @@ -0,0 +1,25 @@ +e2() { dumpe2fs -h /dev/loop40 2>/dev/null | sed -n "s/^$1: *//p"; } +mounted() { findmnt -rn -S "$1" >/dev/null; } + +task 1 "ext4 with few inodes on /dev/loop40, filled up" +need "/dev/loop40 holds ext4" '[ "$(blkid -s TYPE -o value /dev/loop40)" = ext4 ]' +need "it has 1 KiB blocks" '[ "$(e2 "Block size")" = 1024 ]' +need "it has few inodes (mkfs.ext4 -N 1000)" '[ "$(e2 "Inode count")" -le 2000 ]' +need "every inode is in use" 'if mounted /dev/loop40; then [ "$(df --output=iavail /dev/loop40 | tail -n1 | tr -d " ")" = 0 ]; else [ "$(e2 "Free inodes")" = 0 ]; fi' + +task 2 "Unmounted and checked with e2fsck -f" +need "/dev/loop40 isn't mounted" '! mounted /dev/loop40' +need "a full check (e2fsck -f) has run since it was made" '[ "$(date -d "$(e2 "Last checked")" +%s)" -gt "$(date -d "$(e2 "Filesystem created")" +%s)" ]' + +task 3 "The first backup superblock" +need "/root/lab-answers/3 has the first backup superblock's block number" '[ -n "$(answer 3)" ] && [ "$(answer 3 | grep -oE "[0-9]+" | head -n1)" = "$(dumpe2fs /dev/loop40 2>/dev/null | grep -m1 -oE "Backup superblock at [0-9]+" | grep -oE "[0-9]+$")" ]' + +task 4 "Label scratch, 2% reserved" +need "the label is scratch" '[ "$(e2 "Filesystem volume name")" = scratch ]' +need "2% of the blocks are reserved" 'r=$(e2 "Reserved block count"); b=$(e2 "Block count"); [ $((r * 1000 / b)) -ge 19 ] && [ $((r * 1000 / b)) -le 21 ]' + +task 5 "XFS on /dev/loop41, checked" +need "/dev/loop41 holds XFS" '[ "$(blkid -s TYPE -o value /dev/loop41)" = xfs ]' +need "xfs_repair -n finds nothing wrong" 'mounted /dev/loop41 || xfs_repair -n /dev/loop41' + +task 6 "A deleted file held open by tail" self diff --git a/labs/lpic-1/104-2-filesystem-integrity/lab.conf b/labs/lpic-1/104-2-filesystem-integrity/lab.conf new file mode 100644 index 0000000..d4cdd40 --- /dev/null +++ b/labs/lpic-1/104-2-filesystem-integrity/lab.conf @@ -0,0 +1,3 @@ +TITLE="104.2 Keeping filesystems healthy" +CHAPTER=/lpic-1/104-devices-filesystems-fhs/104-2-filesystem-integrity/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/104-2-filesystem-integrity/reset.sh b/labs/lpic-1/104-2-filesystem-integrity/reset.sh new file mode 100644 index 0000000..45f34fb --- /dev/null +++ b/labs/lpic-1/104-2-filesystem-integrity/reset.sh @@ -0,0 +1,2 @@ +sed -i '\|^[[:space:]]*/dev/loop4[01][[:space:]]|d' /etc/fstab +true diff --git a/labs/lpic-1/104-2-filesystem-integrity/setup.sh b/labs/lpic-1/104-2-filesystem-integrity/setup.sh new file mode 100644 index 0000000..ae83714 --- /dev/null +++ b/labs/lpic-1/104-2-filesystem-integrity/setup.sh @@ -0,0 +1,3 @@ +command -v mkfs.xfs >/dev/null || pkg_install xfsprogs +disk_add 0 100M >/dev/null +disk_add 1 512M >/dev/null diff --git a/labs/lpic-1/104-3-mounting/README.md b/labs/lpic-1/104-3-mounting/README.md new file mode 100644 index 0000000..1efd33f --- /dev/null +++ b/labs/lpic-1/104-3-mounting/README.md @@ -0,0 +1,39 @@ +# LPIC-1 104.3: Mounting and unmounting filesystems + +Mounting by hand, finding what keeps a filesystem busy, mounting at boot +by UUID, systemd mount units, user mounts, and remounting read-only. + +## Before you start + +The lab adds two spare disks, each with an ext4 filesystem that isn't +mounted: `/dev/loop40p1` (1 GiB, standing in for the one from 104.1) and +`/dev/loop41` (256 MiB, labeled `stick`, standing in for a USB stick). They +come back at every boot, before filesystems are mounted. It also creates +user `uma` (password `Lab-Pass-1`). Write answers to the files in +`/root/lab-answers/`. + +## Tasks + +1. Mount `/dev/loop40p1` by hand on `/mnt/data`, create a file in it, `cd` + into it in another terminal, and try to unmount. Find the blocker with + `fuser -vm`. +2. Add it to `/etc/fstab` by UUID, still at `/mnt/data`, check with + `findmnt --verify` and `mount -a`, and reboot to prove it comes back. +3. Find the mount unit systemd made for it, and read it with `systemctl + cat`. Write the unit's name to `/root/lab-answers/3`. +4. Add a `noauto,users` line for `/dev/loop41` at `/mnt/stick`, and mount + and unmount it as `uma`. +5. Remount `/mnt/data` read-only with `mount -o remount,ro`, try to write + to it, and remount it read-write. Check it yourself: the write fails + with "Read-only file system" in between. + +## Hints + +- `mount`, `umount`, `fuser -vm /mnt/data`; `lsof /mnt/data` works too. +- `blkid`, `UUID=... /mnt/data ext4 defaults 0 2`; `systemctl + daemon-reload` after editing fstab. +- `systemctl list-units --type=mount`, `systemd-escape -p --suffix=mount + /mnt/data`. +- `LABEL=stick /mnt/stick ext4 noauto,users 0 0`; then `su - uma` and + `mount /mnt/stick`. +- `touch /mnt/data/x` while it's read-only. diff --git a/labs/lpic-1/104-3-mounting/check.sh b/labs/lpic-1/104-3-mounting/check.sh new file mode 100644 index 0000000..a319401 --- /dev/null +++ b/labs/lpic-1/104-3-mounting/check.sh @@ -0,0 +1,35 @@ +fstab_src() { awk -v m="$1" '!/^[[:space:]]*#/ && $2 == m {print $1}' /etc/fstab | tr -d '"' | head -n1; } +fstab_opts() { awk -v m="$1" '!/^[[:space:]]*#/ && $2 == m {print $4}' /etc/fstab | head -n1; } +stick_src_ok() { + case $(fstab_src /mnt/stick) in + /dev/loop41 | "UUID=$(blkid -s UUID -o value /dev/loop41)" | LABEL=stick) return 0 ;; + esac + return 1 +} +# uma can mount and unmount it (left as it was found). +uma_mounts() { + if findmnt -n /mnt/stick >/dev/null; then + runuser -u uma -- umount /mnt/stick && runuser -u uma -- mount /mnt/stick + else + runuser -u uma -- mount /mnt/stick && runuser -u uma -- umount /mnt/stick + fi +} +has_file() { [ "$(debugfs -R "ls -p /" /dev/loop40p1 2>/dev/null | grep '^/' | grep -cvE "/(\.|\.\.|lost\+found)/")" -gt 0 ]; } + +task 1 "/dev/loop40p1 mounted by hand, a file created on it" +need "the filesystem holds a file of yours" has_file + +task 2 "/mnt/data mounted at boot by UUID" +need "/etc/fstab mounts /mnt/data by its UUID" '[ "$(fstab_src /mnt/data)" = "UUID=$(blkid -s UUID -o value /dev/loop40p1)" ]' +need "findmnt --verify reports no errors" 'findmnt --verify 2>&1 | grep -Eq "(^| )0 errors|no errors"' +need "/mnt/data is mounted from /dev/loop40p1" '[ "$(findmnt -n -o SOURCE /mnt/data)" = /dev/loop40p1 ]' + +task 3 "The mount unit for /mnt/data" +need "/root/lab-answers/3 names the mount unit" '[ "$(answer 3)" = mnt-data.mount ]' + +task 4 "A noauto,users line for the stick, mounted by uma" +need "/etc/fstab has a line for /dev/loop41 at /mnt/stick" stick_src_ok +need "its options include noauto and users" 'o=",$(fstab_opts /mnt/stick),"; case $o in *,noauto,*) ;; *) exit 1 ;; esac; case $o in *,users,* | *,user,*) ;; *) exit 1 ;; esac' +need "uma can mount and unmount /mnt/stick" uma_mounts + +task 5 "Remounted read-only and back" self diff --git a/labs/lpic-1/104-3-mounting/lab.conf b/labs/lpic-1/104-3-mounting/lab.conf new file mode 100644 index 0000000..93cd502 --- /dev/null +++ b/labs/lpic-1/104-3-mounting/lab.conf @@ -0,0 +1,3 @@ +TITLE="104.3 Mounting and unmounting filesystems" +CHAPTER=/lpic-1/104-devices-filesystems-fhs/104-3-mounting/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/104-3-mounting/reset.sh b/labs/lpic-1/104-3-mounting/reset.sh new file mode 100644 index 0000000..4c6f2b5 --- /dev/null +++ b/labs/lpic-1/104-3-mounting/reset.sh @@ -0,0 +1,14 @@ +ids=" LABEL=data LABEL=stick " +for p in /dev/loop40p1 /dev/loop41; do + u=$(blkid -s UUID -o value "$p" 2>/dev/null); [ -n "$u" ] && ids="$ids UUID=$u " +done +awk -v ids="$ids" ' + /^[[:space:]]*#/ || NF < 2 { print; next } + { src = $1; gsub(/"/, "", src) } + $2 == "/mnt/data" || $2 == "/mnt/stick" || index(ids, " " src " ") || src ~ /^\/dev\/loop4[01]/ { next } + { print }' /etc/fstab >/etc/fstab.le-lab && cat /etc/fstab.le-lab >/etc/fstab +rm -f /etc/fstab.le-lab +umount -l /mnt/data /mnt/stick 2>/dev/null +systemctl daemon-reload +rmdir /mnt/data /mnt/stick 2>/dev/null +true diff --git a/labs/lpic-1/104-3-mounting/setup.sh b/labs/lpic-1/104-3-mounting/setup.sh new file mode 100644 index 0000000..d28a75a --- /dev/null +++ b/labs/lpic-1/104-3-mounting/setup.sh @@ -0,0 +1,9 @@ +command -v fuser >/dev/null || pkg_install psmisc +d0=$(disk_add 0 1G) +parted -s "$d0" mklabel gpt mkpart data ext4 1MiB 100% +udevadm settle +[ -b "${d0}p1" ] || partx -u "$d0" +mkfs.ext4 -q -L data "${d0}p1" +d1=$(disk_add 1 256M) +mkfs.ext4 -q -L stick "$d1" +user_add uma diff --git a/labs/lpic-1/104-5-permissions-and-ownership/README.md b/labs/lpic-1/104-5-permissions-and-ownership/README.md new file mode 100644 index 0000000..1252744 --- /dev/null +++ b/labs/lpic-1/104-5-permissions-and-ownership/README.md @@ -0,0 +1,42 @@ +# LPIC-1 104.5: File permissions and ownership + +Octal modes, execute-only directories, the umask, a shared group +directory with setgid and sticky bits, and finding setuid programs. + +## Before you start + +The lab creates `/root/perm/report.txt` (mode 644) and three users with +password `Lab-Pass-1`: `tess`, and `anna` and `ben` for the shared +directory. Work as root (`sudo -i`) unless a task says otherwise, and +write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Work out the octal mode for "owner reads and writes, group reads, + others nothing", apply it to `/root/perm/report.txt`, and check with + `ls -l` and `stat -c %a`. Write the mode to `/root/lab-answers/1`. +2. As root, make `/srv/test` with mode `711` and put a readable file in + it. As `tess`, try `ls` on the directory and `cat` on the file by its + full path. +3. Set `umask 077`, create a file `/root/perm/f077` and a directory + `/root/perm/d077`, and check their modes. Do the same with `umask 033` + for `/root/perm/f033` and `/root/perm/d033`. +4. Build the shared `/srv/web` directory from the chapter for group `web`, + with `anna` and `ben` as members. With `chmod 2770` and `umask 002`, + show that each user's new files get the `web` group and that the other + user can add to them (`echo more >> file`). Then `chmod 3770` and show + that neither can delete the other's files. +5. List the setuid and setgid programs on your system with `find`, and + write the list to `/root/lab-answers/5`. + +## Hints + +- `r` is 4, `w` 2, `x` 1; `chmod`, `stat -c %a`. +- `mkdir -m 711`; `su - tess` (or `sudo -u tess`); `ls /srv/test` and + `cat /srv/test/FILE`. +- `umask` only affects the current shell; new files start from 666 and + directories from 777. +- `groupadd web`, `usermod -aG web anna`; `chgrp`, `chmod`. Group + membership counts from the next login. As each user, `umask 002` before + creating a file. +- `find / -xdev -type f -perm /6000`. diff --git a/labs/lpic-1/104-5-permissions-and-ownership/check.sh b/labs/lpic-1/104-5-permissions-and-ownership/check.sh new file mode 100644 index 0000000..24cc0e4 --- /dev/null +++ b/labs/lpic-1/104-5-permissions-and-ownership/check.sh @@ -0,0 +1,53 @@ +mode() { stat -c %a "$1" 2>/dev/null; } +readable_file() { + local f + for f in /srv/test/*; do + [ -f "$f" ] && runuser -u tess -- cat "$f" >/dev/null 2>&1 && return 0 + done + return 1 +} +# Files in /srv/web from anna and from ben, all in group web and +# group-writable. +web_files() { + local f a=0 b=0 + for f in /srv/web/*; do + [ -f "$f" ] || continue + [ "$(stat -c %G "$f")" = web ] || return 1 + case $(stat -c %U "$f") in anna) a=1 ;; ben) b=1 ;; esac + [ $(( $(mode "$f") / 10 % 10 & 2 )) -ne 0 ] || return 1 + done + [ $a = 1 ] && [ $b = 1 ] +} +in_web() { id -nG "$1" 2>/dev/null | grep -qw web; } +setid_ok() { + local f n=0 all + for f in $(grep -oE '/[^[:space:]]+' /root/lab-answers/5 2>/dev/null); do + [ -u "$f" ] || [ -g "$f" ] || return 1 + n=$((n + 1)) + done + grep -Eq '/bin/passwd([[:space:]]|$)' /root/lab-answers/5 || return 1 + all=$(find / -xdev -type f -perm /6000 2>/dev/null | wc -l) + [ "$n" -ge $((all * 8 / 10)) ] +} + +task 1 "report.txt: owner reads and writes, group reads, others nothing" +need "/root/perm/report.txt has mode 640" '[ "$(mode /root/perm/report.txt)" = 640 ]' +need "/root/lab-answers/1 has the mode" '[ "$(answer 1 | sed "s/^0*//")" = 640 ]' + +task 2 "/srv/test: mode 711, its file readable by path only" +need "/srv/test has mode 711" '[ "$(mode /srv/test)" = 711 ]' +need "tess can cat a file in it by its full path" readable_file +need "tess can't list it" '! runuser -u tess -- ls /srv/test' + +task 3 "Files and directories made under umask 077 and 033" +need "f077 has mode 600 and d077 mode 700" '[ "$(mode /root/perm/f077)" = 600 ] && [ "$(mode /root/perm/d077)" = 700 ]' +need "f033 has mode 644 and d033 mode 744" '[ "$(mode /root/perm/f033)" = 644 ] && [ "$(mode /root/perm/d033)" = 744 ]' + +task 4 "/srv/web shared by anna and ben, mode 3770" +need "group web has anna and ben" 'in_web anna && in_web ben' +need "/srv/web belongs to group web" '[ "$(stat -c %G /srv/web)" = web ]' +need "/srv/web has mode 3770 (setgid and sticky)" '[ "$(mode /srv/web)" = 3770 ]' +need "anna and ben each have a file in it, in group web and group-writable" web_files + +task 5 "Setuid and setgid programs listed" +need "/root/lab-answers/5 lists the setuid and setgid programs, passwd among them" setid_ok diff --git a/labs/lpic-1/104-5-permissions-and-ownership/lab.conf b/labs/lpic-1/104-5-permissions-and-ownership/lab.conf new file mode 100644 index 0000000..a01c725 --- /dev/null +++ b/labs/lpic-1/104-5-permissions-and-ownership/lab.conf @@ -0,0 +1,3 @@ +TITLE="104.5 File permissions and ownership" +CHAPTER=/lpic-1/104-devices-filesystems-fhs/104-5-permissions-and-ownership/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/104-5-permissions-and-ownership/reset.sh b/labs/lpic-1/104-5-permissions-and-ownership/reset.sh new file mode 100644 index 0000000..ab275bf --- /dev/null +++ b/labs/lpic-1/104-5-permissions-and-ownership/reset.sh @@ -0,0 +1,3 @@ +rm -rf /root/perm /srv/test /srv/web +groupdel web 2>/dev/null +true diff --git a/labs/lpic-1/104-5-permissions-and-ownership/setup.sh b/labs/lpic-1/104-5-permissions-and-ownership/setup.sh new file mode 100644 index 0000000..85f3556 --- /dev/null +++ b/labs/lpic-1/104-5-permissions-and-ownership/setup.sh @@ -0,0 +1,6 @@ +mkdir -p /root/perm +echo "Quarterly numbers" >/root/perm/report.txt +chmod 644 /root/perm/report.txt +user_add tess +user_add anna +user_add ben diff --git a/labs/lpic-1/104-6-links/README.md b/labs/lpic-1/104-6-links/README.md new file mode 100644 index 0000000..8fb8a8e --- /dev/null +++ b/labs/lpic-1/104-6-links/README.md @@ -0,0 +1,41 @@ +# LPIC-1 104.6: Hard and symbolic links + +Hard links and symlinks side by side, the limits of hard links, directory +link counts, switching a `current` symlink, and following a chain of +links. + +## Before you start + +The lab creates an empty `/root/links/`. Work as root (`sudo -i`) and in +that directory, and write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Create `original.txt` with some text, make a hard link `hard.txt` and a + symlink `soft.txt` to it, and look at all three with `ls -li`. Delete + the original, then `cat` both links. +2. Try to hard-link a file onto another filesystem (`df` shows which + directories are on separate ones), and to hard-link a directory. Write + both error messages to `/root/lab-answers/2`. +3. Make a directory `parent`, check its link count, add two + subdirectories, and check again. Write the first count on line 1 of + `/root/lab-answers/3` and the second on line 2. +4. Set up `releases/1.0`, `releases/1.1` and a `current` symlink in + `releases/` pointing to `1.0`. Switch `current` to `1.1` with `ln -sf`, + and then with `ln -sfn`, and look at what each did. +5. On Debian or Ubuntu, follow `/usr/bin/editor` to the real program with + `readlink -f`, and with `ls -l` one link at a time. Write every name in + the chain, from `/usr/bin/editor` to the real program, one per line, to + `/root/lab-answers/5`. + +## Hints + +- `ln`, `ln -s`, `ls -li`; a symlink stores a name, a hard link is another + name for the same inode. +- `/tmp` is on the root filesystem on many systems; `/dev/shm` and `/run` + are tmpfs. `ln FILE /dev/shm/`; `ln DIR NEWNAME`. +- `ls -ld parent` (the second column), `stat -c %h parent`. +- Without `-n`, `ln -sf 1.1 current` follows `current` into `1.0` and + makes the link there. +- `readlink -f`, `ls -l /usr/bin/editor`, then `ls -l` on what it points + to. diff --git a/labs/lpic-1/104-6-links/check.sh b/labs/lpic-1/104-6-links/check.sh new file mode 100644 index 0000000..a9b8968 --- /dev/null +++ b/labs/lpic-1/104-6-links/check.sh @@ -0,0 +1,33 @@ +D=/root/links +current_ok() { + [ -L $D/releases/current ] && [ "$(readlink -f $D/releases/current)" = "$(readlink -f $D/releases/1.1)" ] && [ -d $D/releases/1.1 ] +} +chain_ok() { + local first last + first=$(grep -m1 . /root/lab-answers/5 2>/dev/null | awk '{print $NF}') + last=$(grep . /root/lab-answers/5 2>/dev/null | tail -n1 | awk '{print $NF}') + [ "$first" = /usr/bin/editor ] && [ -e "$last" ] && [ ! -L "$last" ] && + [ "$(readlink -f "$last")" = "$(readlink -f /usr/bin/editor)" ] && + grep -q /etc/alternatives/editor /root/lab-answers/5 +} + +task 1 "A hard link and a symlink, the original deleted" +need "original.txt is deleted" '[ ! -e $D/original.txt ] && [ ! -L $D/original.txt ]' +need "hard.txt still has the text" '[ -f $D/hard.txt ] && [ ! -L $D/hard.txt ] && [ -s $D/hard.txt ]' +need "soft.txt is a symlink to original.txt, now dangling" '[ -L $D/soft.txt ] && [ "$(basename "$(readlink $D/soft.txt)")" = original.txt ] && [ ! -e $D/soft.txt ]' + +task 2 "The two hard-link errors" +need "/root/lab-answers/2 has the cross-device error" 'grep -qi "cross-device" /root/lab-answers/2' +need "and the one about directories" 'grep -qi "directory" /root/lab-answers/2' + +task 3 "Link counts of a directory" +need "parent has two subdirectories" '[ "$(find $D/parent -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 2 ]' +need "/root/lab-answers/3 has 2, then 4" '[ "$(sed -n 1p /root/lab-answers/3 | tr -dc 0-9)" = 2 ] && [ "$(sed -n 2p /root/lab-answers/3 | tr -dc 0-9)" = 4 ]' + +task 4 "current switched to 1.1 with ln -sf, then ln -sfn" +need "releases/1.0 and releases/1.1 are directories" '[ -d $D/releases/1.0 ] && [ ! -L $D/releases/1.0 ] && [ -d $D/releases/1.1 ] && [ ! -L $D/releases/1.1 ]' +need "ln -sf left a 1.1 link inside releases/1.0" '[ -L $D/releases/1.0/1.1 ]' +need "current points to 1.1" current_ok + +task 5 "/usr/bin/editor followed to the real program" debian +need "/root/lab-answers/5 starts at /usr/bin/editor, goes through /etc/alternatives/editor and ends at the real program" chain_ok diff --git a/labs/lpic-1/104-6-links/lab.conf b/labs/lpic-1/104-6-links/lab.conf new file mode 100644 index 0000000..d120c9d --- /dev/null +++ b/labs/lpic-1/104-6-links/lab.conf @@ -0,0 +1,3 @@ +TITLE="104.6 Hard and symbolic links" +CHAPTER=/lpic-1/104-devices-filesystems-fhs/104-6-links/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/104-6-links/reset.sh b/labs/lpic-1/104-6-links/reset.sh new file mode 100644 index 0000000..44611b7 --- /dev/null +++ b/labs/lpic-1/104-6-links/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/links +true diff --git a/labs/lpic-1/104-6-links/setup.sh b/labs/lpic-1/104-6-links/setup.sh new file mode 100644 index 0000000..64283f8 --- /dev/null +++ b/labs/lpic-1/104-6-links/setup.sh @@ -0,0 +1,2 @@ +rm -rf /root/links +mkdir -p /root/links diff --git a/labs/lpic-1/104-7-fhs-and-finding-files/README.md b/labs/lpic-1/104-7-fhs-and-finding-files/README.md new file mode 100644 index 0000000..5b7ad89 --- /dev/null +++ b/labs/lpic-1/104-7-fhs-and-finding-files/README.md @@ -0,0 +1,38 @@ +# LPIC-1 104.7: The FHS and finding files + +Where a service's files live, the `locate` database and what it leaves +out, and the different ways to look up a command. + +## Before you start + +The lab installs `plocate` if it's missing, creates `/srv/archive/` with a +few old files in it, and builds the `locate` database. It saves +`/etc/updatedb.conf`; reset puts it back. Work as root (`sudo -i`), and +write answers to the files in `/root/lab-answers/`. + +## Tasks + +1. Take sshd. Find its program, configuration, logs, runtime files and man + pages, and say why each lives where it does. Write one path (or + command) per line to `/root/lab-answers/1`: the program, the main + configuration file, where its log messages go, a runtime file under + `/run`, and its man page. +2. Create a file, `locate` it, run `updatedb` as root, and `locate` it + again. Delete it and compare `locate` with `locate -e`. Write which of + the two still lists the deleted file to `/root/lab-answers/2`. +3. Add `/srv/archive` to `PRUNEPATHS` in `/etc/updatedb.conf`, run + `updatedb`, and show that its files are no longer found. +4. Compare `which -a`, `type -a` and `whereis` for `ls`, `passwd` and + `cd`. Write what `type -a cd` prints to `/root/lab-answers/4`. +5. Time `locate passwd` against `find / -name passwd 2>/dev/null` with + `time`. Check it yourself: `locate` answers in milliseconds. + +## Hints + +- `command -v sshd`, `systemctl cat sshd` (`ssh` on Ubuntu), `ls /etc/ssh`, + `journalctl -u`, `ls /run`, `man -w sshd`. +- `locate` searches a database that only `updatedb` refreshes; `-e` + checks that each file still exists. +- `PRUNEPATHS` is a space-separated list in double quotes. +- `type` knows about builtins and aliases; `which` and `whereis` only + about files. diff --git a/labs/lpic-1/104-7-fhs-and-finding-files/check.sh b/labs/lpic-1/104-7-fhs-and-finding-files/check.sh new file mode 100644 index 0000000..a4807d2 --- /dev/null +++ b/labs/lpic-1/104-7-fhs-and-finding-files/check.sh @@ -0,0 +1,32 @@ +sshd_ok() { + local a=/root/lab-answers/1 f ok_run='' + grep -Eq '(^|[[:space:]])(/usr)?/sbin/sshd([[:space:]]|$)' $a || return 1 + grep -q '/etc/ssh/sshd_config' $a || return 1 + grep -Eq 'journal|/var/log/' $a || return 1 + for f in $(grep -oE '/run/[^[:space:]]+' $a); do [ -e "$f" ] && ok_run=1; done + [ -n "$ok_run" ] || return 1 + for f in $(grep -oE '/usr/share/man/[^[:space:]]+' $a); do [ -e "$f" ] && return 0; done + return 1 +} +prune_ok() { + local p + p=$(sed -n 's/^[[:space:]]*PRUNEPATHS[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' /etc/updatedb.conf 2>/dev/null) + printf ' %s ' "$p" | grep -Eq ' /srv/archive/? ' +} +db() { ls /var/lib/plocate/plocate.db /var/lib/mlocate/mlocate.db 2>/dev/null | head -n1; } + +task 1 "Where sshd's files live" +need "/root/lab-answers/1 has sshd's program, config, logs, a runtime file and a man page" sshd_ok + +task 2 "locate and locate -e after deleting a file" +need "/root/lab-answers/2 names the one that still lists it" '[ "$(answer 2)" = locate ]' + +task 3 "/srv/archive left out of the locate database" +need "PRUNEPATHS in /etc/updatedb.conf has /srv/archive" prune_ok +need "updatedb has run since" '[ "$(stat -c %Y "$(db)")" -ge "$(stat -c %Y /etc/updatedb.conf)" ]' +need "locate no longer finds the files in /srv/archive" '[ -d /srv/archive ] && ! locate le-archive-report' + +task 4 "type -a for cd" +need "/root/lab-answers/4 has what type -a cd prints" 'grep -q "cd is a shell builtin" /root/lab-answers/4' + +task 5 "locate and find timed" self diff --git a/labs/lpic-1/104-7-fhs-and-finding-files/lab.conf b/labs/lpic-1/104-7-fhs-and-finding-files/lab.conf new file mode 100644 index 0000000..6cffb1d --- /dev/null +++ b/labs/lpic-1/104-7-fhs-and-finding-files/lab.conf @@ -0,0 +1,3 @@ +TITLE="104.7 The FHS and finding files" +CHAPTER=/lpic-1/104-devices-filesystems-fhs/104-7-fhs-and-finding-files/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/104-7-fhs-and-finding-files/reset.sh b/labs/lpic-1/104-7-fhs-and-finding-files/reset.sh new file mode 100644 index 0000000..0e96a54 --- /dev/null +++ b/labs/lpic-1/104-7-fhs-and-finding-files/reset.sh @@ -0,0 +1,5 @@ +if [ -f "$LAB_STATE/updatedb.conf" ]; then cp -a "$LAB_STATE/updatedb.conf" /etc/updatedb.conf +else sed -i 's| /srv/archive/\?||' /etc/updatedb.conf 2>/dev/null; fi +rm -rf /srv/archive +command -v updatedb >/dev/null && updatedb +true diff --git a/labs/lpic-1/104-7-fhs-and-finding-files/setup.sh b/labs/lpic-1/104-7-fhs-and-finding-files/setup.sh new file mode 100644 index 0000000..449871b --- /dev/null +++ b/labs/lpic-1/104-7-fhs-and-finding-files/setup.sh @@ -0,0 +1,5 @@ +command -v locate >/dev/null || pkg_install plocate +if [ -f /etc/updatedb.conf ]; then cp -a /etc/updatedb.conf "$LAB_STATE/updatedb.conf"; fi +mkdir -p /srv/archive +for y in 2019 2020 2021; do echo "Old report $y" >/srv/archive/le-archive-report-$y.txt; done +updatedb diff --git a/labs/lpic-1/105-1-shell-environment/README.md b/labs/lpic-1/105-1-shell-environment/README.md new file mode 100644 index 0000000..a32460a --- /dev/null +++ b/labs/lpic-1/105-1-shell-environment/README.md @@ -0,0 +1,29 @@ +# LPIC-1 105.1: Customizing the shell environment + +A personal `bin` directory in `PATH`, a shell function kept in `~/.bashrc`, +and the skeleton files new users get. + +## Before you start + +The lab creates user `alice` (password `Lab-Pass-1`). Do tasks 1 and 2 as +`alice`: log in as her, or run `sudo -iu alice`. + +## Tasks + +1. As `alice`, make sure `~/bin` is in her `PATH` permanently, and put an + executable script called `hello` there that prints a greeting. A new + login shell must run it by name: `sudo -iu alice hello`. +2. Write the `mkcd` function (make a directory, then change into it) and + keep it in `alice`'s `~/.bashrc`, so that it works in every new + interactive shell of hers. +3. Put a file called `welcome.txt` in `/etc/skel`, then create user `newbie` + with `useradd -m`, and check that the file is in the new home directory. + +## Hints + +- `echo "$PATH"`, `command -v hello`; `~/.profile` or `~/.bash_profile`, + and what the distribution's own startup files already do with `~/bin`. +- `#!/bin/bash`, `chmod +x`. +- `mkcd() { mkdir -p "$1" && cd "$1"; }`; test in a new shell with + `type mkcd`. A script can't do the same: why? +- `ls -A /etc/skel`, `useradd -m`, `ls -la ~newbie`. diff --git a/labs/lpic-1/105-1-shell-environment/check.sh b/labs/lpic-1/105-1-shell-environment/check.sh new file mode 100644 index 0000000..1e3e92c --- /dev/null +++ b/labs/lpic-1/105-1-shell-environment/check.sh @@ -0,0 +1,21 @@ +H=$(getent passwd alice | cut -d: -f6) +# login_run CMD: run CMD in a new login shell of alice's. +login_run() { sudo -niu alice bash -c "$1" /dev/null; } +# inter_run CMD: run CMD in a new interactive shell of alice's. +inter_run() { sudo -niu alice bash -ic "$1" /dev/null; } + +task 1 "~/bin in alice's PATH, with a script hello that runs by name" +need "$H/bin/hello is an executable file" '[ -f "$H/bin/hello" ] && [ -x "$H/bin/hello" ]' +need "alice's login shell has $H/bin in PATH" 'login_run "echo \$PATH" | tail -n1 | tr : "\n" | grep -Eqx "$H/bin/?|~/bin"' +need "hello runs by name in a new login shell" '[ "$(login_run "command -v hello" | tail -n1)" = "$H/bin/hello" ] && login_run hello | grep -q .' + +task 2 "mkcd in alice's ~/.bashrc" +need "mkcd is in $H/.bashrc" 'grep -Eq "mkcd" "$H/.bashrc"' +need "mkcd is a function in a new interactive shell" '[ "$(inter_run "type -t mkcd" | tail -n1)" = function ]' +need "mkcd /tmp/le-mkcd/a/b creates the directory and changes into it" 'rm -rf /tmp/le-mkcd; [ "$(inter_run "mkcd /tmp/le-mkcd/a/b && pwd" | tail -n1)" = /tmp/le-mkcd/a/b ] && [ -d /tmp/le-mkcd/a/b ]; r=$?; rm -rf /tmp/le-mkcd; exit $r' + +task 3 "/etc/skel/welcome.txt, copied into newbie's home" +need "/etc/skel/welcome.txt exists" '[ -f /etc/skel/welcome.txt ]' +need "user newbie exists" 'id newbie' +need "newbie's home directory has welcome.txt" '[ -f "$(getent passwd newbie | cut -d: -f6)/welcome.txt" ]' +need "newbie owns that welcome.txt" '[ "$(stat -c %U "$(getent passwd newbie | cut -d: -f6)/welcome.txt")" = newbie ]' diff --git a/labs/lpic-1/105-1-shell-environment/lab.conf b/labs/lpic-1/105-1-shell-environment/lab.conf new file mode 100644 index 0000000..53373f8 --- /dev/null +++ b/labs/lpic-1/105-1-shell-environment/lab.conf @@ -0,0 +1,3 @@ +TITLE="105.1 Customizing the shell environment" +CHAPTER=/lpic-1/105-shells-and-scripting/105-1-shell-environment/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/105-1-shell-environment/reset.sh b/labs/lpic-1/105-1-shell-environment/reset.sh new file mode 100644 index 0000000..e176867 --- /dev/null +++ b/labs/lpic-1/105-1-shell-environment/reset.sh @@ -0,0 +1,7 @@ +pkill -KILL -u newbie 2>/dev/null +userdel -r newbie 2>/dev/null +rm -f /etc/skel/welcome.txt +[ -f "$LAB_STATE/skel-welcome.txt" ] && cp -a "$LAB_STATE/skel-welcome.txt" /etc/skel/welcome.txt +rm -rf /tmp/le-mkcd +users_remove +true diff --git a/labs/lpic-1/105-1-shell-environment/setup.sh b/labs/lpic-1/105-1-shell-environment/setup.sh new file mode 100644 index 0000000..713215a --- /dev/null +++ b/labs/lpic-1/105-1-shell-environment/setup.sh @@ -0,0 +1,9 @@ +id newbie >/dev/null 2>&1 && refuse "user newbie exists already; remove it first (userdel -r newbie)" +user_add alice +h=$(getent passwd alice | cut -d: -f6) +rm -f "$h/bin/hello" +if [ -e /etc/skel/welcome.txt ]; then + cp -a /etc/skel/welcome.txt "$LAB_STATE/skel-welcome.txt" + rm -f /etc/skel/welcome.txt +fi +true diff --git a/labs/lpic-1/105-2-shell-scripts/README.md b/labs/lpic-1/105-2-shell-scripts/README.md new file mode 100644 index 0000000..bf6e6a9 --- /dev/null +++ b/labs/lpic-1/105-2-shell-scripts/README.md @@ -0,0 +1,35 @@ +# LPIC-1 105.2: Writing simple scripts + +Tests and exit codes, a script that mails root, loops and `case`. + +## Before you start + +The lab puts the chapter's `diskcheck` script in +`/usr/local/sbin/diskcheck`. It sets aside root's mailbox, if there is +one, so the check sees only new mail. Write your scripts in `/root`, start +each with a `#!` line, and make them executable. + +## Tasks + +1. Write `/root/checkfiles.sh`, which takes any number of file names and + prints one line for each: the name and whether it's `missing`, a + `directory` or a `readable` file. It exits with status 1 if any were + missing, and 0 otherwise. +2. Install a mail client and a local MTA (Postfix), then run + `/usr/local/sbin/diskcheck 5` as root and read root's mail. +3. Write `/root/count.sh`, which prints the numbers 1 to 10, one per line, + with a `while` loop and a counter (the chapter's `for i in $(seq 1 10)` + rewritten). +4. Write `/root/service.sh`, a `case` script that accepts `start`, `stop` + and `status` (printing what it would do), and prints a usage message for + anything else, including no argument. + +## Hints + +- `"$@"`, `[ -e ]`, `[ -d ]`, `[ -r ]`, `exit 1`; test with `echo $?`. +- The mail client is `s-nail` on the RHEL family and `mailutils` on Debian + and Ubuntu; it gives you `mail`. Postfix needs to be running. `mail` with + no arguments reads your mailbox. +- `i=1`, `while [ "$i" -le 10 ]`, `i=$((i + 1))`. +- `case "$1" in start) ... ;; *) echo "usage: ..." ;; esac`; then + `bash -x /root/service.sh start` to follow it step by step. diff --git a/labs/lpic-1/105-2-shell-scripts/check.sh b/labs/lpic-1/105-2-shell-scripts/check.sh new file mode 100644 index 0000000..e09a7bd --- /dev/null +++ b/labs/lpic-1/105-2-shell-scripts/check.sh @@ -0,0 +1,36 @@ +T=$(mktemp -d) +trap 'rm -rf "$T"' EXIT +mkdir "$T/somedir" +echo data >"$T/somefile" +# cf ARGS: run checkfiles.sh in $T; output in $T/out, lowercase; exit status. +cf() { (cd "$T" && /root/checkfiles.sh "$@" >"$T/out.raw" 2>&1); local r=$?; tr A-Z a-z <"$T/out.raw" >"$T/out"; return $r; } +line_for() { grep -E "(^|[^a-z0-9_.-])$1([^a-z0-9_.-]|$)" "$T/out"; } +# svc ARG: service.sh's output, lowercase. +svc() { timeout 10 /root/service.sh "$@" 2>&1 /usr/local/sbin/diskcheck <<'X' +#!/bin/bash +# diskcheck: mail root when a filesystem is fuller than the limit (default 90%) +limit=${1:-90} +report="" +while read -r pct mount; do + pct=${pct%\%} # "93%" becomes "93" + if [ "$pct" -gt "$limit" ]; then + report+="$mount is ${pct}% full"$'\n' + fi +done < <(df --output=pcent,target -x tmpfs -x devtmpfs -x squashfs -x overlay | tail -n +2) + +if [ -n "$report" ]; then + printf '%s' "$report" | mail -s "Disk space on $(hostname)" root +fi +X +chmod 755 /usr/local/sbin/diskcheck + +# Root's mail, set aside so only new messages count. +mkdir -p "$LAB_STATE/mail" +for f in /var/mail/root /var/spool/mail/root /root/mbox; do + [ -f "$f" ] || continue + cp -a "$f" "$LAB_STATE/mail/$(echo "$f" | tr / _)" + : >"$f" +done +rm -f /root/checkfiles.sh /root/count.sh /root/service.sh +true diff --git a/labs/lpic-1/107-1-users-and-groups/README.md b/labs/lpic-1/107-1-users-and-groups/README.md new file mode 100644 index 0000000..a97d741 --- /dev/null +++ b/labs/lpic-1/107-1-users-and-groups/README.md @@ -0,0 +1,38 @@ +# LPIC-1 107.1: User and group accounts + +Creating users and groups, supplementary groups, password ageing, system +accounts, and what deleting a user leaves behind. + +## Before you start + +The lab creates group `testers` and users `pat`, `ken` and `lou`. Their +password is `Lab-Pass-1`. `ken` owns a file outside his home directory. + +## Tasks + +1. Create group `developers`, and user `dana` with a home directory, the + comment `Dana Lee`, the shell `/bin/sh` and the supplementary group + `developers`. Read the new lines in `/etc/passwd`, `/etc/shadow` and + `/etc/group`, and the new home directory. +2. Add `dana` to a second group, `testers`, with `usermod -aG`. Then run + `usermod -G developers dana`, and write the name of the group she lost + to `/root/lab-answers/2`. +3. Make `dana`'s password expire every 30 days, with a 7-day warning. Check + with `chage -l` and the fields in `/etc/shadow`. +4. Force `pat` to change her password with `chage -d 0`, then log in as + `pat` (on the console, or with `ssh pat@localhost`) and see what happens. +5. Create a system account `appsvc` with a `nologin` shell, and try + `su - appsvc`. +6. Delete user `ken` with `userdel -r` and user `lou` without it. Then find + the files they left behind with `find / -xdev -nouser`, and write those + paths to `/root/lab-answers/6`, one per line. + +## Hints + +- `groupadd`, `useradd -m -c -s -G`, `getent passwd dana`, `ls -la`. +- `id dana` before and after; `-a` is what keeps the other groups. +- `chage -M -W`, `chage -l dana`; fields 5 and 6 of `/etc/shadow`. +- `chage -l pat` shows "password must be changed". +- `useradd -r -s` (the path of `nologin` is in `/etc/shells`, or try + `command -v nologin`). +- `userdel` with and without `-r`; `find / -xdev -nouser 2>/dev/null`. diff --git a/labs/lpic-1/107-1-users-and-groups/check.sh b/labs/lpic-1/107-1-users-and-groups/check.sh new file mode 100644 index 0000000..b0b488b --- /dev/null +++ b/labs/lpic-1/107-1-users-and-groups/check.sh @@ -0,0 +1,37 @@ +sh_field() { getent shadow "$1" | cut -d: -f"$2"; } +SYS_MAX=$(awk '$1 == "SYS_UID_MAX" {print $2}' /etc/login.defs) +SYS_MAX=${SYS_MAX:-999} + +task 1 "Group developers; dana with home, comment, /bin/sh and that group" +need "group developers exists" 'getent group developers' +need "user dana exists" 'id dana' +need "dana has a home directory" '[ -d "$(getent passwd dana | cut -d: -f6)" ]' +need "dana's comment is Dana Lee" '[ "$(getent passwd dana | cut -d: -f5 | cut -d, -f1)" = "Dana Lee" ]' +need "dana's shell is /bin/sh" '[ "$(getent passwd dana | cut -d: -f7)" = /bin/sh ]' +need "dana is in developers" 'id -nG dana | grep -qw developers' + +task 2 "usermod -G without -a, and the lost group in /root/lab-answers/2" +need "dana is in developers" 'id -nG dana | grep -qw developers' +need "dana isn't in testers any more" '! id -nG dana | grep -qw testers' +need "the answer is testers" '[ "$(answer 2)" = testers ]' + +task 3 "dana's password expires every 30 days, warning 7 days before" +need "maximum password age is 30" '[ "$(sh_field dana 5)" = 30 ]' +need "warning period is 7 days" '[ "$(sh_field dana 6)" = 7 ]' + +task 4 "pat must change her password (or already has)" +need "user pat exists" 'id pat' +need "pat's password must be changed at next login, or has been changed" '[ "$(sh_field pat 3)" = 0 ] || [ "$(sh_field pat 2)" != "$(state_get pat-hash)" ]' + +task 5 "System account appsvc with a nologin shell" +need "user appsvc exists" 'id appsvc' +need "appsvc is a system account (UID $SYS_MAX or below)" '[ "$(id -u appsvc)" -le "$SYS_MAX" ]' +need "appsvc's shell is nologin" 'getent passwd appsvc | cut -d: -f7 | grep -Eq "/nologin$"' + +task 6 "ken and lou deleted, their leftovers in /root/lab-answers/6" +need "user ken is gone" '! getent passwd ken' +need "user lou is gone" '! getent passwd lou' +need "ken's home directory is gone" '[ ! -e /home/ken ]' +need "lou's home directory is still there" '[ -d /home/lou ]' +need "the answer lists /var/tmp/ken-report.txt" 'grep -qx "/var/tmp/ken-report.txt" /root/lab-answers/6' +need "the answer lists /home/lou" 'grep -qx "/home/lou/\?" /root/lab-answers/6' diff --git a/labs/lpic-1/107-1-users-and-groups/lab.conf b/labs/lpic-1/107-1-users-and-groups/lab.conf new file mode 100644 index 0000000..871ce12 --- /dev/null +++ b/labs/lpic-1/107-1-users-and-groups/lab.conf @@ -0,0 +1,3 @@ +TITLE="107.1 User and group accounts" +CHAPTER=/lpic-1/107-administrative-tasks/107-1-users-and-groups/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/107-1-users-and-groups/reset.sh b/labs/lpic-1/107-1-users-and-groups/reset.sh new file mode 100644 index 0000000..ca87f00 --- /dev/null +++ b/labs/lpic-1/107-1-users-and-groups/reset.sh @@ -0,0 +1,11 @@ +for u in dana appsvc; do + pkill -KILL -u "$u" 2>/dev/null + userdel -r "$u" 2>/dev/null +done +groupdel developers 2>/dev/null +users_remove +rm -rf /home/ken /home/lou /var/spool/mail/ken /var/spool/mail/lou /var/mail/ken /var/mail/lou +rm -f /var/tmp/ken-report.txt +for g in ken lou; do getent group "$g" >/dev/null && [ -z "$(getent group "$g" | cut -d: -f4)" ] && groupdel "$g" 2>/dev/null; done +[ -n "$(state_get made-testers)" ] && groupdel testers 2>/dev/null +true diff --git a/labs/lpic-1/107-1-users-and-groups/setup.sh b/labs/lpic-1/107-1-users-and-groups/setup.sh new file mode 100644 index 0000000..343b136 --- /dev/null +++ b/labs/lpic-1/107-1-users-and-groups/setup.sh @@ -0,0 +1,13 @@ +for u in dana appsvc; do + id "$u" >/dev/null 2>&1 && refuse "user $u exists already; remove it first (userdel -r $u)" +done +getent group developers >/dev/null && refuse "group developers exists already; remove it first (groupdel developers)" +getent group testers >/dev/null || { groupadd testers; state_set made-testers 1; } +user_add pat +user_add ken +user_add lou +state_set pat-hash "$(getent shadow pat | cut -d: -f2)" +# A file ken owns outside his home directory. +echo "ken's report" >/var/tmp/ken-report.txt +chown ken: /var/tmp/ken-report.txt +true diff --git a/labs/lpic-1/107-2-scheduling-jobs/README.md b/labs/lpic-1/107-2-scheduling-jobs/README.md new file mode 100644 index 0000000..7c0f980 --- /dev/null +++ b/labs/lpic-1/107-2-scheduling-jobs/README.md @@ -0,0 +1,38 @@ +# LPIC-1 107.2: Scheduling jobs + +A user crontab, one-off jobs with `at`, who may use cron, and the same job +as a systemd timer. + +## Before you start + +The lab creates user `alice` (password `Lab-Pass-1`) and makes sure cron is +installed and running. `at` isn't set up: installing it and starting `atd` +is part of task 2. Tasks 1 and 2 need a few minutes of waiting. + +## Tasks + +1. As `alice`, add a crontab entry that appends the output of `date` to + `~/date.log` every two minutes. Wait until the file has a line or two, + then remove the entry. +2. Queue a job with `at now + 2 minutes` that writes `first` to + `/var/tmp/at-first`. List it with `atq`, read it with `at -c`, and let it + run. Then queue another that would write `second` to `/var/tmp/at-second`, + write its job number to `/root/lab-answers/2`, and remove it with + `atrm`. +3. Put `alice` in `/etc/cron.deny` and try `crontab -e` as her. Then create + `/etc/cron.allow` without her name, and try again. +4. Turn the two-minute job into `datelog.service` and `datelog.timer`: the + service runs as `alice` and appends `date` to `/home/alice/date.log`, and + the timer runs it with `OnCalendar=*:0/2`. Enable and start the timer, + and watch `systemctl list-timers` and `journalctl -u datelog`. + +## Hints + +- `crontab -e`, `crontab -l`, `*/2 * * * *`; `crontab -r` or edit the line + out. +- The `at` package and its `atd` service; `atq`, `at -c`, `atrm`. +- `/etc/cron.deny`, `/etc/cron.allow`; `crontab -l` as `alice` shows the + refusal. +- `[Service]` with `Type=oneshot`, `User=alice`, and a shell for the `>>`: + `ExecStart=/bin/sh -c '...'`. `[Timer]` and `[Install] + WantedBy=timers.target`. `systemd-analyze calendar '*:0/2'`. diff --git a/labs/lpic-1/107-2-scheduling-jobs/check.sh b/labs/lpic-1/107-2-scheduling-jobs/check.sh new file mode 100644 index 0000000..53acfb9 --- /dev/null +++ b/labs/lpic-1/107-2-scheduling-jobs/check.sh @@ -0,0 +1,49 @@ +SINCE=$(state_get since) +H=$(getent passwd alice | cut -d: -f6) +# cron_ran: cron logged a job of alice's since setup, in the journal or a +# log file. +cron_ran() { + local f n + journalctl -q --since "$SINCE" --no-pager 2>/dev/null | grep -q "(alice) CMD" && return 0 + for f in /var/log/cron /var/log/syslog; do + [ -f "$f" ] || continue + n=$(state_get "size$(echo "$f" | tr / _)"); n=${n:-0} + [ "$(stat -c %s "$f")" -ge "$n" ] || n=0 + tail -c +$((n + 1)) "$f" | grep -q "(alice) CMD" && return 0 + done + return 1 +} +lines() { wc -l <"$1" 2>/dev/null || echo 0; } + +task 1 "A two-minute crontab entry for alice that ran, then was removed" +need "$H/date.log has a line" '[ -s "$H/date.log" ]' +need "cron ran a job as alice" cron_ran +need "alice's crontab no longer has the entry" '! cat /var/spool/cron/alice /var/spool/cron/crontabs/alice 2>/dev/null | grep -Ev "^[[:space:]]*#" | grep -q date' + +task 2 "An at job that ran, and another removed with atrm" +need "atd is running" active atd +need "atd starts at boot" enabled atd +need "/var/tmp/at-first has first" 'grep -qx first /var/tmp/at-first' +need "/root/lab-answers/2 holds a job number" '[[ "$(answer 2)" =~ ^[0-9]+$ ]]' +need "that job is no longer queued" '! atq | awk "{print \$1}" | grep -qx "$(answer 2)"' +need "that job was created" ' + seq=$(cat /var/spool/at/.SEQ /var/spool/cron/atjobs/.SEQ 2>/dev/null | head -n1) + [ -n "$seq" ] && [ "$(answer 2)" -ge 1 ] && [ "$(answer 2)" -le "$((16#$seq))" ]' +need "the second job didn't run (/var/tmp/at-second doesn't exist)" '[ ! -e /var/tmp/at-second ]' + +task 3 "alice in cron.deny, then a cron.allow without her" +need "/etc/cron.deny lists alice" 'grep -qx alice /etc/cron.deny' +need "/etc/cron.allow exists" '[ -f /etc/cron.allow ]' +need "/etc/cron.allow doesn't list alice" '! grep -qx alice /etc/cron.allow' +need "alice is refused by crontab" 'runuser -u alice -- crontab -l 2>&1 | grep -qi "not allowed"' + +task 4 "datelog.timer runs datelog.service as alice every two minutes" +need "datelog.service exists" 'systemctl cat datelog.service' +need "datelog.timer is enabled" enabled datelog.timer +need "datelog.timer is active" active datelog.timer +need "datelog.timer runs every two minutes" 'systemctl show -p TimersCalendar --value datelog.timer | grep -q "OnCalendar=\*-\*-\* \*:00/2:00 "' +need "datelog.timer starts datelog.service" '[ "$(systemctl show -p Unit --value datelog.timer)" = datelog.service ]' +need "datelog.service runs as alice" '[ "$(systemctl show -p User --value datelog.service)" = alice ]' +need "running datelog.service appends a line to $H/date.log" ' + n=$(lines "$H/date.log"); systemctl start datelog.service; sleep 1 + [ "$(lines "$H/date.log")" -gt "$n" ]' diff --git a/labs/lpic-1/107-2-scheduling-jobs/lab.conf b/labs/lpic-1/107-2-scheduling-jobs/lab.conf new file mode 100644 index 0000000..09dbca5 --- /dev/null +++ b/labs/lpic-1/107-2-scheduling-jobs/lab.conf @@ -0,0 +1,3 @@ +TITLE="107.2 Scheduling jobs" +CHAPTER=/lpic-1/107-administrative-tasks/107-2-scheduling-jobs/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/107-2-scheduling-jobs/reset.sh b/labs/lpic-1/107-2-scheduling-jobs/reset.sh new file mode 100644 index 0000000..7ad576d --- /dev/null +++ b/labs/lpic-1/107-2-scheduling-jobs/reset.sh @@ -0,0 +1,14 @@ +command -v atq >/dev/null && for j in $(atq | awk '{print $1}'); do + at -c "$j" 2>/dev/null | grep -q '/var/tmp/at-' && atrm "$j" +done +systemctl disable --quiet --now datelog.timer 2>/dev/null +systemctl stop datelog.service 2>/dev/null +rm -f /etc/systemd/system/datelog.timer /etc/systemd/system/datelog.service +systemctl daemon-reload +rm -f /var/tmp/at-first /var/tmp/at-second +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 +crontab -u alice -r >/dev/null 2>&1 +users_remove +true diff --git a/labs/lpic-1/107-2-scheduling-jobs/setup.sh b/labs/lpic-1/107-2-scheduling-jobs/setup.sh new file mode 100644 index 0000000..edf9045 --- /dev/null +++ b/labs/lpic-1/107-2-scheduling-jobs/setup.sh @@ -0,0 +1,18 @@ +user_add alice +pkg_installed "$(pick cronie cron)" || pkg_install cronie/cron +systemctl enable --quiet --now "$(pick crond cron)" +state_set since "$(date '+%Y-%m-%d %H:%M:%S')" +# Where cron logs, and how much is there already: the journal may not last +# across a reboot. +for f in /var/log/cron /var/log/syslog; do + [ -f "$f" ] && state_set "size$(echo "$f" | tr / _)" "$(stat -c %s "$f")" +done +# Note the access files so reset can put them back, and start without them. +for f in cron.allow cron.deny; do + [ -e /etc/$f ] && cp -a /etc/$f "$LAB_STATE/$f" +done +rm -f /etc/cron.allow +[ -e /etc/cron.deny ] && sed -i '/^alice$/d' /etc/cron.deny +crontab -u alice -r >/dev/null 2>&1 || true +rm -f /var/tmp/at-first /var/tmp/at-second +true diff --git a/labs/lpic-1/107-3-localization/README.md b/labs/lpic-1/107-3-localization/README.md new file mode 100644 index 0000000..39b3013 --- /dev/null +++ b/labs/lpic-1/107-3-localization/README.md @@ -0,0 +1,31 @@ +# LPIC-1 107.3: Localization and internationalization + +A second locale, what it changes, sort order, and converting a file from +Latin-1 to UTF-8. + +## Before you start + +The lab writes a short list of words in mixed case to `/root/words.txt`. +The `de_DE.UTF-8` locale must not be installed yet; the lab refuses to start +if it is. + +## Tasks + +1. Install the `de_DE.UTF-8` locale. Compare `date`, `ls -l` and + `printf "%'d\n" 1234567` under it and under your normal locale, with + `LC_ALL=` in front of each. Write what the `printf` prints under + `de_DE.UTF-8` to `/root/lab-answers/1`. +2. Sort `/root/words.txt` under `LC_ALL=C` into `/root/lab-answers/2-c`, and + under `LC_ALL=de_DE.UTF-8` into `/root/lab-answers/2-de`, and compare. +3. Make a Latin-1 file with `printf 'caf\xe9\n' > /root/latin1.txt`, check it + with `file`, look at it with `cat`, and convert it to UTF-8 with `iconv` + as `/root/utf8.txt`. + +## Hints + +- RHEL family: `dnf install glibc-langpack-de`. Debian and Ubuntu: + `locale-gen de_DE.UTF-8` (or uncomment it in `/etc/locale.gen` and run + `locale-gen`). Check with `locale -a`. Start a new shell after + installing it. +- `LC_ALL=C sort /root/words.txt > ...`; `diff` the two. +- `iconv -f LATIN1 -t UTF-8`; `file` on both files. diff --git a/labs/lpic-1/107-3-localization/check.sh b/labs/lpic-1/107-3-localization/check.sh new file mode 100644 index 0000000..d9400d1 --- /dev/null +++ b/labs/lpic-1/107-3-localization/check.sh @@ -0,0 +1,11 @@ +task 1 "de_DE.UTF-8 installed; its printf output in /root/lab-answers/1" +need "locale -a lists de_DE.utf8" 'locale -a | grep -qix "de_DE.utf-\?8"' +need "the answer is 1.234.567" '[ "$(answer 1)" = 1.234.567 ]' + +task 2 "words.txt sorted under C and under de_DE.UTF-8" +need "/root/lab-answers/2-c is words.txt sorted under LC_ALL=C" '[ -s /root/lab-answers/2-c ] && LC_ALL=C sort /root/words.txt | cmp -s - /root/lab-answers/2-c' +need "/root/lab-answers/2-de is words.txt sorted under LC_ALL=de_DE.UTF-8" '[ -s /root/lab-answers/2-de ] && LC_ALL=de_DE.UTF-8 sort /root/words.txt | cmp -s - /root/lab-answers/2-de' + +task 3 "latin1.txt converted to UTF-8 as utf8.txt" +need "/root/latin1.txt holds caf\\xe9 in Latin-1" 'printf "caf\xe9\n" | cmp -s - /root/latin1.txt' +need "/root/utf8.txt holds café in UTF-8" 'printf "caf\xc3\xa9\n" | cmp -s - /root/utf8.txt' diff --git a/labs/lpic-1/107-3-localization/lab.conf b/labs/lpic-1/107-3-localization/lab.conf new file mode 100644 index 0000000..12035b8 --- /dev/null +++ b/labs/lpic-1/107-3-localization/lab.conf @@ -0,0 +1,3 @@ +TITLE="107.3 Localization and internationalization" +CHAPTER=/lpic-1/107-administrative-tasks/107-3-localization/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/107-3-localization/reset.sh b/labs/lpic-1/107-3-localization/reset.sh new file mode 100644 index 0000000..f1f142e --- /dev/null +++ b/labs/lpic-1/107-3-localization/reset.sh @@ -0,0 +1,17 @@ +rm -f /root/words.txt /root/latin1.txt /root/utf8.txt +# The locale, if the lab's task installed it. +if [ -n "$(state_get langpack)" ] && pkg_installed glibc-langpack-de; then + dnf -y -q remove glibc-langpack-de >/dev/null 2>&1 +fi +if is_debian; then + if [ -f "$LAB_STATE/locale.gen" ]; then cp -a "$LAB_STATE/locale.gen" /etc/locale.gen; fi + for f in /var/lib/locales/supported.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/supported-d" 2>/dev/null || rm -f "$f" + done + for l in $(localedef --list-archive 2>/dev/null | grep -i '^de_DE'); do + localedef --delete-from-archive "$l" 2>/dev/null + done +fi +rm -rf /usr/lib/locale/de_DE.utf8 /usr/lib/locale/de_DE.UTF-8 +true diff --git a/labs/lpic-1/107-3-localization/setup.sh b/labs/lpic-1/107-3-localization/setup.sh new file mode 100644 index 0000000..36aa384 --- /dev/null +++ b/labs/lpic-1/107-3-localization/setup.sh @@ -0,0 +1,8 @@ +locale -a 2>/dev/null | grep -qix 'de_DE.utf-\?8' && + refuse "the de_DE.UTF-8 locale is installed already; task 1 installs it" +printf '%s\n' banana Apple cherry apple Banana Zebra zebra Cherry >/root/words.txt +rm -f /root/latin1.txt /root/utf8.txt +is_rhel && ! pkg_installed glibc-langpack-de && state_set langpack 1 +[ -f /etc/locale.gen ] && cp -a /etc/locale.gen "$LAB_STATE/locale.gen" +ls /var/lib/locales/supported.d >"$LAB_STATE/supported-d" 2>/dev/null || true +true diff --git a/labs/lpic-1/108-1-system-time/README.md b/labs/lpic-1/108-1-system-time/README.md new file mode 100644 index 0000000..427ae6e --- /dev/null +++ b/labs/lpic-1/108-1-system-time/README.md @@ -0,0 +1,33 @@ +# LPIC-1 108.1: Maintaining system time + +Which NTP client runs, the hardware clock, and chrony's sources. + +## Before you start + +The lab notes which NTP client runs now, and saves chrony's configuration +(if chrony is installed) so reset can put it back. Changing the NTP sources +needs the VM to reach the internet on UDP port 123 to see them work, but the +check doesn't depend on that. + +## Tasks + +1. Run `timedatectl` and find out which NTP client the VM uses + (`systemctl status chronyd chrony systemd-timesyncd`). Write the name of + its service to `/root/lab-answers/1`. +2. Compare `sudo hwclock` with `date -u`. Then read `/etc/adjtime`, if it + exists. Write `utc` or `local` to `/root/lab-answers/2`: the time the + hardware clock keeps. +3. Install chrony if it isn't there (it replaces timesyncd), and read + `chronyc sources -v` and `chronyc tracking`. Then change chrony's + sources to just the pool `pool.ntp.org`, restart it, and watch the + sources change with `chronyc sources`. + +## Hints + +- `systemctl is-active chronyd chrony systemd-timesyncd`. +- The third line of `/etc/adjtime` says `UTC` or `LOCAL`; without the file, + the hardware clock is taken to be UTC. `timedatectl` shows it as "RTC in + local TZ". +- `/etc/chrony.conf` on the RHEL family; `/etc/chrony/chrony.conf` and its + `sources.d` directory on Debian and Ubuntu. Comment out the other `pool` + and `server` lines. `chronyc -N sources` shows the names. diff --git a/labs/lpic-1/108-1-system-time/check.sh b/labs/lpic-1/108-1-system-time/check.sh new file mode 100644 index 0000000..f5a9d4f --- /dev/null +++ b/labs/lpic-1/108-1-system-time/check.sh @@ -0,0 +1,18 @@ +CONF="/etc/chrony.conf /etc/chrony.d /etc/chrony/chrony.conf /etc/chrony/conf.d /etc/chrony/sources.d" +# Every source line in chrony's configuration. +sources() { grep -rhsE '^[[:space:]]*(pool|server|peer)[[:space:]]' $CONF | awk '{print $1, $2}'; } +CLIENT=$(state_get client) + +task 1 "The NTP client's service in /root/lab-answers/1" +need "the answer names the NTP client" 'answer 1 | grep -q "$CLIENT"' + +task 2 "utc or local, for the hardware clock, in /root/lab-answers/2" +need "the answer is right" '[ "$(answer 2)" = "$(state_get rtc)" ]' + +task 3 "chrony running with the single pool pool.ntp.org" +need "chrony is installed" 'pkg_installed chrony' +need "chrony is running" 'active "$(pick chronyd chrony)"' +need "chrony starts at boot" 'enabled "$(pick chronyd chrony)"' +need "chrony's configuration has pool pool.ntp.org" 'sources | grep -qx "pool pool.ntp.org"' +need "it has no other pool or server lines" '! sources | grep -vqx "pool pool.ntp.org"' +need "the running chronyd uses no other source names (restart it)" '! chronyc -n -N sources 2>/dev/null | awk "/^\^/ {print \$2}" | grep -v "pool\.ntp\.org" | grep -Evq "^[0-9.]+$|:"' diff --git a/labs/lpic-1/108-1-system-time/lab.conf b/labs/lpic-1/108-1-system-time/lab.conf new file mode 100644 index 0000000..9df1ac7 --- /dev/null +++ b/labs/lpic-1/108-1-system-time/lab.conf @@ -0,0 +1,3 @@ +TITLE="108.1 Maintaining system time" +CHAPTER=/lpic-1/108-essential-system-services/108-1-system-time/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/108-1-system-time/reset.sh b/labs/lpic-1/108-1-system-time/reset.sh new file mode 100644 index 0000000..bc64aa1 --- /dev/null +++ b/labs/lpic-1/108-1-system-time/reset.sh @@ -0,0 +1,21 @@ +if [ -n "$(state_get had-chrony)" ]; then + if is_rhel; then + [ -f "$LAB_STATE/chrony.conf" ] && cp -a "$LAB_STATE/chrony.conf" /etc/chrony.conf + elif [ -f "$LAB_STATE/chrony.tar" ]; then + rm -rf /etc/chrony + tar -C /etc -xf "$LAB_STATE/chrony.tar" + fi + systemctl restart "$(pick chronyd chrony)" 2>/dev/null +elif pkg_installed chrony; then + # The task installed it: take it out again. + if is_rhel; then + dnf -y -q remove chrony >/dev/null 2>&1 + else + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge chrony >/dev/null 2>&1 + fi +fi +if [ -n "$(state_get had-timesyncd)" ] && ! pkg_installed systemd-timesyncd; then + pkg_install systemd-timesyncd + systemctl enable --now --quiet systemd-timesyncd +fi +true diff --git a/labs/lpic-1/108-1-system-time/setup.sh b/labs/lpic-1/108-1-system-time/setup.sh new file mode 100644 index 0000000..f700b82 --- /dev/null +++ b/labs/lpic-1/108-1-system-time/setup.sh @@ -0,0 +1,21 @@ +if systemctl is-active --quiet chronyd || systemctl is-active --quiet chrony; then + state_set client chrony +elif systemctl is-active --quiet systemd-timesyncd; then + state_set client timesyncd +elif systemctl is-active --quiet ntpd || systemctl is-active --quiet ntp || systemctl is-active --quiet ntpsec; then + state_set client ntp +else + refuse "no NTP client is running; start chronyd or systemd-timesyncd first" +fi +if [ -f /etc/adjtime ] && [ "$(sed -n 3p /etc/adjtime)" = LOCAL ]; then + state_set rtc local +else + state_set rtc utc +fi +# chrony's configuration, as it is now. +if pkg_installed chrony; then + state_set had-chrony 1 + if is_rhel; then cp -a /etc/chrony.conf "$LAB_STATE/chrony.conf"; else tar -C /etc -cf "$LAB_STATE/chrony.tar" chrony; fi +fi +is_debian && pkg_installed systemd-timesyncd && state_set had-timesyncd 1 +true diff --git a/labs/lpic-1/108-2-system-logging/README.md b/labs/lpic-1/108-2-system-logging/README.md new file mode 100644 index 0000000..55f9582 --- /dev/null +++ b/labs/lpic-1/108-2-system-logging/README.md @@ -0,0 +1,36 @@ +# LPIC-1 108.2: System logging + +`logger` and the journal, an rsyslog rule, a persistent journal, and a +logrotate rule. + +## Before you start + +The lab makes the journal non-persistent: if `/var/log/journal` exists, it +moves it to `/var/log/journal.le-lab` (reset puts it back), so the journal +lives in `/run` and is lost at every reboot. Task 3 needs a reboot; do task +1 again afterwards if its message was lost with it. + +## Tasks + +1. Send a message with `logger -t test -p user.warning` and find it with + `journalctl -t test` and, if rsyslog is installed, in `/var/log`. +2. Install rsyslog if needed, add a rule sending `local3.*` to + `/var/log/local3.log`, restart it, and test with + `logger -p local3.info`. +3. Check whether the journal is persistent with `ls /var/log/journal`. Make + it persistent, reboot, and read the previous boot with + `journalctl -b -1`. +4. As root, write a logrotate rule `/etc/logrotate.d/lrtest` for + `/var/log/lrtest/test.log`, fill the log, and run `logrotate -d` and then + `logrotate -f` on the rule, so that the log is rotated. + +## Hints + +- `journalctl -t test -p warning`; `grep test /var/log/syslog` or + `/var/log/messages`. +- A file in `/etc/rsyslog.d/` ending in `.conf`; `rsyslogd -N1` checks the + configuration. +- `Storage=persistent` in `/etc/systemd/journald.conf` (or a drop-in), or + create `/var/log/journal`; `journalctl --list-boots`. +- `mkdir /var/log/lrtest`; a block with `rotate`, `size` or `daily`, + `compress`, `missingok`; `logrotate -d /etc/logrotate.d/lrtest`. diff --git a/labs/lpic-1/108-2-system-logging/check.sh b/labs/lpic-1/108-2-system-logging/check.sh new file mode 100644 index 0000000..ddadf89 --- /dev/null +++ b/labs/lpic-1/108-2-system-logging/check.sh @@ -0,0 +1,23 @@ +SINCE=$(state_get since) +storage=$(systemd-analyze cat-config systemd/journald.conf 2>/dev/null | sed -n 's/^[[:space:]]*Storage=//p' | tail -n1) +# wait_for FILE TEXT: up to 10 seconds for TEXT to show up in FILE. +wait_for() { local i; for i in $(seq 10); do grep -qsF "$2" "$1" && return 0; sleep 1; done; return 1; } + +task 1 "A user.warning message tagged test in the journal" +need "journalctl -t test -p warning shows a message since the lab started" 'journalctl -q --no-pager --since "$SINCE" -t test -p warning..warning | grep -q .' + +task 2 "rsyslog sends local3.* to /var/log/local3.log" +need "rsyslog is running" active rsyslog +need "a rule sends local3.* to /var/log/local3.log" 'grep -rhsEv "^[[:space:]]*#" /etc/rsyslog.conf /etc/rsyslog.d/*.conf | grep -E "local3\.(\*|info|debug)" | grep -q "/var/log/local3\.log"' +need "logger -p local3.info reaches /var/log/local3.log" 'm="le-lab check $$-$RANDOM"; logger -p local3.info "$m"; wait_for /var/log/local3.log "$m"' + +task 3 "A persistent journal, with the previous boot readable" +need "/var/log/journal exists" '[ -d /var/log/journal ]' +need "journald isn't set to volatile or none" '[ "$storage" != volatile ] && [ "$storage" != none ]' +need "the journal is written to /var/log/journal" 'ls /var/log/journal/*/system*.journal' +need "journalctl -b -1 shows the previous boot" 'journalctl -q --no-pager -b -1 -n 1 | grep -q .' + +task 4 "A logrotate rule for /var/log/lrtest/test.log, rotated" +need "/etc/logrotate.d/lrtest is a rule for /var/log/lrtest/test.log" 'grep -q "/var/log/lrtest/" /etc/logrotate.d/lrtest' +need "logrotate -d accepts the rule" 'logrotate -d /etc/logrotate.d/lrtest 2>&1 | grep -qi "error" && exit 1; logrotate -d /etc/logrotate.d/lrtest' +need "/var/log/lrtest has a rotated log" 'ls /var/log/lrtest/ | grep -Eq "^test\.log[.-][0-9]"' diff --git a/labs/lpic-1/108-2-system-logging/lab.conf b/labs/lpic-1/108-2-system-logging/lab.conf new file mode 100644 index 0000000..2bbb687 --- /dev/null +++ b/labs/lpic-1/108-2-system-logging/lab.conf @@ -0,0 +1,3 @@ +TITLE="108.2 System logging" +CHAPTER=/lpic-1/108-essential-system-services/108-2-system-logging/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/108-2-system-logging/reset.sh b/labs/lpic-1/108-2-system-logging/reset.sh new file mode 100644 index 0000000..7473453 --- /dev/null +++ b/labs/lpic-1/108-2-system-logging/reset.sh @@ -0,0 +1,27 @@ +# journald as it was. +if [ -f "$LAB_STATE/journald.conf" ]; then cp -a "$LAB_STATE/journald.conf" /etc/systemd/journald.conf; fi +for f in /etc/systemd/journald.conf.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/journald-d" 2>/dev/null || rm -f "$f" +done +journalctl --relinquish-var 2>/dev/null +if [ -n "$(state_get moved-journal)" ]; then + if [ -d /var/log/journal.le-lab ]; then + rm -rf /var/log/journal + mv /var/log/journal.le-lab /var/log/journal + fi +else + rm -rf /var/log/journal +fi +systemctl restart systemd-journald +journalctl --flush 2>/dev/null +# rsyslog as it was. +if [ -f "$LAB_STATE/rsyslog.conf" ]; then cp -a "$LAB_STATE/rsyslog.conf" /etc/rsyslog.conf; fi +for f in /etc/rsyslog.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/rsyslog-d" 2>/dev/null || rm -f "$f" +done +rm -f /var/log/local3.log +systemctl is-active --quiet rsyslog && systemctl restart rsyslog +rm -rf /var/log/lrtest /etc/logrotate.d/lrtest +true diff --git a/labs/lpic-1/108-2-system-logging/setup.sh b/labs/lpic-1/108-2-system-logging/setup.sh new file mode 100644 index 0000000..46c88c9 --- /dev/null +++ b/labs/lpic-1/108-2-system-logging/setup.sh @@ -0,0 +1,19 @@ +storage=$(systemd-analyze cat-config systemd/journald.conf 2>/dev/null | sed -n 's/^[[:space:]]*Storage=//p' | tail -n1) +[ "$storage" = persistent ] && + refuse "journald is set to Storage=persistent already; task 3 does that. Remove the setting first." +pkg_installed rsyslog && state_set had-rsyslog 1 +state_set since "$(date '+%Y-%m-%d %H:%M:%S')" +# journald's and rsyslog's configuration, for reset. +[ -f /etc/systemd/journald.conf ] && cp -a /etc/systemd/journald.conf "$LAB_STATE/journald.conf" +ls /etc/systemd/journald.conf.d >"$LAB_STATE/journald-d" 2>/dev/null || true +[ -f /etc/rsyslog.conf ] && cp -a /etc/rsyslog.conf "$LAB_STATE/rsyslog.conf" +ls /etc/rsyslog.d >"$LAB_STATE/rsyslog-d" 2>/dev/null || true +# A journal kept only in /run. +if [ -d /var/log/journal ]; then + [ -e /var/log/journal.le-lab ] && refuse "/var/log/journal.le-lab exists already; move it out of the way first" + journalctl --relinquish-var + mv /var/log/journal /var/log/journal.le-lab + state_set moved-journal 1 +fi +rm -rf /var/log/lrtest /etc/logrotate.d/lrtest +true diff --git a/labs/lpic-1/108-3-mail-transfer-agents/README.md b/labs/lpic-1/108-3-mail-transfer-agents/README.md new file mode 100644 index 0000000..d4c2fa2 --- /dev/null +++ b/labs/lpic-1/108-3-mail-transfer-agents/README.md @@ -0,0 +1,39 @@ +# LPIC-1 108.3: Mail transfer agent basics + +Local mail with Postfix: sending and reading, an alias for root, a +`.forward` file, and the sendmail command. + +## Before you start + +The lab creates users `alice` and `bob` (password `Lab-Pass-1`). `alice` +stands in for you: log in as her (or `sudo -iu alice`) to send and read her +mail. No MTA should be installed yet; the lab refuses to start if one is. +Reset removes Postfix and the mail client again. + +## Tasks + +1. Install Postfix and choose "Local only" (Debian, Ubuntu), or install it + and start it (RHEL family), so it listens only on this machine. Install + a `mail` command too. +2. As `alice`, send yourself a message with the subject `Hello me` using + `mail -s`, and read it with `mail`. Find the mailbox file in + `/var/mail`. +3. Alias `root` to `alice`, run `newaliases`, send mail to root, and check it + arrives in `alice`'s mailbox. +4. Create a `~/.forward` for `alice` that sends her mail to `bob`, and test + it. Then change it to keep a copy for `alice` too, and send her another + message. +5. Run `mailq` and `sendmail -bp` and compare. Find the sendmail command's + real program with `readlink -f /usr/sbin/sendmail`, and write that path + to `/root/lab-answers/5`. + +## Hints + +- The mail client is `s-nail` on the RHEL family and `mailutils` on Debian + and Ubuntu. `sudo dpkg-reconfigure postfix` asks the questions again. + `postconf inet_interfaces`. +- `mail -s "Hello me" alice`; `mail` reads, `q` quits (read messages may + move to `~/mbox`). +- `/etc/aliases`, `newaliases`; `postmap -q root lmdb:/etc/aliases` (or + `hash:`) shows what Postfix sees. +- `\alice, bob` in `/home/alice/.forward`, owned by `alice`, mode 644. diff --git a/labs/lpic-1/108-3-mail-transfer-agents/check.sh b/labs/lpic-1/108-3-mail-transfer-agents/check.sh new file mode 100644 index 0000000..a36974e --- /dev/null +++ b/labs/lpic-1/108-3-mail-transfer-agents/check.sh @@ -0,0 +1,37 @@ +# The mailbox and saved mail of a user. +boxes() { local h; h=$(getent passwd "$1" | cut -d: -f6); echo "/var/mail/$1 /var/spool/mail/$1 $h/mbox"; } +# alias_of NAME: what the MTA's alias maps give for NAME. +alias_of() { + local m + for m in $(postconf -h alias_maps 2>/dev/null | tr ',' ' '); do + case $m in nis:* | *.mail.aliases) continue ;; esac + postmap -q "$1" "$m" 2>/dev/null && return 0 + done + return 1 +} +FWD=/home/alice/.forward + +task 1 "Postfix running for this machine only, and a mail command" +need "Postfix is installed" 'pkg_installed postfix' +need "Postfix is running" active postfix +need "Postfix starts at boot" enabled postfix +need "Postfix listens only on loopback" 'postconf -h inet_interfaces | grep -Eqx "localhost|loopback-only|127\.0\.0\.1(, *\[?::1\]?)?"' +need "a mail command is installed" 'command -v mail' + +task 2 "alice mailed herself Hello me" +need "alice has a message with the subject Hello me" 'grep -qs "^Subject: Hello me" $(boxes alice)' + +task 3 "root's mail goes to alice" +need "/etc/aliases sends root to alice" 'grep -Eq "^root:[[:space:]]*.*\balice\b" /etc/aliases' +need "Postfix's alias database sends root to alice (newaliases)" 'alias_of root | grep -qw alice' +need "a message to root arrived in alice's mailbox" 'grep -Eqs "^X-Original-To: root(@|$)" $(boxes alice)' + +task 4 "alice's .forward sends to bob and keeps a copy" +need "$FWD exists and is owned by alice" '[ "$(stat -c %U "$FWD")" = alice ]' +need "$FWD isn't writable by others" '! find "$FWD" -perm /022 | grep -q .' +need "$FWD sends to bob" 'grep -Eq "(^|[ ,])bob\b" "$FWD"' +need "$FWD keeps a copy for alice (\\alice)" 'grep -Fq "\\alice" "$FWD"' +need "bob received alice's forwarded mail" 'grep -Eqs "^X-Original-To: alice(@|$)" $(boxes bob)' + +task 5 "The sendmail command's real program in /root/lab-answers/5" +need "the answer is the program /usr/sbin/sendmail really is" '[ -e /usr/sbin/sendmail ] && [ "$(answer 5)" = "$(readlink -f /usr/sbin/sendmail)" ]' diff --git a/labs/lpic-1/108-3-mail-transfer-agents/lab.conf b/labs/lpic-1/108-3-mail-transfer-agents/lab.conf new file mode 100644 index 0000000..5052777 --- /dev/null +++ b/labs/lpic-1/108-3-mail-transfer-agents/lab.conf @@ -0,0 +1,3 @@ +TITLE="108.3 Mail transfer agent basics" +CHAPTER=/lpic-1/108-essential-system-services/108-3-mail-transfer-agents/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/108-3-mail-transfer-agents/reset.sh b/labs/lpic-1/108-3-mail-transfer-agents/reset.sh new file mode 100644 index 0000000..8ec67e0 --- /dev/null +++ b/labs/lpic-1/108-3-mail-transfer-agents/reset.sh @@ -0,0 +1,21 @@ +rm -f /home/alice/.forward +if [ -f "$LAB_STATE/aliases" ]; then cp -a "$LAB_STATE/aliases" /etc/aliases; else rm -f /etc/aliases; fi +# The packages the tasks installed. +rm_pkgs=() +for p in postfix s-nail mailx mailutils bsd-mailx; do + grep -qxF "$p" "$LAB_STATE/had-pkgs" 2>/dev/null && continue + pkg_installed "$p" && rm_pkgs+=("$p") +done +if [ ${#rm_pkgs[@]} -gt 0 ]; then + systemctl stop postfix 2>/dev/null + if is_rhel; then + dnf -y -q remove "${rm_pkgs[@]}" >/dev/null 2>&1 + else + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge "${rm_pkgs[@]}" >/dev/null 2>&1 + fi +fi +rm -f /etc/aliases.db /etc/aliases.lmdb +[ -f /etc/aliases ] && command -v newaliases >/dev/null && newaliases 2>/dev/null +users_remove +rm -f /var/mail/alice /var/mail/bob /var/spool/mail/alice /var/spool/mail/bob +true diff --git a/labs/lpic-1/108-3-mail-transfer-agents/setup.sh b/labs/lpic-1/108-3-mail-transfer-agents/setup.sh new file mode 100644 index 0000000..47847fa --- /dev/null +++ b/labs/lpic-1/108-3-mail-transfer-agents/setup.sh @@ -0,0 +1,10 @@ +for p in postfix exim4 exim4-daemon-light sendmail; do + pkg_installed "$p" && refuse "an MTA ($p) is installed already; this lab installs Postfix. Remove it first." +done +user_add alice +user_add bob +for p in s-nail mailx mailutils bsd-mailx; do + pkg_installed "$p" && echo "$p" >>"$LAB_STATE/had-pkgs" +done +[ -f /etc/aliases ] && cp -a /etc/aliases "$LAB_STATE/aliases" +true diff --git a/labs/lpic-1/108-4-printing/README.md b/labs/lpic-1/108-4-printing/README.md new file mode 100644 index 0000000..8a7455b --- /dev/null +++ b/labs/lpic-1/108-4-printing/README.md @@ -0,0 +1,35 @@ +# LPIC-1 108.4: Printers and printing + +CUPS with a PDF printer: printing, holding and cancelling jobs, and debug +logging. + +## Before you start + +The lab creates user `alice` (password `Lab-Pass-1`) and installs +`ipptool`, which the check uses to read CUPS's job history. CUPS itself isn't installed yet: that's task 1. Reset removes CUPS +and the PDF printer again. + +## Tasks + +1. Install `cups` and a PDF printer (`printer-driver-cups-pdf` on Debian and + Ubuntu; `cups-pdf`, from EPEL, on the RHEL family), and find the printer + with `lpstat -p`. As `alice`, print a text file to it; find the PDF it + makes, and write the PDF's full path to `/root/lab-answers/1`. +2. Disable the printer with a reason, print two files, list them with `lpq` + and `lpstat -o`, remove one with `lprm` and the other with `cancel`, then + enable it again. Also reject jobs on the printer and try to print; read + the message, then accept jobs again. +3. Turn on CUPS's debug logging, print a file, and read what `error_log` + (or `journalctl -u cups`) shows. Leave debug logging on. + +## Hints + +- `lpstat -p -v`; `lp -d PRINTER file`. The PDF lands in `~/PDF` on Debian + and Ubuntu; on the RHEL family it goes to `~/Desktop`, or the home + directory when there's no Desktop. Root's own jobs may fail there. If + `lpstat -p` shows no printer, add one: `lpinfo -m | grep -i pdf`, then + `lpadmin -p PDF -E -v cups-pdf:/ -m ...`. +- `cupsdisable -r "reason"`, `cupsenable`, `cupsreject -r`, `cupsaccept`; + `lpstat -p -l`. On Ubuntu, `lpq` and `lprm` are in the `cups-bsd` + package. +- `cupsctl --debug-logging`, `cupsctl`; `/var/log/cups/error_log`. diff --git a/labs/lpic-1/108-4-printing/check.sh b/labs/lpic-1/108-4-printing/check.sh new file mode 100644 index 0000000..e2ae9aa --- /dev/null +++ b/labs/lpic-1/108-4-printing/check.sh @@ -0,0 +1,36 @@ +# The cups-pdf printer, if there is one. +P=$(lpstat -v 2>/dev/null | awk '$NF ~ /^cups-pdf:/ {sub(/:$/, "", $3); print $3; exit}') +GJ=$(mktemp) +trap 'rm -f "$GJ"' EXIT +cat >"$GJ" <<'X' +{ +OPERATION Get-Jobs +GROUP operation-attributes-tag +ATTR charset attributes-charset utf-8 +ATTR naturalLanguage attributes-natural-language en +ATTR uri printer-uri $uri +ATTR keyword which-jobs completed +ATTR keyword requested-attributes job-id,job-state +DISPLAY job-id +DISPLAY job-state +} +X +# canceled: how many of the printer's jobs were canceled. +canceled() { ipptool -c "ipp://localhost/printers/$P" "$GJ" 2>/dev/null | grep -c ',canceled$'; } +PDF=$(head -n1 /root/lab-answers/1 2>/dev/null) + +task 1 "CUPS with a PDF printer, and a PDF printed" +need "CUPS is running" 'active cups' +need "CUPS starts at boot" 'enabled cups || enabled cups.socket' +need "lpstat -v shows a cups-pdf printer" '[ -n "$P" ]' +need "/root/lab-answers/1 names a PDF file that exists" '[ -f "$PDF" ] && head -c 5 "$PDF" | grep -q "%PDF"' +need "the PDF was printed during the lab" '[ "$PDF" -nt "$LAB_STATE/stamp" ]' + +task 2 "Two jobs removed while the printer was disabled; printer back in service" +need "two of the printer's jobs were canceled (lprm, cancel)" '[ "$(canceled)" -ge 2 ]' +need "the printer is enabled" 'lpstat -p "$P" | grep -q enabled' +need "the printer accepts jobs" 'lpstat -a "$P" | grep -q "accepting requests"' + +task 3 "CUPS debug logging on" +need "cupsd.conf has LogLevel debug" 'grep -Eqi "^[[:space:]]*LogLevel[[:space:]]+debug2?\b" /etc/cups/cupsd.conf' +need "cupsctl shows _debug_logging=1" 'cupsctl 2>/dev/null | grep -qx "_debug_logging=1"' diff --git a/labs/lpic-1/108-4-printing/lab.conf b/labs/lpic-1/108-4-printing/lab.conf new file mode 100644 index 0000000..2b5a6e5 --- /dev/null +++ b/labs/lpic-1/108-4-printing/lab.conf @@ -0,0 +1,3 @@ +TITLE="108.4 Printers and printing" +CHAPTER=/lpic-1/108-essential-system-services/108-4-printing/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/108-4-printing/reset.sh b/labs/lpic-1/108-4-printing/reset.sh new file mode 100644 index 0000000..b3e7d7b --- /dev/null +++ b/labs/lpic-1/108-4-printing/reset.sh @@ -0,0 +1,24 @@ +# Job history and the PDFs. +if command -v cancel >/dev/null; then cancel -a -x 2>/dev/null; fi +systemctl stop cups cups.socket cups.path 2>/dev/null +rm -rf /var/spool/cups/c[0-9]* /var/spool/cups/d[0-9]* /var/cache/cups/job.cache* +find /root /home -maxdepth 3 -name '*.pdf' -newer "$LAB_STATE/stamp" -path '*/PDF/*' -delete 2>/dev/null +find /root /home -maxdepth 3 -name '*.pdf' -newer "$LAB_STATE/stamp" -path '*/Desktop/*' -delete 2>/dev/null +rm -rf /var/spool/cups-pdf/ANONYMOUS/* +# The packages the tasks installed. +rm_pkgs=() +for p in cups cups-daemon cups-pdf printer-driver-cups-pdf cups-bsd cups-client cups-ipptool cups-ipp-utils epel-release; do + grep -qxF "$p" "$LAB_STATE/had-pkgs" 2>/dev/null && continue + pkg_installed "$p" && rm_pkgs+=("$p") +done +if [ ${#rm_pkgs[@]} -gt 0 ]; then + if is_rhel; then + dnf -y -q remove "${rm_pkgs[@]}" >/dev/null 2>&1 + else + DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge "${rm_pkgs[@]}" >/dev/null 2>&1 + fi +fi +users_remove +# Settings dnf leaves behind. +rm -f /etc/cups/printers.conf* /etc/cups/cupsd.conf.rpmsave /etc/cups/ppd/*.ppd +true diff --git a/labs/lpic-1/108-4-printing/setup.sh b/labs/lpic-1/108-4-printing/setup.sh new file mode 100644 index 0000000..8b2ed48 --- /dev/null +++ b/labs/lpic-1/108-4-printing/setup.sh @@ -0,0 +1,8 @@ +for p in cups cups-daemon cups-pdf printer-driver-cups-pdf cups-bsd cups-client cups-ipptool cups-ipp-utils epel-release; do + pkg_installed "$p" && echo "$p" >>"$LAB_STATE/had-pkgs" +done +pkg_installed cups && refuse "CUPS is installed already; task 1 installs it. Remove it first." +user_add alice +pkg_install cups-ipptool/cups-ipp-utils +touch "$LAB_STATE/stamp" +true diff --git a/labs/lpic-1/109-1-internet-protocols/README.md b/labs/lpic-1/109-1-internet-protocols/README.md new file mode 100644 index 0000000..7a64ed9 --- /dev/null +++ b/labs/lpic-1/109-1-internet-protocols/README.md @@ -0,0 +1,29 @@ +# LPIC-1 109.1: Internet protocol fundamentals + +Subnets worked out by hand, well-known ports, and IPv6 address notation. +Nothing here changes the VM: the answers go in files. + +## Before you start + +The lab sets up nothing. Write each answer in `/root/lab-answers/`. + +## Tasks + +1. Work out the network, broadcast, first and last host for + `10.1.2.3/23`, `172.16.40.200/27` and `192.168.5.5/30` on paper, then + check with `ipcalc` (install the `ipcalc` package if needed). Write one + line per network to `/root/lab-answers/1`, in that order, each with four + addresses separated by spaces: network, broadcast, first host, last host. +2. Look up the service names of ten ports from the chapter's table in + `/etc/services`. Write ten lines to `/root/lab-answers/2`, each a port + number and its name as `/etc/services` gives it, such as `22 ssh`. +3. Write `fe80:0000:0000:0000:0204:61ff:fe9d:f156` in its shortest form as + the first line of `/root/lab-answers/3`, and `2001:db8:a::1` expanded to + all eight groups of four digits as the second. + +## Hints + +- A `/23` covers two `/24`s; a `/27` is a block of 32 addresses; a `/30` a + block of 4. +- `grep -w 53 /etc/services`, `getent services 53`. +- Leading zeros in a group can go; one run of zero groups becomes `::`. diff --git a/labs/lpic-1/109-1-internet-protocols/check.sh b/labs/lpic-1/109-1-internet-protocols/check.sh new file mode 100644 index 0000000..24e65f9 --- /dev/null +++ b/labs/lpic-1/109-1-internet-protocols/check.sh @@ -0,0 +1,35 @@ +# net_line N NET BCAST FIRST LAST: line N of answer 1 has these four, in +# order (a /prefix after the network is fine). +net_line() { + local l + l=$(sed -n "$1p" /root/lab-answers/1 2>/dev/null | sed 's#/[0-9]*##g; s/[,;]/ /g' | xargs) + [ "$l" = "$2 $3 $4 $5" ] +} +# The ports in the chapter's table. +TABLE=" 20 21 22 23 25 53 80 110 123 139 143 161 162 389 443 465 514 636 993 995 " +# ports_ok: at least ten distinct table ports, each with a name or alias +# /etc/services gives it. +ports_ok() { + local port name n=0 seen=" " + while read -r port name _; do + [[ $port =~ ^[0-9]+$ ]] || continue + [[ $TABLE == *" $port "* ]] || continue + [[ $seen == *" $port "* ]] && continue + awk -v p="$port" -v n="$name" '$1 !~ /^#/ && $2 ~ "^"p"/" { for (i = 1; i <= NF; i++) { if ($i ~ /^#/) break; if (i != 2 && tolower($i) == tolower(n)) f = 1 } } END { exit !f }' /etc/services || return 1 + seen+="$port " n=$((n + 1)) + done /dev/null | tr A-Z a-z | xargs; } + +task 1 "Network, broadcast, first and last host of three networks" +need "line 1: 10.1.2.0 10.1.3.255 10.1.2.1 10.1.3.254" 'net_line 1 10.1.2.0 10.1.3.255 10.1.2.1 10.1.3.254' +need "line 2 (172.16.40.200/27) is right" 'net_line 2 172.16.40.192 172.16.40.223 172.16.40.193 172.16.40.222' +need "line 3 (192.168.5.5/30) is right" 'net_line 3 192.168.5.4 192.168.5.7 192.168.5.5 192.168.5.6' + +task 2 "Ten ports from the chapter's table and their names" +need "/root/lab-answers/2 has ten ports from the table, each with its name in /etc/services" ports_ok + +task 3 "IPv6 addresses shortened and expanded" +need "line 1 is fe80::204:61ff:fe9d:f156" '[ "$(v6 1)" = fe80::204:61ff:fe9d:f156 ]' +need "line 2 is 2001:0db8:000a:0000:0000:0000:0000:0001" '[ "$(v6 2)" = 2001:0db8:000a:0000:0000:0000:0000:0001 ]' diff --git a/labs/lpic-1/109-1-internet-protocols/lab.conf b/labs/lpic-1/109-1-internet-protocols/lab.conf new file mode 100644 index 0000000..2ba2f99 --- /dev/null +++ b/labs/lpic-1/109-1-internet-protocols/lab.conf @@ -0,0 +1,3 @@ +TITLE="109.1 Internet protocol fundamentals" +CHAPTER=/lpic-1/109-networking-fundamentals/109-1-internet-protocols/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/109-1-internet-protocols/reset.sh b/labs/lpic-1/109-1-internet-protocols/reset.sh new file mode 100644 index 0000000..26eafca --- /dev/null +++ b/labs/lpic-1/109-1-internet-protocols/reset.sh @@ -0,0 +1,2 @@ +# Nothing to undo: le-lab removes /root/lab-answers. +true diff --git a/labs/lpic-1/109-1-internet-protocols/setup.sh b/labs/lpic-1/109-1-internet-protocols/setup.sh new file mode 100644 index 0000000..4392877 --- /dev/null +++ b/labs/lpic-1/109-1-internet-protocols/setup.sh @@ -0,0 +1,2 @@ +# Nothing to set up: le-lab starts with an empty /root/lab-answers. +true diff --git a/labs/lpic-1/109-2-network-configuration/README.md b/labs/lpic-1/109-2-network-configuration/README.md new file mode 100644 index 0000000..ff4f2a4 --- /dev/null +++ b/labs/lpic-1/109-2-network-configuration/README.md @@ -0,0 +1,37 @@ +# LPIC-1 109.2: Persistent network configuration + +NetworkManager's devices, profiles and their files, a static address that +lasts, the host name, and `/etc/hosts`. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for another machine. + +On the RHEL family, the lab also adds a second network for task 2, so your +own connection to the VM is never touched: interface `le-lan`, with the +NetworkManager profile `lab-net`, which gets an address by DHCP from a +server at `10.98.0.1` on the far side. Tasks 1 and 2 use NetworkManager and +are graded on the RHEL family only; Ubuntu servers use netplan instead. + +## Tasks + +1. RHEL family: read `nmcli device` and `nmcli connection show`, and find the + file of the main interface's active connection with + `nmcli -f NAME,FILENAME connection show`. Read it with `sudo`, and write + its path to `/root/lab-answers/1`. +2. RHEL family: give profile `lab-net` the static address `10.98.0.10/24` + with `nmcli connection modify`, bring the connection up, check with + `ip addr` and `ip route`, and reboot to show it lasts. (The chapter then + returns it to DHCP: do that after you've run the check.) +3. Change the host name to `lab2.example.net` with `hostnamectl`, and check + `/etc/hostname`, the prompt in a new shell, and `hostname -f`. +4. Add the name `files.lab.example` for the peer's address, `10.99.0.2`, to + `/etc/hosts`, and `ping` it by name. + +## Hints + +- `nmcli -f NAME,DEVICE,FILENAME connection show --active`. +- `nmcli connection modify lab-net ipv4.method manual ipv4.addresses ...`, + then `nmcli connection up lab-net`; `ip -br addr show le-lan`. +- `hostnamectl set-hostname`, `hostnamectl status`. +- One line in `/etc/hosts`: the address, then the names; `getent hosts`. diff --git a/labs/lpic-1/109-2-network-configuration/check.sh b/labs/lpic-1/109-2-network-configuration/check.sh new file mode 100644 index 0000000..27ca8fd --- /dev/null +++ b/labs/lpic-1/109-2-network-configuration/check.sh @@ -0,0 +1,24 @@ +if is_rhel; then + IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') + MAINFILE=$(nmcli -t -f DEVICE,FILENAME connection show --active 2>/dev/null | awk -F: -v d="$IF" '$1 == d {print $2; exit}') + prof() { nmcli -g "$1" connection show lab-net 2>/dev/null | tr -d '\\'; } +fi + +task 1 "The main connection's file in /root/lab-answers/1" rhel +need "the answer is the file of $IF's active connection" '[ -n "$MAINFILE" ] && [ "$(head -n1 /root/lab-answers/1)" = "$MAINFILE" ]' + +task 2 "lab-net has the static address 10.98.0.10/24, persistently" rhel +need "lab-net's IPv4 method is manual" '[ "$(prof ipv4.method)" = manual ]' +need "lab-net has 10.98.0.10/24" 'prof ipv4.addresses | tr "," "\n" | tr -d " " | grep -qx 10.98.0.10/24' +need "le-lan has 10.98.0.10/24, not from DHCP" 'ip -o -4 addr show dev le-lan | grep -F " 10.98.0.10/24 " | grep -vq dynamic' +need "the route to 10.98.0.0/24 goes out le-lan" 'ip route get 10.98.0.1 | grep -q "dev le-lan"' +need "10.98.0.1 answers ping" 'ping -c1 -W2 10.98.0.1' + +task 3 "Host name lab2.example.net" +need "hostname prints lab2.example.net" '[ "$(hostname)" = lab2.example.net ]' +need "/etc/hostname has lab2.example.net" 'grep -qx lab2.example.net /etc/hostname' + +task 4 "files.lab.example is 10.99.0.2, from /etc/hosts" +need "/etc/hosts has files.lab.example for 10.99.0.2" 'grep -Eq "^10\.99\.0\.2[[:space:]]+(.*[[:space:]])?files\.lab\.example([[:space:]]|$)" /etc/hosts' +need "getent hosts files.lab.example gives 10.99.0.2" 'getent hosts files.lab.example | grep -q "^10\.99\.0\.2 "' +need "ping files.lab.example works" 'ping -c1 -W2 files.lab.example' diff --git a/labs/lpic-1/109-2-network-configuration/lab.conf b/labs/lpic-1/109-2-network-configuration/lab.conf new file mode 100644 index 0000000..f70a785 --- /dev/null +++ b/labs/lpic-1/109-2-network-configuration/lab.conf @@ -0,0 +1,3 @@ +TITLE="109.2 Persistent network configuration" +CHAPTER=/lpic-1/109-networking-fundamentals/109-2-network-configuration/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/109-2-network-configuration/reset.sh b/labs/lpic-1/109-2-network-configuration/reset.sh new file mode 100644 index 0000000..2f5f0d6 --- /dev/null +++ b/labs/lpic-1/109-2-network-configuration/reset.sh @@ -0,0 +1,18 @@ +if is_rhel; then + for c in $(nmcli -g UUID,DEVICE,NAME connection show 2>/dev/null | awk -F: '$2 == "le-lan" || $3 == "lab-net" {print $1}'); do + nmcli connection delete "$c" >/dev/null 2>&1 + done + systemctl disable --now --quiet le-lab-dhcp.service le-lab-lan.service 2>/dev/null + rm -f /etc/systemd/system/le-lab-dhcp.service /etc/systemd/system/le-lab-lan.service + systemctl daemon-reload + ip link del le-lan 2>/dev/null + rm -f /etc/NetworkManager/conf.d/92-le-lab-lan.conf + systemctl reload NetworkManager 2>/dev/null + rm -f /usr/local/lib/le-lab/lan-net.sh /var/lib/dnsmasq/le-lab.leases + rmdir /usr/local/lib/le-lab 2>/dev/null + [ -n "$(state_get added-dnsmasq)" ] && dnf -y -q remove dnsmasq >/dev/null 2>&1 +fi +old=$(state_get hostname) +[ -n "$old" ] && [ "$(hostname)" != "$old" ] && hostnamectl set-hostname "$old" +sed -i -E '/^[^#]*[[:space:]]files\.lab\.example([[:space:]]|$)/d' /etc/hosts +true diff --git a/labs/lpic-1/109-2-network-configuration/setup.sh b/labs/lpic-1/109-2-network-configuration/setup.sh new file mode 100644 index 0000000..dc0aa86 --- /dev/null +++ b/labs/lpic-1/109-2-network-configuration/setup.sh @@ -0,0 +1,60 @@ +state_set hostname "$(hostname)" +[ "$(hostname)" != lab2.example.net ] || hostnamectl set-hostname lab-vm +sed -i -E '/^[^#]*[[:space:]]files\.lab\.example([[:space:]]|$)/d' /etc/hosts +peer_up + +if is_rhel; then + command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager || + refuse "NetworkManager isn't running; this lab uses it" + pkg_installed dnsmasq || { pkg_install dnsmasq; state_set added-dnsmasq 1; } + # le-lan, linked to the peer, where a DHCP server answers on 10.98.0.1. + mkdir -p /usr/local/lib/le-lab + cat >/usr/local/lib/le-lab/lan-net.sh <<'X' +#!/bin/sh +ip link show le-lan >/dev/null 2>&1 || ip link add le-lan type veth peer name le-lanp netns lepeer +ip -n lepeer addr replace 10.98.0.1/24 dev le-lanp +ip -n lepeer link set le-lanp up +ip link set le-lan up +command -v nmcli >/dev/null && nmcli device set le-lan managed yes 2>/dev/null +true +X + chmod 755 /usr/local/lib/le-lab/lan-net.sh + cat >/etc/systemd/system/le-lab-lan.service <<'X' +[Unit] +Description=Practice lab: the le-lan network +Requires=le-lab-peer.service +After=le-lab-peer.service NetworkManager.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/le-lab/lan-net.sh + +[Install] +WantedBy=multi-user.target +X + cat >/etc/systemd/system/le-lab-dhcp.service <<'X' +[Unit] +Description=Practice lab DHCP server at 10.98.0.1, on the peer +Requires=le-lab-lan.service +After=le-lab-lan.service + +[Service] +NetworkNamespacePath=/run/netns/lepeer +ExecStart=/usr/sbin/dnsmasq -k --conf-file=/dev/null --port=0 --pid-file= --bind-interfaces --interface=le-lanp --dhcp-range=10.98.0.100,10.98.0.150,1h --dhcp-leasefile=/var/lib/dnsmasq/le-lab.leases + +[Install] +WantedBy=multi-user.target +X + mkdir -p /var/lib/dnsmasq + # No automatic "Wired connection" for le-lan: lab-net is its profile. + printf '[main]\nno-auto-default=interface-name:le-lan\n' >/etc/NetworkManager/conf.d/92-le-lab-lan.conf + systemctl reload NetworkManager 2>/dev/null || true + systemctl daemon-reload + systemctl enable --now --quiet le-lab-lan.service le-lab-dhcp.service + nmcli connection delete lab-net >/dev/null 2>&1 || true + nmcli connection add type ethernet ifname le-lan con-name lab-net \ + ipv4.method auto ipv6.method disabled >/dev/null + timeout 60 nmcli connection up lab-net >/dev/null 2>&1 || warn "lab-net didn't get an address yet; try: nmcli connection up lab-net" +fi +true diff --git a/labs/lpic-1/109-3-network-troubleshooting/README.md b/labs/lpic-1/109-3-network-troubleshooting/README.md new file mode 100644 index 0000000..294e26c --- /dev/null +++ b/labs/lpic-1/109-3-network-troubleshooting/README.md @@ -0,0 +1,37 @@ +# LPIC-1 109.3: Basic network troubleshooting + +Which process holds a port, a test connection with netcat through the +firewall, and a fault found by working through the layers. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for another VM. It sits +on this VM's second network, `10.99.0.0/24`, where this VM is `10.99.0.1` +on interface `le-host`. Get a shell there with +`sudo ip netns exec lepeer bash`. The lab has broken the link to the peer +in one place, for task 3. Your own connection to the VM isn't touched. + +## Tasks + +1. Find the process listening on port 22 with `sudo ss -tlnp`, and write + its name to `/root/lab-answers/1`. Then install `net-tools` and compare + with `netstat -tlnp`. +2. Install netcat if needed, listen on TCP port 9000 on this VM, and connect + from the peer (`nc 10.99.0.1 9000`). Open the port in the firewall if one + is running. Type in both directions. Then check the port from the peer + with `nc -zv`, and leave a listener running on 9000 for the check + (`nc -lk 9000`). +3. This VM can't reach the peer: `ping 10.99.0.2` fails. Work through the + troubleshooting order (link, address, route, then the far end) to find + the fault, fix it, and write the name of the interface at fault to + `/root/lab-answers/3`. + +## Hints + +- `ss -tlnp 'sport = :22'`; on Ubuntu, `systemd` holds the port for + `ssh.socket`. `netstat` is in `net-tools`. +- `nc` is in `nmap-ncat` on the RHEL family and `netcat-openbsd` on Ubuntu. + `firewall-cmd --add-port=9000/tcp` (add `--permanent` to keep it), or + `ufw allow 9000/tcp`. Run `nc -lk 9000 &` in the background, or under + `setsid`. +- `ip -br link`, `ip -br addr`, `ip route`, `ping`. diff --git a/labs/lpic-1/109-3-network-troubleshooting/check.sh b/labs/lpic-1/109-3-network-troubleshooting/check.sh new file mode 100644 index 0000000..7b7572b --- /dev/null +++ b/labs/lpic-1/109-3-network-troubleshooting/check.sh @@ -0,0 +1,18 @@ +# The names ss shows for whatever listens on port 22. +port22=$(ss -Htlnp 'sport = :22' | grep -o '(("[^"]*"\|,("[^"]*"' | grep -o '"[^"]*"' | tr -d '"' | sort -u) +# reach_9000: the peer opens a TCP connection to 10.99.0.1:9000 (15 seconds). +reach_9000() { local i; for i in $(seq 5); do in_peer timeout 3 bash -c '/dev/null && return 0; sleep 1; done; return 1; } + +task 1 "The process on port 22 in /root/lab-answers/1; netstat installed" +need "the answer is a process that listens on port 22 ($(echo $port22 | tr " " /))" '[ -n "$(answer 1)" ] && grep -qx "$(answer 1)" <<<"$port22"' +need "netstat is installed" 'command -v netstat' + +task 2 "A netcat listener on 9000 that the peer can reach" +need "nc is installed" 'command -v nc' +need "something listens on TCP port 9000" 'listening 9000 tcp' +need "the peer can connect to 10.99.0.1:9000 (open the firewall if one is running)" reach_9000 + +task 3 "The link to the peer fixed; the interface named in /root/lab-answers/3" +need "le-host is up" 'ip link show le-host | grep -q "state UP"' +need "this VM can ping the peer" 'ping -c1 -W2 10.99.0.2' +need "the answer is le-host" '[ "$(answer 3)" = le-host ]' diff --git a/labs/lpic-1/109-3-network-troubleshooting/lab.conf b/labs/lpic-1/109-3-network-troubleshooting/lab.conf new file mode 100644 index 0000000..f3c6265 --- /dev/null +++ b/labs/lpic-1/109-3-network-troubleshooting/lab.conf @@ -0,0 +1,3 @@ +TITLE="109.3 Basic network troubleshooting" +CHAPTER=/lpic-1/109-networking-fundamentals/109-3-network-troubleshooting/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/109-3-network-troubleshooting/reset.sh b/labs/lpic-1/109-3-network-troubleshooting/reset.sh new file mode 100644 index 0000000..c357477 --- /dev/null +++ b/labs/lpic-1/109-3-network-troubleshooting/reset.sh @@ -0,0 +1,15 @@ +# Any listener left on port 9000. +for pid in $(ss -Htlnp 'sport = :9000' | grep -o 'pid=[0-9]*' | cut -d= -f2 | sort -u); do + kill "$pid" 2>/dev/null +done +# The firewall, as it was. +if [ -n "$(state_get firewalld)" ] && [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" + systemctl is-active --quiet firewalld && firewall-cmd -q --reload +elif [ -n "$(state_get ufw)" ] && [ -f "$LAB_STATE/ufw.tar" ]; then + rm -rf /etc/ufw + tar -C /etc -xf "$LAB_STATE/ufw.tar" + ufw reload >/dev/null +fi +true diff --git a/labs/lpic-1/109-3-network-troubleshooting/setup.sh b/labs/lpic-1/109-3-network-troubleshooting/setup.sh new file mode 100644 index 0000000..885700c --- /dev/null +++ b/labs/lpic-1/109-3-network-troubleshooting/setup.sh @@ -0,0 +1,12 @@ +listening 9000 tcp && refuse "something listens on TCP port 9000 already; stop it first" +peer_up +if systemctl is-active --quiet firewalld; then + tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld + state_set firewalld 1 +elif command -v ufw >/dev/null && ufw status | grep -q 'Status: active'; then + tar -C /etc -cf "$LAB_STATE/ufw.tar" ufw + state_set ufw 1 +fi +# The fault: this VM's end of the link to the peer is down. +ip link set le-host down +true diff --git a/labs/lpic-1/109-4-client-dns/README.md b/labs/lpic-1/109-4-client-dns/README.md new file mode 100644 index 0000000..6c03b09 --- /dev/null +++ b/labs/lpic-1/109-4-client-dns/README.md @@ -0,0 +1,34 @@ +# LPIC-1 109.4: Client-side DNS + +Who writes `/etc/resolv.conf`, `/etc/hosts` against DNS, and a search +domain. + +## Before you start + +The lab saves the network configuration (NetworkManager's profiles on the +RHEL family; netplan and systemd-resolved's settings on Ubuntu) so reset +can put it back. Task 2 needs the VM to resolve names on the internet. + +## Tasks + +1. Read `/etc/nsswitch.conf`, and check whether `/etc/resolv.conf` is a link + with `ls -l`. Which program writes it? Write its name to + `/root/lab-answers/1`. +2. Add `example.org` as a search domain (through NetworkManager or + systemd-resolved, since `resolv.conf` is generated), persistently, and + look up the short name `www`: `getent hosts www` must find + `www.example.org`. +3. Add `192.168.1.250 fake.example.org` to `/etc/hosts`. Compare + `getent hosts fake.example.org` with `dig fake.example.org`, and work out + why they differ. Write the address `getent` prints to + `/root/lab-answers/3`, then remove the line from `/etc/hosts`. + +## Hints + +- `ls -l /etc/resolv.conf`; the comment at the top of the file. +- `nmcli connection modify ... ipv4.dns-search`, then + `nmcli device reapply`; or `Domains=` in a drop-in in + `/etc/systemd/resolved.conf.d/`, or `search:` under `nameservers:` in + netplan. `resolvectl status` and `cat /etc/resolv.conf` show it. +- `dig` is in `bind-utils` on the RHEL family and `bind9-dnsutils` on + Ubuntu. Which of the two reads `/etc/hosts`? diff --git a/labs/lpic-1/109-4-client-dns/check.sh b/labs/lpic-1/109-4-client-dns/check.sh new file mode 100644 index 0000000..6f14881 --- /dev/null +++ b/labs/lpic-1/109-4-client-dns/check.sh @@ -0,0 +1,25 @@ +W=$(state_get writer) + +task 1 "The program that writes /etc/resolv.conf in /root/lab-answers/1" +if [ "$W" = resolved ]; then + need "the answer names systemd-resolved" 'answer 1 | grep -q resolved' +elif [ "$W" = networkmanager ]; then + need "the answer names NetworkManager" 'answer 1 | grep -Eq "networkmanager|^nm$"' +elif [ "$W" = resolvconf ]; then + need "the answer names resolvconf" 'answer 1 | grep -q resolvconf' +else + need "/root/lab-answers/1 names a program" '[ -n "$(answer 1)" ]' +fi + +task 2 "example.org as a persistent search domain" +need "getent hosts www finds www.example.org" 'getent hosts www | grep -q "example\.org"' +if is_rhel; then + need "the search domain is in NetworkManager's configuration" 'grep -rqs "example\.org" /etc/NetworkManager/system-connections /etc/NetworkManager/conf.d' +else + need "the search domain is in netplan's, networkd's or resolved's configuration" 'conf_has /etc/netplan /etc/systemd/network /etc/systemd/resolved.conf /etc/systemd/resolved.conf.d "example\.org"' +fi + +task 3 "fake.example.org: getent's answer noted, the line removed" +need "dig is installed" 'command -v dig' +need "the answer is 192.168.1.250" '[ "$(answer 3)" = 192.168.1.250 ]' +need "fake.example.org is no longer in /etc/hosts" '! grep -Eq "^[^#]*[[:space:]]fake\.example\.org([[:space:]]|$)" /etc/hosts' diff --git a/labs/lpic-1/109-4-client-dns/lab.conf b/labs/lpic-1/109-4-client-dns/lab.conf new file mode 100644 index 0000000..dd916ff --- /dev/null +++ b/labs/lpic-1/109-4-client-dns/lab.conf @@ -0,0 +1,3 @@ +TITLE="109.4 Client-side DNS" +CHAPTER=/lpic-1/109-networking-fundamentals/109-4-client-dns/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/109-4-client-dns/reset.sh b/labs/lpic-1/109-4-client-dns/reset.sh new file mode 100644 index 0000000..ee8a2d4 --- /dev/null +++ b/labs/lpic-1/109-4-client-dns/reset.sh @@ -0,0 +1,30 @@ +IF=$(state_get if) +if is_rhel; then + if [ -f "$LAB_STATE/nm.tar" ]; then + rm -rf /etc/NetworkManager/system-connections/* /etc/NetworkManager/conf.d/* + tar -C /etc/NetworkManager -xf "$LAB_STATE/nm.tar" + nmcli connection reload + nmcli general reload 2>/dev/null + [ -n "$IF" ] && nmcli device reapply "$IF" >/dev/null 2>&1 + fi +else + if [ -f "$LAB_STATE/netplan.tar" ] && ! tar -C /etc -df "$LAB_STATE/netplan.tar" >/dev/null 2>&1; then + rm -rf /etc/netplan + tar -C /etc -xf "$LAB_STATE/netplan.tar" + netplan apply 2>/dev/null + fi + for f in /etc/cloud/cloud.cfg.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/cloud-cfg" 2>/dev/null || rm -f "$f" + done + [ -f "$LAB_STATE/resolved.conf" ] && cp -a "$LAB_STATE/resolved.conf" /etc/systemd/resolved.conf + for f in /etc/systemd/resolved.conf.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/resolved-d" 2>/dev/null || rm -f "$f" + done + systemctl restart systemd-resolved + resolvectl domain "$IF" "" 2>/dev/null + networkctl reload 2>/dev/null +fi +sed -i -E '/^[^#]*[[:space:]]fake\.example\.org([[:space:]]|$)/d' /etc/hosts +true diff --git a/labs/lpic-1/109-4-client-dns/setup.sh b/labs/lpic-1/109-4-client-dns/setup.sh new file mode 100644 index 0000000..302fef7 --- /dev/null +++ b/labs/lpic-1/109-4-client-dns/setup.sh @@ -0,0 +1,20 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +[ -n "$IF" ] || refuse "no default route; this lab needs a working network" +state_set if "$IF" +# Who writes resolv.conf. +r=$(readlink -f /etc/resolv.conf) +if [[ $r == /run/systemd/resolve/* ]]; then state_set writer resolved +elif grep -q 'NetworkManager' /etc/resolv.conf; then state_set writer networkmanager +elif [[ $r == /run/resolvconf/* ]] || grep -qi resolvconf /etc/resolv.conf; then state_set writer resolvconf +fi +# The network configuration, for reset. +if is_rhel; then + tar -C /etc/NetworkManager -cf "$LAB_STATE/nm.tar" system-connections conf.d +else + tar -C /etc -cf "$LAB_STATE/netplan.tar" netplan + [ -f /etc/systemd/resolved.conf ] && cp -a /etc/systemd/resolved.conf "$LAB_STATE/resolved.conf" + ls /etc/systemd/resolved.conf.d >"$LAB_STATE/resolved-d" 2>/dev/null || true + ls /etc/cloud/cloud.cfg.d >"$LAB_STATE/cloud-cfg" 2>/dev/null || true +fi +sed -i -E '/^[^#]*[[:space:]]fake\.example\.org([[:space:]]|$)/d' /etc/hosts +true diff --git a/labs/lpic-1/110-1-security-administration/README.md b/labs/lpic-1/110-1-security-administration/README.md new file mode 100644 index 0000000..0243af1 --- /dev/null +++ b/labs/lpic-1/110-1-security-administration/README.md @@ -0,0 +1,43 @@ +# LPIC-1 110.1: Security administration tasks + +Setuid files, open ports and their processes, user limits, a narrow sudo +rule, and a locked account. + +## Before you start + +The lab creates users `tess` and `locky` (password `Lab-Pass-1`) and a +service `labweb`. It runs a program that listens on a TCP port, which task +2 finds. The peer, at `10.99.0.2` (name `peer`), stands in for another VM: +get a shell there with `sudo ip netns exec lepeer bash`. If a firewall +runs here, the lab lets the peer through it, so a scan sees every port. + +## Tasks + +1. List every setuid and setgid file on the root filesystem into + `/root/lab-answers/1-before`. Copy `/usr/bin/bash` to + `/usr/local/bin/testbash`, set the setuid bit on the copy, list again into + `/root/lab-answers/1-after`, and `diff` the two lists. Then delete the + copy. +2. Scan this VM from the peer with `nmap` (scan `10.99.0.1`), and match + each open TCP port to a process with `ss -tlnp` and `fuser`. Write one + line per open port to `/root/lab-answers/2`: the port number and the + process name, such as `22 sshd`. +3. Give `tess` a hard limit of 50 processes (`nproc`) in + `/etc/security/limits.conf` or a file in `/etc/security/limits.d/`, log + in as `tess`, and check `ulimit -u`. +4. Give `tess` permission to run `systemctl restart labweb` with sudo, and + nothing else. Check with `sudo -l` as `tess`, and try another command. +5. Lock `locky`'s password with `passwd -l`, check `passwd -S`, and try to + log in as `locky` (for example with `su - locky` as `tess`). Then read + the attempt with `lastb` or `journalctl`. + +## Hints + +- `find / -xdev -perm /6000 -type f`; `chmod u+s`; `diff`. +- `nmap` is a package of its own; it runs in the peer with + `sudo ip netns exec lepeer nmap 10.99.0.1`. `ss -tlnp`, `fuser -n tcp + PORT`, `ps -p PID -o comm=`. +- `tess hard nproc 50`; `su - tess`, `ulimit -Hu`. +- A file in `/etc/sudoers.d/` checked with `visudo -cf`; the full path of + `systemctl` (`command -v systemctl`); `sudo -l -U tess`. +- `passwd -S locky`; `journalctl -t su` or `journalctl --since -10min`. diff --git a/labs/lpic-1/110-1-security-administration/check.sh b/labs/lpic-1/110-1-security-administration/check.sh new file mode 100644 index 0000000..2ab42e1 --- /dev/null +++ b/labs/lpic-1/110-1-security-administration/check.sh @@ -0,0 +1,28 @@ +SINCE=$(state_get since) +# ports_line PORT NAMES-REGEX: a line of answer 2 has PORT and one of NAMES. +ports_line() { grep -Ei "(^|[^0-9])$1([^0-9]|$)" /root/lab-answers/2 2>/dev/null | grep -Eiq "$2"; } + +task 1 "Setuid and setgid lists before and after testbash, and testbash gone" +need "/root/lab-answers/1-before lists /usr/bin/passwd" 'grep -q "/usr/bin/passwd$" /root/lab-answers/1-before' +need "1-before doesn't list testbash" '! grep -q testbash /root/lab-answers/1-before' +need "/root/lab-answers/1-after lists /usr/local/bin/testbash" 'grep -q "/usr/local/bin/testbash$" /root/lab-answers/1-after' +need "/usr/local/bin/testbash is deleted" '[ ! -e /usr/local/bin/testbash ]' + +task 2 "Open ports and their processes in /root/lab-answers/2" +need "the answer has 9999 and its process" 'ports_line 9999 backupd' +need "the answer has 22 and its process" 'ports_line 22 "sshd|systemd"' + +task 3 "tess has a hard limit of 50 processes" +need "a limits file sets tess's hard nproc to 50" 'conf_has /etc/security/limits.conf /etc/security/limits.d "^[[:space:]]*tess[[:space:]]+(hard|-)[[:space:]]+nproc[[:space:]]+50([[:space:]]|$)"' +need "ulimit -Hu in a login shell of tess's is 50" '[ "$(as_user tess "ulimit -Hu" 2>/dev/null | tail -n1)" = 50 ]' + +task 4 "tess may run systemctl restart labweb with sudo, and nothing else" +need "sudo rules are valid" visudo -c +need "tess may run systemctl restart labweb" 'sudo -n -l -U tess "$(command -v systemctl)" restart labweb' +need "tess may not run systemctl stop labweb" '! sudo -n -l -U tess "$(command -v systemctl)" stop labweb' +need "tess may not run systemctl restart sshd" '! sudo -n -l -U tess "$(command -v systemctl)" restart sshd' +need "tess may not run a shell" '! sudo -n -l -U tess /bin/bash' + +task 5 "locky's password locked, and a failed login as locky logged" +need "passwd -S shows locky's password locked" 'passwd -S locky | awk "\$2 ~ /^L/ {ok=1} END {exit !ok}"' +need "a failed login as locky is in the journal or lastb" 'journalctl -q --no-pager --since "$SINCE" | grep -w locky | grep -Eiq "fail|denied|not allowed|invalid" || lastb 2>/dev/null | grep -qw locky' diff --git a/labs/lpic-1/110-1-security-administration/lab.conf b/labs/lpic-1/110-1-security-administration/lab.conf new file mode 100644 index 0000000..999577d --- /dev/null +++ b/labs/lpic-1/110-1-security-administration/lab.conf @@ -0,0 +1,3 @@ +TITLE="110.1 Security administration tasks" +CHAPTER=/lpic-1/110-security/110-1-security-administration/ +SYSTEMS="rhel debian" diff --git a/labs/lpic-1/110-1-security-administration/reset.sh b/labs/lpic-1/110-1-security-administration/reset.sh new file mode 100644 index 0000000..cc08418 --- /dev/null +++ b/labs/lpic-1/110-1-security-administration/reset.sh @@ -0,0 +1,22 @@ +rm -f /usr/local/bin/testbash +systemctl disable --now --quiet labweb.service le-lab-backupd.service 2>/dev/null +rm -f /etc/systemd/system/labweb.service /etc/systemd/system/le-lab-backupd.service +systemctl daemon-reload +rm -f /usr/local/lib/le-lab/backupd.py +rmdir /usr/local/lib/le-lab 2>/dev/null +for f in /etc/sudoers.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/sudoers-d" 2>/dev/null || rm -f "$f" +done +for f in /etc/security/limits.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/limits-d" 2>/dev/null || rm -f "$f" +done +[ -f "$LAB_STATE/limits.conf" ] && cp -a "$LAB_STATE/limits.conf" /etc/security/limits.conf +if systemctl is-active --quiet firewalld; then + firewall-cmd -q --zone=trusted --remove-interface=le-host 2>/dev/null + firewall-cmd -q --permanent --zone=trusted --remove-interface=le-host 2>/dev/null +fi +[ -n "$(state_get ufw)" ] && ufw delete allow in on le-host >/dev/null 2>&1 +users_remove +true diff --git a/labs/lpic-1/110-1-security-administration/setup.sh b/labs/lpic-1/110-1-security-administration/setup.sh new file mode 100644 index 0000000..b216e86 --- /dev/null +++ b/labs/lpic-1/110-1-security-administration/setup.sh @@ -0,0 +1,59 @@ +[ -e /usr/local/bin/testbash ] && refuse "/usr/local/bin/testbash exists already; remove it first" +for u in tess locky; do + id "$u" >/dev/null 2>&1 && refuse "user $u exists already; remove it first (userdel -r $u)" +done +user_add tess +user_add locky +state_set since "$(date '+%Y-%m-%d %H:%M:%S')" +ls /etc/sudoers.d >"$LAB_STATE/sudoers-d" 2>/dev/null || true +ls /etc/security/limits.d >"$LAB_STATE/limits-d" 2>/dev/null || true +cp -a /etc/security/limits.conf "$LAB_STATE/limits.conf" + +# labweb, a service for task 4, and backupd, a program on TCP port 9999. +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/backupd.py <<'X' +# Listens on TCP port 9999 and closes every connection at once. +import socket +open("/proc/self/comm", "w").write("backupd") +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.bind(("0.0.0.0", 9999)) +s.listen(5) +while True: + c, _ = s.accept() + c.close() +X +cat >/etc/systemd/system/labweb.service <<'X' +[Unit] +Description=Practice lab web service (does nothing) + +[Service] +ExecStart=/bin/sleep infinity + +[Install] +WantedBy=multi-user.target +X +cat >/etc/systemd/system/le-lab-backupd.service <<'X' +[Unit] +Description=Practice lab program on TCP port 9999 +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/backupd.py + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet labweb.service le-lab-backupd.service + +# The peer, and a firewall that lets it in. +peer_up +if systemctl is-active --quiet firewalld; then + firewall-cmd -q --zone=trusted --add-interface=le-host + firewall-cmd -q --permanent --zone=trusted --add-interface=le-host +elif command -v ufw >/dev/null && ufw status | grep -q 'Status: active'; then + ufw allow in on le-host >/dev/null + state_set ufw 1 +fi +true diff --git a/labs/lpic-1/110-2-host-security/README.md b/labs/lpic-1/110-2-host-security/README.md new file mode 100644 index 0000000..2490fa7 --- /dev/null +++ b/labs/lpic-1/110-2-host-security/README.md @@ -0,0 +1,30 @@ +# LPIC-1 110.2: Setting up host security + +Turning off a service nobody needs, `/etc/nologin`, and the `nologin` +shell. + +## Before you start + +The lab runs a service nobody needs, `legacy-echo`, which listens on TCP +port 7007. It creates users `tess` and `testuser` (password `Lab-Pass-1`); +`tess` can log in over SSH with the key `/root/lab-keys/tess` +(`ssh -i /root/lab-keys/tess tess@localhost`). Keep a root session open +while you do task 2. + +## Tasks + +1. List every listening service with `ss -tulpn`, and turn off + `legacy-echo` with `systemctl disable --now`. Check it's gone, then mask + it and try to start it. +2. Create `/etc/nologin` with a message, and try to log in as `tess` over + SSH (and on the console, if you have one). Then remove `/etc/nologin` + from the session you kept open. +3. Give `testuser` the shell `/usr/sbin/nologin` and try `su - testuser`. + +## Hints + +- `ss -tulpn | grep 7007`; `systemctl status legacy-echo`; + `systemctl mask`, `systemctl start`. +- `echo "Down for maintenance" > /etc/nologin`; root can still log in. + `journalctl --since -10min` shows the refused login. +- `usermod -s`, `getent passwd testuser`. diff --git a/labs/lpic-1/110-2-host-security/check.sh b/labs/lpic-1/110-2-host-security/check.sh new file mode 100644 index 0000000..98461a7 --- /dev/null +++ b/labs/lpic-1/110-2-host-security/check.sh @@ -0,0 +1,27 @@ +SINCE=$(state_get since) +# refused: a login of tess's refused since setup, in the journal or a log +# file. +refused() { + local f n re="nologin|refused|denied|not allowed" + journalctl -q --no-pager --since "$SINCE" 2>/dev/null | grep -Ei "$re" | grep -qw tess && return 0 + for f in /var/log/secure /var/log/auth.log; do + [ -f "$f" ] || continue + n=$(state_get "size$(echo "$f" | tr / _)"); n=${n:-0} + [ "$(stat -c %s "$f")" -ge "$n" ] || n=0 + tail -c +$((n + 1)) "$f" | grep -Ei "$re" | grep -qw tess && return 0 + done + return 1 +} + +task 1 "legacy-echo stopped, disabled and masked" +need "legacy-echo isn't running" '! active legacy-echo' +need "nothing listens on port 7007" '! listening 7007 tcp' +need "legacy-echo is masked" '[ "$(systemctl is-enabled legacy-echo 2>/dev/null)" = masked ]' + +task 2 "A login refused because of /etc/nologin, and /etc/nologin removed" +need "/etc/nologin is gone" '[ ! -e /etc/nologin ]' +need "the logs show tess refused while /etc/nologin existed" refused +need "tess can log in again" 'timeout 15 ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i /root/lab-keys/tess tess@localhost "echo le-ok" /dev/null +systemctl disable --now --quiet legacy-echo.service 2>/dev/null +rm -f /usr/local/lib/systemd/system/legacy-echo.service /etc/systemd/system/legacy-echo.service /usr/local/lib/le-lab/legacy-echo.py +rmdir /usr/local/lib/systemd/system /usr/local/lib/systemd 2>/dev/null +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +rm -rf /root/lab-keys +users_remove +true diff --git a/labs/lpic-1/110-2-host-security/setup.sh b/labs/lpic-1/110-2-host-security/setup.sh new file mode 100644 index 0000000..9aeaa91 --- /dev/null +++ b/labs/lpic-1/110-2-host-security/setup.sh @@ -0,0 +1,54 @@ +[ -e /etc/nologin ] && refuse "/etc/nologin exists, so nobody but root can log in; remove it first" +for u in tess testuser; do + id "$u" >/dev/null 2>&1 && refuse "user $u exists already; remove it first (userdel -r $u)" +done +user_add tess +user_add testuser +state_set since "$(date '+%Y-%m-%d %H:%M:%S')" +# Where logins are logged, and how much is there already: the journal may +# not last across a reboot. +for f in /var/log/secure /var/log/auth.log; do + [ -f "$f" ] && state_set "size$(echo "$f" | tr / _)" "$(stat -c %s "$f")" +done +# tess logs in with a key. +mkdir -p /root/lab-keys +chmod 700 /root/lab-keys +rm -f /root/lab-keys/tess /root/lab-keys/tess.pub +ssh-keygen -q -t ed25519 -N '' -C tess@lab -f /root/lab-keys/tess +h=$(getent passwd tess | cut -d: -f6) +install -d -m 700 -o tess -g tess "$h/.ssh" +install -m 600 -o tess -g tess /root/lab-keys/tess.pub "$h/.ssh/authorized_keys" +command -v restorecon >/dev/null && restorecon -R "$h/.ssh" + +# legacy-echo: an echo service on TCP port 7007. +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/legacy-echo.py <<'X' +# Echoes back whatever it gets on TCP port 7007. +import socketserver + + +class Echo(socketserver.StreamRequestHandler): + def handle(self): + for line in self.rfile: + self.wfile.write(line) + + +socketserver.ThreadingTCPServer.allow_reuse_address = True +socketserver.ThreadingTCPServer(("0.0.0.0", 7007), Echo).serve_forever() +X +# In /usr/local/lib/systemd/system, so that it can be masked. +mkdir -p /usr/local/lib/systemd/system +cat >/usr/local/lib/systemd/system/legacy-echo.service <<'X' +[Unit] +Description=Legacy echo service on TCP port 7007 +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /usr/local/lib/le-lab/legacy-echo.py + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet legacy-echo.service +true diff --git a/labs/lpic-1/110-3-encryption/README.md b/labs/lpic-1/110-3-encryption/README.md new file mode 100644 index 0000000..b631919 --- /dev/null +++ b/labs/lpic-1/110-3-encryption/README.md @@ -0,0 +1,61 @@ +# LPIC-1 110.3: Securing data with encryption + +SSH keys and the agent, key-only logins, host keys, a tunnel, and GnuPG +encryption, signatures and revocation. + +## Before you start + +The lab creates users `alice` and `bob` (password `Lab-Pass-1`), and the +files `/home/alice/secret.txt` and `/home/alice/notes.txt`. Do each task as +the user it names: log in as them directly, on the console or with +`ssh alice@localhost` (after `su`, gpg often can't ask for a passphrase). + +The peer, at `10.99.0.2` (name `peer`), is the second VM. It shares this +VM's users and home directories, but runs its own sshd +(`le-lab-peer-sshd.service`, which allows passwords for now) with its own +configuration and host keys. Get a root shell there with +`sudo ip netns exec lepeer bash`: in that shell, `/etc/ssh` is the peer's +(`/etc/netns/lepeer/ssh` seen from this VM). Restart its sshd with +`sudo systemctl restart le-lab-peer-sshd`. + +## Tasks + +1. As `alice`, make an ed25519 key with a passphrase, copy it to the peer + with `ssh-copy-id alice@10.99.0.2`, and log in. Load it into an agent + with `ssh-add` and log in again without typing the passphrase. +2. Turn off password logins on the peer, keeping one session open, and + check in a new terminal that keys still work and passwords don't. +3. Compare the peer's host key fingerprint (`ssh-keygen -lf`) with the one + your client showed. Then regenerate the peer's host keys (in the peer's + shell: `rm /etc/ssh/ssh_host_*` and `ssh-keygen -A`, then restart its + sshd), connect as `alice`, and read the warning. Then remove the old key + from `alice`'s `known_hosts` and connect again. +4. Start a web server that listens only on the peer's localhost + (`python3 -m http.server --bind 127.0.0.1 8000`, in the peer's shell), + and reach it from this VM with `ssh -L`, so that + `curl http://localhost:8000/` here shows its page while the tunnel is up. +5. Make GnuPG keys for `alice` (`Alice `) and `bob` + (`Bob `), each with a passphrase, and exchange public + keys. As `alice`, encrypt `secret.txt` for `bob` as + `/home/alice/secret.txt.gpg`; as `bob`, decrypt it to + `/home/bob/secret.txt`. +6. As `alice`, make a detached signature `/home/alice/notes.txt.sig` of + `notes.txt` and check it. Then change one byte of `notes.txt`, and check + again. +7. As `alice`, generate a revocation certificate for her key with + `--generate-revocation`, import it, and look at `gpg -k`. + +## Hints + +- `ssh-keygen -t ed25519`; `eval "$(ssh-agent)"`, `ssh-add`, `ssh-add -l`. +- `PasswordAuthentication no` in the peer's `sshd_config`; test with + `ssh -o PubkeyAuthentication=no alice@10.99.0.2`. +- `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`; + `ssh-keygen -R 10.99.0.2`. +- Run the web server in the background (`&`) in the peer's shell; + `ssh -N -f -L 8000:127.0.0.1:8000 alice@10.99.0.2`. +- `gpg --full-generate-key` or `--quick-generate-key`, `--export -a`, + `--import`, `-e -r`, `-d -o`; copy files through `/tmp`. +- `gpg --detach-sign`, `gpg --verify notes.txt.sig notes.txt`. +- `gpg --output revoke.asc --generate-revocation alice@example.org`, + `gpg --import revoke.asc`. diff --git a/labs/lpic-1/110-3-encryption/check.sh b/labs/lpic-1/110-3-encryption/check.sh new file mode 100644 index 0000000..1051db1 --- /dev/null +++ b/labs/lpic-1/110-3-encryption/check.sh @@ -0,0 +1,53 @@ +SSHOPT=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR) +KH=/home/alice/.ssh/known_hosts +T=$(mktemp -d) +chmod 755 "$T" +trap 'rm -rf "$T"' EXIT +# known_current: alice's known_hosts has the peer's keys, and only its +# current ones. +known_current() { + local h n=0 + timeout 10 ssh-keyscan -T 5 10.99.0.2 2>/dev/null | awk '{print $2, $3}' | sort -u >"$T/scan" + [ -s "$T/scan" ] || return 1 + for h in 10.99.0.2 peer peer.lab.example; do + ssh-keygen -F "$h" -f "$KH" 2>/dev/null | grep -v '^#' | awk '{print $2, $3}' >>"$T/known" + done + [ -s "$T/known" ] || return 1 + ! grep -vxFf "$T/scan" "$T/known" | grep -q . +} +gpg_as() { local u=$1; shift; runuser -u "$u" -- env HOME="/home/$u" gpg --batch --no-tty "$@" &1; local r=$?; runuser -u "$u" -- env HOME="/home/$u" gpgconf --kill all >/dev/null 2>&1; return $r; } + +task 1 "alice has a passphrase-protected ed25519 key the peer accepts" +need "/home/alice/.ssh/id_ed25519 exists" '[ -f /home/alice/.ssh/id_ed25519 ] && [ -f /home/alice/.ssh/id_ed25519.pub ]' +need "the key is an ed25519 key" 'ssh-keygen -lf /home/alice/.ssh/id_ed25519.pub | grep -q "(ED25519)"' +need "the key has a passphrase" '! ssh-keygen -y -P "" -f /home/alice/.ssh/id_ed25519' +need "the public key is in alice's authorized_keys" 'grep -qF "$(awk "{print \$2}" /home/alice/.ssh/id_ed25519.pub)" /home/alice/.ssh/authorized_keys' + +task 2 "The peer allows keys, not passwords" +need "the peer's sshd -T shows passwordauthentication no" 'mkdir -p /run/sshd; in_peer sshd -T 2>/dev/null | grep -qx "passwordauthentication no"' +need "the running sshd on the peer offers no password logins" 'o=$(timeout 15 ssh "${SSHOPT[@]}" -o PubkeyAuthentication=no alice@10.99.0.2 true 2>&1 &1 /dev/null | grep -qF "$(awk "{print \$2}" /etc/netns/lepeer/ssh/ssh_host_ed25519_key.pub)"' +need "alice's known_hosts has the peer's new keys and none of the old" known_current + +task 4 "curl localhost:8000 reaches the peer's web server through an SSH tunnel" +need "the peer runs a web server on its 127.0.0.1:8000" 'in_peer ss -Hltn "sport = :8000" | grep -q "127\.0\.0\.1:8000"' +need "ssh listens on port 8000 here" 'ss -Hltnp "sport = :8000" | grep -q "\"ssh\""' +need "curl http://localhost:8000/ returns the peer's page" 'curl -s --max-time 5 http://localhost:8000/ | grep -qi "directory listing\|/dev/null +done +systemctl disable --now --quiet le-lab-peer-sshd.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-peer-sshd.service +systemctl daemon-reload +rm -rf /etc/netns/lepeer/ssh +rmdir /etc/netns/lepeer /etc/netns 2>/dev/null +rm -f /tmp/alice.asc /tmp/bob.asc /tmp/secret.txt.gpg +users_remove +true diff --git a/labs/lpic-1/110-3-encryption/setup.sh b/labs/lpic-1/110-3-encryption/setup.sh new file mode 100644 index 0000000..dd4e70d --- /dev/null +++ b/labs/lpic-1/110-3-encryption/setup.sh @@ -0,0 +1,54 @@ +for u in alice bob; do + id "$u" >/dev/null 2>&1 && refuse "user $u exists already; remove it first (userdel -r $u)" +done +listening 8000 tcp && refuse "something listens on TCP port 8000 already; stop it first" +user_add alice +user_add bob +echo "The launch code is 0000." >/home/alice/secret.txt +printf 'Meeting notes\nEverything is on schedule.\n' >/home/alice/notes.txt +chown alice: /home/alice/secret.txt /home/alice/notes.txt + +# The peer, with its own sshd configuration and host keys in +# /etc/netns/lepeer/ssh, which "ip netns exec lepeer" mounts on /etc/ssh. +peer_up +mkdir -p /etc/netns/lepeer/ssh +k=$(mktemp -d) +mkdir -p "$k/etc/ssh" +ssh-keygen -A -f "$k" >/dev/null +mv "$k"/etc/ssh/ssh_host_* /etc/netns/lepeer/ssh/ +rm -rf "$k" +cat >/etc/netns/lepeer/ssh/sshd_config <<'X' +# The peer's sshd configuration. +ListenAddress 10.99.0.2 +HostKey /etc/ssh/ssh_host_ed25519_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_rsa_key +PubkeyAuthentication yes +PasswordAuthentication yes +KbdInteractiveAuthentication no +UsePAM yes +X11Forwarding no +Subsystem sftp internal-sftp +X +command -v restorecon >/dev/null && restorecon -R /etc/netns 2>/dev/null +state_set hostkey "$(ssh-keygen -lf /etc/netns/lepeer/ssh/ssh_host_ed25519_key.pub | awk '{print $2}')" +cat >/etc/systemd/system/le-lab-peer-sshd.service <<'X' +[Unit] +Description=Practice lab sshd on the peer +Requires=le-lab-peer.service +After=le-lab-peer.service network.target + +[Service] +NetworkNamespacePath=/run/netns/lepeer +BindPaths=/etc/netns/lepeer/ssh:/etc/ssh +ExecStartPre=/bin/mkdir -p /run/sshd +ExecStart=/usr/sbin/sshd -D -o PidFile=none +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet le-lab-peer-sshd.service +true diff --git a/labs/rhcsa/1-1-shell-and-redirection/README.md b/labs/rhcsa/1-1-shell-and-redirection/README.md new file mode 100644 index 0000000..993bd4e --- /dev/null +++ b/labs/rhcsa/1-1-shell-and-redirection/README.md @@ -0,0 +1,30 @@ +# RHCSA 1.1: The shell and redirection + +Sending output and errors to different files, appending, counting with a +pipe, and writing to a root-owned file with sudo. + +## Before you start + +The lab removes the files the tasks write in `/tmp`, and any +`test.example.com` line in `/etc/hosts`. Do tasks 1 to 3 as your normal +user, not as root. + +## Tasks + +1. Run `ls /etc/hostname /nonexistent` so that normal output goes to + `/tmp/ok.txt` and the error goes to `/tmp/errors.txt`. +2. Append the current date to `/tmp/log.txt` three times without opening + an editor. +3. Save a list of every file under `/etc` whose name ends in `.conf` to + `/tmp/confs.txt`, with no error messages in it, and show the number of + files on the screen. +4. As a normal user, add the line `192.0.2.50 test.example.com` to + `/etc/hosts` with `sudo` but without opening an editor. + +## Hints + +- `>` and `2>` send the two streams to different files. +- `>>` appends; `date >>file`. +- `find /etc -name '*.conf'`, `2>/dev/null`, `tee`, `wc -l`. +- `sudo echo ... >>/etc/hosts` fails: the shell opens the file, not sudo. + `| sudo tee -a`. diff --git a/labs/rhcsa/1-1-shell-and-redirection/check.sh b/labs/rhcsa/1-1-shell-and-redirection/check.sh new file mode 100644 index 0000000..e754f96 --- /dev/null +++ b/labs/rhcsa/1-1-shell-and-redirection/check.sh @@ -0,0 +1,21 @@ +task 1 "Output in /tmp/ok.txt, the error in /tmp/errors.txt" +need "/tmp/ok.txt holds only /etc/hostname" '[ "$(cat /tmp/ok.txt)" = /etc/hostname ]' +need "/tmp/errors.txt holds one line" '[ "$(wc -l /dev/null | wc -l) && + [ "$(wc -l FILE`. +- `grep -v`, `^#`, `^$`, or `grep -Ev '^(#|$)'`. +- `grep '/bin/bash$'` and `cut -d: -f1`, or `awk -F:`. +- `grep -c`. diff --git a/labs/rhcsa/1-2-grep-and-regular-expressions/check.sh b/labs/rhcsa/1-2-grep-and-regular-expressions/check.sh new file mode 100644 index 0000000..702f23c --- /dev/null +++ b/labs/rhcsa/1-2-grep-and-regular-expressions/check.sh @@ -0,0 +1,17 @@ +task 1 "Lines of /etc/services with ich in /root/ich.txt" +need "/root/ich.txt exists" '[ -s /root/ich.txt ]' +need "it matches grep ich /etc/services, in order" 'diff -q <(grep ich /etc/services) /root/ich.txt' + +task 2 "Active settings of sshd_config in /root/lab-answers/2" +need "/root/lab-answers/2 isn't empty" '[ -s /root/lab-answers/2 ]' +need "it has no comment lines" '! grep -Eq "^[[:space:]]*#" /root/lab-answers/2' +need "it has no blank lines" '! grep -Eq "^[[:space:]]*$" /root/lab-answers/2' +need "it has every active setting of /etc/ssh/sshd_config" 'grep -Ev "^[[:space:]]*(#|$)" /etc/ssh/sshd_config | grep -vxFf /root/lab-answers/2 | { ! grep -q .; }' + +task 3 "Users whose shell is /bin/bash in /root/lab-answers/3" +need "/root/lab-answers/3 lists grace and henry" 'grep -qx grace /root/lab-answers/3 && grep -qx henry /root/lab-answers/3' +need "it doesn't list ivan, whose shell is /sbin/nologin" '! grep -qx ivan /root/lab-answers/3' +need "it matches getent passwd exactly" 'diff -q <(getent passwd | awk -F: "\$7 == \"/bin/bash\" {print \$1}" | sort -u) <(tr -s " \t" "\n\n" /dev/null && head -n1 /root/lab-answers/3 | grep -q "^uid=$(id -u alice)(alice) "' + +task 4 "alice logged in on tty2" +need "who lists alice on tty2" 'who | grep -Eq "^alice[[:space:]]+tty2[[:space:]]"' diff --git a/labs/rhcsa/1-3-ssh-and-switching-users/lab.conf b/labs/rhcsa/1-3-ssh-and-switching-users/lab.conf new file mode 100644 index 0000000..cb92871 --- /dev/null +++ b/labs/rhcsa/1-3-ssh-and-switching-users/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.3 SSH and switching users" +CHAPTER=/rhcsa/1-essential-tools/1-3-ssh-and-switching-users/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/1-3-ssh-and-switching-users/reset.sh b/labs/rhcsa/1-3-ssh-and-switching-users/reset.sh new file mode 100644 index 0000000..f20b452 --- /dev/null +++ b/labs/rhcsa/1-3-ssh-and-switching-users/reset.sh @@ -0,0 +1,6 @@ +systemctl disable --now --quiet le-lab-peer-sshd.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-peer-sshd.service +systemctl daemon-reload +pkill -KILL -u alice 2>/dev/null +userdel -r alice 2>/dev/null +true diff --git a/labs/rhcsa/1-3-ssh-and-switching-users/setup.sh b/labs/rhcsa/1-3-ssh-and-switching-users/setup.sh new file mode 100644 index 0000000..60a7901 --- /dev/null +++ b/labs/rhcsa/1-3-ssh-and-switching-users/setup.sh @@ -0,0 +1,30 @@ +if id alice >/dev/null 2>&1; then + refuse "user alice already exists; task 2 creates her. Remove her (userdel -r alice) and start the lab again." +fi +user_add tester +# A marker in /var/log/secure: tasks 1 and 2 look for logins after it. +token=le-lab-$(date +%s)-$RANDOM +state_set token "$token" +state_set since "$(date +%s)" +# Written straight into the file: through logger it can arrive late. +printf '%s %s le-lab: lab started %s\n' "$(date '+%b %e %T')" "$(hostname -s)" "$token" >>/var/log/secure +peer_up +# The peer's own sshd; logins to it come from 10.99.0.1. +cat >/etc/systemd/system/le-lab-peer-sshd.service <<'X' +[Unit] +Description=Practice lab sshd on the peer +Requires=le-lab-peer.service +After=le-lab-peer.service network.target + +[Service] +NetworkNamespacePath=/run/netns/lepeer +ExecStartPre=/bin/mkdir -p /run/sshd +ExecStart=/usr/sbin/sshd -D -o ListenAddress=10.99.0.2:22 -o PidFile=none -o PasswordAuthentication=yes -o KbdInteractiveAuthentication=yes +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet le-lab-peer-sshd.service diff --git a/labs/rhcsa/1-4-archives-and-compression/README.md b/labs/rhcsa/1-4-archives-and-compression/README.md new file mode 100644 index 0000000..7f2eb41 --- /dev/null +++ b/labs/rhcsa/1-4-archives-and-compression/README.md @@ -0,0 +1,27 @@ +# RHCSA 1.4: Archives and compression + +Creating a compressed archive, extracting one file from it, compressing a +copy while keeping the original, and an archive that keeps owners, +permissions, SELinux contexts and ACLs. + +## Before you start + +The lab installs `bzip2`, and creates users `dana` and `erik`. A file in +dana's home directory, `shared.txt`, has an ACL that lets erik read it. + +## Tasks + +1. Create a bzip2-compressed archive of `/etc` named + `/root/etc-backup.tar.bz2`. +2. Extract only `etc/hostname` from that archive into `/tmp/restore`. +3. Compress a copy of `/var/log/messages` in `/tmp` with gzip, keeping the + uncompressed copy. +4. As root, archive `/home` with gzip so that owners, permissions, SELinux + contexts and ACLs are kept, to `/root/home.tgz`. + +## Hints + +- `tar -cjf`; `file`, `tar -tf`. +- `tar -xf ARCHIVE -C DIR MEMBER`; the directory must exist first. +- `gzip -k`, or `gzip -c FILE >FILE.gz`. +- `tar --selinux --acls --xattrs -czf`; `tar -tvzf`. diff --git a/labs/rhcsa/1-4-archives-and-compression/check.sh b/labs/rhcsa/1-4-archives-and-compression/check.sh new file mode 100644 index 0000000..3556953 --- /dev/null +++ b/labs/rhcsa/1-4-archives-and-compression/check.sh @@ -0,0 +1,20 @@ +task 1 "/root/etc-backup.tar.bz2: a bzip2 archive of /etc" +need "/root/etc-backup.tar.bz2 is bzip2 data" 'file -b /root/etc-backup.tar.bz2 | grep -q "^bzip2 compressed"' +need "it holds files under etc/, such as etc/hostname" 'tar -tjf /root/etc-backup.tar.bz2 | grep -qx etc/hostname' +need "it holds all of /etc (etc/passwd too)" 'tar -tjf /root/etc-backup.tar.bz2 | grep -qx etc/passwd' + +task 2 "Only etc/hostname extracted into /tmp/restore" +need "/tmp/restore/etc/hostname is the host name" 'cmp -s /tmp/restore/etc/hostname /etc/hostname' +need "nothing else was extracted" '[ "$(find /tmp/restore -mindepth 1 | wc -l)" = 2 ]' + +task 3 "/tmp/messages and /tmp/messages.gz" +need "/tmp/messages exists" '[ -s /tmp/messages ]' +need "it's a copy of /var/log/messages" 'cmp -s <(head -n 5 /tmp/messages) <(head -n 5 /var/log/messages) || cmp -s <(head -n 5 /tmp/messages) <(head -n 5 "$(ls -t /var/log/messages-* 2>/dev/null | head -n1)")' +need "/tmp/messages.gz is gzip data" 'gzip -t /tmp/messages.gz' +need "/tmp/messages.gz uncompresses to /tmp/messages" 'cmp -s <(zcat /tmp/messages.gz) /tmp/messages' + +task 4 "/root/home.tgz keeps owners, permissions, SELinux contexts and ACLs" +need "/root/home.tgz is gzip data" 'gzip -t /root/home.tgz' +need "it shows each home directory owned by its user" 'tar -tvzf /root/home.tgz | grep -Eq "^d[^ ]* dana/dana .* home/dana/?$" && tar -tvzf /root/home.tgz | grep -Eq "^d[^ ]* erik/erik .* home/erik/?$"' +need "it holds SELinux contexts (--selinux)" 'zcat /root/home.tgz | grep -aq "RHT\.security\.selinux"' +need "it holds ACLs (--acls)" 'zcat /root/home.tgz | grep -aq "SCHILY\.acl\.access"' diff --git a/labs/rhcsa/1-4-archives-and-compression/lab.conf b/labs/rhcsa/1-4-archives-and-compression/lab.conf new file mode 100644 index 0000000..4aca6bd --- /dev/null +++ b/labs/rhcsa/1-4-archives-and-compression/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.4 Archives and compression" +CHAPTER=/rhcsa/1-essential-tools/1-4-archives-and-compression/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/1-4-archives-and-compression/reset.sh b/labs/rhcsa/1-4-archives-and-compression/reset.sh new file mode 100644 index 0000000..d7892e7 --- /dev/null +++ b/labs/rhcsa/1-4-archives-and-compression/reset.sh @@ -0,0 +1,2 @@ +rm -rf /root/etc-backup.tar.bz2 /tmp/restore /tmp/messages /tmp/messages.gz /root/home.tgz +true diff --git a/labs/rhcsa/1-4-archives-and-compression/setup.sh b/labs/rhcsa/1-4-archives-and-compression/setup.sh new file mode 100644 index 0000000..7808a0d --- /dev/null +++ b/labs/rhcsa/1-4-archives-and-compression/setup.sh @@ -0,0 +1,9 @@ +pkg_install bzip2 acl +[ -f /var/log/messages ] || refuse "/var/log/messages doesn't exist; is rsyslog installed and running?" +user_add dana +user_add erik +echo "Notes dana shares with erik" >/home/dana/shared.txt +chown dana: /home/dana/shared.txt +chmod 600 /home/dana/shared.txt +setfacl -m u:erik:r /home/dana/shared.txt +rm -rf /root/etc-backup.tar.bz2 /tmp/restore /tmp/messages /tmp/messages.gz /root/home.tgz diff --git a/labs/rhcsa/1-5-files-directories-and-links/README.md b/labs/rhcsa/1-5-files-directories-and-links/README.md new file mode 100644 index 0000000..8b86eb1 --- /dev/null +++ b/labs/rhcsa/1-5-files-directories-and-links/README.md @@ -0,0 +1,28 @@ +# RHCSA 1.5: Files, directories and links + +Creating a directory tree in one command, copying with everything kept, +hard and symbolic links, and editing a file with vi. + +## Before you start + +The lab saves `/etc/motd` and puts it back on reset. It removes +`/root/skel-backup`, `/root/hosts-hard` and `/root/hosts-soft` if they +exist. + +## Tasks + +1. Create the directories `/data/web/html` and `/data/web/logs` in one + command. +2. Copy `/etc/skel` to `/root/skel-backup`, keeping permissions, ownership + and timestamps, including the hidden files. +3. Create a hard link `/root/hosts-hard` to `/etc/hosts` and a symbolic + link `/root/hosts-soft` to it. +4. Using vi, add the line `Authorized users only` at the end of + `/etc/motd`, then quit. + +## Hints + +- `mkdir -p` and brace expansion: `{html,logs}`. +- `cp -a`; `ls -la` shows the dot files and dates. +- `ln`, `ln -s`; `ls -li` shows inode numbers. +- `G` goes to the last line, `o` opens a line below it, Esc, then `:wq`. diff --git a/labs/rhcsa/1-5-files-directories-and-links/check.sh b/labs/rhcsa/1-5-files-directories-and-links/check.sh new file mode 100644 index 0000000..236655a --- /dev/null +++ b/labs/rhcsa/1-5-files-directories-and-links/check.sh @@ -0,0 +1,25 @@ +# same_meta A B: same type, mode, owner, group and modification time. +same_meta() { [ "$(stat -c '%F %a %U %G %Y' "$1" 2>/dev/null)" = "$(stat -c '%F %a %U %G %Y' "$2" 2>/dev/null)" ]; } +skel_copied() { + local f + while read -r f; do + same_meta "/etc/skel/$f" "/root/skel-backup/$f" || return 1 + done < <(cd /etc/skel && find . -mindepth 1) +} + +task 1 "/data/web/html and /data/web/logs" +need "/data/web/html is a directory" '[ -d /data/web/html ]' +need "/data/web/logs is a directory" '[ -d /data/web/logs ]' + +task 2 "/etc/skel copied to /root/skel-backup with everything kept" +need "/root/skel-backup is a directory" '[ -d /root/skel-backup ]' +need "it holds the hidden files, such as .bashrc" '[ -f /root/skel-backup/.bashrc ]' +need "every file has the same mode, owner and date as in /etc/skel" skel_copied + +task 3 "Hard link /root/hosts-hard, symbolic link /root/hosts-soft" +need "/root/hosts-hard has the same inode as /etc/hosts" '[ ! -L /root/hosts-hard ] && [ /root/hosts-hard -ef /etc/hosts ]' +need "/root/hosts-soft is a symbolic link" '[ -L /root/hosts-soft ]' +need "/root/hosts-soft points to /etc/hosts" '[ "$(readlink -f /root/hosts-soft)" = /etc/hosts ]' + +task 4 "Authorized users only at the end of /etc/motd" +need "the last line of /etc/motd is Authorized users only" '[ "$(tail -n1 /etc/motd)" = "Authorized users only" ]' diff --git a/labs/rhcsa/1-5-files-directories-and-links/lab.conf b/labs/rhcsa/1-5-files-directories-and-links/lab.conf new file mode 100644 index 0000000..a8dab3a --- /dev/null +++ b/labs/rhcsa/1-5-files-directories-and-links/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.5 Files, directories and links" +CHAPTER=/rhcsa/1-essential-tools/1-5-files-directories-and-links/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/1-5-files-directories-and-links/reset.sh b/labs/rhcsa/1-5-files-directories-and-links/reset.sh new file mode 100644 index 0000000..ddba511 --- /dev/null +++ b/labs/rhcsa/1-5-files-directories-and-links/reset.sh @@ -0,0 +1,9 @@ +rm -rf /data/web /root/skel-backup /root/hosts-hard /root/hosts-soft +[ -n "$(state_get had-data)" ] || rmdir /data 2>/dev/null +if [ -f "$LAB_STATE/motd" ]; then + cp "$LAB_STATE/motd" /etc/motd + restorecon /etc/motd 2>/dev/null +else + sed -i '/^Authorized users only$/d' /etc/motd 2>/dev/null +fi +true diff --git a/labs/rhcsa/1-5-files-directories-and-links/setup.sh b/labs/rhcsa/1-5-files-directories-and-links/setup.sh new file mode 100644 index 0000000..494f669 --- /dev/null +++ b/labs/rhcsa/1-5-files-directories-and-links/setup.sh @@ -0,0 +1,5 @@ +[ -e /data/web ] && refuse "/data/web already exists; task 1 creates it. Remove it and start the lab again." +[ -e /data ] && state_set had-data 1 +rm -rf /root/skel-backup /root/hosts-hard /root/hosts-soft +[ -f /etc/motd ] && cp -a /etc/motd "$LAB_STATE/motd" +sed -i '/^Authorized users only$/d' /etc/motd 2>/dev/null || true diff --git a/labs/rhcsa/1-6-standard-permissions/README.md b/labs/rhcsa/1-6-standard-permissions/README.md new file mode 100644 index 0000000..9595feb --- /dev/null +++ b/labs/rhcsa/1-6-standard-permissions/README.md @@ -0,0 +1,33 @@ +# RHCSA 1.6: Standard permissions + +Owners and modes on a file, a directory only its group can use, fixing a +whole tree in one command, and finding the directory that blocks access. + +## Before you start + +The lab creates users `alice` and `bob` (neither is in `wheel`), a tree +`/srv/project` whose files and directories have a mix of modes, and +`/opt/app/conf/app.conf`, mode 644, that alice can't read. Write the +finding of task 4 to `/root/lab-answers/4`. + +## Tasks + +1. Create `/srv/reports/q3.txt`, owned by `alice` and the group `wheel`, + readable and writable by alice, readable by the group, and with no + access for anyone else. +2. Make the directory `/srv/reports` usable by its group (list, enter, + create files) but closed to others. +3. Make every directory under `/srv/project` mode 755 and every file 644 in + one `chmod` command. +4. Find out why alice can't read `/opt/app/conf/app.conf` although the + file is mode 644. Write the full path of the directory that blocks her + to `/root/lab-answers/4`, then fix it so she can read the file. + +## Hints + +- `chown alice:wheel`, `chmod 640` or `chmod u=rw,g=r,o=`. +- A directory needs `x` to be entered and `r` to be listed: `770`. + `su - bob -c 'ls /srv/reports'` should fail. +- `chmod -R u=rwX,go=rX`: capital `X` sets execute only on directories + (and files that already have it, so check the files too). +- `namei -l` shows the permissions of every directory on the path. diff --git a/labs/rhcsa/1-6-standard-permissions/check.sh b/labs/rhcsa/1-6-standard-permissions/check.sh new file mode 100644 index 0000000..6f812d3 --- /dev/null +++ b/labs/rhcsa/1-6-standard-permissions/check.sh @@ -0,0 +1,18 @@ +task 1 "/srv/reports/q3.txt: alice and wheel, -rw-r-----" +need "/srv/reports/q3.txt exists" '[ -f /srv/reports/q3.txt ]' +need "it's owned by alice and the group wheel" '[ "$(stat -c "%U %G" /srv/reports/q3.txt)" = "alice wheel" ]' +need "its mode is 640 (-rw-r-----)" '[ "$(stat -c %a /srv/reports/q3.txt)" = 640 ]' + +task 2 "/srv/reports usable by its group, closed to others" +need "/srv/reports has mode 770" '[ "$(stat -c %a /srv/reports)" = 770 ]' +need "bob, not in the directory's group, can't list it" '! id -nG bob | grep -qw "$(stat -c %G /srv/reports)" && ! runuser -u bob -- ls /srv/reports' + +task 3 "/srv/project: directories 755, files 644" +need "every directory under /srv/project is 755" '[ -z "$(find /srv/project -type d ! -perm 755)" ]' +need "every file under /srv/project is 644" '[ -z "$(find /srv/project -type f ! -perm 644)" ]' +need "the files are all still there" '[ "$(find /srv/project -type f | wc -l)" = 6 ]' + +task 4 "The blocking directory found; alice can read app.conf" +need "/root/lab-answers/4 names /opt/app/conf" '[ "$(answer 4 | sed "s|/*$||")" = /opt/app/conf ]' +need "alice can read /opt/app/conf/app.conf" 'runuser -u alice -- cat /opt/app/conf/app.conf' +need "app.conf is still mode 644" '[ "$(stat -c %a /opt/app/conf/app.conf)" = 644 ]' diff --git a/labs/rhcsa/1-6-standard-permissions/lab.conf b/labs/rhcsa/1-6-standard-permissions/lab.conf new file mode 100644 index 0000000..8bf18bb --- /dev/null +++ b/labs/rhcsa/1-6-standard-permissions/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.6 Standard permissions" +CHAPTER=/rhcsa/1-essential-tools/1-6-standard-permissions/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/1-6-standard-permissions/reset.sh b/labs/rhcsa/1-6-standard-permissions/reset.sh new file mode 100644 index 0000000..f9cc7ac --- /dev/null +++ b/labs/rhcsa/1-6-standard-permissions/reset.sh @@ -0,0 +1,2 @@ +rm -rf /srv/reports /srv/project /opt/app +true diff --git a/labs/rhcsa/1-6-standard-permissions/setup.sh b/labs/rhcsa/1-6-standard-permissions/setup.sh new file mode 100644 index 0000000..182edc7 --- /dev/null +++ b/labs/rhcsa/1-6-standard-permissions/setup.sh @@ -0,0 +1,23 @@ +for d in /srv/reports /srv/project /opt/app; do + [ -e "$d" ] && refuse "$d already exists; the lab creates or uses it. Remove it and start the lab again." +done +user_add alice +user_add bob +# Task 3: a tree with a mix of modes. +mkdir -p /srv/project/docs/old /srv/project/src/lib /srv/project/bin +for f in README docs/guide.txt docs/old/notes.txt src/main.c src/lib/util.c bin/run.sh; do + echo "lab file $f" >"/srv/project/$f" +done +chmod 700 /srv/project/docs +chmod 777 /srv/project/src +chmod 750 /srv/project/docs/old +chmod 600 /srv/project/README +chmod 666 /srv/project/src/main.c +chmod 755 /srv/project/bin/run.sh +chmod 640 /srv/project/docs/guide.txt +# Task 4: a readable file in a directory alice can't enter. +mkdir -p /opt/app/conf +echo "port = 8080" >/opt/app/conf/app.conf +chmod 755 /opt/app +chmod 700 /opt/app/conf +chmod 644 /opt/app/conf/app.conf diff --git a/labs/rhcsa/1-7-system-documentation/README.md b/labs/rhcsa/1-7-system-documentation/README.md new file mode 100644 index 0000000..733a7b5 --- /dev/null +++ b/labs/rhcsa/1-7-system-documentation/README.md @@ -0,0 +1,31 @@ +# RHCSA 1.7: System documentation + +Man page sections, finding a setting without the internet, the man page +index, and the documentation a package installs. + +## Before you start + +The lab moves the man page index away (`/var/cache/man/index.db`), so +`man -k` and `whatis` find nothing until you rebuild it in task 3. Write +your findings to the files in `/root/lab-answers/` named in the tasks. + +## Tasks + +1. Find which man page section documents the format of `/etc/crontab`, + and open that page. Write the section number to `/root/lab-answers/1`. +2. Without the internet, find the `sshd_config` keyword that sets the + maximum number of authentication attempts per connection. Write the + keyword and its current value, as `sshd -T` shows them (for example + `logingracetime 120`), to `/root/lab-answers/2`. +3. As root, rebuild the man page index, then search it for timers. + `man -k timer` should list `systemd.timer (5)`. +4. List every documentation file installed by the `sudo` package and find + its example sudoers file. Write its full path to `/root/lab-answers/4`. + +## Hints + +- `man -k crontab` and `whatis crontab` need the index (task 3); + `man -aw crontab` lists the pages without it. Section 5 is file formats. +- `man sshd_config`, then `/` to search; `sudo sshd -T | grep -i`. +- `mandb`. +- `rpm -qd sudo`. diff --git a/labs/rhcsa/1-7-system-documentation/check.sh b/labs/rhcsa/1-7-system-documentation/check.sh new file mode 100644 index 0000000..239d2ec --- /dev/null +++ b/labs/rhcsa/1-7-system-documentation/check.sh @@ -0,0 +1,12 @@ +task 1 "The section of /etc/crontab's format in /root/lab-answers/1" +need "/root/lab-answers/1 holds the right section" '[ "$(answer 1)" = 5 ]' + +task 2 "The sshd_config keyword and value in /root/lab-answers/2" +need "/root/lab-answers/2 holds the keyword and its current value" '[ "$(answer 2 | tr -s " \t" " ")" = "$(sshd -T 2>/dev/null | grep -i "^maxauthtries ")" ]' + +task 3 "The man page index rebuilt" +need "/var/cache/man/index.db exists again" '[ -f /var/cache/man/index.db ]' +need "man -k timer lists systemd.timer (5)" 'man -k timer | grep -q "^systemd\.timer (5)"' + +task 4 "sudo's example sudoers file in /root/lab-answers/4" +need "/root/lab-answers/4 holds its full path" '[ "$(head -n1 /root/lab-answers/4 2>/dev/null | tr -d " \t")" = "$(rpm -qd sudo | grep "examples/sudoers$")" ]' diff --git a/labs/rhcsa/1-7-system-documentation/lab.conf b/labs/rhcsa/1-7-system-documentation/lab.conf new file mode 100644 index 0000000..da4918f --- /dev/null +++ b/labs/rhcsa/1-7-system-documentation/lab.conf @@ -0,0 +1,3 @@ +TITLE="1.7 System documentation" +CHAPTER=/rhcsa/1-essential-tools/1-7-system-documentation/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/1-7-system-documentation/reset.sh b/labs/rhcsa/1-7-system-documentation/reset.sh new file mode 100644 index 0000000..70bef8a --- /dev/null +++ b/labs/rhcsa/1-7-system-documentation/reset.sh @@ -0,0 +1,5 @@ +if [ -f "$LAB_STATE/index.db" ] && [ ! -f /var/cache/man/index.db ]; then + mv "$LAB_STATE/index.db" /var/cache/man/index.db + restorecon /var/cache/man/index.db 2>/dev/null +fi +true diff --git a/labs/rhcsa/1-7-system-documentation/setup.sh b/labs/rhcsa/1-7-system-documentation/setup.sh new file mode 100644 index 0000000..58544cc --- /dev/null +++ b/labs/rhcsa/1-7-system-documentation/setup.sh @@ -0,0 +1,5 @@ +command -v mandb >/dev/null || refuse "man-db isn't installed; install it (dnf install man-db man-pages) and start the lab again." +[ -f /usr/share/man/man5/systemd.timer.5.gz ] || refuse "the systemd man pages aren't installed (check tsflags=nodocs in /etc/dnf/dnf.conf)." +if [ -f /var/cache/man/index.db ]; then + mv /var/cache/man/index.db "$LAB_STATE/index.db" +fi diff --git a/labs/rhcsa/10-1-firewall-settings/README.md b/labs/rhcsa/10-1-firewall-settings/README.md new file mode 100644 index 0000000..15328d6 --- /dev/null +++ b/labs/rhcsa/10-1-firewall-settings/README.md @@ -0,0 +1,38 @@ +# RHCSA 10.1: Firewall settings + +A rich rule that allows SSH from one network only, port forwarding, and +sending a network's traffic to another zone. + +## Before you start + +Do this lab at the VM's console, or keep a console open: task 1 changes +who may reach SSH, and a mistake can lock you out. + +The lab installs and starts firewalld if it isn't running, and notes its +configuration so reset can put it back. It installs and starts `httpd` +with a test page on port 80, and adds a second machine, `peer` +(`10.99.0.2`), on the lab network `10.99.0.0/24` (this VM is `10.99.0.1` +there), to test from. Get a shell there with +`sudo ip netns exec lepeer bash`. Make every change persistent. + +## Tasks + +1. Allow SSH only from the network your SSH client is on (such as + `10.0.2.0/24` for a QEMU user-mode network), with a rich rule, and take + the `ssh` service out of the zone. You can still log in from inside + that network; the peer can't reach SSH any more. +2. Forward TCP port 8080 to port 80 on this host, so that from the peer + `curl http://10.99.0.1:8080/` reaches httpd. +3. Send all traffic from `192.168.50.0/24` to the `internal` zone. + +## Hints + +- `ss -tn state established '( sport = :22 )'` shows where your SSH + client connects from. Add the rule before you remove `ssh`: + `firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source + address=NETWORK service name=ssh accept'`, `--remove-service=ssh`, + `--reload`, `--list-rich-rules`. +- `--add-forward-port=port=8080:proto=tcp:toport=80`, + `--list-forward-ports`; `sudo ip netns exec lepeer curl + http://10.99.0.1:8080/`. +- `--zone=internal --add-source=`, `--get-active-zones`. diff --git a/labs/rhcsa/10-1-firewall-settings/check.sh b/labs/rhcsa/10-1-firewall-settings/check.sh new file mode 100644 index 0000000..6ee58e7 --- /dev/null +++ b/labs/rhcsa/10-1-firewall-settings/check.sh @@ -0,0 +1,49 @@ +Z=$(state_get zone) +fw() { firewall-cmd --zone="$Z" "$@"; } +# Rich rules that accept SSH from a source address, now (or --permanent). +ssh_rules() { fw "$@" --list-rich-rules 2>/dev/null | grep -E 'source address="?[0-9.]+(/[0-9]+)?' | grep -E 'service name="?ssh"?|port port="?22"?' | grep -w accept; } +# Every established SSH client address falls in a rule's network. +clients_covered() { + local nets + nets=$(ssh_rules | sed -E 's/.*source address="?([0-9./]+)"?.*/\1/') + ss -Htn state established '( sport = :22 )' | awk '{print $4}' | sed -E 's/:[0-9]+$//; s/^\[?::ffff://; s/\]$//' | + python3 -c ' +import ipaddress, sys +nets = [ipaddress.ip_network(n, strict=False) for n in sys.argv[1].split()] +for a in sys.stdin.read().split(): + try: + ip = ipaddress.ip_address(a) + except ValueError: + continue + if ip.is_loopback or ip.version != 4: + continue + if not any(ip in n for n in nets): + sys.exit(1)' "$nets" +} +peer_ssh() { in_peer timeout 5 bash -c 'exec 3<>/dev/tcp/10.99.0.1/22; head -c 4 <&3' 2>/dev/null | grep -q SSH; } +peer_curl() { + local i + for i in 1 2 3 4 5; do + in_peer curl -s --max-time 3 http://10.99.0.1:8080/ | grep -q "Lab httpd" && return 0 + sleep 2 + done + return 1 +} + +task 1 "SSH allowed only from one network, with a rich rule" +need "firewalld is running" active firewalld +need "a rich rule accepts SSH from a network, now and permanently" '[ -n "$(ssh_rules)" ] && [ -n "$(ssh_rules --permanent)" ]' +need "the ssh service is out of the zone, now and permanently" '! fw --query-service=ssh && ! fw --permanent --query-service=ssh' +need "port 22 isn't open to everyone" '! fw --query-port=22/tcp' +need "your current SSH connections come from inside the rule's network" clients_covered +need "the peer (10.99.0.2) can't reach SSH" '! peer_ssh' + +task 2 "TCP port 8080 forwarded to port 80" +need "httpd is running" active httpd +need "the zone forwards 8080/tcp to port 80, now and permanently" 'fw --list-forward-ports | grep -q "port=8080:proto=tcp:toport=80:" && fw --permanent --list-forward-ports | grep -q "port=8080:proto=tcp:toport=80:"' +need "curl http://10.99.0.1:8080/ from the peer reaches httpd" peer_curl + +task 3 "Traffic from 192.168.50.0/24 goes to the internal zone" +need "the internal zone has source 192.168.50.0/24, now" 'firewall-cmd --zone=internal --list-sources | grep -qw "192\.168\.50\.0/24"' +need "and permanently" 'firewall-cmd --permanent --zone=internal --list-sources | grep -qw "192\.168\.50\.0/24"' +need "firewall-cmd --get-active-zones lists internal with it" 'firewall-cmd --get-active-zones | grep -A3 "^internal" | grep -q "192\.168\.50\.0/24"' diff --git a/labs/rhcsa/10-1-firewall-settings/lab.conf b/labs/rhcsa/10-1-firewall-settings/lab.conf new file mode 100644 index 0000000..4141489 --- /dev/null +++ b/labs/rhcsa/10-1-firewall-settings/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.1 Firewall settings" +CHAPTER=/rhcsa/10-security/10-1-firewall-settings/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-1-firewall-settings/reset.sh b/labs/rhcsa/10-1-firewall-settings/reset.sh new file mode 100644 index 0000000..e9acef4 --- /dev/null +++ b/labs/rhcsa/10-1-firewall-settings/reset.sh @@ -0,0 +1,17 @@ +if [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" +fi +if [ -n "$(state_get firewalld-was-active)" ]; then + firewall-cmd -q --reload +else + systemctl stop firewalld +fi +[ -n "$(state_get firewalld-was-enabled)" ] || systemctl disable --quiet firewalld 2>/dev/null +if [ -f "$LAB_STATE/index.html" ]; then + cp -a "$LAB_STATE/index.html" /var/www/html/index.html +else + rm -f /var/www/html/index.html +fi +[ -n "$(state_get httpd-was-enabled)" ] || systemctl disable --now --quiet httpd 2>/dev/null +true diff --git a/labs/rhcsa/10-1-firewall-settings/setup.sh b/labs/rhcsa/10-1-firewall-settings/setup.sh new file mode 100644 index 0000000..adcf63f --- /dev/null +++ b/labs/rhcsa/10-1-firewall-settings/setup.sh @@ -0,0 +1,22 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +[ -n "$IF" ] || refuse "no default route; this lab needs a working network" +pkg_installed firewalld || pkg_install firewalld +pkg_installed httpd && state_set httpd-before 1 +pkg_installed httpd || pkg_install httpd +systemctl is-active --quiet firewalld && state_set firewalld-was-active 1 +enabled firewalld && state_set firewalld-was-enabled 1 +enabled httpd && state_set httpd-was-enabled 1 +tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld +systemctl enable --now --quiet firewalld +firewall-cmd -q --state +Z=$(firewall-cmd --get-zone-of-interface="$IF" 2>/dev/null || firewall-cmd --get-default-zone) +state_set zone "$Z" +# The starting point: SSH allowed as a service, no forward, no sources. +firewall-cmd -q --permanent --zone="$Z" --query-service=ssh || firewall-cmd -q --permanent --zone="$Z" --add-service=ssh +firewall-cmd -q --reload + +[ -f /var/www/html/index.html ] && cp -a /var/www/html/index.html "$LAB_STATE/index.html" +echo 'Lab httpd on port 80' >/var/www/html/index.html +restorecon /var/www/html/index.html 2>/dev/null || true +systemctl enable --now --quiet httpd +peer_up diff --git a/labs/rhcsa/10-2-default-file-permissions/README.md b/labs/rhcsa/10-2-default-file-permissions/README.md new file mode 100644 index 0000000..0ba9675 --- /dev/null +++ b/labs/rhcsa/10-2-default-file-permissions/README.md @@ -0,0 +1,23 @@ +# RHCSA 10.2: Default file permissions + +A umask for one user, and a default umask for everyone. + +## Before you start + +The lab creates users `alice` and `bob` (password `Lab-Pass-1`), and notes +the files in `/etc/profile.d/` so reset can take out the ones you add. + +## Tasks + +1. Make new files of user `alice` readable and writable only by her, + persistently: `su - alice -c 'umask; touch t; ls -l t'` prints `0077` + and `-rw-------`. +2. Make every user's new directories `rwxr-x---` by default: for a user + without their own umask line, `su - bob -c 'umask; mkdir d; ls -ld d'` + prints `0027` and `drwxr-x---`. `alice` keeps her own umask. + +## Hints + +- `umask 077` in `~alice/.bashrc`. +- `umask 027` in a file in `/etc/profile.d/` ending in `.sh`. Don't edit + `/etc/profile` or `/etc/bashrc`. diff --git a/labs/rhcsa/10-2-default-file-permissions/check.sh b/labs/rhcsa/10-2-default-file-permissions/check.sh new file mode 100644 index 0000000..9c5d64a --- /dev/null +++ b/labs/rhcsa/10-2-default-file-permissions/check.sh @@ -0,0 +1,21 @@ +# A login shell's umask, and the mode of something it creates. +um() { runuser -l "$1" -c umask 2>/dev/null; } +made() { + local u=$1 kind=$2 f m + f=$(runuser -l "$u" -c 'd=$(mktemp -d); echo $d' 2>/dev/null) + [ -n "$f" ] || return 1 + if [ "$kind" = file ]; then runuser -l "$u" -c "touch $f/t"; m=$(stat -c %A "$f/t"); else runuser -l "$u" -c "mkdir $f/d"; m=$(stat -c %A "$f/d"); fi + rm -rf "$f" + echo "$m" +} + +task 1 "alice's new files are rw------- (umask 0077)" +need "su - alice -c umask prints 0077" '[ "$(um alice)" = 0077 ]' +need "a file alice creates is -rw-------" '[ "$(made alice file)" = -rw------- ]' +need "the umask is set in one of alice's own files" 'grep -Eqs "^[[:space:]]*umask[[:space:]]+0?077" /home/alice/.bashrc /home/alice/.bash_profile /home/alice/.profile' + +task 2 "Everyone's new directories are rwxr-x--- (umask 0027)" +need "su - bob -c umask prints 0027" '[ "$(um bob)" = 0027 ]' +need "a directory bob creates is drwxr-x---" '[ "$(made bob dir)" = drwxr-x--- ]' +need "the umask is in a file in /etc/profile.d/" 'grep -Eqs "^[[:space:]]*umask[[:space:]]+0?027" /etc/profile.d/*.sh' +need "alice still has 0077" '[ "$(um alice)" = 0077 ]' diff --git a/labs/rhcsa/10-2-default-file-permissions/lab.conf b/labs/rhcsa/10-2-default-file-permissions/lab.conf new file mode 100644 index 0000000..064a033 --- /dev/null +++ b/labs/rhcsa/10-2-default-file-permissions/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.2 Default file permissions" +CHAPTER=/rhcsa/10-security/10-2-default-file-permissions/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-2-default-file-permissions/reset.sh b/labs/rhcsa/10-2-default-file-permissions/reset.sh new file mode 100644 index 0000000..e06832b --- /dev/null +++ b/labs/rhcsa/10-2-default-file-permissions/reset.sh @@ -0,0 +1,6 @@ +for f in /etc/profile.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/profile-d" 2>/dev/null && continue + grep -q umask "$f" && rm -f "$f" +done +true diff --git a/labs/rhcsa/10-2-default-file-permissions/setup.sh b/labs/rhcsa/10-2-default-file-permissions/setup.sh new file mode 100644 index 0000000..e8f0a48 --- /dev/null +++ b/labs/rhcsa/10-2-default-file-permissions/setup.sh @@ -0,0 +1,7 @@ +ls -A /etc/profile.d >"$LAB_STATE/profile-d" +user_add alice +user_add bob +for u in alice bob; do + m=$(runuser -l "$u" -c umask 2>/dev/null) + [ "$m" != 0077 ] && [ "$m" != 0027 ] || refuse "$u's login shell already has umask $m; remove the system-wide umask line and start again" +done diff --git a/labs/rhcsa/10-3-ssh-keys/README.md b/labs/rhcsa/10-3-ssh-keys/README.md new file mode 100644 index 0000000..f00b491 --- /dev/null +++ b/labs/rhcsa/10-3-ssh-keys/README.md @@ -0,0 +1,42 @@ +# RHCSA 10.3: SSH key-based authentication + +Key-based login to another system, turning off password logins there, and +what sshd does when `authorized_keys` is too open. + +## Before you start + +The lab creates users `student` and `alice` (password `Lab-Pass-1`), and a +second machine, `server1` (`10.99.0.2`), to log in to. `server1` shares +this VM's files and users, and runs its own sshd, with its own +configuration so this VM's SSH logins aren't touched: + +- configuration: `/etc/ssh/server1/sshd_config`, which reads + `/etc/ssh/server1/sshd_config.d/*.conf` first; +- service: `le-lab-server1-sshd` (restart it after a change, and read its + log with `journalctl -u le-lab-server1-sshd`); +- test the configuration with `sshd -t -f /etc/ssh/server1/sshd_config` + and `sshd -T -f /etc/ssh/server1/sshd_config`. + +`server1` allows password logins to start with. + +## Tasks + +1. Set up key-based login for user `student` to user `alice` on `server1`: + as `student`, `ssh -o BatchMode=yes alice@server1 whoami` prints + `alice` without asking anything. +2. Turn off password logins for SSH on `server1`, and confirm keys still + work: `ssh -o PubkeyAuthentication=no alice@server1` fails with + "Permission denied (publickey...)". +3. Make `alice`'s `authorized_keys` writable by her group (mode `620`), and + see what happens to the key login and in sshd's log; then fix it. + +## Hints + +- As `student` (`su - student`): `ssh-keygen`, then + `ssh-copy-id alice@server1` (alice's password is `Lab-Pass-1`). +- `PasswordAuthentication no` in a file in + `/etc/ssh/server1/sshd_config.d/`; `systemctl restart + le-lab-server1-sshd`; `sshd -T -f /etc/ssh/server1/sshd_config | grep + -i passwordauthentication`. +- `chmod 620 ~alice/.ssh/authorized_keys`; `journalctl -u + le-lab-server1-sshd` shows "bad ownership or modes"; `chmod 600`. diff --git a/labs/rhcsa/10-3-ssh-keys/check.sh b/labs/rhcsa/10-3-ssh-keys/check.sh new file mode 100644 index 0000000..73c805e --- /dev/null +++ b/labs/rhcsa/10-3-ssh-keys/check.sh @@ -0,0 +1,26 @@ +SSHOPT=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR) +T=$(sshd -T -f /etc/ssh/server1/sshd_config 2>/dev/null) +AK=/home/alice/.ssh/authorized_keys +key_login() { [ "$(timeout 15 runuser -u student -- ssh "${SSHOPT[@]}" alice@10.99.0.2 whoami /dev/null)" = alice ]; } +pw_refused() { + local out + out=$(timeout 15 runuser -u student -- ssh "${SSHOPT[@]}" -o PubkeyAuthentication=no alice@10.99.0.2 true &1) + grep -q "Permission denied (publickey" <<<"$out" && ! grep -q password <<<"$out" +} + +task 1 "student logs in to alice@server1 with a key" +need "server1's sshd is running" active le-lab-server1-sshd +need "student has a key pair" 'ls /home/student/.ssh/id_* 2>/dev/null | grep -q "\.pub$"' +need "alice's authorized_keys has student's public key" 'for k in /home/student/.ssh/id_*.pub; do grep -qF "$(cut -d" " -f2 "$k")" $AK 2>/dev/null && exit 0; done; exit 1' +need "as student, ssh -o BatchMode=yes alice@server1 whoami prints alice" key_login + +task 2 "No password logins on server1; keys still work" +need "sshd -T -f /etc/ssh/server1/sshd_config prints passwordauthentication no" 'grep -qx "passwordauthentication no" <<<"$T"' +need "server1's sshd is running" active le-lab-server1-sshd +need "a login without a key is refused: Permission denied (publickey)" pw_refused +need "the key login still works" key_login + +task 3 "authorized_keys too open, seen in the log, then fixed" +need "server1's sshd logged bad ownership or modes for alice's authorized_keys" 'grep -qs "bad ownership or modes for file $AK" "$LAB_STATE/bad-modes" || journalctl -q -u le-lab-server1-sshd --since "$(state_get start)" | grep -q "bad ownership or modes for file $AK"' +need "alice's authorized_keys isn't writable by group or others" '[ -f $AK ] && [ -z "$(find $AK -perm /022)" ]' +need "the key login works again" key_login diff --git a/labs/rhcsa/10-3-ssh-keys/lab.conf b/labs/rhcsa/10-3-ssh-keys/lab.conf new file mode 100644 index 0000000..40dc948 --- /dev/null +++ b/labs/rhcsa/10-3-ssh-keys/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.3 SSH key-based authentication" +CHAPTER=/rhcsa/10-security/10-3-ssh-keys/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-3-ssh-keys/reset.sh b/labs/rhcsa/10-3-ssh-keys/reset.sh new file mode 100644 index 0000000..b4eea55 --- /dev/null +++ b/labs/rhcsa/10-3-ssh-keys/reset.sh @@ -0,0 +1,5 @@ +systemctl disable --now --quiet le-lab-server1-sshd.service le-lab-server1-log.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-server1-sshd.service /etc/systemd/system/le-lab-server1-log.service +systemctl daemon-reload +rm -rf /etc/ssh/server1 +true diff --git a/labs/rhcsa/10-3-ssh-keys/setup.sh b/labs/rhcsa/10-3-ssh-keys/setup.sh new file mode 100644 index 0000000..104e977 --- /dev/null +++ b/labs/rhcsa/10-3-ssh-keys/setup.sh @@ -0,0 +1,60 @@ +user_add student +user_add alice +state_set start "$(date '+%Y-%m-%d %H:%M:%S')" +peer_up +hosts_add "$LE_PEER_ADDR server1.lab.example server1" +mkdir -p /etc/ssh/server1/sshd_config.d +keys='' +for t in ed25519 ecdsa rsa; do + [ -f /etc/ssh/ssh_host_${t}_key ] && keys="${keys}HostKey /etc/ssh/ssh_host_${t}_key +" +done +[ -n "$keys" ] || { ssh-keygen -A >/dev/null; keys='HostKey /etc/ssh/ssh_host_ed25519_key +'; } +cat >/etc/ssh/server1/sshd_config </dev/null && restorecon -R /etc/ssh/server1 +cat >/etc/systemd/system/le-lab-server1-sshd.service <<'X' +[Unit] +Description=Practice lab sshd on server1 +Requires=le-lab-peer.service +After=le-lab-peer.service network.target + +[Service] +NetworkNamespacePath=/run/netns/lepeer +ExecStartPre=/usr/sbin/sshd -t -f /etc/ssh/server1/sshd_config +ExecStart=/usr/sbin/sshd -D -f /etc/ssh/server1/sshd_config +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +X +# The journal may not be kept across reboots: copy sshd's complaints about +# file modes for the check. +cat >/etc/systemd/system/le-lab-server1-log.service <>$LAB_STATE/bad-modes' + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet le-lab-server1-log.service le-lab-server1-sshd.service diff --git a/labs/rhcsa/10-4-selinux-modes/README.md b/labs/rhcsa/10-4-selinux-modes/README.md new file mode 100644 index 0000000..4d920a2 --- /dev/null +++ b/labs/rhcsa/10-4-selinux-modes/README.md @@ -0,0 +1,27 @@ +# RHCSA 10.4: SELinux modes + +Switching to permissive mode for now, and booting in permissive mode and +back. + +## Before you start + +SELinux must be enforcing when you start. The tasks end where they +started, so the lab runs a small service, `le-lab-watch`, that notes the +SELinux mode and the `SELINUX=` setting every second (and at every boot) +for the check. Wait a few seconds between steps. Reset sets SELinux back +to enforcing. + +## Tasks + +1. Put SELinux into permissive mode until the next reboot, and make sure + enforcing mode is the setting at boot: `getenforce` prints + `Permissive` and `sestatus` shows `Mode from config file: enforcing`. + After a reboot, `getenforce` prints `Enforcing`. +2. Make the system boot in permissive mode and reboot: `getenforce` prints + `Permissive`. Then put it back to enforcing for good (`SELINUX=enforcing` + and a reboot, or `setenforce 1` as well). + +## Hints + +- `setenforce 0`, `getenforce`, `sestatus`. +- `SELINUX=` in `/etc/selinux/config`; `setenforce 1`. diff --git a/labs/rhcsa/10-4-selinux-modes/check.sh b/labs/rhcsa/10-4-selinux-modes/check.sh new file mode 100644 index 0000000..188077c --- /dev/null +++ b/labs/rhcsa/10-4-selinux-modes/check.sh @@ -0,0 +1,16 @@ +LOG=$LAB_STATE/watch.log +# The first line of each boot: the mode that boot started in. +boot_modes() { awk '!s[$1]++' "$LOG" 2>/dev/null; } +now_enforcing() { + need "/etc/selinux/config sets SELINUX=enforcing" 'grep -Eq "^[[:space:]]*SELINUX=enforcing[[:space:]]*$" /etc/selinux/config' + need "getenforce prints Enforcing now" '[ "$(getenforce)" = Enforcing ]' + need "the kernel command line doesn't override it" '! grep -Eq "(^| )(enforcing=0|selinux=0)( |$)" /proc/cmdline' +} + +task 1 "Permissive until the next reboot, enforcing at boot" +need "SELinux was permissive while the config said enforcing" 'grep -q " Permissive SELINUX=enforcing$" "$LOG"' +now_enforcing + +task 2 "Booted in permissive mode once, then enforcing again" +need "a boot since the lab started ran in permissive mode from the config" 'boot_modes | grep -q " Permissive SELINUX=permissive$"' +now_enforcing diff --git a/labs/rhcsa/10-4-selinux-modes/lab.conf b/labs/rhcsa/10-4-selinux-modes/lab.conf new file mode 100644 index 0000000..4ee6e1e --- /dev/null +++ b/labs/rhcsa/10-4-selinux-modes/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.4 SELinux modes" +CHAPTER=/rhcsa/10-security/10-4-selinux-modes/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-4-selinux-modes/reset.sh b/labs/rhcsa/10-4-selinux-modes/reset.sh new file mode 100644 index 0000000..aa326d9 --- /dev/null +++ b/labs/rhcsa/10-4-selinux-modes/reset.sh @@ -0,0 +1,7 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +sed -i -E 's/^[[:space:]]*SELINUX=(permissive|disabled)[[:space:]]*$/SELINUX=enforcing/' /etc/selinux/config +setenforce 1 2>/dev/null +true diff --git a/labs/rhcsa/10-4-selinux-modes/setup.sh b/labs/rhcsa/10-4-selinux-modes/setup.sh new file mode 100644 index 0000000..1a1d5dd --- /dev/null +++ b/labs/rhcsa/10-4-selinux-modes/setup.sh @@ -0,0 +1,30 @@ +command -v getenforce >/dev/null || refuse "SELinux tools aren't installed" +[ "$(getenforce)" = Enforcing ] || refuse "SELinux isn't enforcing; set SELINUX=enforcing in /etc/selinux/config, reboot, and start again" +grep -Eq '^[[:space:]]*SELINUX=enforcing' /etc/selinux/config || refuse "/etc/selinux/config doesn't set SELINUX=enforcing; set it and start again" +cat /proc/sys/kernel/random/boot_id >"$LAB_STATE/setup-boot" +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints, with the boot it happened in. +log=$1 prev='' +boot=$(cat /proc/sys/kernel/random/boot_id) +snap() { echo "$(getenforce) $(grep -E '^[[:space:]]*SELINUX=' /etc/selinux/config | tail -n1 | tr -d ' ')"; } +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s %s\n' "$boot" "$cur" >>"$log"; prev=$cur; } + sleep 1 +done +X +cat >/etc/systemd/system/le-lab-watch.service <` / `Require all granted` / + `` in a file in `/etc/httpd/conf.d/`. Before the fix, + `curl http://localhost/` gives a `403` and `ausearch -m AVC -ts recent` + shows an `httpd` denial for `/web`; after it, `curl` shows the page and + `restorecon -Rvn /web` lists nothing to change. +2. Create a file `/tmp/page.html`, `mv` it into `/var/www/html`, and fix + it. Write the type it had right after the move to + `/root/lab-answers/2`. +3. Change the type of `/var/www/html/label.html` with `chcon`, then run + `restorecon` on it. Write the type `restorecon` put back to + `/root/lab-answers/3`. + +## Hints + +- `httpd -t`, `systemctl restart httpd`; `ls -Zd /web`; + `semanage fcontext -a -t httpd_sys_content_t '/web(/.*)?'`, + `restorecon -Rv /web`; `matchpathcon /web`. +- `ls -Z`, `restorecon -v`. +- `chcon -t user_home_t FILE`, `restorecon -v FILE`. diff --git a/labs/rhcsa/10-5-selinux-contexts/check.sh b/labs/rhcsa/10-5-selinux-contexts/check.sh new file mode 100644 index 0000000..a98907e --- /dev/null +++ b/labs/rhcsa/10-5-selinux-contexts/check.sh @@ -0,0 +1,19 @@ +type_of() { stat -c %C "$1" 2>/dev/null | cut -d: -f3; } +rule_for() { matchpathcon -n "$1" 2>/dev/null | cut -d: -f3; } + +task 1 "Web content served from /web, labeled for good" +need "SELinux is enforcing" '[ "$(getenforce)" = Enforcing ]' +need "a file in /etc/httpd/conf.d sets DocumentRoot /web" 'grep -Eqs "^[[:space:]]*DocumentRoot[[:space:]]+\"?/web/?\"?[[:space:]]*$" /etc/httpd/conf.d/*.conf' +need "httpd is running" active httpd +need "a file-context rule labels /web httpd_sys_content_t" '[ "$(rule_for /web)" = httpd_sys_content_t ] && [ "$(rule_for /web/index.html)" = httpd_sys_content_t ]' +need "restorecon -Rvn /web lists nothing to change" '[ -z "$(restorecon -Rvn /web 2>&1)" ]' +need "curl http://localhost/ shows /web/index.html" 'curl -s --max-time 5 http://localhost/ | cmp -s - /web/index.html' + +task 2 "A file moved from /tmp into /var/www/html, fixed" +need "/var/www/html/page.html exists" test -f /var/www/html/page.html +need "/root/lab-answers/2 has the type after the move (user_tmp_t)" '[ "$(answer 2)" = user_tmp_t ]' +need "page.html is labeled httpd_sys_content_t now" '[ "$(type_of /var/www/html/page.html)" = httpd_sys_content_t ]' + +task 3 "chcon undone by restorecon" +need "/root/lab-answers/3 has the type restorecon put back" '[ "$(answer 3)" = httpd_sys_content_t ]' +need "label.html is labeled httpd_sys_content_t again" '[ "$(type_of /var/www/html/label.html)" = httpd_sys_content_t ]' diff --git a/labs/rhcsa/10-5-selinux-contexts/lab.conf b/labs/rhcsa/10-5-selinux-contexts/lab.conf new file mode 100644 index 0000000..b988e4d --- /dev/null +++ b/labs/rhcsa/10-5-selinux-contexts/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.5 SELinux file and process contexts" +CHAPTER=/rhcsa/10-security/10-5-selinux-contexts/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-5-selinux-contexts/reset.sh b/labs/rhcsa/10-5-selinux-contexts/reset.sh new file mode 100644 index 0000000..97d85a8 --- /dev/null +++ b/labs/rhcsa/10-5-selinux-contexts/reset.sh @@ -0,0 +1,13 @@ +for f in /etc/httpd/conf.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/conf-d" 2>/dev/null && continue + grep -q '/web' "$f" && rm -f "$f" +done +if command -v semanage >/dev/null; then + semanage fcontext -l -C 2>/dev/null | awk '$1 ~ /^\/web/ {print $1}' | sort -u | + while read -r spec; do semanage fcontext -d "$spec" 2>/dev/null; done +fi +rm -rf /web /var/www/html/page.html /var/www/html/label.html /tmp/page.html +if [ -n "$(state_get httpd-was-enabled)" ]; then systemctl restart httpd 2>/dev/null +else systemctl disable --now --quiet httpd 2>/dev/null; fi +true diff --git a/labs/rhcsa/10-5-selinux-contexts/setup.sh b/labs/rhcsa/10-5-selinux-contexts/setup.sh new file mode 100644 index 0000000..033030a --- /dev/null +++ b/labs/rhcsa/10-5-selinux-contexts/setup.sh @@ -0,0 +1,14 @@ +command -v getenforce >/dev/null && [ "$(getenforce)" = Enforcing ] || refuse "SELinux isn't enforcing; set SELINUX=enforcing in /etc/selinux/config, reboot, and start again" +[ -e /web ] && refuse "/web already exists; remove it and start again" +pkg_installed httpd && state_set httpd-before 1 +pkg_installed httpd || pkg_install httpd +pkg_installed policycoreutils-python-utils || pkg_install policycoreutils-python-utils +enabled httpd && state_set httpd-was-enabled 1 +ls -A /etc/httpd/conf.d >"$LAB_STATE/conf-d" +mkdir -p /web +echo '

Lab page from /web

' >/web/index.html +chcon -R -t default_t /web +echo '

Label test page

' >/var/www/html/label.html +restorecon /var/www/html/label.html +rm -f /var/www/html/page.html /tmp/page.html +systemctl enable --now --quiet httpd diff --git a/labs/rhcsa/10-6-selinux-ports/README.md b/labs/rhcsa/10-6-selinux-ports/README.md new file mode 100644 index 0000000..e7e68f9 --- /dev/null +++ b/labs/rhcsa/10-6-selinux-ports/README.md @@ -0,0 +1,31 @@ +# RHCSA 10.6: SELinux port labels + +Letting httpd and sshd listen on ports SELinux doesn't expect them on. + +## Before you start + +SELinux must be enforcing; keep it that way. The lab installs and starts +`httpd` on port 80, installs `policycoreutils-python-utils` (for +`semanage`), and adds a second machine, `peer` (`10.99.0.2`), on the lab +network `10.99.0.0/24` (this VM is `10.99.0.1` there), to test from. Get a +shell there with `sudo ip netns exec lepeer bash`. Keep a console or a +second session open while you change sshd. + +## Tasks + +1. Make httpd listen on port 8888 as well as 80, with SELinux enforcing, + reachable from other systems: from the peer, + `curl http://10.99.0.1:8888/` gets an answer. Open the firewall if it's + running. +2. Move sshd to port 2222, keeping port 22 working until you've tested it: + add `Port 22` and `Port 2222` in a file in `/etc/ssh/sshd_config.d/`. + `ssh -p 2222 localhost` works. + +## Hints + +- `Listen 8888` in a file in `/etc/httpd/conf.d/`; `systemctl restart + httpd` fails until `semanage port -a -t http_port_t -p tcp 8888`; + `semanage port -l -C`; `ss -tln`. If `firewall-cmd --state` says + running: `firewall-cmd --permanent --add-port=8888/tcp`, `--reload`. +- `semanage port -a -t ssh_port_t -p tcp 2222`, `sshd -t`, + `systemctl restart sshd`. diff --git a/labs/rhcsa/10-6-selinux-ports/check.sh b/labs/rhcsa/10-6-selinux-ports/check.sh new file mode 100644 index 0000000..2be4817 --- /dev/null +++ b/labs/rhcsa/10-6-selinux-ports/check.sh @@ -0,0 +1,26 @@ +peer_curl() { + local i + for i in 1 2 3 4 5; do + in_peer curl -s --max-time 3 http://10.99.0.1:8888/ | grep -q "Lab httpd" && return 0 + sleep 2 + done + return 1 +} +banner() { timeout 5 bash -c "exec 3<>/dev/tcp/127.0.0.1/$1; head -c 7 <&3" 2>/dev/null | grep -q "^SSH-"; } + +task 1 "httpd on 8888 as well as 80, reachable from the peer" +need "SELinux is enforcing" '[ "$(getenforce)" = Enforcing ]' +need "httpd is running" active httpd +need "port 8888 is labeled http_port_t (semanage port -l -C)" 'semanage port -l -C | grep -Eq "^http_port_t[[:space:]]+tcp[[:space:]].*\b8888\b"' +need "httpd listens on 8888 and 80" 'listening 8888 tcp && listening 80 tcp' +if systemctl is-active --quiet firewalld; then + need "the firewall allows 8888/tcp, now and permanently" 'firewall-cmd --query-port=8888/tcp && firewall-cmd --permanent --query-port=8888/tcp' +fi +need "curl http://10.99.0.1:8888/ from the peer gets an answer" peer_curl + +task 2 "sshd on 2222 as well as 22" +need "port 2222 is labeled ssh_port_t" 'semanage port -l | grep -Eq "^ssh_port_t[[:space:]]+tcp[[:space:]].*\b2222\b"' +need "a file in /etc/ssh/sshd_config.d/ has Port 2222" 'grep -Eqs "^[[:space:]]*Port[[:space:]]+2222[[:space:]]*$" /etc/ssh/sshd_config.d/*.conf' +need "sshd's configuration has ports 22 and 2222" 'sshd -T 2>/dev/null | grep -qx "port 22" && sshd -T 2>/dev/null | grep -qx "port 2222"' +need "sshd answers on port 2222" 'banner 2222' +need "sshd still answers on port 22" 'banner 22' diff --git a/labs/rhcsa/10-6-selinux-ports/lab.conf b/labs/rhcsa/10-6-selinux-ports/lab.conf new file mode 100644 index 0000000..4b17ff7 --- /dev/null +++ b/labs/rhcsa/10-6-selinux-ports/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.6 SELinux port labels" +CHAPTER=/rhcsa/10-security/10-6-selinux-ports/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-6-selinux-ports/reset.sh b/labs/rhcsa/10-6-selinux-ports/reset.sh new file mode 100644 index 0000000..12ae5cd --- /dev/null +++ b/labs/rhcsa/10-6-selinux-ports/reset.sh @@ -0,0 +1,25 @@ +for f in /etc/httpd/conf.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/conf-d" 2>/dev/null && continue + grep -qw 8888 "$f" && rm -f "$f" +done +sed -i -E '/^[[:space:]]*Listen[[:space:]]+8888[[:space:]]*$/d' /etc/httpd/conf/httpd.conf 2>/dev/null +if [ -f "$LAB_STATE/index.html" ]; then cp -a "$LAB_STATE/index.html" /var/www/html/index.html +else rm -f /var/www/html/index.html; fi +if [ -n "$(state_get httpd-was-enabled)" ]; then systemctl restart httpd 2>/dev/null +else systemctl disable --now --quiet httpd 2>/dev/null; fi +if [ -f "$LAB_STATE/ssh.tar" ]; then + rm -f /etc/ssh/sshd_config.d/* + tar -C /etc -xf "$LAB_STATE/ssh.tar" + sshd -t && systemctl restart sshd +fi +if command -v semanage >/dev/null; then + [ -n "$(state_get had-8888)" ] || semanage port -d -t http_port_t -p tcp 8888 2>/dev/null + [ -n "$(state_get had-2222)" ] || semanage port -d -t ssh_port_t -p tcp 2222 2>/dev/null +fi +if [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" + systemctl is-active --quiet firewalld && firewall-cmd -q --reload +fi +true diff --git a/labs/rhcsa/10-6-selinux-ports/setup.sh b/labs/rhcsa/10-6-selinux-ports/setup.sh new file mode 100644 index 0000000..dcc0940 --- /dev/null +++ b/labs/rhcsa/10-6-selinux-ports/setup.sh @@ -0,0 +1,17 @@ +command -v getenforce >/dev/null && [ "$(getenforce)" = Enforcing ] || refuse "SELinux isn't enforcing; set SELINUX=enforcing in /etc/selinux/config, reboot, and start again" +pkg_installed httpd || pkg_install httpd +pkg_installed policycoreutils-python-utils || pkg_install policycoreutils-python-utils +enabled httpd && state_set httpd-was-enabled 1 +semanage port -l -C | grep -Eq '^http_port_t[[:space:]]+tcp[[:space:]].*\b8888\b' && state_set had-8888 1 +semanage port -l -C | grep -Eq '^ssh_port_t[[:space:]]+tcp[[:space:]].*\b2222\b' && state_set had-2222 1 +ls -A /etc/httpd/conf.d >"$LAB_STATE/conf-d" +tar -C /etc -cf "$LAB_STATE/ssh.tar" ssh +if systemctl is-active --quiet firewalld; then + state_set firewalld 1 + tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld +fi +[ -f /var/www/html/index.html ] && cp -a /var/www/html/index.html "$LAB_STATE/index.html" +echo 'Lab httpd' >/var/www/html/index.html +restorecon /var/www/html/index.html +systemctl enable --now --quiet httpd +peer_up diff --git a/labs/rhcsa/10-7-selinux-booleans/README.md b/labs/rhcsa/10-7-selinux-booleans/README.md new file mode 100644 index 0000000..37876b2 --- /dev/null +++ b/labs/rhcsa/10-7-selinux-booleans/README.md @@ -0,0 +1,26 @@ +# RHCSA 10.7: SELinux booleans + +A boolean turned on for good, and one turned on only until the next boot. + +## Before you start + +SELinux must be enforcing. The lab installs `policycoreutils-python-utils` +(for `semanage`) and turns `httpd_can_network_connect` and +`httpd_enable_homedirs` off, persistently. Task 2 ends where it started, so +the lab runs a small service, `le-lab-watch`, that notes +`httpd_enable_homedirs` every second (and at every boot) for the check. +Reset turns both booleans off again. + +## Tasks + +1. Let web servers make outgoing network connections, persistently: after + a reboot, `getsebool httpd_can_network_connect` prints `--> on`. +2. Turn `httpd_enable_homedirs` on without `-P`: `semanage boolean -l` + shows it `(on , off)`. Reboot: `getsebool httpd_enable_homedirs` prints + `--> off`. + +## Hints + +- `getsebool -a | grep httpd`, `semanage boolean -l | grep httpd`; + `setsebool -P`. +- `setsebool` without `-P` changes only the running system. diff --git a/labs/rhcsa/10-7-selinux-booleans/check.sh b/labs/rhcsa/10-7-selinux-booleans/check.sh new file mode 100644 index 0000000..8b0e1e9 --- /dev/null +++ b/labs/rhcsa/10-7-selinux-booleans/check.sh @@ -0,0 +1,13 @@ +LOG=$LAB_STATE/watch.log +# A boolean's (current, default) from semanage, as "on,off". +pair() { semanage boolean -l 2>/dev/null | grep -E "^$1[[:space:]]" | sed -E 's/^[^(]*\(([a-z]+)[[:space:]]*,[[:space:]]*([a-z]+)\).*/\1,\2/'; } + +task 1 "httpd_can_network_connect on, persistently" +need "getsebool httpd_can_network_connect prints --> on" 'getsebool httpd_can_network_connect | grep -q -- "--> on$"' +need "it's on in the saved policy too: semanage boolean -l shows (on , on)" '[ "$(pair httpd_can_network_connect)" = on,on ]' + +task 2 "httpd_enable_homedirs on until a reboot only" +need "httpd_enable_homedirs was on in a boot since the lab started" 'grep -q " on$" "$LOG"' +need "a boot after that started with it off" 'awk "\$2 == \"on\" {onl = NR; onb = \$1} !(\$1 in seen) {seen[\$1] = NR; first[\$1] = \$2} END {for (b in seen) if (b != onb && seen[b] > onl && first[b] == \"off\") exit 0; exit 1}" "$LOG"' +need "getsebool httpd_enable_homedirs prints --> off" 'getsebool httpd_enable_homedirs | grep -q -- "--> off$"' +need "it's off in the saved policy: semanage boolean -l shows (off , off)" '[ "$(pair httpd_enable_homedirs)" = off,off ]' diff --git a/labs/rhcsa/10-7-selinux-booleans/lab.conf b/labs/rhcsa/10-7-selinux-booleans/lab.conf new file mode 100644 index 0000000..ab36c00 --- /dev/null +++ b/labs/rhcsa/10-7-selinux-booleans/lab.conf @@ -0,0 +1,3 @@ +TITLE="10.7 SELinux booleans" +CHAPTER=/rhcsa/10-security/10-7-selinux-booleans/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/10-7-selinux-booleans/reset.sh b/labs/rhcsa/10-7-selinux-booleans/reset.sh new file mode 100644 index 0000000..987ddab --- /dev/null +++ b/labs/rhcsa/10-7-selinux-booleans/reset.sh @@ -0,0 +1,6 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +setsebool -P httpd_can_network_connect=off httpd_enable_homedirs=off 2>/dev/null +true diff --git a/labs/rhcsa/10-7-selinux-booleans/setup.sh b/labs/rhcsa/10-7-selinux-booleans/setup.sh new file mode 100644 index 0000000..2815ef5 --- /dev/null +++ b/labs/rhcsa/10-7-selinux-booleans/setup.sh @@ -0,0 +1,30 @@ +command -v getenforce >/dev/null && [ "$(getenforce)" = Enforcing ] || refuse "SELinux isn't enforcing; set SELINUX=enforcing in /etc/selinux/config, reboot, and start again" +pkg_installed policycoreutils-python-utils || pkg_install policycoreutils-python-utils +setsebool -P httpd_can_network_connect=off httpd_enable_homedirs=off +cat /proc/sys/kernel/random/boot_id >"$LAB_STATE/setup-boot" +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints, with the boot it happened in. +log=$1 prev='' +boot=$(cat /proc/sys/kernel/random/boot_id) +snap() { getsebool httpd_enable_homedirs | awk '{print $3}'; } +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s %s\n' "$boot" "$cur" >>"$log"; prev=$cur; } + sleep 1 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null | awk -v f="${first:-0}" '$1 ~ /^[0-9]+$/ && $1 > f {print $1}'); do + dnf -q history info "$id" 2>/dev/null | grep -Eq '^Command Line *: .*history +(undo|rollback)' || continue + local pkgs + pkgs=$(dnf -q history info "$id" | awk '$1 == "Install" {print $2}') + [ -n "$pkgs" ] || continue + rpm -q $pkgs >/dev/null && return 0 + done + return 1 +} + +task 1 "extras-mirror disabled, its file kept" +need "$REPOS/extras-mirror.repo still exists" '[ -f $REPOS/extras-mirror.repo ]' +need "extras-mirror is disabled" 'dnf -q repolist --disabled | awk "{print \$1}" | grep -qx extras-mirror' +need "dnf loads its repositories again (dnf makecache)" 'timeout 300 dnf -q makecache' + +task 2 "Repository lab, served from the peer, with a package installed from it" +need "$REPOS/lab.repo defines repository lab" 'grep -qx "\[lab\]" $REPOS/lab.repo' +need "lab is enabled" 'dnf -q repolist --enabled | awk "{print \$1}" | grep -qx lab' +need "lab's baseurl points at the peer" 'grep -Eq "^baseurl *= *(http|ftp)s?://(10\.99\.0\.2|peer|peer\.lab\.example)[:/]" $REPOS/lab.repo' +need "lab checks signatures (gpgcheck=1)" 'grep -Eiq "^gpgcheck *= *(1|yes|true)" $REPOS/lab.repo' +need "dnf repoinfo lab finds packages in it" 'n=$(timeout 120 dnf -q --refresh --repo lab repoquery 2>/dev/null | wc -l); [ "$n" -gt 0 ]' +need "one of lab's packages, not installed before, is installed now" 'timeout 120 dnf -q --repo lab repoquery --qf "%{name}-%{version}-%{release}.%{arch}" 2>/dev/null | sort -u | grep -vxFf "$LAB_STATE/rpms" | xargs -r rpm -q 2>/dev/null | grep -qv "not installed"' + +task 3 "semanage installed" +need "semanage --help runs" 'semanage --help' +need "rpm -qf names its package" 'rpm -qf "$(command -v semanage)"' + +task 4 "A removal undone from dnf's history" +need "a dnf history undo (or rollback) put a package back, and it's installed" undo_installed diff --git a/labs/rhcsa/2-1-rpm-repositories-and-packages/lab.conf b/labs/rhcsa/2-1-rpm-repositories-and-packages/lab.conf new file mode 100644 index 0000000..24dbc23 --- /dev/null +++ b/labs/rhcsa/2-1-rpm-repositories-and-packages/lab.conf @@ -0,0 +1,3 @@ +TITLE="2.1 RPM repositories and packages" +CHAPTER=/rhcsa/2-software/2-1-rpm-repositories-and-packages/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/2-1-rpm-repositories-and-packages/reset.sh b/labs/rhcsa/2-1-rpm-repositories-and-packages/reset.sh new file mode 100644 index 0000000..8d1d4e0 --- /dev/null +++ b/labs/rhcsa/2-1-rpm-repositories-and-packages/reset.sh @@ -0,0 +1,9 @@ +rm -f /etc/yum.repos.d/lab.repo /etc/yum.repos.d/extras-mirror.repo +rm -rf /srv/repo +if [ -n "$(state_get had-semanage)" ]; then + pkg_installed policycoreutils-python-utils || dnf -y -q install policycoreutils-python-utils >/dev/null +else + pkg_installed policycoreutils-python-utils && dnf -y -q remove policycoreutils-python-utils >/dev/null +fi +dnf -q clean metadata >/dev/null 2>&1 +true diff --git a/labs/rhcsa/2-1-rpm-repositories-and-packages/setup.sh b/labs/rhcsa/2-1-rpm-repositories-and-packages/setup.sh new file mode 100644 index 0000000..d08e5a5 --- /dev/null +++ b/labs/rhcsa/2-1-rpm-repositories-and-packages/setup.sh @@ -0,0 +1,20 @@ +for f in /etc/yum.repos.d/lab.repo /etc/yum.repos.d/extras-mirror.repo /srv/repo; do + [ -e "$f" ] && refuse "$f already exists; the lab creates or uses it. Remove it and start the lab again." +done +pkg_install createrepo_c dnf-plugins-core +if pkg_installed policycoreutils-python-utils; then + state_set had-semanage 1 + dnf -y -q remove policycoreutils-python-utils >/dev/null +fi +rpm -qa --qf '%{name}-%{version}-%{release}.%{arch}\n' | sort >"$LAB_STATE/rpms" +# Task 4 looks at transactions after this one. +state_set history-id "$(dnf -q history list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ {print $1; exit}')" +peer_up +cat >/etc/yum.repos.d/extras-mirror.repo <<'X' +[extras-mirror] +name=AlmaLinux extras mirror +baseurl=http://mirror.lab.invalid/almalinux/extras/ +enabled=1 +gpgcheck=1 +skip_if_unavailable=False +X diff --git a/labs/rhcsa/3-1-conditions/README.md b/labs/rhcsa/3-1-conditions/README.md new file mode 100644 index 0000000..e240d93 --- /dev/null +++ b/labs/rhcsa/3-1-conditions/README.md @@ -0,0 +1,30 @@ +# RHCSA 3.1: Conditions and tests + +Scripts that test files, the user running them, whether a user exists, +and whether a service is running. + +## Before you start + +Write each script where its task says, and make it executable by +everyone. The checks run your scripts, as root and as an unprivileged +user; the check for task 4 stops `chronyd` first and starts it again +afterwards if your script didn't. + +## Tasks + +1. Write `/usr/local/bin/kind` that prints `file`, `directory` or + `missing` for the path given as its first argument. +2. Write `/usr/local/bin/amiroot` that prints `root` and exits 0 when run + as root, and prints `not root` and exits 1 otherwise. +3. Write `/usr/local/bin/userexists` that takes a user name and prints + whether that user exists: `NAME exists` or `NAME does not exist`. +4. Write `/usr/local/bin/start-chronyd` that starts `chronyd` if it isn't + running and does nothing otherwise (it prints nothing then). + +## Hints + +- `[ -d ]`, `[ -f ]`, `[ -e ]`; `if`, `elif`, `else`, `fi`. +- `id -u` is 0 for root, or `$EUID`; `exit 1`. +- `id NAME >/dev/null 2>&1` or `getent passwd NAME`. +- `systemctl is-active --quiet chronyd || systemctl start chronyd`. +- Start each script with `#!/bin/bash`; `chmod 755`. diff --git a/labs/rhcsa/3-1-conditions/check.sh b/labs/rhcsa/3-1-conditions/check.sh new file mode 100644 index 0000000..fd3abb9 --- /dev/null +++ b/labs/rhcsa/3-1-conditions/check.sh @@ -0,0 +1,43 @@ +B=/usr/local/bin +# out USER CMD...: CMD's output, run as USER. +out() { local u=$1; shift; timeout 20 runuser -u "$u" -- "$@" 2>/dev/null; } +# Task 4: stop chronyd, run the script twice, put chronyd back as it was. +chrony_test() { + local was=0 r=1 + systemctl is-active --quiet chronyd && was=1 + systemctl stop chronyd + timeout 30 $B/start-chronyd >/dev/null 2>&1 + sleep 1 + if systemctl is-active --quiet chronyd; then + [ -z "$(timeout 30 $B/start-chronyd 2>&1)" ] && systemctl is-active --quiet chronyd && r=0 + fi + [ "$was" = 1 ] && systemctl start chronyd + return $r +} +chrony_quiet() { + systemctl start chronyd + [ -z "$(timeout 30 $B/start-chronyd 2>&1)" ] && systemctl is-active --quiet chronyd +} + +task 1 "kind prints file, directory or missing" +need "$B/kind exists and is executable" '[ -x $B/kind ]' +need "kind /etc prints directory" '[ "$(out root $B/kind /etc)" = directory ]' +need "kind /etc/hosts prints file" '[ "$(out root $B/kind /etc/hosts)" = file ]' +need "kind /nope prints missing" '[ "$(out root $B/kind /nope)" = missing ]' + +task 2 "amiroot: root and 0 as root, not root and 1 otherwise" +need "$B/amiroot exists and everyone can run it" '[ -x $B/amiroot ] && [ "$(stat -c %A $B/amiroot | cut -c10)" = x ]' +need "as root it prints root" '[ "$(out root $B/amiroot)" = root ]' +need "as root it exits 0" 'out root $B/amiroot' +need "as another user it prints not root" '[ "$(out nobody $B/amiroot)" = "not root" ]' +need "as another user it exits 1" 'out nobody $B/amiroot; [ $? = 1 ]' + +task 3 "userexists says whether a user exists" +need "$B/userexists exists and is executable" '[ -x $B/userexists ]' +need "userexists root prints root exists" '[ "$(out root $B/userexists root)" = "root exists" ]' +need "userexists nosuchuser prints nosuchuser does not exist" '[ "$(out root $B/userexists nosuchuser)" = "nosuchuser does not exist" ]' + +task 4 "start-chronyd starts chronyd only if it isn't running" +need "$B/start-chronyd exists and is executable" '[ -x $B/start-chronyd ]' +need "with chronyd running, it prints nothing and chronyd stays active" chrony_quiet +need "with chronyd stopped, it starts it; run again, it prints nothing" chrony_test diff --git a/labs/rhcsa/3-1-conditions/lab.conf b/labs/rhcsa/3-1-conditions/lab.conf new file mode 100644 index 0000000..dc1028b --- /dev/null +++ b/labs/rhcsa/3-1-conditions/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.1 Conditions and tests" +CHAPTER=/rhcsa/3-shell-scripts/3-1-conditions/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/3-1-conditions/reset.sh b/labs/rhcsa/3-1-conditions/reset.sh new file mode 100644 index 0000000..150bb68 --- /dev/null +++ b/labs/rhcsa/3-1-conditions/reset.sh @@ -0,0 +1,2 @@ +rm -f /usr/local/bin/kind /usr/local/bin/amiroot /usr/local/bin/userexists /usr/local/bin/start-chronyd +true diff --git a/labs/rhcsa/3-1-conditions/setup.sh b/labs/rhcsa/3-1-conditions/setup.sh new file mode 100644 index 0000000..b4b93a3 --- /dev/null +++ b/labs/rhcsa/3-1-conditions/setup.sh @@ -0,0 +1,5 @@ +pkg_installed chrony || pkg_install chrony +for f in kind amiroot userexists start-chronyd; do + [ -e /usr/local/bin/$f ] && refuse "/usr/local/bin/$f already exists; the lab asks you to write it. Remove it and start the lab again." +done +true diff --git a/labs/rhcsa/3-2-loops/README.md b/labs/rhcsa/3-2-loops/README.md new file mode 100644 index 0000000..095fac8 --- /dev/null +++ b/labs/rhcsa/3-2-loops/README.md @@ -0,0 +1,34 @@ +# RHCSA 3.2: Loops + +Looping over the lines of a file, over files, over fields of +`/etc/passwd`, and over arguments. + +## Before you start + +The lab writes `/root/users.txt`: one user name per line, with some blank +lines, and one name (`dora`) that already exists. Write each script where +its task says and make it executable; the checks run them. + +## Tasks + +1. Write `/usr/local/bin/mkusers` that creates a user for each name in + `/root/users.txt`, skipping users who already exist, and run it. + Running it again must create no duplicates and print no errors other + than "already exists". +2. Write `/usr/local/bin/bigfiles` that prints the name and size in bytes + (`NAME SIZE`, one per line) of every regular file directly in `/etc` + larger than 10000 bytes. Symbolic links don't count. +3. Write `/usr/local/bin/regular-users`: a loop that prints the login name + and home directory (`NAME HOME`) of every user with a UID of 1000 or + more, from `/etc/passwd`. +4. Write `/usr/local/bin/updown` that pings each host name given as an + argument and prints `NAME up` or `NAME down` for each. + +## Hints + +- `while read -r name; do ...; done /dev/null 2>&1`. diff --git a/labs/rhcsa/3-2-loops/check.sh b/labs/rhcsa/3-2-loops/check.sh new file mode 100644 index 0000000..5c89d14 --- /dev/null +++ b/labs/rhcsa/3-2-loops/check.sh @@ -0,0 +1,33 @@ +B=/usr/local/bin +run() { timeout 60 "$@" 2>/dev/null; } +# Normalize NAME SIZE lines: file name without its path, one space. +norm() { awk 'NF >= 2 {n = $1; sub(".*/", "", n); print n, $2}' | sort; } +mkusers_again() { + local before after o + before=$(getent passwd | sort | md5sum) + o=$(timeout 60 $B/mkusers 2>&1) + after=$(getent passwd | sort | md5sum) + [ "$before" = "$after" ] || return 1 + ! grep -v "already exists" <<<"$o" | grep -Eiq "error|fail|invalid|usage|cannot|not found" +} + +task 1 "mkusers creates the users in /root/users.txt" +need "$B/mkusers exists and is executable" '[ -x $B/mkusers ]' +need "users kim, lee and max exist" 'id kim && id lee && id max' +need "each name is in /etc/passwd once" 'for u in kim lee max dora; do [ "$(grep -c "^$u:" /etc/passwd)" = 1 ] || exit 1; done' +need "no user with an empty or odd name was made" '! grep -q "^:" /etc/passwd' +need "run again, it creates nothing and prints no other errors" mkusers_again + +task 2 "bigfiles lists files in /etc over 10000 bytes" +need "$B/bigfiles exists and is executable" '[ -x $B/bigfiles ]' +need "its list matches find /etc -maxdepth 1 -type f -size +10000c" 'diff -q <(run $B/bigfiles | norm) <(find /etc -maxdepth 1 -type f -size +10000c -printf "%f %s\n" | sort)' + +task 3 "regular-users lists users with UID 1000 or more" +need "$B/regular-users exists and is executable" '[ -x $B/regular-users ]' +need "its output matches the users with UID 1000 or more in /etc/passwd, in order" 'diff -q <(run $B/regular-users | tr -s " \t" " ") <(awk -F: "\$3 >= 1000 {print \$1, \$6}" /etc/passwd)' + +task 4 "updown prints up or down for each host" +need "$B/updown exists and is executable" '[ -x $B/updown ]' +need "localhost shows up" 'run $B/updown localhost nosuchhost.invalid 127.0.0.1 | grep -qx "localhost up"' +need "127.0.0.1 shows up" 'run $B/updown localhost nosuchhost.invalid 127.0.0.1 | grep -qx "127.0.0.1 up"' +need "a made-up name shows down" 'run $B/updown localhost nosuchhost.invalid 127.0.0.1 | grep -qx "nosuchhost.invalid down"' diff --git a/labs/rhcsa/3-2-loops/lab.conf b/labs/rhcsa/3-2-loops/lab.conf new file mode 100644 index 0000000..36e3114 --- /dev/null +++ b/labs/rhcsa/3-2-loops/lab.conf @@ -0,0 +1,3 @@ +TITLE="3.2 Loops" +CHAPTER=/rhcsa/3-shell-scripts/3-2-loops/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/3-2-loops/reset.sh b/labs/rhcsa/3-2-loops/reset.sh new file mode 100644 index 0000000..a761c64 --- /dev/null +++ b/labs/rhcsa/3-2-loops/reset.sh @@ -0,0 +1,7 @@ +rm -f /usr/local/bin/mkusers /usr/local/bin/bigfiles /usr/local/bin/regular-users /usr/local/bin/updown +for u in kim lee max; do + pkill -KILL -u "$u" 2>/dev/null + userdel -r "$u" 2>/dev/null +done +rm -f /root/users.txt +true diff --git a/labs/rhcsa/3-2-loops/setup.sh b/labs/rhcsa/3-2-loops/setup.sh new file mode 100644 index 0000000..6500cab --- /dev/null +++ b/labs/rhcsa/3-2-loops/setup.sh @@ -0,0 +1,8 @@ +for f in mkusers bigfiles regular-users updown; do + [ -e /usr/local/bin/$f ] && refuse "/usr/local/bin/$f already exists; the lab asks you to write it. Remove it and start the lab again." +done +for u in kim lee max; do + id "$u" >/dev/null 2>&1 && refuse "user $u already exists; task 1 creates it. Remove it (userdel -r $u) and start the lab again." +done +user_add dora +printf 'kim\n\nlee\ndora\n\n\nmax\n' >/root/users.txt diff --git a/labs/rhcsa/3-3-script-input-and-output/README.md b/labs/rhcsa/3-3-script-input-and-output/README.md new file mode 100644 index 0000000..b188f10 --- /dev/null +++ b/labs/rhcsa/3-3-script-input-and-output/README.md @@ -0,0 +1,33 @@ +# RHCSA 3.3: Script input and command output + +Arguments, usage messages and exit codes, looping over all arguments, +command substitution in file names, and adding up command output. + +## Before you start + +The lab creates user `student`. Write each script where its task says +and make it executable; the checks run them. + +## Tasks + +1. Write `/usr/local/bin/count-files` that takes a directory and prints + how many entries it holds, including hidden ones. With no argument or + a non-directory, it prints `usage: count-files DIRECTORY` to standard + error and exits 1. (Write the name literally: run from `PATH`, `$0` is + `/usr/local/bin/count-files`.) +2. Write `/root/homes.sh` that takes any number of user names and prints + each user's home directory, or `NAME: no such user`. + `/root/homes.sh root nosuchuser student` prints `/root`, the error line + and `/home/student`. +3. Write `/root/df-today.sh` that saves the output of `df -h /` to a file + named after today's date, such as `/root/df-2026-10-20.txt`, and run + it. +4. Write `/root/sumlines.sh` that prints the sum of the line counts of + every file given as an argument (just the number). + +## Hints + +- `$#`, `[ -d "$1" ]`, `echo ... >&2`, `exit 1`; `ls -A "$1" | wc -l`. +- `for u in "$@"`; `getent passwd "$u" | cut -d: -f6`. +- `$(date +%F)`. +- `wc -l <"$f"`, `$((total + n))`. diff --git a/labs/rhcsa/3-3-script-input-and-output/check.sh b/labs/rhcsa/3-3-script-input-and-output/check.sh new file mode 100644 index 0000000..e9bba8c --- /dev/null +++ b/labs/rhcsa/3-3-script-input-and-output/check.sh @@ -0,0 +1,29 @@ +run() { timeout 30 "$@"; } +CF=/usr/local/bin/count-files +usage_ok() { + local err rc + err=$(run "$@" 2>&1 >/dev/null); rc=$? + [ "$rc" = 1 ] && [ "$err" = "usage: count-files DIRECTORY" ] && [ -z "$(run "$@" 2>/dev/null)" ] +} +TODAY=/root/df-$(date +%F).txt + +task 1 "count-files counts entries; usage message and exit 1" +need "$CF exists and is executable" '[ -x $CF ]' +need "count-files /etc/skel prints the same number as ls -A /etc/skel | wc -l" '[ "$(run $CF /etc/skel 2>/dev/null)" = "$(ls -A /etc/skel | wc -l)" ]' +need "it counts hidden entries too (6 in a test directory)" '[ "$(run $CF "$LAB_STATE/count" 2>/dev/null)" = 6 ]' +need "with no argument it prints the usage to standard error and exits 1" 'usage_ok $CF' +need "with a file instead of a directory, the same" 'usage_ok $CF /etc/hosts' + +task 2 "homes.sh prints home directories or NAME: no such user" +need "/root/homes.sh exists and is executable" '[ -x /root/homes.sh ]' +need "homes.sh root nosuchuser student prints /root, the error line and /home/student" '[ "$(run /root/homes.sh root nosuchuser student 2>&1)" = "$(printf "/root\nnosuchuser: no such user\n/home/student")" ]' + +task 3 "df-today.sh saves df -h / to a file named after today's date" +need "/root/df-today.sh exists and is executable" '[ -x /root/df-today.sh ]' +need "$TODAY exists (run the script)" 'run /root/df-today.sh >/dev/null 2>&1; [ -f $TODAY ]' +need "it holds the output of df -h /" '[ "$(head -n1 $TODAY | awk "{print \$1}")" = Filesystem ] && [ "$(tail -n1 $TODAY | awk "{print \$NF}")" = / ]' + +task 4 "sumlines.sh adds up the line counts of its arguments" +need "/root/sumlines.sh exists and is executable" '[ -x /root/sumlines.sh ]' +need "its number matches cat FILES | wc -l" '[ "$(run /root/sumlines.sh /etc/passwd /etc/group /etc/hosts 2>/dev/null | tr -d " \t")" = "$(cat /etc/passwd /etc/group /etc/hosts | wc -l)" ]' +need "it works for one file too" '[ "$(run /root/sumlines.sh /etc/services 2>/dev/null | tr -d " \t")" = "$(wc -l /dev/null || + findmnt --fstab -n -o OPTIONS -S LABEL=le-nodisk | grep -qw nofail' +need "findmnt --verify reports no errors" 'findmnt --verify 2>&1 | grep -Eq "(^| )0 errors|no errors"' +need "the system reached multi-user.target" 'systemctl is-active --quiet multi-user.target' +need "the system isn't in emergency or rescue mode" '! systemctl is-active --quiet emergency.target && ! systemctl is-active --quiet rescue.target' +need "systemctl --failed lists no mount units" '! systemctl --failed --no-legend --plain | grep -q "\.mount"' + +task 3 "A reboot scheduled with a message, then cancelled" +need "no shutdown is scheduled now" '[ ! -e /run/systemd/shutdown/scheduled ]' +need "/var/log/messages shows a scheduled shutdown was cancelled" 'since_start | grep -q "System shutdown has been cancelled"' + +task 4 "Switched to the text target and back on a desktop VM" self diff --git a/labs/rhcsa/4-1-boot-and-targets/lab.conf b/labs/rhcsa/4-1-boot-and-targets/lab.conf new file mode 100644 index 0000000..024102b --- /dev/null +++ b/labs/rhcsa/4-1-boot-and-targets/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.1 Booting, shutting down and targets" +CHAPTER=/rhcsa/4-running-systems/4-1-boot-and-targets/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/4-1-boot-and-targets/reset.sh b/labs/rhcsa/4-1-boot-and-targets/reset.sh new file mode 100644 index 0000000..9baf6e1 --- /dev/null +++ b/labs/rhcsa/4-1-boot-and-targets/reset.sh @@ -0,0 +1,5 @@ +sed -i '/le-nodisk/d' /etc/fstab +systemctl daemon-reload +umount /mnt/nodisk 2>/dev/null +rmdir /mnt/nodisk 2>/dev/null +true diff --git a/labs/rhcsa/4-1-boot-and-targets/setup.sh b/labs/rhcsa/4-1-boot-and-targets/setup.sh new file mode 100644 index 0000000..2ab4f06 --- /dev/null +++ b/labs/rhcsa/4-1-boot-and-targets/setup.sh @@ -0,0 +1,16 @@ +# Emergency mode needs root's password; without one the student is stuck. +case $(passwd -S root 2>/dev/null | awk '{print $2}') in +L* | NP) + refuse "root has no usable password, and this lab's next boot stops in emergency mode, which asks for it. Run: sudo passwd root, then start the lab again." + ;; +esac +[ -f /run/systemd/shutdown/scheduled ] && refuse "a shutdown is scheduled already; cancel it with shutdown -c and start the lab again." +# A marker in /var/log/messages: task 3 looks for a cancel after it. +token=le-lab-$(date +%s)-$RANDOM +state_set token "$token" +state_set since "$(date +%s)" +# Written straight into the file: through logger it can arrive late. +printf '%s %s le-lab: lab started %s\n' "$(date '+%b %e %T')" "$(hostname -s)" "$token" >>/var/log/messages +mkdir -p /mnt/nodisk +printf 'LABEL=le-nodisk /mnt/nodisk xfs defaults 0 0\n' >>/etc/fstab +systemctl daemon-reload diff --git a/labs/rhcsa/4-3-processes-and-priorities/README.md b/labs/rhcsa/4-3-processes-and-priorities/README.md new file mode 100644 index 0000000..1726634 --- /dev/null +++ b/labs/rhcsa/4-3-processes-and-priorities/README.md @@ -0,0 +1,28 @@ +# RHCSA 4.3: Processes and scheduling priority + +Finding and ending busy processes, starting a process with a nice value, +changing the nice value of a running one, and listing the processes using +the most memory. + +## Before you start + +The lab starts two `sha256sum /dev/zero` processes as root, each using a +whole CPU. Nothing in this lab survives a reboot: do it all in one boot. + +## Tasks + +1. Find the process using the most CPU with `top`, and end both of the + lab's `sha256sum` processes. +2. As a normal user, start `sha256sum /dev/zero` with a nice value of 10, + in the background, and leave it running. +3. As root, change that process's nice value to -5. +4. Save the PID, user and memory use of the five processes using the most + memory to `/root/topmem.txt`: a header line and five process lines, + largest first. + +## Hints + +- In `top`, `P` sorts by CPU and `k` kills; or `pgrep`, `kill`, `pkill`. +- `nice -n 10 sha256sum /dev/zero &`; `ps -o pid,ni,user,cmd -C sha256sum`. +- `renice -n -5 -p PID`; only root may lower a nice value. +- `ps -eo pid,user,%mem --sort=-%mem | head -n 6`. diff --git a/labs/rhcsa/4-3-processes-and-priorities/check.sh b/labs/rhcsa/4-3-processes-and-priorities/check.sh new file mode 100644 index 0000000..67f0891 --- /dev/null +++ b/labs/rhcsa/4-3-processes-and-priorities/check.sh @@ -0,0 +1,28 @@ +# PIDs of sha256sum processes run by normal users (UID 1000 or more). +user_pids() { ps -C sha256sum -o pid=,uid= | awk '$2 >= 1000 {print $1}'; } +topmem_ok() { + local f=/root/topmem.txt col + [ "$(grep -c . $f)" = 6 ] || return 1 + head -n1 $f | grep -q PID && head -n1 $f | grep -q USER || return 1 + col=$(head -n1 $f | awk '{for (i = 1; i <= NF; i++) if ($i ~ /MEM|RSS/) {print i; exit}}') + [ -n "$col" ] || return 1 + # Every process line starts with a PID-like number somewhere and is + # sorted by the memory column, largest first. + tail -n +2 $f | awk -v c="$col" 'BEGIN {prev = -1} + {v = $c + 0; if (prev >= 0 && v > prev) exit 1; prev = v}' +} + +task 1 "The lab's two sha256sum processes ended" +need "the lab's sha256sum processes are gone" 'while read -r p; do [ -d /proc/$p ] && [ "$(cat /proc/$p/comm 2>/dev/null)" = sha256sum ] && exit 1; done <"$LAB_STATE/pids"; true' +need "no sha256sum runs as root" '! pgrep -x -u root sha256sum' + +task 2 "A normal user's sha256sum /dev/zero runs" +need "a sha256sum process runs as a normal user" '[ -n "$(user_pids)" ]' +need "it reads /dev/zero" 'for p in $(user_pids); do grep -qa /dev/zero /proc/$p/cmdline && exit 0; done; exit 1' + +task 3 "Its nice value changed to -5" +need "ps -o ni= shows -5 for the user's sha256sum" 'for p in $(user_pids); do [ "$(ps -o ni= -p $p | tr -d " ")" = -5 ] && exit 0; done; exit 1' + +task 4 "The top five memory users in /root/topmem.txt" +need "/root/topmem.txt exists" '[ -s /root/topmem.txt ]' +need "it has a header with PID, USER and a memory column, then five process lines, largest first" topmem_ok diff --git a/labs/rhcsa/4-3-processes-and-priorities/lab.conf b/labs/rhcsa/4-3-processes-and-priorities/lab.conf new file mode 100644 index 0000000..dd650d1 --- /dev/null +++ b/labs/rhcsa/4-3-processes-and-priorities/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.3 Processes and scheduling priority" +CHAPTER=/rhcsa/4-running-systems/4-3-processes-and-priorities/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/4-3-processes-and-priorities/reset.sh b/labs/rhcsa/4-3-processes-and-priorities/reset.sh new file mode 100644 index 0000000..31cb5c2 --- /dev/null +++ b/labs/rhcsa/4-3-processes-and-priorities/reset.sh @@ -0,0 +1,3 @@ +pkill -x sha256sum 2>/dev/null +rm -f /root/topmem.txt +true diff --git a/labs/rhcsa/4-3-processes-and-priorities/setup.sh b/labs/rhcsa/4-3-processes-and-priorities/setup.sh new file mode 100644 index 0000000..4996833 --- /dev/null +++ b/labs/rhcsa/4-3-processes-and-priorities/setup.sh @@ -0,0 +1,8 @@ +pgrep -x sha256sum >/dev/null && refuse "sha256sum is already running; end it (pkill sha256sum) and start the lab again." +rm -f /root/topmem.txt +for i in 1 2; do + setsid -f sha256sum /dev/zero >/dev/null 2>&1 "$LAB_STATE/pids" +[ "$(wc -l <"$LAB_STATE/pids")" = 2 ] || die "couldn't start the lab's sha256sum processes" diff --git a/labs/rhcsa/4-4-tuning-profiles/README.md b/labs/rhcsa/4-4-tuning-profiles/README.md new file mode 100644 index 0000000..ac4ded7 --- /dev/null +++ b/labs/rhcsa/4-4-tuning-profiles/README.md @@ -0,0 +1,25 @@ +# RHCSA 4.4: Tuning profiles + +Choosing the recommended tuning profile, setting one that lasts across +reboots, and what happens when the tuned daemon isn't running. + +## Before you start + +The lab installs tuned if it's missing, starts and enables it, and sets +the profile `balanced` (or `throughput-performance`, if `balanced` is the +one tuned recommends on your VM). Reset puts back the profile you had. + +## Tasks + +1. Set the VM's tuning profile to the one tuned recommends. +2. Set the profile to `powersave` and make sure it's still active after a + reboot. +3. Stop and disable `tuned`, reboot, and see what `tuned-adm active` says; + then put it right: tuned enabled and running again, with the profile + you chose active. + +## Hints + +- `tuned-adm recommend`, `tuned-adm profile NAME`, `tuned-adm active`. +- `tuned-adm list`; the choice is kept in `/etc/tuned/active_profile`. +- `systemctl disable --now tuned`, then `systemctl enable --now tuned`. diff --git a/labs/rhcsa/4-4-tuning-profiles/check.sh b/labs/rhcsa/4-4-tuning-profiles/check.sh new file mode 100644 index 0000000..8a332e3 --- /dev/null +++ b/labs/rhcsa/4-4-tuning-profiles/check.sh @@ -0,0 +1,23 @@ +LOG=/var/log/tuned/tuned.log +# tuned's log since the lab started (all of it if it was rotated). +since_start() { + local o; o=$(state_get log-offset) + [ -f $LOG ] || return 0 + if [ "$(stat -c %s $LOG)" -ge "${o:-0}" ]; then tail -c +$((${o:-0} + 1)) $LOG; else cat $LOG; fi +} +REC=$(tuned-adm recommend 2>/dev/null) + +task 1 "The recommended profile set" +need "tuned recommends a profile" '[ -n "$REC" ]' +need "tuned applied the recommended profile ($REC) since the lab started" 'since_start | grep -qF "static tuning from profile '\''$REC'\'' applied"' + +task 2 "powersave active, and kept across reboots" +need "tuned is running" active tuned +need "tuned starts at boot" enabled tuned +need "tuned-adm active shows powersave" 'tuned-adm active | grep -qx "Current active profile: powersave"' +need "powersave is the saved choice (/etc/tuned/active_profile)" '[ "$(cat /etc/tuned/active_profile)" = powersave ]' + +task 3 "tuned stopped, then enabled and running again" +need "tuned was stopped since the lab started" 'since_start | grep -q "terminating TuneD"' +need "tuned is enabled and running again" 'enabled tuned && active tuned' +need "tuned-adm active shows the profile you chose" 'tuned-adm active | grep -qx "Current active profile: $(cat /etc/tuned/active_profile)"' diff --git a/labs/rhcsa/4-4-tuning-profiles/lab.conf b/labs/rhcsa/4-4-tuning-profiles/lab.conf new file mode 100644 index 0000000..ec15907 --- /dev/null +++ b/labs/rhcsa/4-4-tuning-profiles/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.4 Tuning profiles" +CHAPTER=/rhcsa/4-running-systems/4-4-tuning-profiles/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/4-4-tuning-profiles/reset.sh b/labs/rhcsa/4-4-tuning-profiles/reset.sh new file mode 100644 index 0000000..2efb4cd --- /dev/null +++ b/labs/rhcsa/4-4-tuning-profiles/reset.sh @@ -0,0 +1,9 @@ +systemctl start tuned 2>/dev/null +if [ "$(cat "$LAB_STATE/profile_mode" 2>/dev/null)" = auto ]; then + tuned-adm auto_profile +elif [ -s "$LAB_STATE/active_profile" ]; then + tuned-adm profile $(cat "$LAB_STATE/active_profile") +fi +[ "$(state_get was-enabled)" = enabled ] || systemctl disable --quiet tuned 2>/dev/null +[ "$(state_get was-active)" = active ] || systemctl stop tuned 2>/dev/null +true diff --git a/labs/rhcsa/4-4-tuning-profiles/setup.sh b/labs/rhcsa/4-4-tuning-profiles/setup.sh new file mode 100644 index 0000000..940354a --- /dev/null +++ b/labs/rhcsa/4-4-tuning-profiles/setup.sh @@ -0,0 +1,14 @@ +pkg_installed tuned || pkg_install tuned +state_set was-enabled "$(systemctl is-enabled tuned 2>/dev/null)" +state_set was-active "$(systemctl is-active tuned 2>/dev/null)" +[ -f /etc/tuned/active_profile ] && cp -a /etc/tuned/active_profile "$LAB_STATE/active_profile" +[ -f /etc/tuned/profile_mode ] && cp -a /etc/tuned/profile_mode "$LAB_STATE/profile_mode" +systemctl enable --now --quiet tuned +rec=$(tuned-adm recommend 2>/dev/null) +[ -n "$rec" ] || die "tuned-adm recommend printed nothing" +start=balanced +[ "$rec" = balanced ] && start=throughput-performance +tuned-adm profile "$start" +sleep 2 +# Tasks 1 and 3 look in tuned's log after this point. +state_set log-offset "$(stat -c %s /var/log/tuned/tuned.log 2>/dev/null || echo 0)" diff --git a/labs/rhcsa/4-5-logs-and-journal/README.md b/labs/rhcsa/4-5-logs-and-journal/README.md new file mode 100644 index 0000000..e5e0cb0 --- /dev/null +++ b/labs/rhcsa/4-5-logs-and-journal/README.md @@ -0,0 +1,33 @@ +# RHCSA 4.5: Logs and the journal + +A persistent journal, filtering by unit, priority and time, writing to +the log with `logger`, and finding a failed `su`. + +## Before you start + +The lab keeps the journal in memory only (`Storage=volatile`, in +`/etc/systemd/journald.conf.d/00-le-lab.conf`), so it's lost at every +reboot. rsyslog still writes `/var/log/messages` and `/var/log/secure`. +Do task 1 first: messages sent before the journal is persistent are lost +at the reboot. + +## Tasks + +1. Make the journal persistent, reboot, and show the messages from the + previous boot. +2. Save the sshd messages of priority err and above from the last hour + to `/root/ssh-errors.txt` (it may only say `-- No entries --`). +3. Send a message with priority `warning` using `logger`, and find it in + the journal and in `/var/log/messages`. +4. As a normal user, run `su -` and type a wrong password, then find the + attempt in `/var/log/secure`. + +## Hints + +- `Storage=persistent` in a `journald.conf.d` file that sorts after + `00-le-lab.conf` (or remove the lab's file and create `/var/log/journal`), + then `systemctl restart systemd-journald`. `journalctl --list-boots`, + `journalctl -b -1`. +- `journalctl -u sshd -p err --since "1 hour ago" --no-pager`. +- `logger -p user.warning "..."`; `journalctl -p warning -n 5`. +- `sudo grep "authentication failure" /var/log/secure`. diff --git a/labs/rhcsa/4-5-logs-and-journal/check.sh b/labs/rhcsa/4-5-logs-and-journal/check.sh new file mode 100644 index 0000000..599e493 --- /dev/null +++ b/labs/rhcsa/4-5-logs-and-journal/check.sh @@ -0,0 +1,47 @@ +# The part of a log file written since the lab started. +since_start() { + local t; t=$(state_get token) + if grep -q "$t" "$1"; then + sed -n "/$t/,\$p" "$1" + elif [ "$(stat -c %W "$1")" -gt "$(state_get since)" ]; then + cat "$1" # rotated since the lab started + fi +} +BOOT=$(tr -d - /dev/null | + python3 -I -c ' +import json, sys +for line in sys.stdin: + e = json.loads(line) + unit = e.get("_SYSTEMD_UNIT") or "" + m = e.get("MESSAGE") + if isinstance(m, str) and m and not unit.endswith(".service"): + print(m)' +} +logged_too() { + local m + while read -r m; do + since_start /var/log/messages | grep -qF -- "$m" && return 0 + done < <(warning_msgs) + return 1 +} + +task 1 "A persistent journal, with the previous boot in it" +need "the journal of this boot is stored in /var/log/journal" '[ -d /var/log/journal ] && journalctl -q -D /var/log/journal _BOOT_ID=$BOOT -n1 | grep -q .' +need "you rebooted since the lab started" '[ "$(cat /proc/sys/kernel/random/boot_id)" != "$(state_get boot-id)" ]' +need "journalctl --list-boots lists at least two boots" '[ "$(journalctl -q --list-boots 2>/dev/null | grep -Ec "[0-9a-f]{32}")" -ge 2 ]' +need "journalctl -b -1 shows the previous boot" 'journalctl -q -b -1 -n1 | grep -q .' + +task 2 "sshd errors of the last hour in /root/ssh-errors.txt" +need "/root/ssh-errors.txt exists and isn't empty" '[ -s /root/ssh-errors.txt ]' +need "it holds sshd messages or -- No entries --" 'grep -q "No entries" /root/ssh-errors.txt || ! grep -v "^--" /root/ssh-errors.txt | grep -v sshd | grep -q .' + +task 3 "A warning sent with logger, in the journal and /var/log/messages" +need "the journal has a warning-level message sent with logger" '[ -n "$(warning_msgs)" ]' +need "/var/log/messages has it too" logged_too + +task 4 "A failed su - by a normal user in /var/log/secure" +need "/var/log/secure shows a su authentication failure for root by a normal user" 'since_start /var/log/secure | grep -E "su(\[[0-9]+\])?: pam_unix\(su(-l)?:auth\): authentication failure" | grep "user=root" | grep -Eq " uid=[1-9][0-9]{3,} "' diff --git a/labs/rhcsa/4-5-logs-and-journal/lab.conf b/labs/rhcsa/4-5-logs-and-journal/lab.conf new file mode 100644 index 0000000..851f900 --- /dev/null +++ b/labs/rhcsa/4-5-logs-and-journal/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.5 Logs and the journal" +CHAPTER=/rhcsa/4-running-systems/4-5-logs-and-journal/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/4-5-logs-and-journal/reset.sh b/labs/rhcsa/4-5-logs-and-journal/reset.sh new file mode 100644 index 0000000..65c4cb9 --- /dev/null +++ b/labs/rhcsa/4-5-logs-and-journal/reset.sh @@ -0,0 +1,15 @@ +rm -f /root/ssh-errors.txt /etc/systemd/journald.conf.d/00-le-lab.conf +for f in /etc/systemd/journald.conf.d/*; do + [ -e "$f" ] || continue + grep -qxF "${f##*/}" "$LAB_STATE/journald-d" 2>/dev/null || rm -f "$f" +done +rmdir /etc/systemd/journald.conf.d 2>/dev/null +if [ -f "$LAB_STATE/journald.conf" ]; then + cp -a "$LAB_STATE/journald.conf" /etc/systemd/journald.conf +else + rm -f /etc/systemd/journald.conf +fi +[ -n "$(state_get had-journal-dir)" ] || rm -rf /var/log/journal +systemctl restart systemd-journald +systemctl restart rsyslog +true diff --git a/labs/rhcsa/4-5-logs-and-journal/setup.sh b/labs/rhcsa/4-5-logs-and-journal/setup.sh new file mode 100644 index 0000000..fa44a95 --- /dev/null +++ b/labs/rhcsa/4-5-logs-and-journal/setup.sh @@ -0,0 +1,23 @@ +for f in /var/log/messages /var/log/secure; do + [ -f "$f" ] || refuse "$f doesn't exist; is rsyslog installed and running?" +done +[ -d /var/log/journal ] && state_set had-journal-dir 1 +ls /etc/systemd/journald.conf.d >"$LAB_STATE/journald-d" 2>/dev/null || true +[ -f /etc/systemd/journald.conf ] && cp -a /etc/systemd/journald.conf "$LAB_STATE/journald.conf" +rm -f /root/ssh-errors.txt +# Start from a journal kept in memory only. +mkdir -p /etc/systemd/journald.conf.d +printf '# Practice lab: keep the journal in memory only.\n[Journal]\nStorage=volatile\n' >/etc/systemd/journald.conf.d/00-le-lab.conf +systemctl restart systemd-journald +# rsyslog reads the journal; restart it so it follows the new one. +systemctl restart rsyslog +sleep 3 +state_set boot-id "$(cat /proc/sys/kernel/random/boot_id)" +state_set since "$(date +%s)" +# Markers: tasks 3 and 4 look for messages after them. +token=le-lab-$(date +%s)-$RANDOM +state_set token "$token" +# Written straight into the files, so they can't be lost on the way. +for f in /var/log/messages /var/log/secure; do + printf '%s %s le-lab: lab started %s\n' "$(date '+%b %e %T')" "$(hostname -s)" "$token" >>"$f" +done diff --git a/labs/rhcsa/4-6-network-services/README.md b/labs/rhcsa/4-6-network-services/README.md new file mode 100644 index 0000000..5c976d1 --- /dev/null +++ b/labs/rhcsa/4-6-network-services/README.md @@ -0,0 +1,30 @@ +# RHCSA 4.6: Starting and stopping network services + +Installing and starting a web server, stopping it, adding a port with a +reload, and finding out why a service won't start. + +## Before you start + +httpd must not be installed yet: the lab refuses to start if it is. The +checks look at the system log (`/var/log/messages`) for the stop in task +2 and the failure in task 4, so do those for real. + +## Tasks + +1. Install `httpd`, start it now and at every boot, and confirm it's + listening on port 80. +2. Stop `httpd` and confirm nothing listens on port 80 any more. Then + start it again. +3. Add `Listen 8080` to a file in `/etc/httpd/conf.d/` and apply it with + `systemctl reload httpd`, so httpd listens on both 80 and 8080. +4. Break the httpd configuration on purpose (for example a line `Listen` + with no port), restart it, and find out why it failed; then fix it so + httpd is active again. + +## Hints + +- `dnf install httpd`, `systemctl enable --now httpd`, `sudo ss -tlnp`. +- `systemctl stop httpd`, `ss -tln | grep ':80 '`. +- `echo 'Listen 8080' >/etc/httpd/conf.d/...conf`. SELinux allows 8080 for + httpd by default. +- `systemctl status httpd`, `journalctl -u httpd`, `httpd -t`. diff --git a/labs/rhcsa/4-6-network-services/check.sh b/labs/rhcsa/4-6-network-services/check.sh new file mode 100644 index 0000000..a844a1d --- /dev/null +++ b/labs/rhcsa/4-6-network-services/check.sh @@ -0,0 +1,41 @@ +since_start() { + local t; t=$(state_get token) + if grep -q "$t" /var/log/messages; then + sed -n "/$t/,\$p" /var/log/messages + elif [ "$(stat -c %W /var/log/messages)" -gt "$(state_get since)" ]; then + cat /var/log/messages # rotated since the lab started + fi +} +# listening_soon PORT: something listens on PORT within 15 seconds. +listening_soon() { local i; for i in $(seq 15); do listening "$1" tcp && return 0; sleep 1; done; return 1; } +# A stop of httpd on its own: "Stopped httpd" not followed within a second +# by a start (a restart) and not part of a shutdown (a boot comes before +# the next start). +stopped_alone() { + since_start | awk ' + function t( s) { s = $3; split(s, a, ":"); return a[1] * 3600 + a[2] * 60 + a[3] } + /systemd\[1\]: Stopped httpd\.service/ { stop = t(); seen = 1; next } + /kernel: Linux version|kernel: Command line:/ { seen = 0; next } + /systemd\[1\]: Start(ing|ed) httpd\.service/ && seen { d = t() - stop; if (d < 0) d += 86400; if (d >= 2) ok = 1; seen = 0 } + END { exit !ok }' +} + +task 1 "httpd installed, running, enabled, on port 80" +need "httpd is installed" 'rpm -q httpd' +need "httpd is active" active httpd +need "httpd starts at boot" enabled httpd +need "httpd listens on port 80" 'listening_soon 80 && ss -Htlnp "sport = :80" | grep -q httpd' + +task 2 "httpd stopped (and started again)" +need "httpd is installed" 'rpm -q httpd' +need "the log shows httpd was stopped (not just restarted) since the lab started" stopped_alone + +task 3 "Listen 8080 in conf.d; httpd on 80 and 8080" +need "a file in /etc/httpd/conf.d/ has Listen 8080" 'conf_has /etc/httpd/conf.d "^[[:space:]]*Listen[[:space:]]+([0-9.]+:|\[[0-9a-f:]+\]:)?8080([[:space:]]|$)"' +need "httpd listens on port 8080" 'listening_soon 8080 && ss -Htlnp "sport = :8080" | grep -q httpd' +need "httpd still listens on port 80" 'listening 80 tcp' + +task 4 "httpd failed on a broken configuration, then fixed" +need "the log shows httpd failed to start since the lab started" 'since_start | grep -Eq "httpd\.service: (Failed with result|Main process exited, code=exited, status=1)"' +need "httpd -t finds no errors" 'httpd -t' +need "httpd is active again" active httpd diff --git a/labs/rhcsa/4-6-network-services/lab.conf b/labs/rhcsa/4-6-network-services/lab.conf new file mode 100644 index 0000000..a0a09d4 --- /dev/null +++ b/labs/rhcsa/4-6-network-services/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.6 Starting and stopping network services" +CHAPTER=/rhcsa/4-running-systems/4-6-network-services/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/4-6-network-services/reset.sh b/labs/rhcsa/4-6-network-services/reset.sh new file mode 100644 index 0000000..db4159d --- /dev/null +++ b/labs/rhcsa/4-6-network-services/reset.sh @@ -0,0 +1,6 @@ +if pkg_installed httpd; then + systemctl disable --now --quiet httpd 2>/dev/null + dnf -y -q remove httpd >/dev/null +fi +[ -n "$(state_get had-etc-httpd)" ] || rm -rf /etc/httpd +true diff --git a/labs/rhcsa/4-6-network-services/setup.sh b/labs/rhcsa/4-6-network-services/setup.sh new file mode 100644 index 0000000..1725853 --- /dev/null +++ b/labs/rhcsa/4-6-network-services/setup.sh @@ -0,0 +1,10 @@ +pkg_installed httpd && refuse "httpd is already installed; task 1 installs it. Remove it (dnf remove httpd) and start the lab again." +listening 80 tcp && refuse "something already listens on port 80; stop it and start the lab again." +listening 8080 tcp && refuse "something already listens on port 8080; stop it and start the lab again." +[ -d /etc/httpd ] && state_set had-etc-httpd 1 +# A marker in /var/log/messages: tasks 2 and 4 look for messages after it. +token=le-lab-$(date +%s)-$RANDOM +state_set token "$token" +state_set since "$(date +%s)" +# Written straight into the file: through logger it can arrive late. +printf '%s %s le-lab: lab started %s\n' "$(date '+%b %e %T')" "$(hostname -s)" "$token" >>/var/log/messages diff --git a/labs/rhcsa/4-7-secure-file-transfer/README.md b/labs/rhcsa/4-7-secure-file-transfer/README.md new file mode 100644 index 0000000..146a9b5 --- /dev/null +++ b/labs/rhcsa/4-7-secure-file-transfer/README.md @@ -0,0 +1,35 @@ +# RHCSA 4.7: Secure file transfer + +Copying files to and from another machine with scp, rsync and sftp, +keeping permissions and times. + +## Before you start + +The peer, at `10.99.0.2` (name `peer`), stands in for the other VM. It +runs its own sshd, and shares this VM's files and users, so your account +there needs to be a different user to keep the copies apart: you are +`alice` here, and `bob` on the peer. + +The lab creates users `alice` and `bob` (password `Lab-Pass-1`). alice +has an SSH key that logs in as `bob@peer`, a directory `~/proj` with a +few files (one of them mode 640, all dated January 2026), and a file +`~/report.txt`. Do the tasks as alice: `sudo su - alice`. + +## Tasks + +1. Copy the directory `~/proj` to bob's home directory on the peer + (`bob@peer:`), keeping permissions and modification times. +2. Fetch `/etc/os-release` from the peer into a local directory `~/got`. +3. Copy `~/proj/` to `backup/` in bob's home on the peer with rsync, + change one file in `~/proj`, and copy again. The second run lists only + the changed file. +4. Use sftp to upload `~/report.txt` to `/tmp` on the peer, and download + it again as `~/report-back.txt`. + +## Hints + +- `scp -rp proj bob@peer:`; `ssh bob@peer ls -l proj`. +- `scp bob@peer:/etc/os-release ~/got/`; the directory must exist. +- `rsync -av proj/ bob@peer:backup/`; the trailing slash copies the + contents. +- `sftp bob@peer`, then `put`, `get REMOTE LOCAL`, `bye`. diff --git a/labs/rhcsa/4-7-secure-file-transfer/check.sh b/labs/rhcsa/4-7-secure-file-transfer/check.sh new file mode 100644 index 0000000..cc68ffb --- /dev/null +++ b/labs/rhcsa/4-7-secure-file-transfer/check.sh @@ -0,0 +1,38 @@ +A=/home/alice B=/home/bob +# Task 1: every file of the project in bob's copy, with its mode, and its +# date as it was or as alice's copy has it now. +proj_copied() { + local f mode mt sum + [ -s "$LAB_STATE/files" ] || return 1 + while read -r f mode mt sum; do + [ -f "$B/proj/$f" ] || return 1 + [ "$(stat -c %a "$B/proj/$f")" = "$mode" ] || return 1 + t=$(stat -c %Y "$B/proj/$f") + [ "$t" = "$mt" ] || [ "$t" = "$(stat -c %Y "$A/proj/$f" 2>/dev/null)" ] || return 1 + done <"$LAB_STATE/files" +} +proj_changed() { + local f mode mt sum + while read -r f mode mt sum; do + [ "$(sha256sum <"$A/proj/$f" 2>/dev/null | cut -d" " -f1)" != "$sum" ] && return 0 + done <"$LAB_STATE/files" + return 1 +} + +task 1 "~/proj copied to bob@peer:proj with modes and times" +need "$B/proj exists and is bob's" '[ -d $B/proj ] && [ "$(stat -c %U $B/proj)" = bob ]' +need "each file has its mode and modification time" proj_copied + +task 2 "/etc/os-release fetched into ~/got" +need "$A/got/os-release exists" '[ -f $A/got/os-release ]' +need "it's identical to the peer's /etc/os-release" 'cmp -s $A/got/os-release /etc/os-release' + +task 3 "~/proj/ synced to bob@peer:backup/ with rsync, after a change" +need "$B/backup holds the files of proj, not a proj directory" '[ -f $B/backup/notes.txt ] && [ ! -e $B/backup/proj ]' +need "a file in ~/proj was changed" proj_changed +need "$B/backup matches ~/proj, with the change" 'diff -r $A/proj $B/backup' + +task 4 "report.txt uploaded to /tmp with sftp and downloaded as report-back.txt" +need "/tmp/report.txt was uploaded as bob" '[ -f /tmp/report.txt ] && [ "$(stat -c %U /tmp/report.txt)" = bob ]' +need "/tmp/report.txt is identical to ~/report.txt" 'cmp -s /tmp/report.txt $A/report.txt' +need "$A/report-back.txt is identical too" 'cmp -s $A/report-back.txt $A/report.txt' diff --git a/labs/rhcsa/4-7-secure-file-transfer/lab.conf b/labs/rhcsa/4-7-secure-file-transfer/lab.conf new file mode 100644 index 0000000..492caff --- /dev/null +++ b/labs/rhcsa/4-7-secure-file-transfer/lab.conf @@ -0,0 +1,3 @@ +TITLE="4.7 Secure file transfer" +CHAPTER=/rhcsa/4-running-systems/4-7-secure-file-transfer/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/4-7-secure-file-transfer/reset.sh b/labs/rhcsa/4-7-secure-file-transfer/reset.sh new file mode 100644 index 0000000..aa12046 --- /dev/null +++ b/labs/rhcsa/4-7-secure-file-transfer/reset.sh @@ -0,0 +1,5 @@ +systemctl disable --now --quiet le-lab-peer-sshd.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-peer-sshd.service +systemctl daemon-reload +rm -f /tmp/report.txt +true diff --git a/labs/rhcsa/4-7-secure-file-transfer/setup.sh b/labs/rhcsa/4-7-secure-file-transfer/setup.sh new file mode 100644 index 0000000..b242628 --- /dev/null +++ b/labs/rhcsa/4-7-secure-file-transfer/setup.sh @@ -0,0 +1,46 @@ +for u in alice bob; do + id "$u" >/dev/null 2>&1 && refuse "user $u already exists; the lab creates it. Remove it (userdel -r $u) and start the lab again." +done +[ -e /tmp/report.txt ] && refuse "/tmp/report.txt already exists; task 4 uploads it. Remove it and start the lab again." +pkg_installed rsync || pkg_install rsync +user_add alice +user_add bob +# alice's key logs in as bob. +install -d -m 700 -o alice -g alice /home/alice/.ssh /home/bob/.ssh +chown bob: /home/bob/.ssh +runuser -u alice -- ssh-keygen -q -t ed25519 -N '' -C alice@lab -f /home/alice/.ssh/id_ed25519 +install -m 600 -o bob -g bob /home/alice/.ssh/id_ed25519.pub /home/bob/.ssh/authorized_keys +restorecon -R /home/alice/.ssh /home/bob/.ssh +# alice's project, dated January 2026. +P=/home/alice/proj +mkdir -p $P/docs +echo "Project notes" >$P/notes.txt +echo "db_password = not-a-real-one" >$P/secret.conf +echo "Design" >$P/docs/design.txt +chmod 640 $P/secret.conf +chmod 600 $P/docs/design.txt +chown -R alice: $P +touch -h -d '2026-01-15 10:00' $P/notes.txt $P/secret.conf $P/docs/design.txt $P/docs $P +echo "Quarterly report" >/home/alice/report.txt +chown alice: /home/alice/report.txt +(cd $P && find . -type f -printf '%P %m %T@ ' -exec sha256sum {} \; | awk '{print $1, $2, int($3), $4}') >"$LAB_STATE/files" +# The peer's own sshd. +peer_up +cat >/etc/systemd/system/le-lab-peer-sshd.service <<'X' +[Unit] +Description=Practice lab sshd on the peer +Requires=le-lab-peer.service +After=le-lab-peer.service network.target + +[Service] +NetworkNamespacePath=/run/netns/lepeer +ExecStartPre=/bin/mkdir -p /run/sshd +ExecStart=/usr/sbin/sshd -D -o ListenAddress=10.99.0.2:22 -o PidFile=none +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --now --quiet le-lab-peer-sshd.service diff --git a/labs/rhcsa/5-1-gpt-partitions/README.md b/labs/rhcsa/5-1-gpt-partitions/README.md new file mode 100644 index 0000000..308619f --- /dev/null +++ b/labs/rhcsa/5-1-gpt-partitions/README.md @@ -0,0 +1,31 @@ +# RHCSA 5.1: GPT partitions + +A GPT partition table, partitions of a given size and type, and deleting +a partition. + +## Before you start + +The lab adds two 1 GiB spare disks. They're image files under +`/var/lib/le-lab/disks/`, attached again at every boot. + +- `/dev/loop40` is empty: it stands in for the guide's `/dev/vdb`, so its + partitions are `/dev/loop40p1` and `/dev/loop40p2`. +- `/dev/loop41` already has a GPT table with a 500 MiB partition and a + 300 MiB Linux LVM partition, as tasks 1 and 2 leave `/dev/loop40`. + Task 3 works on it, so the first two tasks' results can stay. + +## Tasks + +1. Give `/dev/loop40` a GPT partition table and create a 500 MiB + partition on it. +2. With fdisk, add a second partition of 300 MiB of type Linux LVM beside + the first one. +3. Delete the second partition of `/dev/loop41` again, leaving its first + partition as it is. + +## Hints + +- `parted /dev/loop40 mklabel gpt`, `mkpart`, `print`; or `gdisk`. +- In `fdisk`: `n`, size `+300M`, `t`, type `lvm` (or its number from + `L`), `w`. `fdisk -l /dev/loop40`, `lsblk /dev/loop40`. +- `fdisk`: `d`, `2`, `w`; or `parted /dev/loop41 rm 2`. diff --git a/labs/rhcsa/5-1-gpt-partitions/check.sh b/labs/rhcsa/5-1-gpt-partitions/check.sh new file mode 100644 index 0000000..50099b3 --- /dev/null +++ b/labs/rhcsa/5-1-gpt-partitions/check.sh @@ -0,0 +1,23 @@ +LVM_GUID=E6D6D379-F507-44C2-A23C-238F2A3DF928 +# part DISK N FIELD: a field (start, size, type, uuid) of partition N from +# sfdisk --dump, read from the disk itself. +part() { sfdisk --dump "$1" 2>/dev/null | grep "^$1p$2 " | grep -oE "(^|[ ,])$3= *[^,]+" | sed -E "s/^[ ,]*$3= *//" | tr a-z A-Z; } +nparts() { sfdisk --dump "$1" 2>/dev/null | grep -c "^$1p[0-9]"; } +# Size in MiB of partition N (512-byte sectors). +mib() { local s; s=$(part "$1" "$2" size); [ -n "$s" ] && echo $((s / 2048)); } +about() { [ -n "$1" ] && [ "$1" -ge $(($2 - 1)) ] && [ "$1" -le $(($2 + 1)) ]; } + +task 1 "/dev/loop40: GPT with a 500 MiB partition" +need "/dev/loop40 has a GPT partition table" 'sfdisk --dump /dev/loop40 | grep -qx "label: gpt"' +need "/dev/loop40p1 is 500 MiB" 'about "$(mib /dev/loop40 1)" 500' + +task 2 "/dev/loop40p2: 300 MiB, Linux LVM" +need "/dev/loop40p1 is still 500 MiB" 'about "$(mib /dev/loop40 1)" 500' +need "/dev/loop40p2 is 300 MiB" 'about "$(mib /dev/loop40 2)" 300' +need "/dev/loop40p2 has the type Linux LVM" '[ "$(part /dev/loop40 2 type)" = $LVM_GUID ]' +need "the kernel knows the partitions (lsblk shows loop40p2)" '[ -b /dev/loop40p2 ]' + +task 3 "/dev/loop41: second partition deleted, first one unchanged" +need "/dev/loop41 has only one partition" '[ "$(nparts /dev/loop41)" = 1 ]' +need "/dev/loop41p1 is unchanged" '[ "$(sfdisk --dump /dev/loop41 | grep "^/dev/loop41p1 ")" = "$(cat "$LAB_STATE/loop41p1")" ]' +need "the kernel knows (lsblk shows no loop41p2)" '[ ! -e /dev/loop41p2 ]' diff --git a/labs/rhcsa/5-1-gpt-partitions/lab.conf b/labs/rhcsa/5-1-gpt-partitions/lab.conf new file mode 100644 index 0000000..9ee7242 --- /dev/null +++ b/labs/rhcsa/5-1-gpt-partitions/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.1 GPT partitions" +CHAPTER=/rhcsa/5-local-storage/5-1-gpt-partitions/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/5-1-gpt-partitions/reset.sh b/labs/rhcsa/5-1-gpt-partitions/reset.sh new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/labs/rhcsa/5-1-gpt-partitions/reset.sh @@ -0,0 +1 @@ +true diff --git a/labs/rhcsa/5-1-gpt-partitions/setup.sh b/labs/rhcsa/5-1-gpt-partitions/setup.sh new file mode 100644 index 0000000..186117c --- /dev/null +++ b/labs/rhcsa/5-1-gpt-partitions/setup.sh @@ -0,0 +1,7 @@ +command -v sfdisk >/dev/null || pkg_install util-linux +pkg_installed parted || pkg_install parted +disk_add 0 1G >/dev/null +d1=$(disk_add 1 1G) +parted -s "$d1" mklabel gpt mkpart data 1MiB 501MiB mkpart lvm 501MiB 801MiB set 2 lvm on +udevadm settle +sfdisk --dump "$d1" | grep '^/dev/loop41p1 ' >"$LAB_STATE/loop41p1" diff --git a/labs/rhcsa/5-2-lvm/README.md b/labs/rhcsa/5-2-lvm/README.md new file mode 100644 index 0000000..87666ac --- /dev/null +++ b/labs/rhcsa/5-2-lvm/README.md @@ -0,0 +1,33 @@ +# RHCSA 5.2: LVM physical volumes, volume groups and logical volumes + +A volume group with a chosen extent size, a logical volume sized in +extents, growing a volume group, and taking LVM apart cleanly. + +## Before you start + +The lab adds two 1 GiB spare disks. They're image files under +`/var/lib/le-lab/disks/`, attached again at every boot. + +- `/dev/loop40` has two empty partitions: `/dev/loop40p1` (600 MiB) and + `/dev/loop40p2` (300 MiB). Tasks 1 and 2 use them. +- `/dev/loop41` has two partitions that are physical volumes of a volume + group `vgold`, with logical volumes `lvone` and `lvtwo`, as tasks 1 and + 2 leave `vgdata`. Task 3 takes it apart, so the first two tasks' results + can stay. + +## Tasks + +1. Create a volume group `vgdata` with 8 MiB extents on `/dev/loop40p1`, + and a logical volume `lvdata` of exactly 50 extents. +2. Add `/dev/loop40p2` to `vgdata`, and create a logical volume `lvrest` + that uses all the space left. +3. Remove `lvone`, `lvtwo`, the volume group `vgold` and both its + physical volumes (`/dev/loop41p1` and `/dev/loop41p2`) cleanly. + +## Hints + +- `pvcreate`, `vgcreate -s 8M`, `lvcreate -l 50 -n`; `vgdisplay`, + `lvdisplay` (`Current LE`, `LV Size`). +- `vgextend`, `lvcreate -l 100%FREE`; `vgs vgdata` shows `#PV` and + `VFree`. +- `lvremove`, `vgremove`, `pvremove`; `pvs`, `vgs`, `lvs`, `lsblk -f`. diff --git a/labs/rhcsa/5-2-lvm/check.sh b/labs/rhcsa/5-2-lvm/check.sh new file mode 100644 index 0000000..bbca31a --- /dev/null +++ b/labs/rhcsa/5-2-lvm/check.sh @@ -0,0 +1,22 @@ +vgf() { vgs --noheadings --units m --nosuffix -o "$2" "$1" 2>/dev/null | tr -d ' '; } +lvf() { lvs --noheadings --units m --nosuffix -o "$2" "$1" 2>/dev/null | tr -d ' '; } +pv_vg() { pvs --noheadings -o vg_name "$1" 2>/dev/null | tr -d ' '; } + +task 1 "vgdata with 8 MiB extents; lvdata of 50 extents" +need "volume group vgdata exists" vgs vgdata +need "/dev/loop40p1 is a physical volume in vgdata" '[ "$(pv_vg /dev/loop40p1)" = vgdata ]' +need "vgdata's extents are 8 MiB" '[ "$(vgf vgdata vg_extent_size)" = 8.00 ]' +need "logical volume vgdata/lvdata exists" lvs vgdata/lvdata +need "lvdata is 50 extents (400 MiB)" '[ "$(lvf vgdata/lvdata lv_size)" = 400.00 ]' + +task 2 "/dev/loop40p2 in vgdata; lvrest uses all the space left" +need "/dev/loop40p2 is a physical volume in vgdata" '[ "$(pv_vg /dev/loop40p2)" = vgdata ]' +need "vgdata has 2 physical volumes" '[ "$(vgf vgdata pv_count)" = 2 ]' +need "logical volume vgdata/lvrest exists" lvs vgdata/lvrest +need "vgdata has no free space left" '[ "$(vgf vgdata vg_free_count)" = 0 ]' + +task 3 "vgold, its logical volumes and physical volumes removed" +need "lvone and lvtwo are gone" '! lvs vgold/lvone && ! lvs vgold/lvtwo' +need "volume group vgold is gone" '! vgs vgold' +need "/dev/loop41p1 and /dev/loop41p2 are no longer physical volumes" '! pvs /dev/loop41p1 && ! pvs /dev/loop41p2' +need "lsblk -f shows no LVM2_member on them" '[ -b /dev/loop41p1 ] && [ -b /dev/loop41p2 ] && ! blkid -p /dev/loop41p1 /dev/loop41p2 | grep -q LVM2_member' diff --git a/labs/rhcsa/5-2-lvm/lab.conf b/labs/rhcsa/5-2-lvm/lab.conf new file mode 100644 index 0000000..3ed13cc --- /dev/null +++ b/labs/rhcsa/5-2-lvm/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.2 LVM: physical volumes, volume groups and logical volumes" +CHAPTER=/rhcsa/5-local-storage/5-2-lvm/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/5-2-lvm/reset.sh b/labs/rhcsa/5-2-lvm/reset.sh new file mode 100644 index 0000000..106b6b9 --- /dev/null +++ b/labs/rhcsa/5-2-lvm/reset.sh @@ -0,0 +1,5 @@ +# The volume groups go with the spare disks; deactivate them first. +for vg in vgdata vgold; do + vgchange -an "$vg" >/dev/null 2>&1 +done +true diff --git a/labs/rhcsa/5-2-lvm/setup.sh b/labs/rhcsa/5-2-lvm/setup.sh new file mode 100644 index 0000000..e8a172e --- /dev/null +++ b/labs/rhcsa/5-2-lvm/setup.sh @@ -0,0 +1,18 @@ +command -v vgcreate >/dev/null || pkg_install lvm2 +pkg_installed parted || pkg_install parted +for vg in vgdata vgold; do + vgs "$vg" >/dev/null 2>&1 && refuse "a volume group $vg already exists; the lab makes or asks for it. Remove it and start the lab again." +done +d0=$(disk_add 0 1G) +parted -s "$d0" mklabel gpt mkpart a 1MiB 601MiB mkpart b 601MiB 901MiB +d1=$(disk_add 1 1G) +parted -s "$d1" mklabel gpt mkpart a 1MiB 401MiB mkpart b 401MiB 801MiB set 1 lvm on set 2 lvm on +udevadm settle +for p in "$d0"p1 "$d0"p2 "$d1"p1 "$d1"p2; do + [ -b "$p" ] || partx -u "${p%p[0-9]}" +done +udevadm settle +pvcreate -q "$d1"p1 "$d1"p2 >/dev/null +vgcreate -q vgold "$d1"p1 "$d1"p2 >/dev/null +lvcreate -q -y -n lvone -L 300M vgold >/dev/null +lvcreate -q -y -n lvtwo -l 100%FREE vgold >/dev/null diff --git a/labs/rhcsa/5-3-mounting-at-boot/README.md b/labs/rhcsa/5-3-mounting-at-boot/README.md new file mode 100644 index 0000000..f9bd683 --- /dev/null +++ b/labs/rhcsa/5-3-mounting-at-boot/README.md @@ -0,0 +1,32 @@ +# RHCSA 5.3: Mounting by UUID or label + +Mounting file systems at boot by UUID and by label, and finding a bad +`/etc/fstab` line without rebooting. + +## Before you start + +The lab adds two empty 1 GiB spare disks, `/dev/loop40` and +`/dev/loop41`. They're image files under `/var/lib/le-lab/disks/`, +attached again at every boot, before file systems are mounted. Use them +whole or partition them. + +It also adds a line to `/etc/fstab` for a UUID that doesn't exist, at +`/mnt/archive`. That line has `nofail`, so it doesn't stop the boot. + +## Tasks + +1. Make an XFS file system on a spare disk (or a partition or LV on it) + and mount it on `/data` at every boot, by UUID. +2. Make an ext4 file system labeled `backups` and mount it on `/backups` + at every boot, by label. +3. Find the bad `/mnt/archive` line without rebooting, then remove it. + `findmnt --verify` should then report no errors and no warnings. + +## Hints + +- `mkfs.xfs`, `blkid`, a line `UUID=... /data xfs defaults 0 0`, then + `systemctl daemon-reload` and `mount -a`. +- `mkfs.ext4 -L backups`, or `e2label`; `LABEL=backups /backups ext4 + defaults 0 2`. +- `findmnt --verify` marks the bad line with `[E]`; run + `systemctl daemon-reload` after editing `/etc/fstab`. diff --git a/labs/rhcsa/5-3-mounting-at-boot/check.sh b/labs/rhcsa/5-3-mounting-at-boot/check.sh new file mode 100644 index 0000000..a1c02d6 --- /dev/null +++ b/labs/rhcsa/5-3-mounting-at-boot/check.sh @@ -0,0 +1,21 @@ +src() { findmnt -n -o SOURCE "$1" 2>/dev/null; } +fstab_src() { findmnt --fstab -n -o SOURCE "$1" 2>/dev/null; } +on_spare() { case $(readlink -f "$(src "$1")") in /dev/loop4[01]* | /dev/dm-*) return 0 ;; esac; return 1; } + +task 1 "XFS on /data, mounted at boot by UUID" +need "/data is mounted" 'findmnt -n /data' +need "it's from a spare disk" 'on_spare /data' +need "it's XFS" '[ "$(findmnt -n -o FSTYPE /data)" = xfs ]' +need "/etc/fstab mounts it on /data by UUID" '[ "$(fstab_src /data)" = "UUID=$(findmnt -n -o UUID /data)" ] || [ "$(fstab_src /data)" = "UUID=\"$(findmnt -n -o UUID /data)\"" ]' + +task 2 "ext4 labeled backups on /backups, mounted at boot by label" +need "/backups is mounted" 'findmnt -n /backups' +need "it's from a spare disk" 'on_spare /backups' +need "it's ext4" '[ "$(findmnt -n -o FSTYPE /backups)" = ext4 ]' +need "its label is backups" '[ "$(findmnt -n -o LABEL /backups)" = backups ]' +need "/etc/fstab mounts it by LABEL=backups" 'fstab_src /backups | grep -Eqx "LABEL=\"?backups\"?"' + +task 3 "The bad /mnt/archive line removed" +need "/etc/fstab no longer has the line for the missing UUID" '! grep -q "$(state_get bad-uuid)" /etc/fstab' +need "findmnt --verify reports no errors" 'findmnt --verify 2>&1 | grep -Eq "(^| )0 errors|no errors"' +need "findmnt --verify reports no warnings either" 'findmnt --verify 2>&1 | grep -Eq "0 warnings|no errors or warnings"' diff --git a/labs/rhcsa/5-3-mounting-at-boot/lab.conf b/labs/rhcsa/5-3-mounting-at-boot/lab.conf new file mode 100644 index 0000000..6daecb1 --- /dev/null +++ b/labs/rhcsa/5-3-mounting-at-boot/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.3 Mounting by UUID or label" +CHAPTER=/rhcsa/5-local-storage/5-3-mounting-at-boot/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/5-3-mounting-at-boot/reset.sh b/labs/rhcsa/5-3-mounting-at-boot/reset.sh new file mode 100644 index 0000000..4d29723 --- /dev/null +++ b/labs/rhcsa/5-3-mounting-at-boot/reset.sh @@ -0,0 +1,13 @@ +for m in /data /backups /mnt/archive; do + findmnt -rn -o SOURCE "$m" 2>/dev/null | grep -Eq "^/dev/(loop4|mapper/)" && umount -l "$m" +done +# Setup refused to start with fstab lines for these mount points, so every +# line for them is the lab's or the student's. +sed -i -E '\%^[^#[:space:]]+[[:space:]]+/(data|backups|mnt/archive)/?[[:space:]]%d' /etc/fstab +u=$(state_get bad-uuid) +[ -n "$u" ] && sed -i "/$u/d" /etc/fstab +systemctl daemon-reload +rmdir /mnt/archive 2>/dev/null +[ -n "$(state_get had-data)" ] || rmdir /data 2>/dev/null +[ -n "$(state_get had-backups)" ] || rmdir /backups 2>/dev/null +true diff --git a/labs/rhcsa/5-3-mounting-at-boot/setup.sh b/labs/rhcsa/5-3-mounting-at-boot/setup.sh new file mode 100644 index 0000000..0254c81 --- /dev/null +++ b/labs/rhcsa/5-3-mounting-at-boot/setup.sh @@ -0,0 +1,14 @@ +for m in /data /backups /mnt/archive; do + findmnt -n "$m" >/dev/null && refuse "something is mounted on $m already; unmount it, take its line out of /etc/fstab, and start the lab again." + findmnt --fstab -n "$m" >/dev/null && refuse "/etc/fstab already has a line for $m; take it out and start the lab again." +done +blkid -t LABEL=backups >/dev/null 2>&1 && refuse "a file system labeled backups already exists; the lab asks you to make it." +[ -d /data ] && state_set had-data 1 +[ -d /backups ] && state_set had-backups 1 +disk_add 0 1G >/dev/null +disk_add 1 1G >/dev/null +uuid=$(cat /proc/sys/kernel/random/uuid) +state_set bad-uuid "$uuid" +mkdir -p /mnt/archive +printf 'UUID=%s /mnt/archive xfs defaults,nofail 0 0\n' "$uuid" >>/etc/fstab +systemctl daemon-reload diff --git a/labs/rhcsa/5-4-adding-storage-and-swap/README.md b/labs/rhcsa/5-4-adding-storage-and-swap/README.md new file mode 100644 index 0000000..993d720 --- /dev/null +++ b/labs/rhcsa/5-4-adding-storage-and-swap/README.md @@ -0,0 +1,39 @@ +# RHCSA 5.4: Adding storage and swap without data loss + +A swap partition in the free space of a disk that holds data, swap on a +new logical volume, and removing swap cleanly. + +## Before you start + +The lab adds two 1 GiB spare disks. They're image files under +`/var/lib/le-lab/disks/`, attached again at every boot, before file +systems and swap are activated. + +- `/dev/loop40` has a GPT table with one 300 MiB partition, + `/dev/loop40p1`, holding an ext4 file system with data. The rest of the + disk is free. +- `/dev/loop41` is a physical volume of the volume group `vglab`, with a + logical volume `lvdata` and a 128 MiB swap logical volume `oldswap`, + active and in `/etc/fstab`, as task 2 leaves a swap LV. Task 3 removes + it, so task 2's result can stay. + +## Tasks + +1. On `/dev/loop40`, add a 512 MiB swap partition in the free space and + make it active at every boot. The first partition and its data must + stay unchanged. +2. Add a 256 MiB swap space on a new logical volume in `vglab`, active at + every boot. +3. Remove the swap LV `oldswap` cleanly: not active, the LV gone, no + fstab line naming it, and no `Timed out waiting for device` error at + the next boot. + +## Hints + +- `fdisk` or `parted`: a new partition after the first; type Linux swap. + `mkswap`, a line `UUID=... none swap defaults 0 0`, `swapon -a`, + `swapon --show`. +- `lvcreate -L 256M -n`, `mkswap`, a line in `/etc/fstab`. +- `swapoff`, then remove its line from `/etc/fstab`, + `systemctl daemon-reload`, then `lvremove`. + `journalctl -b -p err` after a reboot. diff --git a/labs/rhcsa/5-4-adding-storage-and-swap/check.sh b/labs/rhcsa/5-4-adding-storage-and-swap/check.sh new file mode 100644 index 0000000..b25b5cd --- /dev/null +++ b/labs/rhcsa/5-4-adding-storage-and-swap/check.sh @@ -0,0 +1,43 @@ +# Swap devices in /etc/fstab, resolved to device paths. +fstab_swaps() { + local s + findmnt --fstab -n -t swap -o SOURCE 2>/dev/null | while read -r s; do + case $s in + UUID=* | LABEL=* | PARTUUID=* | PARTLABEL=*) blkid -t "$s" -o device 2>/dev/null ;; + *) echo "$s" ;; + esac + done | xargs -r -n1 readlink -f +} +in_fstab() { fstab_swaps | grep -qxF "$(readlink -f "$1")"; } +active_swap() { swapon --show=NAME --noheadings | xargs -r -n1 readlink -f | grep -qxF "$(readlink -f "$1")"; } +# The swap partition on loop40, and the new swap LV in vglab. +swap_part() { local p; for p in /dev/loop40p[2-9]; do [ "$(blkid -s TYPE -o value "$p" 2>/dev/null)" = swap ] && { echo "$p"; return; }; done; } +swap_lv() { local l; for l in $(lvs --noheadings -o lv_path vglab 2>/dev/null); do [ "${l##*/}" = oldswap ] && continue; [ "$(blkid -s TYPE -o value "$l" 2>/dev/null)" = swap ] && { echo "$l"; return; }; done; } +mib() { echo $(($(blockdev --getsize64 "$1") / 1048576)); } +about() { [ -n "$1" ] && [ "$1" -ge $(($2 - 2)) ] && [ "$1" -le $(($2 + 2)) ]; } +data_ok() { + local f sum + while read -r sum f; do + [ "$(debugfs -R "cat /$f" /dev/loop40p1 2>/dev/null | sha256sum | cut -d" " -f1)" = "$sum" ] || return 1 + done <"$LAB_STATE/data.sha" +} + +task 1 "A 512 MiB swap partition on /dev/loop40, active at boot" +need "/dev/loop40 has a second partition with swap on it" '[ -n "$(swap_part)" ]' +need "the swap partition is 512 MiB" 'about "$(mib "$(swap_part)")" 512' +need "it's active (swapon --show)" 'active_swap "$(swap_part)"' +need "it's in /etc/fstab" 'in_fstab "$(swap_part)"' +need "the first partition is unchanged" '[ "$(sfdisk --dump /dev/loop40 | grep "^/dev/loop40p1 ")" = "$(cat "$LAB_STATE/p1")" ] && [ "$(blkid -s UUID -o value /dev/loop40p1)" = "$(cat "$LAB_STATE/p1-uuid")" ]' +need "its data is unchanged" data_ok + +task 2 "A 256 MiB swap LV in vglab, active at boot" +need "vglab has a new logical volume with swap on it" '[ -n "$(swap_lv)" ]' +need "it's 256 MiB" 'about "$(lvs --noheadings --units m --nosuffix -o lv_size "$(swap_lv)" | cut -d. -f1 | tr -d " ")" 256' +need "it's active (swapon --show)" 'active_swap "$(swap_lv)"' +need "it's in /etc/fstab" 'in_fstab "$(swap_lv)"' + +task 3 "oldswap removed cleanly" +need "oldswap isn't active" '! swapon --show=NAME --noheadings | grep -q oldswap && ! swapon --show=NAME --noheadings | xargs -r -n1 readlink -f | grep -qxF "$(readlink -f /dev/vglab/oldswap 2>/dev/null || echo none)"' +need "the LV oldswap is gone" '! lvs vglab/oldswap' +need "no /etc/fstab line names it" '! grep -Ev "^[[:space:]]*#" /etc/fstab | grep -Eq "oldswap|$(cat "$LAB_STATE/oldswap-uuid")"' +need "this boot has no Timed out waiting for device error" '! journalctl -q -b -p err | grep -q "Timed out waiting for device"' diff --git a/labs/rhcsa/5-4-adding-storage-and-swap/lab.conf b/labs/rhcsa/5-4-adding-storage-and-swap/lab.conf new file mode 100644 index 0000000..5154e38 --- /dev/null +++ b/labs/rhcsa/5-4-adding-storage-and-swap/lab.conf @@ -0,0 +1,3 @@ +TITLE="5.4 Adding storage and swap without data loss" +CHAPTER=/rhcsa/5-local-storage/5-4-adding-storage-and-swap/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/5-4-adding-storage-and-swap/reset.sh b/labs/rhcsa/5-4-adding-storage-and-swap/reset.sh new file mode 100644 index 0000000..dc873f4 --- /dev/null +++ b/labs/rhcsa/5-4-adding-storage-and-swap/reset.sh @@ -0,0 +1,13 @@ +# Swap on the spare disks off, and their fstab lines out (by path or UUID). +for d in /dev/loop40p* /dev/mapper/vglab-*; do + [ -b "$d" ] || continue + swapoff "$d" 2>/dev/null + u=$(blkid -s UUID -o value "$d" 2>/dev/null) + [ -n "$u" ] && sed -i "/$u/d" /etc/fstab +done +u=$(cat "$LAB_STATE/oldswap-uuid" 2>/dev/null) +[ -n "$u" ] && sed -i "/$u/d" /etc/fstab +sed -i -E '/^[^#]*(\/dev\/loop40|vglab)/d' /etc/fstab +systemctl daemon-reload +vgchange -an vglab >/dev/null 2>&1 +true diff --git a/labs/rhcsa/5-4-adding-storage-and-swap/setup.sh b/labs/rhcsa/5-4-adding-storage-and-swap/setup.sh new file mode 100644 index 0000000..c14e74a --- /dev/null +++ b/labs/rhcsa/5-4-adding-storage-and-swap/setup.sh @@ -0,0 +1,29 @@ +command -v vgcreate >/dev/null || pkg_install lvm2 +pkg_installed parted || pkg_install parted +vgs vglab >/dev/null 2>&1 && refuse "a volume group vglab already exists; the lab makes it. Remove it and start the lab again." +grep -q vglab /etc/fstab && refuse "/etc/fstab already names vglab; take that line out and start the lab again." +# Disk 0: a partition with data, and free space. +d0=$(disk_add 0 1G) +parted -s "$d0" mklabel gpt mkpart data 1MiB 301MiB +udevadm settle +[ -b "${d0}p1" ] || partx -u "$d0" +mkfs.ext4 -q -L le-data "${d0}p1" +m=$(mktemp -d) +mount "${d0}p1" "$m" +cp /etc/services "$m/services" +echo "Lab data: keep me" >"$m/keep.txt" +sha256sum "$m/services" "$m/keep.txt" | sed "s|$m/||" >"$LAB_STATE/data.sha" +umount "$m"; rmdir "$m" +sfdisk --dump "$d0" | grep "^${d0}p1 " >"$LAB_STATE/p1" +blkid -s UUID -o value "${d0}p1" >"$LAB_STATE/p1-uuid" +# Disk 1: volume group vglab with data and an old swap LV in fstab. +d1=$(disk_add 1 1G) +pvcreate -q "$d1" >/dev/null +vgcreate -q vglab "$d1" >/dev/null +lvcreate -q -y -n lvdata -L 200M vglab >/dev/null +lvcreate -q -y -n oldswap -L 128M vglab >/dev/null +mkswap -q /dev/vglab/oldswap >/dev/null +blkid -s UUID -o value /dev/vglab/oldswap >"$LAB_STATE/oldswap-uuid" +printf '/dev/vglab/oldswap none swap defaults 0 0\n' >>/etc/fstab +systemctl daemon-reload +swapon /dev/vglab/oldswap diff --git a/labs/rhcsa/6-1-vfat-ext4-xfs/README.md b/labs/rhcsa/6-1-vfat-ext4-xfs/README.md new file mode 100644 index 0000000..1c174f2 --- /dev/null +++ b/labs/rhcsa/6-1-vfat-ext4-xfs/README.md @@ -0,0 +1,36 @@ +# RHCSA 6.1: VFAT, ext4 and XFS + +Making the three file system types, mounting VFAT for a normal user, and +unmounting a file system that's in use. + +## Before you start + +The lab adds three empty spare disks: `/dev/loop40` (256 MiB), +`/dev/loop41` (256 MiB) and `/dev/loop42` (512 MiB). They come back at +every boot, before file systems are mounted, so `/etc/fstab` lines for them +keep working. It also creates user `student` (password `Lab-Pass-1`) if +there isn't one. + +## Tasks + +1. Create a VFAT file system on `/dev/loop40`, ext4 on `/dev/loop41` and + XFS on `/dev/loop42`, and mount them at `/mnt/fat`, `/mnt/ext` and + `/mnt/xfs`. As root, create a file in each. +2. Mount the VFAT file system so that user `student` owns everything on it, + and make that permanent: after a reboot, `touch /mnt/fat/test` works as + `student`. +3. `cd` into `/mnt/xfs` in one shell and try to unmount it from another. + Find out which program keeps it busy and write its name to + `/root/lab-answers/3`. Then leave the directory and unmount `/mnt/xfs`. + +## Hints + +- `mkfs.vfat` (package `dosfstools`), `mkfs.ext4`, `mkfs.xfs`; `mkdir -p`, + `mount`, `lsblk -f`. +- `uid=` and `gid=` mount options for VFAT; a line in `/etc/fstab` (by + `UUID=`), then `systemctl daemon-reload` and `mount -a`. `chown` doesn't + work on VFAT. +- `fuser -vm /mnt/xfs`, or `lsof /mnt/xfs`; `findmnt /mnt/xfs`. +- The check mounts `/dev/loop42` for a moment to look for your file if it + isn't mounted. Mount `/mnt/ext` at every boot too if you reboot for + task 2, or mount it again afterwards. diff --git a/labs/rhcsa/6-1-vfat-ext4-xfs/check.sh b/labs/rhcsa/6-1-vfat-ext4-xfs/check.sh new file mode 100644 index 0000000..52ad264 --- /dev/null +++ b/labs/rhcsa/6-1-vfat-ext4-xfs/check.sh @@ -0,0 +1,38 @@ +fstype() { blkid -s TYPE -o value "$1" 2>/dev/null; } +mounted_from() { [ "$(findmnt -n -o SOURCE "$1")" = "$2" ]; } +# has_file DIR: a regular file somewhere in DIR, outside lost+found. +has_file() { find "$1" -xdev -type f -not -path '*/lost+found/*' -not -name 'le-check-*' 2>/dev/null | grep -q .; } +# xfs_has_file: the XFS file system on /dev/loop42 holds a file, mounting +# it read-only for a moment if it isn't mounted. +xfs_has_file() { + local m rc + m=$(findmnt -rn -o TARGET -S /dev/loop42 | head -n1) + [ -n "$m" ] && { has_file "$m"; return; } + m=$(mktemp -d) + mount -o ro /dev/loop42 "$m" || { rmdir "$m"; return 1; } + has_file "$m"; rc=$? + umount "$m"; rmdir "$m" + return $rc +} + +task 1 "VFAT, ext4 and XFS file systems, each with a file in it" +need "/dev/loop40 holds a VFAT file system" '[ "$(fstype /dev/loop40)" = vfat ]' +need "/dev/loop41 holds an ext4 file system" '[ "$(fstype /dev/loop41)" = ext4 ]' +need "/dev/loop42 holds an XFS file system" '[ "$(fstype /dev/loop42)" = xfs ]' +need "/mnt/fat is mounted from /dev/loop40" mounted_from /mnt/fat /dev/loop40 +need "/mnt/ext is mounted from /dev/loop41" mounted_from /mnt/ext /dev/loop41 +need "there's a file in /mnt/fat" has_file /mnt/fat +need "there's a file in /mnt/ext" has_file /mnt/ext +need "there's a file on the XFS file system" xfs_has_file + +task 2 "VFAT at /mnt/fat owned by student, at every boot" +need "/mnt/fat is mounted from /dev/loop40" mounted_from /mnt/fat /dev/loop40 +need "/etc/fstab mounts /mnt/fat" 'findmnt -s -n /mnt/fat' +need "/etc/fstab mounts it with uid= for student" 'findmnt -s -n -o OPTIONS /mnt/fat | tr , "\n" | grep -Eqx "uid=(student|$(id -u student))"' +need "ls -l /mnt/fat shows student as the owner" '[ "$(stat -c %U /mnt/fat)" = student ]' +need "student can create a file in /mnt/fat" 'runuser -u student -- touch /mnt/fat/le-check-$$ && rm -f /mnt/fat/le-check-$$' + +task 3 "The busy /mnt/xfs explained and unmounted" +need "/dev/loop42 holds an XFS file system" '[ "$(fstype /dev/loop42)" = xfs ]' +need "/root/lab-answers/3 names the program (your shell)" '[[ "$(answer 3)" =~ ^-?(ba|z|k|da|tc|c|fi)?sh$ ]]' +need "/mnt/xfs isn't mounted" '! findmnt -n /mnt/xfs' diff --git a/labs/rhcsa/6-1-vfat-ext4-xfs/lab.conf b/labs/rhcsa/6-1-vfat-ext4-xfs/lab.conf new file mode 100644 index 0000000..e3ba3d5 --- /dev/null +++ b/labs/rhcsa/6-1-vfat-ext4-xfs/lab.conf @@ -0,0 +1,3 @@ +TITLE="6.1 VFAT, ext4 and XFS" +CHAPTER=/rhcsa/6-file-systems/6-1-vfat-ext4-xfs/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/6-1-vfat-ext4-xfs/reset.sh b/labs/rhcsa/6-1-vfat-ext4-xfs/reset.sh new file mode 100644 index 0000000..245b9d0 --- /dev/null +++ b/labs/rhcsa/6-1-vfat-ext4-xfs/reset.sh @@ -0,0 +1,15 @@ +for m in /mnt/fat /mnt/ext /mnt/xfs; do + findmnt -n "$m" >/dev/null 2>&1 && umount -l "$m" +done +# fstab lines for the spare disks, by path, UUID or mount point. +for d in /dev/loop40 /dev/loop41 /dev/loop42; do + u=$(blkid -s UUID -o value "$d" 2>/dev/null) + [ -n "$u" ] && sed -i "/$u/d" /etc/fstab +done +awk '/^[[:space:]]*#/ || NF < 2 { print; next } + $1 ~ /^\/dev\/loop4[0-2]$/ || $2 == "/mnt/fat" || $2 == "/mnt/ext" || $2 == "/mnt/xfs" { next } + { print }' /etc/fstab >/etc/fstab.le-lab && cat /etc/fstab.le-lab >/etc/fstab +rm -f /etc/fstab.le-lab +systemctl daemon-reload +rmdir /mnt/fat /mnt/ext /mnt/xfs 2>/dev/null +true diff --git a/labs/rhcsa/6-1-vfat-ext4-xfs/setup.sh b/labs/rhcsa/6-1-vfat-ext4-xfs/setup.sh new file mode 100644 index 0000000..f3fc59c --- /dev/null +++ b/labs/rhcsa/6-1-vfat-ext4-xfs/setup.sh @@ -0,0 +1,7 @@ +for m in /mnt/fat /mnt/ext /mnt/xfs; do + findmnt -n "$m" >/dev/null && refuse "$m is already mounted; unmount it and start again" +done +user_add student +disk_add 0 256M >/dev/null +disk_add 1 256M >/dev/null +disk_add 2 512M >/dev/null diff --git a/labs/rhcsa/6-2-nfs/README.md b/labs/rhcsa/6-2-nfs/README.md new file mode 100644 index 0000000..98b3449 --- /dev/null +++ b/labs/rhcsa/6-2-nfs/README.md @@ -0,0 +1,29 @@ +# RHCSA 6.2: Mounting NFS + +Finding an NFS server's exports, mounting one by hand, and mounting it at +every boot. + +## Before you start + +The lab adds a second machine, `server` (`10.99.0.2`), running an NFS +server that exports `/exports/share` and `/exports/homes`. There's a file +`readme.txt` in `/exports/share` with a secret word in it. `server` shares +this VM's files, so you could read the directory here too, but the tasks +are about mounting it from `server`. The lab installs the NFS client tools +(`nfs-utils`). + +## Tasks + +1. List `server`'s exports from this VM, and save the list in + `/root/lab-answers/1`. +2. Mount `server:/exports/share` on `/mnt/nfs` by hand, read `readme.txt` + there, write the secret word from it to `/root/lab-answers/2`, and + unmount it. +3. Mount `server:/exports/share` on `/mnt/nfs` at every boot. + +## Hints + +- `showmount -e server`; `>` saves a command's output. +- `mkdir -p /mnt/nfs`, `mount -t nfs`, `findmnt /mnt/nfs`, `umount`. +- A line in `/etc/fstab` with `_netdev`, then `systemctl daemon-reload` and + `mount -a`. diff --git a/labs/rhcsa/6-2-nfs/check.sh b/labs/rhcsa/6-2-nfs/check.sh new file mode 100644 index 0000000..182877e --- /dev/null +++ b/labs/rhcsa/6-2-nfs/check.sh @@ -0,0 +1,16 @@ +fstab_opts() { findmnt -s -n -o OPTIONS "$1" 2>/dev/null; } +from_server() { grep -Eq "^(server(\.lab\.example)?|10\.99\.0\.2):/exports/share/*$"; } + +task 1 "server's exports listed in /root/lab-answers/1" +need "/root/lab-answers/1 lists /exports/share" 'grep -qs "^/exports/share[[:space:]]" /root/lab-answers/1' +need "/root/lab-answers/1 lists /exports/homes" 'grep -qs "^/exports/homes[[:space:]]" /root/lab-answers/1' + +task 2 "The share mounted by hand and its secret word read" +need "/root/lab-answers/2 holds the secret word from readme.txt" '[ "$(answer 2)" = "$(state_get word)" ]' + +task 3 "server:/exports/share mounted at /mnt/nfs at every boot" +need "/mnt/nfs is an NFS mount" 'findmnt -n -o FSTYPE /mnt/nfs | grep -Eqx "nfs4?"' +need "/mnt/nfs is mounted from server:/exports/share" 'findmnt -n -o SOURCE /mnt/nfs | from_server' +need "/mnt/nfs shows readme.txt" 'timeout 10 test -f /mnt/nfs/readme.txt' +need "/etc/fstab mounts server:/exports/share at /mnt/nfs" 'findmnt -s -n -o SOURCE /mnt/nfs | from_server' +need "the fstab line has _netdev" 'fstab_opts /mnt/nfs | tr , "\n" | grep -qx _netdev' diff --git a/labs/rhcsa/6-2-nfs/lab.conf b/labs/rhcsa/6-2-nfs/lab.conf new file mode 100644 index 0000000..e5fd843 --- /dev/null +++ b/labs/rhcsa/6-2-nfs/lab.conf @@ -0,0 +1,3 @@ +TITLE="6.2 Mounting NFS" +CHAPTER=/rhcsa/6-file-systems/6-2-nfs/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/6-2-nfs/reset.sh b/labs/rhcsa/6-2-nfs/reset.sh new file mode 100644 index 0000000..cc8e19c --- /dev/null +++ b/labs/rhcsa/6-2-nfs/reset.sh @@ -0,0 +1,10 @@ +findmnt -n /mnt/nfs >/dev/null 2>&1 && { umount /mnt/nfs 2>/dev/null || umount -l /mnt/nfs; } +awk '/^[[:space:]]*#/ || NF < 2 { print; next } $2 == "/mnt/nfs" || $2 == "/mnt/nfs/" { next } { print }' /etc/fstab >/etc/fstab.le-lab && + cat /etc/fstab.le-lab >/etc/fstab +rm -f /etc/fstab.le-lab +systemctl disable --now le-lab-nfs-server.service >/dev/null 2>&1 +rm -f /etc/systemd/system/le-lab-nfs-server.service /etc/exports.d/le-lab.exports +systemctl daemon-reload +rm -rf /exports +rmdir /mnt/nfs 2>/dev/null +true diff --git a/labs/rhcsa/6-2-nfs/setup.sh b/labs/rhcsa/6-2-nfs/setup.sh new file mode 100644 index 0000000..6100554 --- /dev/null +++ b/labs/rhcsa/6-2-nfs/setup.sh @@ -0,0 +1,66 @@ +findmnt -n /mnt/nfs >/dev/null && refuse "/mnt/nfs is already mounted; unmount it and start again" +[ -e /exports ] && refuse "/exports already exists; the lab's server needs it" +nfs_was_enabled=no +enabled nfs-server && nfs_was_enabled=yes +pkg_installed nfs-utils || pkg_install nfs-utils +# The server runs in the peer, not on the VM itself. +[ "$nfs_was_enabled" = yes ] || systemctl disable --now nfs-server >/dev/null 2>&1 || true +peer_up +hosts_add "$LE_PEER_ADDR server.lab.example server" + +word=$(shuf -n1 -e marigold juniper cobalt saffron basalt quokka lantern tundra) +state_set word "$word" +mkdir -p /exports/share /exports/homes +printf 'This file is on the NFS server.\nThe secret word is: %s\n' "$word" >/exports/share/readme.txt +mkdir -p /etc/exports.d +cat >/etc/exports.d/le-lab.exports <"$LAB_STATE/server.sh" <<'X' +set -e +export PATH=/usr/sbin:/usr/bin:/sbin:/bin +mount -t tmpfs -o mode=755 tmpfs /run +mkdir -p /run/rpc_pipefs /run/rpcbind +if command -v restorecon >/dev/null; then restorecon -R /run; fi +rpcbind -w +mount -t nfsd nfsd /proc/fs/nfsd +mount -t rpc_pipefs sunrpc /run/rpc_pipefs +if command -v nfsdcld >/dev/null; then + mkdir -p "$1/server-cld" + nfsdcld -p /run/rpc_pipefs -s "$1/server-cld" +fi +echo 10 >/proc/fs/nfsd/nfsv4gracetime +echo 10 >/proc/fs/nfsd/nfsv4leasetime +exportfs -r +rpc.nfsd -H 10.99.0.2 8 +exec rpc.mountd -F +X +cat >"$LAB_STATE/server-stop.sh" <<'X' +export PATH=/usr/sbin:/usr/bin:/sbin:/bin +mount -t nfsd nfsd /proc/fs/nfsd && echo 0 >/proc/fs/nfsd/threads +true +X +cat >/etc/systemd/system/le-lab-nfs-server.service </dev/null; } +use_as() { timeout 30 runuser -u "$1" -- stat "$2" >/dev/null; } +nfs_src() { mnt_field "$1" 'nfs4?' 1; } + +task 1 "server:/exports/share on demand at /shares/data, 60-second timeout" +need "autofs is running" active autofs +need "autofs watches /shares with an indirect map" 'trigger_opts /shares | grep -qw indirect' +need "/shares has a 60-second timeout" 'trigger_opts /shares | tr , "\n" | grep -qx timeout=60' +need "using /shares/data shows readme.txt" use /shares/data/readme.txt +need "/shares/data is then an NFS mount of server:/exports/share" 'nfs_src /shares/data | grep -q ":/exports/share/*$"' + +task 2 "Homes on demand at /rhome/USER with one map line" +need "autofs watches /rhome with an indirect map" 'trigger_opts /rhome | grep -qw indirect' +need "using /rhome/alice shows her welcome.txt" use_as alice /rhome/alice/welcome.txt +need "/rhome/alice is an NFS mount of server:/exports/homes/alice" 'nfs_src /rhome/alice | grep -q ":/exports/homes/alice/*$"' +need "the same map serves bob (/rhome/bob)" use_as bob /rhome/bob/welcome.txt +need "su - alice lands in /rhome/alice" '[ "$(timeout 30 runuser -l alice -c pwd)" = /rhome/alice ]' + +task 3 "server:/exports/share at /mnt/direct with a direct map, at boot" +need "autofs starts at boot" enabled autofs +need "autofs watches /mnt/direct with a direct map" 'trigger_opts /mnt/direct | grep -qw direct' +need "cat /mnt/direct/readme.txt works" 'timeout 30 cat /mnt/direct/readme.txt' +need "/mnt/direct is then an NFS mount of server:/exports/share" 'nfs_src /mnt/direct | grep -q ":/exports/share/*$"' diff --git a/labs/rhcsa/6-3-autofs/lab.conf b/labs/rhcsa/6-3-autofs/lab.conf new file mode 100644 index 0000000..3146f26 --- /dev/null +++ b/labs/rhcsa/6-3-autofs/lab.conf @@ -0,0 +1,3 @@ +TITLE="6.3 autofs" +CHAPTER=/rhcsa/6-file-systems/6-3-autofs/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/6-3-autofs/reset.sh b/labs/rhcsa/6-3-autofs/reset.sh new file mode 100644 index 0000000..e3c5b0b --- /dev/null +++ b/labs/rhcsa/6-3-autofs/reset.sh @@ -0,0 +1,22 @@ +# autofs: stop it (if the lab's tasks started it) and drop the lab's maps. +[ "$(state_get autofs-enabled)" = yes ] || systemctl disable --now autofs >/dev/null 2>&1 +for f in /etc/auto.master /etc/auto.master.d/*.autofs; do + [ -f "$f" ] || continue + awk '!/^[[:space:]]*#/ && ($1 == "/shares" || $1 == "/rhome" || $1 == "/-") {print $1, $2}' "$f" | while read -r mp map; do + case $map in /etc/auto.master*) continue ;; /*) ;; *) continue ;; esac + [ "$mp" != /- ] || grep -qs /mnt/direct "$map" || continue + rm -f "$map" + done + sed -i -E '\#^[[:space:]]*(/shares|/rhome|/-)[[:space:]]#d' "$f" + case $f in *.autofs) grep -qEv '^[[:space:]]*(#|$)' "$f" || rm -f "$f" ;; esac +done +[ "$(state_get autofs-enabled)" = yes ] && systemctl restart autofs >/dev/null 2>&1 +findmnt -rn -o TARGET | grep -E '^(/shares|/rhome|/mnt/direct)' | sort -r | xargs -r -n1 umount -l 2>/dev/null +systemctl disable --now le-lab-nfs-server.service >/dev/null 2>&1 +rm -f /etc/systemd/system/le-lab-nfs-server.service /etc/exports.d/le-lab.exports +systemctl daemon-reload +# alice's home goes back where it was before userdel removes her. +usermod -d /home/alice alice 2>/dev/null +rm -rf /exports +rmdir /shares /rhome /mnt/direct 2>/dev/null +true diff --git a/labs/rhcsa/6-3-autofs/setup.sh b/labs/rhcsa/6-3-autofs/setup.sh new file mode 100644 index 0000000..057b9a0 --- /dev/null +++ b/labs/rhcsa/6-3-autofs/setup.sh @@ -0,0 +1,75 @@ +for m in /shares /rhome /mnt/direct; do + findmnt -n "$m" >/dev/null && refuse "$m is already mounted; unmount it and start again" +done +[ -e /exports ] && refuse "/exports already exists; the lab's server needs it" +nfs_was_enabled=no +enabled nfs-server && nfs_was_enabled=yes +pkg_installed nfs-utils || pkg_install nfs-utils +[ "$nfs_was_enabled" = yes ] || systemctl disable --now nfs-server >/dev/null 2>&1 || true +enabled autofs && state_set autofs-enabled yes +peer_up +hosts_add "$LE_PEER_ADDR server.lab.example server" + +user_add alice +user_add bob +usermod -d /rhome/alice alice +mkdir -p /exports/share /exports/homes/alice /exports/homes/bob +echo 'This file is on the NFS server.' >/exports/share/readme.txt +for u in alice bob; do + cp -a /etc/skel/. "/exports/homes/$u/" + echo "Welcome home, $u" >"/exports/homes/$u/welcome.txt" + chown -R "$u:" "/exports/homes/$u" + chmod 700 "/exports/homes/$u" +done +mkdir -p /etc/exports.d +cat >/etc/exports.d/le-lab.exports <"$LAB_STATE/server.sh" <<'X' +set -e +export PATH=/usr/sbin:/usr/bin:/sbin:/bin +mount -t tmpfs -o mode=755 tmpfs /run +mkdir -p /run/rpc_pipefs /run/rpcbind +if command -v restorecon >/dev/null; then restorecon -R /run; fi +rpcbind -w +mount -t nfsd nfsd /proc/fs/nfsd +mount -t rpc_pipefs sunrpc /run/rpc_pipefs +if command -v nfsdcld >/dev/null; then + mkdir -p "$1/server-cld" + nfsdcld -p /run/rpc_pipefs -s "$1/server-cld" +fi +echo 10 >/proc/fs/nfsd/nfsv4gracetime +echo 10 >/proc/fs/nfsd/nfsv4leasetime +exportfs -r +rpc.nfsd -H 10.99.0.2 8 +exec rpc.mountd -F +X +cat >"$LAB_STATE/server-stop.sh" <<'X' +export PATH=/usr/sbin:/usr/bin:/sbin:/bin +mount -t nfsd nfsd /proc/fs/nfsd && echo 0 >/proc/fs/nfsd/threads +true +X +cat >/etc/systemd/system/le-lab-nfs-server.service </dev/null | awk '{printf "%d", $1}'; } +fs_mib() { + case $(blkid -s TYPE -o value "/dev/vg_lab/$1") in + xfs) xfs_info "/dev/vg_lab/$1" 2>/dev/null | awk -F'[ =,]+' '/^data/ {for (i = 1; i < NF; i++) {if ($i == "bsize") b = $(i + 1); if ($i == "blocks") n = $(i + 1)} print int(b * n / 1048576); exit}' ;; + ext*) dumpe2fs -h "/dev/vg_lab/$1" 2>/dev/null | awk -F: '/^Block count/ {n = $2} /^Block size/ {b = $2} END {print int(b * n / 1048576)}' ;; + esac +} +# The file system fills its volume (within 4 MiB). +fills() { local l f; l=$(lv_mib "$1"); f=$(fs_mib "$1"); [ -n "$f" ] && [ $((l - f)) -le 4 ]; } +# MiB of lv_data on PVs of /dev/loop41. +on_new_pv() { lvs --noheadings --units m --nosuffix -o seg_size,devices --segments vg_lab/lv_data 2>/dev/null | awk '$2 ~ /^\/dev\/loop41/ {s += $1} END {printf "%d", s}'; } + +task 1 "lv_data and its file system grown by 200 MiB, mounted" +need "/data is still mounted from lv_data" '[ "$(findmnt -n -o SOURCE /data)" = /dev/mapper/vg_lab-lv_data ]' +need "lv_data is at least 700 MiB" '[ "$(lv_mib lv_data)" -ge 696 ]' +need "the XFS file system fills lv_data" fills lv_data +need "/data/keep.txt is still there" grep -q "Keep this file" /data/keep.txt + +task 2 "lv_logs grown by 100 MiB, then its file system" +need "/logs is still mounted from lv_logs" '[ "$(findmnt -n -o SOURCE /logs)" = /dev/mapper/vg_lab-lv_logs ]' +need "lv_logs is at least 400 MiB" '[ "$(lv_mib lv_logs)" -ge 396 ]' +need "the ext4 file system fills lv_logs" fills lv_logs +need "df -h /logs shows the larger size" '[ "$(df -BM --output=size /logs | tail -n1 | tr -dc 0-9)" -ge $(( $(state_get logs-df) + 80 )) ]' +need "/logs/keep.txt is still there" grep -q "Keep this file" /logs/keep.txt + +task 3 "vg_lab filled, then lv_data grown onto /dev/loop41" +need "/dev/loop41 (or a partition on it) is in vg_lab" 'pvs --noheadings -o pv_name,vg_name 2>/dev/null | awk "\$1 ~ /^\/dev\/loop41/ && \$2 == \"vg_lab\"" | grep -q .' +need "vg_lab has two physical volumes" '[ "$(vgs --noheadings -o pv_count vg_lab | tr -d " ")" = 2 ]' +need "/dev/loop40 has no free space left" '[ "$(pvs --noheadings --units m --nosuffix -o pv_free /dev/loop40 | awk "{printf \"%d\", \$1}")" = 0 ]' +need "lv_data has about 200 MiB on the new disk" '[ "$(on_new_pv)" -ge 196 ]' +need "the XFS file system fills lv_data" fills lv_data +need "/data/keep.txt is still there" grep -q "Keep this file" /data/keep.txt diff --git a/labs/rhcsa/6-4-extending-logical-volumes/lab.conf b/labs/rhcsa/6-4-extending-logical-volumes/lab.conf new file mode 100644 index 0000000..62bf2c8 --- /dev/null +++ b/labs/rhcsa/6-4-extending-logical-volumes/lab.conf @@ -0,0 +1,3 @@ +TITLE="6.4 Extending logical volumes" +CHAPTER=/rhcsa/6-file-systems/6-4-extending-logical-volumes/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/6-4-extending-logical-volumes/reset.sh b/labs/rhcsa/6-4-extending-logical-volumes/reset.sh new file mode 100644 index 0000000..c052a02 --- /dev/null +++ b/labs/rhcsa/6-4-extending-logical-volumes/reset.sh @@ -0,0 +1,12 @@ +for m in /data /logs; do + findmnt -rn -o SOURCE "$m" 2>/dev/null | grep -q vg_lab && umount -l "$m" +done +for lv in $(lvs --noheadings -o lv_path vg_lab 2>/dev/null); do + uuid=$(blkid -s UUID -o value "$lv" 2>/dev/null) + [ -n "$uuid" ] && sed -i "/$uuid/d" /etc/fstab +done +sed -i '/vg_lab/d' /etc/fstab +systemctl daemon-reload +vgchange -an vg_lab >/dev/null 2>&1 +rmdir /data /logs 2>/dev/null +true diff --git a/labs/rhcsa/6-4-extending-logical-volumes/setup.sh b/labs/rhcsa/6-4-extending-logical-volumes/setup.sh new file mode 100644 index 0000000..225680f --- /dev/null +++ b/labs/rhcsa/6-4-extending-logical-volumes/setup.sh @@ -0,0 +1,22 @@ +vgs vg_lab >/dev/null 2>&1 && refuse "volume group vg_lab already exists; remove it and start again" +for m in /data /logs; do + findmnt -n "$m" >/dev/null && refuse "$m is already mounted; unmount it and start again" +done +disk_add 0 2G >/dev/null +disk_add 1 1G >/dev/null +vgcreate -q vg_lab /dev/loop40 >/dev/null +lvcreate -q -y -n lv_data -L 500M vg_lab >/dev/null +lvcreate -q -y -n lv_logs -L 300M vg_lab >/dev/null +mkfs.xfs -q /dev/vg_lab/lv_data +mkfs.ext4 -q /dev/vg_lab/lv_logs +mkdir -p /data /logs +cat >>/etc/fstab <<'X' +/dev/vg_lab/lv_data /data xfs defaults 0 0 +/dev/vg_lab/lv_logs /logs ext4 defaults 0 2 +X +systemctl daemon-reload +mount /data +mount /logs +echo 'Keep this file.' >/data/keep.txt +echo 'Keep this file too.' >/logs/keep.txt +state_set logs-df "$(df -BM --output=size /logs | tail -n1 | tr -dc 0-9)" diff --git a/labs/rhcsa/6-5-permission-problems/README.md b/labs/rhcsa/6-5-permission-problems/README.md new file mode 100644 index 0000000..78499b0 --- /dev/null +++ b/labs/rhcsa/6-5-permission-problems/README.md @@ -0,0 +1,31 @@ +# RHCSA 6.5: Fixing file permission problems + +A shared group directory, a file one user can't reach, and what `chmod` +does to an ACL's mask. + +## Before you start + +The lab creates group `devs` with members `alice` and `bob`, and a third +user, `carol`, who isn't in it (password `Lab-Pass-1` for all three). It +also creates the directory `/srv/reports` (mode `750`, owned by root) with +an ACL entry that gives `bob` read and execute access. + +## Tasks + +1. Make a directory `/srv/devs` where every new file belongs to group + `devs` and is writable by its members, and members can't delete each + other's files. +2. As root, create `/srv/secret/f.txt` (mode `644`) in a directory + `/srv/secret` with mode `700`. Find out why `bob` can't read it, then + let only `bob` read it, without giving access to anyone else. +3. Run `chmod 700 /srv/reports` and look at `bob`'s ACL entry with + `getfacl`. Write the effective permissions it shows to + `/root/lab-answers/3`, then give `bob` his access back with + `setfacl -m m::rx`. + +## Hints + +- `chgrp`, `chmod 3770` (or `g+s,o+t`), `setfacl -d -m g:devs:rw`; + `ls -ld`, `getfacl`. Test as the users: `su - alice`, `sudo -u bob`. +- `namei -l /srv/secret/f.txt`; `setfacl -m u:bob:x` on the directory. +- `getfacl -c /srv/reports` shows `#effective:` next to masked entries. diff --git a/labs/rhcsa/6-5-permission-problems/check.sh b/labs/rhcsa/6-5-permission-problems/check.sh new file mode 100644 index 0000000..ea56f3a --- /dev/null +++ b/labs/rhcsa/6-5-permission-problems/check.sh @@ -0,0 +1,26 @@ +D=/srv/devs F=/srv/devs/le-check-$$ +cleanup() { rm -f "$F"; } +trap cleanup EXIT + +task 1 "/srv/devs: group devs, writable by members, no deleting others' files" +need "/srv/devs belongs to group devs" '[ "$(stat -c %G $D)" = devs ]' +need "/srv/devs has the setgid bit" '[ -g $D ]' +need "/srv/devs has the sticky bit" '[ -k $D ]' +need "/srv/devs has a default ACL entry group:devs:rw-" 'getfacl -dpc $D 2>/dev/null | grep -Eq "^(default:)?group:devs:rw[x-]$"' +need "a file alice creates there belongs to group devs" 'runuser -u alice -- sh -c "echo a > $F" && [ "$(stat -c %G $F)" = devs ]' +need "bob can append to alice's file" 'runuser -u bob -- sh -c "echo x >> $F" && grep -qx x $F' +need "bob can't delete alice's file" '! runuser -u bob -- rm -f $F 2>/dev/null; [ -e $F ]' + +task 2 "Only bob (and root) can read /srv/secret/f.txt" +need "/srv/secret/f.txt exists" test -f /srv/secret/f.txt +need "/srv/secret has mode 700 for its owner, root" '[ "$(stat -c %U /srv/secret)" = root ] && [ "$(stat -c %a /srv/secret | cut -c1)" = 7 ]' +need "bob can read /srv/secret/f.txt" 'runuser -u bob -- cat /srv/secret/f.txt' +need "alice can't read it" '! runuser -u alice -- cat /srv/secret/f.txt' +need "carol can't read it" '! runuser -u carol -- cat /srv/secret/f.txt' +need "nobody else can read it" '! runuser -u nobody -- cat /srv/secret/f.txt' + +task 3 "chmod's effect on the ACL mask noted, then bob's access back" +need "/root/lab-answers/3 shows the effective permissions after chmod 700 (---)" '[[ "$(answer 3)" =~ ^(#?effective:)?---$ ]]' +need "bob still has a user ACL entry with r-x on /srv/reports" 'getfacl -pc /srv/reports 2>/dev/null | grep -q "^user:bob:r-x"' +need "the mask allows r-x again" 'getfacl -pc /srv/reports 2>/dev/null | grep -Eq "^mask::r-x"' +need "bob can list /srv/reports" 'runuser -u bob -- ls /srv/reports' diff --git a/labs/rhcsa/6-5-permission-problems/lab.conf b/labs/rhcsa/6-5-permission-problems/lab.conf new file mode 100644 index 0000000..2be6165 --- /dev/null +++ b/labs/rhcsa/6-5-permission-problems/lab.conf @@ -0,0 +1,3 @@ +TITLE="6.5 Fixing file permission problems" +CHAPTER=/rhcsa/6-file-systems/6-5-permission-problems/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/6-5-permission-problems/reset.sh b/labs/rhcsa/6-5-permission-problems/reset.sh new file mode 100644 index 0000000..31e992e --- /dev/null +++ b/labs/rhcsa/6-5-permission-problems/reset.sh @@ -0,0 +1,4 @@ +rm -rf /srv/devs /srv/secret /srv/reports +users_remove +[ -n "$(state_get made-devs)" ] && groupdel devs 2>/dev/null +true diff --git a/labs/rhcsa/6-5-permission-problems/setup.sh b/labs/rhcsa/6-5-permission-problems/setup.sh new file mode 100644 index 0000000..6c82650 --- /dev/null +++ b/labs/rhcsa/6-5-permission-problems/setup.sh @@ -0,0 +1,14 @@ +for d in /srv/devs /srv/secret /srv/reports; do + [ -e "$d" ] && refuse "$d already exists; remove it and start again" +done +pkg_installed acl || pkg_install acl +getent group devs >/dev/null || { groupadd devs; state_set made-devs 1; } +user_add alice -G devs +user_add bob -G devs +user_add carol +mkdir -p /srv/reports +chmod 750 /srv/reports +echo 'Quarterly numbers' >/srv/reports/q3.txt +chmod 640 /srv/reports/q3.txt +setfacl -m u:bob:rx /srv/reports +setfacl -m u:bob:r /srv/reports/q3.txt diff --git a/labs/rhcsa/7-1-scheduling-tasks/README.md b/labs/rhcsa/7-1-scheduling-tasks/README.md new file mode 100644 index 0000000..db6fbc9 --- /dev/null +++ b/labs/rhcsa/7-1-scheduling-tasks/README.md @@ -0,0 +1,31 @@ +# RHCSA 7.1: Scheduling tasks: at, cron and systemd timers + +A crontab for another user, a systemd timer that runs every 15 minutes, +and one-off jobs with `at`. + +## Before you start + +The lab creates user `alice` (password `Lab-Pass-1`) with an empty +crontab, makes sure cron is running, and removes `/tmp/at-ran`. `at` isn't +set up; installing it and starting its service is part of task 3. + +## Tasks + +1. As root, schedule a job for user `alice` that runs `/usr/bin/logger + hello` every weekday at 14:30. +2. Make a systemd timer that runs a script every 15 minutes and survives a + reboot. After a reboot, `systemctl list-timers` lists it with a next run + at most 15 minutes away; wait until it has fired once. +3. Use `at` to create a file `/tmp/at-ran` two minutes from now. Then + schedule another job and cancel it. + +## Hints + +- `crontab -u alice -e`, `crontab -l -u alice`; fields are minute, hour, + day of month, month, day of week. +- A script (`chmod +x`), a `.service` with `Type=oneshot` and `ExecStart=`, + and a `.timer` with `OnCalendar=*:0/15` (or `OnUnitActiveSec=15min` + with `OnBootSec=`) in `/etc/systemd/system/`; `systemctl daemon-reload`, + `systemctl enable --now NAME.timer`; `journalctl -u NAME.service`. +- `dnf install at`, `systemctl enable --now atd`; `at now + 2 minutes`, + `atq`, `atrm`. diff --git a/labs/rhcsa/7-1-scheduling-tasks/check.sh b/labs/rhcsa/7-1-scheduling-tasks/check.sh new file mode 100644 index 0000000..3a6bbea --- /dev/null +++ b/labs/rhcsa/7-1-scheduling-tasks/check.sh @@ -0,0 +1,60 @@ +START=$(state_get start) +cronline='^[[:space:]]*30[[:space:]]+14[[:space:]]+\*[[:space:]]+\*[[:space:]]+(1-5|mon-fri|Mon-Fri|MON-FRI|1,2,3,4,5)[[:space:]]+/usr/bin/logger[[:space:]]+hello[[:space:]]*$' + +# Timers made since the lab started, enabled and running, as NAME.timer. +new_timers() { + local f + for f in /etc/systemd/system/*.timer; do + [ -f "$f" ] || continue + grep -qxF "$f" "$LAB_STATE/timers-before" 2>/dev/null && continue + f=${f##*/} + enabled "$f" && active "$f" && echo "$f" + done +} +every15() { + systemctl show -p TimersCalendar -p TimersMonotonic "$1" | + grep -Eq 'OnCalendar=\*-\*-\* \*:00/15:00|OnCalendar=\*-\*-\* \*:00,15,30,45:00|OnUnit(Active|Inactive)USec=15min ;' +} +# Seconds until the timer's next run. +next_in() { + systemctl list-timers --all -o json 2>/dev/null | python3 -c ' +import json, sys, time +for t in json.load(sys.stdin): + if t["unit"] == sys.argv[1] and t.get("next"): + print(int(t["next"] / 1e6 - time.time())); break' "$1" +} +svc_of() { systemctl show -p Unit --value "$1"; } +ran() { journalctl -q -u "$(svc_of "$1")" --since "@$START" 2>/dev/null | grep -q .; } +# The first new timer that passes every test in turn. +pick() { + local t + for t in $(new_timers); do + every15 "$t" || continue + [ "$1" = interval ] && { echo "$t"; return; } + n=$(next_in "$t"); [ -n "$n" ] && [ "$n" -le 905 ] || continue + [ "$1" = next ] && { echo "$t"; return; } + ran "$t" && { echo "$t"; return; } + done + return 1 +} + +task 1 "alice's crontab runs /usr/bin/logger hello at 14:30 on weekdays" +need "crond is running" active crond +need "alice has a crontab" 'crontab -u alice -l' +need "alice's crontab has 30 14 * * 1-5 /usr/bin/logger hello" 'crontab -u alice -l | grep -Eq "$cronline"' + +task 2 "A timer runs a script every 15 minutes, also after a reboot" +need "a new timer in /etc/systemd/system is enabled and active" '[ -n "$(new_timers)" ]' +need "the timer runs every 15 minutes" 'pick interval' +need "the timer's next run is at most 15 minutes away" 'pick next' +need "its service has run since the lab started (journalctl -u)" 'pick ran' +need "the service runs a script that exists" 'svc=$(svc_of "$(pick ran)"); p=$(systemctl show -p ExecStart --value "$svc" | sed -n "s/.*path=\([^ ;]*\).*/\1/p" | head -n1); [ -x "$p" ]' + +task 3 "/tmp/at-ran made by an at job; a second job canceled" +need "atd is running" active atd +need "/tmp/at-ran exists" test -e /tmp/at-ran +need "/tmp/at-ran was made after the lab started" '[ "$(stat -c %Y /tmp/at-ran)" -ge "$START" ]' +need "at least two at jobs were made since the lab started" ' + seq=$(head -n1 /var/spool/at/.SEQ 2>/dev/null) + [ -n "$seq" ] && [ $((16#$seq - $(state_get at-seq))) -ge 2 ]' +need "no at job is left in the queue (atq is empty)" '[ -z "$(atq 2>/dev/null)" ]' diff --git a/labs/rhcsa/7-1-scheduling-tasks/lab.conf b/labs/rhcsa/7-1-scheduling-tasks/lab.conf new file mode 100644 index 0000000..b8c10bb --- /dev/null +++ b/labs/rhcsa/7-1-scheduling-tasks/lab.conf @@ -0,0 +1,3 @@ +TITLE="7.1 Scheduling tasks: at, cron and systemd timers" +CHAPTER=/rhcsa/7-deploy-and-maintain/7-1-scheduling-tasks/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/7-1-scheduling-tasks/reset.sh b/labs/rhcsa/7-1-scheduling-tasks/reset.sh new file mode 100644 index 0000000..003917f --- /dev/null +++ b/labs/rhcsa/7-1-scheduling-tasks/reset.sh @@ -0,0 +1,14 @@ +crontab -u alice -r >/dev/null 2>&1 +for f in /etc/systemd/system/*.timer; do + [ -f "$f" ] || continue + grep -qxF "$f" "$LAB_STATE/timers-before" 2>/dev/null && continue + t=${f##*/} + svc=$(systemctl show -p Unit --value "$t" 2>/dev/null) + systemctl disable --now --quiet "$t" 2>/dev/null + rm -f "$f" + [ -n "$svc" ] && [ -f "/etc/systemd/system/$svc" ] && rm -f "/etc/systemd/system/$svc" +done +systemctl daemon-reload +command -v atq >/dev/null && for j in $(atq | awk '{print $1}'); do atrm "$j"; done +rm -f /tmp/at-ran +true diff --git a/labs/rhcsa/7-1-scheduling-tasks/setup.sh b/labs/rhcsa/7-1-scheduling-tasks/setup.sh new file mode 100644 index 0000000..25b1402 --- /dev/null +++ b/labs/rhcsa/7-1-scheduling-tasks/setup.sh @@ -0,0 +1,10 @@ +user_add alice +pkg_installed cronie || pkg_install cronie +systemctl enable --quiet --now crond +crontab -u alice -r >/dev/null 2>&1 || true +rm -f /tmp/at-ran +state_set start "$(date +%s)" +# How many at jobs were made before the lab. +seq=$(cat /var/spool/at/.SEQ 2>/dev/null | head -n1) +state_set at-seq "$((16#${seq:-0}))" +ls /etc/systemd/system/*.timer >"$LAB_STATE/timers-before" 2>/dev/null || true diff --git a/labs/rhcsa/7-2-services-at-boot/README.md b/labs/rhcsa/7-2-services-at-boot/README.md new file mode 100644 index 0000000..5ed0278 --- /dev/null +++ b/labs/rhcsa/7-2-services-at-boot/README.md @@ -0,0 +1,25 @@ +# RHCSA 7.2: Services at boot + +Starting a service now and at every boot, masking it so nobody can start +it, and undoing that. + +## Before you start + +The tasks change the same service one after another, so the lab runs a +small service, `le-lab-watch`, that notes httpd's state every second +(and at every boot) for the check. Do the tasks in order. + +## Tasks + +1. Install `httpd`, and make it start now and at every boot. Reboot, and + see that it's enabled and running. +2. Stop `httpd` and make sure nobody can start it by hand. +3. Undo that, leaving `httpd` stopped and disabled. + +## Hints + +- `dnf install`, `systemctl enable --now`; `systemctl is-enabled`, + `systemctl is-active`. +- `systemctl stop`, `systemctl mask`; `systemctl start httpd` then fails + with "Unit httpd.service is masked." +- `systemctl unmask`; check `is-enabled` and `is-active` again. diff --git a/labs/rhcsa/7-2-services-at-boot/check.sh b/labs/rhcsa/7-2-services-at-boot/check.sh new file mode 100644 index 0000000..f743f6c --- /dev/null +++ b/labs/rhcsa/7-2-services-at-boot/check.sh @@ -0,0 +1,14 @@ +LOG=$LAB_STATE/watch.log SETUP_BOOT=$(state_get setup-boot) +seen() { grep -Eq "$1" "$LOG" 2>/dev/null; } + +task 1 "httpd installed, enabled and running after a reboot" +need "httpd is installed" pkg_installed httpd +need "httpd ran enabled after a reboot since the lab started" 'awk -v b="$SETUP_BOOT" "\$1 != b && \$2 == \"enabled\" && \$3 == \"active\" {f = 1} END {exit !f}" "$LOG"' + +task 2 "httpd stopped and masked" +need "httpd was masked while stopped" 'seen " masked (inactive|failed)$"' + +task 3 "httpd unmasked, stopped and disabled" +need "httpd was masked before (task 2)" 'seen " masked "' +need "systemctl is-enabled httpd prints disabled" '[ "$(systemctl is-enabled httpd 2>/dev/null)" = disabled ]' +need "systemctl is-active httpd prints inactive" '[ "$(systemctl is-active httpd 2>/dev/null)" = inactive ]' diff --git a/labs/rhcsa/7-2-services-at-boot/lab.conf b/labs/rhcsa/7-2-services-at-boot/lab.conf new file mode 100644 index 0000000..acbb592 --- /dev/null +++ b/labs/rhcsa/7-2-services-at-boot/lab.conf @@ -0,0 +1,3 @@ +TITLE="7.2 Services at boot" +CHAPTER=/rhcsa/7-deploy-and-maintain/7-2-services-at-boot/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/7-2-services-at-boot/reset.sh b/labs/rhcsa/7-2-services-at-boot/reset.sh new file mode 100644 index 0000000..9a74580 --- /dev/null +++ b/labs/rhcsa/7-2-services-at-boot/reset.sh @@ -0,0 +1,11 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl unmask --quiet httpd 2>/dev/null +systemctl disable --now --quiet httpd 2>/dev/null +systemctl daemon-reload +if [ -z "$(state_get httpd-before)" ] && pkg_installed httpd; then + say "Removing httpd..." + dnf -y -q remove httpd >/dev/null 2>&1 +fi +true diff --git a/labs/rhcsa/7-2-services-at-boot/setup.sh b/labs/rhcsa/7-2-services-at-boot/setup.sh new file mode 100644 index 0000000..ecd2149 --- /dev/null +++ b/labs/rhcsa/7-2-services-at-boot/setup.sh @@ -0,0 +1,29 @@ +pkg_installed httpd && state_set httpd-before 1 +cat /proc/sys/kernel/random/boot_id >"$LAB_STATE/setup-boot" +# Note httpd's state every second, and at every boot. +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints, with the boot it happened in. +log=$1 prev='' +boot=$(cat /proc/sys/kernel/random/boot_id) +snap() { echo "$(systemctl is-enabled httpd 2>/dev/null || true) $(systemctl is-active httpd 2>/dev/null || true)"; } +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s %s\n' "$boot" "$cur" >>"$log"; prev=$cur; } + sleep 1 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +d=$(state_get default-before) +[ -n "$d" ] && [ "$(systemctl get-default)" != "$d" ] && systemctl set-default "$d" >/dev/null 2>&1 +true diff --git a/labs/rhcsa/7-3-default-target/setup.sh b/labs/rhcsa/7-3-default-target/setup.sh new file mode 100644 index 0000000..930167e --- /dev/null +++ b/labs/rhcsa/7-3-default-target/setup.sh @@ -0,0 +1,30 @@ +state_set default-before "$(systemctl get-default)" +cat /proc/sys/kernel/random/boot_id >"$LAB_STATE/setup-boot" +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints, with the boot it happened in. +log=$1 prev='' +boot=$(cat /proc/sys/kernel/random/boot_id) +snap() { echo "$(readlink -f /etc/systemd/system/default.target)"; } +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s %s\n' "$boot" "$cur" >>"$log"; prev=$cur; } + sleep 1 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null | cut -d, -f3; } +synced() { + local i + for i in $(seq 15); do + chronyc -n sources 2>/dev/null | grep -Eq '^\^\*[[:space:]]+10\.99\.0\.2[[:space:]]' && return 0 + sleep 2 + done + return 1 +} +# server, pool and peer lines in chrony's configuration. +conf_sources() { + { cat /etc/chrony.conf; ls /etc/chrony.d/*.conf 2>/dev/null | xargs -r cat; } | + grep -E '^[[:space:]]*(server|pool|peer)[[:space:]]' | awk '{print $2}' +} + +task 1 "chronyd uses only ntp1 (10.99.0.2), synchronized" +need "chronyd is running" active chronyd +need "chronyd starts at boot" enabled chronyd +need "chrony's configuration names ntp1 (10.99.0.2)" 'conf_sources | grep -Eqx "10\.99\.0\.2|ntp1(\.lab\.example)?"' +need "chrony's configuration names no other server or pool" '! conf_sources | grep -Evqx "10\.99\.0\.2|ntp1(\.lab\.example)?"' +need "chronyc sources lists only 10.99.0.2" '[ "$(sources | sort -u)" = 10.99.0.2 ]' +need "10.99.0.2 is marked ^* (synchronized)" synced + +task 2 "Time zone America/Phoenix" +need "timedatectl shows America/Phoenix" '[ "$(timedatectl show -p Timezone --value)" = America/Phoenix ]' +need "/etc/localtime points at America/Phoenix" 'readlink /etc/localtime | grep -q "America/Phoenix$"' diff --git a/labs/rhcsa/7-4-time-clients/lab.conf b/labs/rhcsa/7-4-time-clients/lab.conf new file mode 100644 index 0000000..2549747 --- /dev/null +++ b/labs/rhcsa/7-4-time-clients/lab.conf @@ -0,0 +1,3 @@ +TITLE="7.4 Time service clients" +CHAPTER=/rhcsa/7-deploy-and-maintain/7-4-time-clients/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/7-4-time-clients/reset.sh b/labs/rhcsa/7-4-time-clients/reset.sh new file mode 100644 index 0000000..d31095c --- /dev/null +++ b/labs/rhcsa/7-4-time-clients/reset.sh @@ -0,0 +1,10 @@ +if [ -f "$LAB_STATE/chrony.conf" ]; then + cp -a "$LAB_STATE/chrony.conf" /etc/chrony.conf + if [ -n "$(state_get chronyd-enabled)" ]; then systemctl restart chronyd; fi +fi +systemctl disable --now --quiet le-lab-ntp1.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-ntp1.service /etc/le-lab/ntp1.conf +systemctl daemon-reload +tz=$(state_get timezone) +[ -n "$tz" ] && timedatectl set-timezone "$tz" +true diff --git a/labs/rhcsa/7-4-time-clients/setup.sh b/labs/rhcsa/7-4-time-clients/setup.sh new file mode 100644 index 0000000..13f984c --- /dev/null +++ b/labs/rhcsa/7-4-time-clients/setup.sh @@ -0,0 +1,35 @@ +pkg_installed chrony || pkg_install chrony +cp -a /etc/chrony.conf "$LAB_STATE/chrony.conf" +enabled chronyd && state_set chronyd-enabled 1 +tz=$(timedatectl show -p Timezone --value) +state_set timezone "$tz" +[ "$tz" != America/Phoenix ] || timedatectl set-timezone UTC +peer_up +hosts_add "$LE_PEER_ADDR ntp1.lab.example ntp1" +# The time server: chronyd in the peer, serving the VM's own clock and +# never changing it. +mkdir -p /etc/le-lab +cat >/etc/le-lab/ntp1.conf <<'X' +# The lab's time server, ntp1. +local stratum 8 +allow 10.99.0.0/24 +bindaddress 10.99.0.2 +cmdport 0 +bindcmdaddress / +pidfile /run/chrony/ntp1.pid +X +cat >/etc/systemd/system/le-lab-ntp1.service </dev/null | awk -v h="${H:-0}" '$1 ~ /^[0-9]+$/ && $1 > h {print $1}'; } + +task 1 "mariadb installed from a downloaded file, with its dependencies" +need "mariadb is installed" 'rpm -q mariadb' +need "mariadb came from a file (@commandline)" 'dnf -q info --installed mariadb 2>/dev/null | grep -Eq "^From repo[[:space:]]*:[[:space:]]*@?commandline"' +need "its dependencies came along (mariadb-common is installed)" 'rpm -q mariadb-common' + +task 2 "All available updates applied" +need "a transaction since the lab started upgraded packages" 'for t in $(new_tx); do dnf -q history info "$t" 2>/dev/null | grep -Eq "^[[:space:]]+Upgrade(d)?[[:space:]]" && exit 0; done; exit 1' +need "dnf check-update lists nothing (exit status 0)" 'dnf -q check-update' diff --git a/labs/rhcsa/7-5-installing-and-updating/lab.conf b/labs/rhcsa/7-5-installing-and-updating/lab.conf new file mode 100644 index 0000000..40ffd1e --- /dev/null +++ b/labs/rhcsa/7-5-installing-and-updating/lab.conf @@ -0,0 +1,3 @@ +TITLE="7.5 Installing and updating software" +CHAPTER=/rhcsa/7-deploy-and-maintain/7-5-installing-and-updating/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/7-5-installing-and-updating/reset.sh b/labs/rhcsa/7-5-installing-and-updating/reset.sh new file mode 100644 index 0000000..9ecd350 --- /dev/null +++ b/labs/rhcsa/7-5-installing-and-updating/reset.sh @@ -0,0 +1,5 @@ +if pkg_installed mariadb; then + say "Removing mariadb..." + dnf -y -q remove mariadb >/dev/null 2>&1 +fi +true diff --git a/labs/rhcsa/7-5-installing-and-updating/setup.sh b/labs/rhcsa/7-5-installing-and-updating/setup.sh new file mode 100644 index 0000000..028b21c --- /dev/null +++ b/labs/rhcsa/7-5-installing-and-updating/setup.sh @@ -0,0 +1,16 @@ +pkg_installed mariadb && refuse "mariadb is already installed; remove it (dnf remove mariadb) and start again" +say "${C_DIM}Checking for updates...${C_0}" +rc=0 +dnf -q check-update >/dev/null 2>&1 || rc=$? +[ "$rc" = 0 ] || [ "$rc" = 100 ] || refuse "dnf check-update failed; the lab needs working repositories" +if [ "$rc" = 0 ]; then + # Nothing to update: put an older version of a small package in place. + for p in tzdata tar vim-minimal; do + if dnf -y -q downgrade "$p" >/dev/null 2>&1; then + state_set downgraded "$p" + break + fi + done + [ -n "$(state_get downgraded)" ] || refuse "the system is up to date and no package could be downgraded to leave an update to apply" +fi +state_set history "$(dnf -q history list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ {print $1; exit}')" diff --git a/labs/rhcsa/7-6-bootloader/README.md b/labs/rhcsa/7-6-bootloader/README.md new file mode 100644 index 0000000..433763b --- /dev/null +++ b/labs/rhcsa/7-6-bootloader/README.md @@ -0,0 +1,30 @@ +# RHCSA 7.6: The boot loader + +Adding and removing a kernel argument on every installed kernel, and the +GRUB menu timeout. + +## Before you start + +These tasks change the boot loader, so take a snapshot of the VM first. +Tasks 1 and 2 add an argument and take it away again, so the lab runs a +small service, `le-lab-boot-note`, that notes the kernel command line at +every boot for the check. The lab notes your GRUB timeout and puts it back +when you reset it. Do the tasks in order. + +## Tasks + +1. Add the kernel argument `audit_backlog_limit=8192` to every installed + kernel, reboot, and confirm the running kernel got it. +2. Remove that argument again, from every kernel and from + `/etc/default/grub`. +3. Change the GRUB menu timeout to 10 seconds. + +## Hints + +- `grubby --update-kernel=ALL --args=...`, `grubby --info=ALL | grep + ^args`; `cat /proc/cmdline` after the reboot. +- `grubby --update-kernel=ALL --remove-args=...`; `grep CMDLINE + /etc/default/grub`. +- `GRUB_TIMEOUT` in `/etc/default/grub`, then + `grub2-mkconfig -o /boot/grub2/grub.cfg`; `grep -m1 'set timeout=' + /boot/grub2/grub.cfg`. diff --git a/labs/rhcsa/7-6-bootloader/check.sh b/labs/rhcsa/7-6-bootloader/check.sh new file mode 100644 index 0000000..0e1aadf --- /dev/null +++ b/labs/rhcsa/7-6-bootloader/check.sh @@ -0,0 +1,14 @@ +SETUP_BOOT=$(state_get setup-boot) +added() { awk -v b="$SETUP_BOOT" '$1 != b && $2 == "cmdline=yes" && $3 == "entries=all" {f = 1} END {exit !f}' "$LAB_STATE/boots" 2>/dev/null; } + +task 1 "audit_backlog_limit=8192 on every kernel, and in use after a reboot" +need "a boot since the lab started ran with audit_backlog_limit=8192, with every kernel entry having it" added + +task 2 "audit_backlog_limit=8192 removed again" +need "it was added first (task 1)" added +need "no kernel entry has it (grubby --info=ALL)" '! grubby --info=ALL 2>/dev/null | grep -q audit_backlog_limit=' +need "/etc/default/grub doesn't have it" '! grep -q audit_backlog_limit= /etc/default/grub' + +task 3 "GRUB menu timeout 10 seconds" +need "GRUB_TIMEOUT is 10 in /etc/default/grub" 'grep -Eq "^GRUB_TIMEOUT=\"?10\"?$" /etc/default/grub' +need "grub.cfg has set timeout=10 (grub2-mkconfig)" 'grep -m1 "set timeout=" /boot/grub2/grub.cfg | grep -q "set timeout=10$"' diff --git a/labs/rhcsa/7-6-bootloader/lab.conf b/labs/rhcsa/7-6-bootloader/lab.conf new file mode 100644 index 0000000..36ba32a --- /dev/null +++ b/labs/rhcsa/7-6-bootloader/lab.conf @@ -0,0 +1,3 @@ +TITLE="7.6 The boot loader" +CHAPTER=/rhcsa/7-deploy-and-maintain/7-6-bootloader/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/7-6-bootloader/reset.sh b/labs/rhcsa/7-6-bootloader/reset.sh new file mode 100644 index 0000000..6439c03 --- /dev/null +++ b/labs/rhcsa/7-6-bootloader/reset.sh @@ -0,0 +1,12 @@ +systemctl disable --quiet le-lab-boot-note.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-boot-note.service +systemctl daemon-reload +grubby --info=ALL 2>/dev/null | grep -q audit_backlog_limit= && + grubby --update-kernel=ALL --remove-args=audit_backlog_limit=8192 >/dev/null 2>&1 +sed -i -E '/^GRUB_CMDLINE_LINUX/ s/ ?audit_backlog_limit=8192//' /etc/default/grub +t=$(state_get timeout) +if [ -n "$t" ] && ! grep -qx "GRUB_TIMEOUT=$t" /etc/default/grub || [ -n "$(state_get remade)" ]; then + sed -i "s/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=$t/" /etc/default/grub + grub2-mkconfig -o /boot/grub2/grub.cfg >/dev/null 2>&1 +fi +true diff --git a/labs/rhcsa/7-6-bootloader/setup.sh b/labs/rhcsa/7-6-bootloader/setup.sh new file mode 100644 index 0000000..e8c8764 --- /dev/null +++ b/labs/rhcsa/7-6-bootloader/setup.sh @@ -0,0 +1,30 @@ +command -v grubby >/dev/null || refuse "grubby isn't installed; this lab needs a RHEL-style boot loader" +[ -f /boot/grub2/grub.cfg ] && [ -f /etc/default/grub ] || refuse "no /boot/grub2/grub.cfg or /etc/default/grub; this lab needs GRUB 2" +if grubby --info=ALL 2>/dev/null | grep -q audit_backlog_limit= || grep -q audit_backlog_limit= /etc/default/grub; then + refuse "audit_backlog_limit is already set; remove it from the kernels and /etc/default/grub, and start again" +fi +cp -a /etc/default/grub "$LAB_STATE/default-grub" +t=$(sed -n 's/^GRUB_TIMEOUT=//p' /etc/default/grub | tr -d '"') +state_set timeout "$t" +cat /proc/sys/kernel/random/boot_id >"$LAB_STATE/setup-boot" +if [ "$t" = 10 ] || grep -m1 'set timeout=' /boot/grub2/grub.cfg | grep -q 'timeout=10$'; then + # Already 10 seconds: start from 5. + sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=5/' /etc/default/grub + grub2-mkconfig -o /boot/grub2/grub.cfg >/dev/null 2>&1 + state_set remade 1 +fi +# Note the command line, and whether every kernel entry has the argument, +# at every boot. +cat >/etc/systemd/system/le-lab-boot-note.service <>$LAB_STATE/boots' + +[Install] +WantedBy=multi-user.target +X +systemctl daemon-reload +systemctl enable --quiet le-lab-boot-note.service diff --git a/labs/rhcsa/8-1-ip-addresses/README.md b/labs/rhcsa/8-1-ip-addresses/README.md new file mode 100644 index 0000000..ec60a5d --- /dev/null +++ b/labs/rhcsa/8-1-ip-addresses/README.md @@ -0,0 +1,40 @@ +# RHCSA 8.1: IPv4 and IPv6 addresses + +Static IPv4 and IPv6 addresses, a gateway and a DNS server on a spare +interface with `nmcli`, a second address, and the same again with `nmtui`. + +## Before you start + +The lab gives this VM a spare network interface, `eth1`, cabled to a small +lab network, `192.168.50.0/24`, where a machine answers at `192.168.50.1` +(and `fd00:50::1`). It comes back at every boot. `eth1` has no profile +yet. Your SSH connection doesn't use it, so leave the main interface +alone. + +A gateway and DNS server on `eth1` rank below the main interface's (the +lab sets that up in `/etc/NetworkManager/conf.d/`), so the VM keeps its +own way out. Profiles come and go in task 3, so the lab runs a small +service, `le-lab-watch`, that notes the profiles for `eth1` for the check. + +## Tasks + +1. Give `eth1` the static IPv4 address `192.168.50.10/24`, gateway + `192.168.50.1`, DNS server `192.168.50.1`, and the IPv6 address + `fd00:50::10/64`, all persistent, in a profile made with `nmcli`. +2. Add a second IPv4 address, `192.168.50.11/24`, to the same profile + without removing the first. +3. Delete that profile with `nmcli connection delete`, and do task 1 again + with `nmtui` on a fresh profile. + +## Hints + +- `nmcli connection add type ethernet ifname eth1 con-name eth1 + ipv4.method manual ipv4.addresses ... ipv4.gateway ... ipv4.dns ... + ipv6.method manual ipv6.addresses ...`. +- `nmcli connection modify NAME +ipv4.addresses ...`, then + `nmcli connection up NAME`; `nmcli -g ipv4.addresses connection show + NAME`. +- In `nmtui`, "Edit a connection", "Add", "Ethernet", device `eth1`; set + IPv4 and IPv6 to "Manual". +- `ip -br addr show eth1`, `ip route`, `cat /etc/resolv.conf`, + `ping 192.168.50.1`. diff --git a/labs/rhcsa/8-1-ip-addresses/check.sh b/labs/rhcsa/8-1-ip-addresses/check.sh new file mode 100644 index 0000000..162e7a3 --- /dev/null +++ b/labs/rhcsa/8-1-ip-addresses/check.sh @@ -0,0 +1,31 @@ +LOG=$LAB_STATE/watch.log +# The profile active on eth1, and its settings. +CON=$(nmcli -g GENERAL.CON-UUID device show eth1 2>/dev/null) +prof() { nmcli -g "$1" connection show "$CON" 2>/dev/null | tr -d '\\'; } +# UUIDs of every eth1 profile seen, in the order they appeared. +seen_uuids() { tr ' ' '\n' <"$LOG" 2>/dev/null | sed -n 's/=.*//p' | awk 'NF && !s[$0]++'; } +live() { ip -o addr show dev eth1 | grep -F " $1 " | grep -vq dynamic; } + +task1_needs() { + need "a profile is active on eth1" '[ -n "$CON" ]' + need "the profile is saved in /etc (persistent)" 'nmcli -t -f UUID,FILENAME connection show | grep "^$CON:" | grep -q ":/etc/"' + need "the profile starts at boot (autoconnect)" '[ "$(prof connection.autoconnect)" = yes ]' + need "the profile's IPv4 method is manual with 192.168.50.10/24" '[ "$(prof ipv4.method)" = manual ] && prof ipv4.addresses | grep -q "192\.168\.50\.10/24"' + need "the profile has gateway and DNS server 192.168.50.1" '[ "$(prof ipv4.gateway)" = 192.168.50.1 ] && prof ipv4.dns | grep -qw "192\.168\.50\.1"' + need "the profile has fd00:50::10/64" 'prof ipv6.addresses | grep -q "fd00:50::10/64"' + need "eth1 has 192.168.50.10/24 and fd00:50::10/64" 'live 192.168.50.10/24 && live fd00:50::10/64' + need "ip route shows default via 192.168.50.1 dev eth1" 'ip -4 route show default | grep -q "via 192\.168\.50\.1 dev eth1"' + need "/etc/resolv.conf lists nameserver 192.168.50.1" 'grep -q "^nameserver 192\.168\.50\.1$" /etc/resolv.conf' + need "192.168.50.1 answers ping" 'ping -c1 -W2 192.168.50.1' +} + +task 1 "eth1: 192.168.50.10/24, gateway and DNS 192.168.50.1, fd00:50::10/64" +task1_needs + +task 2 "A second address, 192.168.50.11/24, on the same profile" +need "a profile for eth1 had both 192.168.50.10/24 and 192.168.50.11/24" 'tr " " "\n" <"$LOG" | grep "192\.168\.50\.10/24" | grep -q "192\.168\.50\.11/24"' + +task 3 "The profile deleted and made again with nmtui" +need "a second, fresh profile for eth1 was made" '[ "$(seen_uuids | wc -l)" -ge 2 ]' +need "the first profile is gone" '! nmcli -g UUID connection show | grep -qx "$(seen_uuids | head -n1)"' +task1_needs diff --git a/labs/rhcsa/8-1-ip-addresses/lab.conf b/labs/rhcsa/8-1-ip-addresses/lab.conf new file mode 100644 index 0000000..79cdd1c --- /dev/null +++ b/labs/rhcsa/8-1-ip-addresses/lab.conf @@ -0,0 +1,3 @@ +TITLE="8.1 IPv4 and IPv6 addresses" +CHAPTER=/rhcsa/8-networking/8-1-ip-addresses/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/8-1-ip-addresses/reset.sh b/labs/rhcsa/8-1-ip-addresses/reset.sh new file mode 100644 index 0000000..40ca12a --- /dev/null +++ b/labs/rhcsa/8-1-ip-addresses/reset.sh @@ -0,0 +1,16 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +# Profiles on eth1, then the spare network itself. +nmcli -t -f UUID connection show 2>/dev/null | while read -r uuid; do + [ "$(nmcli -g connection.interface-name connection show "$uuid" 2>/dev/null)" = eth1 ] && + nmcli connection delete "$uuid" >/dev/null 2>&1 +done +rm -f /etc/NetworkManager/conf.d/91-le-lab-spare.conf +systemctl reload NetworkManager +systemctl disable --now --quiet le-lab-spare-net.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-spare-net.service /usr/local/lib/le-lab/spare-net.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +ip link del eth1 2>/dev/null +ip netns del le-net50 2>/dev/null +true diff --git a/labs/rhcsa/8-1-ip-addresses/setup.sh b/labs/rhcsa/8-1-ip-addresses/setup.sh new file mode 100644 index 0000000..cdfe3b7 --- /dev/null +++ b/labs/rhcsa/8-1-ip-addresses/setup.sh @@ -0,0 +1,91 @@ +# The spare interface: eth1, cabled to a small lab network, +# 192.168.50.0/24, where a machine answers at 192.168.50.1 (and +# fd00:50::1). +ip link show eth1 >/dev/null 2>&1 && refuse "this VM already has an eth1; the lab adds its own spare eth1" +command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager || refuse "this lab needs NetworkManager running" +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/spare-net.sh <<'X' +#!/bin/sh +# The lab's spare network: eth1 here, the gateway 192.168.50.1 in the +# le-net50 namespace. Run at every boot. +ip netns list | grep -qw le-net50 || ip netns add le-net50 +ip -n le-net50 link set lo up +ip link show eth1 >/dev/null 2>&1 || ip link add eth1 address 52:54:00:4c:45:01 type veth peer name gw netns le-net50 +ip -n le-net50 addr replace 192.168.50.1/24 dev gw +ip -n le-net50 addr replace fd00:50::1/64 dev gw nodad +ip -n le-net50 link set gw up +true +X +chmod 755 /usr/local/lib/le-lab/spare-net.sh +cat >/etc/systemd/system/le-lab-spare-net.service <<'X' +[Unit] +Description=Practice lab: a spare network interface, eth1 +Before=network-pre.target NetworkManager.service +Wants=network-pre.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/le-lab/spare-net.sh + +[Install] +WantedBy=multi-user.target +X +# NetworkManager manages eth1 but makes no profile for it on its own. A +# gateway and DNS server on eth1 rank below the main interface's, so the +# VM keeps its own way out. +cat >/etc/NetworkManager/conf.d/91-le-lab-spare.conf <<'X' +[main] +no-auto-default+=interface-name:eth1 + +[device-le-lab-spare] +match-device=interface-name:eth1 +managed=1 + +[connection-le-lab-spare] +match-device=interface-name:eth1 +ipv4.route-metric=500 +ipv6.route-metric=500 +ipv4.dns-priority=200 +ipv6.dns-priority=200 +X +systemctl reload NetworkManager +systemctl daemon-reload +systemctl enable --now --quiet le-lab-spare-net.service +for i in 1 2 3 4 5 6 7 8 9 10; do + nmcli -g GENERAL.STATE device show eth1 2>/dev/null | grep -qv unmanaged && break + sleep 1 +done + +# Note the profiles for eth1 and their IPv4 addresses (task 2 and 3). +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints. +log=$1 prev='' +snap() { + local u + for u in $(nmcli -g UUID connection show 2>/dev/null); do + [ "$(nmcli -g connection.interface-name connection show "$u" 2>/dev/null)" = eth1 ] || continue + printf '%s=%s ' "$u" "$(nmcli -g ipv4.addresses connection show "$u" 2>/dev/null | tr -d ' ')" + done +} +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s\n' "$cur" >>"$log"; prev=$cur; } + sleep 2 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null | while read -r uuid; do + [ "$(nmcli -g connection.interface-name connection show "$uuid" 2>/dev/null)" = eth1 ] && + nmcli connection delete "$uuid" >/dev/null 2>&1 +done +rm -f /etc/NetworkManager/conf.d/91-le-lab-spare.conf +systemctl reload NetworkManager +systemctl disable --now --quiet le-lab-spare-net.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-spare-net.service /usr/local/lib/le-lab/spare-net.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +ip link del eth1 2>/dev/null +ip netns del le-net50 2>/dev/null +old=$(state_get hostname) +[ -n "$old" ] && hostnamectl set-hostname "$old" +[ -f "$LAB_STATE/hosts" ] && cp "$LAB_STATE/hosts" /etc/hosts +true diff --git a/labs/rhcsa/8-2-hostname-resolution/setup.sh b/labs/rhcsa/8-2-hostname-resolution/setup.sh new file mode 100644 index 0000000..2a827c4 --- /dev/null +++ b/labs/rhcsa/8-2-hostname-resolution/setup.sh @@ -0,0 +1,68 @@ +# The spare interface: eth1, cabled to a small lab network, +# 192.168.50.0/24, where a machine answers at 192.168.50.1 (and +# fd00:50::1). +ip link show eth1 >/dev/null 2>&1 && refuse "this VM already has an eth1; the lab adds its own spare eth1" +command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager || refuse "this lab needs NetworkManager running" +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/spare-net.sh <<'X' +#!/bin/sh +# The lab's spare network: eth1 here, the gateway 192.168.50.1 in the +# le-net50 namespace. Run at every boot. +ip netns list | grep -qw le-net50 || ip netns add le-net50 +ip -n le-net50 link set lo up +ip link show eth1 >/dev/null 2>&1 || ip link add eth1 address 52:54:00:4c:45:01 type veth peer name gw netns le-net50 +ip -n le-net50 addr replace 192.168.50.1/24 dev gw +ip -n le-net50 addr replace fd00:50::1/64 dev gw nodad +ip -n le-net50 link set gw up +true +X +chmod 755 /usr/local/lib/le-lab/spare-net.sh +cat >/etc/systemd/system/le-lab-spare-net.service <<'X' +[Unit] +Description=Practice lab: a spare network interface, eth1 +Before=network-pre.target NetworkManager.service +Wants=network-pre.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/le-lab/spare-net.sh + +[Install] +WantedBy=multi-user.target +X +# NetworkManager manages eth1 but makes no profile for it on its own. A +# gateway and DNS server on eth1 rank below the main interface's, so the +# VM keeps its own way out. +cat >/etc/NetworkManager/conf.d/91-le-lab-spare.conf <<'X' +[main] +no-auto-default+=interface-name:eth1 + +[device-le-lab-spare] +match-device=interface-name:eth1 +managed=1 + +[connection-le-lab-spare] +match-device=interface-name:eth1 +ipv4.route-metric=500 +ipv6.route-metric=500 +ipv4.dns-priority=200 +ipv6.dns-priority=200 +X +systemctl reload NetworkManager +systemctl daemon-reload +systemctl enable --now --quiet le-lab-spare-net.service +for i in 1 2 3 4 5 6 7 8 9 10; do + nmcli -g GENERAL.STATE device show eth1 2>/dev/null | grep -qv unmanaged && break + sleep 1 +done +nmcli connection add type ethernet ifname eth1 con-name eth1 \ + ipv4.method manual ipv4.addresses 192.168.50.10/24 ipv6.method disabled >/dev/null +nmcli connection up eth1 >/dev/null 2>&1 || true + +# A fresh start for tasks 1 and 2. +state_set hostname "$(hostnamectl --static 2>/dev/null || hostname)" +[ "$(hostname)" != server1.example.com ] || hostnamectl set-hostname lab-vm +cp -a /etc/hosts "$LAB_STATE/hosts" +sed -i -E '/^[^#]*[[:space:]]web1(\.example\.com)?([[:space:]]|$)/d' /etc/hosts +true diff --git a/labs/rhcsa/8-3-network-at-boot/README.md b/labs/rhcsa/8-3-network-at-boot/README.md new file mode 100644 index 0000000..037825e --- /dev/null +++ b/labs/rhcsa/8-3-network-at-boot/README.md @@ -0,0 +1,32 @@ +# RHCSA 8.3: Network connections at boot + +Making a static profile come up at boot, and turning that off and on. + +## Before you start + +The lab gives this VM a spare network interface, `eth1`, cabled to a small +lab network, `192.168.50.0/24`, where a machine answers at `192.168.50.1`. +It comes back at every boot. It also adds a static profile for it, as in +chapter 8.1: `eth1-static`, with `192.168.50.10/24`, which doesn't come up +at boot and isn't up now. Your SSH connection doesn't use `eth1`, so leave +the main interface alone. + +Task 2 ends where task 1 does, so the lab runs a small service, +`le-lab-watch`, that notes the profile's state (and every boot) for the +check. + +## Tasks + +1. Make `eth1-static` come up automatically at boot. After a reboot, + `nmcli device` shows `eth1` connected with it, and `ip addr` shows its + address. +2. Turn autoconnect off for `eth1-static` and reboot: `nmcli -f + NAME,AUTOCONNECT,DEVICE connection show` shows it with `no` and no + device. Then turn it back on and bring it up. + +## Hints + +- `nmcli connection modify eth1-static connection.autoconnect yes`, + `nmcli connection up`. +- `nmcli device`, `ip -br addr show eth1`, `nmcli -f + NAME,AUTOCONNECT,DEVICE connection show`. diff --git a/labs/rhcsa/8-3-network-at-boot/check.sh b/labs/rhcsa/8-3-network-at-boot/check.sh new file mode 100644 index 0000000..a0c3697 --- /dev/null +++ b/labs/rhcsa/8-3-network-at-boot/check.sh @@ -0,0 +1,16 @@ +UUID=$(state_get uuid) SETUP_BOOT=$(state_get setup-boot) +prof() { nmcli -g "$1" connection show "$UUID" 2>/dev/null; } + +task1_needs() { + need "the profile eth1-static still exists" '[ -n "$(prof connection.id)" ]' + need "eth1-static comes up at boot (autoconnect yes)" '[ "$(prof connection.autoconnect)" = yes ]' + need "eth1 is connected with eth1-static" '[ "$(nmcli -g GENERAL.STATE device show eth1 | cut -d" " -f1)" = 100 ] && [ "$(nmcli -g GENERAL.CON-UUID device show eth1)" = "$UUID" ]' + need "eth1 has 192.168.50.10/24" 'ip -o addr show dev eth1 | grep -q " 192\.168\.50\.10/24 "' +} + +task 1 "eth1-static comes up at boot" +task1_needs + +task 2 "Booted once with autoconnect off, then back on and up" +need "a boot since the lab started had eth1-static set to no and eth1 not connected" 'awk -v b="$SETUP_BOOT" "\$1 != b && \$2 == \"autoconnect=no\" && \$3 != \"state=100\" {f = 1} END {exit !f}" "$LAB_STATE/watch.log"' +task1_needs diff --git a/labs/rhcsa/8-3-network-at-boot/lab.conf b/labs/rhcsa/8-3-network-at-boot/lab.conf new file mode 100644 index 0000000..aeb0b8f --- /dev/null +++ b/labs/rhcsa/8-3-network-at-boot/lab.conf @@ -0,0 +1,3 @@ +TITLE="8.3 Network connections at boot" +CHAPTER=/rhcsa/8-networking/8-3-network-at-boot/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/8-3-network-at-boot/reset.sh b/labs/rhcsa/8-3-network-at-boot/reset.sh new file mode 100644 index 0000000..40ca12a --- /dev/null +++ b/labs/rhcsa/8-3-network-at-boot/reset.sh @@ -0,0 +1,16 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +# Profiles on eth1, then the spare network itself. +nmcli -t -f UUID connection show 2>/dev/null | while read -r uuid; do + [ "$(nmcli -g connection.interface-name connection show "$uuid" 2>/dev/null)" = eth1 ] && + nmcli connection delete "$uuid" >/dev/null 2>&1 +done +rm -f /etc/NetworkManager/conf.d/91-le-lab-spare.conf +systemctl reload NetworkManager +systemctl disable --now --quiet le-lab-spare-net.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-spare-net.service /usr/local/lib/le-lab/spare-net.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +ip link del eth1 2>/dev/null +ip netns del le-net50 2>/dev/null +true diff --git a/labs/rhcsa/8-3-network-at-boot/setup.sh b/labs/rhcsa/8-3-network-at-boot/setup.sh new file mode 100644 index 0000000..53fe89d --- /dev/null +++ b/labs/rhcsa/8-3-network-at-boot/setup.sh @@ -0,0 +1,93 @@ +# The spare interface: eth1, cabled to a small lab network, +# 192.168.50.0/24, where a machine answers at 192.168.50.1 (and +# fd00:50::1). +ip link show eth1 >/dev/null 2>&1 && refuse "this VM already has an eth1; the lab adds its own spare eth1" +command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager || refuse "this lab needs NetworkManager running" +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/spare-net.sh <<'X' +#!/bin/sh +# The lab's spare network: eth1 here, the gateway 192.168.50.1 in the +# le-net50 namespace. Run at every boot. +ip netns list | grep -qw le-net50 || ip netns add le-net50 +ip -n le-net50 link set lo up +ip link show eth1 >/dev/null 2>&1 || ip link add eth1 address 52:54:00:4c:45:01 type veth peer name gw netns le-net50 +ip -n le-net50 addr replace 192.168.50.1/24 dev gw +ip -n le-net50 addr replace fd00:50::1/64 dev gw nodad +ip -n le-net50 link set gw up +true +X +chmod 755 /usr/local/lib/le-lab/spare-net.sh +cat >/etc/systemd/system/le-lab-spare-net.service <<'X' +[Unit] +Description=Practice lab: a spare network interface, eth1 +Before=network-pre.target NetworkManager.service +Wants=network-pre.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/le-lab/spare-net.sh + +[Install] +WantedBy=multi-user.target +X +# NetworkManager manages eth1 but makes no profile for it on its own. A +# gateway and DNS server on eth1 rank below the main interface's, so the +# VM keeps its own way out. +cat >/etc/NetworkManager/conf.d/91-le-lab-spare.conf <<'X' +[main] +no-auto-default+=interface-name:eth1 + +[device-le-lab-spare] +match-device=interface-name:eth1 +managed=1 + +[connection-le-lab-spare] +match-device=interface-name:eth1 +ipv4.route-metric=500 +ipv6.route-metric=500 +ipv4.dns-priority=200 +ipv6.dns-priority=200 +X +systemctl reload NetworkManager +systemctl daemon-reload +systemctl enable --now --quiet le-lab-spare-net.service +for i in 1 2 3 4 5 6 7 8 9 10; do + nmcli -g GENERAL.STATE device show eth1 2>/dev/null | grep -qv unmanaged && break + sleep 1 +done +nmcli connection add type ethernet ifname eth1 con-name eth1-static connection.autoconnect no \ + ipv4.method manual ipv4.addresses 192.168.50.10/24 ipv6.method disabled >/dev/null +state_set uuid "$(nmcli -g connection.uuid connection show eth1-static)" +cat /proc/sys/kernel/random/boot_id >"$LAB_STATE/setup-boot" + +# Note the profile's autoconnect setting and eth1's state, with the boot. +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints, with the boot it happened in. +log=$1 uuid=$2 prev='' +boot=$(cat /proc/sys/kernel/random/boot_id) +snap() { + echo "autoconnect=$(nmcli -g connection.autoconnect connection show "$uuid" 2>/dev/null)" \ + "state=$(nmcli -g GENERAL.STATE device show eth1 2>/dev/null | cut -d' ' -f1)" +} +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s %s\n' "$boot" "$cur" >>"$log"; prev=$cur; } + sleep 2 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null; } +pm() { sed -n 's/.* P \(.*\)$/\1/p' "$LOG" 2>/dev/null; } + +task 1 "HTTP and HTTPS allowed permanently, nothing else new" +need "firewalld is running and starts at boot" 'active firewalld && enabled firewalld' +need "the permanent services are the ones from before plus http and https" '[ "$(svcs --permanent)" = "$(want)" ]' +need "the runtime services are the same (reloaded)" '[ "$(svcs)" = "$(want)" ]' +need "firewall-cmd --list-ports is empty, now and permanently" '[ -z "$(fw --list-ports)" ] && [ -z "$(fw --permanent --list-ports)" ]' + +task 2 "8080/tcp allowed at runtime only, then gone after a reload" +need "8080/tcp was allowed at runtime but not permanently" 'grep -E "^R ports=[^ ]*8080/tcp" "$LOG" | grep -vq " P ports=[^ ]*8080/tcp"' +need "8080/tcp isn't allowed now" '! fw --query-port=8080/tcp && ! fw --permanent --query-port=8080/tcp' + +task 3 "ftp saved to permanent with --runtime-to-permanent, then removed" +need "ftp was in the permanent configuration" 'pm | grep -Eq "services=([^ ]*,)?ftp(,|$)"' +need "ftp isn't allowed now, at runtime or permanently" '! fw --query-service=ftp && ! fw --permanent --query-service=ftp' diff --git a/labs/rhcsa/8-4-firewalld-network-access/lab.conf b/labs/rhcsa/8-4-firewalld-network-access/lab.conf new file mode 100644 index 0000000..112a87e --- /dev/null +++ b/labs/rhcsa/8-4-firewalld-network-access/lab.conf @@ -0,0 +1,3 @@ +TITLE="8.4 Restricting network access with firewalld" +CHAPTER=/rhcsa/8-networking/8-4-firewalld-network-access/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/8-4-firewalld-network-access/reset.sh b/labs/rhcsa/8-4-firewalld-network-access/reset.sh new file mode 100644 index 0000000..1317d51 --- /dev/null +++ b/labs/rhcsa/8-4-firewalld-network-access/reset.sh @@ -0,0 +1,15 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +if [ -f "$LAB_STATE/firewalld.tar" ]; then + rm -rf /etc/firewalld + tar -C /etc -xf "$LAB_STATE/firewalld.tar" +fi +if [ -n "$(state_get firewalld-was-active)" ]; then + firewall-cmd -q --reload +else + systemctl stop firewalld +fi +[ -n "$(state_get firewalld-was-enabled)" ] || systemctl disable --quiet firewalld 2>/dev/null +true diff --git a/labs/rhcsa/8-4-firewalld-network-access/setup.sh b/labs/rhcsa/8-4-firewalld-network-access/setup.sh new file mode 100644 index 0000000..8afb03b --- /dev/null +++ b/labs/rhcsa/8-4-firewalld-network-access/setup.sh @@ -0,0 +1,50 @@ +IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}') +[ -n "$IF" ] || refuse "no default route; this lab needs a working network" +pkg_installed firewalld || pkg_install firewalld +systemctl is-active --quiet firewalld && state_set firewalld-was-active 1 +enabled firewalld && state_set firewalld-was-enabled 1 +tar -C /etc -cf "$LAB_STATE/firewalld.tar" firewalld +systemctl enable --now --quiet firewalld +firewall-cmd -q --state +Z=$(firewall-cmd --get-zone-of-interface="$IF" 2>/dev/null || firewall-cmd --get-default-zone) +state_set zone "$Z" +# A fresh start: none of the tasks' services or ports. +for s in http https ftp; do + firewall-cmd -q --permanent --zone="$Z" --query-service=$s && firewall-cmd -q --permanent --zone="$Z" --remove-service=$s +done +firewall-cmd -q --permanent --zone="$Z" --query-port=8080/tcp && firewall-cmd -q --permanent --zone="$Z" --remove-port=8080/tcp +firewall-cmd -q --reload +firewall-cmd --permanent --zone="$Z" --list-services | tr ' ' '\n' | sort >"$LAB_STATE/services-before" + +# Note the zone's services and ports, runtime (R) and permanent (P). +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints. +log=$1 z=$2 prev='' +snap() { + echo "R ports=$(firewall-cmd --zone="$z" --list-ports | tr ' ' ,) services=$(firewall-cmd --zone="$z" --list-services | tr ' ' ,)" \ + "P ports=$(firewall-cmd --permanent --zone="$z" --list-ports | tr ' ' ,) services=$(firewall-cmd --permanent --zone="$z" --list-services | tr ' ' ,)" +} +while :; do + if systemctl is-active --quiet firewalld; then + cur=$(snap 2>/dev/null) + [ "$cur" = "$prev" ] || { printf '%s\n' "$cur" >>"$log"; prev=$cur; } + fi + sleep 2 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +for u in carol svcbak; do userdel -r "$u" >/dev/null 2>&1; done +rm -rf /home/svcbak /home/carol /home/carol2 /var/spool/mail/carol /var/spool/mail/svcbak +true diff --git a/labs/rhcsa/9-1-user-accounts/setup.sh b/labs/rhcsa/9-1-user-accounts/setup.sh new file mode 100644 index 0000000..6e397c9 --- /dev/null +++ b/labs/rhcsa/9-1-user-accounts/setup.sh @@ -0,0 +1,35 @@ +for u in svcbak carol; do + id "$u" >/dev/null 2>&1 && refuse "user $u already exists; remove it and start again" +done +getent passwd 2001 >/dev/null && refuse "UID 2001 is already in use" +getent group 2001 >/dev/null && refuse "GID 2001 is already in use" +for d in /home/svcbak /home/carol /home/carol2; do + [ -e "$d" ] && refuse "$d already exists; remove it and start again" +done +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints. +log=$1 prev='' +snap() { + echo "$(grep -E '^(svcbak|carol):' /etc/passwd | tr '\n' '|') carol2=$(ls -A /home/carol2 2>/dev/null | tr '\n' ,)" +} +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s\n' "$cur" >>"$log"; prev=$cur; } + sleep 1 +done +X +cat >/etc/systemd/system/le-lab-watch.service <= 1000 && $3 < 60000 {print $1}' | while read -r u; do + grep -qxF "$u" "$LAB_STATE/users-before" || userdel -r "$u" >/dev/null 2>&1 + done +fi +true diff --git a/labs/rhcsa/9-2-passwords-and-aging/setup.sh b/labs/rhcsa/9-2-passwords-and-aging/setup.sh new file mode 100644 index 0000000..ee714b0 --- /dev/null +++ b/labs/rhcsa/9-2-passwords-and-aging/setup.sh @@ -0,0 +1,9 @@ +id dave >/dev/null 2>&1 && refuse "user dave already exists; remove it and start again" +cp -a /etc/login.defs "$LAB_STATE/login.defs" +# A fresh start for task 2. +if grep -Eq '^[[:space:]]*PASS_MAX_DAYS[[:space:]]+90[[:space:]]*$' /etc/login.defs; then + sed -i -E 's/^[[:space:]]*PASS_MAX_DAYS[[:space:]].*/PASS_MAX_DAYS\t99999/' /etc/login.defs +fi +user_add dave +state_set dave-max "$(getent shadow dave | cut -d: -f5)" +getent passwd | awk -F: '{print $1}' >"$LAB_STATE/users-before" diff --git a/labs/rhcsa/9-3-groups/README.md b/labs/rhcsa/9-3-groups/README.md new file mode 100644 index 0000000..c590baf --- /dev/null +++ b/labs/rhcsa/9-3-groups/README.md @@ -0,0 +1,28 @@ +# RHCSA 9.3: Local groups and membership + +Creating a group with members, changing its members, name and GID, and why +a user's own group can't be deleted. + +## Before you start + +The lab creates users `alice` and `bob` (password `Lab-Pass-1`). `alice` +is already in a supplementary group, `projects`, and `bob` in `support`; +they must stay there. Task 2 changes what task 1 made, so the lab runs a +small service, `le-lab-watch`, that notes the group every second for the +check. Do the tasks in order. + +## Tasks + +1. Create a group `sales` with GID 3000, and add `alice` and `bob` to it + as a supplementary group without changing their other groups. +2. Remove `bob` from `sales`, and rename the group to `market` with GID + 3001. +3. Try to delete `alice`'s private group (`alice`) while she exists, and + write the error message `groupdel` prints to `/root/lab-answers/3`. + +## Hints + +- `groupadd -g`, `usermod -aG` (without `-a`, `-G` replaces the list) or + `gpasswd -a`; `getent group sales`, `id alice`. +- `gpasswd -d`, `groupmod -n -g`. +- `groupdel alice 2>/root/lab-answers/3`. diff --git a/labs/rhcsa/9-3-groups/check.sh b/labs/rhcsa/9-3-groups/check.sh new file mode 100644 index 0000000..5a7f7ac --- /dev/null +++ b/labs/rhcsa/9-3-groups/check.sh @@ -0,0 +1,15 @@ +LOG=$LAB_STATE/watch.log + +task 1 "Group sales (GID 3000) with alice and bob" +need "sales:x:3000 had alice and bob as members" 'grep -Eq "(^| )sales:x:3000:(alice,bob|bob,alice)( |$)" "$LOG"' +need "alice is still in projects" 'id -nG alice | grep -qw projects' +need "bob is still in support" 'id -nG bob | grep -qw support' + +task 2 "bob out, group renamed market with GID 3001" +need "getent group market prints market:x:3001:alice" '[ "$(getent group market)" = "market:x:3001:alice" ]' +need "there's no group sales any more" '! getent group sales' +need "alice is still in projects" 'id -nG alice | grep -qw projects' + +task 3 "groupdel refused to remove alice's primary group" +need "/root/lab-answers/3 has groupdel's message" 'grep -qi "cannot remove the primary group" /root/lab-answers/3' +need "group alice still exists" 'getent group alice' diff --git a/labs/rhcsa/9-3-groups/lab.conf b/labs/rhcsa/9-3-groups/lab.conf new file mode 100644 index 0000000..2f6ae4e --- /dev/null +++ b/labs/rhcsa/9-3-groups/lab.conf @@ -0,0 +1,3 @@ +TITLE="9.3 Local groups and membership" +CHAPTER=/rhcsa/9-users-and-groups/9-3-groups/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/9-3-groups/reset.sh b/labs/rhcsa/9-3-groups/reset.sh new file mode 100644 index 0000000..df7ae09 --- /dev/null +++ b/labs/rhcsa/9-3-groups/reset.sh @@ -0,0 +1,9 @@ +systemctl disable --now --quiet le-lab-watch.service 2>/dev/null +rm -f /etc/systemd/system/le-lab-watch.service /usr/local/lib/le-lab/watch.sh +rmdir /usr/local/lib/le-lab 2>/dev/null +systemctl daemon-reload +groupdel sales 2>/dev/null +groupdel market 2>/dev/null +users_remove +while read -r g; do groupdel "$g" 2>/dev/null; done <"$LAB_STATE/groups" 2>/dev/null +true diff --git a/labs/rhcsa/9-3-groups/setup.sh b/labs/rhcsa/9-3-groups/setup.sh new file mode 100644 index 0000000..9768268 --- /dev/null +++ b/labs/rhcsa/9-3-groups/setup.sh @@ -0,0 +1,36 @@ +for g in sales market; do + getent group "$g" >/dev/null && refuse "group $g already exists; remove it and start again" +done +for g in 3000 3001; do + getent group "$g" >/dev/null && refuse "GID $g is already in use" +done +for g in projects support; do + getent group "$g" >/dev/null || { groupadd "$g"; echo "$g" >>"$LAB_STATE/groups"; } +done +user_add alice -G projects +user_add bob -G support +mkdir -p /usr/local/lib/le-lab +cat >/usr/local/lib/le-lab/watch.sh <<'X' +#!/bin/bash +# Notes each change of what snap prints. +log=$1 prev='' +snap() { grep -E '^(sales|market):' /etc/group | tr '\n' ' '; } +while :; do + cur=$(snap) + [ "$cur" = "$prev" ] || { printf '%s\n' "$cur" >>"$log"; prev=$cur; } + sleep 1 +done +X +cat >/etc/systemd/system/le-lab-watch.service </dev/null)" = 0 ]' +need "the rule is for group devs, not just dan" 'grep -rqsE "^[[:space:]]*%devs[[:space:]]" /etc/sudoers /etc/sudoers.d' +need "gina (not in devs) can't do the same" '! runuser -u gina -- sudo -n id -u' + +task 2 "gina may run only systemctl restart httpd as root" +need "gina may run systemctl restart httpd" 'sudo -n -l -U gina /usr/bin/systemctl restart httpd' +need "gina may not run systemctl stop httpd" '! sudo -n -l -U gina /usr/bin/systemctl stop httpd' +need "gina may not run id" '! sudo -n -l -U gina /usr/bin/id' +need "gina may not run a shell" '! sudo -n -l -U gina /bin/bash' + +task 3 "alice has full sudo through wheel, with her password" +need "alice is in group wheel" 'id -nG alice | grep -qw wheel' +need "sudo -l for alice shows (ALL) ALL" 'sudo -n -l -U alice | grep -Eq "^[[:space:]]+\(ALL(: ?ALL)?\) ALL$"' +need "alice can't use sudo without her password" '! runuser -u alice -- sudo -n true' diff --git a/labs/rhcsa/9-4-privileged-access/lab.conf b/labs/rhcsa/9-4-privileged-access/lab.conf new file mode 100644 index 0000000..78bfaf0 --- /dev/null +++ b/labs/rhcsa/9-4-privileged-access/lab.conf @@ -0,0 +1,3 @@ +TITLE="9.4 Privileged access" +CHAPTER=/rhcsa/9-users-and-groups/9-4-privileged-access/ +SYSTEMS="rhel" diff --git a/labs/rhcsa/9-4-privileged-access/reset.sh b/labs/rhcsa/9-4-privileged-access/reset.sh new file mode 100644 index 0000000..d2bf01d --- /dev/null +++ b/labs/rhcsa/9-4-privileged-access/reset.sh @@ -0,0 +1,7 @@ +if [ -f "$LAB_STATE/sudoers.tar" ]; then + rm -rf /etc/sudoers.d + tar -C /etc -xf "$LAB_STATE/sudoers.tar" +fi +users_remove +[ -n "$(state_get made-devs)" ] && groupdel devs 2>/dev/null +true diff --git a/labs/rhcsa/9-4-privileged-access/setup.sh b/labs/rhcsa/9-4-privileged-access/setup.sh new file mode 100644 index 0000000..8ff62ca --- /dev/null +++ b/labs/rhcsa/9-4-privileged-access/setup.sh @@ -0,0 +1,6 @@ +visudo -cq || refuse "sudo's configuration has errors; fix them (visudo -c) and start again" +tar -C /etc -cf "$LAB_STATE/sudoers.tar" sudoers sudoers.d +getent group devs >/dev/null || { groupadd devs; state_set made-devs 1; } +user_add dan -G devs +user_add gina +user_add alice