Files
Dotta 778e775c35 Add secrets provider vaults and remote import (#5429)
## Thinking Path

> - Paperclip orchestrates AI-agent companies and needs secrets handling
to work across local development, hosted operators, and governed agent
execution.
> - The affected subsystem is the company-scoped secrets control plane:
database schema, server services/routes, CLI workflows, and the Secrets
settings UI.
> - The gap was that secrets were local-only and operators could not
manage provider vaults or import existing remote references without
exposing plaintext.
> - This branch adds provider vault configuration plus an AWS Secrets
Manager remote-import path while preserving company boundaries, binding
context, and audit trails.
> - I kept the PR to a single branch PR, removed unrelated
lockfile/package drift, rebased the full branch onto the current
`public-gh/master`, and addressed fresh Greptile findings.
> - The benefit is a reviewable implementation of provider-backed
secrets with focused tests covering provider selection, import
conflicts, deleted secret reuse, rotation guards, and AWS signing
behavior.

## What Changed

- Added provider vault support for company secrets, including provider
config storage, default vault handling, health checks, binding usage,
access events, and remote import preview/commit.
- Added an AWS Secrets Manager provider using SigV4 request signing,
bounded request timeouts, namespace guardrails, cached runtime
credential resolution, and external-reference linking without plaintext
reads.
- Added Secrets UI surfaces for vault management and remote import, plus
CLI/API documentation for setup and operations.
- Stabilized routine webhook secret binding paths and SSH
environment-driver fixture bindings discovered during verification.
- Addressed Greptile and CI findings: no lockfile/package drift,
monotonic migration metadata, disabled-vault default races, soft-deleted
secret hiding/recreate behavior, remove behavior with disabled vaults,
soft-deleted external-reference re-import, non-active rotation guards,
managed-secret soft deletion through PATCH, and per-call AWS SDK
credential client churn.
- Rebased this branch onto `public-gh/master` at `0e1a5828` and
force-pushed with lease to keep this as the single PR for the branch.

## Verification

- `git fetch public-gh master`
- `git rebase public-gh/master`
- `git diff --name-only public-gh/master...HEAD | grep
'^pnpm-lock\.yaml$' || true` confirmed `pnpm-lock.yaml` is not in the PR
diff.
- Confirmed migration ordering: master ends at `0081_optimal_dormammu`;
this PR adds `0082_dry_vision` and
`0083_company_secret_provider_configs`.
- Inspected migrations for repeat safety: new tables/indexes use `IF NOT
EXISTS`; foreign keys are guarded by `DO $$ ... IF NOT EXISTS`; column
additions use `ADD COLUMN IF NOT EXISTS`.
- `pnpm -r typecheck` passed before the Greptile follow-up commits.
- `pnpm test:run` ran the full stable Vitest path before the Greptile
follow-up commits; it completed with 3 timing-related failures under
parallel load: `codex-local-execute.test.ts`,
`cursor-local-execute.test.ts`, and `environment-service.test.ts`.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/codex-local-execute.test.ts
src/__tests__/cursor-local-execute.test.ts
src/__tests__/environment-service.test.ts` passed on targeted rerun
(`24/24`).
- `pnpm build` passed before the Greptile follow-up commits. Vite
reported existing chunk-size/dynamic-import warnings.
- After Greptile follow-up commits: `pnpm --filter @paperclipai/server
exec vitest run src/__tests__/secrets-service.test.ts` passed (`26/26`).
- After Greptile follow-up commits: `pnpm --filter @paperclipai/server
exec vitest run src/__tests__/aws-secrets-manager-provider.test.ts
src/__tests__/secrets-service.test.ts` passed (`39/39`).
- After Greptile follow-up commits: `pnpm --filter @paperclipai/server
typecheck` passed.
- Captured Storybook screenshots from `ui/storybook-static` for visual
review.
- Latest PR checks on `5ca3a5cf`: `policy`, serialized server suites
1/4-4/4, `Canary Dry Run`, `e2e`, `security/snyk`, and `Greptile Review`
pass; aggregate `verify` is still registering the completed child
checks.
- Greptile review loop continued through the latest requested pass; all
Greptile review threads are resolved and the latest `Greptile Review`
check on `5ca3a5cf` passed with 0 comments added.

## Screenshots

Before: the provider-vault and remote-import surfaces did not exist on
`master`; these are after-state screenshots from the Storybook fixtures.

![Secrets
inventory](https://raw.githubusercontent.com/paperclipai/paperclip/PAP-2339-secrets-make-a-plan/doc/pr/5429/secrets-inventory.png)

![Secret binding
picker](https://raw.githubusercontent.com/paperclipai/paperclip/PAP-2339-secrets-make-a-plan/doc/pr/5429/secret-binding-picker.png)

![Environment editor with
secrets](https://raw.githubusercontent.com/paperclipai/paperclip/PAP-2339-secrets-make-a-plan/doc/pr/5429/env-editor-with-secrets.png)

## Risks

- Migration risk: this adds new secret provider tables and extends
existing secret rows. The migrations were checked for monotonic ordering
and idempotent guards, but reviewers should still inspect upgrade
behavior carefully.
- Provider risk: AWS support uses direct SigV4 requests. Automated tests
cover signing, request timeouts, vault-config selection, namespace
guardrails, pending-version archival, sanitized provider errors, and
service-level cleanup paths. A real-vault AWS smoke test remains
deployment validation for an operator with AWS credentials rather than
an unverified merge blocker in this local branch.
- UI risk: the Secrets page and import dialog are large new surfaces;
screenshots are included above for reviewer inspection.
- Verification risk: the full local stable test command hit
parallel-load timing failures, although the exact failed files passed
when rerun directly.
- Operational risk: remote import intentionally avoids plaintext reads;
operators must understand that imported external references resolve at
runtime and may fail if AWS permissions change.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5 coding agent with local shell/tool use in the
Paperclip worktree. Exact context-window size was not exposed by the
runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 18:22:17 -05:00

402 lines
18 KiB
Markdown

---
title: Secrets Management
summary: Master key, encryption, and strict mode
---
Paperclip encrypts secrets at rest using a local master key. Agent environment variables that contain sensitive values (API keys, tokens) are stored as encrypted secret references.
## Custody Boundaries
Paperclip protects secret values up to the moment they are handed to an agent
or workload:
- Storage: values are encrypted at rest by the active provider. The local
provider keeps them encrypted with a key that never leaves the host.
- Transport: values are decrypted server-side and injected into the agent
process environment, SSH command env, sandbox driver, or HTTP request
immediately before the call. Paperclip does not return decrypted values to
the board UI.
- Audit: each resolution records a non-sensitive event (secret id, version,
provider id, consumer, outcome) without the value or provider credentials.
Once a value reaches the consuming process, Paperclip can no longer guarantee
secrecy. The agent (or sandbox, or remote host) can read the value, write it to
its own logs or transcript, or pass it to downstream tools. Treat any secret
you bind to an agent as exposed to that agent. Limit blast radius with bindings
(only bind what each agent needs), short-lived provider credentials where the
provider supports them, and rotation when an agent transcript or downstream
system might have captured a value.
## Using Secrets In Runs
Creating a company secret does not automatically create an environment variable.
You use a secret by binding it into an agent, project, environment, or plugin
configuration field that supports secret references.
For agent and project environment variables:
1. Create or link the secret in `Company Settings > Secrets`.
2. Open the agent's `Environment variables` field, or the project's `Env`
field.
3. Add the environment variable key the process expects, such as `GH_TOKEN` or
`OPENAI_API_KEY`.
4. Set the row source to `Secret`, select the stored secret, and choose either
`latest` or a pinned version.
At runtime, Paperclip resolves the selected secret server-side and injects the
resolved value under the env key from the binding row. The stored secret name
can be human-readable; the binding key is what the agent process receives.
Project env applies to every issue run in that project. When a project env key
matches an agent env key, the project value wins before Paperclip injects its
own `PAPERCLIP_*` runtime variables.
## Default Provider: `local_encrypted`
Secrets are encrypted with a local master key stored at:
```
~/.paperclip/instances/default/secrets/master.key
```
This key is auto-created during onboarding. The key never leaves your machine.
Paperclip best-effort enforces `0600` permissions when it creates or loads the
key file. `paperclipai doctor` and the provider health API warn when the file is
readable by group or other users.
Back up the key file together with database backups. A database backup without
the key cannot decrypt local secrets, and a key backup without the database
metadata is not enough to restore named secret versions.
## Configuration
### CLI Setup
Onboarding writes default secrets config:
```sh
pnpm paperclipai onboard
```
Update secrets settings:
```sh
pnpm paperclipai configure --section secrets
```
Validate secrets config:
```sh
pnpm paperclipai doctor
pnpm paperclipai secrets doctor --company-id <company-id>
```
### Environment Overrides
| Variable | Description |
|----------|-------------|
| `PAPERCLIP_SECRETS_MASTER_KEY` | 32-byte key as base64, hex, or raw string |
| `PAPERCLIP_SECRETS_MASTER_KEY_FILE` | Custom key file path |
| `PAPERCLIP_SECRETS_STRICT_MODE` | Set to `true` to enforce secret refs |
## Strict Mode
When strict mode is enabled, sensitive env keys (matching `*_API_KEY`, `*_TOKEN`, `*_SECRET`) must use secret references instead of inline plain values.
```sh
PAPERCLIP_SECRETS_STRICT_MODE=true
```
Recommended for any deployment beyond local trusted.
Authenticated deployments default strict mode on unless explicitly overridden by
configuration or `PAPERCLIP_SECRETS_STRICT_MODE=false`.
## External References
Provider-owned secrets can be linked without copying values into Paperclip by
using `managedMode: "external_reference"` plus a provider `externalRef`.
Paperclip stores metadata and a non-sensitive fingerprint, never the value.
Runtime resolution remains server-side and binding-enforced.
The built-in AWS, GCP, and Vault provider IDs currently accept external
reference metadata, but runtime resolution requires provider configuration in the
deployment. Their provider health check reports this as a warning until
configured.
For hosted Paperclip Cloud on AWS, see the AWS Secrets Manager operational
contract — required env vars, IAM/KMS scoping, naming and tag conventions, and
backup/rotation/incident runbooks — in `doc/SECRETS-AWS-PROVIDER.md`.
## Provider Vaults
A *provider vault* is a named, company-scoped configuration that points secret
material at one of the supported provider backends. Each company can configure
multiple vaults, including more than one vault per provider family, and pick a
default vault per family for new secret operations. Existing secrets created
before any vault was configured continue to resolve through the deployment-level
default provider — no migration is required.
### Where to configure
Open `Company Settings → Secrets` in the board UI and switch to the
`Provider vaults` tab. From there you can:
- Create a vault for any supported provider family.
- Edit the non-secret config of an existing vault.
- Set one ready vault per provider family as the company default.
- Disable a vault (a soft delete that keeps audit history).
- Run a health check against a vault and read the latest result inline.
The same operations are exposed under
`/api/companies/{companyId}/secret-provider-configs` for automation. See the
[secrets API reference](/api/secrets#provider-vaults) for the full route table.
### Custody Of Provider Credentials
Provider vaults intentionally store only **non-sensitive** configuration:
region, project id, namespace, prefix, KMS key id, mount path, address, and
similar routing metadata. The API, UI, and activity log never accept, return,
or display provider credential values. Submitting fields with names like
`accessKeyId`, `secretAccessKey`, `token`, `password`, `serviceAccountJson`,
`privateKey`, `keyFile`, `unsealKey`, or any common credential alias is rejected
at validation time.
That keeps the bootstrap rule from the AWS provider applicable to every
provider family: **provider credentials live in deployment infrastructure
identity, not in Paperclip company secrets**. Allowed credential sources are
workload identity attached to the Paperclip server (instance profile, IRSA, ECS
task role), `AWS_PROFILE` / SSO / shared config for local runs, an orchestrator
secret store that boots the server, or short-lived shell credentials for local
development. Do not paste long-lived API keys into the vault config.
### Vault Status
Each vault carries a status that drives what the runtime can do with it:
| Status | Meaning |
|---------------|-----------------------------------------------------------------------------------------------|
| `ready` | Selectable for create/rotate/resolve. Eligible to be the default. |
| `warning` | Saved config exists but health needs attention (for example missing AWS env). Still selectable. |
| `coming_soon` | Visible and editable as draft metadata, but locked out of all runtime operations. |
| `disabled` | Soft-deleted. Hidden from the secret create/rotate flow. |
`gcp_secret_manager` and `vault` are pinned to `coming_soon` until their
runtime modules ship. The settings UI lets you save draft configuration for
those providers (and surfaces them on the vault list), but secret create,
rotate, and resolve calls that target a coming-soon vault fail with a clear
runtime-locked error.
### Default Vault Behavior
A company can mark **one** ready (or warning) vault per provider family as the
default. The secret create and rotate dialogs preselect the default vault for
the chosen provider so operators don't have to remember which vault to pick.
Coming-soon and disabled vaults cannot be marked default; attempting to do so
returns a validation error. Setting a new default automatically clears the
previous default for that provider.
If a secret is created without any `providerConfigId` (no vaults exist yet, or
the operator clears the selector), runtime resolution falls back to the
deployment-level provider configuration — the same path existing installs use.
This keeps secrets created before any provider vault was configured working
without migration. Picking the default in the UI is an explicit selection, not
a runtime fallback: the create call still sends an explicit `providerConfigId`.
### Multiple Vaults Per Provider
Multiple vaults from the same provider family are first-class. Common patterns:
- Two AWS vaults pointing at different regions or KMS keys for environment
separation.
- A staging Vault address alongside a production address.
- A dedicated GCP project for a single product line while the rest of the
company uses another.
Each vault has its own display name, status, default flag, and health record.
Operators choose the vault explicitly when creating or rotating a secret; the
default vault is preselected to avoid accidental routing to the wrong account.
### Per-Vault Health Checks
`POST /api/secret-provider-configs/{id}/health` runs a provider-specific health
probe and stores the result on the vault row. The settings UI exposes the same
action and renders the result inline. Health responses include a status,
operator-facing message, and structured guidance (such as missing env var
names, expected credential sources, and backup reminders). They never include
provider credentials or secret values. Coming-soon vaults always return a
`runtime_locked` health code and never call into provider modules.
### Provider-Specific Notes
**Local encrypted vaults** wrap the existing `local_encrypted` provider. The
master key path and rotation guidance described above still applies. A local
vault config is mostly bookkeeping plus an explicit acknowledgement that the
key file is backed up alongside the database.
**AWS Secrets Manager vaults** read the per-vault `region`, `namespace`,
`secretNamePrefix`, `kmsKeyId`, `ownerTag`, and `environmentTag` to route
managed writes and external-reference reads. The vault config supplements (and
can override) the deployment-level `PAPERCLIP_SECRETS_AWS_*` env. Bootstrap
credentials still come from the AWS SDK default credential chain — see
`doc/SECRETS-AWS-PROVIDER.md` for the full IAM and KMS contract.
**GCP Secret Manager** and **HashiCorp Vault** vaults are coming soon. You can
save draft `projectId`, `location`, `namespace`, `address`, and `mountPath`
metadata so the company is ready to flip them on when the provider modules
ship. Vault `address` values must be origin-only `http(s)://host[:port]` URLs;
addresses with embedded credentials, paths, query strings, or fragments are
rejected.
### Remote Import From AWS Vaults
AWS provider vaults can import existing AWS Secrets Manager entries as
Paperclip `external_reference` secrets. This is a metadata-only link: Paperclip
stores the AWS ARN/path, a fingerprint/version reference, and binding metadata.
It does not read, copy, store, log, or display the remote plaintext secret
value during preview or import.
Operator flow in the board UI:
1. Open `Company Settings -> Secrets`.
2. Confirm at least one AWS provider vault is `ready` or `warning`.
3. In the `Secrets` tab, choose `Import from vault`.
4. Select an AWS vault, search the remote inventory, and load more pages as
needed.
5. Check the rows to import, review/edit the Paperclip name and key, then
submit.
6. Review the result summary for created, skipped, and failed rows.
The preview list is intentionally paged and search-first. AWS accounts can have
large per-Region inventories, and `ListSecrets` returns opaque `NextToken`
cursors. Do not expect Paperclip to crawl a whole account in the background;
load pages deliberately and retry throttled requests with backoff.
Remote import exposes AWS secret metadata visible to the Paperclip runtime
role, including names/ARNs and safe derived fields such as dates, whether a
description or KMS key exists, and tag count. Treat names, ARNs, tags, and
search text as operational metadata that may be sensitive. The API and activity
log must not store raw descriptions, tags, plaintext values, provider
credentials, or raw AWS error blobs.
Required AWS posture:
- Preview needs optional `secretsmanager:ListSecrets` permission on
`Resource: "*"`. AWS does not support constraining `ListSecrets` to
individual secret ARNs or tags as an IAM boundary.
- Preview/import must not call `secretsmanager:GetSecretValue`,
`secretsmanager:BatchGetSecretValue`, or KMS decrypt.
- Runtime resolution of an imported reference still needs
`secretsmanager:GetSecretValue` on the selected external ARN/path and KMS
decrypt when that secret uses a customer-managed key.
- Keep managed create/rotate/delete permissions scoped to the Paperclip
deployment prefix. Do not broaden managed write/delete permissions just
because import inventory is enabled.
Safe scoping comes from deployment posture rather than AWS list filtering:
dedicated Paperclip runtime roles per environment/account, AWS vaults pointed at
the intended account and Region, import-enabled roles only where inventory
exposure is acceptable, and board-only access to the import routes. Tags and
name filters are search aids, not a permission model.
If import preview fails:
- `AccessDenied` or `not authorized`: the runtime role is missing
`secretsmanager:ListSecrets`; add the optional inventory statement only if
remote import should be enabled for that vault.
- Throttling: retry after a short delay and narrow the search before loading
more pages.
- Invalid cursor: refresh the preview; AWS `NextToken` values are opaque and
can expire or become stale.
- Runtime resolution failure after import: verify `GetSecretValue` and KMS
decrypt scope for the selected external secret. Being visible in inventory is
not proof that the runtime role can read the value.
### Backup And Restore
Each provider family has a different backup story:
- `local_encrypted`: back up the local master key file and the Paperclip
database together. Either alone is not enough to restore the encrypted
values, and the vault row only records the path and acknowledgement, not the
key bytes.
- `aws_secrets_manager`: back up Paperclip's database for vault metadata
(vault id, region, prefix, KMS key id, default flag, bindings, version
pointers). The actual secret values live in AWS Secrets Manager under the
configured prefix; restore by pointing the same Paperclip company at the
same AWS namespace and confirming the runtime role still has
`GetSecretValue` plus KMS decrypt. The full restore checklist lives in
`doc/SECRETS-AWS-PROVIDER.md`.
- `gcp_secret_manager` and `vault`: while these are coming soon, only the
draft vault config exists in Paperclip. Database backups capture it. There
is nothing to restore on the provider side until runtime support lands.
### AWS Provider Bootstrap Boundary
The AWS Secrets Manager provider cannot bootstrap itself from Paperclip
`company_secrets`. Its initial AWS access must be present before the server can
create or resolve AWS-backed company secrets, regardless of whether you use the
deployment-level default or a per-company vault.
For Paperclip Cloud, provision the server runtime IAM role/workload identity,
KMS key, deployment prefix, and non-secret `PAPERCLIP_SECRETS_AWS_*` environment
configuration before enabling AWS-backed secrets in the board UI. For
self-hosted and local runs, use the AWS SDK default credential chain: instance
profile, ECS task role, EKS IRSA/OIDC web identity, AWS SSO/shared config via
`AWS_PROFILE`, or short-lived shell credentials for local development.
Do not store AWS root credentials or long-lived IAM user access keys in
Paperclip secrets. Bootstrap material belongs in infrastructure IAM/workload
identity, the process environment, an AWS profile, or the orchestrator secret
store.
## Migrating Inline Secrets
If you have existing agents with inline API keys in their config, migrate them to encrypted secret refs:
```sh
pnpm paperclipai secrets migrate-inline-env --company-id <company-id>
pnpm paperclipai secrets migrate-inline-env --company-id <company-id> --apply
# low-level script for direct database maintenance
pnpm secrets:migrate-inline-env # dry run
pnpm secrets:migrate-inline-env --apply # apply migration
```
Use the CLI command for normal operations because it goes through the Paperclip
API, creates or rotates secret records, and updates agent env bindings with
audit logging.
## Portable Declarations
Company exports include only environment declarations. They do not include
secret IDs, provider references, encrypted material, or plaintext values.
```sh
pnpm paperclipai secrets declarations --company-id <company-id> --kind secret
```
Before importing a package into another instance, use those declarations to
create local values or link hosted provider references in the target deployment.
For hosted providers such as AWS Secrets Manager, the hosted provider remains
the value custodian; Paperclip stores metadata and provider version references,
not provider credentials or plaintext secret values.
## Secret References in Agent Config
Agent environment variables use secret references:
```json
{
"env": {
"ANTHROPIC_API_KEY": {
"type": "secret_ref",
"secretId": "8f884973-c29b-44e4-8ea3-6413437f8081",
"version": "latest"
}
}
}
```
The server resolves and decrypts these at runtime, injecting the real value into the agent process environment.