> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arupa.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Secret Manager

> Integrate with the secret-manager core plugin through its configuration, HTTP API, and plugin message interface.

`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

| Property         | Value                    |
| ---------------- | ------------------------ |
| Plugin name      | `secret-manager`         |
| Type             | `wasm`                   |
| Contract version | `1`                      |
| Navigation entry | `/keys/pages/index.html` |
| HTTP prefix      | `/keys`                  |

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]`:

```toml theme={null}
[Plugins.secret-manager]
Restart = "always"
RunAsUser = ""

[Plugins.secret-manager.Params]
"secretmgr.identity" = "AGE-SECRET-KEY-ABCD"
"secretmgr.secret.server1" = "ABCDEFG"
"secretmgr.policy.server1" = "[\"ssh\"]"
"secretmgr.meta.server1" = "{
    \"name\":\"server1\",
    \"allowed_plugins\":[\"ssh\"],
    \"updated_at\":\"2026-07-17T11:47:31Z\",
    \"encryption\":\"scrypt\"
}"
```

`Params` are passed to the plugin in its `RegisterRequest`. The plugin uses
the following parameter names:

| Parameter                 | Format                 | Meaning                                                                                                                            |
| ------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `secretmgr.identity`      | X25519 identity string | Private identity used for secrets encrypted with `identity`. If absent, the plugin generates one and persists it through the host. |
| `secretmgr.secret.<name>` | Base64 text            | Age-encrypted ciphertext for the secret named `<name>`.                                                                            |
| `secretmgr.policy.<name>` | JSON array of strings  | Sorted, de-duplicated plugin names allowed to retrieve `<name>` through IPC.                                                       |
| `secretmgr.meta.<name>`   | JSON object            | Display and encryption metadata for `<name>`.                                                                                      |

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`:

```toml theme={null}
[Plugins.secret-manager.Params]
"secretmgr.identity" = "env://SECRET_KEY"
```

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:

```json theme={null}
{
  "name": "database-password",
  "description": "Password used by the database plugin",
  "allowed_plugins": ["database"],
  "updated_at": "2026-07-17T00:00:00Z",
  "encryption": "identity"
}
```

`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.

| Mode       | How it is selected                         | Retrieval requirement                                                                | Relative cost                                                                                   |
| ---------- | ------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `identity` | The request does not provide a passphrase. | The plugin's persisted `secretmgr.identity`.                                         | X25519 decryption is normally almost immediate.                                                 |
| `scrypt`   | The request provides a passphrase.         | The same passphrase used during encryption, supplied through HTTP or Plugin Message. | Password-based key derivation uses additional CPU and memory, so it can take noticeably longer. |

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

```http theme={null}
GET /keys
```

Response:

```json theme={null}
{
  "success": true,
  "keys": [
    {
      "name": "database-password",
      "description": "Password used by the database plugin",
      "allowed_plugins": ["database"],
      "updated_at": "2026-07-17T00:00:00Z",
      "encryption": "identity"
    }
  ]
}
```

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

### Add a secret

```http theme={null}
POST /keys/add
Content-Type: application/json
```

Request body:

```json theme={null}
{
  "name": "database-password",
  "description": "Password used by the database plugin",
  "value": "secret-value",
  "passphrase": "",
  "allowed_plugins": ["database"]
}
```

`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`:

```json theme={null}
{
  "success": true,
  "name": "database-password"
}
```

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

```http theme={null}
POST /keys/update
Content-Type: application/json
```

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

```http theme={null}
POST /keys/reveal
Content-Type: application/json
```

Request body:

```json theme={null}
{
  "name": "database-password",
  "passphrase": ""
}
```

The passphrase is required when the secret uses `scrypt` encryption and must
be empty for an `identity` secret. Success returns the plaintext:

```json theme={null}
{
  "success": true,
  "name": "database-password",
  "value": "secret-value"
}
```

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

```http theme={null}
POST /keys/delete
Content-Type: application/json
```

Request body:

```json theme={null}
{
  "name": "database-password"
}
```

Success returns HTTP `200 OK`:

```json theme={null}
{
  "success": true,
  "name": "database-password"
}
```

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:

```text theme={null}
secret-manager.secret.get
```

The payload is UTF-8 JSON:

```json theme={null}
{
  "name": "database-password",
  "passphrase": ""
}
```

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`:

```json theme={null}
{
  "error": "",
  "message": "secret-value"
}
```

On failure, the reply places a description in `error` and does not return a
secret value:

```json theme={null}
{
  "error": "plugin is not allowed to access this secret",
  "message": ""
}
```

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](../../../arupa/v0/en/plugin/ipc-message).

## 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.
