Certifications · LFCS · Mock Exam · Set 3

LFCS Mock Exam · Set 3

This is the third and final LFCS paper in this course, and it's deliberately lopsided in the way real exam prep should be once you know your weak spots. Nineteen performance-based tasks, one unbroken 120-minute sitting, still worth exactly 100 points split 25 / 25 / 20 / 20 / 10 across the same five official domains as Set 1 and Set 2 — but not split evenly across tasks this time. Networking gets six tasks here and Storage gets five, while Operations Deployment and Essential Commands shrink to three apiece to make room, because the two domains most real incident reviews actually live in deserve more independent repetitions under a clock, not just a bigger point total. Every task keeps the same shape as the other two papers — a plain-language brief, a done when check you run yourself against a real terminal, and a full worked solution folded under a summary that stays closed until you've either passed the check or given up on it — and every scenario below is new, not a reskin of anything from Set 1 or Set 2. Build the machine the night before, skim the pre-flight section, then start one timer and don't stop it until task nineteen is behind you.

☺ Explain it like I'm 10

Imagine the first two practice rounds you did were fixing whatever needed it that day, spread evenly across a whole house — a wobbly shelf here, a stuck window there. This one is different on purpose: today almost every job is either "get this box to the exact right address across town" or "make everything already in the garage fit without anything falling over when the door opens" — six delivery jobs, five packing jobs, and just a handful of smaller chores squeezed in around them. It's not that the house stopped having other problems. It's that you already know from the last two rounds which two kinds of job slow you down the most, so today's drill gives you more practice at exactly those, on the clock, before the day someone else times you for real.

🐦🦫Your hosts for this topic: Pip the Hummingbird & Benny the Beaver — Pip flies the six networking tasks, because half of them live or die on whether a packet actually arrives somewhere and not just on whether the syntax looked right; Benny holds the five storage tasks to the same standard, insisting a done-when check only passes when the thing you built actually survives a disk pulled out mid-task, not when it merely looks correctly configured.

Before you start — exam conditions

☺ Like you're 10: This drill only teaches you something if you run it exactly like the real thing — same clock, same rules, no notes you wouldn't have on the day.

Everything that made Set 1 and Set 2 honest applies here too, plus a slightly longer shopping list because this paper's lean into Networking and Storage touches tools the earlier two barely needed.

One — build the machine the night before, with more moving parts than usual. A throwaway VM works, but this paper wants at least three spare block devices attached (one for an LVM snapshot, two for a software RAID1 mirror) plus a second interface or two for VLAN/bridge/bond-adjacent work, and — for the network-block-device task — either a second small VM to act as the NBD server or nbd-server running locally as a stand-in against a loopback export. Install everything the paper touches before the clock starts:

# RHEL/Fedora family
sudo dnf install -y lvm2 mdadm quota nbd haproxy sssd sssd-ldap nftables git openssl \
  libvirt libvirt-daemon virt-install
sudo modprobe 8021q nbd

# Debian/Ubuntu family
# sudo apt install -y lvm2 mdadm quota nbd-client nbd-server haproxy sssd nftables git \
#   openssl libvirt-daemon-system virtinst

lsblk                          # confirm the spare disks for the snapshot + RAID tasks are visible and empty
ip -br link                    # confirm at least two extra interfaces exist for VLAN/bridge work

Two — one timer, started once. 120 minutes, no pausing for a stubborn typo or a VM that needs a reboot. Time lost fighting your own lab environment is real data about exam-day friction, not a reason to stop the clock.

Three — only the permitted resources, open before you start. Man pages, documentation installed by the distribution, and packages that ship with it — accessed from inside the exam terminal itself. No browser, no search engine, no this course, no AI assistant.

Four — no copying commands from the solution, and no AI assistance. If you get stuck, spend the budget, fail the task, and read the solution after the sitting ends.

Five — read all nineteen tasks before you touch a terminal. With six networking tasks bunched together, five minutes spent skimming the whole paper is what stops you from accidentally spending twenty minutes on the fourth one when the sixth was the two-command freebie.

⚠ Numbers here can change — verify officially before booking

This is an independent, unofficial study resource, not affiliated with the CNCF or The Linux Foundation. The 19-task shape, the 120-minute pace, the 100-point sheet and the 67% pass mark are a study design built around the domain weights and logistics documented on LFCS — the exam — including this paper's own choice to run six Networking tasks and five Storage tasks instead of four apiece, which is this course's authoring decision, not a claim about how the real exam allocates its 17–20 tasks. Price, duration, retake terms, the allowed-resource list and the domain weights themselves are all revised over time. Confirm everything current on the official LFCS certification page and the candidate handbook before you register or pay for anything.

Your time budget

☺ Like you're 10: Give every domain a slice of the two hours sized to how many jobs live in it, not to how important the topic sounds — Networking gets the biggest slice here because it has the most jobs, not because it matters more than Storage.

Nineteen tasks in 120 minutes still averages under six and a half minutes each once you carve out five minutes to read and six to verify at the end, but this paper's shape is lumpier than Set 1 or Set 2: Operations Deployment and Essential Commands get three bigger, denser tasks apiece, while Networking's six tasks and Storage's five are individually smaller and faster — the trade this paper is deliberately making is fewer points per task in exchange for more independent chances to get a wire or a disk right under pressure.

BlockTasksBudgetRunning totalNote
Read the whole paper5 min5Six Networking tasks in a row rewards spotting the cheap ones early.
Operations DeploymentOD1–OD39+7+8 = 24 min29Three dense tasks. OD1's libvirt VM is the longest single pole on the paper.
NetworkingN1–N66+6+5+5+6+6 = 34 min63The biggest block on the clock. N5's HAProxy health check is the fiddliest.
StorageS1–S55+5+5+5+5 = 25 min88Five even five-minute tasks — the most evenly paced block on the paper.
Essential CommandsE1–E35+6+6 = 17 min105E1's git bisect is fast once you trust the binary search; don't manually check every commit.
Users & GroupsU1–U24+5 = 9 min114Only 10 points, but two of the cheapest tasks on the whole paper. Never skip these.
Verify & sweep flagged tasks6 min120Re-run every done-when. Finish the nearly-done before starting anything new.

The flag-and-move rule still applies exactly as it did on the first two papers: when a task passes roughly its budgeted minutes without a passing done-when check, stop, write one line about where you stalled, leave whatever you've applied in place, and move to the next task. On a paper this heavy on Networking, the rule matters more than usual — six tasks means six chances to fall down the same rabbit hole, and only one of them is worth more than five points.

120 minutes · 19 tasks · 100 points read 5m Ops Deploy 24m · 3 tasks Networking 34m · 6 tasks Storage 25m · 5 tasks Essential Cmds 17m · 3 tasks Users 9m · 2 Verify 6m 0 120 min Points still follow the official weighting, not the task count Operations Deployment 25 · Networking 25 · Storage 20 · Essential Commands 20 · Users & Groups 10 What actually changed on this paper Task count leans into Networking (6) and Storage (5) for more repetitions — smaller point values, more chances to get it right. Points stay tied to the same 25/25/20/20/10 official split as Set 1 and Set 2, so your domain scores are still directly comparable across all three sittings.
◆ Key idea

More tasks in a domain is not the same as more points in a domain — this paper proves the distinction on purpose. Networking and Storage still cap out at 25 and 20 points respectively, exactly like Set 1 and Set 2; what changed is that those points are now spread across six and five smaller, independent tasks instead of four apiece. That trade is worth making once you already know Networking or Storage is your weak spot: more repetitions under time pressure teach pacing and pattern recognition in a way one large task worth the same total points cannot.

Operations Deployment — OD1 to OD3 (25 points)

☺ Like you're 10: Three bigger jobs this time — bringing a whole virtual machine to life, teaching this box to trust and install new software from a private source, and rescuing a machine that won't finish booting.

Fewer, denser tasks than Set 1 or Set 2 to make room for Networking and Storage below. Background: LFCS — the exam's Operations Deployment section.

OD1 · Bring a pre-built VM to life with libvirt and make it survive a reboot

A qcow2 disk image for a new edge node, /var/lib/libvirt/images/edge-01.qcow2, has been staged on this host by an earlier build step. It needs to become a running, named libvirt domain — 2 vCPUs, 2048 MB of RAM, attached to the default virtual network — that comes back up automatically the next time this host reboots, without anyone running a manual virsh start.

Your task:

  1. Define and start a domain named edge-01 around the existing disk image, without reinstalling an OS onto it.
  2. Confirm it's actually running, not just defined.
  3. Enable autostart so the hypervisor brings it up on the next host boot.

Done when: virsh list --all shows edge-01 in state running; virsh dominfo edge-01 reports Autostart: enable; and virsh domifaddr edge-01 returns at least one lease once the guest has finished booting. (9 points)

Show the worked solution
virt-install \
  --name edge-01 \
  --memory 2048 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/edge-01.qcow2,format=qcow2 \
  --import \
  --os-variant generic \
  --network network=default \
  --graphics none \
  --noautoconsole

virsh list --all                 # edge-01 ... running
virsh autostart edge-01
virsh dominfo edge-01 | grep -i autostart      # Autostart: enable

# give the guest a minute to boot and request a DHCP lease, then:
virsh domifaddr edge-01

Why: --import is the whole trick — it skips the OS installer entirely and boots the existing qcow2 disk as-is, which is the realistic shape of "bring an already-built image online" rather than "install a new OS from scratch," a task that would never fit inside a points budget this small. virsh autostart writes a symlink under /etc/libvirt/qemu/autostart/ pointing at the domain's XML definition, so it's libvirtd itself — not a cron job, not a systemd unit you'd have to write by hand — that brings the guest back after the host reboots.

OD2 · Add a signed private package repository and pin an exact version

A vendor tool, telemetry-agent, ships from a private RPM repository at https://repo.mission.internal/rpm/el9/, GPG-signed. It needs to be installed at exactly version 2.4.1 — not whatever is newest — and it needs to stay at that version even after a routine dnf update runs later.

Your task:

  1. Add the repository with signature checking enabled and import its GPG key.
  2. Install exactly version 2.4.1 of telemetry-agent.
  3. Lock that package so a later dnf update cannot silently move it to a newer version.

Done when: dnf repolist shows mission-tools enabled; rpm -q telemetry-agent reports exactly telemetry-agent-2.4.1; and after dnf versionlock list shows the package locked, running dnf update telemetry-agent reports nothing to do. (8 points)

Show the worked solution
# /etc/yum.repos.d/mission-tools.repo
[mission-tools]
name=Mission Tools
baseurl=https://repo.mission.internal/rpm/el9/
enabled=1
gpgcheck=1
gpgkey=https://repo.mission.internal/RPM-GPG-KEY-mission
rpm --import https://repo.mission.internal/RPM-GPG-KEY-mission
dnf makecache
dnf repolist                                        # mission-tools ... enabled

dnf list --showduplicates telemetry-agent           # confirm 2.4.1 is an available build
dnf install -y telemetry-agent-2.4.1
rpm -q telemetry-agent

dnf install -y python3-dnf-plugin-versionlock       # if not already present
dnf versionlock add telemetry-agent
dnf versionlock list

dnf update telemetry-agent                          # "Nothing to do."

Why: gpgcheck=1 without a correctly imported key just makes every install from the repo fail loudly rather than silently accept an unsigned or tampered package — that failure is the entire point of the check, and skipping the rpm --import step is the most common way this task looks finished right up until the first real install command. Installing a specific version and locking it are two separate actions that people conflate: dnf install telemetry-agent-2.4.1 only controls what lands today, while versionlock is the thing that stops a routine, unrelated dnf update from quietly pulling 2.5.0 over it next week.

OD3 · Recover a host stuck in emergency mode after a bad fstab edit

This host was rebooted after someone hand-edited /etc/fstab and it now drops into emergency mode instead of reaching a normal login — a systemd dependency failure tied to a mount unit. You have an emergency shell.

Your task:

  1. From the emergency shell, identify which fstab entry is actually causing the failure, without guessing.
  2. Fix the entry so the host can boot normally again.
  3. Prove the fix is correct before committing to another reboot.

Done when: mount -a completes with no errors from the emergency shell; after a reboot, systemctl is-system-running reports running (or degraded with no failed mount units specifically); and systemctl list-units --failed shows no .mount units. (8 points)

Show the worked solution
# from the emergency shell
journalctl -xb | grep -iE "fstab|mount|dependency failed"
# ... reveals a mismatched UUID or a bad mount point on one specific fstab line
mount -o remount,rw /            # the emergency shell often mounts root read-only
vi /etc/fstab                    # fix the offending line — corrected UUID, or add nofail
                                  # for a device that isn't always guaranteed to be present

mount -a                         # rehearse the fix exactly as boot would apply it,
                                  # WITHOUT committing to a second reboot yet
systemctl default                # attempt to continue the current boot without rebooting

reboot                           # once mount -a is clean, confirm with a real reboot
systemctl is-system-running
systemctl list-units --failed

Why: mount -a is the safe rehearsal for an fstab edit — it applies the file exactly as the boot sequence would, without committing to a second reboot that could strand you again if the fix is still wrong, which is precisely the mistake that put this host into emergency mode the first time. A device or filesystem that isn't guaranteed to always be present belongs behind the nofail option rather than a plain required entry, because systemd's default assumption is that a missing device is a boot-blocking failure worth stopping for, not a soft warning to log and move past.

Networking — N1 to N6 (25 points)

