---
title: Dynamic Secrets
path: api-guides/dynamic-secrets
status: published
---

# Dynamic Secrets

End-to-end walkthrough: configure a PostgreSQL engine, define a role, generate a lease, use it, let it expire. For the conceptual model, see [Dynamic Secrets](../core-concepts/dynamic-secrets).

**Base path:** `/v1/dynamic/`

## 1. Store the root credentials

The engine needs admin access to the target system. Store those credentials in ScaiVault first, protected by a narrow policy:

```bash
curl -X PUT https://scaivault.scailabs.ai/v1/secrets/infra/postgres/support/root \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {"username": "scaivault_admin", "password": "REDACTED"},
    "secret_type": "json"
  }'
```

Bind a policy that lets only the dynamic engine read this:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/policies \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "dynamic-engine-pg-root",
    "rules": [
      {
        "path_pattern": "infra/postgres/support/root",
        "permissions": ["read", "rotate"]
      }
    ]
  }'
```

Bind it to the system identity `dynamic:support-db`.

## 2. Configure the engine

```bash
curl -X POST https://scaivault.scailabs.ai/v1/dynamic/engines \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support-db",
    "type": "database",
    "config": {
      "plugin": "postgresql",
      "connection_url": "postgresql://{{username}}:{{password}}@db.internal:5432/support",
      "root_credentials_path": "infra/postgres/support/root"
    },
    "default_ttl": "1h",
    "max_ttl": "24h"
  }'
```

```python
resp = httpx.post(
    "https://scaivault.scailabs.ai/v1/dynamic/engines",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "name": "support-db",
        "type": "database",
        "config": {
            "plugin": "postgresql",
            "connection_url": "postgresql://{{username}}:{{password}}@db.internal:5432/support",
            "root_credentials_path": "infra/postgres/support/root",
        },
        "default_ttl": "1h",
        "max_ttl": "24h",
    },
)
```

```typescript
const resp = await fetch("https://scaivault.scailabs.ai/v1/dynamic/engines", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADMIN_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "support-db",
    type: "database",
    config: {
      plugin: "postgresql",
      connection_url: "postgresql://{{username}}:{{password}}@db.internal:5432/support",
      root_credentials_path: "infra/postgres/support/root",
    },
    default_ttl: "1h",
    max_ttl: "24h",
  }),
});
```

ScaiVault reads the root credentials, substitutes them into `connection_url`, and opens a test connection. If the connection fails you get `400 invalid_config` with details.

### Supported plugins

| Plugin | `type` | Notes |
|--------|--------|-------|
| `postgresql` | `database` | |
| `mysql` | `database` | |
| `mongodb` | `database` | Atlas or self-hosted |
| `redis` | `database` | Uses Redis ACL |
| `aws` | `aws` | Assume role or create access keys |
| `azure` | `azure` | Service principals |
| `gcp` | `gcp` | Service accounts or impersonated tokens |
| `ssh` | `ssh` | Signs SSH certs |
| `custom` | `custom` | Your scripts |

## 3. Define a role

```bash
curl -X POST https://scaivault.scailabs.ai/v1/dynamic/engines/support-db/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "readonly",
    "creation_statements": [
      "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '\''{{password}}'\'' VALID UNTIL '\''{{expiration}}'\''",
      "GRANT USAGE ON SCHEMA public TO \"{{name}}\"",
      "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\""
    ],
    "revocation_statements": [
      "REVOKE ALL ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"",
      "REASSIGN OWNED BY \"{{name}}\" TO postgres",
      "DROP OWNED BY \"{{name}}\"",
      "DROP ROLE IF EXISTS \"{{name}}\""
    ],
    "default_ttl": "1h",
    "max_ttl": "8h"
  }'
```

### Template variables

| Variable | Resolves to |
|----------|-------------|
| `{{name}}` | Auto-generated username — `v_<role>_<random>` |
| `{{password}}` | Auto-generated random password |
| `{{expiration}}` | Lease expiration as a Postgres timestamp literal |
| `{{ttl_seconds}}` | Remaining TTL in seconds |

## 4. Grant your application access

A service account `sa:reporting-service` needs to generate credentials:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/policies \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "reporting-service-dynamic",
    "rules": [
      {
        "path_pattern": "dynamic/engines/support-db/roles/readonly",
        "permissions": ["read"]
      }
    ]
  }'
```

