Certifications · LFCS · Mock Exam · Set 2

LFCS Mock Exam · Set 2

Same protocol as before, an entirely different paper. Twenty performance-based tasks, one unbroken two-hour block, a hundred points split exactly 25 / 25 / 20 / 20 / 10 across Operations Deployment, Networking, Storage, Essential Commands and Users and Groups — the split the LFCS blueprint lays out and Set 1 already put you through once. What's different this time is the twenty jobs themselves: IPv6 instead of IPv4, a libvirt guest instead of a container, an LDAP directory lookup instead of a local account, an SELinux port context instead of a bind-mount label. If Set 1 taught you the shape of the exam, Set 2 tests whether you actually learned the domains or just memorised twenty specific answers. Every task still carries a plain brief, a done when check you run yourself, and a full worked solution folded shut until you've either passed it or flagged it and moved on — and the order is shuffled again so no two neighbouring tasks share a domain. Rebuild (or reuse) your throwaway box, skim the pre-flight below for what's new this time, then start one timer and hold it for the full two hours.

☺ Explain it like I'm 10

Imagine you passed your first pre-flight check on a little training capsule — you know where the switches are now. Set 2 straps you into a different capsule. Some panels are in familiar places, some aren't, and one or two switches you've genuinely never touched before are staring back at you. A good astronaut doesn't panic at the unfamiliar panel — they use the same checklist habit that worked last time: read the whole board before touching anything, fix the biggest warning light first, and never skip the step that proves the fix actually worked. That's the entire point of a second paper. It's not there to be harder for the sake of it. It's there to prove the habit travels, not just the memory of switch positions from last time.

🦥🦊Your hosts for this paper: Sol the Sloth & Foxy the Fox — Sol still refuses to let anyone rush a partition table or a snapshot merge, and Foxy, who never met a broken thing she didn't want to poke at, is the right host for a paper where half the tasks start with "something's wrong here — find out why."

How this paper is weighted

☺ Like you're 10: Same split as last time — the two biggest topics still get five jobs each, and the smallest still gets only two.

This paper is built to the same five official weights as Set 1 — 25, 25, 20, 20, 10 — so your score here is directly comparable to your Set 1 score, domain for domain. That comparability is the whole point of sitting a second paper rather than just re-reading Set 1's solutions: if Networking cost you points on both papers, that's a real gap, not a fluke of one particular task. If it cost you on Set 1 but not here, the gap has probably closed.

⚙️Operations Deployment
25%
📡Networking
25%
💾Storage
20%
⌨️Essential Commands
20%
👥Users and Groups
10%
DomainOfficial weightTasks herePoints
⚙️ Operations Deployment25%525
📡 Networking25%525
💾 Storage20%420
⌨️ Essential Commands20%420
👥 Users and Groups10%210
◆ Key idea

Compare this paper's domain breakdown against Set 1's, not just the two raw totals. A jump from 15/25 to 22/25 on Networking tells you the gap actually closed. A flat 15/25 on both tells you Set 1's remediation reading didn't stick, and rereading it a second time with the same eyes is unlikely to do more the second time either — this is the point to switch to hands-on drilling instead, via the practice-task bank.

Pre-flight — what's new to check this time

☺ Like you're 10: Same rule as before — get every light green the day before — but this paper touches a couple of switches you didn't need last time, so check those specifically.

Reuse your Set 1 practice box if it's still around; everything below layers on top of it. Two things are genuinely new. First, Q4 needs nested virtualization — this box has to be able to run a VM of its own, which is a property of the hypervisor underneath your practice box, not something getenforce or lsblk will ever tell you. Check it before the clock starts, because a libvirt guest that silently refuses to boot on a non-nested host looks nothing like a libvirt problem at first glance. Second, partition the spare disk into two pieces this time rather than one — Q2 wants a plain filesystem, Q8 wants an LVM volume group, and giving both tasks the same unpartitioned disk is how you end up rebuilding a partition table mid-sitting instead of doing the task in front of you.

# --- the night before -----------------------------------------------
sudo dnf install -y lvm2 nftables qemu-kvm libvirt virt-install sssd sssd-ldap \
  authselect nfs-utils sysstat iotop logrotate at policycoreutils setools-console \
  git openssl e2fsprogs
# (Debian/Ubuntu family: swap sssd-ldap → sssd-ldap same name; drop policycoreutils
#  and setools-console — no SELinux on this family, so retarget Q16 as a stretch read
#  rather than a hands-on task; nfs-common instead of nfs-utils)

# nested virtualization — required for Q4, not implied by anything else on this list
cat /sys/module/kvm_intel/parameters/nested 2>/dev/null || \
cat /sys/module/kvm_amd/parameters/nested   2>/dev/null
# 'Y' or '1' = you're clear. Empty/N/no such file = fix this BEFORE the clock starts,
# not mid-task-four.

sudo systemctl enable --now libvirtd atd

lsblk                                  # confirm the spare disk is visible and empty
sudo parted /dev/sdb --script mklabel gpt \
  mkpart primary ext4 1MiB 3GiB \
  mkpart primary 3GiB 100%             # sdb1 for Q2, sdb2 left raw for Q8's LVM
getenforce                             # Enforcing/Permissive = Q16 is workable here

# --- ninety seconds before the clock starts --------------------------
whoami ; hostname ; ip -br addr
sudo -i whoami
date
🦊 Foxy's workshop · 15 min

Before the full sitting, dry-run the protocol on just the two riskiest tasks: Q4 (the long pole, and the one new-equipment task on the whole paper) and Q7 (a boot-recovery task, which by nature starts from "something is already broken" rather than "build this from scratch"). Work them back to back with a forced two-minute detour into an unrelated shell in between, the same drill Set 1 used on its own long pole. If Q4 fails silently — virsh list --all shows the guest but it never reaches a running state — that is very likely the nested-virtualization check above, not a typo in your virt-install invocation. Confirm that now, not at minute nine of the real sitting.

Sit it like the real thing

☺ Like you're 10: Root access, a real terminal, someone watching, and the only book allowed on the desk is the one built into the computer itself.

Nothing changes here from Set 1, deliberately — the protocol is exactly what you're rehearsing. Work as root via sudo -i, treat any task naming a designated host as a real SSH hop, remember terminal copy/paste is Ctrl+Shift+C / Ctrl+Shift+V, and allow yourself only man pages and whatever documentation ships with the distribution — accessed from inside the terminal, nothing else. Close every tab except a local man page before you start the clock.

⚠ Exam details change — verify officially before booking

This is an independent, unofficial study resource, not affiliated with the CNCF or The Linux Foundation. The 17–20 task count, the 2-hour duration and the 67% pass mark used to calibrate this paper come from the Linux Foundation's own published Important Instructions for the LFCS as of writing — but price, retake terms, eligibility windows, proctoring rules and even the domain weights are all figures the Foundation revises without much notice. Confirm every one of them on the official LFCS page and the candidate handbook before you register or pay for anything — this page is a study aid, not a substitute for reading them.

Your time budget

☺ Like you're 10: Give every job a small slice of the two hours. When a job runs past its slice, leave a note on it and move to the next one — you can come back if there's time left over.

The arithmetic is identical to Set 1 — 5 minutes to read, 109 minutes of task work, 6 minutes to verify — but the minutes land differently across the four blocks this time, because the long pole moved. Q4's libvirt guest, not Q3's LVM build, is the task that eats the most clock on this paper.

BlockTasksBudgetRunning totalNote
Read every brief5 min5Mark each task cheap / medium / expensive before you touch a terminal.
Block 1Q1–55+6+5+9+4 = 29 min34Q4 is the long pole on the whole paper. Give it its full nine, not more.
Block 2Q6–105+7+6+4+5 = 27 min61Q7's boot recovery is fiddly to read correctly under pressure — budget the full seven.
Block 3Q11–155+6+5+6+6 = 28 min89Three back-to-back six-minute tasks here. None of them is free, but none is the long pole either.
Block 4Q16–205+5+6+6+3 = 25 min114Q20 closes the paper cheap and fast — bank it, don't overthink it.
Verify & sweep flags6 min120Re-run every done-when you flagged. Finish the nearly-done before starting anything new.

The flag-and-move rule still applies, unchanged. When a task passes its budgeted minutes without a passing done-when check, stop, write one line about where you stalled, leave your partial work in place, and go to the next task. It was the single biggest lever on Set 1's scores and nothing about this paper changes that.

120 minutes · 20 tasks · 100 points · 67% to pass read 5m Block 1 Q1–5 · 29m Block 2 Q6–10 · 27m Block 3 Q11–15 · 28m Block 4 Q16–20 · 25m verify 6m Weighted to the five official LFCS domains Ops Deploy 25% Networking 25% Storage 20% Ess. Commands 20% Users 10% No two neighbouring tasks share a domain Every block still mixes multiple domains — IPv6, then ext4, then a Git cherry-pick, back to back. Flag-and-move: the moment a task's budgeted minutes run out, note where you stalled and move on.

The paper — 20 tasks across four blocks

☺ Like you're 10: Twenty new jobs, in the order you should meet them. Read the brief, do the job on your own machine, check it against the done-when line, and only then open the folded solution.

Same rule as Set 1: each task names its domain in parentheses, work every task on your own practice box before opening its solution, and consider a second cold pass later with the domain labels covered. A handful of tasks below name a designated host — dc1.internal, nas1 — that this paper treats as already provisioned, the same way the real exam hands you a designated host rather than making you build one first.

Block 1 — Q1–5

Q1 (Networking · 5 pts, ~5 min). On interface eth1, configure a static IPv6 address 2001:db8:56::50/64 with default gateway 2001:db8:56::1, persistent across reboots. Confirm the host can reach the gateway.

Done when: ip -6 addr show eth1 shows the address after reapplying the connection, and ip -6 route show default shows the gateway via eth1.

Worked solution — Q1
nmcli con mod eth1 ipv6.method manual
nmcli con mod eth1 ipv6.addresses 2001:db8:56::50/64
nmcli con mod eth1 ipv6.gateway 2001:db8:56::1
nmcli con up eth1

ip -6 addr show eth1
ip -6 route show default
ping -6 -c 2 2001:db8:56::1

The competency reads "configure IPv4 and IPv6 networking" for a reason — a station that only knows ipv4.* keys in nmcli stalls completely here. The same connection profile carries both families side by side; nothing about the tool changes, only the key prefix and the address syntax.

