Secrets Manager API
The Secrets Manager API is used by machine clients (CI servers, application servers, Kubernetes operators) to fetch and manage secrets. It uses a separate authentication mechanism from the user-facing API: ECDSA P-256 signed JWTs.
Client Authentication
Secrets Manager clients authenticate by signing a JWT with their ECDSA P-256 private key. The JWT is included in the Authorization header as a Bearer token.
JWT Structure
Header:
{
"alg": "ES256",
"typ": "JWT"
}
Payload:
{
"sub": "c9a1b2d3-4e5f-6789-abcd-ef0123456789",
"aud": "https://vault.example.com",
"iat": 1712440800,
"exp": 1712444400
}
| Claim | Description |
|---|---|
sub | The client_id assigned during client creation |
aud | The SyVault server URL |
iat | Issued-at timestamp (Unix seconds) |
exp | Expiration timestamp (max 1 hour from iat) |
The JWT is signed with the client's ECDSA P-256 private key. The server validates the signature against the public key stored during bootstrap.
Example (using curl with a pre-signed JWT)
export SM_TOKEN="eyJhbGciOiJFUzI1NiIs..."
curl https://vault.example.com/api/sm/v1/secrets \
-H "Authorization: Bearer $SM_TOKEN"
Bootstrap
Exchange a one-time access token for persistent client key material. This endpoint is called once per client, typically by sy init.
POST /api/sm/v1/bootstrap
curl -X POST https://vault.example.com/api/sm/v1/bootstrap \
-H "Content-Type: application/json" \
-d '{
"token": "vft_eyJhbGciOiJFUzI1NiIs...",
"public_key": "base64-encoded-ecdsa-p256-public-key"
}'
The client generates an ECDSA P-256 key pair locally and sends the public key. The server stores the public key for future JWT verification and returns the encrypted folder keys the client is authorized to access.
Response (200):
{
"client_id": "c9a1b2d3-4e5f-6789-abcd-ef0123456789",
"application_id": "app-uuid-1",
"encrypted_folder_keys": {
"folder-uuid-1": "base64-encrypted-folder-key",
"folder-uuid-2": "base64-encrypted-folder-key"
}
}
Response (401) — token already used or invalid:
{
"error": "invalid_token",
"message": "This access token has already been used or is invalid."
}
List Secrets
Retrieve all secrets the client is authorized to access.
GET /api/sm/v1/secrets
curl https://vault.example.com/api/sm/v1/secrets \
-H "Authorization: Bearer $SM_TOKEN"
Response:
{
"data": [
{
"uid": "7Kj9mNpQ2xRs",
"folder_id": "folder-uuid-1",
"title_encrypted": "base64-encrypted-title",
"data_encrypted": "base64-encrypted-data",
"updated_at": "2026-04-05T14:22:00Z"
},
{
"uid": "Xp4qRtY8mNwK",
"folder_id": "folder-uuid-2",
"title_encrypted": "base64-encrypted-title",
"data_encrypted": "base64-encrypted-data",
"updated_at": "2026-04-03T09:15:00Z"
}
]
}
The client decrypts title_encrypted and data_encrypted using the corresponding folder key received during bootstrap.
Get a Single Secret
GET /api/sm/v1/secrets/{uid}
curl https://vault.example.com/api/sm/v1/secrets/7Kj9mNpQ2xRs \
-H "Authorization: Bearer $SM_TOKEN"
Response:
{
"uid": "7Kj9mNpQ2xRs",
"folder_id": "folder-uuid-1",
"title_encrypted": "base64-encrypted-title",
"data_encrypted": "base64-encrypted-data",
"updated_at": "2026-04-05T14:22:00Z"
}
Resolve Notation
Resolve a sy:// notation URI to a single field value server-side. The server looks up the folder, title, and field, then returns the encrypted field value.
POST /api/sm/v1/notation
curl -X POST https://vault.example.com/api/sm/v1/notation \
-H "Authorization: Bearer $SM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"notation": "sy://Production/Database/field/password"
}'
Response:
{
"uid": "7Kj9mNpQ2xRs",
"folder": "Production",
"title": "Database",
"field": "password",
"value_encrypted": "base64-encrypted-value"
}
The client decrypts value_encrypted using the folder key for the "Production" folder.
Error Responses
| Status | Error Code | Description |
|---|---|---|
400 | invalid_request | Malformed request body or invalid notation |
401 | invalid_token | JWT signature invalid, expired, or client not found |
403 | access_denied | Client does not have access to the requested folder |
404 | secret_not_found | No secret matches the UID or notation |
429 | rate_limited | Too many requests (server-side per-IP bucket exhausted) |
Rate Limits
SyVault applies two global per-IP rate-limit buckets (see crates/server/src/api/rate_limit.rs):
- Strict bucket (5 requests/minute/IP): applied to auth-sensitive endpoints such as
POST /api/sm/v1/bootstrap. Designed for brute-force resistance. - Standard bucket (60 requests/minute/IP): applied to all other Secrets Manager client endpoints (
GET /api/sm/v1/secrets,GET /api/sm/v1/secrets/{uid},POST /api/sm/v1/notation, etc.).
Limits are enforced per source IP address, not per client ID. High-volume applications behind a NAT or shared egress should request a dedicated allowlist.
Custom per-endpoint rate limits (e.g. 100 req/min on list, 300 on get) are not currently implemented. A future release may introduce per-client token buckets with per-endpoint policies.
Write Operations
In addition to reading secrets, Secrets Manager clients can create, update, delete, and search secrets. These operations require that the client's access token was created with write permissions on the target folder.
Create a Secret
POST /api/sm/v1/secrets
curl -X POST https://vault.example.com/api/sm/v1/secrets \
-H "Authorization: Bearer $SM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"folder_id": "folder-uuid-1",
"record_type": "login",
"encrypted_payload": "base64-client-encrypted-payload",
"wrapped_dek": "base64-folder-key-wrapped-dek"
}'
| Field | Type | Required | Description |
|---|---|---|---|
folder_id | string | Yes | The folder to create the secret in. The client must have write access. |
record_type | string | Yes | Record type: login, card, note, identity, ssh_key, api_credential, database, env_file. |
encrypted_payload | string | Yes | The record data encrypted with a DEK (AES-256-GCM). |
wrapped_dek | string | Yes | The DEK wrapped (encrypted) with the folder key. |
Response (201):
{
"uid": "Nw8kLmPq3xTs",
"folder_id": "folder-uuid-1",
"record_type": "login",
"created_at": "2026-04-06T12:00:00Z",
"updated_at": "2026-04-06T12:00:00Z"
}
Update a Secret
PUT /api/sm/v1/secrets/{uid}
curl -X PUT https://vault.example.com/api/sm/v1/secrets/Nw8kLmPq3xTs \
-H "Authorization: Bearer $SM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"encrypted_payload": "base64-updated-encrypted-payload",
"wrapped_dek": "base64-folder-key-wrapped-dek"
}'
Response (200):
{
"uid": "Nw8kLmPq3xTs",
"folder_id": "folder-uuid-1",
"record_type": "login",
"created_at": "2026-04-06T12:00:00Z",
"updated_at": "2026-04-06T12:05:00Z"
}
Delete a Secret
DELETE /api/sm/v1/secrets/{uid}
curl -X DELETE https://vault.example.com/api/sm/v1/secrets/Nw8kLmPq3xTs \
-H "Authorization: Bearer $SM_TOKEN"
Returns 204 No Content. The encrypted payload and wrapped DEK are permanently removed from the database. This action is irreversible.
Search Secrets
GET /api/sm/v1/secrets/search?title={query}
Search secrets by encrypted title. The server performs the search against title metadata indexed during creation. Only secrets the client has access to are returned.
curl "https://vault.example.com/api/sm/v1/secrets/search?title=production-db" \
-H "Authorization: Bearer $SM_TOKEN"
Response (200):
{
"data": [
{
"uid": "7Kj9mNpQ2xRs",
"folder_id": "folder-uuid-1",
"title_encrypted": "base64-encrypted-title",
"data_encrypted": "base64-encrypted-data",
"updated_at": "2026-04-05T14:22:00Z"
}
]
}
Get a Specific Field
GET /api/sm/v1/secrets/{uid}/fields/{field}
Retrieve a single encrypted field from a secret without fetching the entire record. Useful when you only need the password or a single API key from a multi-field record.
curl https://vault.example.com/api/sm/v1/secrets/7Kj9mNpQ2xRs/fields/password \
-H "Authorization: Bearer $SM_TOKEN"
Response (200):
{
"uid": "7Kj9mNpQ2xRs",
"field": "password",
"value_encrypted": "base64-encrypted-field-value",
"updated_at": "2026-04-05T14:22:00Z"
}
The client decrypts value_encrypted using the folder key.
Write Operation Rate Limits
Write endpoints share the same standard bucket (60 req/min per IP) described in the main Rate Limits section. Per-endpoint custom limits are not currently implemented.