Skip to content

F2.B — Keys table, sk-cap key generator, first-run admin bootstrap #28

@HXYerror

Description

@HXYerror

Part of #23. Depends on F2.A.

Background

No API key concept exists. We need opaque random keys with two tiers (admin / client), hashed at rest, and a safe first-run bootstrap.

Goal

A keys table, sk-cap-… generator, and a first-run flow that mints exactly one admin key.

Tasks

  • Migration 002_keys.sql:
    CREATE TABLE keys (
      id TEXT PRIMARY KEY,
      hash TEXT UNIQUE NOT NULL,
      tier TEXT NOT NULL CHECK(tier IN ('admin','client')),
      label TEXT,
      allowed_models TEXT NOT NULL DEFAULT '["*"]',  -- JSON array
      rate_limit_override INTEGER,                     -- NULL = use global; capped (see below)
      debug_enabled INTEGER NOT NULL DEFAULT 0,
      created_at INTEGER NOT NULL,
      revoked_at INTEGER
    );
    CREATE INDEX idx_keys_hash ON keys(hash);
  • generateKey() using crypto.randomBytes(32)sk-cap- + base32 (RFC4648 no padding) = 7 + 52 = 59 char total
  • hashKey(plain) = sha256 hex
  • Bootstrap (per security S3, S4):
    • Condition: features.auth === true && admin-tier key count === 0 (NOT just total key count — preserves recovery semantics)
    • Generate one admin key
    • Write plaintext to ~/.local/share/copilot-api/admin.key.txt with mode 0600
    • Print to stdout: only when isatty(1) is true. In non-TTY (Docker logs / journald), print only the file path + sha256 prefix Admin key written to /path/admin.key.txt (sha256:abc1234…). Read & delete this file.
    • On next boot, if admin.key.txt exists, refuse to start with a clear "operator must read+delete the bootstrap file" message
  • Recovery CLI (per security S4): copilot-api admin recover — requires read access to data dir as proof of operator identity, mints a fresh admin key, audit-logs the action (see F2.D for audit log)
  • Rate limit override safety (per security S8): treat 0 / negative / NULL as "use default", NOT unlimited. Hard cap override at 10× the configured global default; reject values above cap with explicit error.
  • CRUD helpers: createKey(tier, label, allowed_models, override?), revokeKey(id), listKeys(), findKeyByHash(hash), setDebugEnabled(id, bool)
  • Tests: key entropy check, hash determinism, bootstrap idempotency, recovery flow, lockout reproduction (revoke all admin keys → recover restores), rate-limit cap enforcement

Acceptance criteria

  • Plain key value never written to DB anywhere; only sha256 hash
  • TTY user sees the literal key once; non-TTY user sees only path
  • Revoking all admin keys does NOT trigger silent rebootstrap (must call admin recover)
  • Rate limit override cannot bypass the global cap

File pointers

  • New: src/lib/migrations/002_keys.sql, src/services/keys.ts, src/cli/admin-recover.ts, tests/keys.test.ts
  • Touch: src/start.ts, src/main.ts (citty subcommand)

Dependencies

Depends on F2.A. Blocks F2.C, F2.D, F3.A, F4.A.

Metadata

Metadata

Assignees

No one assigned

    Labels

    authAuthentication / authorizationsecuritySecurity-sensitivestoragePersistence layer

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions