From 077d41a6b193787dc31718370c1532d6638569bc Mon Sep 17 00:00:00 2001 From: Ashlen Date: Wed, 15 Jul 2026 12:54:05 -0600 Subject: [PATCH 01/61] fix(remove-system.map): continue past undeletable files The deletion loop called `exit 0` on the first System.map that `[ -w ]` rejected, so copies in later locations were never shredded. Where `[ -w ]` passed but shred failed anyway, shred ran as a bare command and tripped the ERR trap that pre.bsh installs, aborting with a "## Please report this bug!" banner and exit 1. Branch on shred's exit status instead of `[ -w ]`, which does not predict removability. Unlinking depends on write permission on the parent directory rather than on the file itself, and an append-only file passes the test yet cannot be unlinked at all. For a full-capability root process the check is also true on a mode 444 file, so it only ever failed on read-only file systems and on files carrying the immutable attribute. Clear the immutable and append-only attributes before each attempt. Per review, a System.map should never be immutable and `chattr +i` would possibly complicate kernel package removal; append-only is cleared on the same grounds, since it blocks removal without affecting `[ -w ]`. Report the files that remain once at the end, under a single wiki link rather than repeating it on every line, and withhold "Done. Success." when anything failed. A file reachable under two names, such as /lib/modules//build and the source symlink beside it, is listed under each, since both matched and both failed. Iterate an array rather than word-splitting a string with IFS, which a /etc/remove-system.map_pre.d drop-in can set; under `IFS=:` the globs matched nothing, so the script deleted nothing and still exited 0. The removal loop no longer exits early on failure and the script ends in an explicit `exit 0`; a non-zero status would fail the kernel package's postinst hook and the oneshot unit on every boot. --- .../remove-system.map#security-misc-shared | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/usr/libexec/security-misc/remove-system.map#security-misc-shared b/usr/libexec/security-misc/remove-system.map#security-misc-shared index 5b75f6de..573907bf 100755 --- a/usr/libexec/security-misc/remove-system.map#security-misc-shared +++ b/usr/libexec/security-misc/remove-system.map#security-misc-shared @@ -12,31 +12,44 @@ fi shopt -s nullglob -system_map_location="/boot/System.map* /usr/src/*/System.map* /lib/modules/*/*/System.map* /System.map*" +system_map_list=( /boot/System.map* /usr/src/*/System.map* /lib/modules/*/*/System.map* /System.map* ) -counter=0 -for filename in ${system_map_location} ; do - counter=$(( counter + 1 )) -done - -if [ "$counter" -ge "1" ]; then +if (( ${#system_map_list[@]} > 0 )); then echo "INFO: Deleting system.map files..." fi +failed_list=() + ## Removes the System.map files as they are only used for debugging or malware. -for filename in ${system_map_location} ; do - if [ -f "${filename}" ]; then - if [ -w "${filename}" ]; then - ## 'shred' with '--verbose' is too chatty. (7 lines) - shred --force --zero -u "${filename}" - echo "INFO: removed '${filename}'" - else - echo "NOTE: Cannot delete '${filename}' - read-only. For details, see: https://www.kicksecure.com/wiki/security-misc#system_map" - exit 0 - fi +for filename in "${system_map_list[@]}"; do + if [[ ! -f "${filename}" ]]; then + continue fi + ## The immutable and append-only file attributes ('chattr +i', 'chattr +a') + ## prevent deletion. Clear them. A System.map file has no legitimate + ## reason to carry either and they would possibly complicate kernel + ## package removal. + chattr -ia -- "${filename}" &>/dev/null || true + + ## 'shred' with '--verbose' is too chatty. (7 lines) + ## 'shred' stderr is suppressed because failure is expected on read-only + ## file systems. Failures are reported below instead. + if shred --force --zero -u -- "${filename}" 2>/dev/null; then + echo "INFO: removed '${filename}'" + continue + fi + failed_list+=( "${filename}" ) done -if [ "$counter" -ge "1" ]; then +if (( ${#failed_list[@]} > 0 )); then + for filename in "${failed_list[@]}"; do + echo "NOTE: Could not remove '${filename}'." + done + echo "NOTE: For details, see: https://www.kicksecure.com/wiki/security-misc#system_map" +elif (( ${#system_map_list[@]} > 0 )); then echo "INFO: Done. Success." fi + +## Deliberately exits 0 even on partial failure. A non-zero exit code would fail +## the kernel package's postinst hook and the oneshot unit on every boot. +exit 0 From 867809170a265b269a34f6d704c97ec53744cd4d Mon Sep 17 00:00:00 2001 From: Ashlen Date: Wed, 15 Jul 2026 19:06:42 -0600 Subject: [PATCH 02/61] fix(pam): pass profile names to pam-auth-update --remove 'pam-auth-update --remove' matches pam-config filenames under /usr/share/pam-configs/, but prerm and postrm passed $DPKG_MAINTSCRIPT_PACKAGE -- the package name security-misc-shared -- which matches no profile. An unknown target is stored without validation and simply never matches, so the --remove was inert in both calls: the prerm call was an effective no-op, while the postrm call still performed a plain reconciliation. The prerm early-disable was therefore dead. On removal /etc/pam.d/common-* kept referencing helper scripts under /usr/libexec/security-misc/ that dpkg then deleted; block-unsafe-logins is a Priority 1100 'requisite' entry, so a dangling reference fails all authentication until postrm's reconciliation regenerates common-* -- permanently, if postrm fails before that point. prerm now passes the eight profile filenames explicitly. postrm drops the ineffective '--remove ' for a plain reconciliation and guards it with 'command -v': Debian Policy 7.2 gives no guarantee that dependencies are available when postrm runs and requires postrm to gracefully skip such actions, and an absent pam-auth-update would abort the script under 'set -e'. prerm stays unguarded: Policy names Depends (here libpam-runtime) as the mechanism prerm scripts rely on, dependencies there normally being at least unpacked. One consequence of a '--remove' that now takes effect: it also drops the eight names from /var/lib/pam/seen. If a removal is aborted, postinst's 'pam-auth-update --package' re-enables the seven 'Default: yes' profiles among them, including any of them an administrator had deliberately disabled (other packages' profiles remain in seen and are unaffected), while console-lockdown-security-misc ('Default: no') stays off until an administrator re-enables it -- reinstalling does not restore it, since preinst's enable is behind a do_once stamp. Both are new relative to the previous inert behavior. --- debian/security-misc-shared.postrm | 6 +++++- debian/security-misc-shared.prerm | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/debian/security-misc-shared.postrm b/debian/security-misc-shared.postrm index 13dc5884..3312a215 100755 --- a/debian/security-misc-shared.postrm +++ b/debian/security-misc-shared.postrm @@ -16,7 +16,11 @@ true " " ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/11 -pam-auth-update --package --remove "$DPKG_MAINTSCRIPT_PACKAGE" +## Debian Policy 7.2: there is no guarantee that dependencies (here +## libpam-runtime) are available when postrm runs; skip gracefully then. +if command -v pam-auth-update >/dev/null 2>&1; then + pam-auth-update --package +fi rm -f /etc/sysctl.d/30_security-misc_aslr-mmap.conf diff --git a/debian/security-misc-shared.prerm b/debian/security-misc-shared.prerm index 1c4cd871..d2b6b270 100755 --- a/debian/security-misc-shared.prerm +++ b/debian/security-misc-shared.prerm @@ -16,7 +16,20 @@ true " " if [ "$1" = remove ]; then - pam-auth-update --package --remove "$DPKG_MAINTSCRIPT_PACKAGE" + ## 'pam-auth-update --remove' matches pam-config FILENAMES under + ## /usr/share/pam-configs/, not the package name. Disable while the helper + ## scripts under /usr/libexec/security-misc/ still exist. Keep in sync with + ## security-misc-shared.install and usr/share/pam-configs/. + ## See pam-auth-update(8). + pam-auth-update --package --remove \ + block-unsafe-logins-security-misc \ + console-lockdown-security-misc \ + faillock-preauth-security-misc \ + mkhomedir-security-misc \ + pam-abort-on-locked-password-security-misc \ + umask-security-misc \ + unix-faillock-security-misc \ + wheel-security-misc fi true "INFO: debhelper beginning here." From a8f26061e0e199b95077aa20696257bddc3db73d Mon Sep 17 00:00:00 2001 From: "claude (dev182 e09ec5cf-4ebf-4e1b-88e5-55fbac6ef1d3)" Date: Mon, 10 Aug 2026 13:56:04 -0400 Subject: [PATCH 03/61] Drain input in USB-controller and config-file lookups check-for-usb-controller: grep --quiet exits on the first match and closes the pipe, sending SIGPIPE to lspci. Under pipefail the pipeline then reports 141, so the guarding if takes the false branch even when a USB controller is present. Let grep read the whole stream and redirect to /dev/null instead. mmap-rnd-bits: head -n 1 has the same early-close hazard against ls; use sed -n '1p', which consumes all input, for consistency. Drop the redundant duplicate set -euo pipefail line already covered by the long-form preamble. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../check-for-usb-controller#security-misc-shared | 2 +- usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared index f720c018..aa70e39a 100755 --- a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared +++ b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared @@ -22,7 +22,7 @@ export LC_ALL='C' ## Package 'pciutils' provides tool 'lspci'. has lspci -if lspci | grep --quiet '^[^ ]* USB controller: '; then +if lspci | grep -- '^[^ ]* USB controller: ' >/dev/null; then exit 0 fi diff --git a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared index 8420389f..ae6abf06 100755 --- a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared +++ b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared @@ -14,8 +14,6 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose - -set -euo pipefail shopt -s failglob more_info_link="https://forums.whonix.org/t/automate-mmap-randomisation-to-fix-ppc64el/16514" @@ -48,7 +46,7 @@ COMPAT_BITS_MAX_DEFAULT=16 ## so the ls-parsing hazard SC2012 warns about is not reachable here, and a ## find-based newest-file rewrite would be a behaviour risk for no gain. # shellcheck disable=SC2012 -if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | head -n 1) ; then +if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | sed -n '1p') ; then ## Find the relevant config options. if ! BITS_MAX=$(grep -E '^CONFIG_ARCH_MMAP_RND_BITS_MAX=[0-9]+$' "${CONFIG}" | cut -d "=" -f 2) ; then printf '%s\n' "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_BITS_MAX! Using built-in default." >&2 From 942ba8a1902c2c8a0d6cd1921f2b540eb413a217 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:39:33 -0400 Subject: [PATCH 04/61] security-misc: permission-hardener parse filenames with spaces - load_state() split config lines on every space, so any entry whose filename contains a space was mis-fielded: silently skipped (a SUID binary left un-hardened) or mapped to a corrupted policy entry. - Recover the filename by parsing options from the right -- a trailing whitelist keyword, or the mode/owner/group[/capability] tail anchored by the octal mode. Space-free lines unchanged. - Add regression test (drives the real script via print-policy) plus its CI workflow. Canary-verified: fails on the pre-fix parser, passes now. - Bring the file to strict-mode / shellcheck compliance as required to touch it (6-directive block, printf, braces). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../local-permission-hardener-test.yml | 59 +++++++++ .../test_whitespace_filename.sh | 69 ++++++++++ .../permission-hardener#security-misc-shared | 119 ++++++++++++------ 3 files changed, 206 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/local-permission-hardener-test.yml create mode 100755 ci/tests/permission_hardener/test_whitespace_filename.sh diff --git a/.github/workflows/local-permission-hardener-test.yml b/.github/workflows/local-permission-hardener-test.yml new file mode 100644 index 00000000..bbc05186 --- /dev/null +++ b/.github/workflows/local-permission-hardener-test.yml @@ -0,0 +1,59 @@ +--- +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Regression test for the permission-hardener config parser. +## +## Scope is intentionally narrow: it exercises load_state()'s +## right-anchored option parsing so a config filename containing a +## space is parsed as one entry instead of being split and silently +## dropped. See ci/tests/permission_hardener/test_whitespace_filename.sh. + +name: Test permission-hardener + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test permission-hardener + runs-on: ubuntu-latest + timeout-minutes: 10 + + ## CI runs only where we enabled it; ANDed with the existing guard. + ## Unset variable -> skipped, no runner, run stays green. + if: >- + vars.CI_ENABLED_ORG_AI_ASSISTED == 'true' + && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + ## permission-hardener sources helper-scripts (log_run_die.sh, + ## safe_echo.sh, strings.bsh) at startup; install-deps provides + ## them under /usr/libexec/helper-scripts/. + - name: Install genmkfile + helper-scripts + safe-rm + uses: org-ai-assisted/developer-meta-files/.github/actions/install-deps@master + with: + apt-packages: 'safe-rm' + + - name: Whitespace-in-filename parser regression test + ## Root: the test writes a temp config under + ## /etc/permission-hardener.d/ and runs 'print-policy'. + run: sudo -E ci/tests/permission_hardener/test_whitespace_filename.sh diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh new file mode 100755 index 00000000..9d45b54e --- /dev/null +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -0,0 +1,69 @@ +#!/bin/bash +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Regression test: permission-hardener must parse a config filename that +## contains spaces. +## +## load_state() recovers the filename by reading the option fields from the +## right (a trailing whitelist keyword, or the mode/owner/group[/capability] +## tail anchored by the octal mode). A space in the filename must NOT split it +## into the wrong fields -- that drops the entry silently, leaving a SUID +## binary un-hardened. +## +## Drives the REAL script via 'print-policy' with a mode-form entry +## ( ) whose filename contains a space, and +## asserts the recovered filename appears in the printed policy. +## +## Requires root: writes a temporary config under /etc/permission-hardener.d/ +## and needs helper-scripts installed (sourced by permission-hardener). + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)" +ph_bin="${PERMISSION_HARDENER_BIN:-${repo_root}/usr/bin/permission-hardener#security-misc-shared}" + +if [ ! -f "${ph_bin}" ]; then + printf '%s\n' "FAIL: permission-hardener not found at '${ph_bin}'." >&2 + exit 1 +fi + +test_dir="$(mktemp -d -t ph-ws-test.XXXXXX)" +## The space in the directory name is the property under test. +spaced_file="${test_dir}/some space/binary" +mkdir -p -- "${test_dir}/some space" +touch -- "${spaced_file}" + +config_dir="/etc/permission-hardener.d" +config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" +mkdir -p -- "${config_dir}" + +## invoked indirectly via 'trap ... EXIT' +# shellcheck disable=SC2317 +cleanup() { + safe-rm -f -- "${config_file}" + safe-rm -rf -- "${test_dir}" +} +trap cleanup EXIT + +## mode-form entry: +printf '%s\n' "${spaced_file} 0744 root root" > "${config_file}" + +policy_output="$( "${ph_bin}" print-policy )" + +if printf '%s\n' "${policy_output}" | grep -qF -- "${spaced_file}"; then + printf '%s\n' "PASS: space-containing filename parsed and present in policy." + exit 0 +fi + +printf '%s\n' "FAIL: space-containing filename '${spaced_file}' missing from print-policy output." >&2 +printf '%s\n' "----- print-policy output -----" >&2 +printf '%s\n' "${policy_output}" >&2 +exit 1 diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 9f9f351c..775cea57 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -14,7 +14,12 @@ ## works very well for literal matching, and it is used that way extensively ## throughout this script. -set -o errexit -o nounset -o pipefail +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose ## Constants # shellcheck disable=SC2034 @@ -115,7 +120,7 @@ output_stat() { return 1 fi - if [ -z "$stat_output" ]; then + if [ -z "${stat_output}" ]; then log error "stat_output is empty. File name: '${file_name}' Stat output: '${stat_output}' @@ -166,7 +171,7 @@ hardlink_count: '${hardlink_count}' return 1 fi - if [ "$file_name" != "$file_name_from_stat" ]; then + if [ "${file_name}" != "${file_name_from_stat}" ]; then log error "\ File name is different from file name received from stat: File name: '${file_name}' @@ -279,7 +284,7 @@ check_nosuid_whitelist() { ## literal matching is intentional here too [[ " ${policy_exact_white_list[*]} " =~ " ${target_file} " ]] && return 1 - for match_white_list_entry in ${policy_match_white_list[@]+"${policy_match_white_list[@]}"}; do + for match_white_list_entry in "${policy_match_white_list[@]}"; do if [[ "${target_file}" == *"${match_white_list_entry}"* ]]; then return 1 fi @@ -425,6 +430,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item + local field_count recovered_file_name mode_index field_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -466,8 +472,38 @@ load_state() { IFS=' ' read -r -a field_list <<< "${line}" + ## A filename may contain spaces. The option grammar is unambiguous at the + ## END of a line -- either a single trailing whitelist keyword, or a + ## mode/owner/group[/capability] tail whose first field is the octal mode + ## -- so recover the filename by parsing options from the right and + ## rejoining everything before them. Space-free lines are left unchanged. + field_count="${#field_list[@]}" + if (( field_count > 2 )); then + case "${field_list[field_count-1]}" in + exactwhitelist|matchwhitelist|disablewhitelist|nosuid) + recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:field_count-1}")" + field_list=( "${recovered_file_name}" "${field_list[field_count-1]}" ) + ;; + *) + mode_index='' + for (( field_index=1; field_index < field_count; field_index++ )); do + if [[ "${field_list[field_index]}" =~ ^0?[0-7]{3,4}$ ]]; then + mode_index="${field_index}" + break + fi + done + if [ -n "${mode_index}" ] && (( mode_index > 1 )); then + recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:mode_index}")" + field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) + fi + ;; + esac + fi + case "${#field_list[@]}" in - 2|4|5) true;; + 2|4|5) + true + ;; *) exit_code=200 log error "Line contains an invalid number of fields: '${line}'" >&2 @@ -738,11 +774,11 @@ undo_policy_for_file() { state_group_owner_item="${state_group_owner_list[state_idx]}" state_mode_item="${state_mode_list[state_idx]}" # shellcheck disable=SC2086 - chown ${verbose} -- "${state_user_owner_item}:${state_group_owner_item}" \ + chown "${verbose}" -- "${state_user_owner_item}:${state_group_owner_item}" \ "${undo_file}" || exit_code=202 ## chmod needs to be run after chown since chown removes suid. # shellcheck disable=SC2086 - chmod ${verbose} "${state_mode_item}" "${undo_file}" || exit_code=203 + chmod "${verbose}" "${state_mode_item}" "${undo_file}" || exit_code=203 else log info "File does not exist: '${undo_file}'" fi @@ -788,6 +824,7 @@ print_columns() { done format_str="${format_str}\n" ## Using a dynamically generated format string on purpose. + ## style-ok: printf-format # shellcheck disable=SC2059 printf "${format_str}" "$@" } @@ -832,9 +869,9 @@ print_raw_policy_config() { if [ ! -f "${config_file}" ]; then continue fi - echo "*** begin ${config_file} ***" + printf '%s\n' "*** begin ${config_file} ***" cat "${config_file}" - echo "*** end ${config_file} ***" + printf '%s\n' "*** end ${config_file} ***" done } @@ -842,13 +879,13 @@ print_raw_state() { local state_file for state_file in "${store_dir}/existing_mode/statoverride" \ "${store_dir}/new_mode/statoverride"; do - echo "*** begin ${state_file} ***" + printf '%s\n' "*** begin ${state_file} ***" if [ -f "${state_file}" ]; then cat "${state_file}" else - echo '(file does not exist)' + printf '%s\n' '(file does not exist)' fi - echo "*** end ${state_file} ***" + printf '%s\n' "*** end ${state_file} ***" done } @@ -856,13 +893,13 @@ print_fs_audit() { local state_idx state_file_item state_user_owner_item state_group_owner_item \ state_mode_item - echo 'Legend:' - echo '... - Warning about an unusual, but not necessarily wrong, condition' - echo '!!! - Warning about an unusual and definitely wrong condition' - echo '*** - File permission data, actual state on filesystem is consistent with policy' - echo '^^^ - File permission data, actual state on filesystem is inconsistent with policy' - echo 'vvv - File permissions specified by state, always shown after a ^^^ item' - echo + printf '%s\n' 'Legend:' + printf '%s\n' '... - Warning about an unusual, but not necessarily wrong, condition' + printf '%s\n' '!!! - Warning about an unusual and definitely wrong condition' + printf '%s\n' '*** - File permission data, actual state on filesystem is consistent with policy' + printf '%s\n' '^^^ - File permission data, actual state on filesystem is inconsistent with policy' + printf '%s\n' 'vvv - File permissions specified by state, always shown after a ^^^ item' + printf '%s\n' '' for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do state_file_item="${state_file_list[state_idx]}" @@ -886,7 +923,7 @@ print_fs_audit() { output_stat "${state_file_item}" if [ -z "${file_name_from_stat}" ]; then - echo "... '${state_file_item}' does not exist or has multiple hardlinks" + printf '%s\n' "... '${state_file_item}' does not exist or has multiple hardlinks" continue fi @@ -895,20 +932,20 @@ print_fs_audit() { || [ "${existing_mode}" != "${state_mode_item}" ]; then if ! [[ $'\n'"${passwd_file_contents}" \ =~ $'\n'"${state_user_owner_item}:" ]]; then - echo "!!! Owner from config does not exist: '${state_user_owner_item}'" + printf '%s\n' "!!! Owner from config does not exist: '${state_user_owner_item}'" continue fi if ! [[ $'\n'"${group_file_contents}" \ =~ $'\n'"${state_group_owner_item}:" ]]; then - echo "!!! Group from config does not exist: '${state_group_owner_item}'" + printf '%s\n' "!!! Group from config does not exist: '${state_group_owner_item}'" continue fi - echo "^^^ ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" - echo "vvv ${file_name_from_stat} ${state_user_owner_item}:${state_group_owner_item} ${state_mode_item}" + printf '%s\n' "^^^ ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" + printf '%s\n' "vvv ${file_name_from_stat} ${state_user_owner_item}:${state_group_owner_item} ${state_mode_item}" else - echo "*** ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" + printf '%s\n' "*** ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" fi done } @@ -988,49 +1025,49 @@ case "${1:-}" in print_state ;; print-diagnostics) - echo '=== BEGIN PERMISSION-HARDENER DIAGNOSTICS ===' + printf '%s\n' '=== BEGIN PERMISSION-HARDENER DIAGNOSTICS ===' - echo '--- BEGIN State without policy ---' + printf '%s\n' '--- BEGIN State without policy ---' load_state_without_policy print_state - echo '--- END State without policy ---' + printf '%s\n' '--- END State without policy ---' reset_global_vars - echo '--- BEGIN Policy without state ---' + printf '%s\n' '--- BEGIN Policy without state ---' load_state print_policy - echo '--- END Policy without state ---' + printf '%s\n' '--- END Policy without state ---' reset_global_vars - echo '--- BEGIN Policy-applied-state ---' + printf '%s\n' '--- BEGIN Policy-applied-state ---' load_state apply_policy print_state - echo '--- END Policy-applied state ---' + printf '%s\n' '--- END Policy-applied state ---' reset_global_vars - echo '--- BEGIN Master dpkg-statoverride database ---' + printf '%s\n' '--- BEGIN Master dpkg-statoverride database ---' dpkg-statoverride --list - echo '--- END Master dpkg-statoverride database ---' + printf '%s\n' '--- END Master dpkg-statoverride database ---' - echo '--- BEGIN Raw policy configuration ---' + printf '%s\n' '--- BEGIN Raw policy configuration ---' print_raw_policy_config - echo '--- END Raw policy configuration ---' + printf '%s\n' '--- END Raw policy configuration ---' - echo '--- BEGIN Raw state data ---' + printf '%s\n' '--- BEGIN Raw state data ---' print_raw_state - echo '--- END Raw state data ---' + printf '%s\n' '--- END Raw state data ---' - echo '--- BEGIN Filesystem state audit ---' + printf '%s\n' '--- BEGIN Filesystem state audit ---' load_state apply_policy print_fs_audit - echo '--- END Filesystem state audit ---' + printf '%s\n' '--- END Filesystem state audit ---' - echo '=== END PERMISSION-HARDENER DIAGNOSTICS ===' + printf '%s\n' '=== END PERMISSION-HARDENER DIAGNOSTICS ===' ;; -h|--help) print_usage From 8c991dd34247e7cc43843a1dff6d5300330646f1 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:39:52 -0400 Subject: [PATCH 05/61] security-misc: compile emerg-shutdown once at postinst, not every boot - emerg-shutdown.c was recompiled into /run on every boot. Move the compile to a standalone build-emerg-shutdown run from postinst, which installs /usr/bin/emerg-shutdown. Compiling on-target keeps the package Architecture: all while still producing a correct per-arch static binary. - Boot-time emerg-shutdown now just copies /usr/bin/emerg-shutdown into /run (memlockd-resident), with a fallback build if the binary is absent. - dracut module ships /usr/bin/emerg-shutdown (exists right after install, fixing the require_binaries /run chicken-and-egg). - Sandbox-verified: build-emerg-shutdown produces a static ELF in /usr/bin. - Strict/style compliance on the touched scripts (waivers for the debconf maintainer script and the dracut-sourced module). Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/security-misc-shared.install | 1 + debian/security-misc-shared.postinst | 29 ++++++++---- .../module-setup.sh#security-misc-shared | 8 ++-- .../build-emerg-shutdown#security-misc-shared | 45 +++++++++++++++++++ ...ompile-emerg-shutdown#security-misc-shared | 10 +++-- .../emerg-shutdown#security-misc-shared | 30 ++++++++----- 6 files changed, 97 insertions(+), 26 deletions(-) create mode 100755 usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared diff --git a/debian/security-misc-shared.install b/debian/security-misc-shared.install index 8e72deee..f149b2e0 100755 --- a/debian/security-misc-shared.install +++ b/debian/security-misc-shared.install @@ -109,6 +109,7 @@ usr/lib/systemd/user/usbguard-notifier.service.d/30_security-misc.conf#security- usr/lib/udev/rules.d/95-emerg-shutdown.rules#security-misc-shared => /usr/lib/udev/rules.d/95-emerg-shutdown.rules usr/libexec/security-misc/askpass#security-misc-shared => /usr/libexec/security-misc/askpass usr/libexec/security-misc/block-unsafe-logins#security-misc-shared => /usr/libexec/security-misc/block-unsafe-logins +usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared => /usr/libexec/security-misc/build-emerg-shutdown usr/libexec/security-misc/build-fm-shim-backend#security-misc-shared => /usr/libexec/security-misc/build-fm-shim-backend usr/libexec/security-misc/check-for-usb-controller#security-misc-shared => /usr/libexec/security-misc/check-for-usb-controller usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared => /usr/libexec/security-misc/compile-emerg-shutdown diff --git a/debian/security-misc-shared.postinst b/debian/security-misc-shared.postinst index af81bf2a..1d304e2d 100755 --- a/debian/security-misc-shared.postinst +++ b/debian/security-misc-shared.postinst @@ -3,6 +3,10 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## style-ok: allow-echo (Debian maintainer-script convention) +## style-ok: no-has (has.sh not reliably available this early in maintainer scripts) +## style-ok: no-strict (debconf maintainer script; full strict block would leak into APT/debconf) + if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh fi @@ -11,11 +15,11 @@ fi ## the postinst itself does not use debconf commands. source /usr/share/debconf/confmodule -set -e +set -o errexit true " ##################################################################### -## INFO: BEGIN: $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $* +## INFO: BEGIN: ${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} $* ##################################################################### " @@ -116,7 +120,7 @@ root root 4755 /usr/bin/mount root root 644 /etc/issue root root 755 /etc/cron.d" - printf '%s\n' "$state_str" | tee /var/lib/permission-hardener-v2/existing_mode/statoverride + printf '%s\n' "${state_str}" | tee /var/lib/permission-hardener-v2/existing_mode/statoverride touch "/var/lib/security-misc/do_once/${FUNCNAME[0]}_version_1" } @@ -125,6 +129,10 @@ build_fm_shim_backend() { /usr/libexec/security-misc/build-fm-shim-backend } +build_emerg_shutdown() { + /usr/libexec/security-misc/build-emerg-shutdown +} + case "$1" in configure) if [ -d /etc/skel/.gnupg ]; then @@ -164,13 +172,18 @@ case "$1" in ## Build the org.freedesktop.FileManager1 shim backend. build_fm_shim_backend + + ## Compile emerg-shutdown into /usr/bin (on-target, so an + ## Architecture: all package still gets a correct per-arch binary); + ## the boot-time script then only copies it into /run. + build_emerg_shutdown ;; abort-upgrade|abort-remove|abort-deconfigure) ;; triggered) - echo "INFO: triggered $DPKG_MAINTSCRIPT_PACKAGE: '$DPKG_MAINTSCRIPT_PACKAGE' $DPKG_MAINTSCRIPT_PACKAGE DPKG_MAINTSCRIPT_NAME: '$DPKG_MAINTSCRIPT_NAME' $\*: '$*' 2: '$2'" + echo "INFO: triggered ${DPKG_MAINTSCRIPT_PACKAGE}: '${DPKG_MAINTSCRIPT_PACKAGE}' ${DPKG_MAINTSCRIPT_PACKAGE} DPKG_MAINTSCRIPT_NAME: '${DPKG_MAINTSCRIPT_NAME}' $\*: '$*' 2: '$2'" /usr/share/security-misc/lkrg/lkrg-virtualbox || true /usr/libexec/security-misc/mmap-rnd-bits || true permission_hardening @@ -178,7 +191,7 @@ case "$1" in ;; *) - echo "$DPKG_MAINTSCRIPT_NAME called with unknown argument \`$1'" >&2 + echo "${DPKG_MAINTSCRIPT_NAME} called with unknown argument \`$1'" >&2 exit 1 ;; esac @@ -194,8 +207,8 @@ permission_hardening ## https://bugs.debian.org/481542 if command -v update-grub >/dev/null 2>&1; then update-grub || \ - echo "$DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME ERROR: Running \ -'update-grub' failed with exit code $?. $DPKG_MAINTSCRIPT_PACKAGE is most \ + echo "${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} ERROR: Running \ +'update-grub' failed with exit code $?. ${DPKG_MAINTSCRIPT_PACKAGE} is most \ likely only the trigger, not the cause. Unless you know this is not an issue, \ you should fix running 'update-grub', otherwise your system might no longer \ boot." >&2 @@ -213,7 +226,7 @@ permission_hardening_legacy_config_folder true " ##################################################################### -## INFO: END : $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $* +## INFO: END : ${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} $* ##################################################################### " diff --git a/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared b/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared index 98d6be9a..a69c36f9 100755 --- a/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared +++ b/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared @@ -3,15 +3,17 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## style-ok: no-strict (dracut sources this module; top-level strict mode would leak into dracut) + ## called by dracut check() { - require_binaries /run/emerg-shutdown || return 1 + require_binaries /usr/bin/emerg-shutdown || return 1 return 255 } ## called by dracut depends() { - echo 'systemd bash' + printf '%s\n' 'systemd bash' return 0 } @@ -23,7 +25,7 @@ install() { inst_simple /usr/libexec/security-misc/emerg-shutdown inst_simple /usr/share/security-misc/emerg-shutdown-initramfs.service /usr/lib/systemd/system/emerg-shutdown-initramfs.service - inst_simple /run/emerg-shutdown /emerg-shutdown + inst_simple /usr/bin/emerg-shutdown /emerg-shutdown for config_file in /etc/security-misc/emerg-shutdown/*.conf; do if [ -f "${config_file}" ]; then diff --git a/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared new file mode 100755 index 00000000..4c9ca911 --- /dev/null +++ b/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared @@ -0,0 +1,45 @@ +#!/bin/bash + +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Compile emerg-shutdown.c into /usr/bin/emerg-shutdown at package postinst +## time. Compiling on the target (rather than shipping a prebuilt binary) keeps +## security-misc-shared Architecture: all while still producing a correct +## per-architecture, statically linked binary. The boot-time emerg-shutdown +## script then only copies this binary into /run instead of recompiling on +## every boot. + +## style-ok: no-safe-rm (only ever removes our own just-created mktemp temp) + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +export LC_ALL=C + +## Compile to a temporary file first, then atomically move into place. +## This prevents leaving a corrupted binary if compilation is interrupted. +tmp_output="$(mktemp -- /usr/bin/emerg-shutdown.XXXXXX)" + +cleanup() { + rm -f -- "${tmp_output}" +} +trap cleanup EXIT + +/usr/libexec/security-misc/compile-emerg-shutdown \ + /usr/src/security-misc/emerg-shutdown.c \ + "${tmp_output}" \ + || { + printf '%s\n' 'Could not compile emerg-shutdown executable!' + exit 1 + } + +chmod 0755 -- "${tmp_output}" +mv -f -- "${tmp_output}" /usr/bin/emerg-shutdown +trap - EXIT diff --git a/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared index 873fa1a8..cab64f7c 100755 --- a/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared @@ -17,9 +17,9 @@ ## compile-emerg-shutdown ## ## Used by: -## * usr/libexec/security-misc/emerg-shutdown (first-run on the -## user's machine: compiles C code into /run/emerg-shutdown then -## execs it) +## * usr/libexec/security-misc/build-emerg-shutdown (package postinst: +## compiles C code into /usr/bin/emerg-shutdown; the boot-time +## emerg-shutdown script then copies that into /run) ## * ci/codeql-build.sh (CodeQL static analysis prebuild) ## ## Holding the gcc command in one place keeps the runtime build path @@ -27,8 +27,10 @@ set -o errexit set -o nounset -set -o errtrace set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose export LC_ALL=C diff --git a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared index 0f3de2d2..fafffc7f 100755 --- a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared @@ -3,15 +3,19 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## TODO: Move most of this to a build script that will compile and place under -## /usr/bin rather than /run. Then turn this script into something that simply -## copies emerg-shutdown to /run and then runs it. This will reduce resource -## consumption for end users. +## emerg-shutdown.c is compiled once to /usr/bin/emerg-shutdown at package +## postinst (by build-emerg-shutdown), not shipped prebuilt -- compiling +## on-target keeps security-misc-shared Architecture: all while still producing +## a correct per-architecture, statically linked binary. This script copies +## that binary into /run (tmpfs, memlockd-resident) at boot and runs it, rather +## than recompiling on every boot. set -o errexit set -o nounset -set -o errtrace set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose ## Make sure globs sort in a predictable, reproducible fashion export LC_ALL=C @@ -45,15 +49,19 @@ else ## Find the devices that make up the root device readarray -t root_devices < <(/usr/libexec/helper-scripts/get-backing-devices-for-mountpoint '/') || true; - ## Build the actual emerg-shutdown executable + ## Copy the prebuilt binary (compiled at postinst by build-emerg-shutdown) + ## into /run (tmpfs) so it is memory-resident for the emergency; memlockd + ## then locks it into RAM. if [ ! -f '/run/emerg-shutdown' ]; then - /usr/libexec/security-misc/compile-emerg-shutdown \ - /usr/src/security-misc/emerg-shutdown.c \ - /run/emerg-shutdown \ - || { - printf "%s\n" 'Could not compile force-shutdown executable!' + if [ ! -f '/usr/bin/emerg-shutdown' ]; then + ## Fallback: build was skipped or removed. Compile now so the emergency + ## shutdown capability is never silently absent. + /usr/libexec/security-misc/build-emerg-shutdown || { + printf '%s\n' 'Could not build emerg-shutdown executable!' exit 1 } + fi + cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown fi ## memlockd daemonizes itself, so no need to background it. From 8ba6ca134e52411ce18f29b553dbfedb42b8ed3e Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:08 -0400 Subject: [PATCH 06/61] security-misc: silence expected pam_exec journal noise via quiet_log - The [success=1 default=ignore] pam_exec lines run helper scripts that exit 1 on their expected control-flow path, emitting an alarming 'pam_exec(...): ... failed: exit code 1' journal line. Plain 'quiet' suppresses only the user-facing message, not the log. - Add 'quiet_log' (linux-pam >= 1.5.2, present on bookworm+) to every skip-logic pam_exec line (console-lockdown, unix-faillock, faillock-preauth, wheel), leaving the control flow byte-identical. - Update the scripts' comments to point at the resolution. Note: not verified on a live login journal (needs a real PAM stack); the change is additive and semantics-preserving regardless. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pam_faillock_not_if_x#security-misc-shared | 7 +++++-- .../security-misc/pam_only_if_login#security-misc-shared | 9 ++++++--- .../console-lockdown-security-misc#security-misc-shared | 2 +- .../faillock-preauth-security-misc#security-misc-shared | 2 +- .../unix-faillock-security-misc#security-misc-shared | 8 ++++---- .../pam-configs/wheel-security-misc#security-misc-shared | 2 +- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared index f5fe0922..22523eed 100755 --- a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared +++ b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared @@ -44,7 +44,10 @@ done ## exit failure so [success=1 default=ignore] will result in running the ## next PAM module (the pam_faillock module). ## -## Causes confusing error message: -## pam_exec(sudo:auth): /usr/libexec/security-misc/pam_faillock_not_if_x failed: exit code 1 +## This previously emitted a confusing journal line: +## pam_exec(sudo:auth): ... pam_faillock_not_if_x failed: exit code 1 +## now suppressed by 'quiet_log' on the pam_exec.so lines in +## /usr/share/pam-configs/{unix-faillock,faillock-preauth}-security-misc +## (linux-pam >= 1.5.2; plain 'quiet' only hides the user-facing message). ## https://github.com/linux-pam/linux-pam/issues/329 exit 1 diff --git a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared index 89497633..e3b1877c 100755 --- a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared +++ b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared @@ -16,9 +16,12 @@ shopt -s shift_verbose true "PAM_SERVICE: ${PAM_SERVICE:-}" if [ "${PAM_SERVICE:-}" = "login" ]; then - ## FIXME: - ## Creates unwanted journal log entry. - ## pam_exec(login:account): /usr/libexec/security-misc/pam_only_if_login failed: exit code 1 + ## exit 1 drives the [success=1 default=ignore] control flow (fall through + ## to the next PAM module). It previously emitted a journal line + ## pam_exec(login:account): ... pam_only_if_login failed: exit code 1 + ## now suppressed by 'quiet_log' on the pam_exec.so line in + ## /usr/share/pam-configs/console-lockdown-security-misc (linux-pam >= 1.5.2; + ## plain 'quiet' only hides the user-facing message, not the log). exit 1 else ## exit success so [success=1 default=ignore] will result in skipping the diff --git a/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared b/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared index 45e5c41b..d760d5fe 100644 --- a/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared +++ b/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared @@ -3,5 +3,5 @@ Default: no Priority: 280 Account-Type: Primary Account: - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_only_if_login + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_only_if_login required pam_access.so accessfile=/etc/security/access-security-misc.conf debug diff --git a/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared b/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared index 9d74cb81..41c61f9a 100644 --- a/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared +++ b/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared @@ -4,5 +4,5 @@ Priority: 1024 Auth-Type: Primary Auth: optional pam_exec.so debug stdout seteuid /usr/libexec/security-misc/pam-info - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x required pam_faillock.so preauth diff --git a/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared b/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared index b1328b56..0950f2ad 100644 --- a/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared +++ b/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared @@ -4,17 +4,17 @@ Priority: 384 Auth-Type: Primary Auth: [success=3 default=ignore] pam_unix.so nullok try_first_pass - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x [default=die] pam_faillock.so authfail requisite pam_deny.so - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x optional pam_faillock.so authsucc required pam_permit.so Auth-Initial: [success=3 default=ignore] pam_unix.so nullok - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x [default=die] pam_faillock.so authfail requisite pam_deny.so - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x optional pam_faillock.so authsucc required pam_permit.so diff --git a/usr/share/pam-configs/wheel-security-misc#security-misc-shared b/usr/share/pam-configs/wheel-security-misc#security-misc-shared index 599d5bc4..12d72100 100644 --- a/usr/share/pam-configs/wheel-security-misc#security-misc-shared +++ b/usr/share/pam-configs/wheel-security-misc#security-misc-shared @@ -3,5 +3,5 @@ Default: yes Priority: 1050 Auth-Type: Primary Auth: - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_only_if_su + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_only_if_su requisite pam_wheel.so group=sudo debug From 8aef705e63595beaa90f70e9c8a4f297fd7a08ee Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:20 -0400 Subject: [PATCH 07/61] security-misc: fm-shim frontend calls xdg-mime/gio by absolute path - fm-shim-frontend invoked xdg-mime and gio by bare name, depending on the systemd --user manager's PATH (minimal, profile-derived entries absent) that fm-shim-backend hands it. Use /usr/bin/xdg-mime and /usr/bin/gio, matching the existing /usr/bin/unicode-show call. - Reword the backend TODO: the whole-session PATH fix belongs in desktop-config-dist's start-lxqt-session (out of this repo); the frontend absolute-path change is the in-repo mitigation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fm_shim_frontend.py#security-misc-shared | 4 ++-- .../fm-shim-backend.c#security-misc-shared | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) mode change 100644 => 100755 usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared diff --git a/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared b/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared old mode 100644 new mode 100755 index 0af75f60..b16f8724 --- a/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared +++ b/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared @@ -393,7 +393,7 @@ class FmShimWindow(QDialog): try: default_fm_desktop_file: str = subprocess.run( - ["xdg-mime", "query", "default", "inode/directory"], + ["/usr/bin/xdg-mime", "query", "default", "inode/directory"], check=True, capture_output=True, encoding="utf-8", @@ -482,7 +482,7 @@ class FmShimWindow(QDialog): for target_dir in self.dir_list: if target_dir.is_dir(): subprocess.run( - ["gio", "launch", str(default_fm_path), str(target_dir)], + ["/usr/bin/gio", "launch", str(default_fm_path), str(target_dir)], check=False, ) else: diff --git a/usr/src/security-misc/fm-shim-backend.c#security-misc-shared b/usr/src/security-misc/fm-shim-backend.c#security-misc-shared index bf3fadeb..548db61d 100644 --- a/usr/src/security-misc/fm-shim-backend.c#security-misc-shared +++ b/usr/src/security-misc/fm-shim-backend.c#security-misc-shared @@ -85,11 +85,15 @@ void launch_frontend_process(const char *mode_opt, char **uri_list, * process, otherwise we won't know what display server to use and may run * into theming issues. * - * TODO: The environment in Non-Qubes-Whonix's user manager has some - * worrying discrepancies when compared to the environment of bash running - * in a qterminal window. In particular, 'PATH' is different. We may need to - * add a 'dbus-update-activation-environment --systemd --all' call to - * /usr/libexec/desktop-config-dist/start-lxqt-session. + * NOTE: Non-Qubes-Whonix's systemd --user manager exports a minimal PATH + * (the systemd default) rather than the profile-derived PATH an interactive + * qterminal bash has, because the graphical session start does not import + * the desktop environment into the user manager. fm-shim-frontend no longer + * depends on that PATH for launching (it invokes xdg-mime and gio by + * absolute path). The complete fix -- reconciling the whole session PATH -- + * belongs in desktop-config-dist's start-lxqt-session (e.g. a + * 'dbus-update-activation-environment --systemd --all' call after PATH is + * set) and is out of scope for this package. */ DBusMessage *env_request = NULL; From 9f12941585d5b18ae79f5d8dfd9f94cc0cd2507d Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:35 -0400 Subject: [PATCH 08/61] security-misc: document whitelisted SUID/SGID helpers, flag human-only - Replace the bare 'TODO: research' markers on the permission-hardener whitelist entries (postfix, selinux/utempter, spice, virtualbox, qubes/qfile-unpacker, pam-tmpdir-helper, gst-ptp-helper) with concise documentation: what each helper is, why it is SUID/SGID, what breaks if stripped, and relevant CVE/hardening notes. - Retag the keep-vs-strip decisions TODO-HUMAN-DEVELOPER-ONLY -- a security-trust call for a human maintainer, not AI-resolvable. - 30_default.conf: fix the commented gst-ptp-helper path typo (gstreamer1.0/grstreamer-1.0 -> gstreamer-1.0/gstreamer-1.0); note that filenames with spaces are now parsed. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...lt_whitelist_pam.conf#security-misc-shared | 6 +++++- ...hitelist_postfix.conf#security-misc-shared | 10 +++++++++- ..._whitelist_qubes.conf#security-misc-shared | 17 ++++++++++------ ...hitelist_selinux.conf#security-misc-shared | 11 +++++++++- ..._whitelist_spice.conf#security-misc-shared | 11 +++++++++- ...elist_virtualbox.conf#security-misc-shared | 16 ++++++++++++++- .../30_default.conf#security-misc-shared | 20 +++++++++++++++---- 7 files changed, 76 insertions(+), 15 deletions(-) diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared index ffb136e6..d770a269 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared @@ -19,5 +19,9 @@ ## ## Without this, LXQt fails to start with a dbus-launch error. ## -## TODO: audit pam-tmpdir-helper +## Confirmed: installed SUID root, mode 4755. Takes NO command-line/user input -- +## derives the directory purely from the caller's real UID/GID. No upstream git +## repo and no known CVE. A source audit is worthwhile given it is SUID root, but +## risk is low given zero attacker-controlled input. +## TODO-HUMAN-DEVELOPER-ONLY: source audit of pam-tmpdir-helper (SUID root). pam-tmpdir-helper matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared index 64dd72b5..1e45a13c 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared @@ -6,6 +6,14 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research and document +## postqueue(1) / postdrop(1): Postfix mail-submission helpers. Installed SGID +## group "postdrop" (Postfix setgid_group), NOT setuid root. The SGID bit lets +## an unprivileged user write into the group-writable maildrop queue and reach +## the Postfix daemon sockets, with no user-level privilege granted. Stripping +## SGID breaks local mail submission ("Permission denied" on maildrop; postqueue +## cannot reach the queue manager). SGID-group is already the minimal-privilege +## design; no notable current CVE tied to the bit. +## Ref: https://www.postfix.org/postconf.5.html (setgid_group) +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SGID is a human trust decision. postqueue matchwhitelist postdrop matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared index 1b65dfac..34656b92 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared @@ -6,18 +6,23 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research +## qfile-unpacker (qubes-core-agent-linux, qubes-rpc): receiver side of the +## inter-qube file copy (qvm-copy/qvm-move); reads the qrexec stream and unpacks +## incoming files into the target qube. SUID root because it must chroot() into +## the recipient dir and setfsuid()/drop to the unprivileged user before writing +## -- both need root, so SUID cannot simply be stripped. It parses attacker- +## influenced input from another qube => high-value local-EoP target. The issue +## below (#8633) is now CLOSED: upstream HARDENED the unpacker (path sanitization, +## qubes-linux-utils PR #87) rather than dropping SUID. Keep the package patched; +## residual risk is the parser itself. ## https://github.com/QubesOS/qubes-core-agent-linux/blob/master/qubes-rpc/qfile-unpacker.c -## -## Historic Qubes upstream security issue: -## qfile-unpacker allows unprivileged users in VMs to gain root privileges -## https://github.com/QubesOS/qubes-issues/issues/8633 +## https://github.com/QubesOS/qubes-issues/issues/8633 (historic local-root EoP) ## ## matches both: ## - /usr/lib/qubes/qfile-unpacker whitelist ## - Not bit-for-bit identical to /usr/lib/qubes/qfile-unpacker. ## - Stripping SUID from this does *not* break file copying. -## - TODO: further research required on its purpose +## - TODO-HUMAN-DEVELOPER-ONLY: further research required on its purpose ## - /usr/bin/qfile-unpacker ## - Appears to be an integral part of file transfer between qubes, stripping ## SUID from this in an AppVM results in that AppVM being unable to receive diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared index 6f5c7f30..986f7a72 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared @@ -6,5 +6,14 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research and document +## utempter (libutempter0): privileged back-end that terminal emulators (xterm, +## screen, tmux) fork to add/remove their pty login records in /var/run/utmp and +## /var/log/wtmp so who/w/last see the session. Installed SGID group "utmp" (NOT +## setuid root) -- the utmp group owns those files, so SGID utmp is the minimum +## privilege to update them. Stripping the bit breaks terminal login accounting +## (who/w miss sessions); no root privilege lost. Old CVEs only (CVE-2004-0233 +## path traversal, fixed in 0.5.3+). Whitelisted under "selinux" because SELinux +## policy references this path. +## Ref: https://bugzilla.redhat.com/show_bug.cgi?id=246063 +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SGID is a human trust decision. /utempter/utempter matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared index 6569621b..684237bc 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared @@ -6,5 +6,14 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research and document +## spice-client-glib-usb-acl-helper (spice-gtk): SUID root helper a SPICE client +## (virt-viewer, GNOME Boxes) calls during USB redirection to grant the invoking +## user a POSIX ACL on a /dev/bus/usb node so it can pass through to a VM. Needs +## root to set the ACL; consults polkit for an active local session first, so +## access is session-scoped rather than a permanent group grant. Stripping SUID +## breaks USB redirection ("Error setting facl: Operation not permitted"). It is +## a setuid-root C helper reachable by any desktop user -- a legitimate hardening +## candidate to disable where USB-to-VM redirection is unused. +## Ref: https://packages.debian.org/bookworm/spice-client-glib-usb-acl-helper +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SUID is a human trust decision. spice-client-glib-usb-acl-helper matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared index 725e3ad6..da26b99d 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared @@ -6,7 +6,21 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research +## VirtualBox ships several helpers under /usr/lib/virtualbox/. The NETWORKING +## helpers carry the SUID-root bit; they need root only to create/configure host +## virtual NICs and run the userspace net stacks, then drop privileges: +## - VBoxNetAdpCtl : creates/removes vboxnetN adapters, assigns IPs +## (CAP_NET_ADMIN operations). +## - VBoxNetDHCP / VBoxNetNAT : userspace DHCP server / NAT engine. +## The front-ends (VBoxHeadless, VBoxSDL, VirtualBoxVM) are generally NOT SUID in +## stock builds; whitelisted defensively for distro/"hardened" builds that +## setuid-root them to load the ring-0 VMM driver. Stripping SUID from the net +## helpers breaks host-only/NAT networking for non-root users. +## SUID history (why the surface is sensitive): CVE-2009-3692 (VBoxNetAdpCtl +## popen() metachar injection -> local root), CVE-2017-3316, recurring Oracle CPU +## local-EoP fixes. Verify on-image bits: find /usr/lib/virtualbox -perm -4000 +## Ref: https://nvd.nist.gov/vuln/detail/CVE-2009-3692 +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SUID is a human trust decision. /usr/lib/virtualbox/ matchwhitelist VirtualBoxVM matchwhitelist VBoxSDL matchwhitelist diff --git a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared index 6e5f9404..cab227f4 100644 --- a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared @@ -12,7 +12,10 @@ ## [filename] [mode] [owner] [group] [capability] ## [filename] [exactwhitelist|matchwhitelist|disablewhitelist|nosuid] ## -## TODO: white spaces inside file name untested and probably will not work. +## White spaces inside a file name are supported: the parser recovers the file +## name by reading the option fields from the right (a trailing whitelist +## keyword, or the mode/owner/group[/capability] tail anchored by the octal +## mode). Quoting/escaping is still not supported; a literal space is fine. ###################################################################### # Global Settings @@ -25,7 +28,9 @@ ###################################################################### ## For example, if you are not using SELinux the following might make sense to -## enable. TODO: research +## enable (utempter is SGID utmp for terminal login accounting; see +## 25_default_whitelist_selinux.conf for the full rationale). +## TODO-HUMAN-DEVELOPER-ONLY: disabling utempter's SGID is a human trust decision. #/utempter/utempter disablewhitelist ## If you are not going to use AppImages such as electrum Bitcoin wallet. @@ -119,5 +124,12 @@ ## anon-apps-config does this. #/usr/bin/ping 0744 root root none -## TODO: research -#/usr/lib/x86_64-linux-gnu/gstreamer1.0/grstreamer-1.0/gst-ptp-helper 0744 root root none +## gst-ptp-helper (gstreamer1.0-plugins-base): started by gst_ptp_init() for PTP +## (IEEE 1588) network clock sync; SUID root or cap_net_bind_service+cap_net_admin +## to bind privileged UDP ports 319/320, then drops privileges. Removing it only +## disables PTP clock sync; ordinary audio/video playback is unaffected, so this +## is a low-risk hardening candidate (cf. Red Hat bug 1724677). Prefer stripping +## to capabilities over setuid-root if kept. Path corrected below (was +## gstreamer1.0/grstreamer-1.0, which does not exist). +## TODO-HUMAN-DEVELOPER-ONLY: enabling this SUID/caps strip is a human trust decision. +#/usr/lib/x86_64-linux-gnu/gstreamer-1.0/gstreamer-1.0/gst-ptp-helper 0744 root root none From 5b34c9406198a47ce0c8d78a143606056e9d51b0 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:49 -0400 Subject: [PATCH 09/61] security-misc: dfuzzer commit pin, marker rewords, remount-secure cleanup - ci/dfuzzer-build.sh: pin dfuzzer to the exact upstream commit and verify the tag resolves to it (a moved tag now fails loudly). - grub 40_kernel_hardening + remount-secure hidepid: reword standing 'TODO' notes to 'REMINDER' (tracking, not pending work). - remount-secure: replace the echo/true toggle with a printf-array output helper; bring the file to strict-mode / shellcheck compliance (6-directive block, braces) as required to touch it. Co-Authored-By: Claude Opus 4.8 (1M context) --- ci/dfuzzer-build.sh | 21 ++- ..._kernel_hardening.cfg#security-misc-shared | 2 +- usr/bin/remount-secure#security-misc-shared | 147 ++++++++++-------- 3 files changed, 97 insertions(+), 73 deletions(-) diff --git a/ci/dfuzzer-build.sh b/ci/dfuzzer-build.sh index b536a002..790419b2 100755 --- a/ci/dfuzzer-build.sh +++ b/ci/dfuzzer-build.sh @@ -25,13 +25,18 @@ ## ## dfuzzer is NOT packaged in Ubuntu 24.04 noble (verified via ## packages.ubuntu.com - 'No such package'); hence the from-source -## build. Pinned to upstream tag v2.6 (latest release as of -## 2026-05-08). Bump when a new release lands. +## build. Pinned to upstream tag v2.6 AND its exact commit (verified +## below); latest release as of 2026-05-08. Bump both DFUZZER_TAG and +## DFUZZER_COMMIT together when a new release lands. set -o errexit set -o nounset set -o pipefail set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +## style-ok: no-tmp-hardcode (ephemeral CI build clone under /tmp/dfuzzer) if [ "${CI:-}" != "true" ] && [ "${ALLOW_LOCAL:-}" != "true" ]; then printf '%s\n' "${BASH_SOURCE[0]}: refusing to run outside CI. Set ALLOW_LOCAL=true to override." >&2 @@ -39,10 +44,20 @@ if [ "${CI:-}" != "true" ] && [ "${ALLOW_LOCAL:-}" != "true" ]; then fi DFUZZER_TAG="${DFUZZER_TAG:-v2.6}" +## Pin the exact upstream commit. A tag is mutable; verifying the +## resolved commit hash detects a re-pointed tag (supply-chain guard). +## Keep in sync with DFUZZER_TAG on every version bump. +DFUZZER_COMMIT="${DFUZZER_COMMIT:-a955a80f7dd20fd7aeffad91da1c495aab5dbbd3}" -## TODO: Better to clone the whole repository, then check out a commit hash? git clone --depth 1 --branch "${DFUZZER_TAG}" \ https://github.com/dbus-fuzzer/dfuzzer /tmp/dfuzzer + +dfuzzer_actual_commit="$(git -C /tmp/dfuzzer rev-parse HEAD)" +if [ "${dfuzzer_actual_commit}" != "${DFUZZER_COMMIT}" ]; then + printf '%s\n' "${BASH_SOURCE[0]}: dfuzzer tag ${DFUZZER_TAG} resolved to ${dfuzzer_actual_commit}, expected ${DFUZZER_COMMIT} -- refusing (tag moved?)." >&2 + exit 1 +fi + meson setup --buildtype=release /tmp/dfuzzer/build /tmp/dfuzzer ninja -C /tmp/dfuzzer/build -v sudo install -m 0755 /tmp/dfuzzer/build/dfuzzer /usr/local/bin/dfuzzer diff --git a/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared b/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared index 498a6ddc..ecbddc7a 100644 --- a/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared +++ b/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared @@ -55,7 +55,7 @@ GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX slab_nomerge" ## KSPP=partial ## KSPP sets the kernel parameters and CONFIG_SLUB_DEBUG. ## -## TODO: Debian forky / 14 +## REMINDER: Debian forky / 14 ## The first parameter is applicable when using Linux kernel >= 6.17 (retained here for future-proofing and completeness). ## #GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX hash_pointers=always" diff --git a/usr/bin/remount-secure#security-misc-shared b/usr/bin/remount-secure#security-misc-shared index 957ad46f..55c2924e 100755 --- a/usr/bin/remount-secure#security-misc-shared +++ b/usr/bin/remount-secure#security-misc-shared @@ -26,24 +26,33 @@ ## https://www.kicksecure.com/wiki/Dev/remount-secure ## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707 +## style-ok: no-has (runs in dracut where helper-scripts has.sh is unavailable; id itself may be absent) + +## Mount helper functions (_boot, _sys, _usr, ...) are invoked indirectly via +## the dispatch in main(); shellcheck cannot see the calls. +# shellcheck disable=SC2317 + #set -x -set -e -set -o pipefail +set -o errexit set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose init() { if test -o xtrace ; then - output_command=true + output_command=( true ) else - output_command=echo + output_command=( printf '%s\n' ) fi - $output_command "$0: INFO: START" + "${output_command[@]}" "$0: INFO: START" ## dracut does not have id. Saving space in initial ramdisk. if command -v id &>/dev/null ; then if [ "$(id -u)" != "0" ]; then - $output_command "ERROR: must be run as root! sudo $0" + "${output_command[@]}" "ERROR: must be run as root! sudo $0" exit 1 fi fi @@ -53,10 +62,10 @@ init() { ## dracut sets NEWROOT=/sysroot [[ -v NEWROOT ]] || NEWROOT="" - if [ "$NEWROOT" = "" ]; then - $output_command "INFO: dracut detected: no" + if [ "${NEWROOT}" = "" ]; then + "${output_command[@]}" "INFO: dracut detected: no" else - $output_command "INFO: dracut detected: yes - NEWROOT: '$NEWROOT'" + "${output_command[@]}" "INFO: dracut detected: yes - NEWROOT: '${NEWROOT}'" fi ## Debugging. @@ -76,33 +85,33 @@ parse_options() { do case ${1:-} in 0) - $output_command "WARNING: Not using remount-secure." + "${output_command[@]}" "WARNING: Not using remount-secure." exit 0 shift ;; 1) - $output_command "INFO: level 1/3 (low)" + "${output_command[@]}" "INFO: level 1/3 (low)" most_noexec_maybe="" home_noexec_maybe="" parsed=true shift ;; 2) - $output_command "INFO: level 2/3 (medium)" + "${output_command[@]}" "INFO: level 2/3 (medium)" most_noexec_maybe=",noexec" home_noexec_maybe="" parsed=true shift ;; 3) - $output_command "INFO: level 3/3 (high)" + "${output_command[@]}" "INFO: level 3/3 (high)" most_noexec_maybe=",noexec" home_noexec_maybe=",noexec" parsed=true shift ;; --force) - $output_command "INFO: --force" + "${output_command[@]}" "INFO: --force" option_force=true shift ;; @@ -111,7 +120,7 @@ parse_options() { break ;; -*) - echo "ERROR: unknown option: $1" >&2 + printf '%s\n' "ERROR: unknown option: ${1}" >&2 exit 1 ;; *) @@ -125,27 +134,27 @@ parse_options() { [[ -v home_noexec_maybe ]] || home_noexec_maybe="" [[ -v most_noexec_maybe ]] || most_noexec_maybe="" - $output_command "INFO: using nosuid,nodev: yes" + "${output_command[@]}" "INFO: using nosuid,nodev: yes" - if [ "$home_noexec_maybe" = "" ]; then - $output_command "INFO: using noexec for all: no" + if [ "${home_noexec_maybe}" = "" ]; then + "${output_command[@]}" "INFO: using noexec for all: no" else - $output_command "INFO: using noexec for all: yes" + "${output_command[@]}" "INFO: using noexec for all: yes" return 0 fi - if [ "$most_noexec_maybe" = "" ]; then - $output_command "INFO: using noexec for most: no" + if [ "${most_noexec_maybe}" = "" ]; then + "${output_command[@]}" "INFO: using noexec for most: no" else - $output_command "INFO: using noexec for most (not all): yes" + "${output_command[@]}" "INFO: using noexec for most (not all): yes" return 0 fi - if [ "$parsed" = "true" ]; then + if [ "${parsed}" = "true" ]; then return 0 fi - $output_command "ERROR: syntax error. use either: + "${output_command[@]}" "ERROR: syntax error. use either: $0 0 $0 1 $0 2 @@ -156,14 +165,14 @@ $0 3" preparation() { ## Debugging. - #$output_command "INFO: 'findmnt --list' output at the START." - #$output_command "$(findmnt --list)" - #$output_command "" + #"${output_command[@]}" "INFO: 'findmnt --list' output at the START." + #"${output_command[@]}" "$(findmnt --list)" + #"${output_command[@]}" "" true } remount_secure() { - $output_command "" + "${output_command[@]}" "" ## ${FUNCNAME[1]} is the name of the calling function. I.e. the function ## which called this function. @@ -174,52 +183,52 @@ remount_secure() { ## example status_file_full_path: ## /run/remount-secure/_home - old_mount_options="$(findmnt --noheadings --output options -- "$mount_folder")" || true + old_mount_options="$(findmnt --noheadings --output options -- "${mount_folder}")" || true ## example old_mount_options: ## rw,nosuid,nodev,relatime,discard - $output_command "INFO: '$mount_folder' old_mount_options: '$old_mount_options'" + "${output_command[@]}" "INFO: '${mount_folder}' old_mount_options: '${old_mount_options}'" - if printf '%s\n' "$old_mount_options" | grep "$intended_mount_options" >/dev/null 2>/dev/null ; then - $output_command "INFO: '$mount_folder' has already intended mount options. ('$intended_mount_options')" + if printf '%s\n' "${old_mount_options}" | grep "${intended_mount_options}" >/dev/null 2>/dev/null ; then + "${output_command[@]}" "INFO: '${mount_folder}' has already intended mount options. ('${intended_mount_options}')" return 0 fi ## When this package is upgraded, the systemd unit will run again. ## If the user meanwhile manually relaxed mount options, this should not be undone. - if [ ! "$option_force" == "true" ]; then - if [ -e "$status_file_full_path" ]; then - $output_command "INFO: '$mount_folder' already remounted earlier. Not remounting again. Use --force if this is what you want." + if [ ! "${option_force}" == "true" ]; then + if [ -e "${status_file_full_path}" ]; then + "${output_command[@]}" "INFO: '${mount_folder}' already remounted earlier. Not remounting again. Use --force if this is what you want." return 0 fi fi - if ! test -d "$mount_folder" ; then + if ! test -d "${mount_folder}" ; then ## For example /boot/efi does not always exist on all systems. - $output_command "INFO: '$mount_folder' folder exists: no" + "${output_command[@]}" "INFO: '${mount_folder}' folder exists: no" return 0 fi - $output_command "INFO: '$mount_folder' folder exists: yes" + "${output_command[@]}" "INFO: '${mount_folder}' folder exists: yes" - if findmnt --noheadings "$mount_folder" >/dev/null ; then - $output_command "INFO: '$mount_folder' already mounted, therefore using remount." - $output_command INFO: Executing: mount --make-private --options "remount,${intended_mount_options}" "$mount_folder" - mount --make-private --options "remount,${intended_mount_options}" "$mount_folder" || exit_code=100 + if findmnt --noheadings "${mount_folder}" >/dev/null ; then + "${output_command[@]}" "INFO: '${mount_folder}' already mounted, therefore using remount." + "${output_command[@]}" "INFO: Executing: mount --make-private --options remount,${intended_mount_options} ${mount_folder}" + mount --make-private --options "remount,${intended_mount_options}" "${mount_folder}" || exit_code=100 else - $output_command "INFO: '$mount_folder' not yet mounted, therefore using mount bind." - $output_command INFO: Executing: mount --make-private --options "$intended_mount_options" --bind "$mount_folder" "$mount_folder" - mount --make-private --options "$intended_mount_options" --bind "$mount_folder" "$mount_folder" || exit_code=101 + "${output_command[@]}" "INFO: '${mount_folder}' not yet mounted, therefore using mount bind." + "${output_command[@]}" "INFO: Executing: mount --make-private --options ${intended_mount_options} --bind ${mount_folder} ${mount_folder}" + mount --make-private --options "${intended_mount_options}" --bind "${mount_folder}" "${mount_folder}" || exit_code=101 fi - new_mount_options="$(findmnt --noheadings --output options -- "$mount_folder")" || true - $output_command "INFO: '$mount_folder' new_mount_options: '$new_mount_options'" + new_mount_options="$(findmnt --noheadings --output options -- "${mount_folder}")" || true + "${output_command[@]}" "INFO: '${mount_folder}' new_mount_options: '${new_mount_options}'" - touch "$status_file_full_path" + touch "${status_file_full_path}" } _boot() { - mount_folder="$NEWROOT/boot" + mount_folder="${NEWROOT}/boot" ## https://lists.freedesktop.org/archives/systemd-devel/2015-February/028456.html intended_mount_options="nosuid,nodev,noexec" remount_secure @@ -227,7 +236,7 @@ _boot() { _boot_efi() { ## TODO: new, test - mount_folder="$NEWROOT/boot/efi" + mount_folder="${NEWROOT}/boot/efi" intended_mount_options="nosuid,nodev,noexec" remount_secure } @@ -261,25 +270,25 @@ _sys() { } _tmp() { - mount_folder="$NEWROOT/tmp" + mount_folder="${NEWROOT}/tmp" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _var_tmp() { - mount_folder="$NEWROOT/var/tmp" + mount_folder="${NEWROOT}/var/tmp" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _var_log() { - mount_folder="$NEWROOT/var/log" + mount_folder="${NEWROOT}/var/log" intended_mount_options="nosuid,nodev,noexec" remount_secure } _var() { - mount_folder="$NEWROOT/var" + mount_folder="${NEWROOT}/var" ## noexec: Not possible. Reason: ## Debian stores executable maintainer scripts in /var/lib/dpkg/info folder. intended_mount_options="nosuid,nodev" @@ -288,48 +297,48 @@ _var() { _usr() { ## TODO: new, test - mount_folder="$NEWROOT/usr" + mount_folder="${NEWROOT}/usr" intended_mount_options="nodev" remount_secure } _home() { - mount_folder="$NEWROOT/home" + mount_folder="${NEWROOT}/home" intended_mount_options="nosuid,nodev${home_noexec_maybe}" remount_secure } _root() { ## TODO: new, test - mount_folder="$NEWROOT/root" + mount_folder="${NEWROOT}/root" intended_mount_options="nosuid,nodev${home_noexec_maybe}" remount_secure } _srv() { ## TODO: new, test - mount_folder="$NEWROOT/srv" + mount_folder="${NEWROOT}/srv" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _media() { ## TODO: new, test - mount_folder="$NEWROOT/media" + mount_folder="${NEWROOT}/media" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _mnt() { ## TODO: new, test - mount_folder="$NEWROOT/mnt" + mount_folder="${NEWROOT}/mnt" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _opt() { ## TODO: new, test - mount_folder="$NEWROOT/opt" + mount_folder="${NEWROOT}/opt" ## Allow /opt exec as usually optional binaries are placed there such as Firefox ## when manually installed from tarball. intended_mount_options="nosuid,nodev" @@ -340,20 +349,20 @@ _etc() { ## TODO: new, test ## /etc cannot be noexec because various executables are there. To find, run: ## sudo find /etc -executable - mount_folder="$NEWROOT/etc" + mount_folder="${NEWROOT}/etc" intended_mount_options="nosuid,nodev" remount_secure } end() { ## Debugging. - #$output_command "INFO: 'findmnt --list' output at the END." - #$output_command "$(findmnt --list)" + #"${output_command[@]}" "INFO: 'findmnt --list' output at the END." + #"${output_command[@]}" "$(findmnt --list)" - $output_command "" - $output_command "INFO: exit_code: $exit_code" - $output_command "$0: INFO: END" - exit $exit_code + "${output_command[@]}" "" + "${output_command[@]}" "INFO: exit_code: ${exit_code}" + "${output_command[@]}" "$0: INFO: END" + exit "${exit_code}" } main() { @@ -382,7 +391,7 @@ main() { end } -## TODO: see also hidepid /usr/lib/systemd/system/proc-hidepid.service +## REMINDER: see also hidepid /usr/lib/systemd/system/proc-hidepid.service #mount --options defaults,nosuid,nodev,noexec,remount,subset=pid /proc main "$@" From c4bf5403d0dbda87b2e1fe165ade7c81bf4e1b30 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:44:07 -0400 Subject: [PATCH 10/61] security-misc: declare bash (>= 4.4) for inherit_errexit - Many scripts use 'shopt -s inherit_errexit' (bash 4.4+) in their strict-mode preamble; ${misc:Depends} does not supply the baseline, so an older /bin/bash would die in the preamble before the script's logic. Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 4a944d1b..d14059e2 100644 --- a/debian/control +++ b/debian/control @@ -32,6 +32,7 @@ Package: security-misc-shared Architecture: all Depends: adduser, apparmor-profile-dist, + bash (>= 4.4), build-essential, dmsetup, helper-scripts, From a66fa50019f95326fef372ec5b3d1cde3b59b4a9 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:55:18 -0400 Subject: [PATCH 11/61] security-misc: fix undo chown empty-arg + emerg fallback (ai-review) - undo_policy_for_file: the strict-mode brace pass quoted "${verbose}", so 'permission-hardener disable all' ran 'chown "" -- ...' (invalid empty owner) and 'chmod "" ...'. Make verbose an array so an empty value expands to zero arguments. Canary-verified. - emerg-shutdown fallback: when the prebuilt binary is absent, compile straight into /run (tmpfs, writable even if /usr is read-only) rather than via build-emerg-shutdown into /usr/bin. - Document the space-filename parser limitations (consecutive spaces collapse; an octal-looking path chunk can misanchor -> entry skipped). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../permission-hardener#security-misc-shared | 14 +++++++------ .../emerg-shutdown#security-misc-shared | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 775cea57..c3e66c23 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -477,6 +477,10 @@ load_state() { ## mode/owner/group[/capability] tail whose first field is the octal mode ## -- so recover the filename by parsing options from the right and ## rejoining everything before them. Space-free lines are left unchanged. + ## Limitation: a single internal space is supported; consecutive spaces + ## collapse, and a space-delimited path chunk that is itself a bare octal + ## mode (e.g. '744') can misanchor. Such a filename does not exist, so the + ## entry is skipped, not misapplied. Quoting/escaping is not supported. field_count="${#field_list[@]}" if (( field_count > 2 )); then case "${field_list[field_count-1]}" in @@ -731,10 +735,10 @@ undo_policy_for_file() { undo_file="${1}" undo_all=false - verbose='--verbose' + verbose=('--verbose') if [ "${undo_file}" = 'all' ]; then undo_all=true - verbose='' + verbose=() fi if [ ! -f "${state_file}" ]; then @@ -773,12 +777,10 @@ undo_policy_for_file() { state_user_owner_item="${state_user_owner_list[state_idx]}" state_group_owner_item="${state_group_owner_list[state_idx]}" state_mode_item="${state_mode_list[state_idx]}" - # shellcheck disable=SC2086 - chown "${verbose}" -- "${state_user_owner_item}:${state_group_owner_item}" \ + chown "${verbose[@]}" -- "${state_user_owner_item}:${state_group_owner_item}" \ "${undo_file}" || exit_code=202 ## chmod needs to be run after chown since chown removes suid. - # shellcheck disable=SC2086 - chmod "${verbose}" "${state_mode_item}" "${undo_file}" || exit_code=203 + chmod "${verbose[@]}" "${state_mode_item}" "${undo_file}" || exit_code=203 else log info "File does not exist: '${undo_file}'" fi diff --git a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared index fafffc7f..27f21856 100755 --- a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared @@ -53,15 +53,20 @@ else ## into /run (tmpfs) so it is memory-resident for the emergency; memlockd ## then locks it into RAM. if [ ! -f '/run/emerg-shutdown' ]; then - if [ ! -f '/usr/bin/emerg-shutdown' ]; then - ## Fallback: build was skipped or removed. Compile now so the emergency - ## shutdown capability is never silently absent. - /usr/libexec/security-misc/build-emerg-shutdown || { - printf '%s\n' 'Could not build emerg-shutdown executable!' - exit 1 - } + if [ -f '/usr/bin/emerg-shutdown' ]; then + cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown + else + ## Fallback: prebuilt binary absent (build skipped/removed). Compile + ## straight into /run (tmpfs, writable even when /usr is read-only) so the + ## emergency-shutdown capability is never silently absent. + /usr/libexec/security-misc/compile-emerg-shutdown \ + /usr/src/security-misc/emerg-shutdown.c \ + /run/emerg-shutdown \ + || { + printf '%s\n' 'Could not compile emerg-shutdown executable!' + exit 1 + } fi - cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown fi ## memlockd daemonizes itself, so no need to background it. From c2fcb2d893bf26a59c1b25415333fafcad8682dc Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 05:15:00 -0400 Subject: [PATCH 12/61] security-misc: drop versioned bash dependency - Revert the bash (>= 4.4) Depends. Supported Debian (trixie+) always ships bash >= 4.4 and earlier is unsupported, so the inherit_errexit builtin is always present; the dependency adds nothing. Policy bans it (bash-style-guide R-010b). Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/control | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/control b/debian/control index d14059e2..4a944d1b 100644 --- a/debian/control +++ b/debian/control @@ -32,7 +32,6 @@ Package: security-misc-shared Architecture: all Depends: adduser, apparmor-profile-dist, - bash (>= 4.4), build-essential, dmsetup, helper-scripts, From 37ad483b25c43b93bb350983c2534f9d775e977a Mon Sep 17 00:00:00 2001 From: "claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4)" Date: Fri, 14 Aug 2026 12:34:17 -0400 Subject: [PATCH 13/61] permission-hardener: anchor mode tail from the right for spaced filenames - Space filenames are recovered from the option tail; the mode/owner/group [/capability] tail has a fixed width (3 or 4 fields). - Old left-to-right octal scan misanchored when a space-delimited filename chunk was itself a bare octal (e.g. '/path/a 744 name 0744 root root'): the field-count check then failed and the WHOLE hardener aborted (exit 200), contradicting the comment that claimed such a line was skipped. - Anchor the mode from the right (field_count-3, else field_count-4); prefer the 3-field reading so a filename ending in an octal chunk is not mistaken for a 4-field capability tail. - Regression test: config line with a bare-octal filename chunk must parse via print-policy; fails on the old code (exit 200 / entry missing). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_whitespace_filename.sh | 47 +++++++++++++++---- .../permission-hardener#security-misc-shared | 26 +++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index 9d45b54e..eb39a65e 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -41,6 +41,13 @@ spaced_file="${test_dir}/some space/binary" mkdir -p -- "${test_dir}/some space" touch -- "${spaced_file}" +## A filename whose space-split yields a BARE octal chunk ('744'). The mode +## must be anchored from the RIGHT: a left-to-right scan anchors on that chunk +## instead of the real mode at the end, fails the field-count check, and aborts +## the whole hardener (exit 200) rather than parsing the entry. +octal_chunk_file="${test_dir}/a 744 name" +touch -- "${octal_chunk_file}" + config_dir="/etc/permission-hardener.d" config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" mkdir -p -- "${config_dir}" @@ -53,17 +60,37 @@ cleanup() { } trap cleanup EXIT -## mode-form entry: -printf '%s\n' "${spaced_file} 0744 root root" > "${config_file}" +## mode-form entries: +{ + printf '%s\n' "${spaced_file} 0744 root root" + printf '%s\n' "${octal_chunk_file} 0744 root root" +} > "${config_file}" -policy_output="$( "${ph_bin}" print-policy )" +## A parse failure on any line aborts the whole run with exit 200; capture the +## status via an 'if' condition (errexit stays enabled) so the failure is +## reported cleanly rather than aborting the test. +if policy_output="$( "${ph_bin}" print-policy 2>/dev/null )"; then + ph_rc=0 +else + ph_rc=$? +fi -if printf '%s\n' "${policy_output}" | grep -qF -- "${spaced_file}"; then - printf '%s\n' "PASS: space-containing filename parsed and present in policy." - exit 0 +test_status=0 +if [ "${ph_rc}" -ne 0 ]; then + printf '%s\n' "FAIL: print-policy exited ${ph_rc} (parse aborted the whole run)." >&2 + test_status=1 fi +for expected_file in "${spaced_file}" "${octal_chunk_file}"; do + if printf '%s\n' "${policy_output}" | grep -qF -- "${expected_file}"; then + printf '%s\n' "PASS: filename '${expected_file}' parsed and present in policy." + else + printf '%s\n' "FAIL: filename '${expected_file}' missing from print-policy output." >&2 + test_status=1 + fi +done -printf '%s\n' "FAIL: space-containing filename '${spaced_file}' missing from print-policy output." >&2 -printf '%s\n' "----- print-policy output -----" >&2 -printf '%s\n' "${policy_output}" >&2 -exit 1 +if [ "${test_status}" -ne 0 ]; then + printf '%s\n' "----- print-policy output -----" >&2 + printf '%s\n' "${policy_output}" >&2 +fi +exit "${test_status}" diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index c3e66c23..1444328d 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -430,7 +430,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local field_count recovered_file_name mode_index field_index + local field_count recovered_file_name mode_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -477,10 +477,13 @@ load_state() { ## mode/owner/group[/capability] tail whose first field is the octal mode ## -- so recover the filename by parsing options from the right and ## rejoining everything before them. Space-free lines are left unchanged. - ## Limitation: a single internal space is supported; consecutive spaces - ## collapse, and a space-delimited path chunk that is itself a bare octal - ## mode (e.g. '744') can misanchor. Such a filename does not exist, so the - ## entry is skipped, not misapplied. Quoting/escaping is not supported. + ## The mode tail has a fixed width -- 3 fields (mode owner group) or 4 + ## (mode owner group capability) -- so anchor the mode from the RIGHT. A + ## left-to-right scan misanchors when a space-delimited filename chunk is + ## itself a bare octal (e.g. '/path/a 744 name 0744 root root'). + ## Limitation: consecutive spaces collapse and quoting/escaping is not + ## supported; a line whose field count is still invalid after recovery is + ## rejected loudly (exit 200 below), not silently skipped. field_count="${#field_list[@]}" if (( field_count > 2 )); then case "${field_list[field_count-1]}" in @@ -489,13 +492,14 @@ load_state() { field_list=( "${recovered_file_name}" "${field_list[field_count-1]}" ) ;; *) + ## Prefer the 3-field reading (field_count-3) so a filename ending + ## in an octal chunk is not mistaken for a 4-field capability tail. mode_index='' - for (( field_index=1; field_index < field_count; field_index++ )); do - if [[ "${field_list[field_index]}" =~ ^0?[0-7]{3,4}$ ]]; then - mode_index="${field_index}" - break - fi - done + if (( field_count >= 4 )) && [[ "${field_list[field_count-3]}" =~ ^0?[0-7]{3,4}$ ]]; then + mode_index="$(( field_count - 3 ))" + elif (( field_count >= 5 )) && [[ "${field_list[field_count-4]}" =~ ^0?[0-7]{3,4}$ ]]; then + mode_index="$(( field_count - 4 ))" + fi if [ -n "${mode_index}" ] && (( mode_index > 1 )); then recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:mode_index}")" field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) From fca4984733e4aff6b90e8dd248d38b8cbb3889cc Mon Sep 17 00:00:00 2001 From: "claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4)" Date: Fri, 14 Aug 2026 12:40:52 -0400 Subject: [PATCH 14/61] permission-hardener confs: correct gst-ptp-helper path/pkg and #8633 fix credit - 30_default.conf: gst-ptp-helper ships in libgstreamer1.0-0, not gstreamer1.0-plugins-base; fix the disabled entry's path to .../gstreamer1.0/gstreamer-1.0/... (first component was gstreamer-1.0, which does not exist). Verified via dpkg -S. - 25_default_whitelist_qubes.conf: the #8633 EoP fix is qfile-unpacker (qubes-core-agent-linux) refusing a UID change other than the caller's; libqubes-pure path/symlink sanitization (qubes-linux-utils PR #87) is a separate layer. The comment credited only PR #87. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../25_default_whitelist_qubes.conf#security-misc-shared | 8 +++++--- .../30_default.conf#security-misc-shared | 7 +++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared index 34656b92..614aca7e 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared @@ -12,9 +12,11 @@ ## the recipient dir and setfsuid()/drop to the unprivileged user before writing ## -- both need root, so SUID cannot simply be stripped. It parses attacker- ## influenced input from another qube => high-value local-EoP target. The issue -## below (#8633) is now CLOSED: upstream HARDENED the unpacker (path sanitization, -## qubes-linux-utils PR #87) rather than dropping SUID. Keep the package patched; -## residual risk is the parser itself. +## below (#8633) is now CLOSED: upstream HARDENED the unpacker rather than +## dropping SUID -- qfile-unpacker (qubes-core-agent-linux) now refuses a UID +## change to anything but the caller's own UID, and libqubes-pure adds path, +## symlink and filename sanitization (qubes-linux-utils PR #87). Keep the +## package patched; residual risk is the parser itself. ## https://github.com/QubesOS/qubes-core-agent-linux/blob/master/qubes-rpc/qfile-unpacker.c ## https://github.com/QubesOS/qubes-issues/issues/8633 (historic local-root EoP) ## diff --git a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared index cab227f4..01dda03a 100644 --- a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared @@ -124,12 +124,11 @@ ## anon-apps-config does this. #/usr/bin/ping 0744 root root none -## gst-ptp-helper (gstreamer1.0-plugins-base): started by gst_ptp_init() for PTP +## gst-ptp-helper (libgstreamer1.0-0): started by gst_ptp_init() for PTP ## (IEEE 1588) network clock sync; SUID root or cap_net_bind_service+cap_net_admin ## to bind privileged UDP ports 319/320, then drops privileges. Removing it only ## disables PTP clock sync; ordinary audio/video playback is unaffected, so this ## is a low-risk hardening candidate (cf. Red Hat bug 1724677). Prefer stripping -## to capabilities over setuid-root if kept. Path corrected below (was -## gstreamer1.0/grstreamer-1.0, which does not exist). +## to capabilities over setuid-root if kept. ## TODO-HUMAN-DEVELOPER-ONLY: enabling this SUID/caps strip is a human trust decision. -#/usr/lib/x86_64-linux-gnu/gstreamer-1.0/gstreamer-1.0/gst-ptp-helper 0744 root root none +#/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper 0744 root root none From 780561e61a6bf1c8d110d1105a5102b59ceb14ea Mon Sep 17 00:00:00 2001 From: "claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4)" Date: Fri, 14 Aug 2026 12:49:10 -0400 Subject: [PATCH 15/61] permission-hardener: fix disable-all directive + numeric-owner misparse Both surfaced by a fresh-claude review of the right-anchor change. - whitelists_disable_all=true was unreachable: the character-class check (which excludes '=') ran before the directive check, so the documented line tripped "invalid characters" and aborted the whole run (exit 200). Recognize the directive first. - Numeric octal-looking owner in a capability entry (e.g. ' 0744 0755 root cap'): right-anchoring folded the owner into the mode position, the folded path did not exist, and the entry was silently dropped -- file left un-hardened. Skip recovery when the line is already a well-formed no-space entry (octal mode at field 1, count 4 or 5). - Correct the stale field-order comment: the tail is 'filemode user_owner group_owner [capability]', mode first (per add_to_policy). - Regression tests: numeric-owner case in the whitespace test; new test_whitelists_disable_all.sh. Both fail on the pre-fix code. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_whitelists_disable_all.sh | 82 +++++++++++++++++++ .../test_whitespace_filename.sh | 13 ++- .../permission-hardener#security-misc-shared | 30 +++++-- 3 files changed, 114 insertions(+), 11 deletions(-) create mode 100755 ci/tests/permission_hardener/test_whitelists_disable_all.sh diff --git a/ci/tests/permission_hardener/test_whitelists_disable_all.sh b/ci/tests/permission_hardener/test_whitelists_disable_all.sh new file mode 100755 index 00000000..5a0dc5b0 --- /dev/null +++ b/ci/tests/permission_hardener/test_whitelists_disable_all.sh @@ -0,0 +1,82 @@ +#!/bin/bash +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Regression test: the documented special directive 'whitelists_disable_all=true' +## must be honored, not rejected. +## +## The line contains '=', which is outside the config character-class allow-set. +## The directive check must run BEFORE that character-class validation; otherwise +## the documented line trips "invalid characters" and aborts the whole hardener +## (exit 200), so every other hardening rule in every conf is dropped too. +## +## Drives the REAL script via 'print-policy' with a conf that holds the directive +## plus one ordinary entry, and asserts print-policy exits 0 with the ordinary +## entry present. +## +## Requires root: writes a temporary config under /etc/permission-hardener.d/ +## and needs helper-scripts installed (sourced by permission-hardener). + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)" +ph_bin="${PERMISSION_HARDENER_BIN:-${repo_root}/usr/bin/permission-hardener#security-misc-shared}" + +if [ ! -f "${ph_bin}" ]; then + printf '%s\n' "FAIL: permission-hardener not found at '${ph_bin}'." >&2 + exit 1 +fi + +test_dir="$(mktemp -d -t ph-wda-test.XXXXXX)" +ordinary_file="${test_dir}/ordinary" +touch -- "${ordinary_file}" + +config_dir="/etc/permission-hardener.d" +config_file="${config_dir}/zz-ai-whitelists-disable-all-test.conf" +mkdir -p -- "${config_dir}" + +## invoked indirectly via 'trap ... EXIT' +# shellcheck disable=SC2317 +cleanup() { + safe-rm -f -- "${config_file}" + safe-rm -rf -- "${test_dir}" +} +trap cleanup EXIT + +{ + printf '%s\n' 'whitelists_disable_all=true' + printf '%s\n' "${ordinary_file} 0744 root root" +} > "${config_file}" + +## The directive line aborts the whole run (exit 200) on the buggy ordering; +## capture the status via an 'if' condition (errexit stays enabled). +if policy_output="$( "${ph_bin}" print-policy 2>/dev/null )"; then + ph_rc=0 +else + ph_rc=$? +fi + +test_status=0 +if [ "${ph_rc}" -ne 0 ]; then + printf '%s\n' "FAIL: print-policy exited ${ph_rc} (directive rejected as invalid characters)." >&2 + test_status=1 +fi +if printf '%s\n' "${policy_output}" | grep -qF -- "${ordinary_file}"; then + printf '%s\n' "PASS: whitelists_disable_all=true accepted; ordinary entry present." +else + printf '%s\n' "FAIL: ordinary entry '${ordinary_file}' missing from print-policy output." >&2 + test_status=1 +fi + +if [ "${test_status}" -ne 0 ]; then + printf '%s\n' "----- print-policy output -----" >&2 + printf '%s\n' "${policy_output}" >&2 +fi +exit "${test_status}" diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index eb39a65e..ed0397f8 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -48,6 +48,14 @@ touch -- "${spaced_file}" octal_chunk_file="${test_dir}/a 744 name" touch -- "${octal_chunk_file}" +## A space-free filename with a 4-option (capability) tail whose OWNER is a +## numeric id that looks octal ('0755'). Right-anchoring must NOT treat that +## owner as the mode: it would fold the real mode into the filename, the folded +## path does not exist, and the entry is silently dropped -- leaving the file +## un-hardened. The line is already well formed, so no recovery must happen. +numeric_owner_file="${test_dir}/numowner" +touch -- "${numeric_owner_file}" + config_dir="/etc/permission-hardener.d" config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" mkdir -p -- "${config_dir}" @@ -60,10 +68,11 @@ cleanup() { } trap cleanup EXIT -## mode-form entries: +## mode-form entries: [capability] { printf '%s\n' "${spaced_file} 0744 root root" printf '%s\n' "${octal_chunk_file} 0744 root root" + printf '%s\n' "${numeric_owner_file} 0744 0755 root cap_net_raw" } > "${config_file}" ## A parse failure on any line aborts the whole run with exit 200; capture the @@ -80,7 +89,7 @@ if [ "${ph_rc}" -ne 0 ]; then printf '%s\n' "FAIL: print-policy exited ${ph_rc} (parse aborted the whole run)." >&2 test_status=1 fi -for expected_file in "${spaced_file}" "${octal_chunk_file}"; do +for expected_file in "${spaced_file}" "${octal_chunk_file}" "${numeric_owner_file}"; do if printf '%s\n' "${policy_output}" | grep -qF -- "${expected_file}"; then printf '%s\n' "PASS: filename '${expected_file}' parsed and present in policy." else diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 1444328d..2a517d19 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -423,14 +423,14 @@ load_state() { ## Config format: ## path options ## where options is one of: - ## user_owner group_owner filemode [capability-setting] + ## filemode user_owner group_owner [capability-setting] ## [nosuid|exactwhitelist|matchwhitelist|disablewhitelist] ## ## Additionally, the special value 'whitelists_disable_all=true' is understood ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local field_count recovered_file_name mode_index + local field_count recovered_file_name mode_index already_wellformed ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -454,6 +454,15 @@ load_state() { continue fi + ## Recognize the special directive before the character-class check below, + ## whose allowed set excludes '=' and would otherwise reject and abort on + ## this documented line. + if [ "${line}" = 'whitelists_disable_all=true' ]; then + whitelists_disable_all=true + log info "whitelists_disable_all=true" + continue + fi + if ! [[ "${line}" =~ ^[-0-9a-zA-Z._/[:space:]]*$ ]]; then exit_code=200 log error "Line contains invalid characters: '${line}'" >&2 @@ -462,12 +471,6 @@ load_state() { exit "${exit_code}" fi - if [ "${line}" = 'whitelists_disable_all=true' ]; then - whitelists_disable_all=true - log info "whitelists_disable_all=true" - continue - fi - processed_config_line="${line}" IFS=' ' read -r -a field_list <<< "${line}" @@ -485,7 +488,16 @@ load_state() { ## supported; a line whose field count is still invalid after recovery is ## rejected loudly (exit 200 below), not silently skipped. field_count="${#field_list[@]}" - if (( field_count > 2 )); then + ## A no-space entry already has an octal mode at field 1 (total fields 4 or + ## 5). Recovering such a line would fold a numeric owner into the mode + ## position, so only attempt filename recovery when it is NOT already well + ## formed. + already_wellformed=false + if (( field_count == 4 || field_count == 5 )) \ + && [[ "${field_list[1]}" =~ ^0?[0-7]{3,4}$ ]]; then + already_wellformed=true + fi + if (( field_count > 2 )) && [ "${already_wellformed}" = 'false' ]; then case "${field_list[field_count-1]}" in exactwhitelist|matchwhitelist|disablewhitelist|nosuid) recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:field_count-1}")" From 73cc7889b271e2f3733daed276ba235dde04dc20 Mon Sep 17 00:00:00 2001 From: "claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4)" Date: Fri, 14 Aug 2026 13:00:51 -0400 Subject: [PATCH 16/61] permission-hardener: disambiguate octal-ambiguous entries by file existence Fresh-claude review found the earlier field-1 guard reintroduced a symmetric misparse: a spaced filename whose second component is a bare octal (e.g. '/srv/a 744 0644 root root') got dropped because the guard mistook the octal component for the mode. The 5-field reading is genuinely ambiguous lexically -- '/a 744 0644 root root' (spaced name) vs '/foo 0744 1000 root cap' (numeric owner). Resolve by which candidate filename exists on disk, consistent with the tool skipping nonexistent targets. Keep the no-recovery reading only when field 1 is an octal mode and only the un-recovered name exists. - Replace the lexical guard with the existence check inside the right-anchor branch; the whitelist-keyword branch is no longer gated (it was wrongly skipped for '/x 744 y matchwhitelist'). - Regression test: spaced octal-second-component case; the assertion now matches the whole first column (trailing TAB) so '/a 744' cannot substring-match the '/a 744 name' entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_whitelists_disable_all.sh | 2 +- .../test_whitespace_filename.sh | 15 ++++++++-- .../permission-hardener#security-misc-shared | 30 +++++++++++-------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/ci/tests/permission_hardener/test_whitelists_disable_all.sh b/ci/tests/permission_hardener/test_whitelists_disable_all.sh index 5a0dc5b0..0957a6b2 100755 --- a/ci/tests/permission_hardener/test_whitelists_disable_all.sh +++ b/ci/tests/permission_hardener/test_whitelists_disable_all.sh @@ -68,7 +68,7 @@ if [ "${ph_rc}" -ne 0 ]; then printf '%s\n' "FAIL: print-policy exited ${ph_rc} (directive rejected as invalid characters)." >&2 test_status=1 fi -if printf '%s\n' "${policy_output}" | grep -qF -- "${ordinary_file}"; then +if printf '%s\n' "${policy_output}" | grep -qF -- "${ordinary_file}"$'\t'; then printf '%s\n' "PASS: whitelists_disable_all=true accepted; ordinary entry present." else printf '%s\n' "FAIL: ordinary entry '${ordinary_file}' missing from print-policy output." >&2 diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index ed0397f8..def1f2a3 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -56,6 +56,13 @@ touch -- "${octal_chunk_file}" numeric_owner_file="${test_dir}/numowner" touch -- "${numeric_owner_file}" +## The mirror-image of the numeric-owner case: a SPACED filename whose second +## component (field index 1) is a bare octal, with a 3-field tail. Here field 1 +## is NOT the mode, so recovery must happen. Existence disambiguates: the spaced +## name exists, its no-recovery prefix does not. +octal_second_file="${test_dir}/a 744" +touch -- "${octal_second_file}" + config_dir="/etc/permission-hardener.d" config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" mkdir -p -- "${config_dir}" @@ -73,6 +80,7 @@ trap cleanup EXIT printf '%s\n' "${spaced_file} 0744 root root" printf '%s\n' "${octal_chunk_file} 0744 root root" printf '%s\n' "${numeric_owner_file} 0744 0755 root cap_net_raw" + printf '%s\n' "${octal_second_file} 0644 root root" } > "${config_file}" ## A parse failure on any line aborts the whole run with exit 200; capture the @@ -89,8 +97,11 @@ if [ "${ph_rc}" -ne 0 ]; then printf '%s\n' "FAIL: print-policy exited ${ph_rc} (parse aborted the whole run)." >&2 test_status=1 fi -for expected_file in "${spaced_file}" "${octal_chunk_file}" "${numeric_owner_file}"; do - if printf '%s\n' "${policy_output}" | grep -qF -- "${expected_file}"; then +## print-policy prints tab-separated columns (FileUser...), so match +## each filename as the whole first field (trailing TAB). A bare substring match +## would let '/a 744' spuriously match the '/a 744 name' entry. +for expected_file in "${spaced_file}" "${octal_chunk_file}" "${numeric_owner_file}" "${octal_second_file}"; do + if printf '%s\n' "${policy_output}" | grep -qF -- "${expected_file}"$'\t'; then printf '%s\n' "PASS: filename '${expected_file}' parsed and present in policy." else printf '%s\n' "FAIL: filename '${expected_file}' missing from print-policy output." >&2 diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 2a517d19..f17c55a1 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -430,7 +430,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local field_count recovered_file_name mode_index already_wellformed + local field_count recovered_file_name mode_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -484,20 +484,16 @@ load_state() { ## (mode owner group capability) -- so anchor the mode from the RIGHT. A ## left-to-right scan misanchors when a space-delimited filename chunk is ## itself a bare octal (e.g. '/path/a 744 name 0744 root root'). + ## Two readings can be lexically valid when a filename component OR a + ## numeric owner looks octal ('/a 744 0644 root root' wants the spaced + ## name; '/foo 0744 1000 root cap' wants no recovery). Disambiguate by + ## which candidate exists on disk -- the tool skips nonexistent targets + ## anyway -- keeping the no-recovery reading when field 1 is really the mode. ## Limitation: consecutive spaces collapse and quoting/escaping is not ## supported; a line whose field count is still invalid after recovery is ## rejected loudly (exit 200 below), not silently skipped. field_count="${#field_list[@]}" - ## A no-space entry already has an octal mode at field 1 (total fields 4 or - ## 5). Recovering such a line would fold a numeric owner into the mode - ## position, so only attempt filename recovery when it is NOT already well - ## formed. - already_wellformed=false - if (( field_count == 4 || field_count == 5 )) \ - && [[ "${field_list[1]}" =~ ^0?[0-7]{3,4}$ ]]; then - already_wellformed=true - fi - if (( field_count > 2 )) && [ "${already_wellformed}" = 'false' ]; then + if (( field_count > 2 )); then case "${field_list[field_count-1]}" in exactwhitelist|matchwhitelist|disablewhitelist|nosuid) recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:field_count-1}")" @@ -514,7 +510,17 @@ load_state() { fi if [ -n "${mode_index}" ] && (( mode_index > 1 )); then recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:mode_index}")" - field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) + ## Keep the no-recovery reading when field 1 is an octal mode and + ## only the un-recovered name exists (field 1 is the mode, not a + ## filename component). + if (( field_count == 4 || field_count == 5 )) \ + && [[ "${field_list[1]}" =~ ^0?[0-7]{3,4}$ ]] \ + && [ ! -e "${recovered_file_name}" ] \ + && [ -e "${field_list[0]}" ]; then + true 'DEBUG: field 1 is the mode; no filename recovery' + else + field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) + fi fi ;; esac From 6dcffee4bf717bfac9a4c13153dcaa1a25cdc89f Mon Sep 17 00:00:00 2001 From: "claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4)" Date: Fri, 14 Aug 2026 13:12:25 -0400 Subject: [PATCH 17/61] permission-hardener: unify mode anchoring across all field widths Fresh-claude follow-up: the field_count==4/5 existence override did not cover a 6-field line (spaced filename + numeric octal-looking owner + capability), which anchored the mode on the owner and silently dropped the entry. No hardening bypass -- numeric owners are rejected later against passwd names -- but a silent drop instead of a loud rejection, and the ==4 disjunct was dead code. Replace the special-case override with one existence-based loop: among the octal candidates at field_count-3 and field_count-4, pick the one whose recovered filename exists; a mode at field 1 means no-space (no recovery); fall back to the rightmost octal candidate when neither name exists. Handles the 3-field, 4-field, spaced, numeric-owner and 6-field cases uniformly. - Regression test: 6-field spaced numeric-owner case; the suite now distinguishes this fix from all three prior parser iterations. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_whitespace_filename.sh | 10 ++++- .../permission-hardener#security-misc-shared | 40 ++++++++++--------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index def1f2a3..0743dd3a 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -63,6 +63,13 @@ touch -- "${numeric_owner_file}" octal_second_file="${test_dir}/a 744" touch -- "${octal_second_file}" +## Six fields: a SPACED filename plus a numeric octal-looking owner and a +## capability tail. The rightmost octal candidate (field_count-3) is the owner, +## not the mode; anchoring there folds the mode into the filename and drops the +## entry. Existence must select field_count-4 (the real mode) instead. +numeric_owner_spaced_file="${test_dir}/x y" +touch -- "${numeric_owner_spaced_file}" + config_dir="/etc/permission-hardener.d" config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" mkdir -p -- "${config_dir}" @@ -81,6 +88,7 @@ trap cleanup EXIT printf '%s\n' "${octal_chunk_file} 0744 root root" printf '%s\n' "${numeric_owner_file} 0744 0755 root cap_net_raw" printf '%s\n' "${octal_second_file} 0644 root root" + printf '%s\n' "${numeric_owner_spaced_file} 0744 0755 root cap_net_raw" } > "${config_file}" ## A parse failure on any line aborts the whole run with exit 200; capture the @@ -100,7 +108,7 @@ fi ## print-policy prints tab-separated columns (FileUser...), so match ## each filename as the whole first field (trailing TAB). A bare substring match ## would let '/a 744' spuriously match the '/a 744 name' entry. -for expected_file in "${spaced_file}" "${octal_chunk_file}" "${numeric_owner_file}" "${octal_second_file}"; do +for expected_file in "${spaced_file}" "${octal_chunk_file}" "${numeric_owner_file}" "${octal_second_file}" "${numeric_owner_spaced_file}"; do if printf '%s\n' "${policy_output}" | grep -qF -- "${expected_file}"$'\t'; then printf '%s\n' "PASS: filename '${expected_file}' parsed and present in policy." else diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index f17c55a1..7e794682 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -430,7 +430,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local field_count recovered_file_name mode_index + local field_count recovered_file_name mode_index fallback_index candidate_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -500,27 +500,29 @@ load_state() { field_list=( "${recovered_file_name}" "${field_list[field_count-1]}" ) ;; *) - ## Prefer the 3-field reading (field_count-3) so a filename ending - ## in an octal chunk is not mistaken for a 4-field capability tail. + ## The octal mode sits at field_count-3 (3-field tail) or + ## field_count-4 (4-field tail with capability). Both positions can + ## look octal -- a numeric owner, or a filename component -- so pick + ## the candidate whose recovered filename exists on disk (the tool + ## skips nonexistent targets anyway). A mode at field 1 means a + ## single-field, no-space filename: nothing to recover. Fall back to + ## the first (rightmost) octal candidate when neither name exists. mode_index='' - if (( field_count >= 4 )) && [[ "${field_list[field_count-3]}" =~ ^0?[0-7]{3,4}$ ]]; then - mode_index="$(( field_count - 3 ))" - elif (( field_count >= 5 )) && [[ "${field_list[field_count-4]}" =~ ^0?[0-7]{3,4}$ ]]; then - mode_index="$(( field_count - 4 ))" - fi + fallback_index='' + for candidate_index in "$(( field_count - 3 ))" "$(( field_count - 4 ))"; do + (( candidate_index >= 1 )) || continue + [[ "${field_list[candidate_index]}" =~ ^0?[0-7]{3,4}$ ]] || continue + [ -z "${fallback_index}" ] && fallback_index="${candidate_index}" + recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:candidate_index}")" + if (( candidate_index == 1 )) || [ -e "${recovered_file_name}" ]; then + mode_index="${candidate_index}" + break + fi + done + [ -z "${mode_index}" ] && mode_index="${fallback_index}" if [ -n "${mode_index}" ] && (( mode_index > 1 )); then recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:mode_index}")" - ## Keep the no-recovery reading when field 1 is an octal mode and - ## only the un-recovered name exists (field 1 is the mode, not a - ## filename component). - if (( field_count == 4 || field_count == 5 )) \ - && [[ "${field_list[1]}" =~ ^0?[0-7]{3,4}$ ]] \ - && [ ! -e "${recovered_file_name}" ] \ - && [ -e "${field_list[0]}" ]; then - true 'DEBUG: field 1 is the mode; no filename recovery' - else - field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) - fi + field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) fi ;; esac From f2c1e7517cf8a8017db91ef7cf42784e93e14ae8 Mon Sep 17 00:00:00 2001 From: "claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4)" Date: Fri, 14 Aug 2026 13:28:37 -0400 Subject: [PATCH 18/61] permission-hardener: read spaced paths back from the state DB Fresh-claude review of the whitespace feature found the write/read sides disagree: parsing now accepts space-containing filenames into the policy, but load_state_without_policy split the dpkg-statoverride DB line on whitespace and required exactly 4 fields, silently dropping any spaced path. A spaced SUID file then got hardened but could never be un-hardened ('disable' never found it), and repeated 'enable' failed with exit 203. - Read the three fixed fields (owner group mode) and take the remainder as the path, matching the DB's 'owner group mode path' format. - Regression test test_state_spaced_filename.sh: seed a spaced entry, disable, assert the mode is restored (fails on the old reader: file stays un-restored). - Test hygiene: print-policy captures modes into the state DB, so the two print-policy tests now remove the entries they create instead of leaving stale root-owned overrides for deleted temp files. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_state_spaced_filename.sh | 68 +++++++++++++++++++ .../test_whitelists_disable_all.sh | 7 ++ .../test_whitespace_filename.sh | 12 ++++ .../permission-hardener#security-misc-shared | 19 +++--- 4 files changed, 98 insertions(+), 8 deletions(-) create mode 100755 ci/tests/permission_hardener/test_state_spaced_filename.sh diff --git a/ci/tests/permission_hardener/test_state_spaced_filename.sh b/ci/tests/permission_hardener/test_state_spaced_filename.sh new file mode 100755 index 00000000..520e6e4d --- /dev/null +++ b/ci/tests/permission_hardener/test_state_spaced_filename.sh @@ -0,0 +1,68 @@ +#!/bin/bash +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Regression test: a hardened file whose path contains spaces must be +## un-hardenable via 'disable'. +## +## load_state_without_policy() reads the dpkg-statoverride state DB, one entry +## per line as 'owner group mode path'. A plain 4-field split drops any spaced +## path, so 'disable' never finds the entry and the file can never be restored. +## +## Seeds one spaced entry (mode 0744) into the state DB, sets the file to 0600, +## runs 'disable', and asserts the mode is restored to 0744 -- which only +## happens if load_state_without_policy read the spaced path back. +## +## Requires root: writes the state DB and chmod/chowns the target file. + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)" +ph_bin="${PERMISSION_HARDENER_BIN:-${repo_root}/usr/bin/permission-hardener#security-misc-shared}" + +if [ ! -f "${ph_bin}" ]; then + printf '%s\n' "FAIL: permission-hardener not found at '${ph_bin}'." >&2 + exit 1 +fi + +## Must match store_dir in the script under test. +existing_mode_admindir='/var/lib/permission-hardener-v2/existing_mode' +mkdir -p -- "${existing_mode_admindir}" + +test_dir="$(mktemp -d -t ph-state-test.XXXXXX)" +spaced_file="${test_dir}/spaced tool" +touch -- "${spaced_file}" +chmod 0600 -- "${spaced_file}" + +## invoked indirectly via 'trap ... EXIT' +# shellcheck disable=SC2317 +cleanup() { + ## undo touches the master and new_mode DBs, not existing_mode, so remove the + ## seeded entry explicitly. + dpkg-statoverride --admindir "${existing_mode_admindir}" --remove \ + "${spaced_file}" >/dev/null 2>&1 || true + safe-rm -rf -- "${test_dir}" +} +trap cleanup EXIT + +## Seed the state DB: this is the record 'enable' would have written. +dpkg-statoverride --admindir "${existing_mode_admindir}" --add \ + root root 0744 "${spaced_file}" + +"${ph_bin}" disable "${spaced_file}" >/dev/null 2>&1 || true + +actual_mode="$(stat -c '%a' -- "${spaced_file}")" +if [ "${actual_mode}" = '744' ]; then + printf '%s\n' "PASS: spaced-path state entry read back; mode restored to 744." + exit 0 +fi + +printf '%s\n' "FAIL: mode is '${actual_mode}', expected '744' -- spaced state entry not read back, file cannot be un-hardened." >&2 +exit 1 diff --git a/ci/tests/permission_hardener/test_whitelists_disable_all.sh b/ci/tests/permission_hardener/test_whitelists_disable_all.sh index 0957a6b2..d3727b83 100755 --- a/ci/tests/permission_hardener/test_whitelists_disable_all.sh +++ b/ci/tests/permission_hardener/test_whitelists_disable_all.sh @@ -42,9 +42,16 @@ config_dir="/etc/permission-hardener.d" config_file="${config_dir}/zz-ai-whitelists-disable-all-test.conf" mkdir -p -- "${config_dir}" +## print-policy captures the file's current mode into this DB (must match +## store_dir in the script under test); remove the entry so a test run does not +## leave a stale root-owned override pointing at a deleted temp file. +existing_mode_admindir='/var/lib/permission-hardener-v2/existing_mode' + ## invoked indirectly via 'trap ... EXIT' # shellcheck disable=SC2317 cleanup() { + dpkg-statoverride --admindir "${existing_mode_admindir}" --remove \ + "${ordinary_file}" >/dev/null 2>&1 || true safe-rm -f -- "${config_file}" safe-rm -rf -- "${test_dir}" } diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index 0743dd3a..2011edff 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -74,9 +74,21 @@ config_dir="/etc/permission-hardener.d" config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" mkdir -p -- "${config_dir}" +## print-policy captures each file's current mode into this DB (must match +## store_dir in the script under test); remove the entries so a test run does +## not leave stale root-owned overrides pointing at deleted temp files. +existing_mode_admindir='/var/lib/permission-hardener-v2/existing_mode' + ## invoked indirectly via 'trap ... EXIT' # shellcheck disable=SC2317 cleanup() { + local leaked_file + for leaked_file in "${spaced_file}" "${octal_chunk_file}" \ + "${numeric_owner_file}" "${octal_second_file}" \ + "${numeric_owner_spaced_file}"; do + dpkg-statoverride --admindir "${existing_mode_admindir}" --remove \ + "${leaked_file}" >/dev/null 2>&1 || true + done safe-rm -f -- "${config_file}" safe-rm -rf -- "${test_dir}" } diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 7e794682..342a0649 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -400,21 +400,24 @@ load_late_nosuid_policy() { } load_state_without_policy() { - local line field_list + local line sf_owner sf_group sf_mode sf_path - ## Load the state file from disk + ## Load the state file from disk. Each dpkg-statoverride entry is + ## 'owner group mode path'; the path is last and may contain spaces, so read + ## the three fixed fields and take the remainder as the path. A plain 4-way + ## split would drop any spaced path -- it could then never be un-hardened. if [ -f "${state_file}" ]; then while read -r line; do - read -r -a field_list <<< "${line}" - if (( ${#field_list[@]} != 4 )); then + read -r sf_owner sf_group sf_mode sf_path <<< "${line}" + if [ -z "${sf_path}" ]; then log info \ "Invalid number of fields in state file line: '${line}'. Skipping." continue fi - state_user_owner_list+=( "${field_list[0]}" ) - state_group_owner_list+=( "${field_list[1]}" ) - state_mode_list+=( "${field_list[2]}" ) - state_file_list+=( "${field_list[3]}" ) + state_user_owner_list+=( "${sf_owner}" ) + state_group_owner_list+=( "${sf_group}" ) + state_mode_list+=( "${sf_mode}" ) + state_file_list+=( "${sf_path}" ) done < "${state_file}" fi } From 1c9f144b9ebe58daa174de05f5b0c0e42038bfc1 Mon Sep 17 00:00:00 2001 From: "claude (dev272 e8b76afb-795c-4739-8ebd-023de854021e)" Date: Fri, 21 Aug 2026 04:41:27 -0400 Subject: [PATCH 19/61] security-misc: source helper-scripts via HELPER_SCRIPTS_PATH, drop SC1091 disable - check-for-usb-controller, permission-hardener, lkrg-virtualbox: use source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/; drop the dead '# shellcheck disable=SC1091' and the stale sibling note. - completed pre-existing whole-file style debt: R-010 export LC_ALL=C in the strict-mode block (dropping the duplicate single-quoted mid-file one). Co-Authored-By: Claude Opus 4.8 (1M context) --- usr/bin/permission-hardener#security-misc-shared | 13 +++++++------ .../check-for-usb-controller#security-misc-shared | 8 ++------ .../lkrg/lkrg-virtualbox#security-misc-shared | 6 ++---- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 342a0649..0bdb997d 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -20,6 +20,7 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose +export LC_ALL=C ## Constants # shellcheck disable=SC2034 @@ -31,12 +32,12 @@ dpkg_admindir_parameter_new_mode="--admindir ${store_dir}/new_mode" delimiter="#permission-hardener-delimiter#" ## Library imports -# shellcheck disable=SC1091 -source /usr/libexec/helper-scripts/safe_echo.sh -# shellcheck disable=SC1091 -source /usr/libexec/helper-scripts/log_run_die.sh -# shellcheck disable=SC1091 -source /usr/libexec/helper-scripts/strings.bsh +# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/safe_echo.sh +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/safe_echo.sh +# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/log_run_die.sh +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/log_run_die.sh +# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/strings.bsh +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/strings.bsh ## Functions echo_wrapper_ignore() { diff --git a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared index aa70e39a..6ba31508 100755 --- a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared +++ b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared @@ -9,15 +9,11 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose +export LC_ALL=C ## security-misc-shared Depends on helper-scripts. # shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh -## Sibling repo: absent in an isolated CI checkout, so shellcheck cannot -## follow it there. The source= path above still documents where it lives. -# shellcheck disable=SC1091 -source /usr/libexec/helper-scripts/has.sh - -export LC_ALL='C' +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.sh ## Package 'pciutils' provides tool 'lspci'. has lspci diff --git a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared index e000b8cf..fb18eb61 100755 --- a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared +++ b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared @@ -10,13 +10,11 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose +export LC_ALL=C ## security-misc-shared Depends on helper-scripts. # shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh -## Sibling repo: absent in an isolated CI checkout, so shellcheck cannot -## follow it there. The source= path above still documents where it lives. -# shellcheck disable=SC1091 -source /usr/libexec/helper-scripts/has.sh +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.sh ## Check if the VirtualBox host software is installed. if ! has vboxmanage ; then From e6a81266555130673dc7d8bcb85ea50bbad9e852 Mon Sep 17 00:00:00 2001 From: "assisted-by-ai (Bot Account) (Machine Account)" <228987186+assisted-by-ai@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:31:16 -0400 Subject: [PATCH 20/61] ci: drop orphaned consumer-claude-code.yml (reusable decommissioned) --- .github/workflows/consumer-claude-code.yml | 85 ---------------------- 1 file changed, 85 deletions(-) delete mode 100644 .github/workflows/consumer-claude-code.yml diff --git a/.github/workflows/consumer-claude-code.yml b/.github/workflows/consumer-claude-code.yml deleted file mode 100644 index a81d2f7f..00000000 --- a/.github/workflows/consumer-claude-code.yml +++ /dev/null @@ -1,85 +0,0 @@ -## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## AI-Assisted - -## Managed by pkg_update_consumer_workflows. Byte-identical -## across consumers; this file is `cp`-ed from -## developer-meta-files/consumer-templates/.github/workflows/consumer-claude-code.yml. -## DO NOT hand-edit this file in the consumer repo - changes -## will be overwritten on the next propagation pass. -## -## Reusable docs: -## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-claude-code-review.yml - -name: Claude Code Review - -## Mention-driven only. claude-code-action picks its mode from the -## event: comment events with no 'prompt' input select tag mode, -## where the '@claude' phrase is the trigger and the model runs. -## Every other event (pull_request, workflow_dispatch, schedule) -## selects agent mode, whose trigger is a non-empty 'prompt' - which -## this workflow does not pass - so those events can only spin up a -## runner and exit without calling the model. Listing them here -## would put a green "Claude Code Review" check on every PR that -## never reviewed anything. See the reusable's header for the full -## rationale and for what enabling real auto-review would cost. -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -permissions: - contents: read - -## The two comment events differ: issue_comment fires with -## github.ref = the default branch, pull_request_review_comment with -## the PR merge ref (refs/pull//merge). Neither is the PR head -## ref, and plain ${{ github.ref }} would queue unrelated PRs' -## comment-triggered runs into the same group. The PR/issue-number -## chain below is what actually applies: one of those two number -## fields is populated for every event this workflow accepts, so -## github.ref is a fallback that is never reached here. -## -## Bot-vs-human split via the trailing actor-class suffix: when the -## Claude action posts a review comment, it does so as claude[bot], -## which fires this workflow again. Without the split, that -## bot-triggered run would land in the same concurrency group as the -## in-flight human @claude request, and 'cancel-in-progress: true' -## below would cancel the review mid-flight before any output -## reached the PR. The job-level 'if:' filter further down catches -## the bot run (claude[bot]'s own comment body does not contain -## '@claude'), but by then concurrency has already done the damage. -## GitHub's standard '[bot]' suffix on App accounts (claude[bot], -## github-advanced-security[bot], dependabot[bot], ...) lets a single -## endsWith() check isolate all bot-authored events into a separate -## group so they only cancel each other. -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}-${{ endsWith(github.actor, '[bot]') && 'bot' || 'human' }} - cancel-in-progress: true - -jobs: - review: - ## Pre-filter for the '@claude' substring before the runner spins - ## up. The reusable's job-level 'if:' enforces the same gate plus - ## the allowed-users roster check, but doing the cheap substring - ## test here too prevents a runner from starting (and stopping - ## seconds later) for every unrelated comment on every issue/PR. - if: contains(github.event.comment.body, '@claude') - uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-claude-code-review.yml@master - ## Explicit secret forwarding (NOT 'secrets: inherit'). With - ## inherit, every caller-side secret would flow into the - ## reusable; the explicit map caps the reusable's secrets - ## context to exactly CLAUDE_CODE_OAUTH_TOKEN. - secrets: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - ## 'contents: read' does NOT bound what Claude can do - the - ## action swaps the runner token for an OIDC-minted claude[bot] - ## App token carrying contents/issues/pull-requests write. The - ## block is required for 'id-token: write'. See the reusable. - permissions: - contents: read - pull-requests: write - issues: write - id-token: write From a6d7a3585f8905bc3f898d8dd83c3b07cb04cba9 Mon Sep 17 00:00:00 2001 From: "assisted-by-ai (Bot Account) (Machine Account)" <228987186+assisted-by-ai@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:31:19 -0400 Subject: [PATCH 21/61] ci: drop orphaned consumer-codex-review.yml (reusable decommissioned) --- .github/workflows/consumer-codex-review.yml | 46 --------------------- 1 file changed, 46 deletions(-) delete mode 100644 .github/workflows/consumer-codex-review.yml diff --git a/.github/workflows/consumer-codex-review.yml b/.github/workflows/consumer-codex-review.yml deleted file mode 100644 index f333728c..00000000 --- a/.github/workflows/consumer-codex-review.yml +++ /dev/null @@ -1,46 +0,0 @@ -## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## AI-Assisted - -## Managed by pkg_update_consumer_workflows. Byte-identical -## across consumers; this file is `cp`-ed from -## developer-meta-files/consumer-templates/.github/workflows/consumer-codex-review.yml. -## DO NOT hand-edit this file in the consumer repo - changes -## will be overwritten on the next propagation pass. -## -## Reusable docs: -## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-codex-review.yml - -name: OpenAI Codex Review - -on: - pull_request: - branches: [master] - workflow_dispatch: - inputs: - pr_ref: - description: | - Ref of the fork PR to review. - Use `refs/pull//merge` (most accurate; mirrors what - would land), or `refs/pull//head` for the PR head. - A plain branch name in the upstream repo also works for - non-fork PRs. - required: true - type: string - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - review: - uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-codex-review.yml@master - with: - pr-ref: ${{ inputs.pr_ref || '' }} - permissions: - contents: read - pull-requests: write From 908cadc6177c00d4ff3465b33b20b3c971ce246a Mon Sep 17 00:00:00 2001 From: "assisted-by-ai (Bot Account) (Machine Account)" <228987186+assisted-by-ai@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:31:21 -0400 Subject: [PATCH 22/61] ci: drop orphaned consumer-secrets-audit.yml (reusable decommissioned) --- .github/workflows/consumer-secrets-audit.yml | 41 -------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/consumer-secrets-audit.yml diff --git a/.github/workflows/consumer-secrets-audit.yml b/.github/workflows/consumer-secrets-audit.yml deleted file mode 100644 index 579f9915..00000000 --- a/.github/workflows/consumer-secrets-audit.yml +++ /dev/null @@ -1,41 +0,0 @@ -## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## AI-Assisted - -## Managed by pkg_update_consumer_workflows. Byte-identical -## across consumers; this file is `cp`-ed from -## developer-meta-files/consumer-templates/.github/workflows/consumer-secrets-audit.yml. -## DO NOT hand-edit this file in the consumer repo - changes -## will be overwritten on the next propagation pass. -## -## Manual-trigger entry point. Run from the Actions tab: -## 'Secrets surface audit' > 'Run workflow'. Use as a sanity -## check after changing any reusable's 'workflow_call.secrets' -## schema or any consumer's 'secrets:' map. -## -## 'secrets: inherit' here forwards the entire repo-level + -## org-level secret set into the audit reusable, which prints -## presence flags. The audit boolean is resolved at -## expression-evaluation time, so secret values themselves never -## land in env or step output. -## -## Reusable docs: -## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-secrets-audit.yml - -name: Secrets surface audit - -on: - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - audit: - uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-secrets-audit.yml@master - secrets: inherit From cc7a93d10a30e19631c64437b5c6078ba5d74f77 Mon Sep 17 00:00:00 2001 From: "claude (dev352 385b930f-4313-4883-93d5-e6857c3f7ea7)" Date: Thu, 27 Aug 2026 11:21:51 -0400 Subject: [PATCH 23/61] lintian: override false-positive maintainer-script-ignores-errors The postinst enables errexit via long-form 'set -o errexit' (project style, R-013); lintian's check only matches short 'set -e'. Same override tb-updater already carries. --- debian/security-misc-shared.lintian-overrides | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 debian/security-misc-shared.lintian-overrides diff --git a/debian/security-misc-shared.lintian-overrides b/debian/security-misc-shared.lintian-overrides new file mode 100644 index 00000000..624de4d4 --- /dev/null +++ b/debian/security-misc-shared.lintian-overrides @@ -0,0 +1,4 @@ +## The postinst enables errexit via the long-form 'set -o errexit'. lintian's +## scripts check only matches the short 'set -e', so it wrongly reports the +## script as ignoring errors. False positive (same as tb-updater's override). +security-misc-shared: maintainer-script-ignores-errors [postinst] From bb399ba73704e4c20fbc6965d13bee4f2c63626c Mon Sep 17 00:00:00 2001 From: "claude (dev352 385b930f-4313-4883-93d5-e6857c3f7ea7)" Date: Thu, 27 Aug 2026 12:07:58 -0400 Subject: [PATCH 24/61] lintian: override maintainer-script-ignores-errors in the shipped file The postinst uses long-form 'set -o errexit' (style R-013); lintian's scripts check only matches short 'set -e'. Add the false-positive override to the package's own usr/share/lintian/overrides file -- a separate debian/*.lintian-overrides displaces it (dh_lintian installs to the same path), losing the etc-skel and other overrides it carries. --- debian/security-misc-shared.lintian-overrides | 4 ---- .../overrides/security-misc-shared#security-misc-shared | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 debian/security-misc-shared.lintian-overrides diff --git a/debian/security-misc-shared.lintian-overrides b/debian/security-misc-shared.lintian-overrides deleted file mode 100644 index 624de4d4..00000000 --- a/debian/security-misc-shared.lintian-overrides +++ /dev/null @@ -1,4 +0,0 @@ -## The postinst enables errexit via the long-form 'set -o errexit'. lintian's -## scripts check only matches the short 'set -e', so it wrongly reports the -## script as ignoring errors. False positive (same as tb-updater's override). -security-misc-shared: maintainer-script-ignores-errors [postinst] diff --git a/usr/share/lintian/overrides/security-misc-shared#security-misc-shared b/usr/share/lintian/overrides/security-misc-shared#security-misc-shared index 1dad42d7..dc9c90a0 100644 --- a/usr/share/lintian/overrides/security-misc-shared#security-misc-shared +++ b/usr/share/lintian/overrides/security-misc-shared#security-misc-shared @@ -21,3 +21,7 @@ security-misc-shared: systemd-service-file-missing-install-key [usr/lib/systemd/ ## Minor bug: Unknown. security-misc-shared: package-contains-empty-directory [usr/share/security-misc-shared/] + +## The postinst enables errexit via long-form 'set -o errexit'; lintian's +## scripts check only matches short 'set -e'. False-positive (like tb-updater). +security-misc-shared: maintainer-script-ignores-errors [postinst] From 87506b1eaf45fa7501d54cf83b9be13578f86ecb Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Fri, 28 Aug 2026 01:03:22 -0400 Subject: [PATCH 25/61] Polish, add TODOs --- .../local-permission-hardener-test.yml | 5 + ci/dfuzzer-build.sh | 5 +- .../test_state_spaced_filename.sh | 2 + .../test_whitelists_disable_all.sh | 2 + .../test_whitespace_filename.sh | 2 + debian/security-misc-shared.postinst | 25 ++-- .../permission-hardener#security-misc-shared | 119 +++++++----------- usr/bin/remount-secure#security-misc-shared | 6 +- .../module-setup.sh#security-misc-shared | 2 +- ...lt_whitelist_pam.conf#security-misc-shared | 6 +- ...hitelist_postfix.conf#security-misc-shared | 9 +- ..._whitelist_qubes.conf#security-misc-shared | 18 +-- ...hitelist_selinux.conf#security-misc-shared | 19 --- ..._whitelist_spice.conf#security-misc-shared | 12 +- ...elist_virtualbox.conf#security-misc-shared | 16 +-- .../30_default.conf#security-misc-shared | 20 +-- .../build-emerg-shutdown#security-misc-shared | 9 -- ...ompile-emerg-shutdown#security-misc-shared | 14 +-- .../emerg-shutdown#security-misc-shared | 13 -- .../mmap-rnd-bits#security-misc-shared | 2 +- ...pam_faillock_not_if_x#security-misc-shared | 7 -- .../pam_only_if_login#security-misc-shared | 6 - .../security-misc-shared#security-misc-shared | 3 +- .../lkrg/lkrg-virtualbox#security-misc-shared | 1 - .../fm-shim-backend.c#security-misc-shared | 15 +-- 25 files changed, 97 insertions(+), 241 deletions(-) delete mode 100644 usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared diff --git a/.github/workflows/local-permission-hardener-test.yml b/.github/workflows/local-permission-hardener-test.yml index bbc05186..e1d7e320 100644 --- a/.github/workflows/local-permission-hardener-test.yml +++ b/.github/workflows/local-permission-hardener-test.yml @@ -11,6 +11,11 @@ ## space is parsed as one entry instead of being split and silently ## dropped. See ci/tests/permission_hardener/test_whitespace_filename.sh. +## FIXME: The actual scripts run here should be in dist-ai. + +## WARNING: The new test scripts will not be reviewed by humans as they are +## slated to be moved to dist-ai. This workflow file has been reviewed. + name: Test permission-hardener on: diff --git a/ci/dfuzzer-build.sh b/ci/dfuzzer-build.sh index 790419b2..607fe9ad 100755 --- a/ci/dfuzzer-build.sh +++ b/ci/dfuzzer-build.sh @@ -44,15 +44,12 @@ if [ "${CI:-}" != "true" ] && [ "${ALLOW_LOCAL:-}" != "true" ]; then fi DFUZZER_TAG="${DFUZZER_TAG:-v2.6}" -## Pin the exact upstream commit. A tag is mutable; verifying the -## resolved commit hash detects a re-pointed tag (supply-chain guard). -## Keep in sync with DFUZZER_TAG on every version bump. DFUZZER_COMMIT="${DFUZZER_COMMIT:-a955a80f7dd20fd7aeffad91da1c495aab5dbbd3}" git clone --depth 1 --branch "${DFUZZER_TAG}" \ https://github.com/dbus-fuzzer/dfuzzer /tmp/dfuzzer -dfuzzer_actual_commit="$(git -C /tmp/dfuzzer rev-parse HEAD)" +dfuzzer_actual_commit="$(git -C /tmp/dfuzzer rev-parse --verify HEAD)" if [ "${dfuzzer_actual_commit}" != "${DFUZZER_COMMIT}" ]; then printf '%s\n' "${BASH_SOURCE[0]}: dfuzzer tag ${DFUZZER_TAG} resolved to ${dfuzzer_actual_commit}, expected ${DFUZZER_COMMIT} -- refusing (tag moved?)." >&2 exit 1 diff --git a/ci/tests/permission_hardener/test_state_spaced_filename.sh b/ci/tests/permission_hardener/test_state_spaced_filename.sh index 520e6e4d..bfd77155 100755 --- a/ci/tests/permission_hardener/test_state_spaced_filename.sh +++ b/ci/tests/permission_hardener/test_state_spaced_filename.sh @@ -4,6 +4,8 @@ ## AI-Assisted +## WARNING: Not human-reviewed, MUST be moved to dist-ai. + ## Regression test: a hardened file whose path contains spaces must be ## un-hardenable via 'disable'. ## diff --git a/ci/tests/permission_hardener/test_whitelists_disable_all.sh b/ci/tests/permission_hardener/test_whitelists_disable_all.sh index d3727b83..0044f796 100755 --- a/ci/tests/permission_hardener/test_whitelists_disable_all.sh +++ b/ci/tests/permission_hardener/test_whitelists_disable_all.sh @@ -4,6 +4,8 @@ ## AI-Assisted +## WARNING: Not human-reviewed, MUST be moved to dist-ai. + ## Regression test: the documented special directive 'whitelists_disable_all=true' ## must be honored, not rejected. ## diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index 2011edff..4234b67b 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -4,6 +4,8 @@ ## AI-Assisted +## WARNING: Not human-reviewed, MUST be moved to dist-ai. + ## Regression test: permission-hardener must parse a config filename that ## contains spaces. ## diff --git a/debian/security-misc-shared.postinst b/debian/security-misc-shared.postinst index 1d304e2d..1ea1633e 100755 --- a/debian/security-misc-shared.postinst +++ b/debian/security-misc-shared.postinst @@ -3,9 +3,8 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## style-ok: allow-echo (Debian maintainer-script convention) -## style-ok: no-has (has.sh not reliably available this early in maintainer scripts) -## style-ok: no-strict (debconf maintainer script; full strict block would leak into APT/debconf) +## style-ok: no-has - has.sh isn't reliably available in maintainer scripts +## style-ok: no-strict - full strict block could leak into APT/debconf if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh @@ -31,16 +30,16 @@ permission_hardening_legacy_config_folder() { } permission_hardening() { - echo "Running SUID Disabler and Permission Hardener... See also:" - echo "https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener" - echo "$0: INFO: running: permission-hardener enable" + printf '%s\n' "Running SUID Disabler and Permission Hardener... See also:" + printf '%s\n' "https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener" + printf '%s\n' "$0: INFO: running: permission-hardener enable" ## By design: report error but return 0 to avoid breaking APT. ## A failing postinst would leave the package in a broken state. if ! permission-hardener enable ; then - echo "$0: ERROR: Permission hardening failed." >&2 + printf '%s\n' "$0: ERROR: Permission hardening failed." >&2 return 0 fi - echo "$0: INFO: Permission hardening success." + printf '%s\n' "$0: INFO: Permission hardening success." } fix_pkexec_remembered_permissions() { @@ -173,9 +172,7 @@ case "$1" in ## Build the org.freedesktop.FileManager1 shim backend. build_fm_shim_backend - ## Compile emerg-shutdown into /usr/bin (on-target, so an - ## Architecture: all package still gets a correct per-arch binary); - ## the boot-time script then only copies it into /run. + ## Pre-compile emerg-shutdown. build_emerg_shutdown ;; @@ -183,7 +180,7 @@ case "$1" in ;; triggered) - echo "INFO: triggered ${DPKG_MAINTSCRIPT_PACKAGE}: '${DPKG_MAINTSCRIPT_PACKAGE}' ${DPKG_MAINTSCRIPT_PACKAGE} DPKG_MAINTSCRIPT_NAME: '${DPKG_MAINTSCRIPT_NAME}' $\*: '$*' 2: '$2'" + printf '%s\n' "INFO: triggered ${DPKG_MAINTSCRIPT_PACKAGE}: '${DPKG_MAINTSCRIPT_PACKAGE}' ${DPKG_MAINTSCRIPT_PACKAGE} DPKG_MAINTSCRIPT_NAME: '${DPKG_MAINTSCRIPT_NAME}' $\*: '$*' 2: '$2'" /usr/share/security-misc/lkrg/lkrg-virtualbox || true /usr/libexec/security-misc/mmap-rnd-bits || true permission_hardening @@ -191,7 +188,7 @@ case "$1" in ;; *) - echo "${DPKG_MAINTSCRIPT_NAME} called with unknown argument \`$1'" >&2 + printf '%s\n' "${DPKG_MAINTSCRIPT_NAME} called with unknown argument \`$1'" >&2 exit 1 ;; esac @@ -207,7 +204,7 @@ permission_hardening ## https://bugs.debian.org/481542 if command -v update-grub >/dev/null 2>&1; then update-grub || \ - echo "${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} ERROR: Running \ + printf '%s\n' "${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} ERROR: Running \ 'update-grub' failed with exit code $?. ${DPKG_MAINTSCRIPT_PACKAGE} is most \ likely only the trigger, not the cause. Unless you know this is not an issue, \ you should fix running 'update-grub', otherwise your system might no longer \ diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 0bdb997d..6d061dfd 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -403,10 +403,7 @@ load_late_nosuid_policy() { load_state_without_policy() { local line sf_owner sf_group sf_mode sf_path - ## Load the state file from disk. Each dpkg-statoverride entry is - ## 'owner group mode path'; the path is last and may contain spaces, so read - ## the three fixed fields and take the remainder as the path. A plain 4-way - ## split would drop any spaced path -- it could then never be un-hardened. + ## Load the state file from disk. if [ -f "${state_file}" ]; then while read -r line; do read -r sf_owner sf_group sf_mode sf_path <<< "${line}" @@ -434,7 +431,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local field_count recovered_file_name mode_index fallback_index candidate_index + local one_field field_count fso_name meta_list field_index hit_barrier ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -458,9 +455,6 @@ load_state() { continue fi - ## Recognize the special directive before the character-class check below, - ## whose allowed set excludes '=' and would otherwise reject and abort on - ## this documented line. if [ "${line}" = 'whitelists_disable_all=true' ]; then whitelists_disable_all=true log info "whitelists_disable_all=true" @@ -477,63 +471,42 @@ load_state() { processed_config_line="${line}" - IFS=' ' read -r -a field_list <<< "${line}" - - ## A filename may contain spaces. The option grammar is unambiguous at the - ## END of a line -- either a single trailing whitelist keyword, or a - ## mode/owner/group[/capability] tail whose first field is the octal mode - ## -- so recover the filename by parsing options from the right and - ## rejoining everything before them. Space-free lines are left unchanged. - ## The mode tail has a fixed width -- 3 fields (mode owner group) or 4 - ## (mode owner group capability) -- so anchor the mode from the RIGHT. A - ## left-to-right scan misanchors when a space-delimited filename chunk is - ## itself a bare octal (e.g. '/path/a 744 name 0744 root root'). - ## Two readings can be lexically valid when a filename component OR a - ## numeric owner looks octal ('/a 744 0644 root root' wants the spaced - ## name; '/foo 0744 1000 root cap' wants no recovery). Disambiguate by - ## which candidate exists on disk -- the tool skips nonexistent targets - ## anyway -- keeping the no-recovery reading when field 1 is really the mode. - ## Limitation: consecutive spaces collapse and quoting/escaping is not - ## supported; a line whose field count is still invalid after recovery is - ## rejected loudly (exit 200 below), not silently skipped. + ## We can handle filenames with spaces. This is because the first item + ## after a filename is an octal file mode, and we only support user/group + ## names, not UIDs and GIDs, as owner identifiers. This means that we can + ## scan from right to left for the furthest-right octal integer field and + ## use it to separate the filename from the rest of the options. (If + ## usernames could themselves be octal integers, this would break, but + ## adduser.conf's default NAME_REGEX forbids this. This is definitely a + ## hack, but the alternative is to change the format of the policy files, + ## which would break custom config for users. + ## + ## 'read' will collapse consecutive spaces when splitting on spaces. + ## Convert spaces into newlines first, then split on newlines with a + ## loop. + field_list=() + while read IFS= -r one_field; do + field_list+=( "${one_field}" ) + done < <(printf '%s\n' "${line}" | tr ' ' '\n') field_count="${#field_list[@]}" - if (( field_count > 2 )); then - case "${field_list[field_count-1]}" in - exactwhitelist|matchwhitelist|disablewhitelist|nosuid) - recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:field_count-1}")" - field_list=( "${recovered_file_name}" "${field_list[field_count-1]}" ) - ;; - *) - ## The octal mode sits at field_count-3 (3-field tail) or - ## field_count-4 (4-field tail with capability). Both positions can - ## look octal -- a numeric owner, or a filename component -- so pick - ## the candidate whose recovered filename exists on disk (the tool - ## skips nonexistent targets anyway). A mode at field 1 means a - ## single-field, no-space filename: nothing to recover. Fall back to - ## the first (rightmost) octal candidate when neither name exists. - mode_index='' - fallback_index='' - for candidate_index in "$(( field_count - 3 ))" "$(( field_count - 4 ))"; do - (( candidate_index >= 1 )) || continue - [[ "${field_list[candidate_index]}" =~ ^0?[0-7]{3,4}$ ]] || continue - [ -z "${fallback_index}" ] && fallback_index="${candidate_index}" - recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:candidate_index}")" - if (( candidate_index == 1 )) || [ -e "${recovered_file_name}" ]; then - mode_index="${candidate_index}" - break - fi - done - [ -z "${mode_index}" ] && mode_index="${fallback_index}" - if [ -n "${mode_index}" ] && (( mode_index > 1 )); then - recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:mode_index}")" - field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) - fi - ;; - esac - fi - case "${#field_list[@]}" in - 2|4|5) + hit_barrier='false' + fso_name="" + meta_list=() + for (( field_index = field_count - 1; field_index <= 0; \ + field_index-- )); do + if [ "${hit_barrier}" = 'false' ]; then + if [[ "${field_list[field_index]}" =~ ^[0-7]{3,4}$ ]]; then + hit_barrier='true' + fi + meta_list=( "${field_list[field_index]}" "${meta_list[@]}" ) + else + fso_name="${field_list[field_index]}${fso_name:+" ${fso_name}"}" + fi + done + + case "${#meta_list[@]}" in + 1|3|4) true ;; *) @@ -544,29 +517,29 @@ load_state() { esac # Strip trailing slash if appropriate - field_list[0]="${field_list[0]%/}" + fso_name="${fso_name%/}" - case "${field_list[1]}" in + case "${meta_list[0]}" in 'exactwhitelist') - [ ! -e "${field_list[0]}" ] && continue - policy_exact_white_list+=( "${field_list[0]}" ) + [ ! -e "${fso_name}" ] && continue + policy_exact_white_list+=( "${fso_name}" ) continue ;; 'matchwhitelist') - policy_match_white_list+=( "${field_list[0]}" ) + policy_match_white_list+=( "${fso_name}" ) continue ;; 'disablewhitelist') - policy_disable_white_list+=( "${field_list[0]}" ) + policy_disable_white_list+=( "${fso_name}" ) continue ;; 'nosuid') - [ ! -e "${field_list[0]}" ] && continue - policy_nosuid_file_list+=( "${field_list[0]}" ) + [ ! -e "${fso_name}" ] && continue + policy_nosuid_file_list+=( "${fso_name}" ) ;; *) - [ ! -e "${field_list[0]}" ] && continue - add_to_policy "${field_list[@]}" + [ ! -e "${fso_name}" ] && continue + add_to_policy "${fso_name}" "${meta_list[@]}" ;; esac done < "${config_file}" diff --git a/usr/bin/remount-secure#security-misc-shared b/usr/bin/remount-secure#security-misc-shared index 55c2924e..5d1322ab 100755 --- a/usr/bin/remount-secure#security-misc-shared +++ b/usr/bin/remount-secure#security-misc-shared @@ -26,10 +26,10 @@ ## https://www.kicksecure.com/wiki/Dev/remount-secure ## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707 -## style-ok: no-has (runs in dracut where helper-scripts has.sh is unavailable; id itself may be absent) +## style-ok: no-has - may run in dracut without helper-scripts ## Mount helper functions (_boot, _sys, _usr, ...) are invoked indirectly via -## the dispatch in main(); shellcheck cannot see the calls. +## the dispatch in main(). # shellcheck disable=SC2317 #set -x @@ -120,7 +120,7 @@ parse_options() { break ;; -*) - printf '%s\n' "ERROR: unknown option: ${1}" >&2 + "${output_command[@]}" "ERROR: unknown option: ${1}" >&2 exit 1 ;; *) diff --git a/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared b/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared index a69c36f9..092b610f 100755 --- a/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared +++ b/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared @@ -3,7 +3,7 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## style-ok: no-strict (dracut sources this module; top-level strict mode would leak into dracut) +## style-ok: no-strict (sourced by dracut) ## called by dracut check() { diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared index d770a269..a851cbb6 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared @@ -19,9 +19,5 @@ ## ## Without this, LXQt fails to start with a dbus-launch error. ## -## Confirmed: installed SUID root, mode 4755. Takes NO command-line/user input -- -## derives the directory purely from the caller's real UID/GID. No upstream git -## repo and no known CVE. A source audit is worthwhile given it is SUID root, but -## risk is low given zero attacker-controlled input. -## TODO-HUMAN-DEVELOPER-ONLY: source audit of pam-tmpdir-helper (SUID root). +## TODO-HUMAN-DEVELOPER-ONLY: source audit pam-tmpdir-helper pam-tmpdir-helper matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared index 1e45a13c..e4508770 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared @@ -6,14 +6,7 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## postqueue(1) / postdrop(1): Postfix mail-submission helpers. Installed SGID -## group "postdrop" (Postfix setgid_group), NOT setuid root. The SGID bit lets -## an unprivileged user write into the group-writable maildrop queue and reach -## the Postfix daemon sockets, with no user-level privilege granted. Stripping -## SGID breaks local mail submission ("Permission denied" on maildrop; postqueue -## cannot reach the queue manager). SGID-group is already the minimal-privilege -## design; no notable current CVE tied to the bit. ## Ref: https://www.postfix.org/postconf.5.html (setgid_group) -## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SGID is a human trust decision. +## TODO-HUMAN-DEVELOPER-ONLY: research postqueue matchwhitelist postdrop matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared index 614aca7e..82be6d2e 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared @@ -6,19 +6,13 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## qfile-unpacker (qubes-core-agent-linux, qubes-rpc): receiver side of the -## inter-qube file copy (qvm-copy/qvm-move); reads the qrexec stream and unpacks -## incoming files into the target qube. SUID root because it must chroot() into -## the recipient dir and setfsuid()/drop to the unprivileged user before writing -## -- both need root, so SUID cannot simply be stripped. It parses attacker- -## influenced input from another qube => high-value local-EoP target. The issue -## below (#8633) is now CLOSED: upstream HARDENED the unpacker rather than -## dropping SUID -- qfile-unpacker (qubes-core-agent-linux) now refuses a UID -## change to anything but the caller's own UID, and libqubes-pure adds path, -## symlink and filename sanitization (qubes-linux-utils PR #87). Keep the -## package patched; residual risk is the parser itself. +## qfile-unpacker is written by Qubes and is trusted. Needed as part of copying +## files between VMs. ## https://github.com/QubesOS/qubes-core-agent-linux/blob/master/qubes-rpc/qfile-unpacker.c -## https://github.com/QubesOS/qubes-issues/issues/8633 (historic local-root EoP) +## +## Historic Qubes upstream security issue: +## qfile-unpacker allows unprivileged users in VMs to gain root privileges +## https://github.com/QubesOS/qubes-issues/issues/8633 ## ## matches both: ## - /usr/lib/qubes/qfile-unpacker whitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared deleted file mode 100644 index 986f7a72..00000000 --- a/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared +++ /dev/null @@ -1,19 +0,0 @@ -## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## Please use "/etc/permission-hardener.d/20_user.conf" or -## "/usr/local/etc/permission-hardener.d/20_user.conf" for your custom -## configuration. When security-misc-shared is updated, this file may be -## overwritten. - -## utempter (libutempter0): privileged back-end that terminal emulators (xterm, -## screen, tmux) fork to add/remove their pty login records in /var/run/utmp and -## /var/log/wtmp so who/w/last see the session. Installed SGID group "utmp" (NOT -## setuid root) -- the utmp group owns those files, so SGID utmp is the minimum -## privilege to update them. Stripping the bit breaks terminal login accounting -## (who/w miss sessions); no root privilege lost. Old CVEs only (CVE-2004-0233 -## path traversal, fixed in 0.5.3+). Whitelisted under "selinux" because SELinux -## policy references this path. -## Ref: https://bugzilla.redhat.com/show_bug.cgi?id=246063 -## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SGID is a human trust decision. -/utempter/utempter matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared index 684237bc..6265587f 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared @@ -6,14 +6,6 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## spice-client-glib-usb-acl-helper (spice-gtk): SUID root helper a SPICE client -## (virt-viewer, GNOME Boxes) calls during USB redirection to grant the invoking -## user a POSIX ACL on a /dev/bus/usb node so it can pass through to a VM. Needs -## root to set the ACL; consults polkit for an active local session first, so -## access is session-scoped rather than a permanent group grant. Stripping SUID -## breaks USB redirection ("Error setting facl: Operation not permitted"). It is -## a setuid-root C helper reachable by any desktop user -- a legitimate hardening -## candidate to disable where USB-to-VM redirection is unused. -## Ref: https://packages.debian.org/bookworm/spice-client-glib-usb-acl-helper -## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SUID is a human trust decision. +## Ref: https://packages.debian.org/trixie/spice-client-glib-usb-acl-helper +## TODO-HUMAN-DEVELOPER-ONLY: research spice-client-glib-usb-acl-helper matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared index da26b99d..6e27e081 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared @@ -6,21 +6,7 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## VirtualBox ships several helpers under /usr/lib/virtualbox/. The NETWORKING -## helpers carry the SUID-root bit; they need root only to create/configure host -## virtual NICs and run the userspace net stacks, then drop privileges: -## - VBoxNetAdpCtl : creates/removes vboxnetN adapters, assigns IPs -## (CAP_NET_ADMIN operations). -## - VBoxNetDHCP / VBoxNetNAT : userspace DHCP server / NAT engine. -## The front-ends (VBoxHeadless, VBoxSDL, VirtualBoxVM) are generally NOT SUID in -## stock builds; whitelisted defensively for distro/"hardened" builds that -## setuid-root them to load the ring-0 VMM driver. Stripping SUID from the net -## helpers breaks host-only/NAT networking for non-root users. -## SUID history (why the surface is sensitive): CVE-2009-3692 (VBoxNetAdpCtl -## popen() metachar injection -> local root), CVE-2017-3316, recurring Oracle CPU -## local-EoP fixes. Verify on-image bits: find /usr/lib/virtualbox -perm -4000 -## Ref: https://nvd.nist.gov/vuln/detail/CVE-2009-3692 -## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SUID is a human trust decision. +## TODO-HUMAN-DEVELOPER-ONLY: research /usr/lib/virtualbox/ matchwhitelist VirtualBoxVM matchwhitelist VBoxSDL matchwhitelist diff --git a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared index 01dda03a..91e15eb5 100644 --- a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared @@ -12,10 +12,8 @@ ## [filename] [mode] [owner] [group] [capability] ## [filename] [exactwhitelist|matchwhitelist|disablewhitelist|nosuid] ## -## White spaces inside a file name are supported: the parser recovers the file -## name by reading the option fields from the right (a trailing whitelist -## keyword, or the mode/owner/group[/capability] tail anchored by the octal -## mode). Quoting/escaping is still not supported; a literal space is fine. +## White spaces inside a file name are supported, no quoting or escaping is +## necessary (or permitted). ###################################################################### # Global Settings @@ -27,12 +25,6 @@ # SUID disables below (or in lexically higher) files: disablewhitelist ###################################################################### -## For example, if you are not using SELinux the following might make sense to -## enable (utempter is SGID utmp for terminal login accounting; see -## 25_default_whitelist_selinux.conf for the full rationale). -## TODO-HUMAN-DEVELOPER-ONLY: disabling utempter's SGID is a human trust decision. -#/utempter/utempter disablewhitelist - ## If you are not going to use AppImages such as electrum Bitcoin wallet. #/fusermount disablewhitelist @@ -124,11 +116,5 @@ ## anon-apps-config does this. #/usr/bin/ping 0744 root root none -## gst-ptp-helper (libgstreamer1.0-0): started by gst_ptp_init() for PTP -## (IEEE 1588) network clock sync; SUID root or cap_net_bind_service+cap_net_admin -## to bind privileged UDP ports 319/320, then drops privileges. Removing it only -## disables PTP clock sync; ordinary audio/video playback is unaffected, so this -## is a low-risk hardening candidate (cf. Red Hat bug 1724677). Prefer stripping -## to capabilities over setuid-root if kept. -## TODO-HUMAN-DEVELOPER-ONLY: enabling this SUID/caps strip is a human trust decision. +## TODO-HUMAN-DEVELOPER-ONLY: research #/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper 0744 root root none diff --git a/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared index 4c9ca911..c66662cb 100755 --- a/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared @@ -5,15 +5,6 @@ ## AI-Assisted -## Compile emerg-shutdown.c into /usr/bin/emerg-shutdown at package postinst -## time. Compiling on the target (rather than shipping a prebuilt binary) keeps -## security-misc-shared Architecture: all while still producing a correct -## per-architecture, statically linked binary. The boot-time emerg-shutdown -## script then only copies this binary into /run instead of recompiling on -## every boot. - -## style-ok: no-safe-rm (only ever removes our own just-created mktemp temp) - set -o errexit set -o nounset set -o pipefail diff --git a/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared index cab64f7c..1954de3f 100755 --- a/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared @@ -6,24 +6,14 @@ ## AI-Assisted ## Compile emerg-shutdown.c with the project's standard hardening -## flag set, statically linked (the binary is intended to run after -## the root device has been yanked - libc can't be paged in from a -## gone disk, so libc must be in the image already). -## -## Pure compile - the helper does not chmod, mv, or otherwise install -## the produced binary; the caller decides. +## flag set, statically linked. ## ## Usage: ## compile-emerg-shutdown ## ## Used by: -## * usr/libexec/security-misc/build-emerg-shutdown (package postinst: -## compiles C code into /usr/bin/emerg-shutdown; the boot-time -## emerg-shutdown script then copies that into /run) +## * usr/libexec/security-misc/build-emerg-shutdown ## * ci/codeql-build.sh (CodeQL static analysis prebuild) -## -## Holding the gcc command in one place keeps the runtime build path -## and the CI analysis path from drifting on hardening flags. set -o errexit set -o nounset diff --git a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared index 27f21856..10ac93b5 100755 --- a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared @@ -3,13 +3,6 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## emerg-shutdown.c is compiled once to /usr/bin/emerg-shutdown at package -## postinst (by build-emerg-shutdown), not shipped prebuilt -- compiling -## on-target keeps security-misc-shared Architecture: all while still producing -## a correct per-architecture, statically linked binary. This script copies -## that binary into /run (tmpfs, memlockd-resident) at boot and runs it, rather -## than recompiling on every boot. - set -o errexit set -o nounset set -o pipefail @@ -49,16 +42,10 @@ else ## Find the devices that make up the root device readarray -t root_devices < <(/usr/libexec/helper-scripts/get-backing-devices-for-mountpoint '/') || true; - ## Copy the prebuilt binary (compiled at postinst by build-emerg-shutdown) - ## into /run (tmpfs) so it is memory-resident for the emergency; memlockd - ## then locks it into RAM. if [ ! -f '/run/emerg-shutdown' ]; then if [ -f '/usr/bin/emerg-shutdown' ]; then cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown else - ## Fallback: prebuilt binary absent (build skipped/removed). Compile - ## straight into /run (tmpfs, writable even when /usr is read-only) so the - ## emergency-shutdown capability is never silently absent. /usr/libexec/security-misc/compile-emerg-shutdown \ /usr/src/security-misc/emerg-shutdown.c \ /run/emerg-shutdown \ diff --git a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared index ae6abf06..c7ec5782 100755 --- a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared +++ b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared @@ -46,7 +46,7 @@ COMPAT_BITS_MAX_DEFAULT=16 ## so the ls-parsing hazard SC2012 warns about is not reachable here, and a ## find-based newest-file rewrite would be a behaviour risk for no gain. # shellcheck disable=SC2012 -if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | sed -n '1p') ; then +if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | head -n1) ; then ## Find the relevant config options. if ! BITS_MAX=$(grep -E '^CONFIG_ARCH_MMAP_RND_BITS_MAX=[0-9]+$' "${CONFIG}" | cut -d "=" -f 2) ; then printf '%s\n' "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_BITS_MAX! Using built-in default." >&2 diff --git a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared index 1012f46f..4cb725a8 100755 --- a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared +++ b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared @@ -41,11 +41,4 @@ done ## exit failure so [success=1 default=ignore] will result in running the ## next PAM module (the pam_faillock module). -## -## This previously emitted a confusing journal line: -## pam_exec(sudo:auth): ... pam_faillock_not_if_x failed: exit code 1 -## now suppressed by 'quiet_log' on the pam_exec.so lines in -## /usr/share/pam-configs/{unix-faillock,faillock-preauth}-security-misc -## (linux-pam >= 1.5.2; plain 'quiet' only hides the user-facing message). -## https://github.com/linux-pam/linux-pam/issues/329 exit 1 diff --git a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared index e3b1877c..a462e19a 100755 --- a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared +++ b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared @@ -16,12 +16,6 @@ shopt -s shift_verbose true "PAM_SERVICE: ${PAM_SERVICE:-}" if [ "${PAM_SERVICE:-}" = "login" ]; then - ## exit 1 drives the [success=1 default=ignore] control flow (fall through - ## to the next PAM module). It previously emitted a journal line - ## pam_exec(login:account): ... pam_only_if_login failed: exit code 1 - ## now suppressed by 'quiet_log' on the pam_exec.so line in - ## /usr/share/pam-configs/console-lockdown-security-misc (linux-pam >= 1.5.2; - ## plain 'quiet' only hides the user-facing message, not the log). exit 1 else ## exit success so [success=1 default=ignore] will result in skipping the diff --git a/usr/share/lintian/overrides/security-misc-shared#security-misc-shared b/usr/share/lintian/overrides/security-misc-shared#security-misc-shared index dc9c90a0..0735aaa3 100644 --- a/usr/share/lintian/overrides/security-misc-shared#security-misc-shared +++ b/usr/share/lintian/overrides/security-misc-shared#security-misc-shared @@ -22,6 +22,5 @@ security-misc-shared: systemd-service-file-missing-install-key [usr/lib/systemd/ ## Minor bug: Unknown. security-misc-shared: package-contains-empty-directory [usr/share/security-misc-shared/] -## The postinst enables errexit via long-form 'set -o errexit'; lintian's -## scripts check only matches short 'set -e'. False-positive (like tb-updater). +## Lintian doesn't know that set -o errexit is the same as set -e. security-misc-shared: maintainer-script-ignores-errors [postinst] diff --git a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared index d25ea83e..f2da0c97 100755 --- a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared +++ b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared @@ -12,7 +12,6 @@ shopt -s inherit_errexit shopt -s shift_verbose export LC_ALL=C -## security-misc-shared Depends on helper-scripts. # shellcheck source=../../../../../helper-scripts/usr/libexec/helper-scripts/has.sh source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.sh diff --git a/usr/src/security-misc/fm-shim-backend.c#security-misc-shared b/usr/src/security-misc/fm-shim-backend.c#security-misc-shared index 548db61d..dd349493 100644 --- a/usr/src/security-misc/fm-shim-backend.c#security-misc-shared +++ b/usr/src/security-misc/fm-shim-backend.c#security-misc-shared @@ -85,15 +85,12 @@ void launch_frontend_process(const char *mode_opt, char **uri_list, * process, otherwise we won't know what display server to use and may run * into theming issues. * - * NOTE: Non-Qubes-Whonix's systemd --user manager exports a minimal PATH - * (the systemd default) rather than the profile-derived PATH an interactive - * qterminal bash has, because the graphical session start does not import - * the desktop environment into the user manager. fm-shim-frontend no longer - * depends on that PATH for launching (it invokes xdg-mime and gio by - * absolute path). The complete fix -- reconciling the whole session PATH -- - * belongs in desktop-config-dist's start-lxqt-session (e.g. a - * 'dbus-update-activation-environment --systemd --all' call after PATH is - * set) and is out of scope for this package. + * TODO: The environment in Non-Qubes-Whonix's user manager has some + * worrying discrepancies when compared to the environment of bash running + * in a qterminal window. In particular, 'PATH' is different. We may need to + * add a 'dbus-update-activation-environment --systemd --all' call to + * /usr/libexec/desktop-config-dist/start-lxqt-session. Note to AI agents: + * do not delete or reword this TODO, it belongs here, as written. */ DBusMessage *env_request = NULL; From 3e3d7453e3318ac79ea36ae14ed90442ded0c165 Mon Sep 17 00:00:00 2001 From: "claude (dev371 3fff1819-b77d-4dde-be19-8c01e09d3394)" Date: Fri, 28 Aug 2026 01:23:45 -0400 Subject: [PATCH 26/61] lintian: drop redundant maintainer-script-ignores-errors override Now suppressed globally in genmkfile; the false positive is the same errexit long-form case. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../overrides/security-misc-shared#security-misc-shared | 4 ---- 1 file changed, 4 deletions(-) diff --git a/usr/share/lintian/overrides/security-misc-shared#security-misc-shared b/usr/share/lintian/overrides/security-misc-shared#security-misc-shared index dc9c90a0..1dad42d7 100644 --- a/usr/share/lintian/overrides/security-misc-shared#security-misc-shared +++ b/usr/share/lintian/overrides/security-misc-shared#security-misc-shared @@ -21,7 +21,3 @@ security-misc-shared: systemd-service-file-missing-install-key [usr/lib/systemd/ ## Minor bug: Unknown. security-misc-shared: package-contains-empty-directory [usr/share/security-misc-shared/] - -## The postinst enables errexit via long-form 'set -o errexit'; lintian's -## scripts check only matches short 'set -e'. False-positive (like tb-updater). -security-misc-shared: maintainer-script-ignores-errors [postinst] From 0088727e4a9ca17d07916a30544ffa6e13890bfc Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Sun, 6 Sep 2026 12:56:41 -0400 Subject: [PATCH 27/61] cleanup --- debian/security-misc-shared.install | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/security-misc-shared.install b/debian/security-misc-shared.install index f149b2e0..b141aba2 100755 --- a/debian/security-misc-shared.install +++ b/debian/security-misc-shared.install @@ -71,7 +71,6 @@ usr/lib/permission-hardener.d/25_default_whitelist_passwd.conf#security-misc-sha usr/lib/permission-hardener.d/25_default_whitelist_policykit.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_policykit.conf usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf -usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_spice.conf usr/lib/permission-hardener.d/25_default_whitelist_ssh.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_ssh.conf usr/lib/permission-hardener.d/25_default_whitelist_sudo.conf#security-misc-shared => /usr/lib/permission-hardener.d/25_default_whitelist_sudo.conf From 94ea3994027115b02bdcc8d8246c4c5b5c40a776 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Sun, 6 Sep 2026 16:58:23 +0000 Subject: [PATCH 28/61] bumped changelog version --- changelog.upstream | 406 +++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 + 2 files changed, 412 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index ce87d20c..0e02863f 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,96 @@ +commit 0088727e4a9ca17d07916a30544ffa6e13890bfc +Author: Patrick Schleizer +Date: Sun Sep 6 12:56:41 2026 -0400 + + cleanup + +commit ebd72d593c81fad95d09a055d4e839b6eecca332 +Merge: 78491468 87506b1e +Author: Patrick Schleizer +Date: Sun Aug 30 10:02:32 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit 87506b1eaf45fa7501d54cf83b9be13578f86ecb +Author: Aaron Rainbolt +Date: Fri Aug 28 01:03:22 2026 -0400 + + Polish, add TODOs + +commit 0f1f701b5c413b1ab2229577261fe6e0187d84ca +Merge: eb5b301d bb399ba7 +Author: Aaron Rainbolt +Date: Thu Aug 27 21:02:55 2026 -0400 + + Merge branch 'ai' into arraybolt3/trixie + +commit bb399ba73704e4c20fbc6965d13bee4f2c63626c +Author: claude (dev352 385b930f-4313-4883-93d5-e6857c3f7ea7) +Date: Thu Aug 27 12:07:58 2026 -0400 + + lintian: override maintainer-script-ignores-errors in the shipped file + + The postinst uses long-form 'set -o errexit' (style R-013); lintian's + scripts check only matches short 'set -e'. Add the false-positive + override to the package's own usr/share/lintian/overrides file -- a + separate debian/*.lintian-overrides displaces it (dh_lintian installs to + the same path), losing the etc-skel and other overrides it carries. + +commit d1b9f4c0517f03936af556445fedd2ee22d14e98 +Merge: cc7a93d1 908cadc6 +Author: claude (dev352 385b930f-4313-4883-93d5-e6857c3f7ea7) +Date: Thu Aug 27 11:22:32 2026 -0400 + + Merge branch 'ai' of ssh://github.com/org-ai-assisted/security-misc into ai + +commit cc7a93d10a30e19631c64437b5c6078ba5d74f77 +Author: claude (dev352 385b930f-4313-4883-93d5-e6857c3f7ea7) +Date: Thu Aug 27 11:21:51 2026 -0400 + + lintian: override false-positive maintainer-script-ignores-errors + + The postinst enables errexit via long-form 'set -o errexit' (project + style, R-013); lintian's check only matches short 'set -e'. Same + override tb-updater already carries. + +commit 908cadc6177c00d4ff3465b33b20b3c971ce246a +Author: assisted-by-ai (Bot Account) (Machine Account) <228987186+assisted-by-ai@users.noreply.github.com> +Date: Mon Aug 24 23:31:21 2026 -0400 + + ci: drop orphaned consumer-secrets-audit.yml (reusable decommissioned) + +commit a6d7a3585f8905bc3f898d8dd83c3b07cb04cba9 +Author: assisted-by-ai (Bot Account) (Machine Account) <228987186+assisted-by-ai@users.noreply.github.com> +Date: Mon Aug 24 23:31:19 2026 -0400 + + ci: drop orphaned consumer-codex-review.yml (reusable decommissioned) + +commit e6a81266555130673dc7d8bcb85ea50bbad9e852 +Author: assisted-by-ai (Bot Account) (Machine Account) <228987186+assisted-by-ai@users.noreply.github.com> +Date: Mon Aug 24 23:31:16 2026 -0400 + + ci: drop orphaned consumer-claude-code.yml (reusable decommissioned) + +commit 1c9f144b9ebe58daa174de05f5b0c0e42038bfc1 +Author: claude (dev272 e8b76afb-795c-4739-8ebd-023de854021e) +Date: Fri Aug 21 04:41:27 2026 -0400 + + security-misc: source helper-scripts via HELPER_SCRIPTS_PATH, drop SC1091 disable + + - check-for-usb-controller, permission-hardener, lkrg-virtualbox: use + source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/; + drop the dead '# shellcheck disable=SC1091' and the stale sibling note. + - completed pre-existing whole-file style debt: R-010 export LC_ALL=C in + the strict-mode block (dropping the duplicate single-quoted mid-file one). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 78491468a7c209f423f48f74de015f1df2b9a980 +Author: Patrick Schleizer +Date: Sun Aug 16 08:24:20 2026 +0000 + + bumped changelog version + commit f1e9da8fb911b1e3962f219290df14753794aee8 Merge: 52e18f72 1fcf4967 Author: Patrick Schleizer @@ -5,6 +98,302 @@ Date: Sat Aug 15 12:55:45 2026 -0400 Merge remote-tracking branch 'refs/remotes/org-ai-assisted/master' +commit f2c1e7517cf8a8017db91ef7cf42784e93e14ae8 +Author: claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4) +Date: Fri Aug 14 13:28:37 2026 -0400 + + permission-hardener: read spaced paths back from the state DB + + Fresh-claude review of the whitespace feature found the write/read sides + disagree: parsing now accepts space-containing filenames into the policy, but + load_state_without_policy split the dpkg-statoverride DB line on whitespace and + required exactly 4 fields, silently dropping any spaced path. A spaced SUID file + then got hardened but could never be un-hardened ('disable' never found it), and + repeated 'enable' failed with exit 203. + + - Read the three fixed fields (owner group mode) and take the remainder as the + path, matching the DB's 'owner group mode path' format. + - Regression test test_state_spaced_filename.sh: seed a spaced entry, disable, + assert the mode is restored (fails on the old reader: file stays un-restored). + - Test hygiene: print-policy captures modes into the state DB, so the two + print-policy tests now remove the entries they create instead of leaving stale + root-owned overrides for deleted temp files. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 6dcffee4bf717bfac9a4c13153dcaa1a25cdc89f +Author: claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4) +Date: Fri Aug 14 13:12:25 2026 -0400 + + permission-hardener: unify mode anchoring across all field widths + + Fresh-claude follow-up: the field_count==4/5 existence override did not cover a + 6-field line (spaced filename + numeric octal-looking owner + capability), which + anchored the mode on the owner and silently dropped the entry. No hardening + bypass -- numeric owners are rejected later against passwd names -- but a silent + drop instead of a loud rejection, and the ==4 disjunct was dead code. + + Replace the special-case override with one existence-based loop: among the octal + candidates at field_count-3 and field_count-4, pick the one whose recovered + filename exists; a mode at field 1 means no-space (no recovery); fall back to + the rightmost octal candidate when neither name exists. Handles the 3-field, + 4-field, spaced, numeric-owner and 6-field cases uniformly. + + - Regression test: 6-field spaced numeric-owner case; the suite now + distinguishes this fix from all three prior parser iterations. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 73cc7889b271e2f3733daed276ba235dde04dc20 +Author: claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4) +Date: Fri Aug 14 13:00:51 2026 -0400 + + permission-hardener: disambiguate octal-ambiguous entries by file existence + + Fresh-claude review found the earlier field-1 guard reintroduced a symmetric + misparse: a spaced filename whose second component is a bare octal (e.g. + '/srv/a 744 0644 root root') got dropped because the guard mistook the octal + component for the mode. + + The 5-field reading is genuinely ambiguous lexically -- '/a 744 0644 root root' + (spaced name) vs '/foo 0744 1000 root cap' (numeric owner). Resolve by which + candidate filename exists on disk, consistent with the tool skipping + nonexistent targets. Keep the no-recovery reading only when field 1 is an octal + mode and only the un-recovered name exists. + + - Replace the lexical guard with the existence check inside the right-anchor + branch; the whitelist-keyword branch is no longer gated (it was wrongly + skipped for '/x 744 y matchwhitelist'). + - Regression test: spaced octal-second-component case; the assertion now matches + the whole first column (trailing TAB) so '/a 744' cannot substring-match the + '/a 744 name' entry. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 780561e61a6bf1c8d110d1105a5102b59ceb14ea +Author: claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4) +Date: Fri Aug 14 12:49:10 2026 -0400 + + permission-hardener: fix disable-all directive + numeric-owner misparse + + Both surfaced by a fresh-claude review of the right-anchor change. + + - whitelists_disable_all=true was unreachable: the character-class check (which + excludes '=') ran before the directive check, so the documented line tripped + "invalid characters" and aborted the whole run (exit 200). Recognize the + directive first. + - Numeric octal-looking owner in a capability entry (e.g. ' 0744 0755 root + cap'): right-anchoring folded the owner into the mode position, the folded + path did not exist, and the entry was silently dropped -- file left + un-hardened. Skip recovery when the line is already a well-formed no-space + entry (octal mode at field 1, count 4 or 5). + - Correct the stale field-order comment: the tail is + 'filemode user_owner group_owner [capability]', mode first (per add_to_policy). + - Regression tests: numeric-owner case in the whitespace test; new + test_whitelists_disable_all.sh. Both fail on the pre-fix code. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit fca4984733e4aff6b90e8dd248d38b8cbb3889cc +Author: claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4) +Date: Fri Aug 14 12:40:52 2026 -0400 + + permission-hardener confs: correct gst-ptp-helper path/pkg and #8633 fix credit + + - 30_default.conf: gst-ptp-helper ships in libgstreamer1.0-0, not + gstreamer1.0-plugins-base; fix the disabled entry's path to + .../gstreamer1.0/gstreamer-1.0/... (first component was gstreamer-1.0, + which does not exist). Verified via dpkg -S. + - 25_default_whitelist_qubes.conf: the #8633 EoP fix is qfile-unpacker + (qubes-core-agent-linux) refusing a UID change other than the caller's; + libqubes-pure path/symlink sanitization (qubes-linux-utils PR #87) is a + separate layer. The comment credited only PR #87. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 37ad483b25c43b93bb350983c2534f9d775e977a +Author: claude (reviewdrain4 8d32e35d-2a57-4379-829c-6ef986f68cd4) +Date: Fri Aug 14 12:34:17 2026 -0400 + + permission-hardener: anchor mode tail from the right for spaced filenames + + - Space filenames are recovered from the option tail; the mode/owner/group + [/capability] tail has a fixed width (3 or 4 fields). + - Old left-to-right octal scan misanchored when a space-delimited filename + chunk was itself a bare octal (e.g. '/path/a 744 name 0744 root root'): the + field-count check then failed and the WHOLE hardener aborted (exit 200), + contradicting the comment that claimed such a line was skipped. + - Anchor the mode from the right (field_count-3, else field_count-4); prefer + the 3-field reading so a filename ending in an octal chunk is not mistaken + for a 4-field capability tail. + - Regression test: config line with a bare-octal filename chunk must parse via + print-policy; fails on the old code (exit 200 / entry missing). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit c2fcb2d893bf26a59c1b25415333fafcad8682dc +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 05:15:00 2026 -0400 + + security-misc: drop versioned bash dependency + + - Revert the bash (>= 4.4) Depends. Supported Debian (trixie+) always + ships bash >= 4.4 and earlier is unsupported, so the inherit_errexit + builtin is always present; the dependency adds nothing. Policy bans it + (bash-style-guide R-010b). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit a66fa50019f95326fef372ec5b3d1cde3b59b4a9 +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:55:18 2026 -0400 + + security-misc: fix undo chown empty-arg + emerg fallback (ai-review) + + - undo_policy_for_file: the strict-mode brace pass quoted "${verbose}", + so 'permission-hardener disable all' ran 'chown "" -- ...' (invalid + empty owner) and 'chmod "" ...'. Make verbose an array so an empty + value expands to zero arguments. Canary-verified. + - emerg-shutdown fallback: when the prebuilt binary is absent, compile + straight into /run (tmpfs, writable even if /usr is read-only) rather + than via build-emerg-shutdown into /usr/bin. + - Document the space-filename parser limitations (consecutive spaces + collapse; an octal-looking path chunk can misanchor -> entry skipped). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit c4bf5403d0dbda87b2e1fe165ade7c81bf4e1b30 +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:44:07 2026 -0400 + + security-misc: declare bash (>= 4.4) for inherit_errexit + + - Many scripts use 'shopt -s inherit_errexit' (bash 4.4+) in their + strict-mode preamble; ${misc:Depends} does not supply the baseline, so + an older /bin/bash would die in the preamble before the script's logic. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 5b34c9406198a47ce0c8d78a143606056e9d51b0 +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:40:49 2026 -0400 + + security-misc: dfuzzer commit pin, marker rewords, remount-secure cleanup + + - ci/dfuzzer-build.sh: pin dfuzzer to the exact upstream commit and + verify the tag resolves to it (a moved tag now fails loudly). + - grub 40_kernel_hardening + remount-secure hidepid: reword standing + 'TODO' notes to 'REMINDER' (tracking, not pending work). + - remount-secure: replace the echo/true toggle with a printf-array + output helper; bring the file to strict-mode / shellcheck compliance + (6-directive block, braces) as required to touch it. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 9f12941585d5b18ae79f5d8dfd9f94cc0cd2507d +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:40:35 2026 -0400 + + security-misc: document whitelisted SUID/SGID helpers, flag human-only + + - Replace the bare 'TODO: research' markers on the permission-hardener + whitelist entries (postfix, selinux/utempter, spice, virtualbox, + qubes/qfile-unpacker, pam-tmpdir-helper, gst-ptp-helper) with concise + documentation: what each helper is, why it is SUID/SGID, what breaks if + stripped, and relevant CVE/hardening notes. + - Retag the keep-vs-strip decisions TODO-HUMAN-DEVELOPER-ONLY -- a + security-trust call for a human maintainer, not AI-resolvable. + - 30_default.conf: fix the commented gst-ptp-helper path typo + (gstreamer1.0/grstreamer-1.0 -> gstreamer-1.0/gstreamer-1.0); note that + filenames with spaces are now parsed. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 8aef705e63595beaa90f70e9c8a4f297fd7a08ee +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:40:20 2026 -0400 + + security-misc: fm-shim frontend calls xdg-mime/gio by absolute path + + - fm-shim-frontend invoked xdg-mime and gio by bare name, depending on + the systemd --user manager's PATH (minimal, profile-derived entries + absent) that fm-shim-backend hands it. Use /usr/bin/xdg-mime and + /usr/bin/gio, matching the existing /usr/bin/unicode-show call. + - Reword the backend TODO: the whole-session PATH fix belongs in + desktop-config-dist's start-lxqt-session (out of this repo); the + frontend absolute-path change is the in-repo mitigation. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 8ba6ca134e52411ce18f29b553dbfedb42b8ed3e +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:40:08 2026 -0400 + + security-misc: silence expected pam_exec journal noise via quiet_log + + - The [success=1 default=ignore] pam_exec lines run helper scripts that + exit 1 on their expected control-flow path, emitting an alarming + 'pam_exec(...): ... failed: exit code 1' journal line. Plain 'quiet' + suppresses only the user-facing message, not the log. + - Add 'quiet_log' (linux-pam >= 1.5.2, present on bookworm+) to every + skip-logic pam_exec line (console-lockdown, unix-faillock, + faillock-preauth, wheel), leaving the control flow byte-identical. + - Update the scripts' comments to point at the resolution. + + Note: not verified on a live login journal (needs a real PAM stack); + the change is additive and semantics-preserving regardless. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 8c991dd34247e7cc43843a1dff6d5300330646f1 +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:39:52 2026 -0400 + + security-misc: compile emerg-shutdown once at postinst, not every boot + + - emerg-shutdown.c was recompiled into /run on every boot. Move the + compile to a standalone build-emerg-shutdown run from postinst, which + installs /usr/bin/emerg-shutdown. Compiling on-target keeps the package + Architecture: all while still producing a correct per-arch static binary. + - Boot-time emerg-shutdown now just copies /usr/bin/emerg-shutdown into + /run (memlockd-resident), with a fallback build if the binary is absent. + - dracut module ships /usr/bin/emerg-shutdown (exists right after install, + fixing the require_binaries /run chicken-and-egg). + - Sandbox-verified: build-emerg-shutdown produces a static ELF in /usr/bin. + - Strict/style compliance on the touched scripts (waivers for the debconf + maintainer script and the dracut-sourced module). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 942ba8a1902c2c8a0d6cd1921f2b540eb413a217 +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 04:39:33 2026 -0400 + + security-misc: permission-hardener parse filenames with spaces + + - load_state() split config lines on every space, so any entry whose + filename contains a space was mis-fielded: silently skipped (a SUID + binary left un-hardened) or mapped to a corrupted policy entry. + - Recover the filename by parsing options from the right -- a trailing + whitelist keyword, or the mode/owner/group[/capability] tail anchored + by the octal mode. Space-free lines unchanged. + - Add regression test (drives the real script via print-policy) plus its + CI workflow. Canary-verified: fails on the pre-fix parser, passes now. + - Bring the file to strict-mode / shellcheck compliance as required to + touch it (6-directive block, printf, braces). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit ec78b99bb1be3be87fb07b3c269d54dc4f1db113 +Merge: a8f26061 1fcf4967 +Author: claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c) +Date: Wed Aug 12 03:30:39 2026 -0400 + + Merge branch 'master' into ai + + Absorb upstream + fork-trunk work (merged PRs #23/#26, CI-workflow + propagation, changelog bump) so the PR diff carries only our own commits. + commit 52e18f723e37b5e5616ae6681f9e3cd28369e88c Merge: 2da1f21c eb5b301d Author: Patrick Schleizer @@ -12,6 +401,23 @@ Date: Wed Aug 12 03:02:01 2026 -0400 Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' +commit a8f26061e0e199b95077aa20696257bddc3db73d +Author: claude (dev182 e09ec5cf-4ebf-4e1b-88e5-55fbac6ef1d3) +Date: Mon Aug 10 13:56:04 2026 -0400 + + Drain input in USB-controller and config-file lookups + + check-for-usb-controller: grep --quiet exits on the first match and closes + the pipe, sending SIGPIPE to lspci. Under pipefail the pipeline then reports + 141, so the guarding if takes the false branch even when a USB controller is + present. Let grep read the whole stream and redirect to /dev/null instead. + + mmap-rnd-bits: head -n 1 has the same early-close hazard against ls; use + sed -n '1p', which consumes all input, for consistency. Drop the redundant + duplicate set -euo pipefail line already covered by the long-form preamble. + + Co-Authored-By: Claude Opus 4.8 (1M context) + commit eb5b301db6069a5ccabf327745a000053ece1475 Merge: a5655e63 2da1f21c Author: Aaron Rainbolt diff --git a/debian/changelog b/debian/changelog index e4f43ab1..47bac83f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:53.5-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Sun, 06 Sep 2026 16:58:23 +0000 + security-misc (3:53.4-1) unstable; urgency=medium * New upstream version (local package). From aa458bac395d6fd450c749d0934f4a63d4a76c17 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Sun, 6 Sep 2026 23:11:56 -0400 Subject: [PATCH 29/61] Fix coding style, shorten comments --- .../remove-system.map#security-misc-shared | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/usr/libexec/security-misc/remove-system.map#security-misc-shared b/usr/libexec/security-misc/remove-system.map#security-misc-shared index 2c7d99d6..a0dff730 100755 --- a/usr/libexec/security-misc/remove-system.map#security-misc-shared +++ b/usr/libexec/security-misc/remove-system.map#security-misc-shared @@ -21,40 +21,38 @@ shopt -s nullglob system_map_list=( /boot/System.map* /usr/src/*/System.map* /lib/modules/*/*/System.map* /System.map* ) -if (( ${#system_map_list[@]} > 0 )); then - echo "INFO: Deleting system.map files..." +if [ "${#system_map_list[@]}" -gt 0 ]; then + printf '%s\n' "INFO: Deleting system.map files..." fi failed_list=() ## Removes the System.map files as they are only used for debugging or malware. for filename in "${system_map_list[@]}"; do - if [[ ! -f "${filename}" ]]; then + if ! [ -f "${filename}" ]; then continue fi - ## The immutable and append-only file attributes ('chattr +i', 'chattr +a') - ## prevent deletion. Clear them. A System.map file has no legitimate - ## reason to carry either and they would possibly complicate kernel - ## package removal. - chattr -ia -- "${filename}" &>/dev/null || true + ## The immutable and append-only file attributes prevent deletion. + ## System.map files have no reason to have either. + chattr -ia -- "${filename}" >/dev/null 2>&1 || true ## 'shred' with '--verbose' is too chatty. (7 lines) ## 'shred' stderr is suppressed because failure is expected on read-only - ## file systems. Failures are reported below instead. + ## file systems. if shred --force --zero -u -- "${filename}" 2>/dev/null; then - echo "INFO: removed '${filename}'" + printf '%s\n' "INFO: removed '${filename}'" continue fi failed_list+=( "${filename}" ) done -if (( ${#failed_list[@]} > 0 )); then +if [ "${#failed_list[@]}" -gt 0 ]; then for filename in "${failed_list[@]}"; do - echo "NOTE: Could not remove '${filename}'." + printf '%s\n' "NOTE: Could not remove '${filename}'." done - echo "NOTE: For details, see: https://www.kicksecure.com/wiki/security-misc#system_map" -elif (( ${#system_map_list[@]} > 0 )); then - echo "INFO: Done. Success." + printf '%s\n' "NOTE: For details, see: https://www.kicksecure.com/wiki/security-misc#system_map" +elif [ "${#system_map_list[@]}" -gt 0 ]; then + printf '%s\n' "INFO: Done. Success." fi ## Deliberately exits 0 even on partial failure. A non-zero exit code would fail From 24b39b0ca1b6c5a0fd35036703f56a2a1f5b9225 Mon Sep 17 00:00:00 2001 From: "claude (dev474 c7439536-fad8-4884-b71a-d2f3c4c23905)" Date: Mon, 7 Sep 2026 11:49:54 -0400 Subject: [PATCH 30/61] rename has.sh to has.bsh (bashism convention) Sourced helper-scripts lib uses bashisms; .bsh marks it. Scoped to has.sh consumers (repo carries unrelated concurrent WIP). --- debian/security-misc-shared.postinst | 2 +- usr/bin/remount-secure#security-misc-shared | 2 +- .../check-for-usb-controller#security-misc-shared | 4 ++-- .../security-misc/lkrg/lkrg-virtualbox#security-misc-shared | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/debian/security-misc-shared.postinst b/debian/security-misc-shared.postinst index 1d304e2d..995d2f70 100755 --- a/debian/security-misc-shared.postinst +++ b/debian/security-misc-shared.postinst @@ -4,7 +4,7 @@ ## See the file COPYING for copying conditions. ## style-ok: allow-echo (Debian maintainer-script convention) -## style-ok: no-has (has.sh not reliably available this early in maintainer scripts) +## style-ok: no-has (has.bsh not reliably available this early in maintainer scripts) ## style-ok: no-strict (debconf maintainer script; full strict block would leak into APT/debconf) if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then diff --git a/usr/bin/remount-secure#security-misc-shared b/usr/bin/remount-secure#security-misc-shared index 55c2924e..87dee021 100755 --- a/usr/bin/remount-secure#security-misc-shared +++ b/usr/bin/remount-secure#security-misc-shared @@ -26,7 +26,7 @@ ## https://www.kicksecure.com/wiki/Dev/remount-secure ## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707 -## style-ok: no-has (runs in dracut where helper-scripts has.sh is unavailable; id itself may be absent) +## style-ok: no-has (runs in dracut where helper-scripts has.bsh is unavailable; id itself may be absent) ## Mount helper functions (_boot, _sys, _usr, ...) are invoked indirectly via ## the dispatch in main(); shellcheck cannot see the calls. diff --git a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared index 6ba31508..efb7641d 100755 --- a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared +++ b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared @@ -12,8 +12,8 @@ shopt -s shift_verbose export LC_ALL=C ## security-misc-shared Depends on helper-scripts. -# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh -source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.sh +# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.bsh +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.bsh ## Package 'pciutils' provides tool 'lspci'. has lspci diff --git a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared index fb18eb61..86b9a6fa 100755 --- a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared +++ b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared @@ -13,8 +13,8 @@ shopt -s shift_verbose export LC_ALL=C ## security-misc-shared Depends on helper-scripts. -# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh -source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.sh +# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.bsh +source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.bsh ## Check if the VirtualBox host software is installed. if ! has vboxmanage ; then From fc2afb158592f8a8ad7270dcb5ee5abb33e1824d Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Tue, 8 Sep 2026 15:30:04 +0000 Subject: [PATCH 31/61] bumped changelog version --- changelog.upstream | 65 ++++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 +++++ 2 files changed, 71 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index 0e02863f..ab45bf9b 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,29 @@ +commit 24a55b78fe94ef69bb866f71b86d54e3a659b83c +Merge: 94ea3994 aa458bac +Author: Patrick Schleizer +Date: Mon Sep 7 01:11:01 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit aa458bac395d6fd450c749d0934f4a63d4a76c17 +Author: Aaron Rainbolt +Date: Sun Sep 6 23:11:56 2026 -0400 + + Fix coding style, shorten comments + +commit 97f78d8262aeea854cef3d654ab6aeb57e1f08ec +Merge: 94ea3994 077d41a6 +Author: Aaron Rainbolt +Date: Sun Sep 6 22:38:54 2026 -0400 + + Merge remote-tracking branch 'maybebyte/fix/remove-system-map-readonly-continue' into arraybolt3/trixie + +commit 94ea3994027115b02bdcc8d8246c4c5b5c40a776 +Author: Patrick Schleizer +Date: Sun Sep 6 16:58:23 2026 +0000 + + bumped changelog version + commit 0088727e4a9ca17d07916a30544ffa6e13890bfc Author: Patrick Schleizer Date: Sun Sep 6 12:56:41 2026 -0400 @@ -665,6 +691,45 @@ Date: Thu Jul 30 11:59:15 2026 -0400 Generated with assistance from Claude Code. +commit 077d41a6b193787dc31718370c1532d6638569bc +Author: Ashlen +Date: Wed Jul 15 12:54:05 2026 -0600 + + fix(remove-system.map): continue past undeletable files + + The deletion loop called `exit 0` on the first System.map that `[ -w ]` + rejected, so copies in later locations were never shredded. Where + `[ -w ]` passed but shred failed anyway, shred ran as a bare command and + tripped the ERR trap that pre.bsh installs, aborting with a "## Please + report this bug!" banner and exit 1. + + Branch on shred's exit status instead of `[ -w ]`, which does not + predict removability. Unlinking depends on write permission on the + parent directory rather than on the file itself, and an append-only file + passes the test yet cannot be unlinked at all. For a full-capability + root process the check is also true on a mode 444 file, so it only ever + failed on read-only file systems and on files carrying the immutable + attribute. + + Clear the immutable and append-only attributes before each attempt. Per + review, a System.map should never be immutable and `chattr +i` would + possibly complicate kernel package removal; append-only is cleared on + the same grounds, since it blocks removal without affecting `[ -w ]`. + + Report the files that remain once at the end, under a single wiki link + rather than repeating it on every line, and withhold "Done. Success." + when anything failed. A file reachable under two names, such as + /lib/modules//build and the source symlink beside it, is listed + under each, since both matched and both failed. + + Iterate an array rather than word-splitting a string with IFS, which a + /etc/remove-system.map_pre.d drop-in can set; under `IFS=:` the globs + matched nothing, so the script deleted nothing and still exited 0. + + The removal loop no longer exits early on failure and the script ends in + an explicit `exit 0`; a non-zero status would fail the kernel package's + postinst hook and the oneshot unit on every boot. + commit 5daefde4e8cb4705d8fbe7886f2e003e61e0b974 Author: claude (dev118 bd22decd-3027-4ab5-9e48-11adfe39b279) Date: Tue Jul 28 13:50:24 2026 -0400 diff --git a/debian/changelog b/debian/changelog index 47bac83f..6b048e3d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:53.6-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Tue, 08 Sep 2026 15:30:04 +0000 + security-misc (3:53.5-1) unstable; urgency=medium * New upstream version (local package). From dd05179ea3310d0cac4d2de9db220ebbd5224fbf Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Tue, 8 Sep 2026 21:24:00 -0400 Subject: [PATCH 32/61] fix --- .../permission-hardener#security-misc-shared | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 6d061dfd..1cc80184 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -472,20 +472,21 @@ load_state() { processed_config_line="${line}" ## We can handle filenames with spaces. This is because the first item - ## after a filename is an octal file mode, and we only support user/group - ## names, not UIDs and GIDs, as owner identifiers. This means that we can - ## scan from right to left for the furthest-right octal integer field and - ## use it to separate the filename from the rest of the options. (If - ## usernames could themselves be octal integers, this would break, but - ## adduser.conf's default NAME_REGEX forbids this. This is definitely a - ## hack, but the alternative is to change the format of the policy files, - ## which would break custom config for users. + ## after a filename is either an octal file mode or a keyword, and we + ## only support user/group names, not UIDs and GIDs, as owner + ## identifiers. This means that we can scan from right to left for the + ## furthest-right octal integer rr keyword field and use it to separate + ## the filename from the rest of the options. (If usernames could + ## themselves be octal integers, this would break, but adduser.conf's + ## default NAME_REGEX forbids this. This is definitely a hack, but the + ## alternative is to change the format of the policy files, which would + ## break custom config for users. ## ## 'read' will collapse consecutive spaces when splitting on spaces. ## Convert spaces into newlines first, then split on newlines with a ## loop. field_list=() - while read IFS= -r one_field; do + while IFS= read -r one_field; do field_list+=( "${one_field}" ) done < <(printf '%s\n' "${line}" | tr ' ' '\n') field_count="${#field_list[@]}" @@ -493,10 +494,11 @@ load_state() { hit_barrier='false' fso_name="" meta_list=() - for (( field_index = field_count - 1; field_index <= 0; \ + for (( field_index = field_count - 1; field_index >= 0; \ field_index-- )); do if [ "${hit_barrier}" = 'false' ]; then - if [[ "${field_list[field_index]}" =~ ^[0-7]{3,4}$ ]]; then + if [[ "${field_list[field_index]}" =~ ^[0-7]{3,4}$ ]] \ + || [[ "${field_list[field_index]}" =~ ^(nosuid|exactwhitelist|matchwhitelist|disablewhitelist)$ ]]; then hit_barrier='true' fi meta_list=( "${field_list[field_index]}" "${meta_list[@]}" ) From b9cf508116757b7fff13c199a142742246162877 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Wed, 9 Sep 2026 11:05:44 +0000 Subject: [PATCH 33/61] bumped changelog version --- changelog.upstream | 19 +++++++++++++++++++ debian/changelog | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index ab45bf9b..792317ef 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,22 @@ +commit 0ba912995c461446bf527cf0c6a9db47e8aaab08 +Merge: fc2afb15 dd05179e +Author: Patrick Schleizer +Date: Wed Sep 9 06:56:30 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit dd05179ea3310d0cac4d2de9db220ebbd5224fbf +Author: Aaron Rainbolt +Date: Tue Sep 8 21:24:00 2026 -0400 + + fix + +commit fc2afb158592f8a8ad7270dcb5ee5abb33e1824d +Author: Patrick Schleizer +Date: Tue Sep 8 15:30:04 2026 +0000 + + bumped changelog version + commit 24a55b78fe94ef69bb866f71b86d54e3a659b83c Merge: 94ea3994 aa458bac Author: Patrick Schleizer diff --git a/debian/changelog b/debian/changelog index 6b048e3d..169f9e16 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:53.7-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Wed, 09 Sep 2026 11:05:43 +0000 + security-misc (3:53.6-1) unstable; urgency=medium * New upstream version (local package). From 439ddce5bf08465710b81bd2353ac105b1e522c4 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Fri, 11 Sep 2026 17:06:29 -0400 Subject: [PATCH 34/61] Revert some faulty changes to style-ok comments (uglier style, excessive verbosity, allow-echo in a script that did not use echo) --- debian/security-misc-shared.postinst | 5 ++--- usr/bin/remount-secure#security-misc-shared | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/debian/security-misc-shared.postinst b/debian/security-misc-shared.postinst index 19e4ab9f..1ea1633e 100755 --- a/debian/security-misc-shared.postinst +++ b/debian/security-misc-shared.postinst @@ -3,9 +3,8 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## style-ok: allow-echo (Debian maintainer-script convention) -## style-ok: no-has (has.bsh not reliably available this early in maintainer scripts) -## style-ok: no-strict (debconf maintainer script; full strict block would leak into APT/debconf) +## style-ok: no-has - has.sh isn't reliably available in maintainer scripts +## style-ok: no-strict - full strict block could leak into APT/debconf if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh diff --git a/usr/bin/remount-secure#security-misc-shared b/usr/bin/remount-secure#security-misc-shared index c5efc9f5..5d1322ab 100755 --- a/usr/bin/remount-secure#security-misc-shared +++ b/usr/bin/remount-secure#security-misc-shared @@ -26,7 +26,7 @@ ## https://www.kicksecure.com/wiki/Dev/remount-secure ## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707 -## style-ok: no-has (runs in dracut where helper-scripts has.bsh is unavailable; id itself may be absent) +## style-ok: no-has - may run in dracut without helper-scripts ## Mount helper functions (_boot, _sys, _usr, ...) are invoked indirectly via ## the dispatch in main(). From e64a2c7d0481dfd38c8a575a49ddf55248e6230d Mon Sep 17 00:00:00 2001 From: "claude (dev541 abd2cdfa-632f-4eda-bd69-14b67be2017e)" Date: Sun, 13 Sep 2026 05:28:23 -0400 Subject: [PATCH 35/61] Python -Bsu shebangs; grep --quiet in tests --- ci/tests/fm_shim_frontend/test_property.py | 2 +- .../permission_hardener/test_whitelists_disable_all.sh | 2 +- ci/tests/permission_hardener/test_whitespace_filename.sh | 2 +- usr/bin/fm-shim-frontend#security-misc-shared | 7 ++++++- .../fm_shim_frontend.py#security-misc-shared | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ci/tests/fm_shim_frontend/test_property.py b/ci/tests/fm_shim_frontend/test_property.py index e0f48a75..1361258e 100644 --- a/ci/tests/fm_shim_frontend/test_property.py +++ b/ci/tests/fm_shim_frontend/test_property.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 -su +#!/usr/bin/python3 -Bsu ## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. diff --git a/ci/tests/permission_hardener/test_whitelists_disable_all.sh b/ci/tests/permission_hardener/test_whitelists_disable_all.sh index 0044f796..e69b200c 100755 --- a/ci/tests/permission_hardener/test_whitelists_disable_all.sh +++ b/ci/tests/permission_hardener/test_whitelists_disable_all.sh @@ -77,7 +77,7 @@ if [ "${ph_rc}" -ne 0 ]; then printf '%s\n' "FAIL: print-policy exited ${ph_rc} (directive rejected as invalid characters)." >&2 test_status=1 fi -if printf '%s\n' "${policy_output}" | grep -qF -- "${ordinary_file}"$'\t'; then +if printf '%s\n' "${policy_output}" | grep --quiet --fixed-strings -- "${ordinary_file}"$'\t'; then printf '%s\n' "PASS: whitelists_disable_all=true accepted; ordinary entry present." else printf '%s\n' "FAIL: ordinary entry '${ordinary_file}' missing from print-policy output." >&2 diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh index 4234b67b..6d2a63b6 100755 --- a/ci/tests/permission_hardener/test_whitespace_filename.sh +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -123,7 +123,7 @@ fi ## each filename as the whole first field (trailing TAB). A bare substring match ## would let '/a 744' spuriously match the '/a 744 name' entry. for expected_file in "${spaced_file}" "${octal_chunk_file}" "${numeric_owner_file}" "${octal_second_file}" "${numeric_owner_spaced_file}"; do - if printf '%s\n' "${policy_output}" | grep -qF -- "${expected_file}"$'\t'; then + if printf '%s\n' "${policy_output}" | grep --quiet --fixed-strings -- "${expected_file}"$'\t'; then printf '%s\n' "PASS: filename '${expected_file}' parsed and present in policy." else printf '%s\n' "FAIL: filename '${expected_file}' missing from print-policy output." >&2 diff --git a/usr/bin/fm-shim-frontend#security-misc-shared b/usr/bin/fm-shim-frontend#security-misc-shared index 899465f5..5671244b 100755 --- a/usr/bin/fm-shim-frontend#security-misc-shared +++ b/usr/bin/fm-shim-frontend#security-misc-shared @@ -1,7 +1,12 @@ -#!/usr/bin/python3 -u +#!/usr/bin/python3 -Bsu ## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## Shell-invocation guard: under bash/sh the shebang is ignored and the `import` +## lines below would run as shell commands (`import` is ImageMagick -> XGrabServer, +## which freezes X). Re-exec under python3; inert as a string literal in python3. +"exec" "python3" "-Bsu" "$0" "$@" + from fm_shim_frontend.fm_shim_frontend import main main() diff --git a/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared b/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared index b16f8724..3fb3053e 100755 --- a/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared +++ b/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared @@ -1,4 +1,4 @@ -#!/usr/bin/python3 -su +#!/usr/bin/python3 -Bsu ## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. From ac4b5023ae7c0ca034b8e85dda91d34de4e2e920 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Sun, 13 Sep 2026 17:21:40 -0400 Subject: [PATCH 36/61] Polish --- usr/bin/fm-shim-frontend#security-misc-shared | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/usr/bin/fm-shim-frontend#security-misc-shared b/usr/bin/fm-shim-frontend#security-misc-shared index 5671244b..b0a949a4 100755 --- a/usr/bin/fm-shim-frontend#security-misc-shared +++ b/usr/bin/fm-shim-frontend#security-misc-shared @@ -3,10 +3,8 @@ ## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## Shell-invocation guard: under bash/sh the shebang is ignored and the `import` -## lines below would run as shell commands (`import` is ImageMagick -> XGrabServer, -## which freezes X). Re-exec under python3; inert as a string literal in python3. -"exec" "python3" "-Bsu" "$0" "$@" +## Shell-invocation guard. +"exec" "bash" "-c" "printf '%s\n' 'Do not execute this script with bash'; exit 1" from fm_shim_frontend.fm_shim_frontend import main main() From d108f60a20cfc51cbe3d98f6200e8387a47f7488 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Sun, 13 Sep 2026 17:25:53 -0400 Subject: [PATCH 37/61] comment --- debian/security-misc-shared.postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/security-misc-shared.postinst b/debian/security-misc-shared.postinst index 1ea1633e..0b1567c7 100755 --- a/debian/security-misc-shared.postinst +++ b/debian/security-misc-shared.postinst @@ -3,7 +3,7 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## style-ok: no-has - has.sh isn't reliably available in maintainer scripts +## style-ok: no-has - has.bsh isn't reliably available in maintainer scripts ## style-ok: no-strict - full strict block could leak into APT/debconf if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then From 827e5ee1f8a250ba8b04e08c91892b1887e83d61 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Sun, 13 Sep 2026 17:50:16 -0400 Subject: [PATCH 38/61] comments --- debian/security-misc-shared.postrm | 2 -- debian/security-misc-shared.prerm | 7 ++----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/debian/security-misc-shared.postrm b/debian/security-misc-shared.postrm index 3312a215..0be096ef 100755 --- a/debian/security-misc-shared.postrm +++ b/debian/security-misc-shared.postrm @@ -16,8 +16,6 @@ true " " ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/11 -## Debian Policy 7.2: there is no guarantee that dependencies (here -## libpam-runtime) are available when postrm runs; skip gracefully then. if command -v pam-auth-update >/dev/null 2>&1; then pam-auth-update --package fi diff --git a/debian/security-misc-shared.prerm b/debian/security-misc-shared.prerm index d2b6b270..a4106c3e 100755 --- a/debian/security-misc-shared.prerm +++ b/debian/security-misc-shared.prerm @@ -16,11 +16,8 @@ true " " if [ "$1" = remove ]; then - ## 'pam-auth-update --remove' matches pam-config FILENAMES under - ## /usr/share/pam-configs/, not the package name. Disable while the helper - ## scripts under /usr/libexec/security-misc/ still exist. Keep in sync with - ## security-misc-shared.install and usr/share/pam-configs/. - ## See pam-auth-update(8). + ## REMINDER: This list of policies must be kept in sync with the contents of + ## usr/share/pam-configs. pam-auth-update --package --remove \ block-unsafe-logins-security-misc \ console-lockdown-security-misc \ From dc103b260b2dfd8b94ccdd23aaaaa54000d8d56c Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Mon, 14 Sep 2026 09:35:22 -0400 Subject: [PATCH 39/61] Also skip install checks on dist_build_iso; house-style - Skip the sudo/console-user install checks when dist_build_iso=true (an ISO/live build) in addition to the existing skip_install_check file, so the derivative-maker build no longer needs to write that marker file. The manual file-based bypass is kept for non-derivative-maker users. - Modernize to dist-ai house style: strict-mode block, printf not echo, has not command -v, braced + nounset-guarded variables. --- debian/security-misc-shared.preinst | 118 ++++++++++++++++------------ 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 93d8b2f5..22873fe4 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -6,12 +6,19 @@ if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh fi +source /usr/libexec/helper-scripts/has.bsh -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose +export LC_ALL=C true " ##################################################################### -## INFO: BEGIN: $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $@ +## INFO: BEGIN: ${DPKG_MAINTSCRIPT_PACKAGE:-} ${DPKG_MAINTSCRIPT_NAME:-} $* ##################################################################### " @@ -60,14 +67,14 @@ user_groups_modifications() { } output_skip_checks() { - echo "security-misc-shared '$0' INFO: Allow installation of security-misc-shared anyway." >&2 - echo "security-misc-shared '$0' INFO: (technical reason: $@)" >&2 - echo "security-misc-shared '$0' INFO: If this is a chroot this is probably OK." >&2 - echo "security-misc-shared '$0' INFO: Otherwise you might not be able to login." >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: Allow installation of security-misc-shared anyway." >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: (technical reason: $*)" >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: If this is a chroot this is probably OK." >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: Otherwise you might not be able to login." >&2 } sudo_users_check () { - if command -v "qubesdb-read" &>/dev/null; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -80,12 +87,14 @@ sudo_users_check () { ## example sudo_users: ## user,root - OLD_IFS="$IFS" + OLD_IFS="${IFS}" IFS="," export IFS - for user_with_sudo in $sudo_users ; do - if [ "$user_with_sudo" = "root" ]; then + ## Intentional word-split of the comma-separated group member list. + # shellcheck disable=SC2086 + for user_with_sudo in ${sudo_users} ; do + if [ "${user_with_sudo}" = "root" ]; then ## root login is also restricted. ## Therefore user "root" being member of group "sudo" is ## considered insufficient. @@ -95,31 +104,32 @@ sudo_users_check () { break done - IFS="$OLD_IFS" + IFS="${OLD_IFS}" export IFS - if [ "$are_there_any_sudo_users" = "yes" ]; then + if [ "${are_there_any_sudo_users:-}" = "yes" ]; then return 0 fi ## Prevent users from locking themselves out. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 - echo "$0: ERROR: No account is a member of group 'sudo'. Installation aborted." >&2 - echo "$0: ERROR: You probably want to run:" >&2 - echo "$0: NOTE: Replace account 'user' with your actual Linux user account name." >&2 - echo "" >&2 - echo "sudo adduser user sudo" >&2 - echo "sudo adduser user console" >&2 - echo "" >&2 - echo "$0: ERROR: See also installation instructions:" >&2 - echo "https://www.kicksecure.com/wiki/security-misc#install" >&2 - - if [ "$SECURITY_MISC_INSTALL" = "force" ]; then + printf '%s\n' "${0}: ERROR: No account is a member of group 'sudo'. Installation aborted." >&2 + printf '%s\n' "${0}: ERROR: You probably want to run:" >&2 + printf '%s\n' "${0}: NOTE: Replace account 'user' with your actual Linux user account name." >&2 + printf '%s\n' "" >&2 + printf '%s\n' "sudo adduser user sudo" >&2 + printf '%s\n' "sudo adduser user console" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "${0}: ERROR: See also installation instructions:" >&2 + printf '%s\n' "https://www.kicksecure.com/wiki/security-misc#install" >&2 + + if [ "${SECURITY_MISC_INSTALL:-}" = "force" ]; then output_skip_checks "Environment variable SECURITY_MISC_INSTALL is set to 'force'." return 0 fi - if test -f "/var/lib/security-misc/skip_install_check" ; then - output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists." + if test -f "/var/lib/security-misc/skip_install_check" \ + || [ "${dist_build_iso:-}" = "true" ] ; then + output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists or this is an ISO/live build (dist_build_iso=true)." return 0 fi @@ -127,13 +137,14 @@ sudo_users_check () { } console_users_check() { - if [ "$SECURITY_MISC_INSTALL" = "force" ]; then + if [ "${SECURITY_MISC_INSTALL:-}" = "force" ]; then return 0 fi - if test -f "/var/lib/security-misc/skip_install_check" ; then + if test -f "/var/lib/security-misc/skip_install_check" \ + || [ "${dist_build_iso:-}" = "true" ] ; then return 0 fi - if command -v "qubesdb-read" &>/dev/null; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -147,12 +158,14 @@ console_users_check() { ## user console_unrestricted_users="$(getent group console-unrestricted | cut -d: -f4)" - OLD_IFS="$IFS" + OLD_IFS="${IFS}" IFS="," export IFS - for user_with_console in $console_users $console_unrestricted_users ; do - if [ "$user_with_console" = "root" ]; then + ## Intentional word-split of the comma-separated group member lists. + # shellcheck disable=SC2086 + for user_with_console in ${console_users} ${console_unrestricted_users} ; do + if [ "${user_with_console}" = "root" ]; then ## root login is also restricted. ## Therefore account "root" being member of group "console" is ## considered insufficient. @@ -162,29 +175,30 @@ console_users_check() { break done - IFS="$OLD_IFS" + IFS="${OLD_IFS}" export IFS ## Prevent users from locking themselves out. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 - if [ "$are_there_any_console_users" = "yes" ]; then + if [ "${are_there_any_console_users:-}" = "yes" ]; then return 0 fi - echo "$0: ERROR: No account is a member of group 'console'. Installation aborted." >&2 - echo "$0: ERROR: You probably want to run:" >&2 - echo "" >&2 - echo "sudo adduser user console" >&2 - echo "" >&2 - echo "$0: ERROR: See also installation instructions:" >&2 - echo "https://www.whonix.org/wiki/security-misc#install" >&2 + printf '%s\n' "${0}: ERROR: No account is a member of group 'console'. Installation aborted." >&2 + printf '%s\n' "${0}: ERROR: You probably want to run:" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "sudo adduser user console" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "${0}: ERROR: See also installation instructions:" >&2 + printf '%s\n' "https://www.whonix.org/wiki/security-misc#install" >&2 - if [ "$SECURITY_MISC_INSTALL" = "force" ]; then + if [ "${SECURITY_MISC_INSTALL:-}" = "force" ]; then output_skip_checks "Environment variable SECURITY_MISC_INSTALL is set to 'force'." return 0 fi - if test -f "/var/lib/security-misc/skip_install_check" ; then - output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists." + if test -f "/var/lib/security-misc/skip_install_check" \ + || [ "${dist_build_iso:-}" = "true" ] ; then + output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists or this is an ISO/live build (dist_build_iso=true)." return 0 fi @@ -192,7 +206,7 @@ console_users_check() { } legacy() { - if [ -f "/var/lib/legacy/do_once/${FUNCNAME}_version_1" ]; then + if [ -f "/var/lib/legacy/do_once/${FUNCNAME[0]}_version_1" ]; then return 0 fi @@ -205,11 +219,11 @@ legacy() { continue_yes=true fi - if [ ! "$continue_yes" = "true" ]; then + if [ ! "${continue_yes:-}" = "true" ]; then return 0 fi - if command -v "qubesdb-read" &>/dev/null; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -220,23 +234,23 @@ legacy() { user_to_be_created=user - if ! id "$user_to_be_created" &>/dev/null ; then - true "INFO: Account '$user_to_be_created' does not exist. Skipping adding account '$user_to_be_created' to group 'console' and also skipping 'pam-auth-update --enable console-lockdown-security-misc'." + if ! id "${user_to_be_created}" &>/dev/null ; then + true "INFO: Account '${user_to_be_created}' does not exist. Skipping adding account '${user_to_be_created}' to group 'console' and also skipping 'pam-auth-update --enable console-lockdown-security-misc'." return 0 fi - adduser "$user_to_be_created" console + adduser "${user_to_be_created}" console pam-auth-update --enable console-lockdown-security-misc mkdir --parents "/var/lib/legacy/do_once" - touch "/var/lib/legacy/do_once/${FUNCNAME}_version_1" + touch "/var/lib/legacy/do_once/${FUNCNAME[0]}_version_1" } user_groups_modifications legacy -if [ "$1" = "install" ] || [ "$1" = "upgrade" ]; then +if [ "${1:-}" = "install" ] || [ "${1:-}" = "upgrade" ]; then sudo_users_check console_users_check fi @@ -249,7 +263,7 @@ true "INFO: Done with debhelper." true " ##################################################################### -## INFO: END : $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $@ +## INFO: END : ${DPKG_MAINTSCRIPT_PACKAGE:-} ${DPKG_MAINTSCRIPT_NAME:-} $* ##################################################################### " From 88abbcaa436a86dcd15a37c8ba4f4351b565c050 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Mon, 14 Sep 2026 11:13:01 -0400 Subject: [PATCH 40/61] Detect primary-GID members in the install checks - sudo_users_check / console_users_check now count a non-root account whose PRIMARY group is sudo/console (via a vendored group_has_nonroot_member), not only supplementary members; a supplementary-only scan falsely aborted install for such an admin. - Revert the qubesdb-read probe to 'command -v' (no-has waiver): a preinst runs at unpack time, before helper-scripts is guaranteed configured, so it must not source has.bsh. - group_has_nonroot_member is VENDORED from helper-scripts accountctl.sh (same reason it cannot be sourced); a dist-ai drift guard keeps them in sync. --- debian/security-misc-shared.preinst | 104 ++++++++++++---------------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 22873fe4..bd5783dd 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -3,10 +3,12 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## style-ok: no-has -- a preinst runs at unpack time, before helper-scripts (a +## plain Depends) is guaranteed configured, so it must not source has.bsh; it +## uses 'command -v' and a vendored group_has_nonroot_member instead. if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh fi -source /usr/libexec/helper-scripts/has.bsh set -o errexit set -o nounset @@ -73,41 +75,48 @@ output_skip_checks() { printf '%s\n' "security-misc-shared '${0}' INFO: Otherwise you might not be able to login." >&2 } +## VENDORED verbatim from helper-scripts accountctl.sh group_has_nonroot_member. +## A preinst runs before helper-scripts is guaranteed installed, so it cannot be +## sourced. Keep in sync with accountctl.sh; drift is caught by the dist-ai +## 'group_has_nonroot_member_drift' test. +group_has_nonroot_member() { + local group group_gid members member entry_name entry_gid + local -a member_list + group="${1:-}" + [ -n "${group}" ] || return 1 + group_gid="$(getent group -- "${group}" 2>/dev/null | cut -d: -f3)" || true + [ -n "${group_gid}" ] || return 1 + + ## Primary-GID members: a passwd account whose GID equals the group's GID + ## (such accounts are absent from the group's supplementary member field). + while IFS=":" read -r entry_name _ _ entry_gid _; do + if [ "${entry_gid}" = "${group_gid}" ] && [ "${entry_name}" != "root" ]; then + return 0 + fi + done < <(getent passwd) + + ## Supplementary members (comma-separated member field). + members="$(getent group -- "${group}" 2>/dev/null | cut -d: -f4)" || true + IFS="," read -r -a member_list <<< "${members}" || true + for member in "${member_list[@]}"; do + if [ -n "${member}" ] && [ "${member}" != "root" ]; then + return 0 + fi + done + return 1 +} + sudo_users_check () { - if has qubesdb-read; then + if command -v qubesdb-read >/dev/null 2>&1; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm return 0 fi - local sudo_users user_with_sudo are_there_any_sudo_users OLD_IFS - - sudo_users="$(getent group sudo | cut -d: -f4)" - ## example sudo_users: - ## user,root - - OLD_IFS="${IFS}" - IFS="," - export IFS - - ## Intentional word-split of the comma-separated group member list. - # shellcheck disable=SC2086 - for user_with_sudo in ${sudo_users} ; do - if [ "${user_with_sudo}" = "root" ]; then - ## root login is also restricted. - ## Therefore user "root" being member of group "sudo" is - ## considered insufficient. - continue - fi - are_there_any_sudo_users=yes - break - done - - IFS="${OLD_IFS}" - export IFS - - if [ "${are_there_any_sudo_users:-}" = "yes" ]; then + ## Any non-root account with sudo access (supplementary member OR primary + ## GID) prevents a lockout, so it satisfies the check. + if group_has_nonroot_member sudo; then return 0 fi @@ -144,43 +153,18 @@ console_users_check() { || [ "${dist_build_iso:-}" = "true" ] ; then return 0 fi - if has qubesdb-read; then + if command -v qubesdb-read >/dev/null 2>&1; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm return 0 fi - local console_users console_unrestricted_users user_with_console are_there_any_console_users OLD_IFS - - console_users="$(getent group console | cut -d: -f4)" - ## example console_users: - ## user - console_unrestricted_users="$(getent group console-unrestricted | cut -d: -f4)" - - OLD_IFS="${IFS}" - IFS="," - export IFS - - ## Intentional word-split of the comma-separated group member lists. - # shellcheck disable=SC2086 - for user_with_console in ${console_users} ${console_unrestricted_users} ; do - if [ "${user_with_console}" = "root" ]; then - ## root login is also restricted. - ## Therefore account "root" being member of group "console" is - ## considered insufficient. - continue - fi - are_there_any_console_users=yes - break - done - - IFS="${OLD_IFS}" - export IFS - - ## Prevent users from locking themselves out. + ## A non-root member (supplementary OR primary GID) of either console group + ## prevents a lockout. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 - if [ "${are_there_any_console_users:-}" = "yes" ]; then + if group_has_nonroot_member console \ + || group_has_nonroot_member console-unrestricted; then return 0 fi @@ -223,7 +207,7 @@ legacy() { return 0 fi - if has qubesdb-read; then + if command -v qubesdb-read >/dev/null 2>&1; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm From 497a23fbb31e2550fc858e6139e84b595be49d20 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Mon, 14 Sep 2026 12:49:30 -0400 Subject: [PATCH 41/61] Sync vendored group_has_nonroot_member (reject numeric arg) --- debian/security-misc-shared.preinst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index bd5783dd..df09f7f1 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -84,6 +84,13 @@ group_has_nonroot_member() { local -a member_list group="${1:-}" [ -n "${group}" ] || return 1 + ## A group NAME starts with a letter or underscore (per is_name_valid). + ## Reject anything else so a numeric argument is not silently reinterpreted + ## by getent as a GID lookup (answering about the wrong group). Kept + ## self-contained -- no is_name_valid call -- for vendoring. + if [[ "${group}" != [a-z_]* ]]; then + return 1 + fi group_gid="$(getent group -- "${group}" 2>/dev/null | cut -d: -f3)" || true [ -n "${group_gid}" ] || return 1 From 239532762966b9fc57f158bb02fb19b04bf1e1bc Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 02:16:06 -0400 Subject: [PATCH 42/61] Pre-Depend on helper-scripts; source accountctl.sh, drop vendored copy - helper-scripts moved from Depends to Pre-Depends: it is configured before this package is unpacked, so the preinst can source its libraries. Reuse the real group_has_nonroot_member from accountctl.sh instead of a vendored copy (and its drift guard). - accountctl.sh pulls in has.bsh, so 'has' is available: use it, drop the no-has waiver and the command -v fallbacks. --- debian/control | 2 +- debian/security-misc-shared.preinst | 56 +++++------------------------ 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/debian/control b/debian/control index 4a944d1b..1673ab33 100644 --- a/debian/control +++ b/debian/control @@ -30,11 +30,11 @@ Rules-Requires-Root: no ## > security-misc is to be removed Package: security-misc-shared Architecture: all +Pre-Depends: helper-scripts Depends: adduser, apparmor-profile-dist, build-essential, dmsetup, - helper-scripts, libcap2-bin, libdbus-1-dev, libglib2.0-bin, diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index df09f7f1..c7108b08 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -3,12 +3,12 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## style-ok: no-has -- a preinst runs at unpack time, before helper-scripts (a -## plain Depends) is guaranteed configured, so it must not source has.bsh; it -## uses 'command -v' and a vendored group_has_nonroot_member instead. -if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then - source /usr/libexec/helper-scripts/pre.bsh -fi +## helper-scripts is a Pre-Depends: it is fully configured before this preinst +## runs, so its libraries are guaranteed present and source-able here. Source +## pre.bsh (error handler) and accountctl.sh (group_has_nonroot_member; also +## pulls in has.bsh, so 'has' is available). +source /usr/libexec/helper-scripts/pre.bsh +source /usr/libexec/helper-scripts/accountctl.sh set -o errexit set -o nounset @@ -75,46 +75,8 @@ output_skip_checks() { printf '%s\n' "security-misc-shared '${0}' INFO: Otherwise you might not be able to login." >&2 } -## VENDORED verbatim from helper-scripts accountctl.sh group_has_nonroot_member. -## A preinst runs before helper-scripts is guaranteed installed, so it cannot be -## sourced. Keep in sync with accountctl.sh; drift is caught by the dist-ai -## 'group_has_nonroot_member_drift' test. -group_has_nonroot_member() { - local group group_gid members member entry_name entry_gid - local -a member_list - group="${1:-}" - [ -n "${group}" ] || return 1 - ## A group NAME starts with a letter or underscore (per is_name_valid). - ## Reject anything else so a numeric argument is not silently reinterpreted - ## by getent as a GID lookup (answering about the wrong group). Kept - ## self-contained -- no is_name_valid call -- for vendoring. - if [[ "${group}" != [a-z_]* ]]; then - return 1 - fi - group_gid="$(getent group -- "${group}" 2>/dev/null | cut -d: -f3)" || true - [ -n "${group_gid}" ] || return 1 - - ## Primary-GID members: a passwd account whose GID equals the group's GID - ## (such accounts are absent from the group's supplementary member field). - while IFS=":" read -r entry_name _ _ entry_gid _; do - if [ "${entry_gid}" = "${group_gid}" ] && [ "${entry_name}" != "root" ]; then - return 0 - fi - done < <(getent passwd) - - ## Supplementary members (comma-separated member field). - members="$(getent group -- "${group}" 2>/dev/null | cut -d: -f4)" || true - IFS="," read -r -a member_list <<< "${members}" || true - for member in "${member_list[@]}"; do - if [ -n "${member}" ] && [ "${member}" != "root" ]; then - return 0 - fi - done - return 1 -} - sudo_users_check () { - if command -v qubesdb-read >/dev/null 2>&1; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -160,7 +122,7 @@ console_users_check() { || [ "${dist_build_iso:-}" = "true" ] ; then return 0 fi - if command -v qubesdb-read >/dev/null 2>&1; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -214,7 +176,7 @@ legacy() { return 0 fi - if command -v qubesdb-read >/dev/null 2>&1; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm From c894bbcf75bbe3c63f17b75f0ffc8a0ae2f98ef3 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 02:37:02 -0400 Subject: [PATCH 43/61] preinst: fail closed loudly if helper-scripts library missing - source failures were swallowed (errexit not set yet); abort with the real cause instead. - a missing group_has_nonroot_member now aborts with the true reason rather than degrading the lockout check into a misleading 'no account is a member of sudo' abort. --- debian/security-misc-shared.preinst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index c7108b08..50dbc7fc 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -7,8 +7,18 @@ ## runs, so its libraries are guaranteed present and source-able here. Source ## pre.bsh (error handler) and accountctl.sh (group_has_nonroot_member; also ## pulls in has.bsh, so 'has' is available). -source /usr/libexec/helper-scripts/pre.bsh -source /usr/libexec/helper-scripts/accountctl.sh +## +## Fail CLOSED, loudly: a source failure (unreadable/partial/syntax) here would +## otherwise be swallowed (errexit is not set yet), and a missing +## group_has_nonroot_member would degrade the lockout check into a misleading +## 'no account is a member of sudo' abort. A security check that cannot load its +## verifier must stop with the TRUE cause, not skip or mislead. +source /usr/libexec/helper-scripts/pre.bsh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/pre.bsh; helper-scripts is broken or missing." >&2; exit 1; } +source /usr/libexec/helper-scripts/accountctl.sh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/accountctl.sh; helper-scripts is broken or missing." >&2; exit 1; } +if ! type -t group_has_nonroot_member >/dev/null 2>&1; then + printf '%s\n' "${0}: ERROR: helper-scripts accountctl.sh did not provide group_has_nonroot_member; cannot verify account lockout safety. helper-scripts is too old or incomplete." >&2 + exit 1 +fi set -o errexit set -o nounset From 888a5f679d816aeb8c50fc14ab604d27d158863d Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 04:27:13 -0400 Subject: [PATCH 44/61] permission-hardener: restore octal-owner filename disambiguation - the right-to-left octal scan mis-anchors on a numeric octal-looking owner (e.g. 0755), folding the real mode into the filename; the nonexistent path is then silently dropped, leaving a SUID binary un-hardened. - restore fixed-width tail anchoring + disambiguation by on-disk existence; test_whitespace_filename.sh (numowner, 'x y') parses again. --- .../permission-hardener#security-misc-shared | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index ec71c610..cc7de229 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -431,7 +431,8 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local one_field field_count fso_name meta_list field_index hit_barrier + local one_field field_count fso_name meta_list + local split_index candidate_index fallback_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -471,16 +472,12 @@ load_state() { processed_config_line="${line}" - ## We can handle filenames with spaces. This is because the first item - ## after a filename is either an octal file mode or a keyword, and we - ## only support user/group names, not UIDs and GIDs, as owner - ## identifiers. This means that we can scan from right to left for the - ## furthest-right octal integer or keyword field and use it to separate - ## the filename from the rest of the options. (If usernames could - ## themselves be octal integers, this would break, but adduser.conf's - ## default NAME_REGEX forbids this. This is definitely a hack, but the - ## alternative is to change the format of the policy files, which would - ## break custom config for users. + ## We can handle filenames with spaces. The options after a filename are a + ## fixed-width tail: either a lone whitelist keyword, or an octal mode + + ## owner + group + optional capability. The filename is whatever precedes + ## that tail, recovered by anchoring the tail from the right (see the field + ## split below). An owner that itself looks octal is disambiguated by which + ## recovered filename exists on disk. ## ## 'read' will collapse consecutive spaces when splitting on spaces. ## Convert spaces into newlines first, then split on newlines with a @@ -491,21 +488,48 @@ load_state() { done < <(printf '%s\n' "${line}" | tr ' ' '\n') field_count="${#field_list[@]}" - hit_barrier='false' + ## The option tail is fixed-width: a lone trailing whitelist keyword, or a + ## mode/owner/group[/capability] tail whose FIRST field is the octal mode. + ## Anchor the tail from the RIGHT. Two readings can be lexically valid when + ## a filename component OR a numeric owner looks octal (mode at field + ## count-3 vs count-4), so disambiguate by which recovered filename exists + ## on disk -- the tool skips nonexistent targets anyway. Fall back to the + ## rightmost octal candidate when neither recovered name exists. fso_name="" meta_list=() - for (( field_index = field_count - 1; field_index >= 0; \ - field_index-- )); do - if [ "${hit_barrier}" = 'false' ]; then - if [[ "${field_list[field_index]}" =~ ^[0-7]{3,4}$ ]] \ - || [[ "${field_list[field_index]}" =~ ^(nosuid|exactwhitelist|matchwhitelist|disablewhitelist)$ ]]; then - hit_barrier='true' + split_index='' + case "${field_list[field_count-1]}" in + nosuid|exactwhitelist|matchwhitelist|disablewhitelist) + split_index=$(( field_count - 1 )) + ;; + *) + fallback_index='' + for candidate_index in "$(( field_count - 3 ))" "$(( field_count - 4 ))"; do + (( candidate_index >= 1 )) || continue + [[ "${field_list[candidate_index]}" =~ ^[0-7]{3,4}$ ]] || continue + if [ -z "${fallback_index}" ]; then + fallback_index="${candidate_index}" fi - meta_list=( "${field_list[field_index]}" "${meta_list[@]}" ) - else - fso_name="${field_list[field_index]}${fso_name:+" ${fso_name}"}" - fi - done + fso_name="$(IFS=' '; printf '%s' "${field_list[*]:0:candidate_index}")" + if (( candidate_index == 1 )) || [ -e "${fso_name}" ]; then + split_index="${candidate_index}" + break + fi + done + if [ -z "${split_index}" ]; then + split_index="${fallback_index}" + fi + ;; + esac + if [ -n "${split_index}" ]; then + fso_name="$(IFS=' '; printf '%s' "${field_list[*]:0:split_index}")" + meta_list=( "${field_list[@]:split_index}" ) + else + ## No recoverable mode/keyword tail: hand every field to the field-count + ## check below, which rejects the line loudly (exit 200). + fso_name="" + meta_list=( "${field_list[@]}" ) + fi case "${#meta_list[@]}" in 1|3|4) From edbaf1c198cb727ab00ff57ef573edc8c7974fd6 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Tue, 15 Sep 2026 12:00:55 +0000 Subject: [PATCH 45/61] bumped changelog version --- changelog.upstream | 181 +++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 ++ 2 files changed, 187 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index 792317ef..d11ec5a3 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,95 @@ +commit 4db7b41bda5f7f2a1d0c1f163a070c998a5a284e +Merge: 81d6905a 827e5ee1 +Author: Patrick Schleizer +Date: Mon Sep 14 02:29:46 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit 827e5ee1f8a250ba8b04e08c91892b1887e83d61 +Author: Aaron Rainbolt +Date: Sun Sep 13 17:50:16 2026 -0400 + + comments + +commit a85c5b57045e95a840c4059e66d8296b8e51c155 +Merge: d108f60a 28d05b0c +Author: Aaron Rainbolt +Date: Sun Sep 13 17:40:14 2026 -0400 + + Merge branch 'arraybolt3/maybebyte-pam-auth-update' into arraybolt3/trixie + +commit 28d05b0cd17c9f3d77786cf14798c1cdec912d0c +Merge: 86780917 d108f60a +Author: Aaron Rainbolt +Date: Sun Sep 13 17:39:32 2026 -0400 + + Merge branch 'arraybolt3/trixie' into arraybolt3/maybebyte-pam-auth-update + +commit d108f60a20cfc51cbe3d98f6200e8387a47f7488 +Author: Aaron Rainbolt +Date: Sun Sep 13 17:25:53 2026 -0400 + + comment + +commit ac4b5023ae7c0ca034b8e85dda91d34de4e2e920 +Author: Aaron Rainbolt +Date: Sun Sep 13 17:21:40 2026 -0400 + + Polish + +commit 81ed36c2e3e5777ca81e33f8a40edd1cd952e001 +Merge: 439ddce5 db8f36fd +Author: Aaron Rainbolt +Date: Sun Sep 13 17:11:40 2026 -0400 + + Merge branch 'ai' into arraybolt3/trixie + +commit db8f36fd5ab660ad1c2dd02fffa998a2b3f1ee58 +Merge: e64a2c7d b8e218c6 +Author: claude +Date: Sun Sep 13 06:07:26 2026 -0400 + + Merge branch 'master' into ai + +commit b8e218c6438b862acd881eac9599ff40224fe862 +Merge: c77beac7 b9cf5081 +Author: claude +Date: Sun Sep 13 06:07:25 2026 -0400 + + Merge remote-tracking branch 'upstream/master' + +commit e64a2c7d0481dfd38c8a575a49ddf55248e6230d +Author: claude (dev541 abd2cdfa-632f-4eda-bd69-14b67be2017e) +Date: Sun Sep 13 05:28:23 2026 -0400 + + Python -Bsu shebangs; grep --quiet in tests + +commit 81d6905af8bd14a21163a1ab308c996e38869f74 +Merge: b9cf5081 439ddce5 +Author: Patrick Schleizer +Date: Sat Sep 12 09:49:38 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit 439ddce5bf08465710b81bd2353ac105b1e522c4 +Author: Aaron Rainbolt +Date: Fri Sep 11 17:06:29 2026 -0400 + + Revert some faulty changes to style-ok comments (uglier style, excessive verbosity, allow-echo in a script that did not use echo) + +commit d4e50dc6d74b6cf13c3dff2ef2b7d16a2b682ec2 +Merge: b9cf5081 409e0483 +Author: Aaron Rainbolt +Date: Fri Sep 11 16:58:09 2026 -0400 + + Merge branch 'ai' into arraybolt3/trixie + +commit b9cf508116757b7fff13c199a142742246162877 +Author: Patrick Schleizer +Date: Wed Sep 9 11:05:44 2026 +0000 + + bumped changelog version + commit 0ba912995c461446bf527cf0c6a9db47e8aaab08 Merge: fc2afb15 dd05179e Author: Patrick Schleizer @@ -5,6 +97,34 @@ Date: Wed Sep 9 06:56:30 2026 -0400 Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' +commit 409e0483071c2ca6a313272ec1957aab80a8cc3f +Merge: 24b39b0c c77beac7 +Author: claude (dev490 427cd1a1-bb80-4a36-9e14-2b6cec600780) +Date: Tue Sep 8 23:45:54 2026 -0400 + + Merge branch 'master' into ai + + # Conflicts: + # debian/security-misc-shared.postinst + # usr/bin/remount-secure#security-misc-shared + # usr/libexec/security-misc/check-for-usb-controller#security-misc-shared + # usr/share/lintian/overrides/security-misc-shared#security-misc-shared + # usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared + +commit c77beac7b4db560cd6d58abbc8943b77ee0ae62c +Merge: 6355b72c dd05179e +Author: claude (dev490 427cd1a1-bb80-4a36-9e14-2b6cec600780) +Date: Tue Sep 8 23:43:07 2026 -0400 + + Merge remote-tracking branch 'ab/trixie' + +commit 6355b72c6d9de2002506a3ab797b153f83c92a9d +Merge: 78491468 fc2afb15 +Author: claude (dev490 427cd1a1-bb80-4a36-9e14-2b6cec600780) +Date: Tue Sep 8 23:43:06 2026 -0400 + + Merge remote-tracking branch 'up/master' + commit dd05179ea3310d0cac4d2de9db220ebbd5224fbf Author: Aaron Rainbolt Date: Tue Sep 8 21:24:00 2026 -0400 @@ -17,6 +137,14 @@ Date: Tue Sep 8 15:30:04 2026 +0000 bumped changelog version +commit 24b39b0ca1b6c5a0fd35036703f56a2a1f5b9225 +Author: claude (dev474 c7439536-fad8-4884-b71a-d2f3c4c23905) +Date: Mon Sep 7 11:49:54 2026 -0400 + + rename has.sh to has.bsh (bashism convention) + + Sourced helper-scripts lib uses bashisms; .bsh marks it. Scoped to has.sh consumers (repo carries unrelated concurrent WIP). + commit 24a55b78fe94ef69bb866f71b86d54e3a659b83c Merge: 94ea3994 aa458bac Author: Patrick Schleizer @@ -56,6 +184,16 @@ Date: Sun Aug 30 10:02:32 2026 -0400 Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' +commit 3e3d7453e3318ac79ea36ae14ed90442ded0c165 +Author: claude (dev371 3fff1819-b77d-4dde-be19-8c01e09d3394) +Date: Fri Aug 28 01:23:45 2026 -0400 + + lintian: drop redundant maintainer-script-ignores-errors override + + Now suppressed globally in genmkfile; the false positive is the same errexit long-form case. + + Co-Authored-By: Claude Opus 4.8 (1M context) + commit 87506b1eaf45fa7501d54cf83b9be13578f86ecb Author: Aaron Rainbolt Date: Fri Aug 28 01:03:22 2026 -0400 @@ -710,6 +848,49 @@ Date: Thu Jul 30 11:59:15 2026 -0400 Generated with assistance from Claude Code. +commit 867809170a265b269a34f6d704c97ec53744cd4d +Author: Ashlen +Date: Wed Jul 15 19:06:42 2026 -0600 + + fix(pam): pass profile names to pam-auth-update --remove + + 'pam-auth-update --remove' matches pam-config filenames under + /usr/share/pam-configs/, but prerm and postrm passed + $DPKG_MAINTSCRIPT_PACKAGE -- the package name security-misc-shared -- + which matches no profile. An unknown target is stored without + validation and simply never matches, so the --remove was inert in + both calls: the prerm call was an effective no-op, while the postrm + call still performed a plain reconciliation. + + The prerm early-disable was therefore dead. On removal + /etc/pam.d/common-* kept referencing helper scripts under + /usr/libexec/security-misc/ that dpkg then deleted; + block-unsafe-logins is a Priority 1100 'requisite' entry, so a + dangling reference fails all authentication until postrm's + reconciliation regenerates common-* -- permanently, if postrm fails + before that point. + + prerm now passes the eight profile filenames explicitly. postrm + drops the ineffective '--remove ' for a plain + reconciliation and guards it with 'command -v': Debian Policy 7.2 + gives no guarantee that dependencies are available when postrm runs + and requires postrm to gracefully skip such actions, and an absent + pam-auth-update would abort the script under 'set -e'. prerm stays + unguarded: Policy names Depends (here libpam-runtime) as the + mechanism prerm scripts rely on, dependencies there normally being + at least unpacked. + + One consequence of a '--remove' that now takes effect: it also drops + the eight names from /var/lib/pam/seen. If a removal is aborted, + postinst's 'pam-auth-update --package' re-enables the seven + 'Default: yes' profiles among them, including any of them an + administrator had deliberately disabled (other packages' profiles + remain in seen and are unaffected), while + console-lockdown-security-misc ('Default: no') stays off until an + administrator re-enables it -- reinstalling does not restore it, + since preinst's enable is behind a do_once stamp. Both are new + relative to the previous inert behavior. + commit 077d41a6b193787dc31718370c1532d6638569bc Author: Ashlen Date: Wed Jul 15 12:54:05 2026 -0600 diff --git a/debian/changelog b/debian/changelog index 169f9e16..31cb0ab3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:53.8-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Tue, 15 Sep 2026 12:00:54 +0000 + security-misc (3:53.7-1) unstable; urgency=medium * New upstream version (local package). From 0a004b6675d22e4e54f79f07b086cebe78177280 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Tue, 15 Sep 2026 10:27:21 -0400 Subject: [PATCH 46/61] output --- usr/bin/fm-shim-frontend#security-misc-shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/bin/fm-shim-frontend#security-misc-shared b/usr/bin/fm-shim-frontend#security-misc-shared index b0a949a4..fadcd9e0 100755 --- a/usr/bin/fm-shim-frontend#security-misc-shared +++ b/usr/bin/fm-shim-frontend#security-misc-shared @@ -4,7 +4,7 @@ ## See the file COPYING for copying conditions. ## Shell-invocation guard. -"exec" "bash" "-c" "printf '%s\n' 'Do not execute this script with bash'; exit 1" +"exec" "bash" "-c" "printf '%s\n' '$0: ERROR: Do not execute this script with bash!' >&2; exit 1" from fm_shim_frontend.fm_shim_frontend import main main() From a70c168da6730ce86d1ea12f29bff2448734a8a9 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Tue, 15 Sep 2026 14:32:35 +0000 Subject: [PATCH 47/61] bumped changelog version --- changelog.upstream | 12 ++++++++++++ debian/changelog | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index d11ec5a3..21cecd65 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,15 @@ +commit 0a004b6675d22e4e54f79f07b086cebe78177280 +Author: Patrick Schleizer +Date: Tue Sep 15 10:27:21 2026 -0400 + + output + +commit edbaf1c198cb727ab00ff57ef573edc8c7974fd6 +Author: Patrick Schleizer +Date: Tue Sep 15 12:00:55 2026 +0000 + + bumped changelog version + commit 4db7b41bda5f7f2a1d0c1f163a070c998a5a284e Merge: 81d6905a 827e5ee1 Author: Patrick Schleizer diff --git a/debian/changelog b/debian/changelog index 31cb0ab3..9c7a3e49 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:53.9-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Tue, 15 Sep 2026 14:32:35 +0000 + security-misc (3:53.8-1) unstable; urgency=medium * New upstream version (local package). From 57a6530574d21307d77a371a1fb53259bfbe22fa Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 10:52:46 -0400 Subject: [PATCH 48/61] security-misc: simplify preinst helper-scripts sourcing - helper-scripts is a Pre-Depends, configured before this preinst; strict options first, then plain source -- errexit turns any load failure into a hard stop, so the bespoke fail-closed handling + type -t check are redundant --- debian/security-misc-shared.preinst | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 50dbc7fc..1fa5b35f 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -3,23 +3,6 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## helper-scripts is a Pre-Depends: it is fully configured before this preinst -## runs, so its libraries are guaranteed present and source-able here. Source -## pre.bsh (error handler) and accountctl.sh (group_has_nonroot_member; also -## pulls in has.bsh, so 'has' is available). -## -## Fail CLOSED, loudly: a source failure (unreadable/partial/syntax) here would -## otherwise be swallowed (errexit is not set yet), and a missing -## group_has_nonroot_member would degrade the lockout check into a misleading -## 'no account is a member of sudo' abort. A security check that cannot load its -## verifier must stop with the TRUE cause, not skip or mislead. -source /usr/libexec/helper-scripts/pre.bsh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/pre.bsh; helper-scripts is broken or missing." >&2; exit 1; } -source /usr/libexec/helper-scripts/accountctl.sh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/accountctl.sh; helper-scripts is broken or missing." >&2; exit 1; } -if ! type -t group_has_nonroot_member >/dev/null 2>&1; then - printf '%s\n' "${0}: ERROR: helper-scripts accountctl.sh did not provide group_has_nonroot_member; cannot verify account lockout safety. helper-scripts is too old or incomplete." >&2 - exit 1 -fi - set -o errexit set -o nounset set -o pipefail @@ -28,6 +11,11 @@ shopt -s inherit_errexit shopt -s shift_verbose export LC_ALL=C +## helper-scripts is a Pre-Depends, so it is fully configured and source-able +## here (accountctl.sh provides group_has_nonroot_member; pulls in has.bsh). +source /usr/libexec/helper-scripts/pre.bsh +source /usr/libexec/helper-scripts/accountctl.sh + true " ##################################################################### ## INFO: BEGIN: ${DPKG_MAINTSCRIPT_PACKAGE:-} ${DPKG_MAINTSCRIPT_NAME:-} $* From 00bad98497794d461fe4923e4ff182a61b918df7 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 10:56:10 -0400 Subject: [PATCH 49/61] security-misc: source has.bsh explicitly in preinst --- debian/security-misc-shared.preinst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 1fa5b35f..1c2856cc 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -12,8 +12,9 @@ shopt -s shift_verbose export LC_ALL=C ## helper-scripts is a Pre-Depends, so it is fully configured and source-able -## here (accountctl.sh provides group_has_nonroot_member; pulls in has.bsh). +## here (accountctl.sh provides group_has_nonroot_member). source /usr/libexec/helper-scripts/pre.bsh +source /usr/libexec/helper-scripts/has.bsh source /usr/libexec/helper-scripts/accountctl.sh true " From 87ff42a19c786050471b5350e228a26a82bd61eb Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Wed, 16 Sep 2026 00:01:44 -0400 Subject: [PATCH 50/61] Comments, revert an unreliable feature addition in permission-hardener's state file parsing --- debian/security-misc-shared.preinst | 8 +- .../permission-hardener#security-misc-shared | 74 +++++++------------ 2 files changed, 29 insertions(+), 53 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 1c2856cc..43a07ff6 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -11,8 +11,7 @@ shopt -s inherit_errexit shopt -s shift_verbose export LC_ALL=C -## helper-scripts is a Pre-Depends, so it is fully configured and source-able -## here (accountctl.sh provides group_has_nonroot_member). +## helper-scripts is a Pre-Depends. source /usr/libexec/helper-scripts/pre.bsh source /usr/libexec/helper-scripts/has.bsh source /usr/libexec/helper-scripts/accountctl.sh @@ -82,8 +81,6 @@ sudo_users_check () { return 0 fi - ## Any non-root account with sudo access (supplementary member OR primary - ## GID) prevents a lockout, so it satisfies the check. if group_has_nonroot_member sudo; then return 0 fi @@ -128,8 +125,7 @@ console_users_check() { return 0 fi - ## A non-root member (supplementary OR primary GID) of either console group - ## prevents a lockout. + ## Prevent users from locking themselves out. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 if group_has_nonroot_member console \ || group_has_nonroot_member console-unrestricted; then diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index cc7de229..7e92e0a5 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -431,8 +431,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local one_field field_count fso_name meta_list - local split_index candidate_index fallback_index + local one_field field_count fso_name meta_list field_index hit_barrier ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -472,12 +471,20 @@ load_state() { processed_config_line="${line}" - ## We can handle filenames with spaces. The options after a filename are a - ## fixed-width tail: either a lone whitelist keyword, or an octal mode + - ## owner + group + optional capability. The filename is whatever precedes - ## that tail, recovered by anchoring the tail from the right (see the field - ## split below). An owner that itself looks octal is disambiguated by which - ## recovered filename exists on disk. + ## We can handle filenames with spaces. This is because the first item + ## after a filename is either an octal file mode or a keyword, and we + ## only support user/group names, not UIDs and GIDs, as owner + ## identifiers. This means that we can scan from right to left for the + ## furthest-right octal integer or keyword field and use it to separate + ## the filename from the rest of the options. (If usernames could + ## themselves be octal integers, this would break, but adduser.conf's + ## default NAME_REGEX forbids this. This is definitely a hack, but the + ## alternative is to change the format of the policy files, which would + ## break custom config for users. + ## + ## Do not attempt to reimplement this in a way that copes with usernames + ## that look identical to UIDs. Systems containing such usernames are + ## very broken and unsupported. ## ## 'read' will collapse consecutive spaces when splitting on spaces. ## Convert spaces into newlines first, then split on newlines with a @@ -488,48 +495,21 @@ load_state() { done < <(printf '%s\n' "${line}" | tr ' ' '\n') field_count="${#field_list[@]}" - ## The option tail is fixed-width: a lone trailing whitelist keyword, or a - ## mode/owner/group[/capability] tail whose FIRST field is the octal mode. - ## Anchor the tail from the RIGHT. Two readings can be lexically valid when - ## a filename component OR a numeric owner looks octal (mode at field - ## count-3 vs count-4), so disambiguate by which recovered filename exists - ## on disk -- the tool skips nonexistent targets anyway. Fall back to the - ## rightmost octal candidate when neither recovered name exists. + hit_barrier='false' fso_name="" meta_list=() - split_index='' - case "${field_list[field_count-1]}" in - nosuid|exactwhitelist|matchwhitelist|disablewhitelist) - split_index=$(( field_count - 1 )) - ;; - *) - fallback_index='' - for candidate_index in "$(( field_count - 3 ))" "$(( field_count - 4 ))"; do - (( candidate_index >= 1 )) || continue - [[ "${field_list[candidate_index]}" =~ ^[0-7]{3,4}$ ]] || continue - if [ -z "${fallback_index}" ]; then - fallback_index="${candidate_index}" - fi - fso_name="$(IFS=' '; printf '%s' "${field_list[*]:0:candidate_index}")" - if (( candidate_index == 1 )) || [ -e "${fso_name}" ]; then - split_index="${candidate_index}" - break + for (( field_index = field_count - 1; field_index >= 0; \ + field_index-- )); do + if [ "${hit_barrier}" = 'false' ]; then + if [[ "${field_list[field_index]}" =~ ^[0-7]{3,4}$ ]] \ + || [[ "${field_list[field_index]}" =~ ^(nosuid|exactwhitelist|matchwhitelist|disablewhitelist)$ ]]; then + hit_barrier='true' fi - done - if [ -z "${split_index}" ]; then - split_index="${fallback_index}" - fi - ;; - esac - if [ -n "${split_index}" ]; then - fso_name="$(IFS=' '; printf '%s' "${field_list[*]:0:split_index}")" - meta_list=( "${field_list[@]:split_index}" ) - else - ## No recoverable mode/keyword tail: hand every field to the field-count - ## check below, which rejects the line loudly (exit 200). - fso_name="" - meta_list=( "${field_list[@]}" ) - fi + meta_list=( "${field_list[field_index]}" "${meta_list[@]}" ) + else + fso_name="${field_list[field_index]}${fso_name:+" ${fso_name}"}" + fi + done case "${#meta_list[@]}" in 1|3|4) From 9cb4efa8a716343271e3339fb7e13408146b7943 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Sat, 19 Sep 2026 14:09:18 +0000 Subject: [PATCH 51/61] bumped changelog version --- changelog.upstream | 122 +++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 +++ 2 files changed, 128 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index 21cecd65..f8ae1e7e 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,52 @@ +commit f7e4aa5ed2dbf0cd0d9155695ad4fdf315a1181f +Merge: a70c168d 87ff42a1 +Author: Patrick Schleizer +Date: Sat Sep 19 10:00:10 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit 87ff42a19c786050471b5350e228a26a82bd61eb +Author: Aaron Rainbolt +Date: Wed Sep 16 00:01:44 2026 -0400 + + Comments, revert an unreliable feature addition in permission-hardener's state file parsing + +commit 3bc6ec2a046f20d292273095c40352ec6d914a52 +Merge: a70c168d 00bad984 +Author: Aaron Rainbolt +Date: Tue Sep 15 23:26:46 2026 -0400 + + Merge branch 'ai' into arraybolt3/trixie + +commit 00bad98497794d461fe4923e4ff182a61b918df7 +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Tue Sep 15 10:56:10 2026 -0400 + + security-misc: source has.bsh explicitly in preinst + +commit 57a6530574d21307d77a371a1fb53259bfbe22fa +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Tue Sep 15 10:52:46 2026 -0400 + + security-misc: simplify preinst helper-scripts sourcing + + - helper-scripts is a Pre-Depends, configured before this preinst; strict + options first, then plain source -- errexit turns any load failure into a + hard stop, so the bespoke fail-closed handling + type -t check are redundant + +commit f0ea22f6b0f631485fb8f405eb352d35a3d33d84 +Merge: 888a5f67 a70c168d +Author: assisted-by-ai +Date: Tue Sep 15 10:45:48 2026 -0400 + + Merge master into ai (remaster) + +commit a70c168da6730ce86d1ea12f29bff2448734a8a9 +Author: Patrick Schleizer +Date: Tue Sep 15 14:32:35 2026 +0000 + + bumped changelog version + commit 0a004b6675d22e4e54f79f07b086cebe78177280 Author: Patrick Schleizer Date: Tue Sep 15 10:27:21 2026 -0400 @@ -10,6 +59,79 @@ Date: Tue Sep 15 12:00:55 2026 +0000 bumped changelog version +commit 888a5f679d816aeb8c50fc14ab604d27d158863d +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Tue Sep 15 04:27:13 2026 -0400 + + permission-hardener: restore octal-owner filename disambiguation + + - the right-to-left octal scan mis-anchors on a numeric octal-looking owner + (e.g. 0755), folding the real mode into the filename; the nonexistent path is + then silently dropped, leaving a SUID binary un-hardened. + - restore fixed-width tail anchoring + disambiguation by on-disk existence; + test_whitespace_filename.sh (numowner, 'x y') parses again. + +commit c894bbcf75bbe3c63f17b75f0ffc8a0ae2f98ef3 +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Tue Sep 15 02:37:02 2026 -0400 + + preinst: fail closed loudly if helper-scripts library missing + + - source failures were swallowed (errexit not set yet); abort with the real + cause instead. + - a missing group_has_nonroot_member now aborts with the true reason rather + than degrading the lockout check into a misleading 'no account is a member of + sudo' abort. + +commit 239532762966b9fc57f158bb02fb19b04bf1e1bc +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Tue Sep 15 02:16:06 2026 -0400 + + Pre-Depend on helper-scripts; source accountctl.sh, drop vendored copy + + - helper-scripts moved from Depends to Pre-Depends: it is configured before this + package is unpacked, so the preinst can source its libraries. Reuse the real + group_has_nonroot_member from accountctl.sh instead of a vendored copy (and its + drift guard). + - accountctl.sh pulls in has.bsh, so 'has' is available: use it, drop the + no-has waiver and the command -v fallbacks. + +commit 497a23fbb31e2550fc858e6139e84b595be49d20 +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Mon Sep 14 12:49:30 2026 -0400 + + Sync vendored group_has_nonroot_member (reject numeric arg) + +commit 88abbcaa436a86dcd15a37c8ba4f4351b565c050 +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Mon Sep 14 11:13:01 2026 -0400 + + Detect primary-GID members in the install checks + + - sudo_users_check / console_users_check now count a non-root account + whose PRIMARY group is sudo/console (via a vendored + group_has_nonroot_member), not only supplementary members; a + supplementary-only scan falsely aborted install for such an admin. + - Revert the qubesdb-read probe to 'command -v' (no-has waiver): a + preinst runs at unpack time, before helper-scripts is guaranteed + configured, so it must not source has.bsh. + - group_has_nonroot_member is VENDORED from helper-scripts accountctl.sh + (same reason it cannot be sourced); a dist-ai drift guard keeps them + in sync. + +commit dc103b260b2dfd8b94ccdd23aaaaa54000d8d56c +Author: claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291) +Date: Mon Sep 14 09:35:22 2026 -0400 + + Also skip install checks on dist_build_iso; house-style + + - Skip the sudo/console-user install checks when dist_build_iso=true (an + ISO/live build) in addition to the existing skip_install_check file, so + the derivative-maker build no longer needs to write that marker file. + The manual file-based bypass is kept for non-derivative-maker users. + - Modernize to dist-ai house style: strict-mode block, printf not echo, + has not command -v, braced + nounset-guarded variables. + commit 4db7b41bda5f7f2a1d0c1f163a070c998a5a284e Merge: 81d6905a 827e5ee1 Author: Patrick Schleizer diff --git a/debian/changelog b/debian/changelog index 9c7a3e49..5afc2fd5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:54.0-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Sat, 19 Sep 2026 14:09:17 +0000 + security-misc (3:53.9-1) unstable; urgency=medium * New upstream version (local package). From 3ce6b81d5f7f9f1561d33ae0f474d11de8021195 Mon Sep 17 00:00:00 2001 From: "claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5)" Date: Sat, 26 Sep 2026 01:46:37 -0400 Subject: [PATCH 52/61] Add ssh-audit hardening policies, test tool and CI wiring - ssh-audit-test + server/client policies pin the exact hardened algorithm set (allow_algorithm_subset_and_reordering = false); a regression that loosens the ssh config, or a server missing the hardening, now fails loud. - dist-ai consumer wiring runs the config suite per PR and scans production SSH on a schedule. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/dm-consumer.yml | 5 + .github/workflows/consumer-dist-ai-tests.yml | 38 ++++++ .github/workflows/local-dist-ai-ssh-prod.yml | 41 +++++++ debian/security-misc-shared.install | 3 + usr/bin/ssh-audit-test#security-misc-shared | 113 ++++++++++++++++++ .../client.policy#security-misc-shared | 25 ++++ .../server.policy#security-misc-shared | 24 ++++ 7 files changed, 249 insertions(+) create mode 100644 .github/workflows/consumer-dist-ai-tests.yml create mode 100644 .github/workflows/local-dist-ai-ssh-prod.yml create mode 100755 usr/bin/ssh-audit-test#security-misc-shared create mode 100644 usr/share/security-misc/ssh-audit/client.policy#security-misc-shared create mode 100644 usr/share/security-misc/ssh-audit/server.policy#security-misc-shared diff --git a/.github/dm-consumer.yml b/.github/dm-consumer.yml index d4261d16..b61f2810 100644 --- a/.github/dm-consumer.yml +++ b/.github/dm-consumer.yml @@ -37,3 +37,8 @@ codeql-cpp: ## CodeQL's manual build-mode tracing. prepare-command: "ci/codeql-prepare.sh" build-command: "ci/codeql-build.sh" + +dist-ai-tests: + ## The ssh-audit config suite (security-misc-tests) stands up a + ## throwaway sshd and drives ssh-audit; python3-pytest is baseline. + apt-packages: "openssh-server openssh-client ssh-audit" diff --git a/.github/workflows/consumer-dist-ai-tests.yml b/.github/workflows/consumer-dist-ai-tests.yml new file mode 100644 index 00000000..f9894d7e --- /dev/null +++ b/.github/workflows/consumer-dist-ai-tests.yml @@ -0,0 +1,38 @@ +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Managed by pkg_update_consumer_workflows. Byte-identical +## across consumers; this file is `cp`-ed from +## developer-meta-files/consumer-templates/.github/workflows/consumer-dist-ai-tests.yml. +## DO NOT hand-edit this file in the consumer repo - changes +## will be overwritten on the next propagation pass. +## +## Runs this package's comprehensive dist-ai regression suites (which +## live in org-ai-assisted/dist-ai, not in-tree) on each PR and on push +## to master. The component is derived from the repo name by the +## reusable, so this wrapper carries no per-repo state. +## +## Reusable docs: +## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-dist-ai-tests.yml + +name: dist-ai tests + +on: + pull_request: + branches: [master] + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dist-ai-tests: + uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-dist-ai-tests.yml@master diff --git a/.github/workflows/local-dist-ai-ssh-prod.yml b/.github/workflows/local-dist-ai-ssh-prod.yml new file mode 100644 index 00000000..901fd493 --- /dev/null +++ b/.github/workflows/local-dist-ai-ssh-prod.yml @@ -0,0 +1,41 @@ +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Hard-gating scan of the live production SSH servers (kicksecure.com, +## whonix.org) against security-misc's shipped ssh-audit server policy, via the +## dist-ai --e2e suite security-misc-tests-e2e (which lives in +## org-ai-assisted/dist-ai, not in-tree). This is NOT the managed +## consumer-dist-ai-tests.yml (--core) template. +## +## It runs on push to master, on a daily schedule, and on demand -- NOT on +## every pull request: a feature PR cannot change the production servers, so +## gating unrelated PRs on live external state would only wedge them. A failure +## here means a production server dropped (or never had) the security-misc SSH +## hardening; fix is on the host. +## +## Reusable docs: +## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-dist-ai-tests.yml + +name: dist-ai ssh prod + +on: + push: + branches: [master] + schedule: + - cron: '17 6 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dist-ai-ssh-prod: + uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-dist-ai-tests.yml@master + with: + categories: '--e2e' diff --git a/debian/security-misc-shared.install b/debian/security-misc-shared.install index b141aba2..9ace6474 100755 --- a/debian/security-misc-shared.install +++ b/debian/security-misc-shared.install @@ -54,6 +54,7 @@ usr/bin/disabled-thunderbolt-by-security-misc#security-misc-shared => /usr/bin/d usr/bin/fm-shim-frontend#security-misc-shared => /usr/bin/fm-shim-frontend usr/bin/permission-hardener#security-misc-shared => /usr/bin/permission-hardener usr/bin/remount-secure#security-misc-shared => /usr/bin/remount-secure +usr/bin/ssh-audit-test#security-misc-shared => /usr/bin/ssh-audit-test usr/lib/dracut/modules.d-disabled/20remount-secure/module-setup.sh#security-misc-shared => /usr/lib/dracut/modules.d-disabled/20remount-secure/module-setup.sh usr/lib/dracut/modules.d-disabled/20remount-secure/remount-secure.sh#security-misc-shared => /usr/lib/dracut/modules.d-disabled/20remount-secure/remount-secure.sh usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared => /usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh @@ -148,6 +149,8 @@ usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared => /usr/share/ usr/share/security-misc/permission-hardener-existing-mode-legacy-hardcoded#security-misc-shared => /usr/share/security-misc/permission-hardener-existing-mode-legacy-hardcoded usr/share/security-misc/permission-hardener-new-mode-legacy-hardcoded#security-misc-shared => /usr/share/security-misc/permission-hardener-new-mode-legacy-hardcoded usr/share/security-misc/security-misc-memlockd.cfg#security-misc-shared => /usr/share/security-misc/security-misc-memlockd.cfg +usr/share/security-misc/ssh-audit/client.policy#security-misc-shared => /usr/share/security-misc/ssh-audit/client.policy +usr/share/security-misc/ssh-audit/server.policy#security-misc-shared => /usr/share/security-misc/ssh-audit/server.policy usr/src/security-misc/emerg-shutdown.c#security-misc-shared => /usr/src/security-misc/emerg-shutdown.c usr/src/security-misc/fm-shim-backend.c#security-misc-shared => /usr/src/security-misc/fm-shim-backend.c var/cache/security-misc/state-files/placeholder#security-misc-shared => /var/cache/security-misc/state-files/placeholder diff --git a/usr/bin/ssh-audit-test#security-misc-shared b/usr/bin/ssh-audit-test#security-misc-shared new file mode 100755 index 00000000..193ccc9c --- /dev/null +++ b/usr/bin/ssh-audit-test#security-misc-shared @@ -0,0 +1,113 @@ +#!/bin/bash + +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Audit an SSH server against the security-misc hardened ssh-audit policy. +## +## Exit 0 only when the target presents EXACTLY the hardened algorithm set +## shipped by security-misc; any weaker, extra, or reordered algorithm fails +## (the policy sets allow_algorithm_subset_and_reordering = false). No legacy, +## no backwards compatibility. +## +## Usage: ssh-audit-test [--policy FILE] [--port PORT] [HOST ...] +## default target 127.0.0.1:22, default policy the installed server policy. +## +## style-ok: no-has (self-contained; helper-scripts not sourced) + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose +export LC_ALL=C + +me="${0##*/}" + +die() { + printf '%s\n' "${me}: ERROR: $1" >&2 + exit 1 +} + +info() { + printf '%s\n' "${me}: $1" >&2 +} + +policy="" +port="22" +hosts=() + +while [ "$#" -gt 0 ]; do + case "$1" in + --policy) + shift + policy="${1:-}" + ;; + --port) + shift + port="${1:-}" + ;; + --) + shift + while [ "$#" -gt 0 ]; do + hosts+=( "$1" ) + shift + done + break + ;; + -*) + die "unknown option: $1" + ;; + *) + hosts+=( "$1" ) + ;; + esac + shift +done + +command -v ssh-audit >/dev/null 2>&1 \ + || die "ssh-audit not installed" + +## Resolve the server policy: explicit --policy, else the installed file, else +## the in-repo source (SECURITY_MISC_REPO, whose tree keeps the genmkfile +## '#security-misc-shared' suffix). +if [ -z "${policy}" ]; then + for candidate in \ + "/usr/share/security-misc/ssh-audit/server.policy" \ + "${SECURITY_MISC_REPO:-}/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared"; do + if [ -n "${candidate}" ] && [ -f "${candidate}" ]; then + policy="${candidate}" + break + fi + done +fi + +if [ -z "${policy}" ] || [ ! -f "${policy}" ]; then + die "server policy not found (pass --policy FILE)" +fi + +if [ "${#hosts[@]}" -eq 0 ]; then + hosts=( "127.0.0.1" ) +fi + +failed=0 +for host in "${hosts[@]}"; do + rc=0 + out="$(ssh-audit --port="${port}" --policy="${policy}" -- "${host}" 2>&1)" || rc=$? + if [ "${rc}" -eq 0 ]; then + info "PASS: ${host}:${port} matches ${policy##*/}" + else + failed=$(( failed + 1 )) + printf '%s\n' "${out}" + info "FAIL: ${host}:${port} does not match ${policy##*/}" + fi +done + +if [ "${failed}" -ne 0 ]; then + die "${failed} host(s) failed the hardened policy" +fi + +info "OK: all host(s) match the hardened policy" diff --git a/usr/share/security-misc/ssh-audit/client.policy#security-misc-shared b/usr/share/security-misc/ssh-audit/client.policy#security-misc-shared new file mode 100644 index 00000000..0b45004d --- /dev/null +++ b/usr/share/security-misc/ssh-audit/client.policy#security-misc-shared @@ -0,0 +1,25 @@ +# +# security-misc hardened OpenSSH CLIENT policy (ssh-audit, client-audit mode). +# +# Source of truth: etc/ssh/ssh_config.d/30_security-misc.conf. This policy +# MUST stay in sync with that file's Ciphers / MACs / KexAlgorithms / +# HostKeyAlgorithms; the dist-ai drift-guard test +# (security-misc-tests: test_ssh_config.py) fails if they diverge. +# +# The client set intentionally keeps aes256-ctr (interop headroom) which the +# server set omits; that asymmetry is deliberate, not legacy. +# +# allow_algorithm_subset_and_reordering = false makes this an EXACT match. +# + +client policy = true +name = "security-misc hardened OpenSSH client" +version = 1 + +allow_algorithm_subset_and_reordering = false +allow_larger_keys = false + +host keys = sk-ssh-ed25519@openssh.com, ssh-ed25519 +key exchanges = sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ext-info-c, kex-strict-c-v00@openssh.com +ciphers = chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes256-ctr +macs = umac-128-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com diff --git a/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared b/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared new file mode 100644 index 00000000..37aec5e4 --- /dev/null +++ b/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared @@ -0,0 +1,24 @@ +# +# security-misc hardened OpenSSH SERVER policy (ssh-audit). +# +# Source of truth: etc/ssh/sshd_config.d/30_security-misc.conf. This policy +# MUST stay in sync with that file's Ciphers / MACs / KexAlgorithms / +# HostKeyAlgorithms; the dist-ai drift-guard test +# (security-misc-tests: test_ssh_config.py) fails if they diverge. +# +# allow_algorithm_subset_and_reordering = false makes this an EXACT match: +# any weaker, extra, or reordered algorithm fails the policy. No legacy, no +# backwards compatibility. +# + +name = "security-misc hardened OpenSSH server" +version = 1 + +allow_algorithm_subset_and_reordering = false +allow_larger_keys = false + +host keys = ssh-ed25519 +host_key_sizes = {"ssh-ed25519": {"hostkey_size": 256}} +key exchanges = sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ext-info-s, kex-strict-s-v00@openssh.com +ciphers = chacha20-poly1305@openssh.com, aes256-gcm@openssh.com +macs = umac-128-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com From 6d70b404e1812ade14912840263bf4671890dcc6 Mon Sep 17 00:00:00 2001 From: "claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5)" Date: Sat, 26 Sep 2026 02:54:03 -0400 Subject: [PATCH 53/61] ssh-audit-test: clean error on missing option operand - --policy / --port with no value crashed on shift (shift count out of range) instead of reporting the missing operand; validate and die cleanly. - Document that the ssh-audit policy check is server-to-client; OpenSSH sets both directions symmetrically, so it is complete for our OpenSSH targets. Co-Authored-By: Claude Opus 4.8 (1M context) --- usr/bin/ssh-audit-test#security-misc-shared | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/usr/bin/ssh-audit-test#security-misc-shared b/usr/bin/ssh-audit-test#security-misc-shared index 193ccc9c..cb19fa42 100755 --- a/usr/bin/ssh-audit-test#security-misc-shared +++ b/usr/bin/ssh-audit-test#security-misc-shared @@ -12,6 +12,12 @@ ## (the policy sets allow_algorithm_subset_and_reordering = false). No legacy, ## no backwards compatibility. ## +## Scope: ssh-audit's policy compares the server-to-client algorithm lists. +## OpenSSH's Ciphers/MACs/KexAlgorithms apply to BOTH directions from a single +## list, so a security-misc-configured (OpenSSH) target is symmetric and the +## check is complete. It would not catch a non-OpenSSH peer offering weaker +## client-to-server algorithms only, which is not a target here. +## ## Usage: ssh-audit-test [--policy FILE] [--port PORT] [HOST ...] ## default target 127.0.0.1:22, default policy the installed server policy. ## @@ -43,12 +49,18 @@ hosts=() while [ "$#" -gt 0 ]; do case "$1" in --policy) - shift - policy="${1:-}" + if [ "$#" -lt 2 ]; then + die "option --policy requires a value" + fi + policy="$2" + shift 2 ;; --port) - shift - port="${1:-}" + if [ "$#" -lt 2 ]; then + die "option --port requires a value" + fi + port="$2" + shift 2 ;; --) shift @@ -63,9 +75,9 @@ while [ "$#" -gt 0 ]; do ;; *) hosts+=( "$1" ) + shift ;; esac - shift done command -v ssh-audit >/dev/null 2>&1 \ From fcbf940c25f57af3ddf2ec8dda1e2f8e95847bda Mon Sep 17 00:00:00 2001 From: "claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5)" Date: Sun, 27 Sep 2026 04:21:49 -0400 Subject: [PATCH 54/61] Move ssh-audit test tool + policies out to dist-ai - The ssh-audit-test driver and the server/client ssh-audit policies are test artifacts, not runtime hardening; they now live in the dist-ai security-misc-tests suite. security-misc's shipped package stays lean. - Kept here: only the CI wiring (dm-consumer.yml + the dist-ai-tests / ssh-prod workflows), which is inherently per-consumer-repo. The dist-ai drift-guard test keeps the moved policies in sync with the ssh config drop-ins that remain the source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/security-misc-shared.install | 3 - usr/bin/ssh-audit-test#security-misc-shared | 125 ------------------ .../client.policy#security-misc-shared | 25 ---- .../server.policy#security-misc-shared | 24 ---- 4 files changed, 177 deletions(-) delete mode 100755 usr/bin/ssh-audit-test#security-misc-shared delete mode 100644 usr/share/security-misc/ssh-audit/client.policy#security-misc-shared delete mode 100644 usr/share/security-misc/ssh-audit/server.policy#security-misc-shared diff --git a/debian/security-misc-shared.install b/debian/security-misc-shared.install index 9ace6474..b141aba2 100755 --- a/debian/security-misc-shared.install +++ b/debian/security-misc-shared.install @@ -54,7 +54,6 @@ usr/bin/disabled-thunderbolt-by-security-misc#security-misc-shared => /usr/bin/d usr/bin/fm-shim-frontend#security-misc-shared => /usr/bin/fm-shim-frontend usr/bin/permission-hardener#security-misc-shared => /usr/bin/permission-hardener usr/bin/remount-secure#security-misc-shared => /usr/bin/remount-secure -usr/bin/ssh-audit-test#security-misc-shared => /usr/bin/ssh-audit-test usr/lib/dracut/modules.d-disabled/20remount-secure/module-setup.sh#security-misc-shared => /usr/lib/dracut/modules.d-disabled/20remount-secure/module-setup.sh usr/lib/dracut/modules.d-disabled/20remount-secure/remount-secure.sh#security-misc-shared => /usr/lib/dracut/modules.d-disabled/20remount-secure/remount-secure.sh usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared => /usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh @@ -149,8 +148,6 @@ usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared => /usr/share/ usr/share/security-misc/permission-hardener-existing-mode-legacy-hardcoded#security-misc-shared => /usr/share/security-misc/permission-hardener-existing-mode-legacy-hardcoded usr/share/security-misc/permission-hardener-new-mode-legacy-hardcoded#security-misc-shared => /usr/share/security-misc/permission-hardener-new-mode-legacy-hardcoded usr/share/security-misc/security-misc-memlockd.cfg#security-misc-shared => /usr/share/security-misc/security-misc-memlockd.cfg -usr/share/security-misc/ssh-audit/client.policy#security-misc-shared => /usr/share/security-misc/ssh-audit/client.policy -usr/share/security-misc/ssh-audit/server.policy#security-misc-shared => /usr/share/security-misc/ssh-audit/server.policy usr/src/security-misc/emerg-shutdown.c#security-misc-shared => /usr/src/security-misc/emerg-shutdown.c usr/src/security-misc/fm-shim-backend.c#security-misc-shared => /usr/src/security-misc/fm-shim-backend.c var/cache/security-misc/state-files/placeholder#security-misc-shared => /var/cache/security-misc/state-files/placeholder diff --git a/usr/bin/ssh-audit-test#security-misc-shared b/usr/bin/ssh-audit-test#security-misc-shared deleted file mode 100755 index cb19fa42..00000000 --- a/usr/bin/ssh-audit-test#security-misc-shared +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash - -## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## AI-Assisted - -## Audit an SSH server against the security-misc hardened ssh-audit policy. -## -## Exit 0 only when the target presents EXACTLY the hardened algorithm set -## shipped by security-misc; any weaker, extra, or reordered algorithm fails -## (the policy sets allow_algorithm_subset_and_reordering = false). No legacy, -## no backwards compatibility. -## -## Scope: ssh-audit's policy compares the server-to-client algorithm lists. -## OpenSSH's Ciphers/MACs/KexAlgorithms apply to BOTH directions from a single -## list, so a security-misc-configured (OpenSSH) target is symmetric and the -## check is complete. It would not catch a non-OpenSSH peer offering weaker -## client-to-server algorithms only, which is not a target here. -## -## Usage: ssh-audit-test [--policy FILE] [--port PORT] [HOST ...] -## default target 127.0.0.1:22, default policy the installed server policy. -## -## style-ok: no-has (self-contained; helper-scripts not sourced) - -set -o errexit -set -o nounset -set -o pipefail -set -o errtrace -shopt -s inherit_errexit -shopt -s shift_verbose -export LC_ALL=C - -me="${0##*/}" - -die() { - printf '%s\n' "${me}: ERROR: $1" >&2 - exit 1 -} - -info() { - printf '%s\n' "${me}: $1" >&2 -} - -policy="" -port="22" -hosts=() - -while [ "$#" -gt 0 ]; do - case "$1" in - --policy) - if [ "$#" -lt 2 ]; then - die "option --policy requires a value" - fi - policy="$2" - shift 2 - ;; - --port) - if [ "$#" -lt 2 ]; then - die "option --port requires a value" - fi - port="$2" - shift 2 - ;; - --) - shift - while [ "$#" -gt 0 ]; do - hosts+=( "$1" ) - shift - done - break - ;; - -*) - die "unknown option: $1" - ;; - *) - hosts+=( "$1" ) - shift - ;; - esac -done - -command -v ssh-audit >/dev/null 2>&1 \ - || die "ssh-audit not installed" - -## Resolve the server policy: explicit --policy, else the installed file, else -## the in-repo source (SECURITY_MISC_REPO, whose tree keeps the genmkfile -## '#security-misc-shared' suffix). -if [ -z "${policy}" ]; then - for candidate in \ - "/usr/share/security-misc/ssh-audit/server.policy" \ - "${SECURITY_MISC_REPO:-}/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared"; do - if [ -n "${candidate}" ] && [ -f "${candidate}" ]; then - policy="${candidate}" - break - fi - done -fi - -if [ -z "${policy}" ] || [ ! -f "${policy}" ]; then - die "server policy not found (pass --policy FILE)" -fi - -if [ "${#hosts[@]}" -eq 0 ]; then - hosts=( "127.0.0.1" ) -fi - -failed=0 -for host in "${hosts[@]}"; do - rc=0 - out="$(ssh-audit --port="${port}" --policy="${policy}" -- "${host}" 2>&1)" || rc=$? - if [ "${rc}" -eq 0 ]; then - info "PASS: ${host}:${port} matches ${policy##*/}" - else - failed=$(( failed + 1 )) - printf '%s\n' "${out}" - info "FAIL: ${host}:${port} does not match ${policy##*/}" - fi -done - -if [ "${failed}" -ne 0 ]; then - die "${failed} host(s) failed the hardened policy" -fi - -info "OK: all host(s) match the hardened policy" diff --git a/usr/share/security-misc/ssh-audit/client.policy#security-misc-shared b/usr/share/security-misc/ssh-audit/client.policy#security-misc-shared deleted file mode 100644 index 0b45004d..00000000 --- a/usr/share/security-misc/ssh-audit/client.policy#security-misc-shared +++ /dev/null @@ -1,25 +0,0 @@ -# -# security-misc hardened OpenSSH CLIENT policy (ssh-audit, client-audit mode). -# -# Source of truth: etc/ssh/ssh_config.d/30_security-misc.conf. This policy -# MUST stay in sync with that file's Ciphers / MACs / KexAlgorithms / -# HostKeyAlgorithms; the dist-ai drift-guard test -# (security-misc-tests: test_ssh_config.py) fails if they diverge. -# -# The client set intentionally keeps aes256-ctr (interop headroom) which the -# server set omits; that asymmetry is deliberate, not legacy. -# -# allow_algorithm_subset_and_reordering = false makes this an EXACT match. -# - -client policy = true -name = "security-misc hardened OpenSSH client" -version = 1 - -allow_algorithm_subset_and_reordering = false -allow_larger_keys = false - -host keys = sk-ssh-ed25519@openssh.com, ssh-ed25519 -key exchanges = sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ext-info-c, kex-strict-c-v00@openssh.com -ciphers = chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes256-ctr -macs = umac-128-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com diff --git a/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared b/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared deleted file mode 100644 index 37aec5e4..00000000 --- a/usr/share/security-misc/ssh-audit/server.policy#security-misc-shared +++ /dev/null @@ -1,24 +0,0 @@ -# -# security-misc hardened OpenSSH SERVER policy (ssh-audit). -# -# Source of truth: etc/ssh/sshd_config.d/30_security-misc.conf. This policy -# MUST stay in sync with that file's Ciphers / MACs / KexAlgorithms / -# HostKeyAlgorithms; the dist-ai drift-guard test -# (security-misc-tests: test_ssh_config.py) fails if they diverge. -# -# allow_algorithm_subset_and_reordering = false makes this an EXACT match: -# any weaker, extra, or reordered algorithm fails the policy. No legacy, no -# backwards compatibility. -# - -name = "security-misc hardened OpenSSH server" -version = 1 - -allow_algorithm_subset_and_reordering = false -allow_larger_keys = false - -host keys = ssh-ed25519 -host_key_sizes = {"ssh-ed25519": {"hostkey_size": 256}} -key exchanges = sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ext-info-s, kex-strict-s-v00@openssh.com -ciphers = chacha20-poly1305@openssh.com, aes256-gcm@openssh.com -macs = umac-128-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com From 0a35d857cf38f02e99389bbfc0e299619b4105da Mon Sep 17 00:00:00 2001 From: "claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5)" Date: Sun, 27 Sep 2026 08:22:31 -0400 Subject: [PATCH 55/61] sshd: post-quantum-only kex + aes256-gcm (sshaudit.com A+/100) - Server default now offers PQ-only key exchange (drop classical curve25519) and an AEAD, Terrapin-immune cipher (aes256-gcm, drop chacha20). This is the A+ / 100-of-100 sshaudit.com profile (verified equivalent to grapheneos.org): resists harvest-now-decrypt-later and removes chacha20's residual Terrapin exposure for unpatched peers. - Interop: requires an OpenSSH 9.0+ (post-quantum) peer. Client config stays broad so outbound SSH to older servers is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../30_security-misc.conf#security-misc-shared | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared index 45496fd5..830f3d67 100644 --- a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared @@ -12,15 +12,23 @@ MaxAuthTries 3 ## Require strong ciphers and algorithms. +## +## Post-quantum-ONLY key exchange (no classical curve25519) and an AEAD, +## Terrapin-immune cipher (aes256-gcm, no chacha20). This is the A+ / 100-of-100 +## sshaudit.com server profile: it resists harvest-now-decrypt-later, and +## removes the residual Terrapin (CVE-2023-48795) exposure that chacha20 leaves +## for unpatched peers. +## +## Interop note: this REQUIRES a post-quantum-capable peer -- OpenSSH 9.0+ +## (sntrup761) or 9.9+ (mlkem). Pre-9.0 / non-post-quantum clients cannot +## connect to this server. The client config (ssh_config.d) stays broad so +## outbound SSH to older servers still works. HostKey /etc/ssh/ssh_host_ed25519_key HostKeyAlgorithms ssh-ed25519 PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com -Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com +Ciphers aes256-gcm@openssh.com MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com -KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org -## To force the use of quantum-resistant key exchange algorithms, override the -## above with -# KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 +KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 ## Override with 'no' to fully deny root login, or leave this as ## 'prohibit-password' for denying root password login but still allowing From d217e28daaaa3ef05d3f489180ec920eb2a4e18a Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Sun, 27 Sep 2026 21:11:28 -0400 Subject: [PATCH 56/61] Undo dangerous SSH settings changes applied by AI --- ...30_security-misc.conf#security-misc-shared | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared index 830f3d67..c9f01d87 100644 --- a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared @@ -12,23 +12,21 @@ MaxAuthTries 3 ## Require strong ciphers and algorithms. -## -## Post-quantum-ONLY key exchange (no classical curve25519) and an AEAD, -## Terrapin-immune cipher (aes256-gcm, no chacha20). This is the A+ / 100-of-100 -## sshaudit.com server profile: it resists harvest-now-decrypt-later, and -## removes the residual Terrapin (CVE-2023-48795) exposure that chacha20 leaves -## for unpatched peers. -## -## Interop note: this REQUIRES a post-quantum-capable peer -- OpenSSH 9.0+ -## (sntrup761) or 9.9+ (mlkem). Pre-9.0 / non-post-quantum clients cannot -## connect to this server. The client config (ssh_config.d) stays broad so -## outbound SSH to older servers still works. HostKey /etc/ssh/ssh_host_ed25519_key HostKeyAlgorithms ssh-ed25519 PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com -Ciphers aes256-gcm@openssh.com +## chacha20-poly1305 preferred intentionally. Better to use good crypto than to +## mitigate an attack that only affects people using old and insecure OpenSSH +## clients. +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com -KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 +## Disabling non-quantum-resistant kex algorithms for users in the wild is too +## risky for now. +## REMINDER: Remove Curve25519 from the kex algorithm list for Forky +KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org +## To force the use of quantum-resistant key exchange algorithms, override the +## above with +# KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 ## Override with 'no' to fully deny root login, or leave this as ## 'prohibit-password' for denying root password login but still allowing From 802205a4b8e9519ebd04eda34f5428ec8719ed59 Mon Sep 17 00:00:00 2001 From: "claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5)" Date: Mon, 28 Sep 2026 01:19:07 -0400 Subject: [PATCH 57/61] ssh client: post-quantum-only kex + aes256-gcm (match server) - The client default now uses the same strictest set as the server: PQ-only key exchange and the aes256-gcm cipher only (drop curve25519, chacha20, aes256-ctr). Outbound SSH therefore only reaches post-quantum-capable servers; override per-host in ~/.ssh/config or a 50_user.conf drop-in for a server with a narrower algorithm set. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../30_security-misc.conf#security-misc-shared | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared index b582dd12..0a0b9b26 100644 --- a/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared @@ -11,12 +11,14 @@ ## https://www.kicksecure.com/wiki/SSH#Client_Configuration_File Host * + ## Strictest posture, matching the server: post-quantum-only key exchange and + ## the aes256-gcm cipher only. Outbound SSH therefore only succeeds to a + ## post-quantum-capable server; to reach a server that offers a narrower + ## algorithm set, override per-host in '~/.ssh/config' or in a higher-numbered + ## '/etc/ssh/ssh_config.d/50_user.conf' drop-in. VisualHostKey yes - Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr + Ciphers aes256-gcm@openssh.com MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com - KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org - ## To force the use of quantum-resistant key exchange algorithms, override - ## the above with - # KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 + KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519 PubkeyAcceptedAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519 From 96c0366cdc53005b5a06104d6c9c325c12c87882 Mon Sep 17 00:00:00 2001 From: "claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5)" Date: Mon, 28 Sep 2026 02:58:45 -0400 Subject: [PATCH 58/61] ssh: fix override advice; sync interop comments - OpenSSH is first-wins: a lower-numbered drop-in (or ~/.ssh/config) overrides, not a higher-numbered one - server comment no longer claims the client stays broad; the client config is equally strict Co-Authored-By: Claude Opus 4.8 (1M context) --- ...30_security-misc.conf#security-misc-shared | 14 ++++++------- ...30_security-misc.conf#security-misc-shared | 21 ++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared index 0a0b9b26..51d228ca 100644 --- a/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared @@ -1,11 +1,11 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## Don't edit this file, to overwrite any options, edit a file with a higher -## number that is read later by SSH, such as -## '/etc/ssh/ssh_config.d/50_user.conf'. If your configuration changes do not -## need to be system-wide, you may also consider placing overrides in -## ~/.ssh/config. +## Don't edit this file. OpenSSH uses the FIRST value it obtains for each option. +## To override an option per-user, use '~/.ssh/config' (read before the system +## configuration). To override system-wide, use a drop-in with a LOWER number +## than this one, such as '/etc/ssh/ssh_config.d/20_user.conf' (drop-ins are read +## in lexical order). ## See also: ## https://www.kicksecure.com/wiki/SSH#Client_Configuration_File @@ -14,8 +14,8 @@ Host * ## Strictest posture, matching the server: post-quantum-only key exchange and ## the aes256-gcm cipher only. Outbound SSH therefore only succeeds to a ## post-quantum-capable server; to reach a server that offers a narrower - ## algorithm set, override per-host in '~/.ssh/config' or in a higher-numbered - ## '/etc/ssh/ssh_config.d/50_user.conf' drop-in. + ## algorithm set, override per-host in '~/.ssh/config' (SSH reads it before the + ## system configuration, so its value wins). VisualHostKey yes Ciphers aes256-gcm@openssh.com MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com diff --git a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared index 830f3d67..8a3cb557 100644 --- a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared @@ -1,9 +1,10 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## Don't edit this file, to overwrite any options, edit a file with a higher -## number that is read later by SSHD, such as -## '/etc/ssh/sshd_config.d/50_user.conf'. +## Don't edit this file. OpenSSH uses the FIRST value it obtains for each option +## and reads drop-ins in lexical order, so to override an option use a drop-in +## with a LOWER number than this one, such as +## '/etc/ssh/sshd_config.d/20_user.conf'. ## See also: ## https://www.kicksecure.com/wiki/SSH#Server_Configuration_File @@ -13,16 +14,12 @@ MaxAuthTries 3 ## Require strong ciphers and algorithms. ## -## Post-quantum-ONLY key exchange (no classical curve25519) and an AEAD, -## Terrapin-immune cipher (aes256-gcm, no chacha20). This is the A+ / 100-of-100 -## sshaudit.com server profile: it resists harvest-now-decrypt-later, and -## removes the residual Terrapin (CVE-2023-48795) exposure that chacha20 leaves -## for unpatched peers. +## Post-quantum-ONLY key exchange (no classical curve25519) and an AEAD-only +## cipher (aes256-gcm, no chacha20). Resists harvest-now-decrypt-later capture. ## -## Interop note: this REQUIRES a post-quantum-capable peer -- OpenSSH 9.0+ -## (sntrup761) or 9.9+ (mlkem). Pre-9.0 / non-post-quantum clients cannot -## connect to this server. The client config (ssh_config.d) stays broad so -## outbound SSH to older servers still works. +## Interop note: this REQUIRES a post-quantum-capable peer; a client without +## post-quantum key exchange cannot connect. The client config (ssh_config.d) is +## equally strict. HostKey /etc/ssh/ssh_host_ed25519_key HostKeyAlgorithms ssh-ed25519 PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com From d80e26defda7271a513cb6ba8ddee7c3e9ae83cf Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Tue, 29 Sep 2026 08:08:57 +0000 Subject: [PATCH 59/61] bumped changelog version --- changelog.upstream | 92 ++++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 +++ 2 files changed, 98 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index f8ae1e7e..7fda5777 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,95 @@ +commit aba02ec3634c006ea993c4609cf9a31c2e65a2b4 +Merge: 9cb4efa8 d217e28d +Author: Patrick Schleizer +Date: Mon Sep 28 02:40:50 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit d217e28daaaa3ef05d3f489180ec920eb2a4e18a +Author: Aaron Rainbolt +Date: Sun Sep 27 21:11:28 2026 -0400 + + Undo dangerous SSH settings changes applied by AI + +commit 6cee7b5659c60bd6e2f995786c178cb575f8c980 +Merge: 9cb4efa8 0a35d857 +Author: Aaron Rainbolt +Date: Sun Sep 27 20:49:32 2026 -0400 + + Merge branch 'ai' into arraybolt3/trixie + +commit 0a35d857cf38f02e99389bbfc0e299619b4105da +Author: claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5) +Date: Sun Sep 27 08:22:31 2026 -0400 + + sshd: post-quantum-only kex + aes256-gcm (sshaudit.com A+/100) + + - Server default now offers PQ-only key exchange (drop classical curve25519) + and an AEAD, Terrapin-immune cipher (aes256-gcm, drop chacha20). This is the + A+ / 100-of-100 sshaudit.com profile (verified equivalent to grapheneos.org): + resists harvest-now-decrypt-later and removes chacha20's residual Terrapin + exposure for unpatched peers. + - Interop: requires an OpenSSH 9.0+ (post-quantum) peer. Client config stays + broad so outbound SSH to older servers is unaffected. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit fcbf940c25f57af3ddf2ec8dda1e2f8e95847bda +Author: claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5) +Date: Sun Sep 27 04:21:49 2026 -0400 + + Move ssh-audit test tool + policies out to dist-ai + + - The ssh-audit-test driver and the server/client ssh-audit policies are test + artifacts, not runtime hardening; they now live in the dist-ai + security-misc-tests suite. security-misc's shipped package stays lean. + - Kept here: only the CI wiring (dm-consumer.yml + the dist-ai-tests / + ssh-prod workflows), which is inherently per-consumer-repo. The dist-ai + drift-guard test keeps the moved policies in sync with the ssh config drop-ins + that remain the source of truth. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 6d70b404e1812ade14912840263bf4671890dcc6 +Author: claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5) +Date: Sat Sep 26 02:54:03 2026 -0400 + + ssh-audit-test: clean error on missing option operand + + - --policy / --port with no value crashed on shift (shift count out of range) + instead of reporting the missing operand; validate and die cleanly. + - Document that the ssh-audit policy check is server-to-client; OpenSSH sets + both directions symmetrically, so it is complete for our OpenSSH targets. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 3ce6b81d5f7f9f1561d33ae0f474d11de8021195 +Author: claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5) +Date: Sat Sep 26 01:46:37 2026 -0400 + + Add ssh-audit hardening policies, test tool and CI wiring + + - ssh-audit-test + server/client policies pin the exact hardened algorithm + set (allow_algorithm_subset_and_reordering = false); a regression that + loosens the ssh config, or a server missing the hardening, now fails loud. + - dist-ai consumer wiring runs the config suite per PR and scans production + SSH on a schedule. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 2bf04eee21a66fed4f3c45556ebd362cfc6f810f +Merge: 00bad984 9cb4efa8 +Author: assisted-by-ai +Date: Thu Sep 24 22:51:46 2026 -0400 + + Merge master into ai (remaster) + +commit 9cb4efa8a716343271e3339fb7e13408146b7943 +Author: Patrick Schleizer +Date: Sat Sep 19 14:09:18 2026 +0000 + + bumped changelog version + commit f7e4aa5ed2dbf0cd0d9155695ad4fdf315a1181f Merge: a70c168d 87ff42a1 Author: Patrick Schleizer diff --git a/debian/changelog b/debian/changelog index 5afc2fd5..4265698d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:54.1-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Tue, 29 Sep 2026 08:08:57 +0000 + security-misc (3:54.0-1) unstable; urgency=medium * New upstream version (local package). From cbba86cb06d7acd46aaf780f9cd1e5ab4ac97d8d Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Tue, 29 Sep 2026 22:57:46 -0400 Subject: [PATCH 60/61] Polish, reintroduce removed ciphers again but keep the requirement to use post-quantum key exchange --- ...30_security-misc.conf#security-misc-shared | 22 +++++++++---------- ...30_security-misc.conf#security-misc-shared | 21 ++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared index 51d228ca..67ad1e86 100644 --- a/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/ssh_config.d/30_security-misc.conf#security-misc-shared @@ -1,24 +1,24 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## Don't edit this file. OpenSSH uses the FIRST value it obtains for each option. -## To override an option per-user, use '~/.ssh/config' (read before the system -## configuration). To override system-wide, use a drop-in with a LOWER number -## than this one, such as '/etc/ssh/ssh_config.d/20_user.conf' (drop-ins are read -## in lexical order). +## Don't edit this file; to overwrite any options, edit a file with a higher +## number that is read earlier by SSH, such as +## '/etc/ssh/ssh_config.d/10_user.conf'. If your configuration changes do not +## need to be system-wide, you may also consider placing overrides in +## ~/.ssh/config. ## See also: ## https://www.kicksecure.com/wiki/SSH#Client_Configuration_File Host * - ## Strictest posture, matching the server: post-quantum-only key exchange and - ## the aes256-gcm cipher only. Outbound SSH therefore only succeeds to a - ## post-quantum-capable server; to reach a server that offers a narrower - ## algorithm set, override per-host in '~/.ssh/config' (SSH reads it before the - ## system configuration, so its value wins). VisualHostKey yes - Ciphers aes256-gcm@openssh.com + ## Ciphers that are Terrapin-vulnerable when an outdated client or server are + ## used are intentionally retained. Connecting to a Terrapin-vulnerable + ## server cannot be made secure (as any Terrapin-vulnerable server will be + ## vulnerable to other issues as well) and is therefore unsupported. + Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com + ## Only post-quantum-sequre KexAlgorithms are allowed. KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519 PubkeyAcceptedAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519 diff --git a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared index 8a3cb557..6d52003d 100644 --- a/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared +++ b/etc/ssh/sshd_config.d/30_security-misc.conf#security-misc-shared @@ -1,10 +1,9 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## Don't edit this file. OpenSSH uses the FIRST value it obtains for each option -## and reads drop-ins in lexical order, so to override an option use a drop-in -## with a LOWER number than this one, such as -## '/etc/ssh/sshd_config.d/20_user.conf'. +## Don't edit this file; to overwrite any options, edit a file with a lower +## number that is read earlier by sshd, such as +## '/etc/ssh/sshd_config.d/10_user.conf'. ## See also: ## https://www.kicksecure.com/wiki/SSH#Server_Configuration_File @@ -13,18 +12,16 @@ MaxAuthTries 3 ## Require strong ciphers and algorithms. -## -## Post-quantum-ONLY key exchange (no classical curve25519) and an AEAD-only -## cipher (aes256-gcm, no chacha20). Resists harvest-now-decrypt-later capture. -## -## Interop note: this REQUIRES a post-quantum-capable peer; a client without -## post-quantum key exchange cannot connect. The client config (ssh_config.d) is -## equally strict. HostKey /etc/ssh/ssh_host_ed25519_key HostKeyAlgorithms ssh-ed25519 PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com -Ciphers aes256-gcm@openssh.com +## Ciphers that are Terrapin-vulnerable when an outdated client or server are +## used are intentionally retained. Connections from a Terrapin-vulnerable +## client cannot be made secure (as any Terrapin-vulnerable client will be +## vulnerable to other issues as well) and is therefore unsupported. +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com MACs umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com +## Only post-quantum-sequre KexAlgorithms are allowed. KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256 ## Override with 'no' to fully deny root login, or leave this as From 9535bf8e9c37bb65d1b4cabb39b7f6bc5ee9f29f Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Wed, 30 Sep 2026 08:33:58 +0000 Subject: [PATCH 61/61] bumped changelog version --- changelog.upstream | 59 ++++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 +++++ 2 files changed, 65 insertions(+) diff --git a/changelog.upstream b/changelog.upstream index 7fda5777..7d8ff45c 100644 --- a/changelog.upstream +++ b/changelog.upstream @@ -1,3 +1,48 @@ +commit 7f60beb8bffa344453bd6794f92ff72b6098edf3 +Merge: d80e26de cbba86cb +Author: Patrick Schleizer +Date: Wed Sep 30 04:20:30 2026 -0400 + + Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' + +commit cbba86cb06d7acd46aaf780f9cd1e5ab4ac97d8d +Author: Aaron Rainbolt +Date: Tue Sep 29 22:57:46 2026 -0400 + + Polish, reintroduce removed ciphers again but keep the requirement to use post-quantum key exchange + +commit 5202e7564b9133ae267c6064c88e69d23d45cd87 +Merge: d80e26de c3b4e219 +Author: Aaron Rainbolt +Date: Tue Sep 29 22:33:57 2026 -0400 + + Merge branch 'ai' into arraybolt3/trixie + +commit c3b4e219acaf5b853c8a3d71f32360ae5db3aa32 +Merge: 96c0366c d217e28d +Author: assisted-by-ai +Date: Tue Sep 29 04:28:20 2026 -0400 + + Merge arraybolt3/trixie into ai (remaster) + +commit d80e26defda7271a513cb6ba8ddee7c3e9ae83cf +Author: Patrick Schleizer +Date: Tue Sep 29 08:08:57 2026 +0000 + + bumped changelog version + +commit 96c0366cdc53005b5a06104d6c9c325c12c87882 +Author: claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5) +Date: Mon Sep 28 02:58:45 2026 -0400 + + ssh: fix override advice; sync interop comments + + - OpenSSH is first-wins: a lower-numbered drop-in (or ~/.ssh/config) overrides, not a higher-numbered one + + - server comment no longer claims the client stays broad; the client config is equally strict + + Co-Authored-By: Claude Opus 4.8 (1M context) + commit aba02ec3634c006ea993c4609cf9a31c2e65a2b4 Merge: 9cb4efa8 d217e28d Author: Patrick Schleizer @@ -5,6 +50,20 @@ Date: Mon Sep 28 02:40:50 2026 -0400 Merge remote-tracking branch 'refs/remotes/ArrayBolt3/arraybolt3/trixie' +commit 802205a4b8e9519ebd04eda34f5428ec8719ed59 +Author: claude (dev706 06a4d27a-451a-4444-b924-ffce1da248b5) +Date: Mon Sep 28 01:19:07 2026 -0400 + + ssh client: post-quantum-only kex + aes256-gcm (match server) + + - The client default now uses the same strictest set as the server: PQ-only + key exchange and the aes256-gcm cipher only (drop curve25519, chacha20, + aes256-ctr). Outbound SSH therefore only reaches post-quantum-capable + servers; override per-host in ~/.ssh/config or a 50_user.conf drop-in for a + server with a narrower algorithm set. + + Co-Authored-By: Claude Opus 4.8 (1M context) + commit d217e28daaaa3ef05d3f489180ec920eb2a4e18a Author: Aaron Rainbolt Date: Sun Sep 27 21:11:28 2026 -0400 diff --git a/debian/changelog b/debian/changelog index 4265698d..3d05dca1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +security-misc (3:54.2-1) unstable; urgency=medium + + * New upstream version (local package). + + -- Patrick Schleizer Wed, 30 Sep 2026 08:33:58 +0000 + security-misc (3:54.1-1) unstable; urgency=medium * New upstream version (local package).