--- title: "Secrets" description: "Encrypted storage for API keys and sensitive values." order: 7 section: "Core Concepts" --- # Secrets Management Encrypted storage for API keys and sensitive values. Agents can *use* secrets without being able to read or expose them. The capability is owned by the bundled `signet.secrets` core plugin. --- ## How It Works The core problem with AI agents and secrets: if an agent can read `OPENAI_API_KEY` from the environment, a prompt injection attack or a careless response could leak it. Signet's solution: secrets are encrypted at rest, and agents never receive decrypted values. When a secret is needed (for embeddings, tool calls, etc.), the [[daemon]] resolves it internally, either by using the value directly in API calls or by injecting it into a subprocess environment that the agent cannot inspect. `signet.secrets` is the privileged core plugin for the capability. In V1 it registers plugin diagnostics, surface metadata, and a local provider that adopts the existing encrypted store in place. Secret operations emit structured daemon diagnostics such as `secret.listed`, `secret.stored`, `secret.deleted`, `secret.resolved_for_exec`, `secret.exec_started`, and `secret.exec_completed`; payloads include names or counts where current API policy already exposes them, but never raw values. Plugin lifecycle, capability-denial, and secret-provider events are also appended to `$SIGNET_WORKSPACE/.daemon/plugins/audit-v1.ndjson` with sensitive fields redacted before storage. Secrets API routes are guarded by the plugin registry. If `signet.secrets` is disabled, blocked, or missing the required grant for a route, the daemon returns a structured plugin-capability error. Stored secret files are left in place. ### Setup defaults Existing installs default to Signet Secrets enabled, even if they do not yet have a plugin registry file. New interactive installs include a **Core plugins** step that explains what Signet Secrets stores, how it connects to Signet's encrypted local store and compatible 1Password references, which value-safe surfaces it enables, and why that is safer than putting credentials in prompts, shell history, logs, or source files. For non-interactive setup, Signet Secrets is enabled by default. Use `signet setup --disable-signet-secrets` to leave the bundled plugin installed but disabled. Disabling the plugin blocks secret API routes and tool surfaces without deleting existing encrypted secret files. --- ## Storage ``` $SIGNET_WORKSPACE/ └── .secrets/ └── secrets.enc # Encrypted secret store (JSON, mode 0600) ``` The `secrets.enc` file is JSON, but every value is encrypted individually. It's readable by humans as a list of names and metadata, but the actual values are ciphertext. The local provider keeps this file format unchanged. Existing `$SIGNET_WORKSPACE/.secrets/secrets.enc` files remain valid without migration, re-encryption, relocation, or user action. Bare secret names are compatibility aliases for local references: ```text OPENAI_API_KEY == local://OPENAI_API_KEY ``` ### Encryption - **Algorithm:** XSalsa20-Poly1305 via libsodium (`crypto_secretbox_easy`) - **Key derivation:** BLAKE2b hash of `signet:secrets:` stretched to 32 bytes - **Machine ID:** reads `/etc/machine-id` (Linux) or `IOPlatformUUID` (macOS); falls back to `hostname-username` - **Nonces:** random, prepended to each ciphertext - **File permissions:** `0600` (owner read/write only) The master key is bound to the machine, so the encrypted file cannot be decrypted on another computer without the same machine ID. --- ## [[cli|CLI]] Commands ### Add a secret ```bash signet secret put OPENAI_API_KEY # Prompts: Enter value: •••••••• # ✓ Secret OPENAI_API_KEY saved ``` The value is never echoed. Prompt input is hidden. ### List secrets ```bash signet secret list # OPENAI_API_KEY # ANTHROPIC_API_KEY # GITHUB_TOKEN ``` Only names are shown — never values. ### Check if a secret exists ```bash signet secret has OPENAI_API_KEY # true ``` ### Delete a secret ```bash signet secret delete GITHUB_TOKEN # ✓ Secret GITHUB_TOKEN deleted ``` ### 1Password integration ```bash # Connect using a service account token (prompted if omitted) signet secret onepassword connect # Check status and list vaults signet secret onepassword status signet secret onepassword vaults # Import password-like fields from selected vaults signet secret onepassword import --vault Engineering --prefix OP # Disconnect and remove stored service account token signet secret onepassword disconnect ``` Imported values are stored as regular Signet secrets with generated names, and `secret_exec` can also reference 1Password secrets directly via `op://vault/item/field` when connected. ### Bitwarden provider Bitwarden is opt-in. By default, Signet continues to use the existing local encrypted `secrets.enc` store. After connecting Bitwarden, you can either keep using local Signet secrets or make Bitwarden the active provider for future `put`, `list`, `delete`, config resolution, and secret exec references. Signet uses the official Bitwarden CLI session model: log in/unlock with `bw`, then hand Signet the short-lived session token. ```bash # One-time Bitwarden CLI login outside Signet bw login # Connect but keep the local Signet store active. The session token is read # from stdin so it is not written to shell history or process argv. bw unlock --raw | signet secret bitwarden connect --session-stdin # Connect and immediately make Bitwarden the active backing store bw unlock --raw | signet secret bitwarden connect --session-stdin --activate # Switch providers later without losing either store signet secret bitwarden use bitwarden signet secret bitwarden use local # Copy existing local Signet secrets into Bitwarden signet secret bitwarden migrate # dry run signet secret bitwarden migrate --write # copy, keep local copies signet secret bitwarden migrate --write --delete-local ``` When Bitwarden is active, Signet stores new secrets as Bitwarden login items and resolves bare `$secret:NAME` references from Bitwarden first, falling back to local Signet secrets for backwards compatibility. Explicit `bw://name/NAME` and `bw://item/ITEM_ID/password` references are also accepted. Internal Signet provider credentials remain in the local encrypted store so connecting Bitwarden does not create a circular dependency. ### Export / import (planned) ```bash signet secret export > secrets.enc.backup signet secret import < secrets.enc.backup ``` --- ## Dashboard UI The [[dashboard]]'s **Settings -> Secrets** panel lets you: - View all secret names (values always masked as `•••••`) - Add new secrets via an input form - Delete secrets - Connect/disconnect a 1Password service account token - Select vaults and import password-like fields into Signet secrets There is intentionally no "reveal" button — the UI never has access to secret values. --- ## Using Secrets in Config Reference a stored secret in `agent.yaml` with the `$secret:NAME` syntax: ```yaml embedding: provider: openai model: text-embedding-3-small api_key: $secret:OPENAI_API_KEY ``` The daemon resolves `$secret:NAME` references internally when making API calls. The actual value never appears in the config file or the agent's context. --- ## Executing Commands with Secrets The daemon queues a subprocess with secrets injected into its environment. The agent provides references (names), not values. **HTTP API:** ```http POST /api/secrets/exec Content-Type: application/json { "command": "curl https://api.openai.com/v1/models", "secrets": { "OPENAI_API_KEY": "OPENAI_API_KEY", "DB_PASSWORD": "op://Engineering/Prod DB/password" } } ``` The map is `{ env_var_name: secret_reference }` where a reference can be a stored Signet secret name, a Bitwarden `bw://...` reference, or a 1Password `op://...` reference. The daemon: 1. Resolves each secret reference to its value 2. Queues the subprocess with the resolved values in the environment 3. Enforces a bounded timeout (5 minutes by default, max 30 minutes) 4. Returns HTTP `202` immediately with a job id 5. Exposes redacted stdout/stderr and exit code through `GET /api/secrets/exec/:jobId` Secret exec is always queued; there is no synchronous request mode. This keeps large jobs such as SSH/rsync transfers from tying daemon request handling to subprocess lifetime. Pass optional `"timeoutMs"` to bound the background job. The daemon also bounds the secret exec worker pool and pending queue, terminates the subprocess process group on timeout, and redacts output before truncating it. **Queued response:** ```json { "id": "uuid", "status": "queued", "createdAt": "...", "timeoutMs": 300000 } ``` When the job completes, if a secret value appears anywhere in stdout or stderr, it is replaced with `[REDACTED]`. Provider-qualified local refs are accepted wherever secret references are accepted: ```json { "OPENAI_API_KEY": "local://OPENAI_API_KEY" } ``` --- ## Security Model **What the daemon does:** - Reads the machine ID from `/etc/machine-id` or equivalent - Derives a 32-byte master key using BLAKE2b - Encrypts each secret value with a random nonce - Stores ciphertext in `$SIGNET_WORKSPACE/.secrets/secrets.enc` at `0600` **What agents can't do:** - Read secret values from config files or environment - Inspect subprocess environments - Enumerate secret values through the API (only names) - Access the `GET /api/secrets/:name` route (there is none — only `POST`, `DELETE`, and `GET /api/secrets` for names) - Retrieve raw values through SDK, MCP, dashboard, connector, or plugin diagnostics responses **What you should know:** - The master key is machine-bound, not passphrase-protected by default. If someone has shell access as your user, they can derive the key. - Passphrase-protected keys are planned for a future version. - Don't commit `$SIGNET_WORKSPACE/.secrets/` to version control. Add it to `.gitignore`. - Secrets are zeroed from memory (best-effort in JavaScript) after use. --- ## API Reference The full secrets API is documented in the [Runtime extensions API](./api/runtime-extensions.md#secrets). Summary: | Endpoint | Method | Description | |----------|--------|-------------| | `/api/secrets` | GET | List secret names | | `/api/secrets/:name` | POST | Store a secret | | `/api/secrets/:name` | DELETE | Delete a secret | | `/api/secrets/exec` | POST | Queue command with one or more secrets injected | | `/api/secrets/exec/:jobId` | GET | Inspect queued secret exec job status | | `/api/secrets/:name/exec` | POST | Legacy single-secret queued exec | | `/api/secrets/bitwarden/status` | GET | Bitwarden provider status | | `/api/secrets/bitwarden/connect` | POST | Connect/save Bitwarden CLI session | | `/api/secrets/bitwarden/connect` | DELETE | Disconnect/remove stored Bitwarden session | | `/api/secrets/bitwarden/provider` | POST | Switch active provider (`local` or `bitwarden`) | | `/api/secrets/bitwarden/folders` | GET | List Bitwarden folders | | `/api/secrets/bitwarden/migrate` | POST | Copy local Signet secrets into Bitwarden | | `/api/secrets/1password/status` | GET | 1Password integration status | | `/api/secrets/1password/connect` | POST | Connect/save service account token | | `/api/secrets/1password/connect` | DELETE | Disconnect/remove stored token | | `/api/secrets/1password/vaults` | GET | List accessible 1Password vaults | | `/api/secrets/1password/import` | POST | Import vault secrets into Signet | | `/api/plugins` | GET | List plugin registry records, including `signet.secrets` | | `/api/plugins/audit` | GET | List redacted durable plugin audit events | | `/api/plugins/signet.secrets/diagnostics` | GET | Inspect Secrets plugin diagnostics and surface metadata | --- ## Planned Improvements - **Passphrase protection** — optional user passphrase added to key derivation (Argon2) - **OS keychain backend** — macOS Keychain, GNOME Keyring, Windows Credential Manager - **Export/import** — encrypted backup bundles for moving between machines - **Team secrets** — shared encrypted secrets via asymmetric encryption - **Audit log** — log of secret usage (not values)