Scope & Objectives
Audit the operating-system identity and kernel baseline of one running Linux host (or its config-as-code). Seven domains, in priority order:
- Accounts & sudo — UID-0 duplication, stale/unused accounts, empty passwords, sudo grant breadth, sudo brute-force throttling.
- SSH daemon — effective
sshd_configstate, authentication methods, forwarding surface, key hygiene. - PAM & password policy — lockout, password quality, expiry, password hashing.
- Kernel & network sysctl — anti-MITM, anti-DoS, information-disclosure and local-privesc-surface controls.
- Filesystem & permissions — SUID/SGID inventory, world-writable artifacts, mount flags, cron/at gating, umask, core dumps.
- Boot & disk — bootloader protection (contextual), encryption-at-rest presence, protected data mounts.
- Time sync — NTP health; forensic prerequisite for every other module's evidence.
Out of scope (cross-references): firewall/exposure map → FW; TLS/proxy edge → TLS; systemd sandboxing, SELinux/AppArmor policy authoring, container runtime → SANDBOX; secret contents/env files → HSECRET; patch level/EOL → PATCH; audit rules/log shipping → LOGMON.
Operating rules:
- All inspection is read-only; mutating commands appear only under Remediation (Phase 6 approval). Prefer effective-state evidence (
sshd -T,sysctl -n KEY) over config files — drop-in dirs (sshd_config.d/,sysctl.d/) make file text misleading. - Commands needing root are tagged
[ROOT]; without root, audit world-readable state plus the config repo. Repo-only access: judge the rendered config (Ansible templates, cloud-init user-data) using Patterns & Signatures. Distro variance is called out inline — never assume Ubuntu values on RHEL or Alpine; detect first (cat /etc/os-release).
Prerequisites & Vocabulary
Zero-background primer: the terms this module uses, one line each. Deeper plain-language explanations for every class live in the repository GUIDE.md glossary.
- privesc: the local steps that turn a low-level account into root
- sudo NOPASSWD:ALL: password-free full-root command rights for an account
- sysctl: kernel tunables closing redirection and information-leak paths
- SUID binary: a program that runs as its file's owner (often root) no matter who starts it
- sticky bit / noexec mount: shared-directory protections against planted trojans
- ptrace_scope: kernel restriction on one process debugging another user's processes
- PAM: the pluggable login-policy layer (lockouts, password quality, expiry)
- taint flow / source→sink: path untrusted data travels from entry point to dangerous function
Mental Model
Attackers do not attack "the baseline" — they chain specific gaps into a path:
Entry point Amplifier (this module) Realized outcome
--------------------- ----------------------------------- --------------------------------
internet SSH :22 ──▶ PasswordAuthentication yes + ▶ online credential stuffing →
PermitRootLogin yes direct root, no per-user trail
app RCE as svc acct ▶ sudo NOPASSWD:ALL ▶ instant takeover + root persistence
on-path attacker ─▶ accept_redirects=1 / ▶ route hijack, traffic interception,
accept_source_route=1 plaintext credential theft
local low-priv user ─▶ kptr_restrict=0, dmesg open, ▶ KASLR defeated → reliable kernel-
ptrace_scope=0, userns open exploit privesc to root
any foothold ─▶ world-writable dir without sticky ▶ planted SUID trojan = persistent
+ executable /dev/shm local root, evades noexec /tmp
post-exploit ─▶ NTP unsynchronized, coredumps on ▶ forensics blinded, secrets in cores
Four defensive layers map onto the check sections:
| Layer | Sections | Question it answers |
|---|---|---|
| Perimeter identity | sshd | Can an outsider become a local identity? |
| Local privilege boundary | accounts, sudo, PAM, SUID | Can that identity become root? |
| Containment | sysctl, mounts, namespaces | How much damage can a non-root foothold do? |
| Forensics | time sync, core dumps | Can we reconstruct what happened? |
Classify every finding by layer and by what it chains with — a Medium gap becomes Critical once an adjacent entry point exists (see Taint Tracing Guidance).
What To Check
1. Accounts and UID-0 Inventory
Run and verify exactly one result: root with a legitimate shell. A second UID-0 account (toor, backup, svc-root) is a Critical backdoor indicator:
awk -F: '$3==0{printf "%s uid=%d shell=%s\n",$1,$3,$7}' /etc/passwd
awk -F: '($3!=0 && $4==0){print "gid0:",$1}' /etc/passwd
Inventory every interactive-capable account and confirm each against the asset owner's list; check lock state [ROOT] (field 2 of passwd -S: L locked, P hash set, NP empty password). Flag unlocked accounts unused >90 days, service accounts owning shells, and .ssh/authorized_keys nobody claims (cross-check HSECRET):
awk -F: '$7!~/(nologin|false|sync)$/{print $1"\tuid="$3"\thome="$6"\tshell="$7}' /etc/passwd
for u in $(cut -d: -f1 /etc/passwd); do passwd -S "$u"; done; lastlog -b 90
Empty-password check [ROOT] — any hit is a direct-login finding (locked hashes look like !, !!, !*; that is the good state):
awk -F: '($2==""){print $1" EMPTY-PASSWORD"}' /etc/shadow
2. Sudo Delegation
Validate parse integrity first, then enumerate grants and judge breadth:
visudo -c # syntax errors = finding by themselves
grep -RnsE '^[^#]*(NOPASSWD|[[:space:]]ALL=\(ALL' /etc/sudoers /etc/sudoers.d/
grep -RnsE '\(ALL(:\s*ALL)?\)\s+NOPASSWD:\s+(ALL|/bin/su|/bin/bash|/bin/sh|/usr/bin/(find|python[0-9]*|perl|ruby|php|vi[m]?|less|more|tar|zip|chmod|chown))' /etc/sudoers.d/
sudo -l # as your audit identity
Flag: NOPASSWD: ALL for a network-facing service account; grants to su, editors, interpreters, archivers (GTFOBins abuse — https://gtfobins.github.io). Accept as scoped: NOPASSWD: /usr/bin/systemctl restart myapp.service.
Throttle knobs (password prompts route through PAM, so faillock covers sudo once Section 4 is configured):
grep -RhE '^\s*Defaults\s' /etc/sudoers /etc/sudoers.d/ | grep -E 'timestamp_timeout|passwd_tries|use_pty'
Target values: Defaults timestamp_timeout=15, passwd_tries=3, use_pty (use_pty needs sudo ≥1.9).
3. SSH Daemon Effective Configuration
Effective state wins over file text; Include /etc/ssh/sshd_config.d/*.conf is processed where it appears and the first value obtained for a keyword wins, so an early-numbered drop-in silently overrides later main-file lines. Run [ROOT]:
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|authenticationmethods|kbdinteractiveauthentication|maxauthtries|logingracetime|permitemptypasswords|clientaliveinterval|clientalivecountmax|x11forwarding|allowagentforwarding|allowtcpforwarding|allowusers|allowgroups|banner)'
Without root, fall back to the sweep's file-text grep (drop-ins can contradict it — say so in evidence).
Judgement table (risky column = value to flag):
| sshd_config key | Risky value | Hardened value | Why |
|---|---|---|---|
PermitRootLogin |
yes; prohibit-password on hosts lacking key discipline |
no |
root logins skip per-user audit and give brute force the highest-value target |
PasswordAuthentication |
yes |
no |
enables online password guessing; keys are not sprayable |
KbdInteractiveAuthentication |
yes (alias ChallengeResponseAuthentication, deprecated name) |
no |
second password-ish path bypassing key-only intent |
PubkeyAuthentication |
absent (default yes) or no |
yes explicit |
assert the intended mechanism |
AuthenticationMethods |
unset (= any single method) | publickey |
forces key-only even if other methods reappear |
MaxAuthTries |
6 default or higher |
3 |
caps guessing per connection |
LoginGraceTime |
120 (default) |
30 |
halves window for pre-auth resource abuse |
PermitEmptyPasswords |
yes (never leave) |
no |
pairs with shadow check in §1 |
X11Forwarding |
yes (Debian default!) |
no |
forwards X authority; server has no business running X clients remotely |
AllowAgentForwarding |
yes |
no |
agent socket forwarding lets a compromised jump host reuse operator keys |
AllowTcpForwarding |
yes |
no or local if tunnels required |
turns any SSH account into an internal-network pivot |
ClientAliveInterval/CountMax |
0 (never probes) |
300 / 3 |
reaps dead sessions; do not confuse with TCPKeepAlive |
AllowUsers/AllowGroups |
unset (any valid account logs in) | explicit group e.g. AllowGroups ssh-users |
shrinks auth surface to named identities |
Banner |
absent | /etc/issue.net |
legal deterrent only — Low severity |
Protocol note: no Protocol directive is needed — OpenSSH ≥7.6 speaks SSH-2 only; encountering Protocol 2 is harmless legacy, not a finding.
Key hygiene. Enumerate and grade keys:
find /root /home -name authorized_keys -exec sh -c 'echo "--- {}"; awk "{print \$1\" \"\$3}" "{}"' \;
for k in /root/.ssh/*.pub /home/*/.ssh/*.pub; do [ -f "$k" ] && ssh-keygen -lf "$k"; done
Rules: prefer ed25519 (ssh-ed25519); RSA only at ≥3072 bits (size shown by ssh-keygen -lf) — flag smaller and any ssh-dss; passphrase-protect interactive-use private keys (client-side read-only check: ssh-keygen -y -P "" -f ID_FILE printing output = unencrypted). Automation keys must carry restrictions instead of passphrases they cannot type:
restrict,command="/usr/local/bin/deploy-sync",from="10.0.20.0/24" ssh-ed25519 AAAA... ci-runner
authorized_keys(5) option set: restrict, command="...", from="pattern", no-port-forwarding, no-agent-forwarding, no-x11-forwarding, no-pty, expiry-time="YYYYMMDDHHMM" (OpenSSH ≥7.7). Flag bare unrestricted keys used by CI/automation.
known_hosts trust: prefer hashed entries (HashKnownHosts yes). Treat unexpected host-key mismatch as potential MITM — never advise deleting the line without out-of-band fingerprint verification. In repos, flag -oStrictHostKeyChecking=no (and blind accept-new) against production hosts: trust-on-first-use over an unauthenticated network is standing interception risk.
4. PAM Lockout, Password Quality, Expiry, Hashing
Locate the active stack first — names differ by distro (Debian/Ubuntu: common-auth/common-password includes; RHEL/Rocky: system-auth/password-auth via authselect; Alpine: minimal per-service files):
ls /etc/pam.d/; authselect current 2>/dev/null
grep -RsE 'pam_faillock|pam_tally2' /etc/pam.d/
grep -E '^\s*(deny|unlock_time|even_deny_root|users)\s*=' /etc/security/faillock.conf 2>/dev/null
Prefer pam_faillock — pam_tally2 was removed from RHEL 8+ and Debian 11+; seeing tally2 means old stack, migrate. Target: faillock present in both preauth and authfail phases, deny = 5, unlock_time = 900. Root is exempt unless even_deny_root; exempt fragile automation identities explicitly via users =. Alpine's BusyBox login often ships neither pam_faillock nor pam_pwquality — report control absence, do not pretend a config value exists.
Quality and expiry:
grep -Ev '^\s*(#|$)' /etc/security/pwquality.conf
grep -E '^(PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE|ENCRYPT_METHOD|UMASK)' /etc/login.defs
chage -l SOMEHUMANUSER
Targets: minlen >= 14 plus minclass = 3 (or three of dcredit/ucredit/lcredit/ocredit negative); PASS_MAX_DAYS <= 365, PASS_MIN_DAYS >= 1, PASS_WARN_AGE >= 7 for human accounts (system/service accounts exempt — record exceptions). login.defs defaults apply to new accounts only; remediate existing users with chage. Hashing: accept SHA512 or YESCRYPT (YESCRYPT is default on Debian 12, RHEL 9, Ubuntu 22.04; SHA512 on older) — flag MD5/DES.
Distro honesty: Alpine's minimal BusyBox login often ships neither pam_faillock nor pam_pwquality; installing them changes the audit target from "value wrong" to "control absent" — report absence, do not pretend a config value exists.
5. Kernel and Network Sysctl
Always read live values; file presence proves nothing:
sysctl kernel.kptr_restrict kernel.dmesg_restrict kernel.unprivileged_bpf_disabled \
kernel.randomize_va_space kernel.yama.ptrace_scope kernel.perf_event_paranoid \
fs.protected_symlinks fs.protected_hardlinks fs.protected_fifos fs.protected_regular fs.suid_dumpable
sysctl net.ipv4.ip_forward net.ipv4.icmp_echo_ignore_broadcasts net.ipv4.tcp_syncookies \
net.ipv4.tcp_max_syn_backlog net.core.somaxconn net.ipv4.tcp_fin_timeout
sysctl -a 2>/dev/null | grep -E '(accept_redirects|send_redirects|accept_source_route|rp_filter|log_martians)'
Network table:
| Sysctl key | Risky value | Hardened value | Why |
|---|---|---|---|
net.ipv4.ip_forward |
1 on non-router |
0 — context-dependent |
routers/NAT/Docker/K8s nodes legitimately need 1; verify role before flagging |
net.ipv4.icmp_echo_ignore_broadcasts |
0 |
1 (modern default) |
smurf-amplification refusal |
net.ipv4.conf.{all,default}.accept_redirects |
1 |
0 |
ICMP redirect route injection |
net.ipv6.conf.{all,default}.accept_redirects |
1 |
0 |
same, IPv6 |
net.ipv4.conf.{all,default}.send_redirects |
1 on non-router |
0 |
stops host acting as redirect source |
net.{ipv4,ipv6}.conf.{all,default}.accept_source_route |
1 |
0 |
obsolete source-routed packets are pure attack surface |
net.ipv4.conf.{all,default}.rp_filter |
0/2 |
1 strict — caveat multi-homed |
drops spoofed-source packets; breaks asymmetric routing (BGP/policy-routed hosts) — document exception |
net.ipv4.tcp_syncookies |
0 |
1 (default) |
SYN-flood survival |
net.ipv4.tcp_max_syn_backlog |
128 legacy default |
>= 1024 |
absorbs SYN bursts (cross-ref DOS) |
net.core.somaxconn |
128 legacy default |
>= 1024 |
listen backlog ceiling for apps |
net.ipv4.tcp_fin_timeout |
60 default |
<= 30 |
faster orphaned-FIN reclaim |
net.ipv4.conf.{all,default}.log_martians |
0 |
1 |
logs spoofed/martian packets for FW correlation |
conf.all vs conf.default trap: for accept_redirects, per-interface values govern behavior (all is an aggregate guard) while default applies only to interfaces created after being set — verify one live NIC too: sysctl net.ipv4.conf.eth0.accept_redirects. Namespace honesty: kernel.unprivileged_userns_clone exists only on Debian/Ubuntu-patched kernels; mainline exposes user.max_user_namespaces instead. Restricting unprivileged userns hardens kernel surface but breaks rootless podman/toolbox/sandboxed browsers — role-dependent decision, never a silent default fix.
Kernel/self-protection table:
| Sysctl key | Risky value | Hardened value | Why |
|---|---|---|---|
kernel.kptr_restrict |
0 |
2 |
hides kernel pointers from unprivileged readers (defeats KASLR-leak privesc); 1 = restricted-to-CAP_SYSLOG middle ground |
kernel.dmesg_restrict |
0 |
1 |
blocks unprivileged dmesg (leaks addresses, module info) |
kernel.unprivileged_bpf_disabled |
0 |
1 (permanent variant 2 on kernels ≥5.16) |
unprivileged BPF is a recurring exploit primitive; breaks eBPF observability agents — coordinate |
fs.protected_symlinks / _hardlinks |
0 |
1 |
blocks sticky-dir symlink/hardlink attacks on /tmp-class dirs |
fs.protected_fifos / _regular |
0 |
1 (2 stricter) |
blocks FIFO/regular-file creation races in world-writable sticky dirs |
kernel.randomize_va_space |
0/1 |
2 |
full ASLR incl. heap/stack/libraries |
kernel.yama.ptrace_scope |
0 |
>= 1 (2 typical server target) |
stops arbitrary process injection/credential scraping via ptrace |
kernel.perf_event_paranoid |
< 2 |
2 (3 = fully disable user perf_event_open) |
perf-based side-channel and address-leak surface |
6. Filesystem and Permissions
SUID/SGID inventory. Drop -xdev (or iterate mountpoints) when /usr or /var are separate filesystems:
find / -xdev \( -type f -o -type d \) \( -perm -4000 -o -perm -2000 \) -printf '%m %u:%g %p\n' 2>/dev/null | sort -k3
getcap -r / 2>/dev/null | grep -v '^/proc'
Expected-vs-unexpected judgement table:
| Path | Normal on | Action if missing/unexpected |
|---|---|---|
/usr/bin/sudo, /usr/bin/su, /usr/bin/passwd, /usr/bin/chage, /usr/bin/gpasswd, /usr/bin/chsh, /usr/bin/chfn, /usr/bin/newgrp |
all distros | expected suid root |
/usr/bin/mount, /usr/bin/umount |
util-linux installs (often cap-based now) | expected; absence fine on modern |
/usr/bin/pkexec, /usr/bin/fusermount3 |
desktop/systemd distros | unexpected on headless servers → investigate |
/usr/bin/crontab |
sgid crontab (Debian) / suid root (RHEL) |
mode varies — compare to package DB |
/usr/lib/openssh/ssh-keysign, /sbin/unix_chkpwd |
OpenSSH/util-linux helpers | expected suid root |
anything under /home, /tmp, /var/tmp, /usr/local |
never expected | planted binary — treat as incident (see Exploitation §3) |
Verify provenance of every entry outside the table: dpkg -S FILE (Debian) / rpm -qf FILE (RHEL); "not owned by any package" = immediate escalation. Modern distros move capabilities instead of SUID (getent ping uses cap_net_raw) — getcap hits like cap_setuid deserve the same scrutiny as SUID.
World-writable artifacts and ownership gaps:
find / -xdev -type d -perm -0002 ! -perm -1000 -print # writable dirs MISSING sticky bit
find / -xdev -type f -perm -0002 -print # any hit is a finding
find / -xdev \( -nouser -o -nogroup \) -print 2>/dev/null | head -20
stat -c '%a %n' /tmp /var/tmp /dev/shm # expect 1777 (sticky)
findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /dev/shm /tmp /var/tmp 2>/dev/null
Require /dev/shm mounted nosuid,nodev,noexec (and dedicated /tmp, /var/tmp partitions likewise). Single-partition cloud hosts: document residual risk; at minimum persist hardened /dev/shm options in fstab.
cron/at gating (allow-list model preferred), umask, homes, core dumps:
ls -l /etc/cron.allow /etc/at.allow 2>/dev/null || echo "ALLOW-LISTS-MISSING"
stat -c '%a %U:%G %n' /etc/crontab /etc/cron.d /etc/cron.daily 2>/dev/null
ls -la /var/spool/cron /var/spool/cron/atjobs 2>/dev/null # Debian paths; RHEL: /var/spool/at
grep -E '^\s*UMASK' /etc/login.defs
grep -RhsE '^\s*umask\s+00[02]\b' /etc/profile /etc/bash.bashrc /etc/profile.d/ 2>/dev/null
stat -c '%a %U:%G %n' /home/* 2>/dev/null # expect <= 750 each
grep -RhE '^\s*\*\s+hard\s+core\b' /etc/security/limits.conf /etc/security/limits.d/ 2>/dev/null
grep -E '^\s*(Storage|ProcessSizeMax)' /etc/systemd/coredump.conf /etc/systemd/coredump.conf.d/*.conf 2>/dev/null
ulimit -c # expect 0 in a login shell
Flag when neither allow nor deny files exist (everyone may schedule), when deny-only model is used, or when cron dirs are group/world-writable. Targets: UMASK 027 (077 high-sensitivity); home dirs without group/world write (and without world read on sensitive boxes); * hard core 0; coredump.conf Storage=none plus ProcessSizeMax=0; fs.suid_dumpable=0 (Section 5 table block).
7. Boot and Disk
GRUB password is contextual: required when console/KVM/cloud-serial access is reachable; fleet behind tight IAM may carry a documented exception. LUKS absence on hosts holding regulated data is an architectural gap (severity scales with data classification; remediation is rebuild/migration, not inline change). fstab protection applies nodev,nosuid (+noexec where nothing executes) to dedicated data/removable mounts — never demand them on / or /usr.
grep -l password_pbkdf2 /boot/grub/grub.cfg /boot/grub2/grub.cfg 2>/dev/null # GRUB pw present?
stat -c '%a %U:%G %n' /boot/grub*/grub.cfg /boot/grub2/user.cfg 2>/dev/null
lsblk -o NAME,FSTYPE,MOUNTPOINT | grep -i crypto_luks || echo "NO-LUKS-VISIBLE"
cat /etc/crypttab 2>/dev/null
awk '$3!~/^(swap|proc|sysfs|devpts|tmpfs|overlay|iso9660)$/ {print $1,$2,$3,"opts="$4}' /etc/fstab
findmnt -no TARGET,PARTLABEL,OPTIONS -t ext4,xfs,btrfs,vfat,exfat,ntfs3
8. Time Synchronization
timedatectl show -p NTPSynchronized -p NTP -p TimeUSec
systemctl is-active chronyd chrony systemd-timesyncd ntpd 2>/dev/null
chronyc sources 2>/dev/null || ntpq -p 2>/dev/null
grep -RhE '^(server|pool|NTP=|FallbackNTP)' /etc/chrony/chrony.conf /etc/chrony.conf \
/etc/systemd/timesyncd.conf /etc/ntp.conf 2>/dev/null
Flag: no active sync daemon, NTPSynchronized=no, sources pointing at unroutable or vendor-example default addresses. Every timestamp in every other module's evidence depends on this — unsynchronized clocks make LOGMON correlation unreliable and break TLS/Kerberos time-sensitive flows.
Where To Look
Evidence collection: tools/sweeps/sweep-baseline.sh captures [BASE-nn] sections verbatim; judge them against this module's rubrics, never against raw output alone. The paste-ready sweep below covers the same ground for interactive use.
Paste-ready audit sweep — highest-yield checks, strictly read-only, safe on production; [ROOT] lines need root, everything else works unprivileged:
#!/usr/bin/env bash
# Baseline sweep — READ-ONLY. [ROOT] lines need root.
echo "== OS =="; head -2 /etc/os-release; uname -r
echo "== UID-0 accounts (expect only root) =="; awk -F: '$3==0{print $1,$7}' /etc/passwd
echo "== interactive accounts =="; awk -F: '$7!~/(nologin|false|sync)$/{print $1":"$3":"$7}' /etc/passwd
echo "== empty passwords [ROOT] =="; awk -F: '($2==""){print $1}' /etc/shadow 2>/dev/null || echo "need-root"
echo "== NOPASSWD sudo =="; grep -RsE '^[^#].*NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null || echo none
echo "== sshd effective [ROOT] =="; sshd -T 2>/dev/null | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|authenticationmethods|maxauthtries|x11forwarding|allowtcpforwarding|allowagentforwarding|permitemptypasswords)' || grep -RhE '^\s*(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|AuthenticationMethods|MaxAuthTries|PermitEmptyPasswords|X11Forwarding|AllowTcpForwarding|AllowAgentForwarding|LoginGraceTime)' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null
echo "== PAM/policy =="; grep -E '^\s*(deny|unlock_time)\s*=' /etc/security/faillock.conf 2>/dev/null; grep -RsE 'pam_(faillock|tally2)' /etc/pam.d/ 2>/dev/null | head -3; grep -E '^\s*(minlen|minclass)\s*=' /etc/security/pwquality.conf 2>/dev/null; grep -E '^(PASS_MAX_DAYS|ENCRYPT_METHOD|UMASK)' /etc/login.defs
echo "== sysctls =="; sysctl kernel.kptr_restrict kernel.dmesg_restrict kernel.randomize_va_space kernel.yama.ptrace_scope kernel.unprivileged_bpf_disabled fs.protected_symlinks fs.protected_hardlinks fs.suid_dumpable net.ipv4.ip_forward net.ipv4.tcp_syncookies net.ipv4.conf.all.accept_redirects net.ipv6.conf.all.accept_redirects net.ipv4.conf.all.send_redirects 2>&1
echo "== SUID =="; find / -xdev -type f -perm -4000 2>/dev/null
echo "== world-writable dirs w/o sticky =="; find / -xdev -type d -perm -0002 ! -perm -1000 -print 2>/dev/null
echo "== world-writable files =="; find / -xdev -type f -perm -0002 -print 2>/dev/null
echo "== tmp modes (expect 1777) =="; stat -c '%a %n' /tmp /var/tmp /dev/shm 2>/dev/null
echo "== /dev/shm opts (want nosuid,nodev,noexec) =="; findmnt -no OPTIONS /dev/shm 2>/dev/null
echo "== cron/at allow-lists =="; ls -l /etc/cron.allow /etc/at.allow 2>/dev/null || echo missing
echo "== coredumps =="; ulimit -c; grep -hE '^\s*\*\s+hard\s+core' /etc/security/limits.conf /etc/security/limits.d/* 2>/dev/null; grep -hE '^\s*(Storage|ProcessSizeMax)' /etc/systemd/coredump.conf 2>/dev/null
echo "== time sync =="; timedatectl show -p NTPSynchronized -p NTP 2>/dev/null || systemctl is-active systemd-timesyncd chronyd 2>/dev/null
Artifact map:
| Evidence | Primary path(s) | Live command |
|---|---|---|
| Accounts/groups | /etc/passwd, /etc/shadow [ROOT], /etc/group |
awk -F: filters above |
| sudo | /etc/sudoers, /etc/sudoers.d/* (mode 0440) |
visudo -c, sudo -l |
| sshd config | /etc/ssh/sshd_config, /etc/ssh/sshd_config.d/*.conf |
sshd -T [ROOT] (needs root for host keys; fallback = file text, but say so — drop-ins can contradict it) |
| Authorized keys | /root/.ssh/authorized_keys, /home/*/.ssh/authorized_keys |
find ... -name authorized_keys |
| PAM + policy files | /etc/pam.d/common-* (Deb), /etc/pam.d/{system,password}-auth (RHEL); /etc/security/faillock.conf, /etc/security/pwquality.conf, /etc/login.defs, /etc/security/limits.conf(.d/) |
grep -Rs pam_faillock /etc/pam.d/, grep deny /etc/security/faillock.conf |
| sysctl runtime vs files | kernel state; /etc/sysctl.conf, /etc/sysctl.d/*, /usr/lib/sysctl.d/* (last-applied wins — trust sysctl -n KEY, not file text) |
sysctl KEY; diff files vs live values |
| Mounts/crypto | /etc/fstab, /proc/mounts, /etc/crypttab |
findmnt, lsblk -o NAME,FSTYPE |
| cron/at | /etc/crontab, /etc/cron.d/, /etc/cron.{allow,deny}, /etc/at.{allow,deny} |
ls -l |
| Core dumps / time sync / boot | /etc/systemd/coredump.conf(.d/), /etc/chrony/chrony.conf (Deb) or /etc/chrony.conf (RHEL), /etc/systemd/timesyncd.conf, /boot/grub/grub.cfg or /boot/grub2/grub.cfg |
coredumpctl list, timedatectl, chronyc sources, grep password_pbkdf2 |
Patterns & Signatures
Host-side signature greps — each should return zero hits on a hardened host; every hit is candidate evidence:
grep -RnsE '^\s*(PermitRootLogin|PasswordAuthentication|PermitEmptyPasswords)\s+yes' /etc/ssh/ 2>/dev/null
grep -RnsE '^\s*X11Forwarding\s+yes' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null
grep -RnsE 'NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null # judge scope per What To Check §2
grep -RnsE '^\s*umask\s+(000|002)\b' /etc/profile /etc/bash.bashrc /etc/profile.d/ 2>/dev/null
grep -RnsE '(accept_redirects|send_redirects|accept_source_route)\s*=\s*1|(randomize_va_space|yama\.ptrace_scope|kptr_restrict|dmesg_restrict)\s*=\s*0' /etc/sysctl.conf /etc/sysctl.d/ 2>/dev/null
grep -RnsE '^ENCRYPT_METHOD\s+(MD5|DES)' /etc/login.defs
Config-as-code sweeps (repo root; covers Ansible, cloud-init, Terraform, Dockerfile, Packer; requires ripgrep):
rg -n --hidden -g '!.git/' \
-e '(PermitRootLogin|PasswordAuthentication):\s*(yes|"yes")' \
-e '(PermitRootLogin|PasswordAuthentication|PermitEmptyPasswords)\s+yes' \
-e 'NOPASSWD:\s*(ALL\b|/bin/su|/bin/bash|/bin/sh)' \
-e '/usr/bin/(find|python[0-9.]*|perl|ruby|php|vi[m]?|less|tar)\b.*NOPASSWD'
rg -n --hidden -g '!.git/' \
-e 'chmod\s+(-R\s+)?(777|666)\b' \
-e 'umask\s+(000|002)\b' \
-e 'mode:\s*0?777\b'
rg -n --hidden -g '!.git/' \
-e 'lock_passwd:\s*[Ff]alse' \
-e 'plain_text_passwd' \
-e 'ssh_pwauth:\s*true' \
-e 'StrictHostKeyChecking=\s*no'
rg -n --hidden -g '!.git/' \
-e 'net\.(ipv4|ipv6)\.conf\.(all|default)\.(accept_redirects|send_redirects|accept_source_route)\s*=\s*1' \
-e 'kernel\.(randomize_va_space|yama\.ptrace_scope|kptr_restrict)\s*=\s*0' \
-e '^ENCRYPT_METHOD\s+MD5'
rg -ln --hidden -g '!.git/' -e '/var/run/docker\.sock' # chains into SANDBOX module
Rendered-example pair (cloud-init user-data), showing the vulnerable and fixed shapes side by side:
# VULNERABLE — cloud-init user-data
users:
- { name: deploy, lock_passwd: false }
ssh_pwauth: true # PasswordAuthentication yes for the whole host
# FIXED — key-only from first boot; no password path exists
users:
- { name: deploy, lock_passwd: true, ssh_authorized_keys: ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... deploy@ci"] }
ssh_pwauth: false # renders as PasswordAuthentication no
Taint Tracing Guidance
Classify each finding as entry, amplifier, or containment-loss; then report chains, not isolated values. Chain table (taint origin → combines-with → attack path → ceiling severity):
| # | Origin misconfig | Chains with | Attack path | Ceiling |
|---|---|---|---|---|
| 1 | PermitRootLogin yes + PasswordAuthentication yes on :22 internet-exposed |
absent faillock/fail2ban | botnet credential spray reaches root directly; no intermediate identity to lock out | Critical 9.8 |
| 2 | Broad NOPASSWD: ALL for web/service user |
app-layer RCE (any module) | svc shell → silent root → authorized_keys persistence in /root/.ssh, cron implants |
High 8.8 |
| 3 | AllowAgentForwarding yes on jump hosts |
operator habit of agent reuse | compromised jump steals forwarded agent socket → lateral movement with operator's keys | High |
| 4 | AcceptRedirects=1 (v4/v6) |
attacker on-path (rogue AP/hostile LAN) | forged ICMP redirect rewrites routes → interception of plaintext protocols | Medium–High by position |
| 5 | kptr_restrict=0 + dmesg_restrict=0 + ptrace_scope=0 (+ open userns) |
any local foothold; public kernel exploit | KASLR leak + process injection converts unreliable bug into deterministic privesc | amplifier to Critical |
| 6 | World-writable dir w/o sticky + planted SUID | any foothold | drop SUID shell in writable PATH dir → persistent local root | High 7.7 |
| 7 | Executable /dev/shm |
app file-write primitive | stage ELF outside hardened /tmp; memory-resident malware |
amplifier |
| 8 | Empty-password account | any local shell access | su ACCOUNT with empty string = instant identity theft |
High 7.9 |
| 9 | No cron/at allow-list | low-priv foothold | schedule persistence tasks without admin visibility | Low–Medium |
| 10 | NTP unsynchronized | every other module | timestamps untrustworthy → detection gaps, replay-window failures, forensic ambiguity | force multiplier |
Guidance rules:
- Position beats absolute value: identical
PasswordAuthentication yesis Medium behind a VPN allowlist, Critical on an edge host — pull the socket map (ss -tlnp | grep ':22') and FW module output before assigning. Count the chain, not the finding: rows 1, 2, 6 together mean one web-app bug yields full host compromise with persistence — say so in SUMMARY.md chains (Phase 4). - Amplifier findings (rows 4, 5, 7) get severity from what they enable, not their own CVSS base; annotate "enabler" rather than inflating standalone scores.
Exploitation & Reproduction
Reproductions are READ-ONLY commands demonstrating weak state; attacker paths are described, not scripted. Attach actual output to each finding.
1. Internet-exposed root password login (Critical).
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication)'
# permitrootlogin yes
# passwordauthentication yes
ss -tlnp | grep ':22 ' # 0.0.0.0/[::] binding = internet-reachable (confirm with FW module)
Attacker path: mass scanners fingerprint :22 continuously; botnets spray breached credential pairs against root — one hit yields a root shell with no local foothold and no per-user trail.
2. Service-account sudo takeover (High).
sudo -u www-data sudo -n -l # or run as the service account
# User www-data may run the following commands: (ALL) NOPASSWD: ALL
Attacker path: web-app RCE lands as www-data; sudo -n succeeds silently; attacker spawns a root shell and drops an SSH key into /root/.ssh/authorized_keys for persistence.
3. Planted SUID binary (High).
find / -xdev -type f -perm -4000 -printf '%m %u:%g %TY-%Tm-%Td %p\n' 2>/dev/null | sort -k4
dpkg -S /usr/local/bin/backup-runner 2>&1 # 'no path found' = not from any package; expect -rwsr-xr-x root root
Attacker path: any local user executes the planted binary → EUID 0. /usr/local plus absent package provenance is the tell — treat as active compromise (quarantine mode-bit, preserve evidence).
4. ICMP redirect acceptance, pointer leaks, executable shm (Medium amplifiers).
sysctl net.ipv4.conf.all.accept_redirects net.ipv6.conf.all.accept_redirects # = 1 both
dmesg | tail -5 # works unprivileged when dmesg_restrict=0
grep -m3 ' [tT] ' /proc/kallsyms # resolved addresses when kptr_restrict=0
cat /proc/sys/kernel/randomize_va_space # 0 or 1 = weakened ASLR
findmnt -no OPTIONS /dev/shm # lacks noexec,nosuid,nodev
Attacker paths: a rogue segment host sends an ICMP redirect announcing a "better" route through itself — the host installs it and connections traverse the attacker. Readable dmesg/kallsyms defeat KASLR first, converting crash-prone public kernel exploits into one-shot privesc. With any app-level file-write primitive, an ELF staged in executable /dev/shm runs while sidestepping hardened /tmp.
5. Empty-password identity theft (High).
sudo awk -F: '($2==""){print $1}' /etc/shadow
# backupsvc
su - backupsvc # empty string at prompt succeeds for any local user
Attacker path: every local account holder instantly becomes backupsvc — inheriting its cron jobs, service credentials, group memberships.
Remediation
All commands in this section are mutating: Phase 6 only, after approval, with rollback notes. Lockout-risk operations carry inline warnings — honor them.
sshd hardening
# FIXED — /etc/ssh/sshd_config.d/99-hardening.conf
# Check no lower-numbered drop-in re-enables these; first value obtained wins.
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
PermitEmptyPasswords no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no # use 'local' only if tunnels are a stated requirement
ClientAliveInterval 300
ClientAliveCountMax 3
AllowGroups ssh-users # populate BEFORE reload
Apply protocol (order matters): populate ssh-users and verify membership (id) → confirm key login from a second terminal (ssh -i ~/.ssh/id_ed25519 host true) → sshd -t → systemctl reload sshd (reload keeps established sessions; never restart from a session you cannot afford to lose). Confirm console/out-of-band access exists before touching sshd on any remote host.
Accounts and sudo
usermod -L BACKDOOR_ACCT && usermod -s /usr/sbin/nologin BACKDOOR_ACCT # duplicate UID-0: also alert/incident
passwd -l STALE_ACCT # lock unused accounts
chage -E 1 STALE_ACCT # dated retirement alternative
passwd SERVICE_ACCT # replace empty password; prefer locking + key access
chage -m 1 -M 365 -W 7 HUMAN_USER # apply expiry policy to existing users
# FIXED — /etc/sudoers.d/010-hardening (0440 root:root; edit ONLY via visudo -f)
Defaults timestamp_timeout=15
Defaults passwd_tries=3
Defaults use_pty
%sysadmins ALL=(ALL:ALL) ALL
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service
Remove offending grant lines from their original files rather than layering overrides; validate with visudo -c before ending your session.
PAM and password policy
# FIXED — /etc/security/faillock.conf
deny = 5
unlock_time = 900
# users = svc-backup # explicit exemptions for automation identities only
# Wire-up: Debian/Ubuntu add pam_faillock.so preauth/authfail to /etc/pam.d/common-auth;
# RHEL/Rocky: authselect enable-feature with-faillock && authselect apply-changes
# FIXED — /etc/security/pwquality.conf
minlen = 14
minclass = 3
dictcheck = 1
# FIXED — /etc/login.defs (new-account defaults)
PASS_MAX_DAYS 365
PASS_MIN_DAYS 1
PASS_WARN_AGE 7
UMASK 027
ENCRYPT_METHOD SHA512 # keep distro default YESCRYPT where present — do not downgrade
sysctl
# FIXED — /etc/sysctl.d/99-hardening.conf
net.ipv4.ip_forward = 0 # DELETE this line on routers/NAT/container hosts
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.core.somaxconn = 1024
net.ipv4.tcp_fin_timeout = 30
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.unprivileged_bpf_disabled = 1 # coordinate with eBPF observability owners first
kernel.randomize_va_space = 2
kernel.yama.ptrace_scope = 1 # 2 where no dev/debug workflows exist
kernel.perf_event_paranoid = 2 # 3 to disable user perf_event_open entirely
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_fifos = 1
fs.protected_regular = 2
fs.suid_dumpable = 0
Apply with sysctl --system, then sweep per-interface leftovers that predate the default settings:
for i in $(ls /sys/class/net | grep -v lo); do
sysctl -w "net.ipv4.conf.$i.accept_redirects=0"
sysctl -w "net.ipv4.conf.$i.send_redirects=0"
done
Filesystem, mounts, cron/at, core dumps
chmod o-w /path/from/world-writable-scan # each world-writable FILE
chmod +t /var/tmp # restore missing sticky
chmod u-s /usr/local/bin/SUSPECT_BINARY # quarantine planted SUID — do NOT delete evidence
touch /etc/cron.allow /etc/at.allow && chmod 600 /etc/cron.allow /etc/at.allow && chown root:root /etc/cron.allow /etc/at.allow
rm -f /etc/cron.deny /etc/at.deny # switch deny-model -> allow-model (populate allow files FIRST)
# FIXED — /etc/fstab line (then: mount -o remount /dev/shm && findmnt /dev/shm)
tmpfs /dev/shm tmpfs rw,nosuid,nodev,noexec,mode=1777 0 0
# FIXED — /etc/security/limits.d/99-core.conf
* hard core 0
# FIXED — /etc/systemd/coredump.conf
[Coredump]
Storage=none
ProcessSizeMax=0
# then: systemctl daemon-reload
Time sync
# FIXED — /etc/chrony/chrony.conf (Debian path; RHEL drops the chrony/ dir level)
pool 2.debian.pool.ntp.org iburst # or: server ntp.internal.example.com iburst prefer
makestep 1.0 3
Enable the distro daemon: systemctl enable --now chrony (Alpine), chronyd (RHEL), or systemd-timesyncd (Debian minimal).
Verification & Validation
Post-fix proof list — run each command, compare to the expected output; every fix from Remediation maps to exactly one line here:
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|authenticationmethods|pubkeyauthentication)'
# permitrootlogin no / passwordauthentication no / authenticationmethods publickey / pubkeyauthentication yes
awk -F: '$3==0{print $1}' /etc/passwd # root only
sudo awk -F: '($2==""){print $1}' /etc/shadow # (no output)
grep -RsE 'NOPASSWD:\s*ALL' /etc/sudoers /etc/sudoers.d/ && visudo -c # no hits; parsed OK (scoped grants reviewed)
sysctl -n kernel.kptr_restrict kernel.dmesg_restrict kernel.randomize_va_space fs.protected_symlinks fs.protected_hardlinks fs.suid_dumpable # 2 1 2 1 1 0
sysctl -n net.ipv4.conf.all.accept_redirects net.ipv6.conf.all.accept_redirects # 0 / 0
find / -xdev -type f -perm -4000 2>/dev/null | sort > /tmp/suid-post
diff /tmp/suid-pre /tmp/suid-post # only intentional deltas vs pre-change baseline
findmnt -no OPTIONS /dev/shm | tr ',' '\n' | grep -E 'nosuid|nodev|noexec' # all three present
ls -l /etc/cron.allow /etc/at.allow # exist, 600, root:root
timedatectl show -p NTPSynchronized # NTPSynchronized=yes
chronyc sources # >=1 ^* synced source
su - NOLOGIN_TEST -c true 2>&1 | grep -q nologin && echo "shell-gating OK" # throwaway sanity check
# Session survival before releasing the window: from a SECOND terminal run `sudo -k && sudo -l`
# and a fresh key-based ssh login; only then close the original session.
Regression notes — what breaks when hardening is applied wrongly:
| Change | Classic breakage | Precaution |
|---|---|---|
AllowUsers/AllowGroups typo |
every SSH login refused, including admins | confirm console/out-of-band access first; keep current session open; sshd -t; reload not restart |
PasswordAuthentication no deployed before keys distributed |
password-only colleagues and break-glass automation locked out | distribute+test keys first, flip switch second |
kernel.unprivileged_bpf_disabled=1 |
eBPF-based monitoring/tracing agents (Cilium, bcc-style profilers) silently die | coordinate with observability owners; schedule with their deploy |
forcing ip_forward=0 on infra roles |
Docker/Kubernetes NAT breaks; containers lose egress | exempt routers/NAT/container nodes explicitly in findings |
rp_filter=1 on asymmetric/multi-homed hosts |
BGP/VRRP/policy-routed traffic dropped | documented exception instead of blanket value |
fs.protected_regular=2 |
legacy daemons pre-creating shared files in /tmp fail on write |
canary one host first; drop to 1 if an app breaks, record which |
| faillock without exemptions | automation with rotating/typo-prone creds locks itself out repeatedly | use users= exemptions + monitor /var/log/faillock |
Storage=none for coredumps |
crash debugging loses dumps | journald still records metadata; raise ProcessSizeMax temporarily per-unit when triaging |
Regression hygiene: store the sweep output (Where To Look block) as the pre-change baseline; post-change diff is your regression report. Re-run the full sweep after kernel updates — new kernels occasionally reset sysctl defaults.
Severity Assessment
Anchors with CVSS v3.1 example vectors. Adjust for exposure (internet vs internal), compensating controls, and host role; document every adjustment.
| # | Finding class | Anchor severity | Example vector (CVSS:3.1) | Score | Context modifiers |
|---|---|---|---|---|---|
| 1 | PermitRootLogin yes + PasswordAuthentication yes, SSH internet-exposed |
Critical | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
9.8 | VPN/allowlist-only → downgrade one tier with evidence |
| 2 | Unknown/duplicate UID-0 account or planted SUID root binary | High (treat as incident) | AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
7.7 | C2/persistence evidence → escalate Critical, hand to IR |
| 3 | Broad NOPASSWD: ALL on network-facing service account |
High | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
8.8 | requires adjacent app RCE — note as chain enabler too |
| 4 | Empty-password account with login shell | High | AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
7.9 | UID-0 member or multi-user host → Critical |
| 5 | Missing sysctl hardening (redirects, kptr/dmesg, ASLR) internal host | Medium | AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
5.7 | edge/internet-exposed position raises tier |
| 6 | Executable /dev/shm (+ app write primitive) |
Medium | AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N |
6.3 | amplifier — pair with taint row 7 |
| 7 | cron/at unrestricted (no allow-list) | Medium | AV:A/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N |
6.3 | single-admin box → Low acceptable with note |
| 8 | Time sync absent/unsynchronized | Low standalone | AV:A/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N |
3.5 | force multiplier — annotate forensic impact regardless of score |
| 9 | Missing login banner, lax LoginGraceTime/MaxAuthTries only |
Low | AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N |
3.0 | never aggregate up; keep cosmetic findings honest |
Severity discipline: anchors 1–4 are reachable paths, not theoretical — verify exposure via FW module before finalizing; amplifier findings inherit narrative weight from Taint Tracing but keep their own measured CVSS.
Common False Positives
Suppress or re-scope these before writing findings:
- Cloud-init owns sshd. Cloud images rewrite SSH settings at boot (
ssh_pwauth,cc_sshmodule,ssh_deletekeys). A manual fix to/etc/ssh/sshd_configmay be reverted on rebuild. Verify where truth lives; if user-data/image is the source, file the finding against the repo artifact, not just the live file. - Silent directive ≠ risky when distro default is safe.
PermitRootLoginabsent means OpenSSH defaultprohibit-password(key-only root) — not a finding unless effective value isyes. Ubuntu cloud images commonly ship/etc/ssh/sshd_config.d/60-cloudimg-settings.confwithPasswordAuthentication no; judgesshd -T, never file absence. - Containers are not VMs. Docker/Podman images lack systemd, PAM, timesync daemons, and often run root by design inside a namespace. Applying this baseline inside a container generates noise; audit the container host here, and container-specific risks via SANDBOX.
- Modern kernel defaults already satisfy several sysctl targets (
tcp_syncookies=1,icmp_echo_ignore_broadcasts=1, protected_* enabled on recent kernels). Missing config files are not misconfigurations — compare livesysctl -nvalues against the tables only. kptr_restrict=1is partial protection, not wide-open. Distinguish 0 (open), 1 (restricted to CAP_SYSLOG), 2 (always hidden); flagging 1 as "KASLR defeated" overstates it.- Scoped NOPASSWD is legitimate.
NOPASSWD: /usr/bin/systemctl restart myapp.serviceis an accepted pattern; flag only wildcard grants and GTFOBins-abusable commands (interpreters, editors,su, archivers,find). - Missing SUID on legacy binaries is progress, not anomaly.
ping/mountlose SUID in favor of file capabilities on modern distros; checkgetcapoutput instead of expecting the old inventory verbatim. - cron.allow absent can be correct on hosts running exclusively systemd timers — confirm which schedulers exist (
systemctl list-timers,/etc/cron.d/). WSL/devcontainers/laptops are out of server scope per the engagement agreement; do not report their divergent baselines.
References
- CIS Benchmarks — select the benchmark matching
/etc/os-releaseID and agreed profile level: CIS Ubuntu Linux LTS Benchmark (Level 1/2 Server), CIS Debian Linux Benchmark, CIS Red Hat Enterprise Linux Benchmark, CIS Rocky Linux Benchmark, CIS Amazon Linux Benchmark, CIS Alpine Linux Benchmark. https://www.cisecurity.org/cis-benchmarks - CWE-16 Configuration, CWE-250 Execution with Unnecessary Privileges, CWE-732 Incorrect Permission Assignment for Critical Resource — https://cwe.mitre.org/data/definitions/16.html , https://cwe.mitre.org/data/definitions/250.html , https://cwe.mitre.org/data/definitions/732.html
- OpenSSH man pages:
sshd_config(5),ssh_config(5),authorized_keys(5),sshd(8),ssh-keygen(1)— https://man.openbsd.com/sshd_config , https://man.openbsd.com/authorized_keys - Identity/PAM man pages:
passwd(5),shadow(5),sudoers(5),visudo(8),chage(1),faillock.conf(5),pam.d(5),pwquality.conf(5),login.defs(5) - Resource/storage/scheduling/time man pages:
limits.conf(5),coredump.conf(5),systemd-coredump(8),sysctl.d(5),sysctl(8),fstab(5),crypttab(5),findmnt(8),crontab(1),at(1),timesyncd.conf(5),chrony.conf(5) - Kernel sysctl docs: kernel knobs (kptr_restrict, dmesg_restrict, randomize_va_space, unprivileged_bpf_disabled) https://docs.kernel.org/admin-guide/sysctl/kernel.html ; IP sysctls (redirects, rp_filter, syncookies) https://docs.kernel.org/networking/ip-sysctl.html ; Yama ptrace_scope https://docs.kernel.org/admin-guide/LSM/Yama.html
- CVSS v3.1 specification (vector math used above): https://www.first.org/cvss/v3.1/specification-document
- GTFOBins sudo/SUID abuse reference: https://gtfobins.github.io/
- OpenSSH release notes (version boundaries cited: ed25519 keys,
expiry-time≥7.7, SSH-2-only ≥7.6): https://www.openssh.com/releasenotes.html - systemd coredump configuration: https://www.freedesktop.org/software/systemd/man/latest/coredump.conf.html