#!/bin/sh # cis_os.sh: the CIS controls image_tools/cis.py measures from inside the image. # # Runs as root in the image, fed on stdin (docker run -i --user 0 ... sh -s), so # nothing has to be mounted from a runner whose paths the host daemon does not # know. Reads nothing from outside but CIS_PACKAGES, the package names the image # may hold. Prints one line per control: # # TAB PASS|FAIL TAB TAB <what fails> # # docker:* are the CIS Docker Benchmark 1.6.0 controls Trivy leaves to "Manual" # and that can be seen from inside; debian:* are the CIS Debian Linux 12 v1.1.0 # controls that make sense in a container. Boot, partitions, kernel parameters, # services, logging, audit and sshd belong to the host (infrastructure # BAUSTANDARD §F) and are not measured here. set -u # check <id> <title> <function>: PASS when the function names no offender. check() { out=$("$3" 2>/dev/null | head -n 20 | tr '\n' ' ') if [ -z "$out" ]; then printf '%s\tPASS\t%s\t\n' "$1" "$2" else printf '%s\tFAIL\t%s\t%s\n' "$1" "$2" "${out% }" fi } login_defs() { awk -v k="$1" '$1 == k { v = $2 } END { print v }' /etc/login.defs; } # Shells one can log in with: /etc/shells without the nologin ones. SHELLS=$(grep '^/' /etc/shells | grep -v nologin) # --- CIS Docker Benchmark 1.6.0, section 4 ----------------------------------- extra_packages() { dpkg-query -W -f '${Package}\n' | while read -r p; do case " ${CIS_PACKAGES:-} " in *" $p "*) ;; *) echo "$p" ;; esac done } setid_files() { find / -xdev -type f -perm /6000; } unverified_sources() { grep -rEil 'trusted=yes|Trusted: *yes|AllowUnauthenticated|AllowInsecureRepositories|AllowDowngradeToInsecureRepositories' /etc/apt for f in /etc/apt/sources.list.d/*.sources; do [ -e "$f" ] || continue awk -v f="$f" 'BEGIN { RS = "" } index($0, "Signed-By:") == 0 { print f }' "$f" done grep -sE '^[[:space:]]*deb[[:space:]]' /etc/apt/sources.list /etc/apt/sources.list.d/*.list \ | grep -v 'signed-by=' } check docker:4.3 "Unnecessary packages are not installed (packages.allow)" extra_packages check docker:4.8 "setuid and setgid permissions are removed" setid_files check docker:4.11 "Only verified packages are installed" unverified_sources # --- CIS Debian Linux 12 v1.1.0 ---------------------------------------------- # in_range <key> <min> [max]: names the key unless min <= its value <= max. in_range() { v=$(login_defs "$1") case $v in '' | *[!0-9]*) echo "$1=${v:-unset}"; return ;; esac if [ "$v" -lt "$2" ] || [ "$v" -gt "${3:-$v}" ]; then echo "$1=$v"; fi } pass_max_days() { in_range PASS_MAX_DAYS 1 365; } pass_min_days() { in_range PASS_MIN_DAYS 1; } pass_warn_age() { in_range PASS_WARN_AGE 7; } weak_hashing() { v=$(login_defs ENCRYPT_METHOD) case $v in SHA512 | YESCRYPT) ;; *) echo "ENCRYPT_METHOD=${v:-unset}" ;; esac } weak_umask() { v=$(login_defs UMASK) case $v in '' | *[!0-7]*) echo "UMASK=${v:-unset}"; return ;; esac # 027 or stricter: group may not write, others get nothing if [ $(((0$v >> 3) & 2)) -eq 0 ] || [ $((0$v & 7)) -ne 7 ]; then echo "UMASK=$v"; fi } other_uid0() { awk -F: '$3 == "0" && $1 != "root" { print $1 }' /etc/passwd; } other_gid0_accounts() { awk -F: '$4 == "0" && $1 !~ /^(root|sync|shutdown|halt|operator)$/ { print $1 }' /etc/passwd } other_gid0_groups() { awk -F: '$3 == "0" && $1 != "root" { print $1 }' /etc/group; } root_unlocked() { awk -F: '$1 == "root" && $2 !~ /^[!*]/ { print "root" }' /etc/shadow; } system_login_shells() { min=$(login_defs UID_MIN) echo "$SHELLS" | awk -F: -v min="${min:-1000}" ' NR == FNR { if ($0 != "") shell[$0] = 1; next } $1 != "root" && ($3 + 0 < min + 0 || $3 == "65534") && ($7 in shell) { print $1 } ' - /etc/passwd } unlocked_without_shell() { echo "$SHELLS" | awk -F: ' FILENAME == "-" { if ($0 != "") shell[$0] = 1; next } FILENAME == "/etc/passwd" { if ($1 != "root" && !($7 in shell)) nologin[$1] = 1; next } ($1 in nologin) && $2 !~ /^[!*]/ { print $1 } ' - /etc/passwd /etc/shadow } # too_open <file> <max mode> <owner> <group regex>: names the file when it is # more open than the mode allows or owned by someone else; a missing file is fine. too_open() { [ -e "$1" ] || return 0 # shellcheck disable=SC2046 set -- "$1" "$2" "$3" "$4" $(stat -Lc '%a %U %G' "$1") if [ $((0$5 & ~0$2 & 07777)) -ne 0 ] || [ "$6" != "$3" ] || ! echo "$7" | grep -qxE "$4"; then echo "$1=$5:$6:$7" fi } perm_passwd() { too_open /etc/passwd 644 root root; } perm_passwd_() { too_open /etc/passwd- 644 root root; } perm_group() { too_open /etc/group 644 root root; } perm_group_() { too_open /etc/group- 644 root root; } perm_shadow() { too_open /etc/shadow 640 root 'root|shadow'; } perm_shadow_() { too_open /etc/shadow- 640 root 'root|shadow'; } perm_gshadow() { too_open /etc/gshadow 640 root 'root|shadow'; } perm_gshadow_() { too_open /etc/gshadow- 640 root 'root|shadow'; } perm_shells() { too_open /etc/shells 644 root root; } perm_opasswd() { too_open /etc/security/opasswd 600 root root; } world_writable() { find / -xdev -type f -perm -0002 find / -xdev -type d -perm -0002 ! -perm -1000 } unowned() { find / -xdev \( -nouser -o -nogroup \); } not_shadowed() { awk -F: '$2 != "x" { print $1 }' /etc/passwd; } empty_passwords() { awk -F: '$2 == "" { print $1 }' /etc/shadow; } missing_groups() { awk -F: 'NR == FNR { g[$3] = 1; next } !($4 in g) { print $1 ":" $4 }' /etc/group /etc/passwd; } shadow_members() { awk -F: ' NR == FNR { if ($1 == "shadow") { gid = $3; if ($4 != "") print "shadow:" $4 } next } gid != "" && $4 == gid { print $1 } ' /etc/group /etc/passwd } dup_uids() { cut -d: -f3 /etc/passwd | sort | uniq -d; } dup_gids() { cut -d: -f3 /etc/group | sort | uniq -d; } dup_users() { cut -d: -f1 /etc/passwd | sort | uniq -d; } dup_groups() { cut -d: -f1 /etc/group | sort | uniq -d; } check debian:5.4.1.1 "Password expiration is configured" pass_max_days check debian:5.4.1.2 "Minimum password days are configured" pass_min_days check debian:5.4.1.3 "Password expiration warning days are configured" pass_warn_age check debian:5.4.1.4 "Strong password hashing algorithm is configured" weak_hashing check debian:5.4.2.1 "root is the only UID 0 account" other_uid0 check debian:5.4.2.2 "root is the only GID 0 account" other_gid0_accounts check debian:5.4.2.3 "group root is the only GID 0 group" other_gid0_groups check debian:5.4.2.4 "root account access is controlled (locked)" root_unlocked check debian:5.4.2.7 "System accounts do not have a valid login shell" system_login_shells check debian:5.4.2.8 "Accounts without a valid login shell are locked" unlocked_without_shell check debian:5.4.3.3 "Default user umask is configured" weak_umask check debian:7.1.1 "Permissions on /etc/passwd are configured" perm_passwd check debian:7.1.2 "Permissions on /etc/passwd- are configured" perm_passwd_ check debian:7.1.3 "Permissions on /etc/group are configured" perm_group check debian:7.1.4 "Permissions on /etc/group- are configured" perm_group_ check debian:7.1.5 "Permissions on /etc/shadow are configured" perm_shadow check debian:7.1.6 "Permissions on /etc/shadow- are configured" perm_shadow_ check debian:7.1.7 "Permissions on /etc/gshadow are configured" perm_gshadow check debian:7.1.8 "Permissions on /etc/gshadow- are configured" perm_gshadow_ check debian:7.1.9 "Permissions on /etc/shells are configured" perm_shells check debian:7.1.10 "Permissions on /etc/security/opasswd are configured" perm_opasswd check debian:7.1.11 "World writable files and directories are secured" world_writable check debian:7.1.12 "No files or directories without an owner and a group exist" unowned check debian:7.2.1 "Accounts in /etc/passwd use shadowed passwords" not_shadowed check debian:7.2.2 "/etc/shadow password fields are not empty" empty_passwords check debian:7.2.3 "All groups in /etc/passwd exist in /etc/group" missing_groups check debian:7.2.4 "The shadow group is empty" shadow_members check debian:7.2.5 "No duplicate UIDs exist" dup_uids check debian:7.2.6 "No duplicate GIDs exist" dup_gids check debian:7.2.7 "No duplicate user names exist" dup_users check debian:7.2.8 "No duplicate group names exist" dup_groups