Skip to content

marmot-protocol/keycast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Keycast

Secure remote signing and team-managed permissions for Nostr keys.

Keycast is an early-stage project. A May 2026 audit found and fixed several auth, permission, data integrity, and dependency issues, but this should still get a deployment review before it is trusted with real team keys on the public internet. See AUDIT.md for the current status.

What It Does

Keycast lets a team:

  • sign in with a Nostr key and manage team membership,
  • store Nostr private keys encrypted at rest in SQLite,
  • create policies and permissions for stored keys,
  • generate NIP-46 bunker connection strings,
  • run signer processes that approve or deny remote signing requests from those policies.

The project was written for teams rather than individual remote-signing workflows like nsec.app, Knox, or Amber.

Project Layout

  • core/ - shared Rust models, database helpers, encryption, and permission checks.
  • api/ - Axum API, SQLite setup, NIP-98 HTTP auth middleware, and team/key/policy routes.
  • signer/ - signer manager plus one signer_daemon process per authorization.
  • web/ - SvelteKit app that asks a NIP-07 signer to sign NIP-98 auth events.
  • database/migrations/ - SQLite schema.
  • scripts/ - key generation, Docker entrypoint, health checks, and deployment init.

Current Validation State

These commands were run after the May 14, 2026 hardening pass:

  • cargo check --workspace passes with no warnings.
  • cargo build --workspace passes.
  • cargo test --workspace passes.
  • cd web && bun test passes.
  • cd web && bun run check passes with no warnings.
  • cd web && bun run build passes with no warnings.
  • bun audit and cd web && bun audit report no vulnerabilities.
  • bun pm scan and cd web && bun pm scan run through the configured Socket scanner and report no advisories in free mode.
  • cargo audit exits quietly. .cargo/audit.toml documents the ignored upstream instant advisory, which is pulled into the lockfile by nostr for wasm targets and is not used by the native runtime.
  • bash -n passes for scripts/init.sh, scripts/upgrade_preflight.sh, scripts/docker-entrypoint.sh, scripts/healthcheck.sh, and scripts/generate_key.sh.
  • Migration 0002_normalize_allowed_kinds_permissions.sql was smoke-tested against a migrated SQLite database and converts the legacy {"sign":[...]} permission shape to {"allowed_kinds":[...]}.
  • Docker Compose config rendering passes with explicit ALLOWED_PUBKEYS, KEYCAST_UID, and KEYCAST_GID values.
  • DOCKER_BUILDKIT=1 docker build -t keycast-runtime-check . passes with digest-pinned base images, frozen lockfiles, and production frontend dependencies.
  • The built API image starts under --read-only, creates a fresh SQLite database on a writable bind mount, and applies migrations 1 and 2.
  • The built web image starts as UID/GID 10001, serves /health under --read-only plus /tmp tmpfs, and emits the expected CSP, HSTS, frame, referrer, permissions, and content-type headers.

Setup

Requirements:

  • Rust toolchain
  • Bun
  • SQLx CLI for database reset commands
  • cargo-watch for the combined dev command
  • openssl or /dev/urandom for master.key generation

Install dependencies:

bun install
cd web && bun install

Generate the local encryption key:

bun run key:generate

This creates master.key at the repo root. It is ignored by git and must never be committed. The current FileKeyManager reads this root file directly.

Create .env from .env.example for Docker/deployment settings:

cp .env.example .env

ALLOWED_PUBKEYS is enforced by the API for NIP-98-authenticated requests. The API also exposes an unauthenticated /api/config?pubkey=<hex> check so the browser can ask whether the current pubkey is allowed without receiving the full server allowlist.

Development

Run API, web, and signer together:

bun run dev

The local dev scripts run the API on http://localhost:3100 and point the web app at that origin. The Docker/runtime default API port remains 3000.

Run pieces separately:

bun run dev:api
bun run dev:web
bun run dev:signer

Reset the SQLite database:

bun run db:reset

Useful validation commands:

cargo check --workspace
cargo build --workspace
cargo test --workspace
cd web && bun test
cd web && bun run check
cd web && bun run build
cargo audit
bun audit
cd web && bun audit
bun pm scan
cd web && bun pm scan
cd web && bun pm untrusted

Bun Security Scanning

bun pm scan is Bun's pluggable package scanner. Bun does not include a scanner by default; it loads an npm package named in bunfig.toml:

[install.security]
scanner = "@socketsecurity/bun-security-scanner"

This repo now installs @socketsecurity/bun-security-scanner@1.1.2 exactly in both Bun projects. It runs in Socket free mode without credentials. Set SOCKET_API_KEY only if you want scans to use a Socket organization policy.