☺ Like you're 10: Six jobs about making sure the right message reaches the right place — a private lane on a shared wire, a reply that goes back out the door it came in, a shared fence between two rooms, a newer kind of address, traffic split between two helpers, and a name that points to the right desk.

The biggest single block on this paper, both in points and in task count. Per LFCS — the exam and Platform Engineering's own Networking chapter, this is also the domain where real incidents concentrate. Use ip, not ifconfig; ss, not netstat; nft, not raw iptables; resolvectl, not a hand-edited resolv.conf, wherever the host runs systemd-resolved.

N1 · Tag a VLAN subinterface for isolated management traffic

Management traffic for the telemetry rack needs to move onto VLAN 100, isolated from the untagged production LAN sharing the same physical NIC, eth0. The subinterface needs address 10.100.0.5/24 and must survive a reboot.

Your task:

  1. Create the tagged VLAN subinterface eth0.100 on eth0 and assign it the required address.
  2. Persist it using your distribution's real network configuration mechanism.
  3. Confirm the tag and address survive a service restart, not just a live check.

Done when: ip -d link show eth0.100 reports vlan protocol 802.1Q id 100; ip addr show eth0.100 lists 10.100.0.5/24; and restarting systemd-networkd (or the NetworkManager equivalent) leaves both in place. (5 points)

Show the worked solution
ip link add link eth0 name eth0.100 type vlan id 100
ip addr add 10.100.0.5/24 dev eth0.100
ip link set eth0.100 up
ip -d link show eth0.100                 # confirm: vlan protocol 802.1Q id 100
# /etc/systemd/network/10-eth0.network  (the parent interface — must reference the VLAN)
[Match]
Name=eth0
[Network]
VLAN=eth0.100

# /etc/systemd/network/20-eth0.100.netdev
[NetDev]
Name=eth0.100
Kind=vlan
[VLAN]
Id=100

# /etc/systemd/network/20-eth0.100.network
[Match]
Name=eth0.100
[Network]
Address=10.100.0.5/24
systemctl restart systemd-networkd
ip -d link show eth0.100
ip addr show eth0.100

Why: an 802.1Q tag is how one physical NIC and one upstream switch port carry several logically isolated networks at once — but the switch port on the other end has to be configured as a trunk carrying VLAN 100, or the tagged frames never arrive, which is the single most common reason this task looks correct on the host and still passes no traffic. The parent interface's .network file has to explicitly list VLAN=eth0.100 under [Network]; without that line, networkd creates the tagged device from the .netdev file but never actually attaches it to eth0.

N2 · Route return traffic out the interface it actually belongs to

This host is multi-homed: eth0 carries the default route via ISP-A, and eth1 is a secondary uplink through ISP-B at 198.51.100.20/24, gateway 198.51.100.1. Traffic sourced from 198.51.100.20 keeps leaving via the default route on eth0 instead of back out eth1, and ISP-B's edge firewall — correctly — drops it as spoofed.

Your task:

  1. Add a policy rule so any packet sourced from 198.51.100.20 uses a separate routing table.
  2. Populate that table with a default route via ISP-B's gateway, out eth1.
  3. Persist both the rule and the table so they survive a network service restart.

Done when: ip route get 8.8.8.8 from 198.51.100.20 reports it would go via 198.51.100.1 dev eth1; ip rule show lists the custom rule; and restarting systemd-networkd leaves both the rule and the table populated. (4 points)

Show the worked solution
ip rule add from 198.51.100.20 table 100
ip route add default via 198.51.100.1 dev eth1 table 100
ip route add 198.51.100.0/24 dev eth1 src 198.51.100.20 table 100

ip rule show
ip route show table 100
ip route get 8.8.8.8 from 198.51.100.20        # confirm it now selects table 100 / eth1
# /etc/systemd/network/10-eth1.network  (additions)
[RoutingPolicyRule]
From=198.51.100.20
Table=100

[Route]
Gateway=198.51.100.1
Table=100
systemctl restart systemd-networkd
ip rule show
ip route show table 100

Why: a routing table can only hold one default route, so "reply out whichever interface a packet's source address belongs to" needs a second table entirely — ip rule decides which table a route lookup uses based on the packet's source address, before the ordinary destination-based lookup ever runs. Skipping this on a multi-homed host produces exactly this symptom: the packet leaves correctly addressed, on the wrong wire, and a remote firewall correctly treats a reply arriving from the "wrong" path as spoofed traffic and silently drops it.

N3 · Bridge a physical interface for two isolated lab guests

A pair of local KVM guests need a private lab network on their own bridge — not the default NAT'd libvirt network — with a dedicated physical uplink, eth3, enslaved to it. The bridge itself needs a usable management address.

Your task:

  1. Create bridge br0 and enslave eth3 to it.
  2. Assign 10.50.0.1/24 to the bridge itself, not to eth3.
  3. Confirm the slave interface is actually forwarding, not just attached.

Done when: bridge link show lists eth3 with master br0 state forwarding; ip addr show br0 shows 10.50.0.1/24; and ip addr show eth3 shows no IPv4 address of its own. (4 points)

Show the worked solution
ip link add name br0 type bridge
ip link set eth3 down
ip link set eth3 master br0
ip link set eth3 up
ip link set br0 up
ip addr add 10.50.0.1/24 dev br0

bridge link show                    # eth3 ... master br0 state forwarding
ip addr show br0                    # 10.50.0.1/24
ip addr show eth3                   # no IPv4 address — the bridge owns it now
# /etc/systemd/network/25-br0.netdev
[NetDev]
Name=br0
Kind=bridge

# /etc/systemd/network/25-br0.network
[Match]
Name=br0
[Network]
Address=10.50.0.1/24

# /etc/systemd/network/10-eth3.network
[Match]
Name=eth3
[Network]
Bridge=br0

Why: bridging works at layer 2 — enslaving eth3 to br0 means the bridge, not the physical NIC, now owns any IP address for this segment, which is why the address moves to br0 instead of being duplicated on both. Bringing the slave down before enslaving it and back up afterward avoids a brief window where the interface still carries stale IP state from before it joined the bridge — a subtlety that almost never bites on a fresh lab box but reliably does on one that's been reconfigured a few times already.

N4 · Add a routable IPv6 address and prove connectivity, not just presence

The telemetry-archive host needs a routable IPv6 address on eth02001:db8:100::5/64 — with a default IPv6 route via 2001:db8:100::1, persisted, and actually reachable, not merely configured.

Your task:

  1. Assign the address and add the default IPv6 route.
  2. Persist both.
  3. Prove the address is a global one and that the route actually carries traffic.

Done when: ip -6 addr show eth0 lists 2001:db8:100::5/64 with scope global (not only the automatic fe80:: link-local address); ip -6 route get 2001:db8:100::1 resolves via eth0; and ping -6 -c2 2001:db8:100::1 receives replies. (4 points)

