Transparency log
hushrun's server keeps each repository's events in an append-only log, and signs what it says about that log with a key hushrun.yaml pins, so every command can check the server instead of trusting it.
The log key in hushrun.yaml
Every write to a repository on hushrun is a leaf of an append-only Merkle log. The server signs the log's checkpoints, each listing it answers, and its answer about who counts, with the repository's log key. hushrun.yaml names that key at its top level, so the server can't vouch for itself:
transparency_log:
origin: api.hushrun.com/l/acme/api
key: "ed25519:…"npx hushrun initwrites these lines when the server tells it the key. Otherwise,hushrun doctorprints them.originis the repository's URL on hushrun without its scheme, andkeythe log's Ed25519 public key.- hushrun reads them with the rest of
hushrun.yaml, as committed, so changing them is a reviewed change like any other.
What every command checks
With the key pinned, every command that reads or writes the repository's events checks what the server answers:
- the receipt of each write;
- the signed statement of each listing;
- the signers answer, which says who counts;
- that every checkpoint it meets, the server's own and those recorded inside events, belongs to one history.
A check that fails ends the command with exit code 6. hushrun plan --start records the listings its decision read in its started event, for monitors to check, and hushrun mark deployed refuses an attempt without that event, so every deploy has one.
Until hushrun.yaml names the key, hushrun checks none of this, and warns on every command.
Monitor the log
A listing that leaves an event out could hide a failure from a plan. hushrun repository monitor finds it afterwards:
hushrun repository monitor --state monitor.json- It replays the repository's log, and checks it against the server's latest signed checkpoint, which must extend the one the last run kept in
--state. - It checks every checkpoint and listing plans recorded, every deletion against an org admin's signed statement, and every map the server committed to.
- It exits 5 when the server contradicts its own log, and prints the server's own signed statements as proof, and 0 when everything agrees.
- It needs read access only, and
hushrun.yaml's log key.
The monitor that counts is one the server's operator doesn't run. Run it from your own CI, every hour:
name: Monitor the hushrun repository
on:
schedule:
- cron: "17 * * * *"
workflow_dispatch:
concurrency:
group: hushrun-monitor
cancel-in-progress: false
jobs:
monitor:
runs-on: ubuntu-latest
# id-token: write lets hushrun ask for an ID token whose audience is the repository's URL.
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@v4 # hushrun.yaml names the log key
- uses: hushrun/hushrun/action/setup@main # pin to a release commit
with: { version: 0.1.0 }
# The newest checkpoint the last run verified: a server that rolls back between runs
# contradicts it.
- uses: actions/cache/restore@v4
with:
path: hushrun-monitor.json
key: hushrun-monitor-${{ github.run_id }}
restore-keys: hushrun-monitor-
- run: hushrun repository monitor --state hushrun-monitor.json --format json > monitor.json
- if: always()
run: jq '{ok, checkpoint: .checkpoint.size, checked, findings}' monitor.json
- uses: actions/cache/save@v4
if: success()
with:
path: hushrun-monitor.json
key: hushrun-monitor-${{ github.run_id }}
- uses: actions/upload-artifact@v4 # the findings and their proofs, if any
if: failure()
with: { name: hushrun-monitor, path: monitor.json }Who can read and write
A repository's events must be append-only for CI: a pipeline that could delete or overwrite an event could remove a failed result and unblock a digest that never passed. Signatures can't prove an event is missing, so hushrun's server enforces it:
| Who | Events | Delete |
|---|---|---|
| CI, with its ID token or a writer token | Append and read | Never |
| A member or an agent, from a registered machine | Append what that machine signed, and read | Never |
| Org admins and owners | Read | Only with hushrun repository gc |
- A token has a role:
readerreads,writeralso appends events, andadminalso deletes. hushrun creates each event only if it doesn't exist yet, and the roles keep a client that misbehaves from overwriting or deleting one. - A GitHub Actions job with
id-token: writegets the writer role for the repository its ID token names. Elsewhere, CI uses an agent token (GitLab CI). - An org admin makes repository tokens,
hrl_…, for monitors and scripts, on the repository's page in the dashboard. They go inHUSHRUN_REPOSITORY_TOKEN, or come from the command inHUSHRUN_REPOSITORY_TOKEN_COMMAND. - hushrun reads tokens only from the environment, never from the repository, and never logs them.
HUSHRUN_REPOSITORY_TOKENgoes only to a URL thatHUSHRUN_REPOSITORY_URLor--repository-urlnames. A URL onlyhushrun.yamlnames gets a CI job's ID token; outside CI, it gets the login you made for its origin, or an agent token when it is on hushrun's own host. - A repository's URL is
https://;http://works only on a loopback host.
Old events go with hushrun repository gc, run by an org admin or owner from their signed-in machine, first with --dry-run:
hushrun repository gc --older-than 180d --dry-runIt keeps the newest deployment of each scope and stage, and the events of its digest. On a repository that keeps a log, it signs each deletion with the admin's machine, so a deletion is never the server's own doing, and monitors report any deletion without such a signature.