Runtime Flow

  1. The web app signs in through a selected external signer: NIP-07 browser extension, Amber/NIP-55 clipboard signing on Android, or a bunker:// NIP-46 remote signer.
  2. API calls include a NIP-98 HTTP auth event in the Authorization header.
  3. The API verifies the event, extracts the pubkey, and checks team membership for team reads and admin rights for mutations or authorization-secret-bearing routes.
  4. Stored keys and per-authorization bunker keys are encrypted with the root master.key.
  5. The signer manager watches authorization rows and starts a signer_daemon for each one.
  6. Each signer daemon decrypts the stored key and bunker key, listens on configured relays, and calls Authorization::validate_policy before approving NIP-46 requests.

Security Notes

The current hardening posture:

  • NIP-98 auth now requires exactly one u tag and one method tag, rejects stale or far-future timestamps, validates request-body payload hashes, and rejects oversized authenticated bodies.
  • ALLOWED_PUBKEYS is parsed exactly and enforced server-side.
  • Allowed-kinds permission config now uses one Rust/TypeScript shape and rejects unknown fields.
  • Empty policies default-deny sign/encrypt/decrypt requests, and policy creation rejects empty, unknown, or malformed permission configs.
  • SQLite connections enable foreign-key enforcement, and team deletion no longer loses permission join data before cleanup.
  • Server-side route protection now covers nested app routes such as /teams/:id, not only exact top-level paths.
  • Web responses set CSP, frame, content-type, referrer, permissions, and HTTPS HSTS headers.

Keep future fixes narrow and test-backed. The code handles private key material, so correctness and access control matter more than cosmetic cleanup.

Deployment

Docker deployment uses:

  • docker-compose.yml for local source builds of API, web, and signer containers,
  • docker-compose.prod.yml for pulling the published ghcr.io/marmot-protocol/keycast image,
  • master.key mounted into API and signer containers,
  • an external Docker network named keycast,
  • Caddy labels for routing /api/* to the API and the rest to the web app.

The compose defaults now require ALLOWED_PUBKEYS, expose the API only on the Docker network, build with frozen Bun/Cargo lockfiles, and avoid copying master.key into the image. Runtime containers run as a non-root UID/GID, use a read-only root filesystem, drop Linux capabilities, set no-new-privileges, and get a writable /tmp tmpfs. The Caddy example uses a read-only Docker socket and a digest-pinned caddy-docker-proxy image. .dockerignore excludes local build output, package installs, databases, .env files, and master.key from the build context.

Initialize a server checkout:

bash scripts/init.sh --domain keycast.example.com --allowed-pubkeys "hexpubkey1,hexpubkey2"
sudo docker compose -f docker-compose.prod.yml pull
sudo docker compose -f docker-compose.prod.yml up -d

scripts/init.sh writes KEYCAST_UID and KEYCAST_GID into .env, defaults them to the current user, and tries to set matching ownership on database/ and master.key. Pass --uid and --gid if the container user should be different.

Every push to master publishes one reusable image to GitHub Container Registry with master, latest, and sha-<commit> tags. Set KEYCAST_IMAGE_TAG=sha-<commit> in .env when you want a pin-and-roll-forward deployment instead of tracking the moving master tag. If GitHub creates the first package as private, change the package visibility to public in the GitHub Packages settings.

For an existing deployment, do not use the blank-install flow blindly. Read UPGRADE.md, back up database/keycast.db and master.key together, verify the host master.key matches the currently running container, set ALLOWED_PUBKEYS, and run:

scripts/upgrade_preflight.sh
sudo scripts/upgrade_preflight.sh --fix-permissions

The new SQL migration normalizes old allowed-kinds permission JSON during API or signer startup. It does not rotate keys, change encrypted stored-key material, or intentionally invalidate existing NIP-46 bunker connection strings. Keep database/migrations/ present on the server because the API and signer read migrations from the bind-mounted database/ directory. If you roll back after the migration has run, restore the database backup as well as the matching master.key.

Before using this for real keys, review the residual notes in AUDIT.md, configure the proxy headers used by NIP-98 URL validation, and rerun the validation commands above.

Custom Permissions

Permissions are implemented in core/src/custom_permissions. To add or change a permission, update all of these places together:

  • Rust permission implementation and config type in core/src/custom_permissions/
  • AVAILABLE_PERMISSIONS in core/src/custom_permissions/mod.rs
  • Permission::to_custom_permission in core/src/types/permission.rs
  • web permission types in web/src/lib/types.ts
  • web form rendering in web/src/lib/components/PermissionForm.svelte
  • README or audit notes if the semantics affect signing approval

Do not rely on frontend-only validation for permission safety.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors