gabriel / musehub public
threat-model.md markdown
184 lines 7.8 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day 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 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago