Secrets Management DevOps Guide
Secrets Management DevOps Guide (Docker, Kubernetes, GitHub)
This guide explains how to run and consume the CRUDService secrets engine across Docker and Kubernetes, and how to automate secret reads/rotations from CI (GitHub Actions). It is derived from the production code paths and supported providers.
What this implements
- Providers:
yaml(dev-only),openbao+kv2,hashicorp+kv2via hvac - Canonical URIs:
<provider>[+engine]://<mount>/<path>#<fragment>?<params> - Endpoints: CRUD, rotate, versions/metadata, bulk, copy/move, SSE events
- PEP: VaultService enforces PDP decisions, audience/scope, sender-binding, anti-replay
Key implementation references:
- Rotation endpoint and PDP/scope enforcement
CRUDService/src/api/secrets_routes.py→ POST/api/secrets/rotate
- Rotation controller (KVv2): write new version + best-effort old/new lookup + Kafka audit
CRUDService/src/services/rotation_controller.py
- Provider strategies: OpenBao/HashiCorp KVv2 via hvac
CRUDService/src/vault_strategies/openbao_vault_strategy.py
- Canonical URI parser and tenant mount guard
CRUDService/src/secret_uri.py
- System mapping to APIs (internal service-to-service use)
ServiceConfigs/CRUDService/config/systems/secrets.yaml
1) Configuration model
Global env (PEP, policy, audits)
TENANT_ID(default:dev)TENANT_ALLOWED_MOUNTS(CSV, e.g.secret,kv) – tenant mount guardSECRETS_AUDIENCE(default:crud.secrets) – expected audience whenaudpresentSECRETS_API_REQUIRE_AUTH(default:false) – require session/bearer on/api/secrets/*SECRETS_ENFORCE_SCOPES(default:false) – enforce per-endpoint scopesTENANT_SALTorSECRET_TENANT_SALT– HMAC salt to compute non‑leakyresource_refin auditsENVIRONMENT– non‑dev disables YAML writes/deletes
Optional fallback toggles (not recommended for prod):
VAULT_ENABLE_FALLBACK,VAULT_FALLBACK_ORDER– provider fallback when a secret/path is missing
Provider configuration (Vault-compatible)
For openbao or hashicorp (KVv2):
- Required keys (env):
OPENBAO_URL/HASHICORP_URL(orVAULT_URL), and eitherOPENBAO_TOKEN/HASHICORP_TOKEN(orVAULT_TOKEN) or AppRole withOPENBAO_ROLE_ID+OPENBAO_SECRET_ID(analogous for HashiCorp) - Optional:
VAULT_TIMEOUT,VAULT_VERIFY_SSL(true/false),VAULT_POOL_SIZE,VAULT_TOKEN_RENEWAL_THRESHOLD,VAULT_MAX_CONCURRENT_OPERATIONS
Dev YAML provider:
YAML_VAULT_PATH– path to dev secrets YAML (e.g.,/app/config/data/dev_secrets.yaml)
2) Docker Compose
Dev (YAML provider)
services:
crudservice:
image: your-registry/crudservice:latest
environment:
ENVIRONMENT: dev
YAML_VAULT_PATH: /app/config/data/dev_secrets.yaml
TENANT_ID: dev
TENANT_ALLOWED_MOUNTS: secret
SECRETS_API_REQUIRE_AUTH: "false"
SECRETS_ENFORCE_SCOPES: "false"
# Optional auditing salt
# TENANT_SALT: "..."
volumes:
- ../ServiceConfigs/CRUDService/config/data/dev_secrets.yaml:/app/config/data/dev_secrets.yaml:ro
ports:
- "9100:9100"
Populate YAML quickly from local .env files using the bootstrap script (dev only):
python scripts/yaml_dev_bootstrap.py --manifest scripts/bootstrap_manifest.yaml --out ..\ServiceConfigs\CRUDService\config\data\dev_secrets.yaml --mount secret --clear
Prod (OpenBao KVv2)
services:
crudservice:
image: your-registry/crudservice:latest
environment:
ENVIRONMENT: prod
TENANT_ID: acme
TENANT_ALLOWED_MOUNTS: secret
SECRETS_API_REQUIRE_AUTH: "true"
SECRETS_ENFORCE_SCOPES: "true"
SECRETS_AUDIENCE: crud.secrets
TENANT_SALT: ${TENANT_SALT}
# Vault-compatible provider (OpenBao shown)
OPENBAO_URL: https://openbao.acme.local
OPENBAO_TOKEN: ${OPENBAO_TOKEN} # or use AppRole below
# OPENBAO_ROLE_ID: ${OPENBAO_ROLE_ID}
# OPENBAO_SECRET_ID: ${OPENBAO_SECRET_ID}
VAULT_TIMEOUT: 30
VAULT_VERIFY_SSL: "true"
VAULT_POOL_SIZE: 10
VAULT_TOKEN_RENEWAL_THRESHOLD: 600
VAULT_MAX_CONCURRENT_OPERATIONS: 50
ports:
- "9100:9100"
Notes:
- The service reads secrets provider credentials from environment variables (no
_FILEconvention). Inject them through your secret management for Compose deployments. - For HashiCorp Vault, replace
OPENBAO_*withHASHICORP_*(same keys) or useVAULT_*fallbacks where applicable.
3) Kubernetes
Use a Secret for tokens/AppRole, and a ConfigMap (or inline env) for non‑sensitive settings.
apiVersion: v1
kind: Secret
metadata:
name: crud-secrets-provider
type: Opaque
stringData:
OPENBAO_TOKEN: "${OPENBAO_TOKEN}"
# Or AppRole:
# OPENBAO_ROLE_ID: "..."
# OPENBAO_SECRET_ID: "..."
---
apiVersion: v1
kind: ConfigMap
metadata:
name: crud-config
data:
ENVIRONMENT: "prod"
TENANT_ID: "acme"
TENANT_ALLOWED_MOUNTS: "secret"
SECRETS_API_REQUIRE_AUTH: "true"
SECRETS_ENFORCE_SCOPES: "true"
SECRETS_AUDIENCE: "crud.secrets"
VAULT_VERIFY_SSL: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: crudservice
spec:
replicas: 2
selector:
matchLabels:
app: crudservice
template:
metadata:
labels:
app: crudservice
spec:
containers:
- name: crudservice
image: your-registry/crudservice:latest
ports:
- containerPort: 9100
env:
- name: ENVIRONMENT
valueFrom: { configMapKeyRef: { name: crud-config, key: ENVIRONMENT } }
- name: TENANT_ID
valueFrom: { configMapKeyRef: { name: crud-config, key: TENANT_ID } }
- name: TENANT_ALLOWED_MOUNTS
valueFrom: { configMapKeyRef: { name: crud-config, key: TENANT_ALLOWED_MOUNTS } }
- name: SECRETS_API_REQUIRE_AUTH
valueFrom: { configMapKeyRef: { name: crud-config, key: SECRETS_API_REQUIRE_AUTH } }
- name: SECRETS_ENFORCE_SCOPES
valueFrom: { configMapKeyRef: { name: crud-config, key: SECRETS_ENFORCE_SCOPES } }
- name: SECRETS_AUDIENCE
valueFrom: { configMapKeyRef: { name: crud-config, key: SECRETS_AUDIENCE } }
- name: OPENBAO_URL
value: "https://openbao.acme.local"
- name: OPENBAO_TOKEN
valueFrom: { secretKeyRef: { name: crud-secrets-provider, key: OPENBAO_TOKEN } }
# Or AppRole:
# - name: OPENBAO_ROLE_ID
# valueFrom: { secretKeyRef: { name: crud-secrets-provider, key: OPENBAO_ROLE_ID } }
# - name: OPENBAO_SECRET_ID
# valueFrom: { secretKeyRef: { name: crud-secrets-provider, key: OPENBAO_SECRET_ID } }
- name: VAULT_TIMEOUT
value: "30"
- name: VAULT_VERIFY_SSL
valueFrom: { configMapKeyRef: { name: crud-config, key: VAULT_VERIFY_SSL } }
- name: VAULT_POOL_SIZE
value: "10"
- name: VAULT_TOKEN_RENEWAL_THRESHOLD
value: "600"
- name: VAULT_MAX_CONCURRENT_OPERATIONS
value: "50"
Dev on Kubernetes with YAML provider (not recommended beyond local clusters): mount a ConfigMap with dev_secrets.yaml and set YAML_VAULT_PATH accordingly.
4) CI automation (GitHub Actions)
The API supports reads and rotations with PDP/scope enforcement. When SECRETS_API_REQUIRE_AUTH=true and SECRETS_ENFORCE_SCOPES=true, the access token must carry:
- Audience:
crud.secrets(or yourSECRETS_AUDIENCE) - Scopes: e.g.,
secrets.read,secrets.rotate, etc.
Example: rotate a KVv2 secret fragment via GitHub Actions.
name: Rotate secret
on:
workflow_dispatch:
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- name: Call CRUDService rotate (OpenBao KVv2)
env:
API_BASE: ${{ secrets.CRUD_BASE_URL }} # e.g., https://crud.yourdomain
TOKEN: ${{ secrets.CRUD_BEARER_TOKEN }} # must include audience/scope
run: |
uri="openbao+kv2://secret/app/api#token"
body=$(jq -nc --arg u "$uri" --argjson v '{"token":"new-value"}' '{uri:$u, value:$v}')
curl -sS -X POST "$API_BASE/api/secrets/rotate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data "$body"
Read a pinned version:
- name: Read versioned value
env:
API_BASE: ${{ secrets.CRUD_BASE_URL }}
TOKEN: ${{ secrets.CRUD_BEARER_TOKEN }}
run: |
uri="openbao+kv2://secret/app/api#token&version=12"
curl -sS "$API_BASE/api/secrets/value?uri=$uri" -H "Authorization: Bearer $TOKEN"
Bulk operations (set/delete/undelete/destroy/rotate) are available via POST /api/secrets/bulk.
5) Supported operations and examples
All operations use Canonical URIs. For KVv2, fragments represent individual keys within the secret object; without a fragment, payloads must be JSON objects.
- Read (provider-backed):
curl "$CRUD/api/secrets/value?uri=openbao+kv2://secret/app/api#token" -H "Authorization: Bearer $TOKEN"
- Rotate (YAML dev behaves like update):
curl -X POST "$CRUD/api/secrets/rotate" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
"uri": "openbao+kv2://secret/app/api#token",
"value": {"token":"s3cr3t-new"}
}'
- Versions metadata:
curl "$CRUD/api/secrets/versions?uri=openbao+kv2://secret/app/api" -H "Authorization: Bearer $TOKEN"
- Undelete versions:
curl -X POST "$CRUD/api/secrets/undelete" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
"uri": "openbao+kv2://secret/app/api",
"versions": [3,4]
}'
- Destroy versions:
curl -X POST "$CRUD/api/secrets/destroy-versions" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
"uri": "openbao+kv2://secret/app/api",
"versions": [1,2]
}'
6) Security guidance
- Enable
SECRETS_API_REQUIRE_AUTHandSECRETS_ENFORCE_SCOPESin production - Prefer AppRole (role_id/secret_id) over static tokens for Vault
- Keep
VAULT_VERIFY_SSL=true; only disable for controlled dev clusters - Restrict
TENANT_ALLOWED_MOUNTSto the precise set used per tenant - Set
TENANT_SALTto enable non‑leaky audit references in Kafka and SSE - YAML provider is dev‑only; it is automatically blocked for writes/deletes when
ENVIRONMENTis not dev/test
7) Troubleshooting
- 403 on rotate/read: missing scope or PDP denial; ensure token scopes and PDP rules allow the operation
- 400
UNSUPPORTED_ENGINE: canonical URI engine not supported (KVv2 required for Vault providers) - 400
TENANT_MOUNT_MISMATCH: mount not inTENANT_ALLOWED_MOUNTS - 404 on fragment: path exists but key is absent – check fragment vs object payload
- Token renewal failures: check provider token/AppRole and network access; hvac renewal is automatic before reads/writes
- Version errors on pinned reads: deleted/destroyed versions raise explicit errors; omit
versionto read latest
8) Internal service mapping
Internal components can call the secrets API using the system mapping at ServiceConfigs/CRUDService/config/systems/secrets.yaml (inject INTERNAL_EXECUTION_TOKEN). This mapping exposes value, create_or_update, delete, destroy, rotate, bulk, copy, move, and metadata operations that forward to the HTTP endpoints.