Q2 (Storage · 5 pts, ~6 min). Using /dev/sdb1, create an ext4 filesystem labelled DATA, reduce its reserved-blocks percentage to 1%, and mount it persistently at /srv/archive with noatime.

Done when: blkid shows LABEL="DATA" on /dev/sdb1, tune2fs -l shows the lowered reserved-block count, and mount | grep /srv/archive shows noatime after a fresh mount -a.

Worked solution — Q2
sudo mkfs.ext4 -L DATA /dev/sdb1
sudo tune2fs -m 1 /dev/sdb1
sudo mkdir -p /srv/archive

UUID=$(blkid -s UUID -o value /dev/sdb1)
echo "UUID=$UUID  /srv/archive  ext4  defaults,noatime  0 2" | sudo tee -a /etc/fstab
sudo mount -a

mount | grep /srv/archive
tune2fs -l /dev/sdb1 | grep -i reserved

ext4 reserves 5% of the filesystem for root by default — a sane guard on a root filesystem, so a runaway log can't lock root out of writing anywhere at all. On a large, non-root data volume that 5% is often just wasted capacity; tune2fs -m is the one-line fix, and it's live-adjustable without reformatting.

Q3 (Essential Commands · 5 pts, ~5 min). In an existing Git repository with a feature branch two commits ahead of main, cherry-pick only the second of those two commits onto main — not the whole branch — then tag the resulting commit v1.1 with an annotated tag.

Done when: git log --oneline main shows the cherry-picked change with a different commit hash than it has on feature, the first of the two feature commits is absent from main, and git tag -n shows v1.1 pointing at the new commit.

Worked solution — Q3
git init mission-config && cd mission-config
echo "stage: alpha" > launch.yaml
git add launch.yaml && git commit -m "initial state"

git switch -c feature
echo "stage: beta"  > launch.yaml && git commit -am "bump to beta"
echo "checklist: complete" >> launch.yaml && git commit -am "mark checklist complete"

git switch main
git log --oneline feature                # note the hash of "mark checklist complete"
git cherry-pick <that-hash>               # only THIS commit lands on main

git tag -a v1.1 -m "checklist-complete milestone"
git log --oneline main
git tag -n
git show v1.1 --stat

A cherry-pick replays a commit's changes as a brand-new commit with a new hash and a new parent — it is not the same object moved sideways, which is exactly why main ends up with the checklist change but not the beta-stage change that came before it on feature. Confusing this with a merge is the most common way this task shape loses points.

Q4 (Operations Deployment · 5 pts, ~9 min — the long pole on this paper). Using the existing base image /var/lib/libvirt/images/base.qcow2, define and start a new KVM guest named relay-vm with 2 vCPUs and 2048 MB of RAM on the default NAT network, and set it to autostart on host boot.

Done when: virsh list --all shows relay-vm in a running state, virsh dominfo relay-vm reports the correct vCPU count and memory, and virsh list --autostart includes it.

Worked solution — Q4
sudo qemu-img create -f qcow2 -F qcow2 \
  -b /var/lib/libvirt/images/base.qcow2 \
  /var/lib/libvirt/images/relay-vm.qcow2

sudo virt-install \
  --name relay-vm \
  --memory 2048 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/relay-vm.qcow2,format=qcow2 \
  --network network=default \
  --os-variant detect=on \
  --import \
  --noautoconsole

sudo virsh autostart relay-vm
virsh list --all
virsh dominfo relay-vm
virsh list --autostart

The -b/-F pair on qemu-img create builds a copy-on-write overlay against the base image rather than copying it wholesale — the base stays pristine and reusable for the next guest, and this single-guest disk starts out consuming almost no space of its own. --import tells virt-install the disk already has a bootable OS on it, skipping the installer entirely — the wrong flag here (or omitting --noautoconsole on a headless box) is the single most common way this task stalls without an obvious error.

Q5 (Users and Groups · 5 pts, ~4 min). Grant user dot passwordless sudo to run only systemctl restart beacon.service — nothing else — and separately set a hard limit of 100 processes (nproc) for dot.

Done when: sudo -u dot sudo -n systemctl restart beacon.service succeeds with no password prompt, sudo -u dot sudo -n whoami is denied, and a fresh login shell for dot reports ulimit -u as 100.

Worked solution — Q5
echo 'dot ALL=(root) NOPASSWD: /usr/bin/systemctl restart beacon.service' \
  | sudo tee /etc/sudoers.d/dot-beacon
sudo visudo -cf /etc/sudoers.d/dot-beacon   # syntax-check THIS file before it's live
sudo chmod 440 /etc/sudoers.d/dot-beacon

echo 'dot   hard   nproc   100' | sudo tee /etc/security/limits.d/91-dot-nproc.conf

sudo -u dot sudo -n systemctl restart beacon.service   # works, no prompt
sudo -u dot sudo -n whoami                              # denied — not on the allowlist
su - dot -c 'ulimit -u'

visudo -cf <path> validates a specific sudoers drop-in without touching the live file — skipping it is how people lock themselves, or worse, everyone, out of sudo entirely on a box with no other way in. The command in the rule has to match exactly what gets typed, full path included; a rule written against systemctl with no path is not the same grant as one written against /usr/bin/systemctl.

Block 2 — Q6–10

