Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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.

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
1
2
3
4
5
6
7
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
1
2
3
4
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "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
1
2
3
4
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
1
2
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "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
1
2
3
4
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
1
2
3
4
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#

Updated 2026-05-17 13:26:50 View source (.md) rev 2