feat(web): gate agent-binary download with scoped install tokens (VS-57) #77

Closed
nrupard wants to merge 2 commits from feat/VS-57-authorize-agent-binary-download into main
Owner

Stacked on #74 (VS-56). Base is feat/VS-56-public-agent-binary-download; review/merge that first. The diff here is only the VS-57 authz layer.

What

Gates the public agent-binary download endpoint (/api/v1/agent/binary/:id, VS-56) so it is not an open redistribution point on the API host.

Why this model

The endpoint's real consumer is the device-side install snippet's unattended curl (curl -fL -o vervain-agent <api>/api/v1/agent/binary/6), which runs minutes/hours after the operator generates it and carries no vervain_session. A browser-session gate or the single-use 60s control-token (VS-32) does not fit. Chosen model (confirmed with the maintainer): a scoped, expiring install token.

How

  • InstallTokenStore mirrors the VS-32 control-token store but is multi-use within a 24 h lifetime - one generated snippet enrolls a batch of devices within a day, then the URL stops working. In-memory only (a restart clears outstanding tokens; regenerate the snippet), carrying the minting user id for audit.
  • GET /agentbinaries/install-token mints one, gated by the existing session, returning {"token", "ttl_secs"}. The apps enrollment flow (get_group_install) will call it with the forwarded session and bake the token into the download URL as ?token= (that wiring is VAPP-35).
  • The download handler validates the token first, before any upstream read, so an absent/unknown/expired token returns a bare 401 with no body and no Forgejo contact.

Acceptance criteria

  • Unauthorized requests -> 401, no binary bytes, no upstream detail (tests download_401_without_install_token, download_401_with_unknown_install_token).
  • Authorized-but-unconfigured -> 404 only after the token passes (download_404_when_authorized_but_no_fetcher).
  • Authz decision lands before the Forgejo stream begins (token check is the first thing in the handler).
  • The server-side Forgejo PAT is unaffected and never client-derivable (VS-56 already ensures this).
  • Model documented in the README (who / credential / lifetime).

Test

cargo clippy -p vervain-web --all-targets -- -D warnings clean; full pre-commit (fmt + clippy + build + test) green, 448 tests pass incl. 10 new.

**Stacked on #74 (VS-56).** Base is `feat/VS-56-public-agent-binary-download`; review/merge that first. The diff here is only the VS-57 authz layer. ## What Gates the public agent-binary download endpoint (`/api/v1/agent/binary/:id`, VS-56) so it is not an open redistribution point on the API host. ## Why this model The endpoint's real consumer is the device-side install snippet's unattended `curl` (`curl -fL -o vervain-agent <api>/api/v1/agent/binary/6`), which runs minutes/hours after the operator generates it and carries no `vervain_session`. A browser-session gate or the single-use 60s control-token (VS-32) does not fit. Chosen model (confirmed with the maintainer): a scoped, expiring **install token**. ## How - `InstallTokenStore` mirrors the VS-32 control-token store but is **multi-use within a 24 h lifetime** - one generated snippet enrolls a batch of devices within a day, then the URL stops working. In-memory only (a restart clears outstanding tokens; regenerate the snippet), carrying the minting user id for audit. - `GET /agentbinaries/install-token` mints one, gated by the existing session, returning `{"token", "ttl_secs"}`. The apps enrollment flow (`get_group_install`) will call it with the forwarded session and bake the token into the download URL as `?token=` (that wiring is VAPP-35). - The download handler validates the token **first, before any upstream read**, so an absent/unknown/expired token returns a bare `401` with no body and no Forgejo contact. ## Acceptance criteria - Unauthorized requests -> `401`, no binary bytes, no upstream detail (tests `download_401_without_install_token`, `download_401_with_unknown_install_token`). - Authorized-but-unconfigured -> `404` only after the token passes (`download_404_when_authorized_but_no_fetcher`). - Authz decision lands before the Forgejo stream begins (token check is the first thing in the handler). - The server-side Forgejo PAT is unaffected and never client-derivable (VS-56 already ensures this). - Model documented in the README (who / credential / lifetime). ## Test `cargo clippy -p vervain-web --all-targets -- -D warnings` clean; full pre-commit (fmt + clippy + build + test) green, 448 tests pass incl. 10 new.
The public agent-binary download endpoint (VS-56, /api/v1/agent/binary/:id) is on the API host, so it must not be an open redistribution point. Its real consumer is the device-side install snippet's unattended curl, which carries no vervain_session, so a browser-session or single-use control-token gate does not fit. Add a scoped, expiring install token instead.

