Documents the secrets sync cross-node protocol in architecture.md and secrets.md, with a reliability backstop test covering the cross-node sync behavior. Fusion-Task-Id: FN-4882
173 lines
8.5 KiB
Markdown
173 lines
8.5 KiB
Markdown
# Secrets
|
|
|
|
[← Docs index](./README.md)
|
|
|
|
## Overview
|
|
|
|
Fusion's secrets subsystem provides encrypted-at-rest secret storage with project scope (`.fusion/fusion.db`) and global scope (`~/.fusion/fusion-central.db`).
|
|
|
|
Current shipped behavior in this branch includes:
|
|
|
|
- AES-256-GCM encryption primitives (`packages/core/src/secrets-crypto.ts`)
|
|
- CRUD + reveal APIs via `SecretsStore` (`packages/core/src/secrets-store.ts`)
|
|
- Per-secret access policy metadata (`auto` / `prompt` / `deny`)
|
|
- Schema-backed read metadata (`last_read_at`, `last_read_by`)
|
|
|
|
Threat-model baseline:
|
|
|
|
- Secret plaintext is **not** stored in SQLite.
|
|
- Ciphertext + nonce are persisted; plaintext exists only in process memory during create/reveal.
|
|
- Secret values must never be logged.
|
|
|
|
See also: [Storage](./storage.md), [Multi-project](./multi-project.md), [Architecture](./architecture.md), [Settings reference](./settings-reference.md).
|
|
|
|
## Architecture
|
|
|
|
Fusion stores secrets in two SQLite tables:
|
|
|
|
- Project scope: `secrets` in `.fusion/fusion.db`
|
|
- Global scope: `secrets_global` in `~/.fusion/fusion-central.db`
|
|
|
|
Both tables share the same column contract:
|
|
|
|
| Column | Type | Notes |
|
|
|---|---|---|
|
|
| `id` | `TEXT` | Primary key UUID. |
|
|
| `key` | `TEXT` | Unique secret key (`idxSecretsKey` / `idxSecretsGlobalKey`). |
|
|
| `value_ciphertext` | `BLOB` | AES-GCM ciphertext payload (includes auth tag). |
|
|
| `nonce` | `BLOB` | Per-row random nonce. |
|
|
| `description` | `TEXT` | Optional metadata. |
|
|
| `access_policy` | `TEXT` | `CHECK` constrained to `auto`, `prompt`, `deny`. |
|
|
| `env_exportable` | `INTEGER` | `0/1` flag for env-materialization intent metadata. |
|
|
| `env_export_key` | `TEXT` | Optional env variable key metadata. |
|
|
| `created_at` | `TEXT` | ISO timestamp. |
|
|
| `updated_at` | `TEXT` | ISO timestamp. |
|
|
| `last_read_at` | `TEXT` | Last reveal timestamp. |
|
|
| `last_read_by` | `TEXT` | Agent/user identifier recorded on reveal. |
|
|
|
|
For broader database inventory, see [docs/storage.md](./storage.md).
|
|
|
|
## Encryption
|
|
|
|
Secret crypto uses AES-256-GCM with:
|
|
|
|
- 32-byte master key
|
|
- 12-byte random nonce per encrypt operation
|
|
- 16-byte auth tag appended to ciphertext
|
|
|
|
Implementation reference: `packages/core/src/secrets-crypto.ts`.
|
|
|
|
## Master Key Resolution
|
|
|
|
The current implementation exposes a `MasterKeyProvider` abstraction consumed by `createSecretCipher` / `SecretsStore`.
|
|
|
|
- Required contract: async provider that returns a **32-byte** key.
|
|
- Validation failures return non-sensitive `SecretCryptoError` codes.
|
|
|
|
Runtime keychain/filesystem resolution is wired via `MasterKeyProvider` and consumed by `SecretsStore`; rotation UX remains follow-up work.
|
|
|
|
## Access Policies
|
|
|
|
Per-secret policy values are:
|
|
|
|
- `auto`
|
|
- `prompt`
|
|
- `deny`
|
|
|
|
Resolution helper (`resolveSecretAccessPolicy`) uses:
|
|
|
|
1. Row-level secret policy (if set)
|
|
2. Global settings default `secretsAccessPolicy` (if set)
|
|
3. Fallback: `prompt`
|
|
|
|
Implementation references:
|
|
|
|
- `packages/core/src/secret-access-policy.ts`
|
|
- `packages/core/src/types.ts` (`GlobalSettings.secretsAccessPolicy`)
|
|
|
|
Approval integration is active through `fn_secret_get` policy handling (`packages/cli/src/extension.ts:1581-1611`) and approvals lifecycle APIs.
|
|
|
|
## Dashboard CRUD
|
|
|
|
Secret persistence primitives are shipped at the store/API layer; a dedicated end-user `SecretsView` remains separate product work.
|
|
|
|
## Agent Access (`fn_secret_get`)
|
|
|
|
`fn_secret_get` is shipped in `packages/cli/src/extension.ts:1542-1629`.
|
|
|
|
Tool contract:
|
|
- Params: `key` (required), `scope` (`project` or `global`, optional).
|
|
- Resolution: key lookup in requested scope; missing key returns not-found result without plaintext.
|
|
- Policy outcomes:
|
|
- `auto` → reveals and returns plaintext value (`secret:read` audit at `extension.ts:1615`).
|
|
- `prompt` → creates approval request and returns `details.outcome: "pending_approval"` (`extension.ts:1607-1611`).
|
|
- `deny` → immediate refusal and `secret:approval-denied` audit (`extension.ts:1581-1583`).
|
|
|
|
## `.env` Auto-write into Worktrees
|
|
|
|
Fusion can materialize env-exportable secrets into each acquired task worktree when project settings enable it (`secretsEnv.enabled=true`).
|
|
|
|
- Supported settings: `enabled`, `filename` (default `.env`, validated as local filename only), `overwritePolicy` (`skip`/`merge`/`replace`), `keyPrefix`, `requireGitignored` (default `true`).
|
|
- Safety guard: when `requireGitignored` is enabled, Fusion runs `git check-ignore -- <filename>` and refuses writes unless the file is ignored.
|
|
- Write contract: managed content is canonicalized and written atomically with mode `0o600`; audit metadata includes keys and counts, never values.
|
|
- Fingerprint sidecar: successful writes persist `.fusion-secrets-env.fingerprint` containing `<sha256>\n<filename>\n` (mode `0o600`) so teardown can verify file integrity before deletion.
|
|
- Teardown cleanup: when a worktree is removed, Fusion deletes the managed env file only when the on-disk fingerprint still matches; edited files are preserved and only the sidecar is removed.
|
|
|
|
Settings shape is project-scoped in `ProjectSettings` (`packages/core/src/types.ts:2599-2609`): `secretsEnv` (env materialization config) and `secretsSyncPassphrase` (ciphertext already wrapped under local master key by caller; see `types.ts:2602-2604`).
|
|
|
|
## Cross-node Sync
|
|
|
|
Fusion now exposes four secrets sync endpoints:
|
|
|
|
- `POST /api/nodes/:id/secrets/push` — wraps local secrets into a passphrase-protected envelope and sends it to a remote node.
|
|
- `POST /api/nodes/:id/secrets/pull` — fetches a remote envelope from `GET /api/secrets/sync-export` and applies it locally.
|
|
- `POST /api/secrets/sync-receive` — inbound apply endpoint (Bearer `apiKey` required).
|
|
- `GET /api/secrets/sync-export` — inbound export endpoint (Bearer `apiKey` required).
|
|
|
|
Envelope format is `WrappedSecretsBundle` from `packages/core/src/secrets-sync.ts:33-38`: `{ version, ciphertext, salt, nonce, kdf, kdfParams }` plus transport metadata (`sourceNodeId`, `exportedAt`). Wrapping uses scrypt (`N=32768, r=8, p=1, keyLen=32`, `secrets-sync.ts:17-22`) and AES-256-GCM with base64 `ciphertext`/`salt`/`nonce` (`secrets-sync.ts:68-78`).
|
|
|
|
Sync passphrase storage is local-only: reserved key `__sync_passphrase__` in `secrets_global` with `access_policy="deny"` and `env_exportable=false`, encrypted under the local master key. The passphrase is never transmitted and never returned by HTTP endpoints.
|
|
|
|
Error mapping:
|
|
|
|
- `SecretsSyncError` codes (`wrong-passphrase`, `version-mismatch`, `malformed`) return HTTP `400` with `{ "error": <code> }`.
|
|
- Missing passphrase returns HTTP `400` with `{ "error": "passphrase-not-configured" }`.
|
|
- Bearer auth failures return HTTP `401`.
|
|
|
|
Inbound auth contract is enforced in route code (`packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts:99-114`, `:181-196`): missing/invalid Bearer `Authorization` or mismatched local `apiKey` returns 401.
|
|
|
|
Audit payloads exclude plaintext values, passphrases, and envelope crypto material (`ciphertext`, `salt`, `nonce`).
|
|
|
|
## Audit Events
|
|
|
|
Filesystem-domain secret audit taxonomy:
|
|
|
|
- `secret:read`
|
|
- `secret:create`
|
|
- `secret:update`
|
|
- `secret:delete`
|
|
- `secret:approval-requested`
|
|
- `secret:approval-granted`
|
|
- `secret:approval-denied`
|
|
- `secret:sync-push`
|
|
- `secret:sync-pull`
|
|
- `secret:env-write`
|
|
- `secret:env-write-skipped`
|
|
- `secret:env-cleanup`
|
|
- `secret:env-cleanup-skipped`
|
|
|
|
All listed events are enumerated in `packages/engine/src/run-audit.ts:261-274` (union at `run-audit.ts:325`). Route/tool emitters include: `secret:sync-push` (`packages/dashboard/src/routes/register-secrets-sync-routes.ts:92`), `secret:sync-pull` (`register-secrets-sync-routes.ts:180`, `register-secrets-sync-inbound-routes.ts:158-164`), `secret:read` + approval events (`packages/cli/src/extension.ts:1581-1615`), env materialization/cleanup (`packages/engine/src/secrets-env-writer.ts:99-217`).
|
|
|
|
Track follow-up: **FN-5031** (missing `packages/core/src/__tests__/secrets-env.test.ts` contract file).
|
|
Track follow-up: **FN-5032** (`docs/settings-reference.md` still marks shipped secrets settings as planned in some rows).
|
|
|
|
**Plaintext prohibition:** audit payload metadata must never include plaintext, decrypted values, ciphertext, or nonce fields. Use `assertNoSecretPlaintext(...)` as the canonical enforcement helper before emitting secret audit events.
|
|
|
|
## Operational Notes
|
|
|
|
- Backups: preserve both SQLite data and master-key material/provider source used by deployment.
|
|
- If master key material is lost, encrypted secret values become unrecoverable.
|
|
- Pending advanced capabilities:
|
|
- Full rotation UX and key lifecycle tooling
|
|
- TTL/rotation automation, env-set profiles, KMS/Vault backends, per-node asymmetric sync
|