Q6 (Networking · 5 pts, ~5 min). This host can't resolve api.internal even though the link is up. Diagnose and repair name resolution so it queries internal DNS server 10.0.0.53, and confirm the lookup actually succeeds — not just that a DNS service is running.

Done when: resolvectl status lists 10.0.0.53 as a DNS server for the relevant link, and getent hosts api.internal returns an address with no server specified on the command line.

Worked solution — Q6
resolvectl status                          # no DNS servers configured on eth0 — the actual fault
nmcli con show eth0 | grep -i dns          # confirms the connection profile never set one

nmcli con mod eth0 ipv4.dns "10.0.0.53"
nmcli con mod eth0 ipv4.ignore-auto-dns yes
nmcli con up eth0

resolvectl status
dig +short api.internal @10.0.0.53         # confirm the server itself answers correctly
getent hosts api.internal                  # confirm the HOST now resolves it without a server flag

The same "running isn't the same as working" lesson from time sync applies here to DNS: systemd-resolved can be active and healthy with zero configured upstream servers, quietly falling back to nothing. Checking resolvectl status for an actual server, not just systemctl is-active systemd-resolved, is what separates "looks fine" from "is fine."

Q7 (Operations Deployment · 5 pts, ~7 min). An edit to /etc/fstab yesterday left this host unable to reach multi-user.target cleanly — a filesystem entry is broken. Without permanently dropping the intended mount, fix the boot so it succeeds even if that device is briefly unavailable in future.

Done when: sudo mount -a completes with no errors, systemctl status local-fs.target shows it reached, and the corrected entry still mounts the filesystem when the device is present.

Worked solution — Q7
journalctl -xb | grep -iE 'emergency|dependency failed|mount'
cat /etc/fstab                              # a stale UUID= that matches no current block device
blkid                                       # find the device's REAL current UUID

sudo sed -i "s/UUID=oldstale-uuid-here/UUID=$(blkid -s UUID -o value /dev/sdb3)/" /etc/fstab
sudo sed -i '/\/srv\/telemetry/ s/defaults/defaults,nofail,x-systemd.device-timeout=5s/' /etc/fstab

sudo systemctl daemon-reload
sudo mount -a                               # proves the corrected fstab parses and mounts cleanly
systemctl status local-fs.target

Two separate fixes, doing two separate jobs. Correcting the UUID= fixes the actual lie in the file — the device that entry pointed at doesn't exist under that identifier any more. Adding nofail and a device timeout doesn't fix a wrong UUID by itself, but hardens against the next time this class of failure happens — a genuinely absent device no longer drags the whole boot into emergency mode over one filesystem.

Q8 (Storage · 5 pts, ~6 min). On /dev/sdb2, build an LVM volume group vg_data with a 2G logical volume lv_app (XFS, mounted at /opt/app). Before simulating a risky change, snapshot it at 500M; after the change writes a marker file, use the snapshot to roll the change back.

Done when: after lvconvert --merge, /opt/app no longer contains the marker file, and lvs no longer lists the snapshot.

Worked solution — Q8
sudo pvcreate /dev/sdb2
sudo vgcreate vg_data /dev/sdb2
sudo lvcreate -L 2G -n lv_app vg_data
sudo mkfs.xfs /dev/vg_data/lv_app
sudo mkdir -p /opt/app
UUID=$(blkid -s UUID -o value /dev/vg_data/lv_app)
echo "UUID=$UUID  /opt/app  xfs  defaults  0 2" | sudo tee -a /etc/fstab
sudo mount -a

sudo lvcreate -L 500M -s -n lv_app_snap /dev/vg_data/lv_app
lvs -o lv_name,origin,snap_percent

touch /opt/app/upgrade-marker             # simulate the risky change
ls /opt/app                               # marker present

sudo umount /opt/app
sudo lvconvert --merge /dev/vg_data/lv_app_snap
sudo mount /opt/app
ls /opt/app                               # marker is GONE — origin rolled back to snapshot time
lvs

A merge needs the origin LV inactive at the moment it runs on most kernels, which is why the umount before lvconvert --merge isn't optional housekeeping — skip it and the merge simply queues itself for the next activation instead of completing, and it's easy to walk away thinking it finished when it hasn't.

Q9 (Essential Commands · 5 pts, ~4 min). A process named cruncher is pegging one CPU core near 100% and starving everything else on the box. Without killing it, lower its scheduling priority so other work gets preference, and confirm the change actually took hold.

Done when: ps -o pid,ni,cmd -C cruncher shows a nice value of 15, and a short pidstat sample shows its CPU share trending down from before.

Worked solution — Q9
top -bn1 | head -15                       # confirm cruncher's PID, nice 0, ~100% CPU
pgrep cruncher

sudo renice 15 -p $(pgrep cruncher)
ps -o pid,ni,cmd -C cruncher              # NI column now reads 15

pidstat -p $(pgrep cruncher) 1 3          # watch %CPU ease down as it yields more slices

renice is a polite request to the scheduler, not a hard ceiling — cruncher can still burst back to 100% if nothing else wants the core at that instant. A genuine cap needs a cgroup control like a unit's CPUQuota= instead; knowing which of the two a task is actually asking for is half the competency.

Q10 (Networking · 5 pts, ~5 min). Host relay (10.0.0.20, user dot) is only reachable through bastion host bastion (192.168.56.5, user ops). Configure a client-side SSH shortcut so ssh relay jumps through the bastion transparently, with no manual double-hop.

