Attestation design: signer, key ownership, and verification (deferred) #125

Open
opened 2026-10-06 21:37:04 +02:00 by clawdie · 2 comments
Owner

Summary

Tracks the attestation design that colibri#94 deferred. The current contract is already recorded in docs/plans/JEV-DECISION-GATE.md §7.1 and docs/wiki/credentials.md: caller_attribution is self-declared provenance metadata, and no verified identity field exists yet.

Deferred decisions (all owned here)

  • trusted principal, and how it is independently authenticated;
  • signer identity and its relationship to that principal;
  • key ownership and where keys live;
  • signing direction and algorithm;
  • key management (issuance, rotation, revocation);
  • verification mechanics;
  • the field name and schema for a verified identity/attestation record.

Constraints

  • A new public field name requires operator consolidation (naming standards §5) and must not bake a harness name into a neutral concept.
  • Key management is not yet tested or developed; this design must not assume a scheme that has not been proven.
  • Jev never signs: Jev output is never a signature, authentication, authorization, or approval (JEV-DECISION-GATE.md §9).

Refs

  • colibri#94 — closed by the contract update
  • docs/plans/JEV-DECISION-GATE.md §7 (provenance), §9 (no-authority invariant)
  • docs/wiki/credentials.md — principals, profiles, credential lifecycle
  • agent-skills PR #37 (caller_attribution), PR #38 (jev.env fix)
## Summary Tracks the attestation design that colibri#94 deferred. The current contract is already recorded in `docs/plans/JEV-DECISION-GATE.md` §7.1 and `docs/wiki/credentials.md`: `caller_attribution` is self-declared provenance metadata, and no verified identity field exists yet. ## Deferred decisions (all owned here) - trusted principal, and how it is independently authenticated; - signer identity and its relationship to that principal; - key ownership and where keys live; - signing direction and algorithm; - key management (issuance, rotation, revocation); - verification mechanics; - the field name and schema for a verified identity/attestation record. ## Constraints - A new public field name requires operator consolidation (naming standards §5) and must not bake a harness name into a neutral concept. - Key management is not yet tested or developed; this design must not assume a scheme that has not been proven. - Jev never signs: Jev output is never a signature, authentication, authorization, or approval (`JEV-DECISION-GATE.md` §9). ## Refs - colibri#94 — closed by the contract update - `docs/plans/JEV-DECISION-GATE.md` §7 (provenance), §9 (no-authority invariant) - `docs/wiki/credentials.md` — principals, profiles, credential lifecycle - agent-skills PR #37 (`caller_attribution`), PR #38 (`jev.env` fix)
Author
Owner

Status update — 2026-10-07

Following PR #127 (local caller check) and PR #128 (documentation alignment):

1. Local Unix-socket authorization — implemented

  • peer credentials are obtained via Tokio UnixStream::peer_cred() (SO_PEERCRED on Linux, getpeereid on FreeBSD);
  • the caller must present either the daemon's own UID or root (UID 0);
  • unauthorized callers are rejected before any command dispatch;
  • non-Unix targets fail closed.

This is complete for the current single-UID local-socket policy (daemon UID or root). Two items remain pending: native FreeBSD 15 validation on Valhala, and any future multi-UID broker authorization model (open decision — docs/PRIVILEGE-BOUNDARY-SPEC.md §5.6).

2. Cross-host transport — bridge exists, app-layer identity does not

Colibri's application-layer RPC is a local Unix domain socket. The optional colibri-bridge (socat) exposes that socket over the tailnet, but that boundary is transport-level (Tailscale): the bridge connects to the daemon as a single local UID, so remote callers are not individually identified at the application layer. Kernel peer credentials therefore establish the local transport boundary only; they provide no cross-host identity.

3. Cryptographic payload attestation — deferred

Issue #125 continues to cover:

  • Ed25519 payload signing;
  • signer identity and key ownership;
  • key issuance, rotation, and revocation;
  • verification envelopes and mechanics;
  • authenticated cross-host trust.

Introducing signing envelopes before an authenticated cross-host RPC transport and a key-management design exists would add complexity without changing the current local trust boundary.

Status: #125 remains open and deferred, pending an authenticated cross-host RPC transport, a key-management design, and an operator-approved contract.

Native FreeBSD validation and the multi-UID broker decision remain separate pending work.

