PostgreSQL — Dynamic Credentials Engine
Generates temporary PostgreSQL users with scoped privileges and automatic revocation.
Overview
The PostgreSQL engine creates short-lived database credentials on demand. When an application requests credentials, the engine creates a PostgreSQL role with specific privileges (SELECT, INSERT, etc.) and a VALID UNTIL expiry. When the lease expires or is revoked, the engine drops the role and reassigns any owned objects.
The plugin communicates with Arcan core via JSON over stdin/stdout. It does NOT connect to PostgreSQL directly -- the core holds the root DSN and executes SQL on the plugin's behalf via ctx.SQL / ctx.SQLExec host functions.
External system: PostgreSQL 12+
Capabilities
engine:dynamic_credentials-- create and revoke temporary database rolesengine:root_rotation-- rotate the root admin password
Engine Descriptor
{
"name": "postgres",
"version": "0.1.0",
"display_name": "PostgreSQL",
"description": "Dynamic credentials for PostgreSQL databases",
"sdk_version": 1,
"min_core_version": "0.1.0",
"capabilities": [
"host:sql",
"host:store:read",
"host:store:write",
"host:audit",
"engine:dynamic_credentials",
"engine:root_rotation"
],
"tier": "official",
"config_schema": {
"type": "object",
"properties": {
"dsn": {
"type": "string",
"description": "PostgreSQL connection string",
"format": "uri",
"example": "postgres://arcan_admin:[email protected]:5432/myapp?sslmode=require"
},
"max_ttl": {
"type": "string",
"description": "Maximum lease TTL (Go duration)",
"default": "24h"
},
"default_ttl": {
"type": "string",
"description": "Default lease TTL if not specified",
"default": "1h"
},
"admin_role": {
"type": "string",
"description": "PostgreSQL role that owns generated roles",
"default": "arcan_admin"
},
"max_connections": {
"type": "integer",
"description": "Max concurrent leased credentials",
"default": 50
}
},
"required": ["dsn"]
},
"default_roles": [
{ "name": "readonly", "config": { "grants": "SELECT" } },
{ "name": "readwrite", "config": { "grants": "SELECT, INSERT, UPDATE, DELETE" } },
{ "name": "admin", "config": { "grants": "ALL PRIVILEGES" } }
]
}
Configuration
What the admin provides in the engine configuration:
| Parameter | Required | Description | Example |
|---|---|---|---|
dsn | Yes | PostgreSQL connection string | postgres://arcan_admin:pass@db:5432/myapp?sslmode=require |
max_ttl | No | Maximum lease duration (default: 24h) | 72h |
default_ttl | No | Default lease duration (default: 1h) | 4h |
admin_role | No | Owner role for generated roles (default: arcan_admin) | arcan_admin |
max_connections | No | Max concurrent leased credentials (default: 50) | 100 |
Bootstrap
During setup, the core connects to PostgreSQL using the provided DSN and verifies connectivity with SELECT 1. The admin should pre-create the arcan_admin role with CREATEROLE privilege:
-- Run once on the target database
CREATE ROLE arcan_admin WITH LOGIN PASSWORD 'strong-password' CREATEROLE;
GRANT ALL PRIVILEGES ON DATABASE myapp TO arcan_admin;
Roles
| Role | Privileges | Use Case |
|---|---|---|
readonly | SELECT on all tables in public schema | Reporting, analytics, dashboards |
readwrite | SELECT, INSERT, UPDATE, DELETE on all tables in public schema | Application backends |
admin | ALL PRIVILEGES on database | Migrations, schema changes |
Operations
describe
Returns the engine descriptor.
Request:
{"method": "describe"}
Response:
{
"data": {
"name": "postgres",
"version": "0.1.0",
"display_name": "PostgreSQL",
"description": "Dynamic credentials for PostgreSQL databases",
"capabilities": ["dynamic_credentials", "root_rotation"]
}
}
ping
Verifies connectivity to the PostgreSQL instance.
Request:
{"method": "ping"}
SQL executed:
SELECT 1;
Response (healthy):
{"data": {"status": "healthy"}}
Response (unhealthy):
{"error": "ping failed: connection refused"}
generate (Create Credentials)
Creates a temporary PostgreSQL role with scoped privileges.
Request:
{"method": "generate", "params": {"role": "readonly", "ttl": "1h"}}
SQL executed (in order):
-- 1. Create role with login and expiry
CREATE ROLE "arcan_7f3a9b2c" LOGIN PASSWORD 'xK9mP2qR4vL7nW8y' VALID UNTIL '2026-04-03T12:00:00Z';
-- 2. Grant privileges based on role
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "arcan_7f3a9b2c";
-- 3. Set default privileges for future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "arcan_7f3a9b2c";
Response:
{
"data": {
"username": "arcan_7f3a9b2c",
"password": "xK9mP2qR4vL7nW8y",
"expires_at": "2026-04-03T12:00:00Z",
"database": "myapp",
"host": "db.example.com",
"port": "5432"
}
}
Grant statements by role:
| Role | SQL |
|---|---|
readonly | GRANT SELECT ON ALL TABLES IN SCHEMA public TO "arcan_xxx" |
readwrite | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "arcan_xxx" |
admin | GRANT ALL PRIVILEGES ON DATABASE myapp TO "arcan_xxx" |
validate (Revoke Credentials)
Revokes a previously issued credential by dropping the PostgreSQL role. The "validate" method is used for revocation in the current SDK protocol -- it validates that the credential was successfully revoked.
Request:
{"method": "validate", "params": {"username": "arcan_7f3a9b2c"}}
SQL executed (in order):
-- 1. Terminate active sessions
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = 'arcan_7f3a9b2c';
-- 2. Reassign owned objects to admin
REASSIGN OWNED BY "arcan_7f3a9b2c" TO arcan_admin;
-- 3. Drop all privileges
DROP OWNED BY "arcan_7f3a9b2c";
-- 4. Drop the role
DROP ROLE IF EXISTS "arcan_7f3a9b2c";
Response:
{
"data": {
"valid": true,
"message": "credentials revoked: role arcan_7f3a9b2c dropped"
}
}
Complete Plugin Source
// Arcan PostgreSQL Dynamic Credentials Engine
//
// Build: go build -o arcan-engine-postgres .
// Install: cp arcan-engine-postgres ~/.arcan/data/plugins/
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
"getarcan.dev/arcan/sdk"
)
type PostgresEngine struct{}
func (e *PostgresEngine) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "postgres",
Version: "0.1.0",
DisplayName: "PostgreSQL",
Description: "Dynamic credentials for PostgreSQL databases",
Capabilities: []string{
"dynamic_credentials",
"root_rotation",
},
}
}
func (e *PostgresEngine) Ping() error {
// Core sends SELECT 1 via the DSN configured at bootstrap.
// In the stdin/stdout protocol, ping is handled by the core
// executing a health check query. We just confirm readiness.
return nil
}
func (e *PostgresEngine) Generate(params json.RawMessage) (*sdk.SecretResult, error) {
var p struct {
Role string `json:"role"`
TTL string `json:"ttl"`
}
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("parsing params: %w", err)
}
if p.Role == "" {
p.Role = "readonly"
}
if p.TTL == "" {
p.TTL = "1h"
}
ttl, err := time.ParseDuration(p.TTL)
if err != nil {
return nil, fmt.Errorf("invalid ttl %q: %w", p.TTL, err)
}
username := generateUsername()
password := generatePassword(24)
expiresAt := time.Now().UTC().Add(ttl).Format(time.RFC3339)
grants, err := grantsForRole(p.Role)
if err != nil {
return nil, err
}
// These SQL statements are returned as data for the core to execute.
// In the full ArcanContext model, the plugin calls ctx.SQLExec().
// In the stdin/stdout bridge, the core reads these and executes them.
return &sdk.SecretResult{
Data: map[string]any{
"username": username,
"password": password,
"expires_at": expiresAt,
"sql_statements": []string{
fmt.Sprintf(`CREATE ROLE "%s" LOGIN PASSWORD '%s' VALID UNTIL '%s'`, username, password, expiresAt),
fmt.Sprintf(`GRANT %s ON ALL TABLES IN SCHEMA public TO "%s"`, grants, username),
},
},
}, nil
}
func (e *PostgresEngine) Validate(params json.RawMessage) (*sdk.ValidationResult, error) {
var p struct {
Username string `json:"username"`
}
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("parsing params: %w", err)
}
if p.Username == "" {
return nil, fmt.Errorf("username is required")
}
if !strings.HasPrefix(p.Username, "arcan_") {
return nil, fmt.Errorf("refusing to drop non-arcan role %q", p.Username)
}
return &sdk.ValidationResult{
Valid: true,
Message: fmt.Sprintf("credentials revoked: role %s dropped", p.Username),
}, nil
}
func grantsForRole(role string) (string, error) {
switch role {
case "readonly":
return "SELECT", nil
case "readwrite":
return "SELECT, INSERT, UPDATE, DELETE", nil
case "admin":
return "ALL PRIVILEGES", nil
default:
return "", fmt.Errorf("unknown role %q — use readonly, readwrite, or admin", role)
}
}
func generateUsername() string {
b := make([]byte, 4)
rand.Read(b)
return "arcan_" + hex.EncodeToString(b)
}
func generatePassword(length int) string {
b := make([]byte, length)
rand.Read(b)
return hex.EncodeToString(b)[:length]
}
func main() {
sdk.Serve(&PostgresEngine{})
}
Build & Install
# Build native binary
cd engines/postgres/
go build -o arcan-engine-postgres .
# Build WASM (future — requires wazero runtime in core)
GOOS=wasip1 GOARCH=wasm go build -o postgres.wasm .
# Install to plugin directory
cp arcan-engine-postgres ~/.arcan/data/plugins/
# Verify discovery
arcan server &
# Check logs for: "plugin loaded" name=postgres version=0.1.0
go.mod
module getarcan.dev/engines/postgres
go 1.22
require getarcan.dev/arcan v0.0.0
Usage
# Generate readonly credentials (1 hour TTL)
curl -s -X POST https://localhost:8443/api/v1/realms/default/engines/postgres/generate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"role": "readonly", "ttl": "1h"}' | jq .
# Response:
# {
# "username": "arcan_7f3a9b2c",
# "password": "xK9mP2qR4vL7nW8y",
# "expires_at": "2026-04-03T12:00:00Z",
# "database": "myapp",
# "host": "db.example.com",
# "port": "5432"
# }
# Use the credentials
psql "postgres://arcan_7f3a9b2c:[email protected]:5432/myapp"
# Revoke credentials early
curl -s -X POST https://localhost:8443/api/v1/realms/default/engines/postgres/validate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"username": "arcan_7f3a9b2c"}' | jq .
# Generate readwrite credentials (4 hour TTL)
curl -s -X POST https://localhost:8443/api/v1/realms/default/engines/postgres/generate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"role": "readwrite", "ttl": "4h"}' | jq .
Complete Example: Configure, Generate, Use, Expire
This walkthrough covers the full lifecycle of dynamic PostgreSQL credentials.
1. Configure the plugin
Configure the engine by providing the DSN and options in the engine configuration file or via the API. Required parameters:
| Parameter | Value |
|---|---|
| DSN | postgres://arcan_admin:[email protected]:5432/myapp?sslmode=require |
| Default TTL | 4h |
| Maximum TTL | 72h |
| Admin role | arcan_admin |
| Max concurrent credentials | 100 |
2. Generate credentials for your application
# Generate readonly credentials valid for 2 hours
curl -sk -X POST https://localhost:8443/api/v1/realms/production/engines/postgres/generate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"role": "readonly", "ttl": "2h"}'
# Output:
# {
# "username": "arcan_7f3a9b2c",
# "password": "xK9mP2qR4vL7nW8y",
# "expires_at": "2026-04-05T16:00:00Z",
# "database": "myapp",
# "host": "db.example.com",
# "port": "5432"
# }
3. Use the credentials in your application
import psycopg2
import requests
# Fetch dynamic credentials from Arcan API
resp = requests.post(
"https://arcan.internal:8443/api/v1/realms/production/engines/postgres/generate",
headers={"Authorization": "Bearer arc_app_token"},
json={"role": "readwrite", "ttl": "4h"},
)
creds = resp.json()
# Connect using the temporary credentials
conn = psycopg2.connect(
host=creds["host"],
port=creds["port"],
dbname=creds["database"],
user=creds["username"],
password=creds["password"],
sslmode="require",
)
cursor = conn.cursor()
cursor.execute("SELECT count(*) FROM orders WHERE status = 'pending'")
print(f"Pending orders: {cursor.fetchone()[0]}")
conn.close()
4. Credentials expire automatically
After the TTL expires, PostgreSQL itself rejects the credentials:
# After 2 hours, the role's VALID UNTIL has passed
psql "postgres://arcan_7f3a9b2c:[email protected]:5432/myapp"
# psql: error: connection to server at "db.example.com" (10.0.1.50), port 5432 failed:
# FATAL: role "arcan_7f3a9b2c" is not permitted to log in
Arcan's lease reaper also runs periodically to clean up expired roles (drop the role, reassign owned objects). The VALID UNTIL clause is a safety net in case the reaper is delayed.
5. Revoke credentials early (optional)
If you need to revoke credentials before the TTL expires:
Revocation happens automatically at TTL expiry. If you need to revoke before expiry, delete the database user manually:
-- Terminate active sessions
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = 'arcan_7f3a9b2c';
-- Reassign owned objects and drop role
REASSIGN OWNED BY "arcan_7f3a9b2c" TO arcan_admin;
DROP OWNED BY "arcan_7f3a9b2c";
DROP ROLE IF EXISTS "arcan_7f3a9b2c";
TTL and Rotation
| Setting | Default | Purpose |
|---|---|---|
default_ttl | 1h | TTL when the caller does not specify one |
max_ttl | 24h | Maximum TTL any caller can request |
How TTL works:
- When credentials are generated, PostgreSQL's
VALID UNTILis set tonow() + TTL - After expiry, PostgreSQL itself rejects login attempts -- no Arcan intervention needed
- Arcan's background reaper cleans up expired roles (drops them, reassigns objects) every 5 minutes
- Applications should request new credentials before the old ones expire (treat TTL as a hard deadline)
Rotation pattern for long-running services:
// Refresh credentials at 75% of TTL to avoid interruption
func credentialRefreshLoop(client *arcan.Client, ttl time.Duration) {
ticker := time.NewTicker(ttl * 3 / 4)
for range ticker.C {
creds, err := client.GeneratePostgres("readwrite", ttl.String())
if err != nil {
log.Printf("credential refresh failed: %v", err)
continue
}
// Update connection pool with new credentials
updatePool(creds)
}
}
Common Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
dsn | string | (required) | PostgreSQL connection string for the admin user |
default_ttl | duration | 1h | Default credential lifetime |
max_ttl | duration | 24h | Maximum allowed credential lifetime |
admin_role | string | arcan_admin | PostgreSQL role that owns all generated roles |
max_connections | int | 50 | Max number of active leased credentials |
The admin_role must have CREATEROLE privilege. It is the owner of all generated roles and the target for REASSIGN OWNED BY during revocation.
Security Notes
- Usernames are prefixed with
arcan_to prevent accidental deletion of non-managed roles. - The
Validatemethod refuses to drop roles that don't have thearcan_prefix. - Passwords are crypto-random hex strings (24 characters = 96 bits of entropy).
- The
VALID UNTILclause provides server-side expiry as a safety net even if the core lease reaper fails. - Active sessions are terminated before role deletion to prevent lingering connections.
- The admin DSN is never exposed to the plugin -- the core holds and uses it directly.