Repository navigation
The docs name the programs, the compartment and the five kinds of test #623
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| schedule: | |
| # Weekly: a pinned kernel's support status changes even when the pin does not (ADR-009). | |
| - cron: '0 6 * * 1' | |
| permissions: | |
| contents: read | |
| jobs: | |
| identity: | |
| name: Commit identity | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Every commit is DevomB <Devom.hb@yahoo.com> | |
| run: | | |
| # 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 | |
| ./tools/check-commit-identity.sh --all | |
| fi | |
| lint: | |
| name: Shell lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: Install shellcheck | |
| run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck | |
| - name: Syntax check every script | |
| run: | | |
| fail=0 | |
| while IFS= read -r f; do | |
| if bash -n "$f"; then | |
| echo " ok $f" | |
| else | |
| echo " FAIL $f" | |
| fail=1 | |
| fi | |
| done < <(find . -name '*.sh' -not -path './build/work/*') | |
| exit $fail | |
| - name: Tool fixture suites | |
| run: | | |
| # 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 | |
| echo "=== $t" | |
| # The step runs under bash -e; capture the status without tripping it. | |
| "$t" && rc=0 || rc=$? | |
| if [ "$rc" -eq 77 ]; then | |
| echo " SKIPPED: $t" | |
| skipped="$skipped $t" | |
| elif [ "$rc" -ne 0 ]; then | |
| echo " FAILED: $t" | |
| fail=1 | |
| fi | |
| done | |
| if [ -n "$skipped" ]; then | |
| echo | |
| echo "did not run (not passes):$skipped" | |
| fi | |
| exit $fail | |
| - name: Shellcheck | |
| # Every tracked file whose first line names a shell. | |
| run: | | |
| mapfile -t scripts < <(git ls-files | xargs awk 'FNR==1 { if ($0 ~ /^#! ?\/(usr\/)?bin\/(env +)?(ba|da)?sh/) print FILENAME; nextfile }') | |
| echo "${#scripts[@]} shell scripts" | |
| [ "${#scripts[@]}" -ge 100 ] || { echo "the search for scripts found too few: it is broken"; exit 1; } | |
| shellcheck -S warning -x "${scripts[@]}" | |
| - name: Executable bits are recorded | |
| run: | | |
| # 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 \ | |
| compartments/tests/*.sh tools/git-hooks/*; do | |
| mode=$(git ls-files -s "$f" | awk '{print $1}') | |
| if [ "$mode" != "100755" ]; then | |
| echo " NOT EXECUTABLE ($mode): $f" | |
| fail=1 | |
| fi | |
| done | |
| exit $fail | |
| - name: No CRLF in scripts | |
| run: | | |
| # By name or by shebang: tools/kryptik and the run files have no extension. | |
| git ls-files | while IFS= read -r f; do | |
| case "$f" in *.sh|*.rs|*.py|Makefile|*/Makefile) ;; *) [ "$(head -c 2 "$f" 2>/dev/null)" = '#!' ] || continue ;; esac | |
| if grep -qU $'\r' "$f"; then echo "$f"; fi | |
| done > /tmp/crlf | |
| if [ -s /tmp/crlf ]; then | |
| cat /tmp/crlf | |
| echo "CRLF found - scripts will fail on Linux with 'bad interpreter'" | |
| exit 1 | |
| fi | |
| echo "no CRLF" | |
| manifest: | |
| name: Source manifest | |
| runs-on: ubuntu-latest | |
| # 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: | |
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: Manifest resolves | |
| run: | | |
| sudo mkdir -p /mnt/kryptik && sudo chown "$(id -u):$(id -g)" /mnt/kryptik | |
| ./tools/fetch-sources.sh --list | |
| # 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 | |
| with: | |
| path: /mnt/kryptik/sources | |
| key: sources-v3-${{ hashFiles('sources.lock') }} | |
| restore-keys: | | |
| sources- | |
| # 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 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, 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: | | |
| reach() { | |
| curl -fsSL --range 0-0 --retry 3 --retry-delay 2 \ | |
| --connect-timeout 20 --max-time 60 -o /dev/null "$1" | |
| } | |
| missing=0 | |
| while read -r name ver url _; do | |
| if reach "$url"; then | |
| echo " ok $name $ver" | |
| continue | |
| fi | |
| # Same fallback as fetch-sources.sh: mirrors.kernel.org for GNU. | |
| fallback="${url/https:\/\/ftpmirror.gnu.org/https://mirrors.kernel.org/gnu}" | |
| if [ "$fallback" != "$url" ] && reach "$fallback"; then | |
| echo " ok $name $ver (via fallback mirror)" | |
| else | |
| echo " UNREACHED $name $ver $url" | |
| missing=$((missing + 1)) | |
| fi | |
| done < <(./tools/fetch-sources.sh --list) | |
| echo | |
| if [ "$missing" -gt 0 ]; then | |
| 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 | |
| # Strict on push and schedule; informational on pull requests, where a fork gets no token. | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "pull request: informational (no token for fork PRs)" | |
| ./tools/verify-provenance.sh | |
| else | |
| ./tools/verify-provenance.sh --strict | |
| fi | |
| - name: Provenance inventory | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # One class per source and no total: the classes are different strengths of evidence. | |
| ./tools/provenance-inventory.sh | |
| # 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 | |
| - name: sources.lock covers the manifest | |
| run: | | |
| fail=0 | |
| while read -r name ver url _; do | |
| file=$(basename "$url") | |
| if ! grep -q " $file\$" sources.lock; then | |
| echo " MISSING FROM LOCK: $file" | |
| fail=1 | |
| fi | |
| done < <(./tools/fetch-sources.sh --list) | |
| [ $fail -eq 0 ] && echo "sources.lock covers every manifest entry" | |
| exit $fail | |
| compartments: | |
| name: Compartment layer | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: kryptikd unit tests | |
| run: cd compartments/kryptikd && cargo test | |
| # wlproxy and zoneid tests, and the shipped zone files against the identity invariant. | |
| - name: Compositor tests | |
| run: tools/tests/compositor.sh | |
| - name: Zone definitions validate | |
| run: | | |
| cd compartments/kryptikd && cargo build --quiet | |
| ./target/debug/kryptikd check --zones ../zones | |
| - name: Allow unprivileged user namespaces | |
| run: | | |
| # 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; 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 | |
| cargo test --target x86_64-unknown-linux-musl | |
| - name: Isolation exit test (the primitives) | |
| run: | | |
| cd compartments/kryptikd && cargo build --quiet | |
| cd ../.. && ./compartments/tests/adversarial.sh | |
| - name: Real-launcher suite (the launch path itself) | |
| 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 launcher suite reports its gaps | |
| run: | | |
| # 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 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 | |
| echo "The suite reported its gaps, as it must on a runner:" | |
| sed -n '/not run (mandatory gaps/,/^$/p' /tmp/launcher.out | |
| - name: What this CI run did and did not establish | |
| if: always() | |
| run: | | |
| cat >> "$GITHUB_STEP_SUMMARY" <<'SUMMARY' | |
| ### What a green run here means | |
| | tier | established here? | | |
| |---|---| | |
| | component (unit tests, tool fixtures) | yes | | |
| | developer host (this runner's kernel, unprivileged) | yes | | |
| | **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 are tested by `make acceptance` on the built | |
| media under KVM, not here. | |
| SUMMARY | |
| - name: The user-facing command | |
| run: | | |
| ./compartments/tests/cli.sh | |
| - name: The launch daemon, over its socket | |
| 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" | |
| cd compartments/kryptikd && cargo build --quiet | |
| cd ../.. && ./compartments/tests/serve.sh | |
| - name: The zone boundary's regression probes | |
| run: | | |
| # 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 | |
| kernel: | |
| name: Kernel currency | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: Pinned series are supported upstream | |
| run: ./tools/check-support-status.sh --strict | |
| # 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 }} | |
| run: | | |
| ./tools/check-source-currency.sh --tsv > pin-survey.tsv | |
| echo "survey: $(wc -l < pin-survey.tsv) sources" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| ./tools/check-pin-reviews.sh --survey pin-survey.tsv || echo "pull request: informational" | |
| else | |
| ./tools/check-pin-reviews.sh --survey pin-survey.tsv | |
| fi | |
| - name: Pinned kernel is longterm and not EOL | |
| run: ./tools/check-kernel-eol.sh | |
| - name: Kernel config fragments validate against pinned source | |
| run: | | |
| # 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 \ | |
| | awk '$1 == "linux" || $1 == "linux-hardened" || $1 == "kernel-hardening-checker" {print $3}' \ | |
| | while read -r u; do | |
| echo "fetching $(basename "$u")" | |
| curl -fL --no-progress-meter -o "sources/$(basename "$u")" "$u" | |
| done | |
| ./tools/validate-kernel-config.sh | |
| ./tools/validate-kernel-config.sh --hardened | |
| ./tools/validate-kernel-config.sh --boot | |
| - name: Resolved kernel config keeps every fragment line and passes the hardening checker | |
| run: | | |
| # 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 |