Done when: ssh -G relay resolves proxyjump to the bastion and the correct final hostname/user, and ssh relay hostname connects in one command.

Worked solution — Q10
cat >> ~/.ssh/config <<'EOF'
Host bastion
    HostName 192.168.56.5
    User ops
    IdentityFile ~/.ssh/id_ed25519

Host relay
    HostName 10.0.0.20
    User dot
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519
EOF
chmod 600 ~/.ssh/config

ssh -G relay | grep -E '^(proxyjump|hostname|user) '
ssh relay hostname                        # one command — the bastion hop is invisible here

ProxyJump (OpenSSH 7.3+) replaces the older ProxyCommand ssh -W %h:%p bastion incantation with a one-line directive that means the same thing — same result, far less to get wrong from memory under exam pressure. The equivalent works inline too, with no config file at all: ssh -J ops@192.168.56.5 dot@10.0.0.20.

🦆 Dot's-eye view

"Block 2 bounced me between 'build this from nothing' and 'something is already broken, go find out why' every single task — DNS was broken, fstab was broken, a process was misbehaving, then back to building an SSH shortcut clean. My instinct on the broken ones was to start changing things immediately. The habit that actually saved time was the boring one: read the evidence first — resolvectl status, journalctl -xb, top — before touching a single config file. Every fix I had to walk back this block was one where I skipped that and guessed."

Block 3 — Q11–15

Q11 (Operations Deployment · 5 pts, ~5 min). Installing telemetry-agent fails: it depends on an older libtelemetry build than what's currently installed, and a newer package already depends on the current one. Get telemetry-agent installed and working without --nodeps/--force and without breaking the package that needs the current libtelemetry, then verify package integrity.

Done when: rpm -q telemetry-agent (or dpkg -s) shows it installed via normal dependency resolution, and rpm -V telemetry-agent reports no altered files.

Worked solution — Q11
sudo dnf install telemetry-agent
# error: telemetry-agent-2.1 requires libtelemetry < 3.0, but libtelemetry-3.2 is installed

dnf provides libtelemetry                 # what's actually available in the repos right now
sudo dnf install telemetry-agent-3.0      # the release actually BUILT against the current library

rpm -q telemetry-agent
rpm -V telemetry-agent                    # silent output = every file matches its recorded checksum

# Debian-family equivalent
# apt policy telemetry-agent
# sudo apt install telemetry-agent=<version-matching-current-libtelemetry>

The tempting shortcut is --nodeps or --force, which installs the package anyway with a genuinely unmet dependency and leaves it — or something that depended on the newer library — quietly broken the moment either one actually runs. Dependency resolution finding a compatible release is not an obstacle to route around; it's the tool doing exactly its job.

Q12 (Storage · 5 pts, ~6 min). Application response times have degraded and disk I/O is suspected. Without restarting anything, identify which process is generating the heaviest disk I/O right now and cite the specific metric showing the underlying device is saturated.

Done when: you can name the offending PID and the device, and point to a specific iostat value (e.g. %util or await) that shows saturation.

Worked solution — Q12
iostat -xz 2 5                            # sdb1 sitting at ~98% util, await climbing — the saturated device

sudo iotop -o -b -n 3                     # -o: only processes doing I/O right now
# PID 4821  logcompactor   R/W 42.1 M/s   -- the culprit

pidstat -d -p 4821 1 3                    # confirm a sustained, not momentary, read/write rate
ls -la /proc/4821/fd | grep -i log        # which file(s) is it actually hammering?

%util and await are two different signals worth citing separately rather than interchangeably: %util says the device is busy essentially all the time, await says how long each request is actually waiting once it's queued — a device can show high %util with low await (busy but keeping up) or the reverse (idle-looking but each request stalls), and the exam's wording rewards naming the specific number, not just gesturing at "iostat looked bad."

Q13 (Essential Commands · 5 pts, ~5 min). /var/log/app/current.log is growing unbounded and has already eaten most of /var. Configure rotation for it (daily, keep 7, compressed) so this can't recur, and reclaim the space already lost — without restarting the application.

Done when: logrotate -d /etc/logrotate.d/app runs with no syntax errors, and df -hT /var shows meaningfully reclaimed space after handling the oversized file.

Worked solution — Q13
df -hT /var
du -sh /var/log/app/current.log           # e.g. 9.4G

sudo tee /etc/logrotate.d/app >/dev/null <<'EOF'
/var/log/app/current.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
}
EOF

sudo logrotate -d /etc/logrotate.d/app    # dry run — syntax only, no changes made
sudo logrotate -f /etc/logrotate.d/app    # force an immediate rotation right now
df -hT /var

copytruncate is the deliberate choice here: it copies the log's current contents out, then truncates the original file in place, rather than renaming it. An application still holding that file descriptor open keeps writing into the now-empty file instead of into an orphaned, deleted one that never gives its space back — the exact "deleted but still open" trap from Set 1's disk-space task, prevented in advance this time rather than diagnosed after the fact. The tradeoff: a handful of log lines written in the gap between copy and truncate can be lost, which postrotate plus a signal-driven reopen avoids for an application that supports it.

Q14 (Networking · 5 pts, ~6 min). This host has eth0 facing the internet and eth1 facing an internal LAN (10.0.0.0/24) with no direct route out. Configure it to act as a router: enable forwarding and masquerade (source NAT) LAN traffic out through eth0.

