Skip to content
Draft
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
62 changes: 62 additions & 0 deletions .github/scripts/linux-hardened.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Integration checks for a hardened Linux host: classification, peer verification both ways, unverified-agent
# replacement, re-exec and `mairu harden` itself. Runs as a user with passwordless sudo, after `cargo build`.
set -uo pipefail
cd "$(dirname "$0")/../.." || exit 1

failures=0
check() { local d="$1"; shift; if "$@"; then echo "PASS: $d"; else echo "FAIL: $d"; failures=$((failures + 1)); fi; }
has() { grep -q -- "$1" <<<"$2"; }
# Through a file: an auto-spawned agent inherits stderr and would keep a $(...) pipe open.
capture() { local f; f="$(mktemp)"; "$@" >"$f" 2>&1; rc=$?; out="$(cat "$f")"; rm -f "$f"; echo "$out"; }

dev=./target/debug/mairu
hardened=/usr/local/libexec/mairu/mairu
# /opt is world-writable on GitHub runners, which harden refuses.
scratch=/usr/local/mairu-ci

echo "--- harden"
capture sudo "$dev" harden
check "harden succeeds" test "$rc" -eq 0
check "hardened copy root:mairu 2755" test "$(stat -c '%U:%G %a' "$hardened")" = "root:mairu 2755"
check "state directory root:mairu 2770" test "$(sudo stat -c '%U:%G %a' /var/lib/mairu)" = "root:mairu 2770"

echo "--- classification"
capture "$hardened" doctor
check "hardened copy classifies as hardened" has "^client hardened" "$out"
capture "$dev" doctor
check "debug build in the dev profile stays unhardened" has "^client unhardened" "$out"

echo "--- re-exec"
capture env MAIRU_PROFILE=default "$dev" doctor
check "user copy re-execs the hardened copy" has "^client hardened $hardened" "$out"
capture env MAIRU_PROFILE=default MAIRU_NO_HARDENED_REEXEC=1 "$dev" doctor
check "MAIRU_NO_HARDENED_REEXEC=1 stays unhardened" has "^client unhardened" "$out"

echo "--- unverified-agent replacement and peer verification"
"$hardened" kill-agent >/dev/null 2>&1
capture "$dev" agent --daemonize
sleep 1
capture "$hardened" list-sessions
check "hardened client replaces an unhardened agent" has "Replacing an agent that is not hardened" "$out"
capture "$hardened" doctor
# shellcheck disable=SC2016
check "agent is hardened and verified" bash -c 'grep "^agent hardened" <<<"$1" | grep -q " verified$"' _ "$out"
check "caller verified" has "^caller verified" "$out"
capture "$dev" list-sessions
check "hardened agent refuses an unhardened client" has "the agent refused this request" "$out"
"$hardened" kill-agent >/dev/null 2>&1

echo "--- harden in a scratch home"
capture sudo "$dev" harden --home "$scratch"
check "fresh install" test -x "$scratch/libexec/mairu/mairu" -a -f "$scratch/libexec/mairu/.harden-manifest"
capture sudo "$dev" harden --home "$scratch"
check "re-run" test "$rc" -eq 0
capture sudo "$dev" harden --uninstall --home "$scratch"
check "uninstall" test ! -e "$scratch/libexec/mairu"
capture sudo "$dev" harden --uninstall --purge
check "uninstall --purge" bash -c 'test ! -e /usr/local/libexec/mairu && ! sudo test -e /var/lib/mairu'
check "group left in place" getent group mairu

echo "== $failures failure(s)"
[[ $failures -eq 0 ]]
10 changes: 10 additions & 0 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ jobs:
- run: cargo check --lib --no-default-features
- run: cargo clippy --lib --no-default-features -- -D warnings

linux-hardened:
name: linux-hardened
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
with: { target: x86_64-unknown-linux-gnu }
- run: cargo build
- run: .github/scripts/linux-hardened.sh

build:
if: ${{ inputs.build }}
name: cargo build
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
- Release archives carry a GitHub artifact attestation of `mairu` as `mairu.sigstore.json`; verify with `gh attestation verify mairu --repo sorah/mairu`. Debian packages are attested too.
- When mairu is installed setgid to a dedicated `mairu` group, the agent and its clients verify each other through that group. The agent serves only verified clients, apart from diagnostics; a client sends nothing to an agent it cannot verify, and replaces it with a verified one. A hardened agent also ignores proxies, `AWS_*` variables and `~/.aws/config`.
- New `mairu doctor` command shows how the client and the agent see each other.
- New `mairu harden` command (Linux, as root) installs a hardened copy at `/usr/local/libexec/mairu/mairu`, creates the `mairu` group and `/var/lib/mairu`, and removes them again with `--uninstall [--purge]`. `--home`, `MAIRU_HARDEN_HOME` or `harden_home` in `config.json` choose another prefix. `harden --from FILE` upgrades the hardened copy after verifying the GitHub artifact attestation of `FILE`.
- Other installations (mise, cargo, aqua, tarballs) run the hardened copy when one exists, and warn when its version differs. `MAIRU_NO_HARDENED_REEXEC=1` opts out. Without a hardened copy, mairu suggests `harden` at most once an hour on a terminal (`MAIRU_NO_HARDEN_SUGGESTION=1` or `harden_suggestion` in `config.json`).
- The Debian package installs `/usr/bin/mairu` setgid to the `mairu` group and creates `/var/lib/mairu`. `pkg/sysusers.d` and `pkg/tmpfiles.d` carry the same for other packagers.
- `mairu doctor` lists hardened copies and their versions, and warns when `sudo -n true` succeeds or the user is in `docker`, `lxd`, `incus-admin` or `disk`.

### Miscellaneous

Expand Down
Loading
Loading