~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A repository owner can't give the hosted runner a secret

Closed
Closed chadtmiller.com opened this issue 1 comment
kind/featurearea/cliarea/runner

What happened

I added a release workflow to knotless://chadtmiller.com/hatk that publishes to npm. It lists secrets: [NPM_TOKEN], and the repository names the hosted runner (did:plc:4bocpkn5zkwe4wuu4plrj5kt, runner.knotless.dev). The only way for the run to get NPM_TOKEN is to SSH into the runner host and edit /etc/knotless/run-secrets.json under did:plc:…/hatk. That works only because I operate the runner.

Anyone else using the hosted runner can't do it. Their workflows that need a secret (publish a package, deploy, push to a mirror) never get one, and nothing tells them why: a secret not granted is simply absent (docs/reference/workflows.md, "Secrets file"). Today their options are to run their own runner (the self-hosted runner issue, 3myioqgnrsh2b) or to hand their token to whoever runs the hosted runner, so the operator holds everyone's credentials.

No CLI command, MCP tool or record lets an owner set a secret for a runner they don't run.

One way to do it

  • knotless secret set <url> <NAME> [--ref <ref>]... reads the value from stdin or a hidden prompt, never from argv. secret ls / secret rm work too, and an MCP tool to match.
  • The value is sealed to the runner: each runner publishes an encryption key (a #knotless_secrets key in its DID document, or a record), and the CLI encrypts to every runner the repository names. The owner's PDS, the appview and readers hold only ciphertext.
  • Stored as records the runner can read and nobody else needs to: in a space only the owner and the runners are members of, as a private repository's checks already are (docs/design/runner.md, "Private repositories"). The runner is already made a member, and invited, by knotless runners add.
  • The runner merges these with its --secrets file at each run, and the rules stay the same: refs limits, no secrets for pulls, merge-queue batches or agents, and masking in the log.
  • knotless runners remove makes the owner re-seal, or drops the runner's copies. Rotating is just set again.
  • Until then, a run whose workflow lists a secret that isn't granted could say so in its log (==> NPM_TOKEN is not granted to <repo> at <ref>), not leave it silently absent.

A design

Where the sealed values live: the appview. The issue's space, with only the owner and the runners as members, would be ideal, but spaces need a spaces-alpha PDS (most owners are on bsky.social), and a public repository has none. Spaces also control access without encrypting, so a value there must be sealed anyway. Public sealed records would work on any PDS but stay on the firehose forever: a runner key that leaks later opens every value ever set. So the appview holds sealed values for now, as GitHub holds Actions secrets sealed to a repository key. It can delete them, and it can never read one. It could swap one in, since HPKE's base mode does not authenticate the sender; that is the cost. A private repository's could move to its space later.

  1. The runner's key. knotless-run makes a P-256 key on its first start, kept beside its state (--secrets-key <file>, 0600), and publishes the public half as a dev.knotless.runnerKey record in its own account, with a key id. A rotation keeps old private keys until nothing is sealed to them.
  2. Sealing. HPKE base mode, RFC 9180: DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, AES-128-GCM. The AAD binds the owner's DID, the repository's key (its TID, so a rename keeps its secrets and a new repository under the old name gets none), the runner's DID, the name and its refs. The runner rebuilds the AAD from where it found the value, so one copied to another repository, name or runner does not open.
  3. The appview. dev.knotless.putSecret, deleteSecret and listSecrets, with service tokens as inviteRunner has. It checks the caller owns the repository and the repository names the runner. The owner lists names, refs, runner and when, never a value; a runner lists its own sealed values for a repository.
  4. CLI and MCP. knotless secret <url> set NAME [--ref <pattern>]... reads the value from stdin or a hidden prompt and seals a copy to each runner the repository names; ls and rm; an MCP secret tool to match.
  5. The runner takes its --secrets file first, then asks the appview for the names still missing; the file wins a clash. Refs, no secrets for pulls, batches or agents, and masking all stay. An appview it cannot reach makes the check error, as an unreadable file does.
  6. Runners. runners remove deletes that runner's copies; runners add names the secrets to set again, which nothing can re-seal without the values.
  7. The log. A run says ==> NPM_TOKEN is not granted to <repo> at <ref> for each secret its workflow lists and does not get.

Pulls, in order: the log line; the runner's key; the appview's methods; the CLI, MCP and runner, with the docs.