Done when: an internal host can reach the internet through this box, nft list ruleset shows a postrouting masquerade rule scoped to eth0, and net.ipv4.ip_forward reads 1.

Worked solution — Q14
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/98-router.conf
sudo sysctl --system

sudo nft add table ip nat
sudo nft add chain ip nat postrouting { type nat hook postrouting priority 100 \; }
sudo nft add rule ip nat postrouting oifname "eth0" masquerade

sudo nft add table inet filter
sudo nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
sudo nft add rule inet filter forward ct state established,related accept
sudo nft add rule inet filter forward iifname "eth1" oifname "eth0" accept

sudo nft list ruleset | sudo tee /etc/nftables.conf
sudo systemctl enable --now nftables
# from an internal host: ip route add default via <this-box's-eth1-address> ; curl -I https://example.com

masquerade dynamically substitutes whatever address eth0 currently holds — the right choice on a DHCP-facing interface, versus a fixed snat to <addr> when the address is static and known ahead of time. The forward chain's default-drop plus an explicit established,related accept mirrors the same discipline Set 1's input-chain task used — the rule order matters for exactly the same reason there.

Q15 (Users and Groups · 5 pts, ~6 min). An LDAP directory already exists at ldap://dc1.internal:389 (base DN dc=internal) with real user accounts. Configure this host to resolve and authenticate those accounts via sssd, so a directory user can be looked up and can log in with no matching local account.

Done when: getent passwd someldapuser returns an entry sourced from LDAP, id someldapuser resolves its group membership, and su - someldapuser succeeds.

Worked solution — Q15
sudo dnf install -y sssd sssd-ldap authselect
sudo authselect select sssd --force

sudo tee /etc/sssd/sssd.conf >/dev/null <<'EOF'
[sssd]
services = nss, pam
domains = internal

[domain/internal]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldap://dc1.internal:389
ldap_search_base = dc=internal
cache_credentials = true
EOF
sudo chmod 600 /etc/sssd/sssd.conf
sudo systemctl enable --now sssd

getent passwd someldapuser
id someldapuser
su - someldapuser -c whoami

Two failure modes, both worth knowing apart. A world-readable sssd.conf makes sssd refuse to start outright, since the file can carry bind credentials — chmod 600 isn't cosmetic here. And a correctly written sssd.conf that starts fine still resolves nothing if /etc/nsswitch.conf never lists sss for passwd/group — which is exactly what authselect select sssd wires up for you; editing sssd.conf without ever running authselect is the single most common way this task silently does nothing.

Block 4 — Q16–20

Q16 (Operations Deployment · 5 pts, ~5 min — requires SELinux). nginx has been reconfigured to listen on port 8140 instead of 80, but it won't start under Enforcing SELinux. Fix the labeling — not by disabling or permissive-ing SELinux — so it starts cleanly.

Done when: getenforce still reports Enforcing, systemctl is-active nginx reports active, and ss -ltnp shows nginx bound to 8140.

Worked solution — Q16
systemctl status nginx                    # Active: failed
journalctl -u nginx -b | tail -15

semanage port -l | grep http_port_t       # 8140 is NOT in the list — that's the actual block
sudo semanage port -a -t http_port_t -p tcp 8140

sudo systemctl restart nginx
systemctl is-active nginx
ss -ltnp | grep 8140
sudo ausearch -m avc -ts recent | tail    # confirms the earlier denial, and that it has stopped

semanage port -a adds a brand-new port mapping; if 8140 were already assigned to some other SELinux type you'd need -m to modify it instead, and -a against an existing mapping simply errors out. Disabling SELinux, or dropping it to permissive, also "fixes" the symptom — and throws away the entire mandatory-access-control boundary this domain is explicitly testing you can work within, not around.

Q17 (Storage · 5 pts, ~5 min). Mount the NFS export nas1:/exports/shared persistently at /mnt/shared via /etc/fstab, using vers=4.2 and a soft,timeo=30 pair so a lost server can't hang the whole host indefinitely.

Done when: mount | grep /mnt/shared shows nfs4 with those options after a fresh mount -a, and showmount -e nas1 confirms the export is actually offered.

Worked solution — Q17
showmount -e nas1                         # confirm the export exists and this host is allowed to see it
sudo mkdir -p /mnt/shared
echo 'nas1:/exports/shared  /mnt/shared  nfs4  vers=4.2,soft,timeo=30,_netdev  0 0' \
  | sudo tee -a /etc/fstab
sudo mount -a
mount | grep /mnt/shared
df -hT /mnt/shared

_netdev tells systemd this mount depends on the network stack being up, so it's ordered correctly at boot instead of racing it — the same family of fstab options that decide "boots clean" versus "boots into emergency mode" as Q7's nofail earlier on this paper. soft plus a bounded timeo caps how long an operation blocks against a dead server; the default hard mount retries silently forever and can wedge an entire shell that so much as cds into the mount point.

Q18 (Networking · 5 pts, ~6 min). Create a Linux bridge br0, attach physical interface eth2 to it, and give the bridge — not eth2 directly — the address 192.168.60.10/24, persistent across reboots.

Done when: bridge link show lists eth2 as a member of br0, and ip -br addr show br0 shows 192.168.60.10/24 while ip -br addr show eth2 shows no address at all.

Worked solution — Q18
sudo nmcli con add type bridge ifname br0 con-name br0
sudo nmcli con mod br0 ipv4.method manual ipv4.addresses 192.168.60.10/24

sudo nmcli con add type ethernet ifname eth2 master br0 con-name br0-port-eth2
sudo nmcli con up br0
sudo nmcli con up br0-port-eth2

ip -br link show type bridge
bridge link show
ip -br addr show br0
ip -br addr show eth2                     # no address here — the bridge carries it now

This is the network topology a bridged VM guest actually sits on — worth contrasting with Q4's default libvirt network, which is NAT-based and hides guests behind the host's own address. A bridged guest gets a real address on the same LAN segment as everything else; knowing which topology a task is actually describing, before reaching for either tool, is the real competency.

Q19 (Essential Commands · 5 pts, ~6 min). Generate a private key and a CSR (not a self-signed certificate) for host billing.internal. Then, given an already-issued certificate at /tmp/billing.crt and its issuing CA's certificate at /tmp/ca.crt, verify the certificate's chain and subject, and confirm it actually matches the key you generated.

Done when: openssl verify -CAfile /tmp/ca.crt /tmp/billing.crt reports OK, and the modulus of the private key and the certificate match exactly.

Worked solution — Q19
openssl genrsa -out /etc/ssl/private/billing.internal.key 2048
sudo chmod 600 /etc/ssl/private/billing.internal.key

openssl req -new \
  -key /etc/ssl/private/billing.internal.key \
  -out /tmp/billing.csr \
  -subj "/C=IN/O=Astro Freight/CN=billing.internal"
openssl req -in /tmp/billing.csr -noout -text | grep -A1 Subject:

# once /tmp/billing.crt comes back, signed against that CSR by the CA:
openssl verify -CAfile /tmp/ca.crt /tmp/billing.crt

# prove the returned cert actually belongs to THIS key, not some other CSR
openssl x509 -noout -modulus -in /tmp/billing.crt | openssl md5
openssl rsa  -noout -modulus -in /etc/ssl/private/billing.internal.key | openssl md5
# identical output on both lines = they match

The modulus check catches the single most common real-world certificate mistake — installing a genuinely valid certificate paired with the wrong private key, after juggling more than one CSR at once. A service in that state fails TLS negotiation with an error that looks nothing like "wrong key" on the surface, which is exactly why this one-liner is worth having memorised rather than looked up under pressure.

Q20 (Operations Deployment · 5 pts, ~3 min). Schedule a one-off job that runs /usr/local/bin/snapshot.sh exactly once, 30 minutes from now, using at — not cron, not a systemd timer — and confirm it's actually queued.

Done when: atq lists exactly one pending job at roughly the right time, at -c <job> shows the correct command, and systemctl is-enabled atd reports enabled.

Worked solution — Q20
sudo systemctl enable --now atd

echo '/usr/local/bin/snapshot.sh' | at now + 30 minutes
atq
at -c 3                                   # job number from atq's first column
systemctl is-enabled atd
systemctl is-active atd

atd — not cron — is what actually holds and fires an at job; if it isn't enabled, the job sits in the queue looking scheduled forever and simply never runs, and atq alone won't warn you. Contrast this against Set 1's systemd-timer task: a timer is the better tool for anything recurring, while at is correctly the tool for genuinely one-off, non-repeating work — reaching for the wrong one of the two is a common way this competency loses easy points.

Score yourself

☺ Like you're 10: Add up your points, turn it into a percentage against the real 67% bar, then compare each domain against your Set 1 numbers — that comparison is the part that actually helps this time.

Same scoring discipline as Set 1: mark after a break, full points only when the done-when check actually passed, half credit when the resource exists and is broadly right but the check didn't pass, zero for anything unattempted.

#TaskDomainPointsYour score
1IPv6 static address + routeNetworking5
2ext4 create, tune, mountStorage5
3Git cherry-pick + annotated tagEssential Commands5
4libvirt VM, define + autostartOps Deployment5
5Scoped sudoers rule + nproc limitUsers & Groups5
6DNS resolution repairNetworking5
7Bad fstab entry / boot recoveryOps Deployment5
8LVM snapshot + merge rollbackStorage5
9renice a CPU-bound processEssential Commands5
10SSH ProxyJump bastion shortcutNetworking5
11Broken package dependencyOps Deployment5
12iostat/iotop I/O diagnosisStorage5
13logrotate + reclaim disk spaceEssential Commands5
14nftables masquerade / routingNetworking5
15sssd + LDAP account lookupUsers & Groups5
16SELinux port context (nginx)Ops Deployment5
17NFS static mount, soft/timeoStorage5
18Linux bridge (br0 + eth2)Networking5
19CSR + certificate chain + key matchEssential Commands5
20One-off at jobOps Deployment5
TotalAll five domains100

The bar is the same 67%, and the arithmetic is the same too: 100 points, so your raw score is your percentage. Total each domain separately before you move on — the number that matters for study planning isn't your total, it's which of the five columns below is thinnest.

