feat: judge, sign and verify netOrk's images in one place (NetOrk/netork#1390)
CI / Tests, lint (push) Successful in 18s

The CIS gate, the SBOM, the cosign signature and the matcher's verdict lived
in NetOrk/base-os, the head of the chain. Every link below it -- base-python,
engine, ui, satellite, the service images -- needs the same, so the tools
move here, and every image repository pins them by commit.

- image_tools: cis.py, cis_os.sh, sbom.py and verify.py from base-os. The
  image's own repository holds its cis.toml and packages.allow; both default
  to the working directory.
- bin/sign.sh signs whatever platforms a reference holds: base-os's index of
  amd64 and arm64 (EXPECT_PLATFORMS) or a dev image's single manifest
  (image_tools/platforms.py).
- bin/verify-base.sh checks every FROM image of a Dockerfile against
  cosign.pub before a build; one without a digest is refused
  (image_tools/dockerfile.py).
- bin/install-cosign.sh installs cosign v3.1.3 for amd64 and arm64 runners,
  checked against the release checksums.
- actions/verify-base, judge, sign wrap them for the workflows.
- cosign.pub is the public half of the COSIGN_PRIVATE_KEY_B64 org secret,
  the key rotated on 2026-10-22.
This commit is contained in:
Christian Manivong committed 2026-10-22 16:23:00 +02:00
commit d139d66c3d
27 files changed
+2085

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
#!/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:
#
# <id> TAB PASS|FAIL TAB <title> 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