Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# LF everywhere, whatever core.autocrlf says: a CRLF script fails with
# "bad interpreter".
# LF everywhere, whatever core.autocrlf says: a CRLF script fails with "bad interpreter".
* text=auto eol=lf

*.sh text eol=lf
Expand Down
18 changes: 5 additions & 13 deletions .github/actions/prepare/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ runs:
steps:
- shell: bash
run: |
# Everything the build or a test writes, temporary files and Rust
# targets included, goes under /mnt, and the image's SDKs are deleted:
# a full disk kills the runner before it writes a report. Deleting them
# takes minutes, so it runs on in the background, detached from the
# step's output; nothing needs the room that soon.
# A full disk kills the runner unreported: write under /mnt, delete the SDKs in the background.
sudo mkdir -p /mnt/kryptik
sudo chown "$(id -u):$(id -g)" /mnt/kryptik
mkdir -p "$KRYPTIK_WORK" "$KRYPTIK_SOURCES" "$KRYPTIK_OUT" /mnt/kryptik/tmp /mnt/kryptik/cargo
Expand All @@ -33,8 +29,7 @@ runs:
run: |
sudo pip install --break-system-packages virt-firmware==26.9
virt-fw-vars --help > /dev/null
# /dev/kvm exists on a hosted runner but only root may open it; without
# it every VM falls back to TCG and runs into its timeout.
# Only root may open /dev/kvm on a hosted runner; without it every VM falls back to TCG and times out.
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
Expand All @@ -46,21 +41,18 @@ runs:
with:
path: /mnt/kryptik/sources
key: sources-v3-${{ hashFiles('sources.lock') }}
# A changed lock restores the previous set and fetches only what is
# new; every file is still hashed against the lock.
# A changed lock restores the previous set; every file is still hashed against the lock.
restore-keys: ${{ inputs.sources == 'fetch' && 'sources-' || '' }}

- if: inputs.sources == 'fetch' && steps.sources.outputs.cache-hit != 'true'
shell: bash
run: |
make sources
./tools/prune-sources.sh
# The signatures too, so the set this job saves is the one CI's
# signature gate reads from disk; this pass is not the verdict.
# Fetches the signatures into the saved set for CI's gate; this pass is not the verdict.
./tools/verify-signatures.sh > /dev/null 2>&1 || true

# Saved as soon as the set is verified, not when the job ends: a job that
# fails later would otherwise fetch the lock's new files again next time.
# Saved now rather than at the end, so a job that fails later does not refetch the new files.
- if: inputs.sources == 'fetch' && steps.sources.outputs.cache-hit != 'true'
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
Expand Down
100 changes: 29 additions & 71 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ on:
branches: [main]
pull_request:
schedule:
# Weekly, for the kernel EOL check: a pinned kernel does not change, but
# its support status does (ADR-009).
# Weekly: a pinned kernel's support status changes even when the pin does not (ADR-009).
- cron: '0 6 * * 1'

permissions:
Expand All @@ -23,8 +22,7 @@ jobs:

- name: Every commit is DevomB <Devom.hb@yahoo.com>
run: |
# On a pull request the checkout is GitHub's synthetic merge commit,
# which never reaches main; check the branch head instead.
# A pull request's checkout is a synthetic merge commit; check the branch head instead.
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/check-commit-identity.sh "${{ github.event.pull_request.head.sha }}"
else
Expand Down Expand Up @@ -55,8 +53,7 @@ jobs:

