From aa232f964c96da8bcb01ac62bf4ac812435df95e Mon Sep 17 00:00:00 2001 From: Naduni Pamudika Date: Fri, 26 Jun 2026 17:02:50 +0530 Subject: [PATCH 1/3] Add docs for secret management --- .../cloud/ai-workspace/secrets-management.md | 372 ++++++++++++++++++ en/mkdocs.yml | 1 + 2 files changed, 373 insertions(+) create mode 100644 en/docs/cloud/ai-workspace/secrets-management.md diff --git a/en/docs/cloud/ai-workspace/secrets-management.md b/en/docs/cloud/ai-workspace/secrets-management.md new file mode 100644 index 000000000..c4b320191 --- /dev/null +++ b/en/docs/cloud/ai-workspace/secrets-management.md @@ -0,0 +1,372 @@ +--- +title: "Secrets management" +description: "Store and manage encrypted secrets in AI Workspace and reference them securely in artifact configurations without exposing plaintext credentials." +canonical_url: https://wso2.com/api-platform/docs/cloud/ai-workspace/secrets-management/ +md_url: https://wso2.com/api-platform/docs/cloud/ai-workspace/secrets-management.md +tags: + - cloud + - ai-workspace + - secrets + - security +author: WSO2 API Platform Documentation Team +last_updated: 2026-06-26 +content_type: "how-to" +--- + +# Secrets Management + +AI Workspace lets you store sensitive credentials as **secrets** and reference them securely in artifact configurations. Secrets are encrypted at rest using AES-GCM-256. Plaintext values are never written to the database and are **never returned in any API response** — not even the creation response. + +Use secrets to avoid embedding raw API keys, tokens, or passwords directly in LLM provider configurations, MCP proxy configs, or API backend settings. + +## How It Works + +1. Create a secret via the Platform API with a unique `handle` and the plaintext `value`. +2. Reference the secret in any artifact configuration using the placeholder syntax: + ```text + {{ secret "your-secret-handle" }} + ``` +3. When an artifact that contains a placeholder is deployed, the gateway resolves it with the decrypted value at runtime — the plaintext never appears in the control-plane database or configuration files. +4. To rotate a credential, call `PUT /api/v0.9/secrets/{handle}` with the new value. Because artifacts reference the secret by handle, no artifact changes or redeployment are required. + +## Auto-Encryption in the AI Workspace UI + +When you create or update an **LLM Provider** or **MCP Proxy** through the AI Workspace UI and fill in an upstream API key or auth value, the UI automatically: + +1. Creates a secret via `POST /api/v0.9/secrets` using a deterministic handle derived from the resource ID (e.g. `wso2-openai-provider-api-key`). +2. Substitutes the credential with the `{{ secret "handle" }}` placeholder before saving the resource. + +The raw credential is sent to the secrets API only once and is never stored in the artifact configuration. Re-saving a resource that already contains a placeholder skips the secret creation step. + +## API Reference + +The secrets API is available at `/api/v0.9/secrets`. All requests require a valid JWT with the appropriate scope. + +### Required Scopes + +| Scope | Grants | +|--------------------|------------------------------------------| +| `ap:secret:read` | List secrets and get metadata by handle | +| `ap:secret:create` | Create a new secret | +| `ap:secret:update` | Rotate / update an existing secret value | +| `ap:secret:delete` | Delete a secret | +| `ap:secret:manage` | All of the above | + +--- + +### Create a Secret + +```http +POST /api/v0.9/secrets +Content-Type: multipart/form-data +``` + +Stores a new encrypted secret. The plaintext value is never returned — not even in this response. + +!!! note "Why multipart/form-data?" + The `value` field is designed to carry arbitrary binary content in a future release (e.g. TLS certificates, SSH private keys). Using `multipart/form-data` now avoids a breaking content-type change when file-based secrets are introduced. + +**Request fields** + +| Field | Required | Description | +|-------|----------|-------------| +| `handle` | Yes | Unique identifier within the organization. Used in `{{ secret "handle" }}` references. Immutable after creation. | +| `name` | Yes | Human-readable display name for the secret. | +| `value` | Yes | The sensitive value to encrypt and store. | +| `description` | No | Optional description. | +| `type` | No | Secret type. `GENERIC` (default) for API keys and tokens. | + +**Example request** + +```http +POST /api/v0.9/secrets +Authorization: Bearer +Content-Type: multipart/form-data; boundary=----FormBoundary + +------FormBoundary +Content-Disposition: form-data; name="handle" + +wso2-openai-key +------FormBoundary +Content-Disposition: form-data; name="name" + +WSO2 OpenAI API Key +------FormBoundary +Content-Disposition: form-data; name="description" + +API key for WSO2 OpenAI integration +------FormBoundary +Content-Disposition: form-data; name="type" + +GENERIC +------FormBoundary +Content-Disposition: form-data; name="value" + +sk-xxx +------FormBoundary-- +``` + +**Response — 201 Created** + +```json +{ + "uuid": "a1b2c3d4-...", + "handle": "wso2-openai-key", + "name": "WSO2 OpenAI API Key", + "createdAt": "2026-01-12T10:00:00Z", + "updatedAt": "2026-01-12T10:00:00Z" +} +``` + +The response does not include the `value`. Store the plaintext in a secure location before submitting it — it cannot be retrieved later. + +**Error responses** + +| Status | Reason | +|--------|--------| +| 400 | Missing required fields or invalid request | +| 409 | A secret with the same `handle` already exists in the organization | + +--- + +### List Secrets + +```http +GET /api/v0.9/secrets +``` + +Returns metadata for all secrets in the organization. Plaintext values are never included. + +**Query parameters** + +| Parameter | Default | Max | Description | +|-----------|---------|-----|-------------| +| `limit` | 25 | 100 | Maximum number of results to return | +| `offset` | 0 | — | Number of results to skip for pagination | + +**Response — 200 OK** + +```json +{ + "list": [ + { + "uuid": "a1b2c3d4-...", + "handle": "wso2-openai-key", + "name": "WSO2 OpenAI API Key", + "type": "GENERIC", + "provider": "IN_BUILT", + "createdAt": "2026-01-12T10:00:00Z", + "updatedAt": "2026-01-12T10:00:00Z" + } + ], + "pagination": { + "total": 1, + "limit": 25, + "offset": 0 + } +} +``` + +--- + +### Get Secret Metadata + +```http +GET /api/v0.9/secrets/{handle} +``` + +Returns metadata for a single secret. The plaintext value is not included. + +**Response — 200 OK** + +```json +{ + "uuid": "a1b2c3d4-...", + "handle": "wso2-openai-key", + "name": "WSO2 OpenAI API Key", + "type": "GENERIC", + "provider": "IN_BUILT", + "createdAt": "2026-01-12T10:00:00Z", + "updatedAt": "2026-01-12T10:00:00Z" +} +``` + +**Error responses** + +| Status | Reason | +|--------|--------| +| 404 | No secret found with the given handle in this organization | + +--- + +### Rotate a Secret + +```http +PUT /api/v0.9/secrets/{handle} +Content-Type: multipart/form-data +``` + +Re-encrypts and stores a new value. Because `handle` is immutable, all `{{ secret "handle" }}` placeholders across existing resources remain valid without modification. The plaintext value is not returned in the response. + +**Request fields** + +| Field | Required | Description | +|-------|----------|-------------| +| `value` | Yes | The new sensitive value to encrypt and store. | +| `name` | No | Updated display name. | +| `description` | No | Updated description. | + +**Example request** + +```http +PUT /api/v0.9/secrets/wso2-openai-key +Authorization: Bearer +Content-Type: multipart/form-data; boundary=----FormBoundary + +------FormBoundary +Content-Disposition: form-data; name="value" + +sk-new-value +------FormBoundary +Content-Disposition: form-data; name="name" + +WSO2 OpenAI API Key (rotated) +------FormBoundary +Content-Disposition: form-data; name="description" + +Rotated on 2026-06-26 — old key decommissioned +------FormBoundary-- +``` + +**Response — 200 OK** + +```json +{ + "uuid": "a1b2c3d4-...", + "handle": "wso2-openai-key", + "name": "WSO2 OpenAI API Key (rotated)", + "createdAt": "2026-01-12T10:00:00Z", + "updatedAt": "2026-06-26T11:30:00Z" +} +``` + +**Notes** + +- If the secret's status is `DEPRECATED` (previously soft-deleted), a successful rotation sets it back to `ACTIVE`. The secret can again be referenced by new resources and will be included in gateway sync. +- The gateway picks up the updated value on its next sync cycle — no redeployment of referencing artifacts is required. + +**Error responses** + +| Status | Reason | +|--------|--------| +| 404 | No secret found with the given handle | + +--- + +### Delete a Secret + +```http +DELETE /api/v0.9/secrets/{handle} +``` + +Soft-deletes a secret by setting its status to `DEPRECATED`. Deletion is blocked with `409 Conflict` if the secret is currently referenced by any artifact — either in its saved configuration or in a snapshot currently deployed to a gateway. + +**Response — 204 No Content** + +**Error responses** + +| Status | Reason | +|--------|--------| +| 404 | No secret found with the given handle | +| 409 | Secret is still referenced by one or more artifacts | + +**409 response example** + +```json +{ + "error": "secret is referenced by active resources", + "references": [ + { "type": "llm_provider", "handle": "openai-provider", "name": "OpenAI Provider" }, + { "type": "mcp_proxy", "handle": "my-mcp-proxy", "name": "My MCP Proxy" } + ] +} +``` + +--- + +## Referencing Secrets in Artifact Configurations + +Use the following placeholder syntax wherever a configuration field accepts a sensitive string value: + +```text +{{ secret "your-secret-handle" }} +``` + +**Example — LLM provider upstream API key** + +```yaml +spec: + upstream: + auth: + type: api-key + header: Authorization + value: 'Bearer {{ secret "wso2-openai-key" }}' +``` + +**Validation at save time** + +When creating or updating any resource that contains `{{ secret "..." }}` references, the Platform API validates that an active secret with the referenced handle exists in the organization. If any placeholder cannot be resolved, the request is rejected with `400 Bad Request` and the list of unresolvable handles. + +--- + +## Credential Rotation + +To rotate a credential without touching artifact configurations: + +1. Call `PUT /api/v0.9/secrets/{handle}` with the new value. +2. The gateway picks up the updated secret on the next sync cycle. + +No artifact changes or redeployment are required because resources reference the secret by handle, not by value. + +--- + +## Deleting a Secret Safely + +Attempting to delete a secret that is still in use returns HTTP 409. To remove it cleanly: + +1. Inspect the `references` list in the 409 response. +2. Update each referencing artifact to remove or replace the `{{ secret "handle" }}` reference. +3. Redeploy the updated artifacts to the gateway. +4. Retry `DELETE /api/v0.9/secrets/{handle}`. + +--- + +## Configuration + +The following environment variable controls encryption for the Platform API: + +| Env Var | Description | +|---------|-------------| +| `PLATFORM_SECRET_ENCRYPTION_KEY` | 32-byte AES-256 key as 64 hex characters or base64. If unset, a random ephemeral key is auto-generated at startup — secrets stored in that session will be unreadable after a restart. | + +Generate a stable key with: + +```sh +openssl rand -hex 32 +``` + +For Docker Compose deployments, set the key in a `.env` file next to `docker-compose.yaml`. First generate a key: + +```sh +openssl rand -hex 32 +# example output: a3f1e2d4b5c6... +``` + +Then copy the output value into your `.env` file: + +```sh +PLATFORM_SECRET_ENCRYPTION_KEY=a3f1e2d4b5c6... +``` + +!!! warning + Always set a stable `PLATFORM_SECRET_ENCRYPTION_KEY` in any environment where secrets must persist across restarts or across multiple replicas. An ephemeral auto-generated key will make existing encrypted secrets unreadable after a restart. diff --git a/en/mkdocs.yml b/en/mkdocs.yml index e34081ba5..0d010abee 100644 --- a/en/mkdocs.yml +++ b/en/mkdocs.yml @@ -222,6 +222,7 @@ nav: - Use the MCP Registry API: cloud/mcp-servers/mcp-registry-api.md - GenAI Applications: cloud/ai-workspace/genai-applications.md - Configure Inbound Auth: cloud/ai-workspace/configure-inbound-auth.md + - Secrets Management: cloud/ai-workspace/secrets-management.md - Invoke via SDKs: cloud/ai-workspace/using-sdks.md # - Insights: cloud/ai-workspace/insights.md - Policies: From edc9bd43a729f3f8d21b25d61ff49410f1c151c7 Mon Sep 17 00:00:00 2001 From: Naduni Pamudika Date: Tue, 30 Jun 2026 11:37:21 +0530 Subject: [PATCH 2/3] Update llms.txt file --- en/docs/llms.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/en/docs/llms.txt b/en/docs/llms.txt index 50061a5c4..3bf058e61 100644 --- a/en/docs/llms.txt +++ b/en/docs/llms.txt @@ -134,6 +134,7 @@ - [Apply Policies to MCP Proxy](https://wso2.com/api-platform/docs/cloud/ai-workspace/mcp-proxies/apply-policies.md): Attach authentication, authorization, and ACL policies to an MCP proxy - [GenAI Applications](https://wso2.com/api-platform/docs/cloud/ai-workspace/genai-applications.md): Register and track GenAI applications that consume LLM and MCP resources through AI Workspace - [Configure Inbound Auth](https://wso2.com/api-platform/docs/cloud/ai-workspace/configure-inbound-auth.md): Configure how client applications authenticate to AI Workspace LLM and MCP proxies +- [Secrets Management](https://wso2.com/api-platform/docs/cloud/ai-workspace/secrets-management.md): Store and manage AES-GCM-256 encrypted secrets in AI Workspace and reference them securely in LLM provider, MCP proxy, and API backend configurations using placeholder syntax - [Invoke via SDKs](https://wso2.com/api-platform/docs/cloud/ai-workspace/using-sdks.md): Use OpenAI-compatible SDKs to call LLM proxies managed by AI Workspace - [AI Policies Overview](https://wso2.com/api-platform/docs/cloud/ai-workspace/policies/overview.md): Overview of AI Workspace policies: guardrails, rate limits, prompt management, and semantic caching - [Guardrails Overview (AI Workspace)](https://wso2.com/api-platform/docs/cloud/ai-workspace/policies/guardrails/overview.md): Overview of guardrails in AI Workspace: PII masking, content safety, and prompt injection protection From 9a65226f452a7cf1f9d20a411e7d1cfe752ab826 Mon Sep 17 00:00:00 2001 From: Naduni Pamudika Date: Tue, 21 Jul 2026 21:58:58 +0530 Subject: [PATCH 3/3] Fix build issue in secret management doc page --- en/docs/cloud/ai-workspace/secrets-management.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/en/docs/cloud/ai-workspace/secrets-management.md b/en/docs/cloud/ai-workspace/secrets-management.md index c4b320191..3026c71e5 100644 --- a/en/docs/cloud/ai-workspace/secrets-management.md +++ b/en/docs/cloud/ai-workspace/secrets-management.md @@ -23,9 +23,11 @@ Use secrets to avoid embedding raw API keys, tokens, or passwords directly in LL 1. Create a secret via the Platform API with a unique `handle` and the plaintext `value`. 2. Reference the secret in any artifact configuration using the placeholder syntax: +{% raw %} ```text {{ secret "your-secret-handle" }} ``` +{% endraw %} 3. When an artifact that contains a placeholder is deployed, the gateway resolves it with the decrypted value at runtime — the plaintext never appears in the control-plane database or configuration files. 4. To rotate a credential, call `PUT /api/v0.9/secrets/{handle}` with the new value. Because artifacts reference the secret by handle, no artifact changes or redeployment are required. @@ -34,7 +36,7 @@ Use secrets to avoid embedding raw API keys, tokens, or passwords directly in LL When you create or update an **LLM Provider** or **MCP Proxy** through the AI Workspace UI and fill in an upstream API key or auth value, the UI automatically: 1. Creates a secret via `POST /api/v0.9/secrets` using a deterministic handle derived from the resource ID (e.g. `wso2-openai-provider-api-key`). -2. Substitutes the credential with the `{{ secret "handle" }}` placeholder before saving the resource. +2. Substitutes the credential with a secret placeholder before saving the resource. The raw credential is sent to the secrets API only once and is never stored in the artifact configuration. Re-saving a resource that already contains a placeholder skips the secret creation step. @@ -70,7 +72,7 @@ Stores a new encrypted secret. The plaintext value is never returned — not eve | Field | Required | Description | |-------|----------|-------------| -| `handle` | Yes | Unique identifier within the organization. Used in `{{ secret "handle" }}` references. Immutable after creation. | +| `handle` | Yes | Unique identifier within the organization. Used as the secret name in placeholder references. Immutable after creation. | | `name` | Yes | Human-readable display name for the secret. | | `value` | Yes | The sensitive value to encrypt and store. | | `description` | No | Optional description. | @@ -206,7 +208,7 @@ PUT /api/v0.9/secrets/{handle} Content-Type: multipart/form-data ``` -Re-encrypts and stores a new value. Because `handle` is immutable, all `{{ secret "handle" }}` placeholders across existing resources remain valid without modification. The plaintext value is not returned in the response. +Re-encrypts and stores a new value. Because `handle` is immutable, all secret placeholders across existing resources remain valid without modification. The plaintext value is not returned in the response. **Request fields** @@ -298,12 +300,15 @@ Soft-deletes a secret by setting its status to `DEPRECATED`. Deletion is blocked Use the following placeholder syntax wherever a configuration field accepts a sensitive string value: +{% raw %} ```text {{ secret "your-secret-handle" }} ``` +{% endraw %} **Example — LLM provider upstream API key** +{% raw %} ```yaml spec: upstream: @@ -312,10 +317,11 @@ spec: header: Authorization value: 'Bearer {{ secret "wso2-openai-key" }}' ``` +{% endraw %} **Validation at save time** -When creating or updating any resource that contains `{{ secret "..." }}` references, the Platform API validates that an active secret with the referenced handle exists in the organization. If any placeholder cannot be resolved, the request is rejected with `400 Bad Request` and the list of unresolvable handles. +When creating or updating any resource that contains secret placeholder references, the Platform API validates that an active secret with the referenced handle exists in the organization. If any placeholder cannot be resolved, the request is rejected with `400 Bad Request` and the list of unresolvable handles. --- @@ -335,7 +341,7 @@ No artifact changes or redeployment are required because resources reference the Attempting to delete a secret that is still in use returns HTTP 409. To remove it cleanly: 1. Inspect the `references` list in the 409 response. -2. Update each referencing artifact to remove or replace the `{{ secret "handle" }}` reference. +2. Update each referencing artifact to remove or replace its secret placeholder reference. 3. Redeploy the updated artifacts to the gateway. 4. Retry `DELETE /api/v0.9/secrets/{handle}`.