Environment build, configuration and evidence baseline (10 marks)
Instructor: Teboho Mpholo (Mr.), Founder and Lead Instructor
End Assessment: issued 12 October 2026, closes 9 November 2026 at 23:59
Edition: Red Hat Enterprise Linux 8, 9 or 10, and its rebuilds (AlmaLinux, Rocky Linux, CentOS Stream)
export STUDENT_ID="12345678"
mkdir -p ~/naledi_$STUDENT_ID/bin ~/naledi_$STUDENT_ID/lib ~/naledi_$STUDENT_ID/etc \
~/naledi_$STUDENT_ID/data ~/naledi_$STUDENT_ID/logs ~/naledi_$STUDENT_ID/backup \
~/naledi_$STUDENT_ID/docs ~/naledi_$STUDENT_ID/evidence/screenshots \
~/naledi_$STUDENT_ID/evidence/transcripts ~/naledi_$STUDENT_ID/evidence/sysconfig
cat > ~/naledi_$STUDENT_ID/docs/profile.snippet <<'EOF'
export STUDENT_ID="12345678"
export PS1='[$STUDENT_ID] \u@\h:\w\$ '
naledi_id() { printf '%s | %s@%s | %s\n' "$STUDENT_ID" "$(whoami)" "$(uname -n)" "$(date '+%F %T')"; }
export HISTTIMEFORMAT='%F %T '
shopt -s histappend
EOF
cat ~/naledi_$STUDENT_ID/docs/profile.snippet >> ~/.bashrc
source ~/.bashrc
naledi_id
~/.bash_profile. Add [ -f ~/.bashrc ] && . ~/.bashrc to it.naledi_id first in every screenshot. Capture the full window.HISTTIMEFORMAT only timestamps commands entered after it is set. Set it before you do any Section C or D work, or every earlier history line will be untimed.naledi_id findmnt -T ~/naledi_$STUDENT_ID getenforce cd ~/naledi_$STUDENT_ID && touch .acltest && setfacl -m u:$USER:rw .acltest && getfacl .acltest; rm -f .acltest
How to read it. On Red Hat Enterprise Linux the project directory is usually on XFS, mounted under /dev/mapper/rhel-root (an LVM logical volume), and FSTYPE should read xfs. ext4 and btrfs are also acceptable. OPTIONS must not show noacl. A successful setfacl followed by a user:yourname:rw- line in getfacl proves ACL support. FAT, exFAT, NTFS, NFS and CIFS shares fail this test, and evidence from them scores zero. getenforce should print Enforcing; that is SELinux doing its job and it does not block your project files, so record the value and move on. If your home is on a share, use the local path your lab gives you and write which path you used.
cd ~/naledi_$STUDENT_ID
{ naledi_id; for c in "hostnamectl" "uname -a" "cat /proc/version" "systemctl is-system-running" "ps -p 1 -o comm=" "lsblk -f" "findmnt" "free -h" "swapon --show" "id" "getenforce"; do echo; echo "## $c"; $c 2>&1; done; } | tee evidence/sysconfig/fingerprint.txt
getent passwd $USER | cut -d: -f7
Also screenshot each command on its own. Then write one sentence per item about your machine, using this frame: On (hostname), (command) shows (value), which means (consequence for my project). Example sentences for the made-up lab-pc-07, running Red Hat Enterprise Linux 9.4, to show the level expected:
| Item | Example sentence (made-up values, do not copy) |
|---|---|
| Distribution | hostnamectl reports Red Hat Enterprise Linux 9.4 on bare metal, so there is no container or VM layer limiting what my user can do. |
| Kernel, architecture | Kernel 5.14.0 on x86_64 means every tool in this assessment is available as a standard 64-bit build. |
| Init system | PID 1 is systemd and is-system-running says running, so systemctl --user timers are available and I do not need the cron fallback. |
| Storage and project filesystem | My home sits on /dev/mapper/rhel-root formatted XFS, so ownership, modes and ACLs all work for Section C. |
| Other mounts | findmnt shows my USB stick at /run/media/yourname as exfat with noexec, which is why I run session_end.sh through bash and never work on the stick. |
| Memory and swap | free -h shows 15 GiB total but 11 GiB available, and swapon shows /dev/mapper/rhel-swap sized 2 GiB, so a runaway script would hit the ulimit before the machine suffers. |
| Account and groups | id shows uid 1043 in groups student and users but not wheel, so I cannot use sudo and must use ACLs on my own files. |
| SELinux | getenforce prints Enforcing, so SELinux is on, but my project files are user_home_t and I am not asked to change policy. |
| Default shell | My passwd entry lists /bin/bash, the same shell I type in, so the profile lines in ~/.bashrc apply to my sessions. |
cd ~/naledi_$STUDENT_ID
{ naledi_id; for t in setfacl getfacl flock awk script systemctl tar sha256sum; do p=$(command -v $t) && echo "$t | $p | $($t --version 2>&1 | head -1)" || echo "$t | MISSING"; done; echo "getenforce | $(command -v getenforce) | $(getenforce)"; } | tee evidence/sysconfig/tools.txt
rpm -q acl util-linux gawk tar coreutils
awk --version | head -1
On Red Hat Enterprise Linux, awk is GNU Awk (gawk), so awk --version prints the version. rpm -q gives a definitive installed or not installed answer for the package that provides each tool, and that is what a RHEL administrator would rely on. For anything MISSING, write the substitute and why it is acceptable. The judgement earns the mark:
| Missing | Acceptable substitute | State this limit |
|---|---|---|
| flock | Atomic mkdir lock directory removed by a trap | A killed process leaves the lock behind, so add stale-lock detection |
| script | bash -x piped through tee into logs/ | Does not capture a full interactive session |
| setfacl, getfacl | None that satisfies C1. Record that you asked the lab for the acl package | ACL evidence in C1 becomes impossible |
You will not have dnf entitlement on a shared RHEL lab machine, and you do not need it: every tool this paper requires is preinstalled. If one is missing, the substitute column above tells you what to use.
Answer four things using facts from your own fingerprint: what the machine is (name it), what persists between sessions, what you may not change, and which habit in your routine answers each limit. Check the length with wc -w. Example for the made-up lab-pc-07, to show the shape only:
Self-test: replace your hostname with a classmate's. If the paragraph is still true, it is generic.
Screenshots for A1 (numbered, such as A1-01.png): banner and folders; findmnt, getenforce and the ACL test; each fingerprint command; the tool check and the rpm -q output. Each is full window with the banner, the command and its output visible.
set -e and pipefail, the line that reads the previous link ends the script after the archive is made and before any chain line is written. The fix is one extra line, touch "$CHAIN", added below. If your copy of the paper shows a box-drawing bar instead of a plain pipe, type the ordinary keyboard pipe |.Mount the stick, then save this as session_end.sh on it. On Red Hat Enterprise Linux 8 and later the desktop mounts removable media at /run/media/$USER/MYSTICK. Older RHEL 7 builds used /media, and the paper uses that path, so use whatever findmnt shows on your machine.
#!/usr/bin/env bash
# session_end.sh - run at the end of every working session
# usage: bash session_end.sh /run/media/$USER/MYSTICK
set -euo pipefail
: "${STUDENT_ID:?set STUDENT_ID in your shell profile}"
STICK="${1:?usage: session_end.sh /path/to/your/device}"
ARCHIVE="$STICK/C6LIE19_${STUDENT_ID}_$(date +%F_%H%M).tar.gz"
CHAIN="$STICK/chain.txt"
tar czf "$ARCHIVE" -C "$HOME" "naledi_$STUDENT_ID"
HASH=$(sha256sum "$ARCHIVE" | cut -d" " -f1)
touch "$CHAIN"
PREV=$(tail -n 1 "$CHAIN" 2>/dev/null | awk -F"|" "{print \$NF}")
PREV=${PREV:-GENESIS}
LINK=$(printf "%s%s" "$PREV" "$HASH" | sha256sum | cut -d" " -f1)
printf "%s|%s|%s|%s|%s|%s\n" "$(date -Is)" "$STUDENT_ID" "$(uname -n)" \
"$(basename "$ARCHIVE")" "$HASH" "$LINK" >> "$CHAIN"
printf "Archived %s\nChain link %s\n" "$(basename "$ARCHIVE")" "$LINK"
Know what each part does, because you may be asked: the archive is named with date and time; HASH is the archive's SHA-256; PREV is the last link, or GENESIS on the first run; LINK is the SHA-256 of PREV joined to HASH; one line is appended to chain.txt. Changing an old archive changes its hash and breaks every later link.
# START of every session (skip the restore on your very first one)
tar xzf "$(ls -t /run/media/$USER/MYSTICK/C6LIE19_${STUDENT_ID}_20*.tar.gz | head -1)" -C ~
cat ~/naledi_$STUDENT_ID/docs/profile.snippet >> ~/.bashrc; source ~/.bashrc
script -a ~/naledi_$STUDENT_ID/logs/session-$(date +%F).typescript
# ... do your work ...
# END of every session
history > ~/naledi_$STUDENT_ID/evidence/history.txt
exit # leaves script, closes the typescript
bash /run/media/$USER/MYSTICK/session_end.sh /run/media/$USER/MYSTICK
bash: lab sticks are usually mounted noexec, so ./session_end.sh may be refused.~/.bashrc for duplicates.history is disabled in non-interactive shells. Run the export from an interactive prompt, not inside bash -c or a scheduled job.| CP | Date | State of the work |
|---|---|---|
| CP1 | Mon 19 October | Section A complete, data files seeded |
| CP2 | Mon 26 October | B1 to B4 working |
| CP3 | Mon 2 November | B5 to B8 working, Section C started |
| CP4 | Mon 9 November | Final submission |
N=1 # 1, 2, 3 or 4
cd ~
tar czf C6LIE19_${STUDENT_ID}_cp$N.tar.gz -C ~/naledi_$STUDENT_ID bin lib etc data docs
cp /run/media/$USER/MYSTICK/chain.txt .
sha256sum C6LIE19_${STUDENT_ID}_cp$N.tar.gz | tee -a ~/naledi_$STUDENT_ID/evidence/checkpoints.sha256
Upload the archive and chain.txt to the Blackboard Checkpoint folder, and screenshot the confirmation showing the upload time. Run session_end.sh just before, so the chain on your stick matches what you deposit. Do not build Section B ahead of time: a near-complete system at CP1 scores zero for E2.
Twelve or more entries on twelve or more different days, about three sessions a week. Each quotes the chain link printed by that session.
## 2026-10-18, Session 2 Attempted: created naledi.conf and the config loader. Failed: loader accepted PERMIT_DAYS=abc because the regex was missing the anchors. Changed: anchored the pattern with ^ and $, retested with bad values. Chain link: (paste the link printed by session_end.sh)
Keep docs/logbook.md in the project, the typescripts in logs/ and the history export in evidence/. Include your mistakes in all three, because the marker looks for the real search path.
STICK=/run/media/$USER/MYSTICK; prev=GENESIS while IFS='|' read -r ts sid host name hash link; do calc=$(sha256sum "$STICK/$name" | cut -d' ' -f1) exp=$(printf '%s%s' "$prev" "$calc" | sha256sum | cut -d' ' -f1) if [ "$calc" = "$hash" ] && [ "$exp" = "$link" ]; then echo "OK $name"; else echo "BAD $name"; fi prev=$link done < "$STICK/chain.txt"
Every line should print OK, and the last link must equal the last line of chain.txt. Keep every archive and never edit chain.txt by hand.
# NALEDI operating parameters. Only the administrator role edits this file. PROJECT_ROOT="/home/youruser/naledi_12345678" SEED_MIN_ANIMALS=100 DISTRICT_COUNT=6 TIMER_TIME="02:30" PERMIT_DAYS=14 INSPECT_AGE_MONTHS=18 PENALTY_BWP=75 RETENTION_DAYS=30 TAG_PREFIX="BW-NAL-"
Write PROJECT_ROOT as a full absolute path. A tilde does not expand inside quotes, and the C2 test with env -i runs with no HOME. Do not edit this file with gedit or another GUI editor opened as root; you are an ordinary user and the file is yours.
naledi_load_config() {
local root conf k v
root="$(cd "${BASH_SOURCE[0]%/*}/.." && pwd)" || return 2
conf="${NALEDI_CONF:-$root/etc/naledi.conf}"
[ -e "$conf" ] || { echo "naledi: config file not found: $conf" >&2; return 2; }
[ -r "$conf" ] || { echo "naledi: config file not readable: $conf" >&2; return 2; }
. "$conf"
for k in SEED_MIN_ANIMALS DISTRICT_COUNT PERMIT_DAYS INSPECT_AGE_MONTHS PENALTY_BWP RETENTION_DAYS; do
v="${!k:-}"
[[ $v =~ ^[1-9][0-9]*$ ]] || { echo "naledi: $k must be a positive whole number (got '$v')" >&2; return 2; }
done
[ "${SEED_MIN_ANIMALS:-0}" -ge 100 ] || { echo "naledi: SEED_MIN_ANIMALS must be at least 100 (got '$SEED_MIN_ANIMALS')" >&2; return 2; }
[[ ${TIMER_TIME:-} =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || { echo "naledi: TIMER_TIME must be HH:MM (got '${TIMER_TIME:-}')" >&2; return 2; }
[[ ${TAG_PREFIX:-} =~ ^[A-Z0-9-]+$ ]] || { echo "naledi: TAG_PREFIX must be capitals, digits and hyphens (got '${TAG_PREFIX:-}')" >&2; return 2; }
[ -d "${PROJECT_ROOT:-}" ] || { echo "naledi: PROJECT_ROOT is not a directory (got '${PROJECT_ROOT:-}')" >&2; return 2; }
NALEDI_ROOT="$root"
}
Two RHEL notes. First, ${BASH_SOURCE[0]%/*} is used instead of dirname because dirname is an external binary that a stripped env -i run may not find on PATH, and this form uses only a Bash built-in. Second, the SEED_MIN_ANIMALS guard enforces the paper's "At least 100 animals" rule, so a marker who edits the file to 50 sees a clean refusal instead of silent acceptance.
#!/usr/bin/env bash
set -euo pipefail
NALEDI_ROOT="$(cd "${BASH_SOURCE[0]%/*}/.." && pwd)"
. "$NALEDI_ROOT/lib/config.sh"
naledi_load_config || exit $?
# from here on use $PERMIT_DAYS, $TAG_PREFIX and the rest. Never write 14 or BW-NAL- in code.
The project root is found from the script's own location, so nothing depends on your profile. That is what lets the scheduled job survive env -i later in C2. Sourcing the file runs it as code, which is acceptable here because only the administrator role may edit it (C1).
SELinux note for RHEL. A config file created inside your home directory carries the context user_home_t, which your shell can read normally. If you ever move a file in from /tmp or a USB stick with cp, it may carry tmp_t or unconfined_u:object_r:removable_t, and reads can be denied by SELinux even though the mode bits allow them. ls -Z etc/naledi.conf shows the context, and restorecon -Rv ~/naledi_$STUDENT_ID/etc resets it to the home-directory default. This is the single most common SELinux trap for this project, and a sentence about it in your report shows you understand the RHEL layer that sits above DAC.
cd ~/naledi_$STUDENT_ID env -i /bin/bash bin/naledi # works with no environment sed -i 's/^PERMIT_DAYS=.*/PERMIT_DAYS=21/' etc/naledi.conf env -i /bin/bash bin/naledi # behaviour follows the file sed -i 's/^PERMIT_DAYS=.*/PERMIT_DAYS=abc/' etc/naledi.conf env -i /bin/bash bin/naledi; echo "exit=$?" # invalid value, exit 2 sed -i 's/^PERMIT_DAYS=.*/PERMIT_DAYS=14/' etc/naledi.conf mv etc/naledi.conf etc/naledi.conf.off env -i /bin/bash bin/naledi; echo "exit=$?" # missing file, exit 2 mv etc/naledi.conf.off etc/naledi.conf chmod 000 etc/naledi.conf; env -i /bin/bash bin/naledi; echo "exit=$?" # unreadable chmod 640 etc/naledi.conf
Expected messages: naledi: PERMIT_DAYS must be a positive whole number (got 'abc') and naledi: config file not found: .... The unreadable test only fires when you are an ordinary user; if you have sudo, do not sudo the test, because root bypasses the mode and [ -r ] then succeeds. Your own wording and file layout should differ from this page, because near-identical scripts between students are referred for collusion.
Leftover check. Find hard-coded parameters in your code:
grep -rnE '\b(14|18|75|30|100)\b|BW-NAL' ~/naledi_$STUDENT_ID/bin ~/naledi_$STUDENT_ID/lib
Line numbers and loop counts are harmless. Any hit that is a parameter must move into naledi.conf.
| File or item | Contents | Item |
|---|---|---|
| evidence/sysconfig/fingerprint.txt | Fingerprint commands and output including getenforce, banner first | A1 |
| evidence/sysconfig/tools.txt | Each tool, its path, version, rpm -q output and any substitutes | A1 |
| evidence/screenshots/ | Numbered full-window captures: findmnt, getenforce, ACL test, each fingerprint command, tool check | A1 |
| chain.txt and four checkpoint pairs | Archive plus chain.txt deposited on each date | A2 |
| docs/logbook.md, logs/*.typescript, evidence/history.txt | 12 or more dated entries quoting chain links | A2 |
Config screenshots and ls -Z output | Working, changed, invalid, missing, unreadable, plus SELinux context note | A3 |