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:
| 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:
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.
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"
}'
|
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",
},
)
|
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
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:
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:
| 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:
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:
| 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)
| 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)
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
| {
"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:
| {
"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:
| 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:
| 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