Show the worked solution
ip -6 addr add 2001:db8:100::5/64 dev eth0
ip -6 route add default via 2001:db8:100::1 dev eth0

ip -6 addr show eth0                # confirm scope global, not just link-local
ip -6 route get 2001:db8:100::1
ping -6 -c2 2001:db8:100::1
# /etc/systemd/network/10-eth0.network  (additions)
[Network]
Address=2001:db8:100::5/64
Gateway=2001:db8:100::1

Why: every Linux interface auto-generates a link-local fe80::/10 address on its own the moment it comes up — that address alone proves nothing about whether the global address you were actually asked to configure is present, which is exactly why the done-when check insists on scope global specifically. IPv6 has no ARP; neighbor discovery does that job over ICMPv6, so a failed ping -6 after an address and route that both look correct usually means neighbor-discovery or ICMPv6 traffic is being filtered somewhere on the path, not that the addressing syntax was wrong.

N5 · Load-balance two backends with a health check that actually removes a dead one

Two identical backend services run on 10.0.2.11:8080 and 10.0.2.12:8080. The public-facing host needs to round-robin across both on port 80, and a backend that stops responding needs to fall out of rotation automatically — not stay in it, quietly failing every request routed to it.

Your task:

  1. Configure HAProxy to front both backends on port 80 with round-robin balancing.
  2. Add an active health check against each backend.
  3. Prove the failover actually works by stopping one backend and confirming the frontend keeps serving.

Done when: haproxy -c -f /etc/haproxy/haproxy.cfg reports no errors; repeated requests against the frontend return 200 and are served by both backends; and after one backend is stopped, requests against the frontend still return 200, served entirely by the surviving backend. (4 points)

Show the worked solution
# /etc/haproxy/haproxy.cfg (additions)
frontend telemetry_front
    bind *:80
    default_backend telemetry_back

backend telemetry_back
    balance roundrobin
    option httpchk GET /healthz
    server web1 10.0.2.11:8080 check
    server web2 10.0.2.12:8080 check
haproxy -c -f /etc/haproxy/haproxy.cfg       # validate BEFORE reload — never reload blind
systemctl enable --now haproxy
systemctl reload haproxy

for i in 1 2 3 4; do curl -s localhost/ -o /dev/null -w "%{http_code}\n"; done
# prove the health check actually removes a dead backend
systemctl stop web1-backend                  # simulate a failure on web1

for i in 1 2 3 4; do curl -s localhost/ -o /dev/null -w "%{http_code}\n"; done
# still all 200s — every request now served by web2 alone

Why: option httpchk paired with check on each server line is what makes this a real load balancer instead of a static round-robin list — without an active health check, HAProxy keeps sending a share of every request to a dead backend, and each of those requests simply times out for the client. Validating with haproxy -c before reload matters for the same reason sshd -t mattered on the earlier papers in this set: a syntax error in a config file that's already running doesn't get applied by a reload, it gets silently ignored, and the failure this test is looking for never actually happens.

N6 · Route one internal domain's DNS lookups to a different resolver

Queries for anything under *.mission.internal need to go to an internal resolver, 10.0.0.53, while every other lookup keeps using the normal upstream DNS. A previous engineer's fix was a single /etc/hosts entry, which doesn't scale past one hostname and breaks the moment a second internal host needs resolving.

Your task:

  1. Point eth0's DNS at 10.0.0.53 for the mission.internal domain specifically, not globally.
  2. Persist the configuration.
  3. Confirm internal names resolve via the internal resolver while everything else still resolves normally.

Done when: resolvectl status eth0 shows DNS server 10.0.0.53 with domain ~mission.internal; resolvectl query relay.mission.internal reports it was answered via eth0's configured server; and resolvectl query example.com still resolves through the normal upstream. (4 points)

Show the worked solution
resolvectl dns eth0 10.0.0.53
resolvectl domain eth0 "~mission.internal"

resolvectl status eth0
resolvectl query relay.mission.internal      # answered via eth0's DNS server
resolvectl query example.com                 # still answered via the default upstream
# /etc/systemd/network/10-eth0.network  (additions)
[Network]
DNS=10.0.0.53
Domains=~mission.internal
systemctl restart systemd-networkd
resolvectl status eth0

Why: a domain listed with a leading ~ is a routing-only domain in systemd-resolved's syntax — it tells the resolver "send queries under this suffix to this link's DNS server" without making this link the default resolver for everything, which is exactly the split-horizon behavior a single blanket /etc/hosts entry could never scale to more than one name. This is the real mechanism DNS-based service discovery relies on; a static /etc/hosts line is a one-off patch that has to be hand-maintained forever and covers exactly one hostname, never a subdomain or a host added later.

🦆 Dot's-eye view

"I used to think of networking tasks as the annoying ones — fiddly syntax, easy to get subtly wrong, hard to tell why a curl just hung instead of erroring. Then I spent an afternoon debugging exactly N2's problem on a real multi-homed box: outbound packets left looking perfectly correct, and every single reply vanished, because nothing told the kernel to route the reply back out the interface the request had actually arrived on. Six tasks like this in one sitting isn't excessive — it's the minimum rehearsal for a category of bug that doesn't announce itself as a networking problem at all. It just looks like the internet stopped working for no reason."

Storage — S1 to S5 (20 points)

☺ Like you're 10: Five jobs about protecting and organizing what's already on a disk — a safety copy before a risky change, a mirror that survives one disk dying, a fair share of space per person, a disk borrowed from another machine, and figuring out why "full" doesn't always mean what you think.

Background: LFCS — the exam, and for the tool depth, LVM & Linux Storage Tools.

S1 · Snapshot a logical volume before a risky change, then roll back

A risky in-place migration is about to run against /dev/vg_data/lv_app, mounted at /srv/app. If it corrupts data, the fix needs to be a rollback, not a restore from a separate backup system that might not even exist yet.

Your task:

  1. Create a 2G snapshot of lv_app before the migration.
  2. Assume the migration went wrong. Roll lv_app back to the snapshot.
  3. Confirm the rollback actually took effect.

Done when: before rollback, lvs vg_data lists lv_app_snap with origin lv_app; after lvconvert --merge and reactivating the volume, lvs vg_data no longer lists the snapshot at all; and the filesystem mounted at /srv/app reflects the pre-migration state. (4 points)

Show the worked solution
lvcreate -L 2G -s -n lv_app_snap /dev/vg_data/lv_app
lvs vg_data                                  # lv_app_snap ... Origin lv_app

# --- the migration runs here, and goes wrong ---

umount /srv/app
lvconvert --merge vg_data/lv_app_snap        # schedules the merge

lvchange -an vg_data/lv_app                  # deactivate...
lvchange -ay vg_data/lv_app                  # ...and reactivate to complete a merge on an in-use LV
mount /dev/vg_data/lv_app /srv/app