And bind it. (Note: dynamic-secret access is permitted via policies on synthetic paths like `dynamic/engines/{engine}/roles/{role}`, so the same policy model applies.)

## 5. Generate credentials

The application asks for a lease:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/dynamic/engines/support-db/creds/readonly \
  -H "Authorization: Bearer $SERVICE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttl": "2h", "metadata": {"purpose": "nightly report"}}'
```

Response:

```json
{
  "lease_id": "lease_db_abc123xyz",
  "data": {
    "username": "v_readonly_a1b2c3d4",
    "password": "kX9#mP2$vL5@nQ8&wR3!",
    "connection_url": "postgresql://v_readonly_a1b2c3d4:...@db.internal:5432/support",
    "host": "db.internal",
    "port": 5432,
    "database": "support"
  },
  "lease_duration": "2h",
  "renewable": true,
  "expires_at": "2026-04-23T16:00:00Z"
}
```

The application uses `connection_url` (or the username/password separately) to talk to Postgres. Two hours later, ScaiVault connects back and runs the revocation statements — the user vanishes.

## 6. Renew (optional)

For workers running longer than the initial TTL:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/dynamic/leases/lease_db_abc123xyz/renew \
  -H "Authorization: Bearer $SERVICE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"increment": "1h"}'
```

Extends the lease by 1 hour (up to the role's `max_ttl`). The credential itself doesn't change.

## 7. Revoke early (optional)

```bash
curl -X DELETE https://scaivault.scailabs.ai/v1/dynamic/leases/lease_db_abc123xyz \
  -H "Authorization: Bearer $SERVICE_TOKEN"
```

Good practice: revoke as soon as the worker finishes, even before natural expiration. Keeps the active-lease count low.

## Patterns

### Context manager (Python)

```python
from contextlib import contextmanager
import httpx

@contextmanager
def dynamic_pg_credentials(ttl="1h"):
    resp = httpx.post(
        "https://scaivault.scailabs.ai/v1/dynamic/engines/support-db/creds/readonly",
        headers={"Authorization": f"Bearer {TOKEN}"},
        json={"ttl": ttl},
    )
    lease = resp.json()
    try:
        yield lease["data"]
    finally:
        httpx.delete(
            f"https://scaivault.scailabs.ai/v1/dynamic/leases/{lease['lease_id']}",
            headers={"Authorization": f"Bearer {TOKEN}"},
        )

with dynamic_pg_credentials() as creds:
    run_query_with(creds["connection_url"])
```

### AWS engine

```json
{
  "name": "prod-aws",
  "type": "aws",
  "config": {
    "region": "us-east-1",
    "root_credentials_path": "infra/aws/prod/admin"
  },
  "default_ttl": "1h",
  "max_ttl": "12h"
}
```

Role generates an IAM user with a specific policy:

```json
{
  "name": "s3-read",
  "credential_type": "iam_user",
  "policy_document": {
    "Version": "2012-10-17",
    "Statement": [{"Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::reports/*"}]
  },
  "default_ttl": "30m",
  "max_ttl": "2h"
}
```

Generated lease contains `access_key_id` and `secret_access_key`. AWS credentials propagate in ~10 seconds — your client may want to wait-and-retry on first use.

### Emergency revoke-all

Suspicious activity? Revoke every lease from an engine:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/dynamic/leases/revoke-prefix \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prefix": "lease_", "engine": "support-db"}'
```

Then rotate the engine's root credential:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/secrets/infra/postgres/support/root/rotate \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "incident response", "grace_period": "0"}'
```

## Common error codes

| Code | When |
|------|------|
| `engine_not_found` | |
| `role_not_found` | |
| `engine_unreachable` | Root credentials can't reach the target system |
| `invalid_config` | Connection URL or root creds didn't validate |
| `ttl_exceeds_max` | Requested TTL > role's `max_ttl` |
| `lease_not_found` / `lease_expired` | |

## What's next

- [Dynamic Secrets](../core-concepts/dynamic-secrets) — concepts.
- [Dynamic Reference](../reference/dynamic) — all endpoints.
- [Rotation](./rotation) — rotate the engine's root credentials automatically.
