Repository navigation
[Parked] security(B0): read-only native Linux capability preflight #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
85f4931
2cef22f
615340b
1e83b83
efafdfa
e88cc4e
7ecc3b1
7495f7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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 ;; | ||
| esac | ||
| } | ||
| 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
|
||
| } | ||
| # 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
|
||
| } | ||
| # 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
|
||
| IFS= read -r answer < "$1" || : | ||
|
Check warning on line 36 in scripts/security/linux-preflight.sh
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On hybrid systems or hosts that mount the cgroup2 filesystem somewhere other than 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' | ||
| 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)' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a Smack host, ordinary labels such as
TopSecret,_, or*contain no colon, so this fallback classifies a readable active process label asunknown. The kernel Smack documentation confirms that/proc/self/attr/currentreturns the process's Smack label; when the LSM inventory identifies Smack, a nonempty value should therefore be reported aslabel-presentrather than discarded by the SELinux-specific colon heuristic.Useful? React with 👍 / 👎.