lvs vg_data                                  # lv_app_snap is gone — merged and consumed

Why: an LVM snapshot works by copy-on-write — it doesn't duplicate the origin's data up front, it only starts recording blocks as they change on the origin after the snapshot is taken, which is why it's cheap to create but also why leaving one running for a long time under heavy writes can fill its own allocated space and invalidate it. lvconvert --merge schedules the origin to be replaced by the snapshot's recorded changes on its next activation — for an actively mounted LV, that usually means deactivating and reactivating it (or a reboot) before the merge visibly completes, which is the detail that trips people up when the merge "doesn't seem to happen" right after the command returns.

S2 · Mirror two disks with software RAID1, then survive a simulated failure

A dataset on two spare disks, /dev/sdc and /dev/sdd, must survive a single disk failure without taking the mount offline. Build the mirror, mount it persistently, then prove it actually tolerates a failure.

Your task:

  1. Create a RAID1 array across the two disks, put a filesystem on it, and mount it persistently at /srv/mirrored.
  2. Simulate a failed member disk.
  3. Recover the array by removing the failed member and adding a replacement.

Done when: cat /proc/mdstat initially shows md0 fully in sync across both members; after mdadm --fail, it shows the array degraded but still active, with /srv/mirrored still mounted and readable; and after removing the failed disk and adding a fresh one, /proc/mdstat shows the array resyncing back to full redundancy. (4 points)

Show the worked solution
mdadm --create /dev/md0 --level=1 --raid-devices=2 /dev/sdc /dev/sdd
cat /proc/mdstat                              # resync in progress, then active/in-sync

mkfs.xfs /dev/md0
mkdir -p /srv/mirrored
mount /dev/md0 /srv/mirrored

mdadm --detail --scan >> /etc/mdadm.conf      # Debian family: /etc/mdadm/mdadm.conf — check which yours reads
echo "/dev/md0  /srv/mirrored  xfs  defaults,nofail  0  2" >> /etc/fstab
mount -a
# simulate a failed disk
mdadm /dev/md0 --fail /dev/sdc
cat /proc/mdstat                              # sdc marked (F); array degraded but still active
ls /srv/mirrored                              # still readable — the mount never had to move

# recover: drop the failed member, add a replacement
mdadm /dev/md0 --remove /dev/sdc
mdadm /dev/md0 --add /dev/sde
cat /proc/mdstat                              # resyncing back to full redundancy

Why: RAID1 mirrors every write to both members, so the array tolerates exactly one member failing without losing data or even taking the filesystem offline — the mount point never moves, which is the entire value proposition over restoring from a nightly backup after the fact. mdadm --detail --scan written into the array's own config file is what lets it reassemble automatically at boot by its recorded array UUID, rather than depending on device names like /dev/sdc staying attached to the same physical disk — the same instability a UUID-based fstab entry solves for a plain filesystem elsewhere on this trilogy.

S3 · Cap one user's disk usage without touching anyone else's

/home lives on its own ext4 filesystem, and one account, intern, keeps filling it with a runaway build cache. Set a block quota for intern specifically — 5G soft, 6G hard, with the standard grace period — without capping any other user.

Your task:

  1. Enable quota accounting on /home.
  2. Set intern's soft and hard block limits.
  3. Confirm the limits apply only to intern.

Done when: repquota /home lists intern with a 5G soft / 6G hard block limit; quota -u intern reflects the same; and repquota /home shows no limit set for any other user on the filesystem. (4 points)

Show the worked solution
# /etc/fstab — /home's line needs the usrquota option
/dev/vg_data/lv_home  /home  ext4  defaults,usrquota  0  2
mount -o remount /home
quotacheck -cum /home                         # creates aquota.user, initial usage scan
quotaon /home

# limits are in 1K blocks: 5G = 5242880, 6G = 6291456 — setquota takes no unit suffixes
setquota -u intern 5242880 6291456 0 0 /home

repquota /home                                # intern ... 5242880 6291456 ...
quota -u intern

Why: the soft limit is a warning threshold with a grace period attached, and the hard limit is the actual wall — an account can sit over its soft limit for the whole grace window while still doing real work, and only gets forcibly blocked once it crosses the hard limit or the grace period itself expires, which is deliberately more forgiving than one hard cap that refuses a write the instant usage ticks over a single number. Quotas are tracked per filesystem, not per directory, which is exactly why this only works cleanly because /home already has its own filesystem — the same setup on a shared root filesystem would need every other account's data walled off from intern's first.

S4 · Attach a remote block device over NBD and make it reattach on boot

A remote storage host exports a raw block device over the network at storage-01:10809, export name mission-blk. It needs to appear locally as a normal block device, hold a filesystem, mount at /mnt/remote-block, and come back automatically after a reboot — without anyone manually re-running nbd-client.

Your task:

  1. Attach the export locally as /dev/nbd0, create a filesystem on it, and mount it.
  2. Write a systemd unit that performs the attach automatically, ordered after the network is up.
  3. Wire the mount to depend on that unit so it doesn't try to mount a device that was never attached.

Done when: lsblk /dev/nbd0 shows it attached and mounted at /mnt/remote-block; systemctl is-enabled nbd-mission-blk.service reports enabled; and after systemctl restart nbd-mission-blk.service && mount -a, the mount comes back with no manual nbd-client command typed by hand. (4 points)

Show the worked solution
modprobe nbd max_part=8
nbd-client storage-01 10809 /dev/nbd0 -N mission-blk
lsblk /dev/nbd0

mkfs.ext4 /dev/nbd0
mkdir -p /mnt/remote-block
mount /dev/nbd0 /mnt/remote-block
# /etc/systemd/system/nbd-mission-blk.service
[Unit]
Description=Attach mission-blk NBD export
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/modprobe nbd max_part=8
ExecStart=/usr/sbin/nbd-client storage-01 10809 /dev/nbd0 -N mission-blk
ExecStop=/usr/sbin/nbd-client -d /dev/nbd0

[Install]
WantedBy=multi-user.target
# /etc/fstab
/dev/nbd0  /mnt/remote-block  ext4  _netdev,x-systemd.requires=nbd-mission-blk.service  0  0
systemctl daemon-reload
systemctl enable --now nbd-mission-blk.service
mount -a

Why: unlike a local disk or even an NFS export, an NBD device has to be explicitly attached with nbd-client before it exists as a block device at all — /dev/nbd0 is a pre-created placeholder node, not something the kernel discovers on its own, so nothing in /etc/fstab by itself can make it appear. x-systemd.requires= is what lets a plain fstab line depend on that attach service finishing first — the same "wait for the thing that has to exist before the mount" problem _netdev alone solves for NFS, extended here to a device that has to be manufactured by a client command before it can be mounted.

S5 · Diagnose a "disk full" error on a filesystem that isn't full

A batch job writing into /var/spool/jobs — its own ext4 filesystem — keeps failing with No space left on device, but df -h reports the filesystem is only 40% full by capacity. Find the real cause and fix it without deleting anything the job still needs.

Your task:

  1. Explain the contradiction between the error and what df -h reports, without guessing.
  2. Identify which directory is actually responsible.
  3. Reclaim the resource that's actually exhausted, without touching unrelated live job files.

Done when: df -i /var/spool/jobs shows IUse% back under a safe threshold; the batch job's next write succeeds; and a file count of the directory before and after shows only stale files were removed. (4 points)

Show the worked solution
df -h /var/spool/jobs                         # 40% full by space — doesn't explain the error
df -i /var/spool/jobs                         # IUse% 100%, IFree 0 — every inode is used up

find /var/spool/jobs -xdev -type d -exec sh -c \
  'echo "$(find "$1" -maxdepth 1 | wc -l) $1"' _ {} \; | sort -n | tail
# identifies /var/spool/jobs/tmp/ as the directory holding millions of tiny files
# clean up genuinely stale files only — never a blind rm -rf
find /var/spool/jobs/tmp -type f -empty -mtime +7 -delete

df -i /var/spool/jobs                         # inodes freed, IUse% well under 100%

Why: df -h reports free bytes, but every filesystem also has a fixed number of inodes decided at mkfs time — one inode per file or directory regardless of that file's size — so a directory holding millions of near-empty files can exhaust every inode while barely touching the byte budget, and the kernel refuses new files with the exact same ENOSPC "No space left on device" error either way. df -i is the one command that tells the two failure modes apart; jumping straight to hunting for large files here would send you looking for a problem that doesn't exist, on a filesystem whose actual problem is file count, not file size.

Essential Commands — E1 to E3 (20 points)

☺ Like you're 10: Three jobs — finding exactly which change broke something without checking every change one by one, freeing up space a log file is quietly hoarding, and proving a website's certificate is trusted all the way up, not just today.

Background: LFCS — the exam, and for the TLS half, cert-manager automates in a cluster exactly the chain problem E3 makes you diagnose by hand.

E1 · Find the exact commit that broke a script with a binary search, not a guess

deploy.sh in the ops-scripts repository started silently exiting 1 sometime in the last dozen commits, and nobody knows which one. HEAD is broken; the tag v3 is known good.

Your task:

  1. Use a binary search over the commit history to find the exact first bad commit — not a manual walk through each one.
  2. Confirm which commit it is.
  3. Leave the repository in a clean, restored state when you're done.

Done when: the bisect process reports a single specific commit hash as the first bad commit; git status afterward shows a clean working tree with no detached-HEAD leftover; and that commit's diff plausibly touches deploy.sh. (7 points)

Show the worked solution
cd ops-scripts
git bisect start
git bisect bad HEAD
git bisect good v3
# git checks out a midpoint commit automatically

./deploy.sh --dry-run; echo $?          # test at each step git lands you on
git bisect good                          # or: git bisect bad — depending on the result
# ... repeat; git narrows the range by half each time

# once git reports "<hash> is the first bad commit":
git show <hash>                          # confirm the diff touches deploy.sh
git bisect log > bisect-report.txt      # optional record of the search
git bisect reset                         # return to the original branch and HEAD

Why: bisect is a binary search over commit history — with a dozen suspect commits it needs roughly four tests, not twelve, to isolate the exact one, which is the entire reason it beats manually checking out and testing each commit in order. git bisect reset at the end matters as much as the search itself: bisect leaves the working tree in a detached-HEAD state at whatever commit it last checked out, and skipping the reset is how you accidentally keep working — or worse, commit — on a detached head with no branch pointing at it.

E2 · Rotate a runaway log without leaking the disk space back to nothing

beacon-api.log under /var/log/beacon/ has grown to 40G because no rotation was ever configured, and it's now the actual cause of repeated disk-full alerts on /var. Configure rotation, and make sure the running service actually releases the old file's space rather than continuing to write into it under a different name.

Your task:

  1. Configure daily rotation, keeping 14 days compressed.
  2. Make sure the writing service reopens its log file after each rotation.
  3. Prove a forced rotation right now actually frees space, not just renames the file.

Done when: logrotate -d /etc/logrotate.d/beacon-api reports no config errors; a forced rotation produces a rotated .1 file plus a small, fresh beacon-api.log; and checking the service's open file descriptors shows it holding the new file, not the rotated one. (7 points)

Show the worked solution
# /etc/logrotate.d/beacon-api
/var/log/beacon/beacon-api.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 beacon beacon
    postrotate
        systemctl kill -s HUP beacon-api.service
    endscript
}
logrotate -d /etc/logrotate.d/beacon-api      # dry run — shows what WOULD happen, changes nothing
logrotate -f /etc/logrotate.d/beacon-api      # force one rotation right now to prove it works

ls -la /var/log/beacon/                        # beacon-api.log.1 present, new beacon-api.log small again
ls -la /proc/$(pgrep beacon-api)/fd | grep beacon-api.log   # holding the NEW file, not the rotated one
df -h /var

Why: rotation alone — renaming the log file — doesn't help if the writing process keeps its original file descriptor open; it just keeps appending to what is now an unlinked, invisible file that still occupies disk space until the process exits, which recreates exactly the disk-full problem this task exists to solve. The postrotate block's HUP signal is what tells the running process to close and reopen its log file by name, picking up the fresh file logrotate just created — skipping it is the most common way this task "passes" the file-count check and still frees no actual space.

E3 · Diagnose and fix a broken TLS chain, not a broken certificate

An internal API at api.mission.internal serves a certificate signed by a private intermediate CA, but some clients report a trust failure even though the certificate itself hasn't expired. Determine whether the service is actually sending the full chain, and fix it if it isn't.

Your task:

  1. Determine how many certificates the live service actually sends during a handshake.
  2. Prove whether the chain verifies all the way to a trusted root.
  3. Fix the served chain if the intermediate is missing.

Done when: openssl s_client -showcerts against the live service returns two certificates — leaf then intermediate — not one; openssl verify -CAfile <root> -untrusted <intermediate> <leaf> reports OK; and a client that trusts only the root (not the intermediate directly) completes the handshake without a chain error. (6 points)

Show the worked solution
openssl s_client -connect api.mission.internal:443 -servername api.mission.internal \
  -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE"
# 1 — only the leaf is being sent. The intermediate is missing from the wire.

openssl verify -CAfile /etc/pki/ca-trust/mission-root-ca.pem \
  -untrusted intermediate-ca.pem server.pem
# fails without the intermediate supplied separately
# fix: serve leaf + intermediate concatenated, in that order
cat server.pem intermediate-ca.pem > /etc/nginx/tls/api.mission.internal-fullchain.pem
# update nginx's ssl_certificate directive to point at the fullchain file, not the bare leaf

nginx -t && systemctl reload nginx

openssl s_client -connect api.mission.internal:443 -showcerts </dev/null 2>/dev/null \
  | grep -c "BEGIN CERTIFICATE"
# now 2 — leaf and intermediate both sent

Why: a CA hierarchy only works if every hop between the leaf and a certificate the client already trusts is actually present during the handshake — a server can hold a perfectly valid intermediate certificate on disk and still fail every client that doesn't already trust that intermediate directly, if it never sends it over the wire. openssl verify -untrusted mirrors exactly what a real client does: build a path from the leaf up through supplied intermediates to a trusted root, and refuse to trust a leaf it can't chain all the way up on its own.

Users & Groups — U1 to U2 (10 points)

☺ Like you're 10: The smallest slice of the exam, but two of its cheapest points — giving every account a shared set of tools automatically, and letting one host trust a shared directory of accounts without breaking local logins if that directory ever goes quiet.

Only 10% of the paper, but per the flag-and-move discipline above, never the two tasks worth skipping.

U1 · Set a shared environment for everyone, plus an extra for one account

Every account on this host needs MISSION_HOME=/opt/mission and /opt/mission/bin on its PATH by default. One service account, svc-nav, additionally needs /opt/mission/nav-tools on its own PATH — a directory no other account should see added.

Your task:

  1. Add the shared environment variable and PATH entry for every account, system-wide.
  2. Add svc-nav's extra PATH entry without affecting any other account.
  3. Confirm both layers apply correctly for the right accounts.

Done when: su - alice -c 'echo $PATH' (any other real account) includes /opt/mission/bin but not /opt/mission/nav-tools; su - svc-nav -c 'echo $PATH' includes both; and MISSION_HOME is set correctly under any login session. (4 points)

Show the worked solution
# /etc/profile.d/mission.sh — sourced automatically for every login shell
export MISSION_HOME=/opt/mission
export PATH="$PATH:/opt/mission/bin"
chmod 644 /etc/profile.d/mission.sh
# ~svc-nav/.bash_profile
[ -f /etc/profile ] && . /etc/profile
export PATH="$PATH:/opt/mission/nav-tools"

chown svc-nav:svc-nav ~svc-nav/.bash_profile
su - alice -c 'echo $MISSION_HOME; echo $PATH'    # bin present, nav-tools NOT present
su - svc-nav -c 'echo $PATH'                       # both present