InstallTokenStore mirrors the VS-32 control-token store but is multi-use within a 24 h lifetime: one generated snippet can enroll a batch of devices within a day, after which the URL stops working. Tokens are in-memory only (a restart clears them; the operator regenerates the snippet) and carry the minting user id for audit. GET /agentbinaries/install-token mints one, gated by the existing session, returning {token, ttl_secs}; the apps enrollment flow (get_group_install) calls it with the forwarded session and bakes the token into the download URL as ?token=. The download handler validates the token FIRST, before any upstream read, so an absent/unknown/expired token is rejected with 401, no body, and no Forgejo contact.

The apps repoint that mints + appends the token is VAPP-35. README documents the who/credential/lifetime model; the VS-56 deferral notes in wire_paths are updated now that the gate exists.

#VS-57

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: compile-check with cargo check, not the disk-bombing build --all-targets
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
b1676a8028
The Check workflow ran cargo build --all-targets, which links every integration-test binary (tls_smoke, mcp_gateway, device_file, control_token, login, setup, relay_listing, admin), each statically linking the full wasmtime/leptos/aws-lc/rustls closure. Linking those in parallel exhausted the runner's disk and the link died with ENOSPC / lld SIGBUS, failing the check even though the code is fine.

Those executables are never run: CI only runs cargo test --lib. And clippy --all-targets, which runs first, already typechecks every target. So the build step was pure waste whose only effect was filling the disk. Replace it with cargo check --workspace --all-targets, which verifies all targets compile without producing the binaries, matching the local just check-compile recipe. The just pre-commit hook carried the same wasteful build --all-targets; switch it to cargo check too so local and CI stay in lockstep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard changed target branch from feat/VS-56-public-agent-binary-download to main 2026-06-26 16:42:41 +02:00
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-26 16:43:15 +02:00
Owner

Closing as superseded by VS-60 (#79), already merged to main.

VS-60 built exactly what this PR (VS-57) targeted: it mints the scoped install token and gates /api/v1/agent/binary/:id before any upstream read. Its own commit message notes it adds "the client-facing authorization VS-57 left unbuilt." VS-60 landed a deliberately different (maintainer-chosen) model: a 5-minute token that the handler accepts as EITHER a valid vervain_session OR a ?token=, versus this PR's token-only 24h model.

Everything in this PR is now either already in main or intentionally replaced:

  • Install-token mint + download gate: superseded by VS-60's model.
  • The CI fix (cargo check --workspace --all-targets instead of build --all-targets): already in main's check.yml.
  • Base branch feat/VS-56-public-agent-binary-download has merged and been deleted; VS-56 is in main.

Merging would re-introduce the older token-only model over VS-60 and require resolving heavy conflicts in install_token.rs / lib.rs / wire_paths.rs. No salvageable delta, so closing rather than rebasing.

Closing as superseded by VS-60 (#79), already merged to main. VS-60 built exactly what this PR (VS-57) targeted: it mints the scoped install token and gates `/api/v1/agent/binary/:id` before any upstream read. Its own commit message notes it adds "the client-facing authorization VS-57 left unbuilt." VS-60 landed a deliberately different (maintainer-chosen) model: a 5-minute token that the handler accepts as EITHER a valid `vervain_session` OR a `?token=`, versus this PR's token-only 24h model. Everything in this PR is now either already in main or intentionally replaced: - Install-token mint + download gate: superseded by VS-60's model. - The CI fix (cargo check --workspace --all-targets instead of build --all-targets): already in main's check.yml. - Base branch feat/VS-56-public-agent-binary-download has merged and been deleted; VS-56 is in main. Merging would re-introduce the older token-only model over VS-60 and require resolving heavy conflicts in install_token.rs / lib.rs / wire_paths.rs. No salvageable delta, so closing rather than rebasing.
David closed this pull request 2026-06-26 21:56:05 +02:00
Some checks are pending
Create release / Create release from merged PR (pull_request) Has been skipped
Check / * (pull_request)
Required

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!77
No description provided.