- name: Tool fixture suites
run: |
# Exit 77 means "cannot run here" (a suite that needs root and a
# built sysroot). Those are listed separately, never counted as passes.
# Exit 77 means the suite cannot run here: it is listed as skipped, never counted as a pass.
fail=0
skipped=""
for t in tools/tests/*.sh tools/tests/*.py; do
Expand Down Expand Up @@ -87,8 +84,7 @@ jobs:

- name: Executable bits are recorded
run: |
# A commit from Windows records 100644, and a Linux clone then fails
# with "Permission denied".
# A commit from Windows records 100644; a Linux clone then fails with "Permission denied".
fail=0
for f in build/stages/*.sh tools/*.sh tools/tests/*.sh tools/tests/*.py \
tools/image/*.sh tools/image/*.py \
Expand All @@ -112,9 +108,7 @@ jobs:
manifest:
name: Source manifest
runs-on: ubuntu-latest
# The Distro workflow's path: a cache's version is a hash of its path, so
# under any other one a pull request misses main's copy and saves a whole
# second one of its own, and those evicted the build trees.
# The Distro workflow's path: a cache's version hashes its path, so any other misses main's copy.
env:
KRYPTIK_SOURCES: /mnt/kryptik/sources
steps:
Expand All @@ -125,9 +119,7 @@ jobs:
sudo mkdir -p /mnt/kryptik && sudo chown "$(id -u):$(id -g)" /mnt/kryptik
./tools/fetch-sources.sh --list

# The provenance gate hashes the tarballs, so they are cached. restore-keys
# brings back the last set; `make sources` re-hashes every file against
# the lock and fetches only what is missing, and the prune drops the rest.
# Restores the last set; `make sources` checks it against the lock and fetches what is missing.
- name: Upstream sources (cached by sources.lock)
id: sources
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
Expand All @@ -137,27 +129,23 @@ jobs:
restore-keys: |
sources-

# The signatures are fetched here too, into the set the cache keeps:
# the gate below then reads them from disk instead of asking sixty
# hosts again on every run. What this pass says is not the verdict.
# Fetches the signatures into the cached set for the gate below; this pass is not the verdict.
- name: Fetch and verify the sources
if: steps.sources.outputs.cache-hit != 'true'
run: |
make sources
./tools/prune-sources.sh
./tools/verify-signatures.sh > /dev/null 2>&1 || true

# Not from a pull request: its cache is scoped to its merge ref, so every
# one whose lock differs from main's would keep a whole copy of its own.
# Not from a pull request: its cache is scoped to the merge ref and would duplicate the set.
- name: Save the sources
if: ${{ github.event_name != 'pull_request' && steps.sources.outputs.cache-hit != 'true' }}
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: /mnt/kryptik/sources
key: ${{ steps.sources.outputs.cache-primary-key }}

# Informational: a mirror that is down or throttling is not a defect here.
# A one-byte ranged GET, because several GNU mirrors refuse HEAD.
# Informational, as mirrors go down; a one-byte ranged GET, since some GNU mirrors refuse HEAD.
- name: Every source URL is reachable
continue-on-error: true
run: |
Expand All @@ -183,17 +171,13 @@ jobs:
done < <(./tools/fetch-sources.sh --list)
echo
if [ "$missing" -gt 0 ]; then
echo "$missing source(s) unreachable this run."
echo "Usually a transient mirror problem. Investigate only if the"
echo "same package fails across several runs."
echo "$missing source(s) unreachable; investigate if the same one fails across several runs."
else
echo "all sources reachable"
fi

- name: Provenance of unsigned sources
# `gh` needs GH_TOKEN even for a public repository. Strict on push and
# on the schedule; informational on pull requests, where a fork gets
# no token.
# Strict on push and schedule; informational on pull requests, where a fork gets no token.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -208,12 +192,10 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# One assurance class per source and no total: a signature, a signed
# tag and a publisher checksum are different strengths of evidence.
# One class per source and no total: the classes are different strengths of evidence.
./tools/provenance-inventory.sh

# The gate: every signature verifies against a key a publisher states,
# or tools/source-notes.tsv says which routes to the key were tried.
# Each signature must verify against its publisher's key, or tools/source-notes.tsv says why not.
- name: Signatures, strict
run: ./tools/verify-signatures.sh --strict

Expand All @@ -239,9 +221,7 @@ jobs:
- name: kryptikd unit tests
run: cd compartments/kryptikd && cargo test

# kryptik-wlproxy and zoneid: their unit and live tests, and the shipped
# zone files against the identity invariant. The acceptance runs the same
# suite hours later; this is the answer a compositor change gets first.
# wlproxy and zoneid tests, and the shipped zone files against the identity invariant.
- name: Compositor tests
run: tools/tests/compositor.sh

Expand All @@ -252,17 +232,14 @@ jobs:

- name: Allow unprivileged user namespaces
run: |
# Ubuntu 24.04 restricts unprivileged user namespaces, which the zone
# suites need here. (Kryptik itself disables them: zones are created
# with privilege.)
# Ubuntu 24.04 restricts unprivileged user namespaces, which the zone suites need here.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
echo "userns check:"
unshare --user --map-root-user id -u

- name: kryptikd builds for musl
run: |
# The shipped binary is static musl; glibc-only assumptions show up
# here (ioctl's request type differs, for one).
# The shipped binary is static musl; this catches glibc-only assumptions.
rustup target add x86_64-unknown-linux-musl
cd compartments/kryptikd
cargo build --release --target x86_64-unknown-linux-musl
Expand All @@ -275,24 +252,17 @@ jobs:

- name: Real-launcher suite (the launch path itself)
run: |
# adversarial.sh tests the primitives; this tests `kryptikd run`
# applying them in order. Unprivileged here, so groups K (privileged
# launch) and M (cgroup limits) report NOT RUN.
# Unprivileged here, so groups K (privileged launch) and M (cgroup limits) report NOT RUN.
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/launcher.sh | tee /tmp/launcher.out
exit "${PIPESTATUS[0]}"

- name: The runner must ADMIT what it could not test
- name: The launcher suite reports its gaps
run: |
# On a runner the launcher suite must end "PASSED WITH GAPS". A clean
# pass would mean the root-only checks ran vacuously or stopped
# reporting themselves. They run as root in the Distro workflow's
# acceptance (zones-test).
# Groups K and M need root: they run in the Distro workflow's zones-test, never here.
if ! grep -q 'PASSED WITH GAPS' /tmp/launcher.out; then
echo "::error::The launcher suite reported a clean pass on a CI runner."
echo "Group K (privileged launch) and group M (cgroup limits) cannot run"
echo "here. If they are no longer reported as NOT RUN, they are either"
echo "running vacuously or have been dropped. Neither is a pass."
echo "::error::The launcher suite passed cleanly on a runner, where groups K and M cannot run."
echo "They either ran vacuously or were dropped; neither is a pass."
grep -E 'passed,|not run' /tmp/launcher.out || true
exit 1
fi
Expand All @@ -312,22 +282,17 @@ jobs:
| **target kernel (root, installed system, userns restricted)** | **no — the Distro workflow's acceptance run** |

The privileged launch path, the cgroup limits, the zone lifecycle
under root and the boot itself are tested by `make acceptance` on
the built media under KVM. A green tick here is not evidence about
them.
under root and the boot are tested by `make acceptance` on the built
media under KVM, not here.
SUMMARY

- name: The user-facing command
run: |
# `kryptik` wraps kryptikd. It must not claim encryption the build
# cannot deliver, execute its own config file, or offer commands
# that do not work.
./compartments/tests/cli.sh

- name: The launch daemon, over its socket
run: |
# A runner has no XDG_RUNTIME_DIR; give it one as a session would,
# or the registry and proxy-socket checks cannot run.
# A runner has no XDG_RUNTIME_DIR, which the registry and proxy-socket checks need.
uid="$(id -u)"
sudo install -d -m 0700 -o "$uid" -g "$(id -g)" "/run/user/$uid"
export XDG_RUNTIME_DIR="/run/user/$uid"
Expand All @@ -336,8 +301,7 @@ jobs:

- name: The zone boundary's regression probes
run: |
# Exit status is the number of failures; checks that need root and
# the target kernel say NOT RUN and are covered by zones-test.
# Exits with the number of failures; root-only checks say NOT RUN and run in zones-test.
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/kryptikd/probes/boundary-checks.sh

Expand All @@ -347,12 +311,10 @@ jobs:
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0

# Every pinned series with a published support window is still inside it.
- name: Pinned series are supported upstream
run: ./tools/check-support-status.sh --strict

# Survey (network), then gate (no network). Informational on a pull
# request: an upstream release this morning is not the PR's fault.
# Survey, then gate; informational on a pull request, since an upstream release is not its fault.
- name: Pins behind upstream are reviewed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -370,8 +332,7 @@ jobs:

- name: Kernel config fragments validate against pinned source
run: |
# merge_config.sh drops unknown symbols silently, so every fragment
# symbol is checked against the pinned source.
# merge_config.sh drops unknown symbols silently; check each against the pinned source.
mkdir -p sources
# Exact names: '^linux ' would also match linux-hardened.
./tools/fetch-sources.sh --list \
Expand All @@ -384,12 +345,9 @@ jobs:
./tools/validate-kernel-config.sh --hardened
./tools/validate-kernel-config.sh --boot

- name: The resolved config honours every fragment line and kernel-hardening-checker accepts it
- name: Resolved kernel config keeps every fragment line and passes the hardening checker
run: |
# Resolves the config as stage 05 does and refuses a dropped line
# (kconfig drops one silently for an unmet dependency), then holds
# kernel-hardening-checker's failures to checker-accepted.txt.
# The GCC plugin options exist only with plugin headers installed.
# The GCC plugin options exist only with the plugin headers installed.
sudo apt-get install -y --no-install-recommends \
"gcc-$(gcc -dumpversion | cut -d. -f1)-plugin-dev" flex bison
make check-kernel-hardening
4 changes: 1 addition & 3 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
# SC2164 ("cd ... || exit"): every script sources build/lib/common.sh, whose
# `set -Eeuo pipefail` and ERR trap already abort on a failed cd. A script
# that does not source it should re-enable the check locally.
# common.sh's ERR trap aborts on a failed cd; a script that does not source it should re-enable SC2164.
disable=SC2164
Loading