EX342 Practice ExamLesson 3 of 4
Part B: Software and applications (100 points)
Eight tasks: a damaged package, a broken repository, signatures and rollback, a crash, a service that will not start, limits, strace, and one Ansible playbook.
rpm -Vdnfrpm -Kcoredumpctlgdbsystemctl catsystemd-analyze verifystracelsofansible-playbook
After this lesson you can
- Repair the package, application and automation faults of the exam within about 73 minutes, and prove each with its checks.
Setup for Part B
Run as root on node1 after the Part C setup and before the Part A setup (Part A reboots the machine into emergency mode). It builds test packages, a repository that does not work, the crashing program and the services. It is here so that you can rebuild the exam; do not use it as a source of hints.
dnf install -y rpm-build rpm-sign gnupg2 gdb systemd-coredump tree chrony
mkdir -p /srv/labrepo /root/pkgs
# --- test packages
cat > /root/lab.spec <<'EOT'
Name: %{pkgname}
Version: %{pkgver}
Release: 1
Summary: Lab package %{pkgname}
License: MIT
BuildArch: noarch
AutoReqProv: no
%description
Lab package for the EX342 practice exam.
%install
mkdir -p %{buildroot}/usr/bin
printf '#!/bin/sh\necho %{pkgname} %{pkgver}\n' > %{buildroot}/usr/bin/%{pkgname}
%files
%attr(0755,root,root) /usr/bin/%{pkgname}
EOT
build() { rpmbuild -bb --define "_topdir /root/rpmbuild" --define "pkgname $1" --define "pkgver $2" /root/lab.spec >/dev/null; }
build labtools 1.0; build labtools 2.0; build labdocs 1.0; build labsigned 1.0
cp /root/rpmbuild/RPMS/noarch/labtools-*.rpm /root/rpmbuild/RPMS/noarch/labdocs-*.rpm /srv/labrepo/
# a signed package and its key
export GNUPGHOME=$(mktemp -d)
gpg --batch --passphrase '' --quick-gen-key "Lab Packager <pkg@lab.example>" rsa3072 sign never 2>/dev/null
gpg --armor --export "Lab Packager" > /root/pkgs/RPM-GPG-KEY-lab
cp /root/rpmbuild/RPMS/noarch/labsigned-1.0-1.noarch.rpm /root/pkgs/
rpmsign --define "_gpg_name Lab Packager" --addsign /root/pkgs/labsigned-1.0-1.noarch.rpm
unset GNUPGHOME
# --- B1: damage tree and chrony
chmod 700 /usr/bin/tree; rm -f /usr/share/man/man1/tree.1*
sed -i '/^pool\|^server/d' /etc/chrony.conf; systemctl restart chronyd
# --- B2/B3: a repository with three faults, and a bad update already installed
rpm -ivh --nodeps /srv/labrepo/labtools-2.0-1.noarch.rpm
cat > /etc/yum.repos.d/lab.repo <<'EOT'
[lab]
name=Lab repository
baseurl=file:///srv/lab-repo
enabled=1
gpgcheck=1
EOT
# --- B4: a crashing service (the source is removed afterwards)
cat > /root/ledgerd.c <<'EOT'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct conf { char *name; int interval; };
static void parse(struct conf *c, const char *path) {
char line[128];
FILE *f = fopen(path, "r");
c->interval = 30;
c->name = NULL;
if (!f) return;
while (fgets(line, sizeof line, f)) {
line[strcspn(line, "\n")] = 0;
if (strncmp(line, "name=", 5) == 0) c->name = strdup(line + 5);
else if (strncmp(line, "interval=", 9) == 0) c->interval = atoi(line + 9);
}
fclose(f);
}
static size_t name_length(const struct conf *c) {
return strlen(c->name);
}
int main(void) {
struct conf c;
parse(&c, "/etc/ledgerd.conf");
printf("ledgerd: name length %zu\n", name_length(&c));
fflush(stdout);
for (;;) sleep(c.interval);
}
EOT
gcc -g -O0 -o /usr/local/bin/ledgerd /root/ledgerd.c && rm /root/ledgerd.c
echo 'interval=60' > /etc/ledgerd.conf
printf '[Service]\nExecStart=/usr/local/bin/ledgerd\n[Install]\nWantedBy=multi-user.target\n' > /etc/systemd/system/ledgerd.service
systemctl daemon-reload; systemctl enable ledgerd; systemctl start ledgerd || true
# --- B5: invoiced with four faults
useradd -r -s /sbin/nologin invoice
mkdir -p /etc/invoiced /var/lib/invoiced; echo secret > /etc/invoiced/api.key; chmod 600 /etc/invoiced/api.key
cat > /usr/local/bin/invoiced <<'EOT'
#!/bin/bash
[ -n "$INVOICE_MODE" ] || { echo "INVOICE_MODE not set" >&2; exit 4; }
cat /etc/invoiced/api.key >/dev/null || exit 5
echo "started $(date)" > /var/lib/invoiced/state || exit 6
exec sleep infinity
EOT
chmod 755 /usr/local/bin/invoiced
cat > /etc/systemd/system/invoiced.service <<'EOT'
[Unit]
Description=Invoice daemon
[Service]
User=invoice
WorkingDirectory=/opt/invoiced
EnvironmentFile=/etc/sysconfig/invoiced
ProtectSystem=strict
ExecStart=/usr/local/bin/invoiced
[Install]
WantedBy=multi-user.target
EOT
systemctl daemon-reload; systemctl enable invoiced; systemctl start invoiced || true
# --- B6: a file-descriptor limit and a port conflict
cat > /etc/systemd/system/fdhog.service <<'EOT'
[Service]
LimitNOFILE=64
ExecStart=/usr/bin/python3 -c "import time; fs=[open('/etc/hostname') for _ in range(200)]; time.sleep(100000)"
[Install]
WantedBy=multi-user.target
EOT
cat > /etc/systemd/system/legacy-stats.service <<'EOT'
[Service]
ExecStart=/usr/bin/python3 -m http.server 9100 --bind 127.0.0.1
[Install]
WantedBy=multi-user.target
EOT
cat > /etc/systemd/system/stats.service <<'EOT'
[Unit]
After=legacy-stats.service
[Service]
ExecStart=/usr/bin/python3 -m http.server 9100 --bind 127.0.0.1
[Install]
WantedBy=multi-user.target
EOT
systemctl daemon-reload; systemctl enable --now fdhog legacy-stats || true; systemctl enable --now stats || true
# --- B7: a program with unknown needs
cat > /root/nightly.c <<'EOT'
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void) {
int fd = open("/var/lib/nightly/ready.flag", O_RDONLY);
if (fd < 0) return 2;
close(fd);
int s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(7777) };
inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
if (connect(s, (struct sockaddr *)&a, sizeof a) < 0) return 3;
close(s);
puts("nightly: ok");
return 0;
}
EOT
gcc -o /usr/local/bin/nightly /root/nightly.c && rm /root/nightly.c
# --- B8: a hurried playbook
install -d -o alex -g alex /home/alex/ansible
cat > /home/alex/ansible/ansible.cfg <<'EOT'
[defaults]
inventory = ./inventory
remote_user = alex
[privilege_escalation]
become_ask_pass = false
EOT
printf '[lab]\nnode1 ansible_host=node1.lab.example\nnode2 ansible_host=node2.lab.example\n' > /home/alex/ansible/inventory
cat > /home/alex/ansible/site.yml <<'EOT'
---
- name: Baseline for the lab
hosts: lab
tasks:
- name: Time sync package
ansible.builtin.dnf:
name: chrony
state: present
- name: Time sync service
ansible.builtin.service
name: chronyd
state: started
enabled: true
- name: Release marker
ansible.builtin.copy:
dest: /etc/lab-release
content: "lab: ex342\nhost: {{ inventory_hostname }}\n"
mode: "0644"
- name: No root login over ssh
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config.d/01-local.conf
line: PermitRootLogin no
create: true
notify: reload sshd
handlers:
- name: Reload sshd
ansible.builtin.service:
name: sshd
state: reloaded
EOT
chown -R alex:alex /home/alex/ansible
Part B marking
| Task | Points | Earned |
|---|---|---|
| B1 Damaged package | 12 | |
| B2 Repository | 10 | |
| B3 Signature and rollback | 12 | |
| B4 Crash | 14 | |
| B5 Service will not start | 16 | |
| B6 Limits and port | 12 | |
| B7 strace | 8 | |
| B8 Ansible | 16 | |
| Part B | 100 |
Practice
0 of 8 done
Do these on your own system. They get harder. Check each result, then tick it off.
- Warm-up
B1 · Repair a damaged package (12 points, 8 min)
On node1,
treeandchronywere damaged. Use the RPM database as your evidence:treedoes not run for ordinary users, and its manual page is gone. Restore both without reinstalling anything else.chronydwas changed so that it has no time sources. Restore/etc/chrony.confto the packaged default, and restart the service. Do not simply re-type a configuration from memory.- After your repairs,
rpm -V tree chronymust report no differences in content or mode.
Check your result
sudo -u nobody /usr/bin/tree --versionprints a version;ls -l /usr/bin/treeis-rwxr-xr-x;man -w treefinds the page.grep -c '^pool\|^server' /etc/chrony.confis at least1;systemctl is-active chronydisactive.rpm -V tree chronyprints nothing (a.......T.timestamp-only line on a documentation file is tolerated).
Hint
rpm -V treeshows.M.......andmissing d.dnf reinstall treerestores both. For chrony the reinstall keeps your modified config:dnf download chrony,rpm2cpio ... | cpio -idm ./etc/chrony.conf, thencp -pthe extracted file over the live one.Solution
rpm -V tree chrony sudo dnf reinstall -y tree cd /tmp && dnf download chrony rpm2cpio chrony-*.rpm | cpio -idm ./etc/chrony.conf sudo cp -p /tmp/etc/chrony.conf /etc/chrony.conf sudo restorecon -v /etc/chrony.conf sudo systemctl restart chronyd rpm -V tree chrony; echo "rc=$?" - Practice
B2 · A repository that does not work (10 points, 7 min)
The packages
labtoolsandlabdocsare in the directory/srv/labrepoand should be installable from a repository with the idlabdefined in/etc/yum.repos.d/lab.repo. They are unsigned test packages.- Make
dnf repolistshow the repositorylabwith its packages. Find and fix every reason it does not (there is more than one). - Install
labdocsfrom it. Do not touch the other repositories.
Check your result
ls /srv/labrepo/repodata/repomd.xmlexists.dnf repolist | grep -w labshows the repository with a package count of 3 or more.rpm -q labdocsis installed;rpm -qi labdocs | grep 'Install Date'.grep -E '^(baseurl|gpgcheck)' /etc/yum.repos.d/lab.reposhows a baseurl that points at an existing directory, andgpgcheck=0(the only repository where this is acceptable here: the packages are unsigned test files).
Hint
dnf makecacheshows the first failure. Check thebaseurlpath againstls /srv, then whetherrepodataexists (createrepo_c), then what the signature check says about unsigned packages.Solution
sudo dnf makecache 2>&1 | tail -3 grep baseurl /etc/yum.repos.d/lab.repo; ls /srv sudo sed -i 's#^baseurl=.*#baseurl=file:///srv/labrepo#' /etc/yum.repos.d/lab.repo sudo dnf install -y createrepo_c sudo createrepo_c /srv/labrepo sudo sed -i 's/^gpgcheck=.*/gpgcheck=0/' /etc/yum.repos.d/lab.repo sudo dnf clean all && dnf repolist sudo dnf install -y labdocs - Make
- Practice
B3 · Signatures, keys and rolling back (12 points, 8 min)
On node1:
/root/pkgs/labsigned-1.0-1.noarch.rpmis signed, and its public key is/root/pkgs/RPM-GPG-KEY-lab. Check the package before importing the key and save the one-word result for the signature line (NOKEYorOK) into/root/sig-before.txt. Then import the key and install the package. The check must saydigests signatures OKafterwards.- An unplanned update installed
labtools2.0, which breaks a script. Return to version 1.0 using the repository, and make surednfdoes not offer 2.x again.
Check your result
cat /root/sig-before.txtisNOKEY;rpm -qa 'gpg-pubkey*' --qf '%{SUMMARY}\n' | grep -c 'Lab Packager'is1.rpm -K /root/pkgs/labsigned-1.0-1.noarch.rpmprintsdigests signatures OK;rpm -q labsignedis installed.rpm -q labtoolsprintslabtools-1.0-1.noarch.dnf check-update labtools; echo $?prints0(no update offered).
Hint
rpm -Kvnames the signature result per line.rpm --import FILEbrings a key in.dnf downgrade labtools-1.0needs the repository from B2 working. Stop 2.x withexclude=labtools-2*in the repository file, or theversionlockplugin.Solution
rpm -Kv /root/pkgs/labsigned-1.0-1.noarch.rpm | grep -i signature | awk '{print $NF}' | sudo tee /root/sig-before.txt sudo rpm --import /root/pkgs/RPM-GPG-KEY-lab rpm -K /root/pkgs/labsigned-1.0-1.noarch.rpm sudo dnf install -y /root/pkgs/labsigned-1.0-1.noarch.rpm sudo dnf downgrade -y labtools-1.0 echo 'exclude=labtools-2*' | sudo tee -a /etc/yum.repos.d/lab.repo rpm -q labtools; dnf check-update labtools; echo "rc=$?" - Practice
B4 · Analyse a crash (14 points, 12 min)
ledgerd.service(the program/usr/local/bin/ledgerd) crashes at start. You cannot change the program.- Using the recorded crash dump, write
/root/crash.txtwith exactly three lines:FUNCTION=(the function of ledgerd's own code where the crash happened),LINE=(its source line number) andVARIABLE=(the pointer that was NULL, written as in the source, e.g.c->name). - Make the service start and stay running by correcting its configuration, not the program.
Check your result
cat /root/crash.txtisFUNCTION=name_length,LINE=23,VARIABLE=c->name(the expressionnameis accepted for the variable).coredumpctl list ledgerd --no-pagershows at least oneSIGSEGVentry.systemctl is-active ledgerdisactiveandsystemctl is-enabled ledgerdisenabled.grep -c '^name=' /etc/ledgerd.confis at least1.
Hint
coredumpctl debug ledgerdopens gdb;btshows frames, the first one from ledgerd's own file is the one you need (frame 0 is inside the C library).frame 1,print *c. The program reads/etc/ledgerd.confand looks forname=andinterval=lines.Solution
coredumpctl list ledgerd --no-pager coredumpctl debug ledgerd -A "-batch -ex bt -ex 'frame 1' -ex 'print *c'" printf 'FUNCTION=name_length\nLINE=23\nVARIABLE=c->name\n' | sudo tee /root/crash.txt echo 'name=ledger' | sudo tee -a /etc/ledgerd.conf sudo systemctl restart ledgerd; systemctl is-active ledgerd - Using the recorded crash dump, write
- Challenge
B5 · A service that will not start (16 points, 12 min)
invoiced.servicedoes not start and stays failed. It must run as the userinvoice, at boot, with the following facts about the application: it needs the variableINVOICE_MODE(valueprod), it reads/etc/invoiced/api.key, and it writes its state to/var/lib/invoiced/state. There is more than one problem; each only appears once the previous is fixed.Fix the service without changing the program or relaxing the security settings in the unit that are not the cause, and without making the key readable by everyone.
Check your result
systemctl is-active invoicedisactive;systemctl is-enabled invoicedisenabled.sudo cat /var/lib/invoiced/statecontainsstarted.ls -l /etc/invoiced/api.keyshows groupinvoiceand mode640(or stricter than644, with no "other" access).systemctl show invoiced -p ProtectSystemstill printsProtectSystem=strict, and the unit listsReadWritePaths=/var/lib/invoiced.systemctl show invoiced -p EnvironmentFilesnames a file that exists andgrep INVOICE_MODEin it printsprod.
Hint
Read
systemctl status invoiced, thenjournalctl -u invoiced -b, thensystemctl cat invoiced. Typical exit statuses:200/CHDIR(a directory), a message about the environment file, status5(the key), status6(writing the state). Fix them in this order, restarting after each.Solution
systemctl status invoiced --no-pager | sed -n 1,12p systemctl cat invoiced sudo mkdir -p /opt/invoiced # WorkingDirectory echo 'INVOICE_MODE=prod' | sudo tee /etc/sysconfig/invoiced # EnvironmentFile sudo systemctl restart invoiced; journalctl -u invoiced -n 3 --no-pager sudo chgrp invoice /etc/invoiced/api.key && sudo chmod 640 /etc/invoiced/api.key sudo chown invoice: /var/lib/invoiced sudo mkdir -p /etc/systemd/system/invoiced.service.d printf '[Service]\nReadWritePaths=/var/lib/invoiced\n' | sudo tee /etc/systemd/system/invoiced.service.d/rw.conf sudo systemctl daemon-reload && sudo systemctl restart invoiced systemctl is-active invoiced; sudo cat /var/lib/invoiced/state - Practice
B6 · Limits and a port conflict (12 points, 8 min)
Two services are failing on node1:
fdhog.servicedies with a "Too many open files" error. Raise its limit to1024in the way that survives a reboot and does not edit the packaged/original unit file.stats.servicecannot bind TCP port 9100. Find out which process holds the port and why it runs. That process belongs to the retiredlegacy-stats.service, which must not run any more (not now, not after a reboot). Then startstats.serviceand keep it enabled.
Check your result
systemctl show fdhog -p LimitNOFILEprintsLimitNOFILE=1024;systemctl is-active fdhogisactive.ls /etc/systemd/system/fdhog.service.d/*.confexists; the original/etc/systemd/system/fdhog.servicestill hasLimitNOFILE=64.systemctl is-enabled legacy-statsisdisabledormaskedandsystemctl is-active legacy-statsis notactive.sudo ss -tlnp 'sport = :9100'shows thestatsprogram;systemctl is-active statsisactiveandsystemctl is-enabled statsisenabled.
Hint
journalctl -u fdhoghasErrno 24.systemctl edit fdhogcreates a drop-in.ss -tlnp 'sport = :9100'names the PID;systemctl status PID(orps -o unit= -p PID) names its unit.Solution
journalctl -u fdhog -n 3 --no-pager sudo mkdir -p /etc/systemd/system/fdhog.service.d printf '[Service]\nLimitNOFILE=1024\n' | sudo tee /etc/systemd/system/fdhog.service.d/limits.conf sudo systemctl daemon-reload && sudo systemctl restart fdhog sudo ss -tlnp 'sport = :9100' sudo systemctl status $(sudo ss -tlnpH 'sport = :9100' | grep -o 'pid=[0-9]*' | cut -d= -f2) --no-pager | head -3 sudo systemctl disable --now legacy-stats sudo systemctl enable --now stats systemctl is-active fdhog stats - Practice
B7 · Find what a program is looking for (8 points, 6 min)
/usr/local/bin/nightlyexits silently with a non-zero status, and its source code is not available. Without guessing:- Find out which file it tries to open, and which TCP address and port it connects to afterwards. Write them to
/root/nightly.txtasFILE=/pathandPORT=number. - Provide what it needs so that
nightlyexits with status0, in a way that survives a reboot (the file must exist and something must listen on that port at boot).
Check your result
cat /root/nightly.txtisFILE=/var/lib/nightly/ready.flagandPORT=7777.nightly; echo $?printsnightly: okand0.systemctl is-enabled nightly-sinkisenabled(or any enabled unit that listens on127.0.0.1:7777:ss -tln | grep 7777).
Hint
strace -f -e trace=openat,connect nightly 2>&1 | grep -E 'ENOENT|connect'. The first failing call decides the exit status; fix it, run again, and the next failure appears. A throwaway listener as a service:python3 -m http.server 7777 --bind 127.0.0.1in a unit.Solution
nightly; echo "rc=$?" strace -e trace=openat,connect nightly 2>&1 | grep -E 'ready|connect' printf 'FILE=/var/lib/nightly/ready.flag\nPORT=7777\n' | sudo tee /root/nightly.txt sudo mkdir -p /var/lib/nightly && sudo touch /var/lib/nightly/ready.flag sudo tee /etc/systemd/system/nightly-sink.service <<'EOT' [Unit] Description=Listener for nightly [Service] ExecStart=/usr/bin/python3 -m http.server 7777 --bind 127.0.0.1 [Install] WantedBy=multi-user.target EOT sudo systemctl daemon-reload && sudo systemctl enable --now nightly-sink nightly; echo "rc=$?" - Find out which file it tries to open, and which TCP address and port it connects to afterwards. Write them to
- Challenge
B8 · One playbook for both nodes (16 points, 12 min)
In
/home/alex/ansibleon node1 there is an inventory (grouplabwith both nodes), anansible.cfgand a playbooksite.ymlthat was written in a hurry. As useralex:- Install the Ansible command-line tools on node1 (nothing on node2 except Python).
- Make
site.ymlpass--syntax-checkand run successfully against both nodes. It must: install and enablechrony, create/etc/lab-releasecontaininglab: ex342and a linehost:followed by the host's inventory name, and setPermitRootLogin noin/etc/ssh/sshd_config.d/01-local.conf, reloadingsshdonly when that setting changed. - The run must be idempotent: a second run reports
changed=0for both nodes.
Check your result
rpm -q ansible-coreon node1;ansible-playbook -i inventory site.yml --syntax-checkprints only the playbook name.- First run: ends with
failed=0for both nodes. Second run: both nodes showchanged=0 ... failed=0. ssh node2 cat /etc/lab-releaseprintslab: ex342andhost: node2;ssh node2 sudo sshd -T | grep -i '^permitrootlogin'ispermitrootlogin no.systemctl is-enabled chronydisenabledon both nodes.
Hint
Run
ansible-playbook --syntax-checkfirst: it finds a missing colon and (with a run) a handler that is never called because the names differ in case. Writing under/etcneedsbecome: true. Read the bottom of an error first.Solution
sudo dnf install -y ansible-core cd /home/alex/ansible ansible-playbook -i inventory site.yml --syntax-check # fix: the colon after ansible.builtin.service, become: true on the play, # and notify: Reload sshd (same spelling as the handler name) sed -i 's/ansible.builtin.service$/ansible.builtin.service:/; s/notify: reload sshd/notify: Reload sshd/' site.yml sed -i 's/^ hosts: lab$/ hosts: lab\n become: true/' site.yml ansible-playbook -i inventory site.yml ansible-playbook -i inventory site.yml | tail -4