gabriel / musehub public
threat-model.md markdown
184 lines 7.8 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 9 days ago

MuseHub Threat Model

Last updated: 2026-04-07
Author: gabriel


System Overview

MuseHub is the remote server for the Muse version control system. It stores pushed commits, hosts issue tracking and merge proposals, and serves as the hub for multi-agent orchestration via agentception. Two types of principals interact with MuseHub:

  • Human users — authenticated via Ed25519 keypairs registered at account creation.
  • Agent identities — short-lived Ed25519 keypairs registered programmatically by agentception at runtime, carrying an explicit capability scope and an expiry timestamp.

All communication is over HTTPS. There are no passwords, sessions, or bearer tokens. Every authenticated request carries a per-request Ed25519 signature.


Authentication: MSign

Protocol

Every authenticated request carries:

Authorization: MSign handle="gabriel" ts=1712345678 sig="<base64url>"

The signature covers the canonical message:

METHOD\n
PATH_WITH_QUERY\n
UNIX_TIMESTAMP\n
SHA256_HEX(body_bytes)

The server verifies the signature against the registered public key(s) for the handle. Replay protection: requests are rejected when |server_time - ts| > 30 seconds.

Key properties

Property Value
Algorithm Ed25519 (RFC 8032)
Key storage (server) Public key only, stored in musehub_auth_keys
Key storage (client) Private key in ~/.muse/keys/*.pem
Replay window 30 seconds
Clock skew tolerance ±30 seconds

Threat mitigations

Threat Mitigation
Replay attack Timestamp in canonical message; server rejects if skew > 30s
Man-in-the-middle HTTPS required; signature covers body hash, not just path
Key compromise (human) muse auth keygen + revoke_identity; soft-delete immediately visible
Key compromise (agent) Short TTL (2h by default); agentception revokes at run end
Forged handle Handle is looked up in DB; must match a row with a registered key
Credential stuffing No passwords; Ed25519 keys cannot be brute-forced
Timing oracle Signature verification iterates all keys; failure path is constant

Identity Hierarchy

gabriel (human, permanent)
  └── agentception-service (service identity, 24h TTL)
        └── agentception-{run_id} (ephemeral agent identity, 2h TTL)

Identity types

Type identity_type scope expires_at
Human "human" null (unrestricted) null (never expires)
Service "agent" ["label:write", "issue:write", "proposal:write", "label:read", "issue:read"] 24h from registration
Ephemeral agent "agent" Subset of service scope 2h from dispatch

Identity lifecycle

  1. Registration: POST /api/v1/identities/agents — requires a valid MSign token from the parent identity. Stores public key and scope.
  2. Request signing: Agent signs requests with its private key. Server verifies against stored public key.
  3. Scope check: require_scope("X") dependency rejects requests where identity.scope is non-null and does not contain "X".
  4. Expiry check: _verify_msign rejects requests for identities where expires_at <= now. Checked before signature verification.
  5. Revocation: DELETE /api/v1/identities/{handle} — sets deleted_at, immediately visible to all subsequent requests.

Capability Scopes

Scope enforcement is implemented in require_scope() (musehub/auth/request_signing.py). Rules:

  • scope = null — human identity; no capability restrictions. All authenticated operations are permitted.
  • scope = [...] — agent identity; only listed tokens are permitted. Missing token → HTTP 403.
  • Empty scope [] — agent is blocked from every scoped operation.

Scope taxonomy

Token Endpoints
repo:write POST repos; DELETE repo; transfer ownership; PATCH settings; POST/stop sessions; rebuild symbol index
issue:read GET issues, GET comments (authenticated private repos)
issue:write POST/PATCH issues; POST/DELETE comments; set/remove milestone; reopen/close
label:read GET label assignments (authenticated private repos)
label:write POST/DELETE/PATCH labels; POST/DELETE label assignments on issues and proposals
proposal:read GET proposals, GET reviews (authenticated private repos)
proposal:write POST proposals; merge; POST comments; request/remove reviewers; submit reviews
release:write POST releases; POST/DELETE release assets
milestone:write POST/PATCH/DELETE milestones

Who holds which scopes

Principal Scopes
gabriel (human) All (unrestricted, scope=null)
agentception-service issue:write, issue:read, label:write, label:read, proposal:write
agentception-{run_id} Subset of parent scope, decided at dispatch

Human identities carry scope = null and pass all scope checks unconditionally — there are no human-only capabilities. Any operation can be granted to an agent by including the relevant token in its scope list at registration time.


Defence-in-Depth Layers

Layer 1: Bot throttle (ASGI middleware)

BotThrottleMiddleware runs before any route handler. It short-circuits at the ASGI level for known-bad User-Agent patterns (scrapers, scanners). Exempt paths: /healthz, /static/, /mcp, /api/health/.

MSign-authenticated requests bypass the UA check entirely — cryptographic identity supersedes User-Agent heuristics.

Layer 2: Request signing (MSign)

Every route that mutates state requires a valid MSign token. Anonymous access is permitted only for read operations on public repos.

Layer 3: Scope enforcement

Write routes use Depends(require_scope("X")) instead of Depends(require_valid_token). This ensures agent identities are limited to their registered capability set.

Layer 4: Expiry enforcement

_verify_msign checks expires_at before attempting signature verification. An expired identity is rejected even if its private key is still cryptographically valid.

Layer 5: Soft-delete / revocation

deleted_at is checked in every identity lookup. Revoked identities are immediately invisible without requiring key rotation or cache invalidation.

Layer 6: Per-route slowapi rate limits

Routes are rate-limited by IP + identity. Authentication failures do not count against the route limit but are tracked by failure_limiter to detect credential stuffing.


Known Limitations and Future Work

Item Status
Scope enforcement on wire protocol (push/pull) Not implemented. Wire operations use human keys; agents don't push. Tracked as future work.
Scope audit log Not implemented. Scope denials are logged at WARNING level but not stored in DB.
Agent-to-agent delegation Not implemented. An agent cannot grant sub-scope to another agent directly.
Key rotation without downtime Supported — multiple keys per identity; verification tries all.
Scope changes without re-registration Not supported. Scope is set at registration and cannot be modified. Revoke and re-register.

Incident Response

Compromised human key

  1. muse auth keygen --hub <hub> — generate a new key.
  2. DELETE /api/v1/identities/{handle}/keys/{key_id} — revoke the old key.
  3. All subsequent requests using the old key are rejected immediately.

Compromised agent key

  1. DELETE /api/v1/identities/{handle} — soft-delete the agent identity. Takes effect immediately.
  2. Generate a new ephemeral identity for the next run.

Suspected replay attack

  1. Check access logs for duplicate (handle, ts) pairs within the 30-second window.
  2. If systematic, reduce REPLAY_WINDOW_SECONDS and rotate the affected identity's key.
File History 14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 9 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 9 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 12 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 15 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 30 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 35 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 36 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 51 days ago