Kubernetes integration
Flokk lets your Kubernetes pods connect to PostgreSQL without stored passwords. Pods present a projected service account token; Flokk exchanges it for short-lived database credentials.
Available on Pro and Dedicated tiers.
How it works
- Register your K8s cluster's OIDC issuer in the Flokk dashboard
- Your pod mounts a projected SA token with audience
https://api.flokk.dev - The pod calls the Flokk API with the token
- Flokk verifies the token, generates a short-lived PG password (1 hour TTL)
- The pod connects to PostgreSQL with the fresh credentials
- Before expiry, the pod refreshes — existing connections are not affected
1. Register your OIDC issuer
Go to your database → K8s auth tab. Enter your cluster's OIDC issuer URL and the service accounts you want to allow.
The issuer URL is typically:
- EKS:
https://oidc.eks.{region}.amazonaws.com/id/{id} - GKE:
https://container.googleapis.com/v1/projects/{p}/locations/{z}/clusters/{c} - Self-hosted: your API server's
--service-account-issuerflag value
2. Configure your pod
apiVersion: v1 kind: Pod spec: serviceAccountName: db-access containers: - name: app volumeMounts: - name: db-token mountPath: /var/run/secrets/db-token readOnly: true volumes: - name: db-token projected: sources: - serviceAccountToken: audience: "https://api.flokk.dev" expirationSeconds: 3600 path: token
3. Fetch credentials
JSON (full details)
TOKEN=$(cat /var/run/secrets/db-token/token) curl -s -H "Authorization: Bearer $TOKEN" \ https://api.flokk.dev/api/v1/databases/DB_ID/credentials/k8s
Returns:
{
"host": "mydb.db.flokk.dev",
"port": 5432,
"database": "mydb-a7f3e2",
"user": "flokk_mydb-a7f3e2_k8s",
"password": "flk_tmp_...",
"sslmode": "require",
"expires_at": "2026-04-12T16:30:00Z",
"refresh_at": "2026-04-12T16:20:00Z",
"connection_uri": "postgresql://..."
}
Plain connection string
DATABASE_URL=$(curl -s -H "Authorization: Bearer $TOKEN" \ https://api.flokk.dev/api/v1/databases/DB_ID/connect/k8s) psql "$DATABASE_URL"
4. Connect with automatic rotation
Credentials expire after 1 hour. The best way to handle this is using your PG driver's password callback — the driver calls your function whenever it needs to open a new connection, so credentials are always fresh. No background thread needed.
Go (pgx)
package main import ( "context" "encoding/json" "net/http" "os" "sync" "time" "github.com/jackc/pgx/v5/pgxpool" ) type flokkCreds struct { mu sync.Mutex user string password string expires time.Time dbID string } func (c *flokkCreds) get(ctx context.Context) (string, string, error) { c.mu.Lock() defer c.mu.Unlock() // Return cached if still valid (with 5-min buffer) if time.Until(c.expires) > 5*time.Minute { return c.user, c.password, nil } // Fetch fresh credentials from Flokk API token, _ := os.ReadFile("/var/run/secrets/db-token/token") req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.flokk.dev/api/v1/databases/"+c.dbID+"/credentials/k8s", nil) req.Header.Set("Authorization", "Bearer "+string(token)) resp, err := http.DefaultClient.Do(req) if err != nil { return "", "", err } defer resp.Body.Close() var creds struct { User string `json:"user"` Password string `json:"password"` ExpiresAt time.Time `json:"expires_at"` } json.NewDecoder(resp.Body).Decode(&creds) c.user = creds.User c.password = creds.Password c.expires = creds.ExpiresAt return c.user, c.password, nil } func main() { creds := &flokkCreds{dbID: "YOUR_DB_ID"} cfg, _ := pgxpool.ParseConfig("host=mydb.db.flokk.dev port=5432 dbname=mydb sslmode=require") cfg.BeforeConnect = func(ctx context.Context, cc *pgx.ConnConfig) error { // Called every time the pool opens a new connection. // Credentials are cached and only refreshed when near expiry. user, pass, err := creds.get(ctx) if err != nil { return err } cc.User = user cc.Password = pass return nil } pool, _ := pgxpool.NewWithConfig(context.Background(), cfg) // Use pool — credentials rotate automatically on new connections }
Python (psycopg3)
import requests, time, psycopg DB_ID = "YOUR_DB_ID" _cache = {"user": "", "password": "", "expires": 0} def get_password(): # Return cached if still valid (5-min buffer) if time.time() < _cache["expires"] - 300: return _cache["password"] token = open("/var/run/secrets/db-token/token").read() r = requests.get( f"https://api.flokk.dev/api/v1/databases/{DB_ID}/credentials/k8s", headers={"Authorization": f"Bearer {token}"} ) creds = r.json() _cache["user"] = creds["user"] _cache["password"] = creds["password"] _cache["expires"] = creds["expires_at"] # parse as needed return _cache["password"] # psycopg3: password can be a callable — called on every new connection conn = psycopg.connect( host="mydb.db.flokk.dev", dbname="mydb-a7f3e2", user=get_password, # callable, not a string password=get_password, sslmode="require", )
Node.js (pg)
import pg from 'pg'; import fs from 'fs'; const DB_ID = 'YOUR_DB_ID'; let cached = { user: '', password: '', expiresAt: 0 }; async function getCreds() { if (Date.now() < cached.expiresAt - 300_000) return cached; const token = fs.readFileSync('/var/run/secrets/db-token/token', 'utf8'); const r = await fetch( `https://api.flokk.dev/api/v1/databases/${DB_ID}/credentials/k8s`, { headers: { Authorization: `Bearer ${token}` } } ); const creds = await r.json(); cached = { ...creds, expiresAt: new Date(creds.expires_at).getTime() }; return cached; } // pg Pool: override password on each new connection const pool = new pg.Pool({ host: 'mydb.db.flokk.dev', database: 'mydb-a7f3e2', ssl: { rejectUnauthorized: false }, }); pool.on('connect', async (client) => { const c = await getCreds(); client.user = c.user; client.password = c.password; });
Shell (one-liner)
# For scripts, just fetch fresh on every invocation: psql "$(curl -s -H "Authorization: Bearer $(cat /var/run/secrets/db-token/token)" \ https://api.flokk.dev/api/v1/databases/DB_ID/connect/k8s)"
How rotation works under the hood
- Your app calls the Flokk API (via the password callback or explicitly)
- Flokk generates a random password and runs
ALTER ROLE ... PASSWORD ... VALID UNTIL now()+1h - The SCRAM-SHA-256 hash is stored in PostgreSQL's
pg_authidcatalog - PgBouncer picks up the new hash via
auth_queryon its next connection to PG — no restart needed - After 1 hour, PostgreSQL rejects the old password. Your driver's callback fetches a fresh one.
- Existing open connections are not affected — PG only checks the password at connection time
Security model
| Password lifetime | 1 hour (VALID UNTIL) |
| Password storage | SCRAM-SHA-256 hash in pg_authid |
| PgBouncer auth | auth_query against pg_shadow (dynamic) |
| Open connections | Not affected by rotation |
| K8s role | Inherits owner role via GRANT |
| Audit | Every credential exchange is logged |