Skip to main content
secret-manager stores encrypted application secrets and exposes them to authenticated users and explicitly authorized plugins. It is a WASM plugin, so it uses the same plugin contract and host callbacks as other WASM plugins.

Plugin identity

The plugin name is used in the plugin configuration, as the target of plugin messages, and in the access policy for individual secrets.

Configuration

The standard plugin settings are configured under [Plugins.secret-manager]:
Params are passed to the plugin in its RegisterRequest. The plugin uses the following parameter names: The plugin persists changes to its own explicit Params section. It does not write these values to the shared KV store. The host applies a parameter patch to the current configuration and refreshes the plugin manager’s configuration snapshot; the plugin also updates its in-memory copy. Treat secretmgr.identity and all secretmgr.secret.* values as sensitive. The identity is a private age key, and the secret parameters contain encrypted secret material that should not be exposed in logs or ordinary configuration diagnostics.

Supplying the identity through the environment

The identity parameter supports the Kernel’s environment reference syntax. To keep the private key out of the configuration file, store the value in an environment variable and reference it from Params:
The Kernel resolves env://SECRET_KEY before passing Params to the plugin. The reference without a trailing ? is required: if SECRET_KEY is not set, parameter resolution fails and secret-manager cannot start. This is preferable to silently generating a new identity when encrypted data already exists. On a first installation with no secretmgr.identity parameter, the plugin generates an age X25519 identity and persists it through PatchParams. You can then move that generated identity to a protected environment variable, set "secretmgr.identity" = "env://SECRET_KEY", and start the application with SECRET_KEY present. Keep using the same identity: an identity-encrypted secret cannot be decrypted with a newly generated key. If the identity must be stored directly in the configuration, the individual secret values can still be protected with scrypt encryption and a passphrase, so the configuration contains ciphertext rather than plaintext secret values. The current plugin does not accept a passphrase-wrapped value for secretmgr.identity itself; that parameter must resolve to the raw age identity. The identity is still required to decrypt identity-encrypted secrets.

Secret metadata

Each secretmgr.meta.<name> value has this shape:
encryption is either identity or scrypt. The allowed_plugins and updated_at fields are also maintained in the metadata returned by the HTTP API. Plugin names in the policy are compared with the authenticated source name from the Kernel.

Secret names

Secret names must be 1–128 characters after trimming. They may contain ASCII letters, digits, /, _, -, and ., but may not contain ... Empty names and names containing any other character are rejected.

Encryption

The plugin stores age ciphertext encoded with standard Base64. The passphrase is never persisted. A forgotten passphrase makes an scrypt secret unrecoverable. Scrypt derives the encryption key from the passphrase with a deliberately expensive, memory-hard computation. This makes each offline password guess more costly, which is why passphrase-based encryption is slower than using the stored X25519 identity. Choose it when the additional delay is acceptable. Both the authenticated HTTP API and the plugin-to-plugin IPC interface can retrieve scrypt secrets when the caller supplies the passphrase. Without the correct secretmgr.identity, an identity-encrypted secret cannot be decrypted. The ciphertext is not sufficient to recover it. The maximum plaintext size accepted during decryption is 1 MiB.

HTTP API

All registered HTTP routes require an authenticated user. The plugin returns JSON with Content-Type: application/json; charset=utf-8 and Cache-Control: no-store.

List secrets

Response:
The list contains metadata only. It never contains secret values. Entries are sorted by secret name.

Add a secret

Request body:
passphrase is optional. An empty value selects identity encryption. A non-empty value selects scrypt encryption. allowed_plugins may be empty, which means that no plugin can retrieve the secret through IPC. Success returns HTTP 201 Created:
The endpoint returns 400 Bad Request for malformed JSON, an invalid name, or an invalid plugin policy, 409 Conflict when the secret already exists, and 500 Internal Server Error when encryption or persistence fails.

Update a secret

The request uses the same fields as /keys/add. The name identifies the existing secret and cannot be changed. To keep the existing ciphertext while changing metadata or the access policy, set value to the single character * and leave passphrase empty. Supplying a passphrase with this sentinel is rejected. Success returns HTTP 200 OK with the same { "success": true, "name": "..." } shape. An unknown secret returns 404 Not Found; malformed input or an invalid policy returns 400 Bad Request.

Reveal a secret

Request body:
The passphrase is required when the secret uses scrypt encryption and must be empty for an identity secret. Success returns the plaintext:
The endpoint returns 400 Bad Request for malformed input or a missing or invalid scrypt passphrase. A missing secret or other decryption failure is returned as 404 Not Found.

Delete a secret

Request body:
Success returns HTTP 200 OK:
Deletion removes the ciphertext, policy, and metadata parameters. Deleting a name that is not present is harmless. Invalid names return 400 Bad Request.

Plugin message API

Plugin-to-plugin retrieval uses the Kernel’s synchronous plugin message transport. The target plugin name is secret-manager, and the supported topic is:
The payload is UTF-8 JSON:
The sending plugin must be listed in allowed_plugins for the requested secret. The passphrase may be empty for an identity secret and is required for an scrypt secret. The Kernel sets the message source to the registered name of the sender; a caller cannot spoof this field. On success, the reply places the plaintext in message:
On failure, the reply places a description in error and does not return a secret value:
The message interface rejects unsupported topics, invalid JSON, invalid secret names, unauthorized sources, missing secrets, unsupported encryption metadata, missing scrypt passphrases, and invalid passphrases. For the common message envelope and host-authenticated source behavior, see the plugin message IPC guide.

KV and other plugin interfaces

secret-manager does not use the shared KV store for secret data. Consumers must not look for secrets in a KV namespace; use the HTTP API or the plugin message topic described above. The plugin does not declare a Socket.IO namespace or events. Its host interaction is limited to:
  • receiving Params during registration;
  • using the host PatchParams callback to persist identity and secret changes;
  • handling authenticated HTTP requests;
  • receiving authenticated plugin messages.
Its static page loads the shared frontend resources and the web SDK for the standard theme, but those browser resources are UI dependencies rather than a secret-storage interface.

Security considerations

  • Use HTTP endpoints only for authenticated administrative access.
  • Prefer plugin messages for service-to-service access, and grant each secret only to the plugin names that need it.
  • Treat plugin message replies as plaintext secret material.
  • Supply the passphrase for every scrypt retrieval, and never log or persist it in the calling plugin.
  • Do not log request bodies, passphrases, decrypted values, identities, or ciphertext parameters.