Files
goclaw/docs/20-api-keys-auth.md
T
Viet Tran 037d18f711 docs: comprehensive audit and update of all documentation (#231)
* feat(ui): improve kanban UX, fix dialog scroll, remove delegation page

- Kanban: reorder columns (blocked after pending), show blocked-by info
  on cards, clickable blocker links in task detail, framer-motion card
  animation between columns
- Dialogs: standardize scroll pattern across all modals — header fixed,
  scrollbar flush with outer edge via negative margin trick
- Remove delegation page, types, events, i18n, routes, and all references
- Fix activity_logs NULL jsonb scan error (COALESCE)
- Board header: show text labels on action buttons (desktop)

* docs: comprehensive audit and update of all documentation

- Update Go 1.25 → 1.26, PostgreSQL 15+ → 18 across all docs
- Add 10 missing internal modules to CLAUDE.md project structure
- Expand provider docs from 2 to 6 packages (Anthropic, OpenAI, DashScope, Claude CLI, ACP, Codex)
- Add 8 missing store interfaces to data model docs (22 total)
- Update bootstrap files from 7 to 13 templates
- Expand tool inventory from ~35 to 60+ tools with media/KG/credential categories
- Fix Team Task Board: add blocked status, 3 missing actions, V2 versioning, delegate restrictions
- Remove all references to removed features: handoff, delegate_search, evaluate_loop, agent_links
- Fix lane defaults (2/4/1 → 30/50/100/30), ghost file references, models.list → providers.models
- Add SecureCLI, snapshot worker, cost calculation, pairing security docs
- Comprehensive changelog catch-up
- Trim docs/03-tools-system.md to 800-line limit
2026-03-16 22:51:57 +07:00

409 lines
15 KiB
Markdown

# 20 — API Keys & Authentication
GoClaw supports two authentication mechanisms: a single gateway token (configured at startup) and multiple API keys with fine-grained RBAC scopes. Both work across HTTP REST and WebSocket RPC.
---
## 1. Gateway Token
The gateway token is set in `config.json` under `gateway.token`. It grants full admin access.
```json5
{
"gateway": {
"token": "my-secret-token"
}
}
```
Used as a Bearer token:
```
Authorization: Bearer my-secret-token
```
Or in WebSocket `connect`:
```json
{"method": "connect", "params": {"token": "my-secret-token", "user_id": "admin"}}
```
### Security
The gateway token is compared using **constant-time comparison** (`crypto/subtle.ConstantTimeCompare`) in both HTTP (`auth.go:tokenMatch`) and WebSocket (`router.go:handleConnect`) to prevent timing attacks. The comparison reveals no information about where the provided token first differs from the expected token.
---
## 2. API Keys
API keys provide scoped, revocable access for CI/CD, integrations, and third-party clients.
### Key Format
```
goclaw_a1b2c3d4e5f6789012345678901234567890abcdef
```
- **Prefix:** `goclaw_` (6 chars)
- **Random:** 32 hex characters (128 bits of entropy)
- **Display prefix:** `goclaw_` + first 8 hex chars (shown in UI after creation)
### Security Model
Keys are hashed with SHA-256 before storage — the raw key is never persisted. This follows the same pattern as GitHub Personal Access Tokens.
```
raw key → SHA-256 → stored hash
```
On authentication, the incoming token is hashed and looked up in the `api_keys` table via a partial index on non-revoked keys.
### Show-Once Pattern
The raw key is returned **only once** in the create response. All subsequent list/get calls show only the `prefix` field (8 hex characters representing the first 4 bytes of the random component). Users must copy the key immediately after creation, as it cannot be retrieved again.
---
## 3. RBAC Scopes
Each API key is assigned one or more scopes that determine what operations it can perform. Scopes are validated against the set of valid scopes at creation time.
| Scope | Description |
|-------|-------------|
| `operator.admin` | Full access — equivalent to gateway token, can manage API keys and SecureCLI configs |
| `operator.read` | Read-only access to agents, sessions, skills, chat history, and metadata |
| `operator.write` | Read + write access — can send chat messages, manage sessions, trigger cron jobs |
| `operator.approvals` | Manage shell command execution approvals (approve/deny exec requests) |
| `operator.pairing` | Manage browser device pairings (list, revoke paired devices) |
### Role Derivation
The highest-privilege scope determines the effective role via `RoleFromScopes()` in `permissions/policy.go`:
```
if admin scope present → RoleAdmin
if write/approvals/pairing scope → RoleOperator
if read scope only → RoleViewer
default → RoleViewer
```
The derived role is then used by the `PolicyEngine.CanAccess()` method to gate RPC method access (see [19 — WebSocket RPC](19-websocket-rpc.md#17-permission-matrix)).
---
## 4. Authentication Flow
### Prioritized Auth Paths
GoClaw tries authentication methods in this priority order:
1. **Gateway token** (exact match via constant-time comparison) → `RoleAdmin`
2. **API key** (SHA-256 hash lookup in `api_keys` table) → role from scopes
3. **Browser pairing** (sender ID must be paired with "browser" device type) → `RoleOperator` (HTTP only; requires `X-GoClaw-Sender-Id` header)
4. **No auth configured** (backward compatibility: if no gateway token is set) → `RoleOperator`
5. **No valid auth found**`401 Unauthorized`
### HTTP Request Flow
```mermaid
flowchart TD
A[Incoming HTTP request] --> B{Authorization header?}
B -->|No| C{X-GoClaw-Sender-Id header?}
B -->|Yes, extract Bearer token| D{Match gateway token?}
D -->|Yes| E[RoleAdmin]
D -->|No| F[Hash token + lookup in api_keys]
F --> G{Found + valid?}
G -->|Yes| H[Derive role from scopes]
G -->|No| I{Gateway token configured?}
I -->|Yes| J[401 Unauthorized]
I -->|No| K[RoleOperator - backward compat]
C -->|Check paired device| L{Device paired?}
L -->|Yes| M[RoleOperator]
L -->|No| J
E --> N[Authenticate request]
H --> N
K --> N
M --> N
```
### WebSocket Connect Flow
The same auth paths apply for WebSocket `connect` messages. The connection parameter `token` is checked against the gateway token first, then API keys, then browser pairing.
### API Key Caching
API keys are cached in-memory with a 5-minute TTL to reduce database load. The cache:
- Maps SHA-256 hash → `APIKeyData` + derived role
- Supports **negative caching** (tracks failed lookups) to prevent repeated DB queries for invalid tokens
- Caps negative cache to 10,000 entries to prevent memory exhaustion from token spraying attacks
- Is invalidated globally via pubsub on key creation, revocation, or update
### Last-Used Tracking
On successful API key authentication, `last_used_at` is updated asynchronously (fire-and-forget goroutine with 5-second timeout) to avoid blocking the request path. This tracks API usage without slowing down authentication.
---
## 5. Authentication Priority & Backward Compatibility
### HTTP Request Headers
- **Bearer token**: `Authorization: Bearer <token>` — checked first for gateway token or API key
- **User ID**: `X-GoClaw-User-Id: <user-id>` — optional external user identifier (max 255 chars)
- **Browser pairing**: `X-GoClaw-Sender-Id: <sender-id>` — identifies a previously-paired browser device
- **Locale**: `Accept-Language` — user's preferred language (en, vi, zh; default: en)
### Backward Compatibility
If no gateway token is configured (`gateway.token` is empty in `config.json`), all unauthenticated requests are granted `RoleOperator` access. This enables self-hosted deployments without strict authentication. Once a gateway token is configured, all requests must authenticate or use browser pairing.
---
## 5. Database Schema
```sql
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
name VARCHAR(100) NOT NULL,
prefix VARCHAR(8) NOT NULL, -- first 8 chars for display
key_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hex digest (unique constraint)
scopes TEXT[] NOT NULL DEFAULT '{}', -- e.g. {'operator.admin','operator.read'}
expires_at TIMESTAMPTZ, -- NULL = never expires
last_used_at TIMESTAMPTZ,
revoked BOOLEAN NOT NULL DEFAULT false,
created_by VARCHAR(255), -- user ID who created the key
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Fast lookup by hash (only active, non-revoked keys)
CREATE INDEX idx_api_keys_key_hash ON api_keys (key_hash) WHERE NOT revoked;
-- Fast lookup by prefix (for display/search in web UI)
CREATE INDEX idx_api_keys_prefix ON api_keys (prefix);
```
The partial index on `key_hash` ensures only active (non-revoked) keys are searched during authentication, keeping lookups fast regardless of how many keys have been revoked over time. The `key_hash` column has a UNIQUE constraint, so only one key can have a given hash.
---
## 6. API Endpoints
### HTTP REST
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v1/api-keys` | List all keys (masked) |
| `POST` | `/v1/api-keys` | Create key |
| `POST` | `/v1/api-keys/{id}/revoke` | Revoke key |
### WebSocket RPC
| Method | Description |
|--------|-------------|
| `api_keys.list` | List all keys (masked) |
| `api_keys.create` | Create key |
| `api_keys.revoke` | Revoke key |
All API key management operations require admin access (gateway token or API key with `operator.admin` scope).
### Create Request
```json
{
"name": "ci-deploy",
"scopes": ["operator.read", "operator.write"],
"expires_in": 2592000
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Human-readable label |
| `scopes` | string[] | Yes | Permission scopes |
| `expires_in` | int | No | TTL in seconds (omit for no expiry) |
### Create Response
```json
{
"id": "01961234-5678-7abc-def0-123456789012",
"name": "ci-deploy",
"prefix": "goclaw_a1b2c3d4",
"key": "goclaw_a1b2c3d4e5f6789012345678901234567890abcdef",
"scopes": ["operator.read", "operator.write"],
"expires_at": "2026-04-14T12:00:00Z",
"created_at": "2026-03-15T12:00:00Z"
}
```
### List Response
```json
[
{
"id": "01961234-...",
"name": "ci-deploy",
"prefix": "goclaw_a1b2c3d4",
"scopes": ["operator.read", "operator.write"],
"expires_at": "2026-04-14T12:00:00Z",
"last_used_at": "2026-03-15T14:30:00Z",
"revoked": false,
"created_by": "admin",
"created_at": "2026-03-15T12:00:00Z"
}
]
```
> Note: `key` field is absent in list responses. Only `prefix` is shown.
---
## 7. Backward Compatibility
The gateway token continues to work exactly as before. API keys are an additional authentication path — no breaking changes to existing integrations.
| Auth method | Before | After |
|------------|--------|-------|
| Gateway token | Admin access | Admin access (unchanged) |
| API key | N/A | Scoped access per key |
| Device pairing | Operator access | Operator access (unchanged) |
---
## 8. SecureCLI — CLI Credential Injection
SecureCLI is a feature that allows GoClaw to automatically inject credentials into CLI tools (e.g., `gh`, `gcloud`, `aws`) without requiring the agent to handle plaintext secrets. Credentials are stored encrypted at rest and injected at process startup.
### Use Case
When an agent needs to run `gh auth`, `gcloud auth`, or other authenticated CLI commands, the admin can configure a SecureCLI binary with encrypted environment variables. The agent never sees the raw credentials — they are injected directly into the child process environment via Direct Exec Mode.
### Database Schema
```sql
CREATE TABLE secure_cli_binaries (
id UUID PRIMARY KEY,
binary_name TEXT NOT NULL, -- "gh", "gcloud", "aws", etc.
binary_path TEXT, -- optional absolute path (auto-resolved if null)
description TEXT,
encrypted_env BYTEA NOT NULL, -- AES-256-GCM encrypted JSON
deny_args JSONB DEFAULT '[]', -- regex patterns to block subcommands
deny_verbose JSONB DEFAULT '[]', -- verbose flag patterns
timeout_seconds INTEGER DEFAULT 30,
tips TEXT, -- injected into TOOLS.md context for agents
agent_id UUID, -- null = global (all agents), else agent-specific
enabled BOOLEAN DEFAULT true,
created_by TEXT,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
```
### HTTP REST API
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v1/cli-credentials` | List all SecureCLI configurations |
| `POST` | `/v1/cli-credentials` | Create a new SecureCLI credential config |
| `GET` | `/v1/cli-credentials/{id}` | Get a specific SecureCLI config |
| `PUT` | `/v1/cli-credentials/{id}` | Update a SecureCLI config |
| `DELETE` | `/v1/cli-credentials/{id}` | Delete a SecureCLI config |
| `POST` | `/v1/cli-credentials/{id}/test` | Dry-run test (requires admin) |
| `GET` | `/v1/cli-credentials/presets` | List preset templates for common CLIs |
### Features
- **Agent-specific or global:** Configs can be scoped to a single agent or shared across all agents (agent_id = null)
- **Encryption:** Environment variables are encrypted with AES-256-GCM (same as other secrets)
- **Deny patterns:** Regex patterns to block dangerous subcommands (e.g., prevent `gh auth logout`)
- **Timeout:** Per-config timeout override for long-running CLI operations
- **Tips:** Admin-provided hints injected into agent TOOLS.md context to guide which CLI commands are safe
### Example Configuration
```json
{
"binary_name": "gh",
"binary_path": "/usr/local/bin/gh",
"description": "GitHub CLI for PRs and issues",
"encrypted_env": {
"GH_TOKEN": "ghp_xxxxxxxxxxxx"
},
"deny_args": ["auth", "logout"],
"deny_verbose": ["--verbose", "-v"],
"timeout_seconds": 60,
"tips": "Use 'gh pr list' to search PRs, 'gh issue create' to open issues. Avoid 'auth' and 'logout' commands.",
"agent_id": null
}
```
---
## 9. Web UI
The API Keys management page is accessible from the sidebar under **System > API Keys** (admin only).
Features:
- **Create dialog:** Name, scope checkboxes, optional expiry (1 day, 7 days, 30 days, 90 days, never)
- **Show-once dialog:** Displays the raw key with copy button after creation
- **List view:** Searchable, paginated table with name, prefix, scopes, expiry, last used, status
- **Revoke:** Confirmation dialog before revoking a key
---
## 10. Usage Examples
### cURL with API Key
```bash
# List agents (read scope required)
curl -H "Authorization: Bearer goclaw_a1b2c3d4..." \
http://localhost:9090/v1/agents
# Send chat message (write scope required)
curl -X POST -H "Authorization: Bearer goclaw_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{"model":"goclaw:my-agent","messages":[{"role":"user","content":"Hello"}]}' \
http://localhost:9090/v1/chat/completions
```
### WebSocket with API Key
```json
{"id": 1, "method": "connect", "params": {
"token": "goclaw_a1b2c3d4e5f6...",
"user_id": "ci-bot"
}}
```
### Create Key via API
```bash
curl -X POST -H "Authorization: Bearer gateway-admin-token" \
-H "Content-Type: application/json" \
-d '{"name":"ci","scopes":["operator.read","operator.write"]}' \
http://localhost:9090/v1/api-keys
```
---
## 11. File Reference
| File | Purpose |
|------|---------|
| `internal/crypto/apikey.go` | Key generation + SHA-256 hashing |
| `internal/store/api_key_store.go` | Store interface + `APIKeyData` struct |
| `internal/store/secure_cli_store.go` | SecureCLI store interface + `SecureCLIBinary` struct |
| `internal/store/pg/api_keys.go` | PostgreSQL API key implementation |
| `internal/store/pg/secure_cli.go` | PostgreSQL SecureCLI implementation |
| `internal/http/api_keys.go` | HTTP API handler for API keys |
| `internal/http/secure_cli.go` | HTTP API handler for SecureCLI |
| `internal/http/auth.go` | HTTP auth middleware (resolveAPIKey, tokenMatch) |
| `internal/http/api_key_cache.go` | In-memory API key cache with TTL + pubsub invalidation |
| `internal/gateway/router.go` | WebSocket connect auth (API key path) |
| `internal/gateway/methods/api_keys.go` | WebSocket RPC methods for API keys |
| `internal/permissions/policy.go` | RBAC policy engine + role derivation + scope validation |
| `migrations/000020_secure_cli_and_api_keys.up.sql` | Database migration (api_keys + secure_cli_binaries) |
| `ui/web/src/pages/api-keys/` | Web UI components |