Debian GNU/Linux · Logging & auditing

Audit every use of setuid and setgid programs

When it fails: Use of privileged programs is not recorded. Programs that run with more rights than their caller are the main path to privilege escalation and are used without a record.

CB-DEB-AUDIT-026mediumactiveautomatedautomated fixprofile server-hardenedprofile desktop-hardened

Description

One auditd rule per setuid or setgid program found on the local file systems that allow them (not mounted nosuid or noexec), key privileged, in /etc/audit/rules.d/60-cirsec-privileged.rules. Each rule records every run of the program by a user who logged in. The list is rebuilt from the file system each time the check or the fix runs.

The scan covers local file systems listed by findmnt --real. Programs on network file systems are not covered.

Rationale

passwd, su, sudo, mount, chsh and similar programs run as root on behalf of a user. Recording each use shows who used them and is the first place to look after a privilege escalation.

Impact

Installing or removing a package that ships a setuid program makes the check fail until the fix runs again. Once the audit configuration is locked (CB-DEB-AUDIT-029), the new rule only loads at the next reboot.

Setting

Location/etc/audit/rules.d/60-cirsec-privileged.rules
DefaultNo audit rules
RecommendedRules loaded (auditctl -l) and present in /etc/audit/rules.d/60-cirsec-privileged.rules

Audit

  1. Run auditctl -l as root. It must list the rules shown below (keys privileged).
  2. Run grep -h . /etc/audit/rules.d/*.rules. The same rules must be present, so they load at boot.

Expected rules, as auditctl -l prints them:

-a always,exit -S all -F path=<program> -F perm=x -F auid>=1000 -F auid!=-1 -F key=privileged

Automated: every check must pass. Scanners report each check by its id.

rules

Rules loaded in the kernel and present in /etc/audit/rules.d.

script: |
  command -v auditctl >/dev/null 2>&1 || { echo "auditctl not found: auditd is not installed"; exit 1; }
  want() {
  # every setuid or setgid file on local file systems mounted without nosuid or noexec
  for m in $(findmnt -rn --real -O nonosuid,nonoexec -o TARGET); do
    find "$m" -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null
  done | sort -u | while IFS= read -r p; do
    echo "-a always,exit -S all -F path=$p -F perm=x -F auid>=1000 -F auid!=-1 -F key=privileged"
  done
  }
  loaded=$(auditctl -l 2>&1) || { echo "$loaded"; exit 3; }
  disk=$(cat /etc/audit/rules.d/*.rules 2>/dev/null)
  n=0 rc=0
  while IFS= read -r r; do
    [ -n "$r" ] || continue
    n=$((n + 1))
    printf '%s\n' "$loaded" | grep -Fxq -- "$r" || { echo "not loaded: $r"; rc=1; }
    printf '%s\n' "$disk" | grep -Fxq -- "$r" || { echo "not in /etc/audit/rules.d: $r"; rc=1; }
  done <<EOF
  $(want)
  EOF
  [ "$rc" = 1 ] || echo "$n rules loaded and present in /etc/audit/rules.d"
  exit "$rc"

Remediation

  1. Write the expected rules to /etc/audit/rules.d/60-cirsec-privileged.rules (mode 0640).
  2. Run augenrules --load. If auditctl -s shows enabled 2, the configuration is locked: reboot to load the new rules.
Restartnone
Disruptionnone

Rollback

Delete /etc/audit/rules.d/60-cirsec-privileged.rules and run augenrules --load (or reboot if the configuration is locked).

rules-file

command -v augenrules >/dev/null 2>&1 || { echo "auditd is not installed" >&2; exit 1; }
want() {
# every setuid or setgid file on local file systems mounted without nosuid or noexec
for m in $(findmnt -rn --real -O nonosuid,nonoexec -o TARGET); do
  find "$m" -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null
done | sort -u | while IFS= read -r p; do
  echo "-a always,exit -S all -F path=$p -F perm=x -F auid>=1000 -F auid!=-1 -F key=privileged"
done
}
f=/etc/audit/rules.d/60-cirsec-privileged.rules
t=$(mktemp)
{ echo '# Written by CIRSEC Baselines CB-DEB-AUDIT-026'; want; } >"$t"
if ! cmp -s "$t" "$f"; then
  cb_backup "$f"
  cat "$t" >"$f"
fi
rm -f "$t"
chmod 640 "$f"
if auditctl -s | grep -q '^enabled 2'; then
  echo "audit rules are locked (-e 2): reboot to load $f"
else
  augenrules --load >/dev/null
fi

Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.

References

Mappings

How this control relates to the other framework's: equivalent, stricter, looser, partial or related.

Threats mitigated

People and lifecycle

Authors
Verified2026-10-18 by Sn0wAlice on 12, Proxmox VM, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64, auditd 1:3.0.9-1, BIOS boot. Rules fail on fresh install; fix writes /etc/audit/rules.d/60-cirsec-privileged.rules and loads it; rules match auditctl -l, kept after reboot; second run idempotent.
Verified2026-10-18 by Sn0wAlice on 13, Proxmox VM, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64, auditd 1:4.0.2-2+deb13u1, BIOS boot. Rules fail on fresh install; fix writes /etc/audit/rules.d/60-cirsec-privileged.rules and loads it; rules match auditctl -l, kept after reboot; second run idempotent.
Introduced inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr