Skip to content
Closed
45 changes: 45 additions & 0 deletions scripts/security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
# 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.
18 changes: 18 additions & 0 deletions scripts/security/label-state.sh
Original file line number Diff line number Diff line change
@@ -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 ;;
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize colon-free Smack process labels

On a Smack host, ordinary labels such as TopSecret, _, or * contain no colon, so this fallback classifies a readable active process label as unknown. The kernel Smack documentation confirms that /proc/self/attr/current returns the process's Smack label; when the LSM inventory identifies Smack, a nonempty value should therefore be reported as label-present rather than discarded by the SELinux-specific colon heuristic.

Useful? React with 👍 / 👎.

esac
}
138 changes: 138 additions & 0 deletions scripts/security/linux-preflight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/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
. "$(dirname "$0")/label-state.sh"

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

# 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

Check warning on line 23 in scripts/security/linux-preflight.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_MetaManifold-WebUI&issues=AaGOjAREusAIaC0DvKHg&open=AaGOjAREusAIaC0DvKHg&pullRequest=304
}
# 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")"

Check warning on line 28 in scripts/security/linux-preflight.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_MetaManifold-WebUI&issues=AaGOjAREusAIaC0DvKHi&open=AaGOjAREusAIaC0DvKHi&pullRequest=304

Check warning on line 28 in scripts/security/linux-preflight.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_MetaManifold-WebUI&issues=AaGOjAREusAIaC0DvKHh&open=AaGOjAREusAIaC0DvKHh&pullRequest=304
}
# 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() {
answer=''
if [ -r "$1" ]; then

Check warning on line 35 in scripts/security/linux-preflight.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_MetaManifold-WebUI&issues=AaGOjAREusAIaC0DvKHj&open=AaGOjAREusAIaC0DvKHj&pullRequest=304
IFS= read -r answer < "$1" || :

Check warning on line 36 in scripts/security/linux-preflight.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_MetaManifold-WebUI&issues=AaGOjAREusAIaC0DvKHk&open=AaGOjAREusAIaC0DvKHk&pullRequest=304
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
Comment on lines +54 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report an unobservable SELinux mode conservatively

When getenforce is absent and selinuxfs is unmounted or inaccessible, SELinux may still be active, but this branch emits selinux_mode=not-detected. That contradicts the README's promise that inaccessible interfaces are reported as not observable and can mislead a deployment review into treating missing visibility as absence; emit not-observable here (or account for an observable selinux entry in the LSM list) instead.

Useful? React with 👍 / 👎.

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

# 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)
record apparmor_process "$(classify_apparmor_label "$aa")" 'Observed on this probe process, not on Julia'
else
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)
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

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)
Comment on lines +109 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect cgroup v2 outside the unified root

On hybrid systems or hosts that mount the cgroup2 filesystem somewhere other than /sys/fs/cgroup, this hard-coded probe reports cgroup_v2=not-observable even though the v2 hierarchy and its controllers are visible elsewhere. The kernel cgroup v2 documentation allows the hierarchy to be mounted at an arbitrary mount point and mixed with v1 hierarchies, so the inventory should locate a cgroup2 mount through /proc/self/mountinfo rather than assuming the fully unified layout.

Useful? React with 👍 / 👎.

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'
33 changes: 33 additions & 0 deletions test/security/test-linux-preflight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/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
# 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 }'
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)'
Loading