DomainAvailableYoursIf under two-thirds, go here
⚙️ Operations Deployment25LFCS — the exam, then the practice bank
📡 Networking25Networking on the PE trio, then the practice bank
💾 Storage20Storage & State, then LVM & Linux Storage Tools
⌨️ Essential Commands20systemd & journald, then the practice bank
👥 Users and Groups10Linux Fundamentals for Platform Engineers
◆ Key idea

Lay this paper's five domain scores next to Set 1's. A domain that improved needed exactly what you did between the two papers — keep doing it. A domain that's flat or worse across both sittings is telling you the study method, not just the score, needs to change: swap re-reading for untimed hands-on drills in that specific domain before you sit Set 3, rather than hoping a third read-through of the blueprint does something the first two didn't.

Where each domain is taught

☺ Like you're 10: Every task above traces back to one of a handful of pages, the same handful as last time. Go back to the page, not to a search engine.

Nothing on this paper tests anything outside what's covered on this course. The LFCS-specific depth lives here on Golden Astronaut; the general Linux and networking ground the sibling trio also walks over has its deeper cross-reference on Platform Engineering's own LFCS page, which covers these same five domains from the other side of the trio.

The wider revision kit still applies: flashcards for the vocabulary, the self-check quiz for mixed recall, and the LFCS study plan for sequencing what's left of your prep. And because LFCS shares its performance-based format with CKA and with the CNPE, sitting a second timed paper is rehearsing a format you'll meet again on both.

🎬 At Mission Control
🦊

Foxy: Seventy-one this time! Up from sixty-eight on Set 1!

🦥

Sol: Congratulations on the... three points. Now which domain... ate the other twenty-nine?

🦊

Foxy: Operations Deployment. Q4 — the libvirt VM. It just sat there in "shutoff" no matter what I did to the disk image.

🦥

Sol: Did you check whether the host itself... could nest a guest at all... before you touched virt-install?

🦊

Foxy: ...no. I assumed the practice box could just do it. That's on me, not on the VM.

👺

Gizmo: Or forget the VM entirely and just set SELinux permissive for Q16 while you're at it — one command, way faster than fiddling with port contexts! 😈

🐢

Timmy: That's a second domain failed for the price of one shortcut, Gizmo. semanage port -a was four lines and Enforcing never had to move. Foxy actually got that one right.

🦊

Foxy: Nested virtualization goes on the pre-flight checklist in permanent marker. See you at Set 3, and I'm bringing a working hypervisor this time.

That's the second sitting done. Compare it against Set 1 domain by domain, let whichever gap is left settle for a day or two, then close the trilogy with Set 3 — the same twenty-task format, deliberately skewed toward Networking and Storage instead of the official split.

🐢 Timmy's checkpoint

1. Name the five LFCS domains and their weights, in weight order. 2. Why must nested virtualization be confirmed before attempting Q4, and where do you actually check for it? 3. What's the practical difference between renice and a unit's CPUQuota= when it comes to controlling a runaway process? 4. Q13 used copytruncate in its logrotate config instead of the default rename-based rotation — what specific problem does that avoid, and what's the tradeoff? 5. Why does "fixing" Q16 by disabling SELinux fail to satisfy what the Operations Deployment domain is actually testing? 6. Give the one-line command that proves a certificate and a private key genuinely belong together, and name the real-world mistake it catches.

Check your answers
  1. Operations Deployment 25%, Networking 25%, Storage 20%, Essential Commands 20%, Users and Groups 10%.
  2. Nested virtualization is a property of the underlying hypervisor, not of anything inside the guest you're troubleshooting — a libvirt guest built on a non-nested host will define and even "start" without error and simply never reach a running state. Check /sys/module/kvm_intel/parameters/nested (or kvm_amd) for Y/1 before the clock starts, not after Q4 has already stalled.
  3. renice lowers scheduling priority — a polite request the scheduler can still override under light contention, so the process can still burst back toward 100% CPU. A cgroup control like CPUQuota= is an enforced ceiling the kernel will not let the process exceed, full stop. Knowing which one a task is actually asking for is the real competency.
  4. copytruncate copies a log's contents out and truncates the original file in place, so an application that still holds that file descriptor open keeps writing into the same (now-empty) file instead of into an orphaned, deleted one that never returns its disk space — the "deleted but still open" trap from Set 1's disk-space task, prevented in advance. The tradeoff is a handful of lines written in the gap between copy and truncate can be lost.
  5. The domain's SELinux competency is specifically "create and enforce MAC using SELinux" — disabling or permissive-ing it removes the mandatory access control boundary entirely rather than working correctly within it, which is the opposite of what's being tested. The graded fix is a correct port-context change (semanage port -a) with Enforcing left untouched.
  6. openssl x509 -noout -modulus -in cert.crt | openssl md5 compared against openssl rsa -noout -modulus -in key.key | openssl md5 — identical output means the certificate and key belong together. It catches pairing a valid certificate with the wrong private key after juggling more than one CSR, a mistake that otherwise surfaces only as a confusing TLS negotiation failure with no obvious cause.
⏱️ The three LFCS papers

Set 1 · Set 2 (you are here) · Set 3. This paper and Set 1 both track the official 25/25/20/20/10 domain split, so all three domain breakdowns compare directly against each other; Set 3 deliberately skews toward Networking and Storage instead. See the LFCS study plan for where each paper fits against your remaining prep time, the practice-task bank for shorter untimed drills in between, and LFCS — the exam for the full domain writeup and official logistics.