### Status update — 2026-10-07 Following PR #127 (local caller check) and PR #128 (documentation alignment): **1. Local Unix-socket authorization — implemented** - peer credentials are obtained via Tokio `UnixStream::peer_cred()` (`SO_PEERCRED` on Linux, `getpeereid` on FreeBSD); - the caller must present either the daemon's own UID or root (UID 0); - unauthorized callers are rejected before any command dispatch; - non-Unix targets fail closed. This is complete for the current single-UID local-socket policy (daemon UID or root). Two items remain pending: native FreeBSD 15 validation on Valhala, and any future multi-UID broker authorization model (open decision — `docs/PRIVILEGE-BOUNDARY-SPEC.md` §5.6). **2. Cross-host transport — bridge exists, app-layer identity does not** Colibri's application-layer RPC is a local Unix domain socket. The optional `colibri-bridge` (socat) exposes that socket over the tailnet, but that boundary is transport-level (Tailscale): the bridge connects to the daemon as a single local UID, so remote callers are not individually identified at the application layer. Kernel peer credentials therefore establish the local transport boundary only; they provide no cross-host identity. **3. Cryptographic payload attestation — deferred** Issue #125 continues to cover: - Ed25519 payload signing; - signer identity and key ownership; - key issuance, rotation, and revocation; - verification envelopes and mechanics; - authenticated cross-host trust. Introducing signing envelopes before an authenticated cross-host RPC transport and a key-management design exists would add complexity without changing the current local trust boundary. **Status:** #125 remains open and deferred, pending an authenticated cross-host RPC transport, a key-management design, and an operator-approved contract. _Native FreeBSD validation and the multi-UID broker decision remain separate pending work._
Author
Owner

Transport identity via the tailnet — what it solves here, and what it does not

Following the 2026-10-07 status update: one prerequisite named in this issue — an authenticated cross-host RPC transport — can be satisfied by the tailnet itself. Worth recording, because it changes the scope of the deferred work. This comment is a proposal, not an implementation; no verified identity field exists in code yet.

The join path (fresh host → tailnet)

   OPERATOR              HEADSCALE (mesh.clawdie.si)        FRESH LIVE FreeBSD
        │                        │                                 │
        │ 1. preauthkey create   │                                 │
        │    (1h, tag)           │                                 │
        │───────────────────────>│                                 │
        │<── tskey-auth-XXXX ────│                                 │
        │                        │                                 │
        │ 2. deliver key (firstboot / Colibri / USB)               │
        │─────────────────────────────────────────────────────────>│
        │                        │ 3. tailscale up --login-server  │
        │                        │    https://mesh.clawdie.si      │
        │                        │    --authkey tskey-auth-XXXX    │
        │                        │<────────────────────────────────│
        │                        │ 4. bind node→user+tag, 100.x.y.z│
        │                        │────────────────────────────────>│
        │                        │ 5. WireGuard tunnel up          │
        │                        │<========= 100.x overlay =======>│

Operator authentication path — two options:

  1. Interactive: tailscale up --login-server https://mesh.clawdie.si → prints a register URL → operator runs headscale nodes register --user <user> --key <nodekey>.
  2. Pre-auth key (scriptable, the ISO path): headscale preauthkeys create --user <user> --reusable --expiration 1h --tags tag:operator-usb, then tailscale up … --authkey tskey-auth-XXXX. Firstboot cannot drive a browser handshake, and a scoped/short-lived/tagged key is auditable and revocable. The key is delivered out-of-band and never baked into the image (same rule as provider keys).

After join

  • WhoIs(remoteAddr) resolves a connection to a verified node ID, user, and tag — a real identity, not self-declared.
  • Headscale ACLs provide network-level authorization: default-deny east-west, with explicit allows by tag (e.g. tag:colibri-client → tag:colibri-control:PORT).
  • Colibri can map the verified tag to an application policy, keeping caller_attribution as the separate (unverified) user claim.

The clean wire is tsnet (colibri is Go): embed a Tailscale node, listen on the tailnet interface, WhoIs() per connection. This replaces the socat bridge, which collapses all remote callers into a single local UID — the exact gap noted above. Node→identity, then identity→policy.

What this does not cover

  • No certificate authority. SaaS Tailscale issues *.ts.net certs; Headscale's support is limited. Colibri TLS stays on Let's Encrypt / an internal CA.
  • No secret store. The two-tier profile contract in docs/wiki/credentials.md (outbound Forgejo/Jev credentials) is unaffected — this addresses inbound caller identity.
  • Node ≠ user, and no non-repudiation. WireGuard authenticates the machine for that hop; a relay/bridge can rewrite the payload. Ed25519 payload signing remains needed for non-repudiation and defense-in-depth on high-privilege calls — but it is no longer blocking a transport.
  • The control plane is a root of trust. Whoever controls Headscale can forge node identities, which is the argument for keeping payload signing as a second gate.

Suggested split

  1. Layer 1 — transport identity: tsnet + tag ACLs + WhoIs() → a verified principal field (field name owned by colibri policy, per naming standards §5).
  2. Layer 2 — payload attestation (this issue): scope reduced to non-repudiation and high-privilege defense; keys can be pinned per principal over the tailnet.
  3. Layer 3 — secrets: unchanged.