Why: /etc/profile.d/*.sh is the supported place for system-wide environment additions — every file in it gets sourced by /etc/profile for every login shell, which is why it wins over hand-editing /etc/profile itself directly (a package update can legitimately replace that file; a drop-in survives it, the same pattern as a systemd unit override elsewhere in this trilogy). A per-account addition has to layer on top in that account's own shell startup file, and has to explicitly source the system profile first if it wants both layers — skip that line and svc-nav silently loses MISSION_HOME while still gaining its extra PATH entry.

U2 · Point one host at LDAP-backed accounts without breaking local logins

This host needs to authenticate one group of contractor accounts against an existing corporate LDAP directory at ldap://ldap.mission.internal, while every local account — including root — must keep working exactly as before if LDAP is briefly unreachable.

Your task:

  1. Configure SSSD to look up accounts against the LDAP directory.
  2. Set the NSS lookup order so local accounts are always checked first.
  3. Prove local accounts survive an LDAP outage, while LDAP-only accounts don't.

Done when: getent passwd <ldap-user> returns a full LDAP-backed entry; /etc/nsswitch.conf lists files before sss on the passwd and group lines; and after systemctl stop sssd, getent passwd root and a local-account login both still work, while the LDAP-only account stops resolving until sssd is back. (6 points)

Show the worked solution
# /etc/sssd/sssd.conf
[sssd]
services = nss, pam
domains = mission.internal

[domain/mission.internal]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldap://ldap.mission.internal
ldap_search_base = dc=mission,dc=internal
cache_credentials = true
chmod 600 /etc/sssd/sssd.conf
authselect select sssd --force
systemctl enable --now sssd

grep -E "^(passwd|group):" /etc/nsswitch.conf
# passwd: files sss
# group:  files sss

getent passwd contractor.jane          # resolves via LDAP through sssd
id contractor.jane
getent passwd root                     # resolves purely from /etc/passwd, no LDAP round trip
# prove the outage behavior
systemctl stop sssd
getent passwd root                     # still works — local, unaffected
getent passwd contractor.jane          # fails to resolve until sssd is back

Why: the order in nsswitch.conf is a search order, not a preference — files sss means every lookup checks /etc/passwd//etc/group first and only falls through to SSSD (and therefore LDAP) for names not found locally, which is precisely what keeps root and other local accounts working through an LDAP outage. This is "configure the system to use LDAP user and group accounts" in its actual exam shape: not hand-writing raw LDAP PAM modules, but standing up SSSD as the intermediary and getting the NSS search order right around it.

Score yourself

☺ Like you're 10: Add up the points, turn it into a percentage, and then do the more useful thing — look at which topic cost you the most and go fix that specifically.

Mark after a short break — grading your own work while still adrenalized produces generous nonsense. Award full points only when the done-when check actually passed for real, half when the resource exists and is broadly right but the check didn't pass cleanly, and zero for anything unattempted.

TaskDomainPointsYour score
OD1 · libvirt VM, import + autostartOperations Deployment9
OD2 · Signed repo + versionlockOperations Deployment8
OD3 · Recover from a bad fstab bootOperations Deployment8
N1 · VLAN subinterface + persistenceNetworking5
N2 · Policy routing across two uplinksNetworking4
N3 · Bridge device for lab guestsNetworking4
N4 · IPv6 address + proven routeNetworking4
N5 · HAProxy round-robin + health checkNetworking4
N6 · Split-horizon DNS via resolvedNetworking4
S1 · LVM snapshot + merge rollbackStorage4
S2 · mdadm RAID1 + simulated failureStorage4
S3 · Per-user disk quotaStorage4
S4 · NBD attach + persistent unitStorage4
S5 · Inode exhaustion diagnosisStorage4
E1 · git bisect to a root causeEssential Commands7
E2 · logrotate + service reopenEssential Commands7
E3 · TLS chain diagnosis + fixEssential Commands6
U1 · System-wide + per-account profileUsers & Groups4
U2 · SSSD/LDAP + NSS orderUsers & Groups6
TotalAll five domains100

Computing your result. The points total 100, so your raw score is your percentage, against the published 67% pass mark. Then do the more useful arithmetic: total each domain separately, and this time also compare against your Set 1 and Set 2 domain scores if you sat them. A domain that improved across all three sittings is a real signal; a domain that's flat or worse despite this paper giving it more repetitions is the one that needs a different kind of practice, not just more of the same kind.

DomainAvailableYoursIf you scored under two-thirds, go here
Operations Deployment25LFCS — the exam, then the practice task bank for repo management and service-recovery drills.
Networking25LFCS — the exam and Networking, then re-drill the practice bank against real ip/nft/resolvectl commands.
Storage20LVM & Linux Storage Tools, then the practice bank.
Essential Commands20LFCS — the exam, then the practice bank for the Git and certificate-chain shapes specifically.
Users & Groups10LFCS — the exam, then a fast re-read of man 5 nsswitch.conf on your own machine.

What your score means

☺ Like you're 10: This is the last of the three papers, so the question now isn't "which set do I sit next" — it's "am I actually ready to book."

Treat the number as a diagnostic, never a prediction, same as the earlier two papers. But because this is the third and final sitting in this course, read the band below as a booking signal, not just a study signal.

ScoreRead it asNext move
85–100Comfortably ready, with margin for an unlucky task draw, and this held up even with more Networking/Storage repetitions than the other two papers gave you.Stop grinding tasks. Skim the exam logistics one more time, then book it.
67–84A pass on this paper, but check whether your weak domain is the same one that was weak on Set 1 or Set 2 — a repeat offender needs a different kind of practice, not another timed sitting.Re-drill only your weakest domain from the table above, then reattempt a mixed set of tasks from the practice bank before booking.
50–66Close, usually concentrated in one or two domains rather than spread evenly.A focused week on your two weakest domains, then re-sit this exact paper cold in seven to ten days.
Under 50Genuine gaps that a third timed paper alone won't close.Back to LFCS — the exam and the study plan for your lowest-scoring domains, then return to Set 1 with fresh eyes before attempting this one again.

One check worth running across all three sittings, not just this one: did your Networking and Storage scores actually improve from Set 1 through Set 2 to this paper? If they did, the extra repetitions this paper deliberately front-loaded did their job. If they didn't, the gap probably isn't repetition at all — it's a specific concept (policy routing, snapshot merge semantics, a health-check flag) worth reading about directly rather than drilling blind again.

🎬 At Mission Control
🐦

Pip: Six networking tasks and I still lost points on N2. Not the syntax — I forgot the second route inside the table, just added the rule and stopped.

🦫

Benny: Storage went cleaner for me this time. Twenty out of twenty. Five small jobs beat one big one — I never lost my place.

👺

Gizmo: See, Pip? Networking's not worth the trouble. Just memorize one static route command and call it a day. 😈

🐢

Timmy: One static route command doesn't cover a bridge, a bond, a VLAN, IPv6 and a load balancer, Gizmo. Pip's fix isn't "give up," it's "add the second route to the same table before moving on."

🦊

Foxy: Wait — Pip, did you actually run ip route get ... from ... to prove it, or did ip rule show just look right?

🐦

Pip: ...I trusted ip rule show. That's the mistake, isn't it. A rule pointing at an empty table proves nothing.

🐢

Timmy: That's the whole trilogy in one sentence. Three papers, same lesson every time: prove the outcome, not the configuration. Sit the practice bank on N2's exact shape, then you're done with this course's mock exams.

🐢 Timmy's checkpoint

1. Why does policy-based routing (an ip rule plus a second table) solve a problem a single default route cannot, and what's the tell-tale symptom when it's missing on a multi-homed host? 2. What's the actual difference between lvconvert --merge on a snapshot and simply deleting the snapshot, and why might the merge not finish immediately on a mounted origin? 3. df -h and df -i can disagree completely about a "disk full" complaint. What does each one actually measure? 4. In an HAProxy backend, what does the check keyword plus option httpchk actually give you that a plain list of servers doesn't? 5. Your nsswitch.conf reads passwd: files sss. Why does that order matter for surviving an LDAP outage? 6. A logrotate postrotate script renamed beacon-api.log, but disk usage didn't drop. What's actually still holding the space? 7. Name the one thing Set 3 changed relative to Set 1 and Set 2, and the one thing it deliberately kept identical.

Check your answers
  1. A normal routing table has exactly one default route; a packet that must exit through the same interface its source address belongs to needs a lookup keyed on source address, run before the ordinary destination-based lookup — that's what ip rule supplies. Missing it shows up as traffic leaving with a source address on one link's subnet but exiting through a different interface, which a remote firewall correctly treats as spoofed and silently drops.
  2. lvconvert --merge replaces the origin's data with the snapshot's recorded copy-on-write changes — the opposite of simply deleting the snapshot, which discards those changes instead. Because the origin is normally still mounted, the merge is usually scheduled rather than completed on the spot, finishing only once the LV is deactivated and reactivated (or the host reboots).
  3. df -h measures free bytes; df -i measures free inodes, a fixed count set at mkfs time independent of file size. A directory full of millions of tiny or empty files can exhaust inodes while barely touching byte usage, producing the exact same ENOSPC error either way — df -i is what tells the two apart.
  4. It makes HAProxy actively probe each backend and automatically pull a failed one out of rotation — without it, HAProxy keeps sending a share of every request to a dead server, and each of those requests simply times out for the client.
  5. files first means every lookup checks the local /etc/passwd//etc/group before ever falling through to SSSD, so local accounts — including root — keep resolving with zero dependency on LDAP being reachable. Reversed, a slow or unreachable LDAP server could stall or fail logins for accounts that never needed LDAP at all.
  6. The process that was writing to the original file still holds its old file descriptor open even after the rename — it keeps appending to a now-unlinked file, and the space isn't released until that descriptor closes. The postrotate signal tells the process to close and reopen its log by name, which is what actually lets the disk space free up.
  7. Task count changed — Networking grew to six tasks and Storage to five, versus four apiece on Set 1 and Set 2, trading per-task point value for more independent repetitions in the two domains this paper targets. What stayed identical is the 100-point total and the official 25/25/20/20/10 domain weighting, so scores remain directly comparable, domain for domain, across all three sittings.

That's all three papers. If you've sat all of them, you now have three domain-by-domain scorecards instead of one — look at the trend, not any single number, and let a domain that's stayed weak across all three sittings decide what you study before you book anything. For the deeper cross-reference against a platform engineer's actual daily use of this material, see Platform Engineering's own LFCS page, and for booking mechanics, retake terms and the retired LFCE, see LFCS — the exam and the official LFCS certification page.

⏱️ The three LFCS papers

Set 1 · Set 2 · Set 3 (you are here). All three are weighted to the same five official domains and worth 100 points each, so scores are directly comparable domain for domain, even though this paper spreads Networking and Storage across more, smaller tasks. See the LFCS study plan for where each paper fits against your remaining prep time, the practice task bank for shorter drills in between, and LFCS — the exam for the full domain writeup and current logistics.