From 85f4931e5594e496841a95066283ccc05fce5b5c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:40:44 +0000 Subject: [PATCH 1/8] Add native Linux security preflight: scripts/security/linux-preflight.sh --- scripts/security/linux-preflight.sh | 130 ++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 scripts/security/linux-preflight.sh diff --git a/scripts/security/linux-preflight.sh b/scripts/security/linux-preflight.sh new file mode 100644 index 00000000..aff33af9 --- /dev/null +++ b/scripts/security/linux-preflight.sh @@ -0,0 +1,130 @@ +#!/bin/sh +# SPDX-License-Identifier: MPL-2.0 +# Read-only evidence for a native Linux security deployment review. +# This script DOES NOT certify a deployment or enforce host settings. +set -eu + +if [ "${1-}" = '--help' ]; then + printf '%s\n' 'Usage: sh scripts/security/linux-preflight.sh' \ + 'Print tab-separated check, observation, and explanation records.' \ + 'Read-only. Inspect the actual application/worker process separately.' + exit 0 +fi +if [ "$#" -ne 0 ]; then + printf '%s\n' 'Unexpected arguments; use --help' >&2 + exit 2 +fi + +# Values in observations are bounded to a single printable line. Do not print +# hostnames, usernames, filesystem paths, secrets, or environment variables. +clean() { + printf '%s' "$1" | LC_ALL=C tr '\n\r\t' ' ' | LC_ALL=C cut -c 1-160 +} +record() { + printf '%s\t%s\t%s\n' "$1" "$(clean "$2")" "$(clean "$3")" +} +read_one() { + # One-line read via shell, never evaluate the contents as code. + answer='' + if [ -r "$1" ]; then + IFS= read -r answer < "$1" || : + printf '%s' "${answer-}" + fi +} + +printf '# check\tobservation\tinterpretation (no deployment assurance implied)\n' +if [ "$(uname -s)" != 'Linux' ]; then + record os unsupported 'Native Linux preflight requires Linux' + exit 1 +fi +record os Linux 'Current process is running on Linux' +record kernel "$(uname -r)" 'Kernel version is descriptive, not a feature guarantee' + +# SELinux can be enabled globally while THIS process is unconfined. In +# particular, `getenforce` never proves the Julia/worker process is confined. +if command -v getenforce >/dev/null 2>&1; then + selinux=$(getenforce 2>/dev/null || printf 'unknown') +elif [ -r /sys/fs/selinux/enforce ]; then + case "$(read_one /sys/fs/selinux/enforce)" in + 1) selinux=Enforcing ;; + 0) selinux=Permissive ;; + *) selinux=unknown ;; + esac +else + selinux=not-detected +fi +record selinux_mode "$selinux" 'Global mode only; process confinement not established' + +lsm=$(read_one /sys/kernel/security/lsm) +if [ -n "$lsm" ]; then + record active_lsms "$lsm" 'Kernel LSM list; not proof of application confinement' +else + record active_lsms not-observable 'LSM interface may be inaccessible or unmounted' +fi + +# AppArmor current label and SELinux current label may use different proc paths. +if [ -r /proc/self/attr/apparmor/current ]; then + aa=$(read_one /proc/self/attr/apparmor/current) + case "$aa" in + ''|unconfined*) aa_state=unconfined ;; + *) aa_state=label-present ;; + esac + record apparmor_process "$aa_state" 'Observed on this probe process, not on Julia' +else + record apparmor_process not-observable 'Process AppArmor profile cannot be read' +fi +if [ -r /proc/self/attr/current ]; then + label=$(read_one /proc/self/attr/current) + case "$label" in + *:*) proc_state=label-present ;; + unconfined*) proc_state=unconfined ;; + '') proc_state=unknown ;; + *) proc_state=unknown ;; + esac + record process_security_label "$proc_state" 'Presence of a label is not evidence of effective confinement' +else + record process_security_label not-observable 'Current security label cannot be read' +fi + +if [ -r /proc/self/status ]; then + nnp=$(sed -n 's/^NoNewPrivs:[[:space:]]*//p' /proc/self/status | head -n 1) + case "$nnp" in + 1) nnp_state=enabled ;; + 0) nnp_state=disabled ;; + *) nnp_state=unknown ;; + esac + record no_new_privs "$nnp_state" 'Only for this probe; worker launcher must set and verify its own' +else + record no_new_privs not-observable 'No proc status available' +fi + +case ",$lsm," in + *,landlock,*) record landlock advertised 'Kernel LSM lists Landlock; syscall usability not tested' ;; + *) record landlock not-verified 'Do not infer unsupported merely because kernel LSM list is inaccessible' ;; +esac + +if [ -r /sys/fs/cgroup/cgroup.controllers ]; then + ctrls=$(read_one /sys/fs/cgroup/cgroup.controllers) + record cgroup_v2 detected 'Controller presence does not mean delegated writable quotas' + if [ -n "$ctrls" ]; then + record cgroup_controllers "$ctrls" 'Available controllers; no delegation test performed' + fi +else + record cgroup_v2 not-observable 'Root controller file not available; delegation not established' +fi + +# Portable POSIX shell limits. Values are environmental observations only. +record open_file_limit "$(ulimit -n 2>/dev/null || printf unknown)" 'Soft limit for this probe process' +record cpu_time_limit "$(ulimit -t 2>/dev/null || printf unknown)" 'Soft CPU seconds limit for this probe process' +record file_size_limit "$(ulimit -f 2>/dev/null || printf unknown)" 'Soft file-size limit for this probe process' +record umask "$(umask)" 'Effective creation mask of this probe process' +if command -v ssh >/dev/null 2>&1; then + record ssh_client available 'No connection or host-key verification attempted' +else + record ssh_client unavailable 'SSH remote worker requires supported client' +fi +record tls_ingress not-assessed 'Must independently verify actual browser-facing HTTPS and identity boundary' +record user_authentication not-assessed 'Application identity, sessions and authorisation require runtime tests' +record data_isolation not-assessed 'App/DB/worker cross-tenant isolation requires separate adversarial tests' +record audit_durability not-assessed 'Audit tamper model and crash recovery require separate tests' +record result informational-only 'No permissions, files, network settings, policy or host state changed' From 2cef22f7f3bb50be2ab1c5328c8cbf57eacb2ef4 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:40:47 +0000 Subject: [PATCH 2/8] Add native Linux security preflight: test/security/test-linux-preflight.sh --- test/security/test-linux-preflight.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/security/test-linux-preflight.sh diff --git a/test/security/test-linux-preflight.sh b/test/security/test-linux-preflight.sh new file mode 100644 index 00000000..47a5f784 --- /dev/null +++ b/test/security/test-linux-preflight.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# SPDX-License-Identifier: MPL-2.0 +# Smoke/format regression: run from repository root, no elevated privileges. +set -eu +script=scripts/security/linux-preflight.sh +[ -f "$script" ] || { echo "Missing $script" >&2; exit 1; } +sh -n "$script" +help=$(sh "$script" --help) +case "$help" in *'Read-only'*) : ;; *) echo 'Missing read-only help contract' >&2; exit 1 ;; esac +if sh "$script" --invalid >/dev/null 2>&1; then + echo 'Invalid option unexpectedly succeeded' >&2 + exit 1 +fi +out=$(sh "$script") +# Every non-comment row must have exactly three tab-separated fields. +printf '%s\n' "$out" | awk -F '\t' '/^#/ { next } NF != 3 { bad = 1; print "Bad record: " $0 > "/dev/stderr" } END { exit bad }' +for key in os selinux_mode active_lsms apparmor_process process_security_label no_new_privs landlock cgroup_v2 open_file_limit cpu_time_limit file_size_limit umask ssh_client tls_ingress user_authentication data_isolation audit_durability result; do + count=$(printf '%s\n' "$out" | awk -F '\t' -v key="$key" '$1 == key { n++ } END { print n+0 }') + [ "$count" -eq 1 ] || { echo "Expected one $key record; got $count" >&2; exit 1; } +done +printf '%s\n' "$out" | grep -q '^result[[:space:]]informational-only[[:space:]]' || { echo 'Missing non-certification result' >&2; exit 1; } +echo 'linux-preflight: PASS (format and safety-contract smoke tests)' From 615340b8cab72d3be4fbcebeba57a6d8729187c7 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:40:51 +0000 Subject: [PATCH 3/8] Add native Linux security preflight: scripts/security/README.md --- scripts/security/README.md | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 scripts/security/README.md diff --git a/scripts/security/README.md b/scripts/security/README.md new file mode 100644 index 00000000..db0af966 --- /dev/null +++ b/scripts/security/README.md @@ -0,0 +1,45 @@ + +# Native Linux deployment preflight (B0) + +This is a **read-only inventory**, not a security certification or a startup +gate. Run on each intended **Linux application and worker host**, as the +account that would run the service or worker. It requires only POSIX `sh` +and conventional Linux `/proc` and `/sys` interfaces; it does not run +`sudo`, enable SELinux, install policy, open network connections, inspect +secret material or change the host. It prints no hostname or username. + +```sh +sh scripts/security/linux-preflight.sh +sh test/security/test-linux-preflight.sh +``` + +The report contains three tab-separated columns (`check`, `observation`, +`interpretation`). Keep the result with its deployment review and separately +record the **host identity, date, runtime account and command version** in a +protected location. Do not infer those details from this deliberately redacted +output. CI runs show only CI-runner capabilities, **never the eventual host's**. + +## Interpret conservatively + +- `selinux_mode=Enforcing` reports a global setting, **not** whether the Julia, + R or SSH worker is confined to an appropriate SELinux domain. A host + administrator must install/activate policy and label files as necessary. +- `apparmor_process` and `process_security_label` describe only the probe's + own process. Inspect and test the **actual service and worker processes**. +- `landlock=advertised` only means the kernel LSM list contains Landlock. It + does not prove the intended ABI is usable or that a child process is + restricted; test that separately by creating and checking a restriction. +- `cgroup_v2=detected` does not establish delegated controllers or resource + quotas. `ulimit` values describe this shell, not future services. +- Authentication, HTTPS ingress, authorisation, storage isolation, remote + SSH host keys, backup, logging and audit are **not assessed** here. +- A missing or inaccessible `/proc`/`/sys` interface is reported as + *not observable*, not as proof of insecurity or lack of support. +- The report is diagnostic. **The secure multi-user service cannot be accepted + without application-level trusted identity, default-deny authorisation, + owner-bound jobs, protected cross-tenant data paths and adversarial tests.** + Optional host confinement adds defence in depth, not a replacement. + +Use the security coordination issue for the deployment decision and the +required identity/tenancy architecture. Do not make a pass/fail security claim +from this report alone, and do not require containers, VMs or root access. From 1e83b8399ddc8bc15c9946c296ccfb404b1e312e Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:47:45 +0000 Subject: [PATCH 4/8] Update scripts/security/linux-preflight.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- scripts/security/linux-preflight.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/security/linux-preflight.sh b/scripts/security/linux-preflight.sh index aff33af9..7bc8eed8 100644 --- a/scripts/security/linux-preflight.sh +++ b/scripts/security/linux-preflight.sh @@ -66,7 +66,8 @@ fi if [ -r /proc/self/attr/apparmor/current ]; then aa=$(read_one /proc/self/attr/apparmor/current) case "$aa" in - ''|unconfined*) aa_state=unconfined ;; + '') aa_state=unknown ;; + unconfined*) aa_state=unconfined ;; *) aa_state=label-present ;; esac record apparmor_process "$aa_state" 'Observed on this probe process, not on Julia' From efafdfaa95750d52f3ee1a1a2138e8f6c0ad1c1e Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:52:31 +0000 Subject: [PATCH 5/8] Add pure Linux security-label classification with unconfined exact match --- scripts/security/label-state.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 scripts/security/label-state.sh diff --git a/scripts/security/label-state.sh b/scripts/security/label-state.sh new file mode 100644 index 00000000..42212b96 --- /dev/null +++ b/scripts/security/label-state.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# SPDX-License-Identifier: MPL-2.0 +# Pure text classifiers for observed Linux security labels; not a security gate. +classify_apparmor_label() { + case "$1" in + '') printf '%s' unknown ;; + unconfined|'unconfined ('*) printf '%s' unconfined ;; + *) printf '%s' label-present ;; + esac +} +classify_generic_label() { + case "$1" in + '') printf '%s' unknown ;; + unconfined|'unconfined ('*) printf '%s' unconfined ;; + *:*) printf '%s' label-present ;; + *) printf '%s' unknown ;; + esac +} From e88cc4ef610b4ed5b8b9d2e9821ec7deaddc4c76 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:52:34 +0000 Subject: [PATCH 6/8] Make AppArmor process observations safe and testable: scripts/security/linux-preflight.sh --- scripts/security/linux-preflight.sh | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/security/linux-preflight.sh b/scripts/security/linux-preflight.sh index 7bc8eed8..10aab708 100644 --- a/scripts/security/linux-preflight.sh +++ b/scripts/security/linux-preflight.sh @@ -3,6 +3,7 @@ # Read-only evidence for a native Linux security deployment review. # This script DOES NOT certify a deployment or enforce host settings. set -eu +. "$(dirname "$0")/label-state.sh" if [ "${1-}" = '--help' ]; then printf '%s\n' 'Usage: sh scripts/security/linux-preflight.sh' \ @@ -62,27 +63,28 @@ else record active_lsms not-observable 'LSM interface may be inaccessible or unmounted' fi -# AppArmor current label and SELinux current label may use different proc paths. +# An AppArmor label may appear in either the module-specific attr or the +# generic LSM attr, depending on the kernel. Only use the latter as an +# AppArmor label when the LSM inventory explicitly advertises AppArmor. if [ -r /proc/self/attr/apparmor/current ]; then aa=$(read_one /proc/self/attr/apparmor/current) - case "$aa" in - '') aa_state=unknown ;; - unconfined*) aa_state=unconfined ;; - *) aa_state=label-present ;; - esac - record apparmor_process "$aa_state" 'Observed on this probe process, not on Julia' + record apparmor_process "$(classify_apparmor_label "$aa")" 'Observed on this probe process, not on Julia' else - record apparmor_process not-observable 'Process AppArmor profile cannot be read' + case ",$lsm," in + *,apparmor,*) + if [ -r /proc/self/attr/current ]; then + aa=$(read_one /proc/self/attr/current) + record apparmor_process "$(classify_apparmor_label "$aa")" 'Generic LSM attr fallback for this probe; verify worker separately' + else + record apparmor_process not-observable 'No readable AppArmor process attribute' + fi ;; + *) + record apparmor_process not-observable 'AppArmor profile not established by observable kernel interfaces' ;; + esac fi if [ -r /proc/self/attr/current ]; then label=$(read_one /proc/self/attr/current) - case "$label" in - *:*) proc_state=label-present ;; - unconfined*) proc_state=unconfined ;; - '') proc_state=unknown ;; - *) proc_state=unknown ;; - esac - record process_security_label "$proc_state" 'Presence of a label is not evidence of effective confinement' + record process_security_label "$(classify_generic_label "$label")" 'A label is not evidence of effective worker confinement' else record process_security_label not-observable 'Current security label cannot be read' fi From 7ecc3b1efddaf12caf9f795632bbaa62ed8801b0 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:52:36 +0000 Subject: [PATCH 7/8] Make AppArmor process observations safe and testable: test/security/test-linux-preflight.sh --- test/security/test-linux-preflight.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/security/test-linux-preflight.sh b/test/security/test-linux-preflight.sh index 47a5f784..f89a5fbe 100644 --- a/test/security/test-linux-preflight.sh +++ b/test/security/test-linux-preflight.sh @@ -11,6 +11,17 @@ if sh "$script" --invalid >/dev/null 2>&1; then echo 'Invalid option unexpectedly succeeded' >&2 exit 1 fi +# Pure label fixtures: do not mistake a confined profile named "unconfined_*" +# for an actually unconfined task, or an unreadable value for evidence. +. scripts/security/label-state.sh +[ "$(classify_apparmor_label '')" = unknown ] +[ "$(classify_apparmor_label unconfined)" = unconfined ] +[ "$(classify_apparmor_label 'unconfined (complain)')" = unconfined ] +[ "$(classify_apparmor_label 'unconfined_worker (enforce)')" = label-present ] +[ "$(classify_apparmor_label 'myprofile (enforce)')" = label-present ] +[ "$(classify_generic_label 'unconfined_worker (enforce)')" = unknown ] +[ "$(classify_generic_label 'user_u:user_r:user_t:s0')" = label-present ] +[ "$(classify_generic_label '')" = unknown ] out=$(sh "$script") # Every non-comment row must have exactly three tab-separated fields. printf '%s\n' "$out" | awk -F '\t' '/^#/ { next } NF != 3 { bad = 1; print "Bad record: " $0 > "/dev/stderr" } END { exit bad }' From 7495f7df448c6dee1f32af3ce3c65b0709238746 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:53:21 +0000 Subject: [PATCH 8/8] Document sanitization, record formatting, and file-reading behavior in Linux preflight helpers --- scripts/security/linux-preflight.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/security/linux-preflight.sh b/scripts/security/linux-preflight.sh index 10aab708..b1f2f829 100644 --- a/scripts/security/linux-preflight.sh +++ b/scripts/security/linux-preflight.sh @@ -16,16 +16,21 @@ if [ "$#" -ne 0 ]; then exit 2 fi -# Values in observations are bounded to a single printable line. Do not print +# Write $1 with newlines, carriage returns and tabs replaced by spaces, capped +# at 160 bytes. Other control characters are not removed. Do not print # hostnames, usernames, filesystem paths, secrets, or environment variables. clean() { printf '%s' "$1" | LC_ALL=C tr '\n\r\t' ' ' | LC_ALL=C cut -c 1-160 } +# Write a tab-separated record: $1 is the unchanged check name; $2 and $3 are +# the observation and interpretation, cleaned and capped by clean. record() { printf '%s\t%s\t%s\n' "$1" "$(clean "$2")" "$(clean "$3")" } +# Write the first line of $1 without its newline, or nothing if it is not +# readable. Read failures are ignored and may leave partial data; output write +# failures propagate. Sets answer without evaluating the file contents as code. read_one() { - # One-line read via shell, never evaluate the contents as code. answer='' if [ -r "$1" ]; then IFS= read -r answer < "$1" || :