Open questions

  • Does the live ISO persist tailscaled state (node identity survives reboot), or use ephemeral keys and re-enroll each boot? This is the one decision that changes the join flow.
  • Which verified field name, if any, colibri adopts — still requires operator consolidation.
  • Are tags sufficient as the principal model, or are per-node keys required for layer 2?

Refs: docs/plans/JEV-DECISION-GATE.md §7/§9, docs/wiki/credentials.md, docs/plans/PATH-ENV-IDENTITY-UNIFICATION.md (cross-host attestation gating), docs/PRIVILEGE-BOUNDARY-SPEC.md §5.6.

## Transport identity via the tailnet — what it solves here, and what it does not Following the `2026-10-07` status update: one prerequisite named in this issue — *an authenticated cross-host RPC transport* — can be satisfied by the tailnet itself. Worth recording, because it changes the scope of the deferred work. This comment is a **proposal**, not an implementation; no verified identity field exists in code yet. ### The join path (fresh host → tailnet) ``` OPERATOR HEADSCALE (mesh.clawdie.si) FRESH LIVE FreeBSD │ │ │ │ 1. preauthkey create │ │ │ (1h, tag) │ │ │───────────────────────>│ │ │<── tskey-auth-XXXX ────│ │ │ │ │ │ 2. deliver key (firstboot / Colibri / USB) │ │─────────────────────────────────────────────────────────>│ │ │ 3. tailscale up --login-server │ │ │ https://mesh.clawdie.si │ │ │ --authkey tskey-auth-XXXX │ │ │<────────────────────────────────│ │ │ 4. bind node→user+tag, 100.x.y.z│ │ │────────────────────────────────>│ │ │ 5. WireGuard tunnel up │ │ │<========= 100.x overlay =======>│ ``` **Operator authentication path** — two options: 1. Interactive: `tailscale up --login-server https://mesh.clawdie.si` → prints a register URL → operator runs `headscale nodes register --user <user> --key <nodekey>`. 2. Pre-auth key (scriptable, the ISO path): `headscale preauthkeys create --user <user> --reusable --expiration 1h --tags tag:operator-usb`, then `tailscale up … --authkey tskey-auth-XXXX`. Firstboot cannot drive a browser handshake, and a scoped/short-lived/tagged key is auditable and revocable. The key is delivered out-of-band and never baked into the image (same rule as provider keys). ### After join - `WhoIs(remoteAddr)` resolves a connection to a **verified** node ID, user, and tag — a real identity, not self-declared. - Headscale **ACLs** provide network-level authorization: default-deny east-west, with explicit allows by tag (e.g. `tag:colibri-client` → `tag:colibri-control:PORT`). - Colibri can map the verified tag to an application policy, keeping `caller_attribution` as the separate (unverified) user claim. The clean wire is **`tsnet`** (colibri is Go): embed a Tailscale node, listen on the tailnet interface, `WhoIs()` per connection. This replaces the socat bridge, which collapses all remote callers into a single local UID — the exact gap noted above. Node→identity, then identity→policy. ### What this does *not* cover - **No certificate authority.** SaaS Tailscale issues `*.ts.net` certs; Headscale's support is limited. Colibri TLS stays on Let's Encrypt / an internal CA. - **No secret store.** The two-tier profile contract in `docs/wiki/credentials.md` (outbound Forgejo/Jev credentials) is unaffected — this addresses *inbound* caller identity. - **Node ≠ user, and no non-repudiation.** WireGuard authenticates the *machine* for that hop; a relay/bridge can rewrite the payload. Ed25519 payload signing remains needed for non-repudiation and defense-in-depth on high-privilege calls — but it is no longer blocking a transport. - **The control plane is a root of trust.** Whoever controls Headscale can forge node identities, which is the argument for keeping payload signing as a second gate. ### Suggested split 1. **Layer 1 — transport identity:** `tsnet` + tag ACLs + `WhoIs()` → a verified principal field (field name owned by colibri policy, per naming standards §5). 2. **Layer 2 — payload attestation (this issue):** scope reduced to non-repudiation and high-privilege defense; keys can be pinned per principal over the tailnet. 3. **Layer 3 — secrets:** unchanged. ### Open questions - Does the live ISO persist `tailscaled` state (node identity survives reboot), or use ephemeral keys and re-enroll each boot? This is the one decision that changes the join flow. - Which verified field name, if any, colibri adopts — still requires operator consolidation. - Are tags sufficient as the principal model, or are per-node keys required for layer 2? Refs: `docs/plans/JEV-DECISION-GATE.md` §7/§9, `docs/wiki/credentials.md`, `docs/plans/PATH-ENV-IDENTITY-UNIFICATION.md` (cross-host attestation gating), `docs/PRIVILEGE-BOUNDARY-SPEC.md` §5.6.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
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
clawdie/colibri#125
No description provided.