Secrets Vault Integration
Fetch secrets from HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault — memory-only, never written to disk or logs.
Secrets Vault Integration
CoreSDK integrates directly with your secrets infrastructure so that credentials, API keys, and other sensitive values are fetched at startup and injected into the request context and policy input — without ever touching disk or appearing in logs or traces.
Key guarantees:
- Memory-only — secret values are stored in protected heap memory and are never written to disk, log sinks, or OTEL attributes.
- TLS 1.3 — all connections to vault endpoints are made over TLS 1.3; older protocol versions are rejected.
- PII masking — CoreSDK's log and trace pipeline recognises secret values and replaces them with
[REDACTED]before any export. - Request context injection — secrets are available in handler code via the request context, and as structured input to Rego policies.
SecretProvider trait
All secrets backends implement the SecretProvider trait. You can supply a custom provider by implementing it:
use coresdk_secrets::{SecretProvider, SecretValue, SecretError};
struct MySecretProvider { /* ... */ }
impl SecretProvider for MySecretProvider {
fn get(&self, path: &str) -> Result<SecretValue, SecretError> {
// fetch from your backend; SecretValue holds the plaintext in
// protected heap memory and is never written to disk
todo!()
}
}The two built-in providers are selected via the CORESDK_VAULT_TYPE environment variable:
CORESDK_VAULT_TYPE | Provider |
|---|---|
hashicorp (default) | HashiCorpVault — full implementation, ships Phase 1b |
aws | AwsSecretsManager — ships Phase 2 |
azure | AzureKeyVault — ships Phase 2 |
Key environment variables
| Variable | Description |
|---|---|
CORESDK_VAULT_TYPE | Provider selection: hashicorp, aws, or azure |
CORESDK_VAULT_ADDR | HashiCorp Vault address (e.g. https://vault.acme.internal:8200) |
CORESDK_VAULT_TOKEN | Static Vault token (use AppRole or K8s auth in production instead) |
Secret values are held in process heap memory only — they are never written to disk, logged, or included in OTEL spans.
HashiCorp Vault
AppRole authentication
AppRole is the recommended auth method for non-Kubernetes workloads. Store your role_id and
secret_id as environment variables and pass them to CoreSDK at build time.
use coresdk_engine::secrets::vault::{VaultConfig, VaultAuth};
let sdk = Engine::from_env()?
.tenant("acme")
.secrets_vault(
VaultConfig::builder()
.address("https://vault.acme.internal:8200")
.auth(VaultAuth::AppRole {
role_id: std::env::var("VAULT_ROLE_ID")?,
secret_id: std::env::var("VAULT_SECRET_ID")?,
})
.secret("db/password", "secrets.db_password")
.secret("stripe/api_key", "secrets.stripe_key")
.secret("internal/hmac_seed", "secrets.hmac_seed")
.namespace("acme/") // Vault Enterprise namespace (optional)
.build(),
)
.build()
.await?;# Configure HashiCorp Vault via environment variables — no code changes needed.
# CORESDK_VAULT_TYPE=hashicorp
# CORESDK_VAULT_ADDR=https://vault.acme.internal:8200
# VAULT_ROLE_ID and VAULT_SECRET_ID are picked up automatically (AppRole).
# Secret paths are declared in coresdk.toml or the sidecar YAML (see below).
from coresdk import CoreSDKClient, SDKConfig
_sdk = CoreSDKClient(SDKConfig(
sidecar_addr="[::1]:50051",
tenant_id="acme-corp",
fail_mode="open",
))import (
"os"
"github.com/coresdk/sdk"
"github.com/coresdk/sdk/secrets/vault"
)
cfg := sdk.Config{
Tenant: "acme",
SecretsVault: &vault.Config{
Address: "https://vault.acme.internal:8200",
Auth: vault.AppRoleAuth{
RoleID: os.Getenv("VAULT_ROLE_ID"),
SecretID: os.Getenv("VAULT_SECRET_ID"),
},
Secrets: map[string]string{
"db/password": "secrets.db_password",
"stripe/api_key": "secrets.stripe_key",
"internal/hmac_seed": "secrets.hmac_seed",
},
Namespace: "acme/", // Vault Enterprise namespace (optional)
},
}
client, err := sdk.New(cfg)import { CoreSDK } from "@coresdk/sdk";
import { VaultConfig, AppRoleAuth } from "@coresdk/sdk/secrets/vault";
const sdk = new CoreSDK({
tenant: "acme",
secretsVault: new VaultConfig({
address: "https://vault.acme.internal:8200",
auth: new AppRoleAuth({
roleId: process.env.VAULT_ROLE_ID!,
secretId: process.env.VAULT_SECRET_ID!,
}),
secrets: {
"db/password": "secrets.db_password",
"stripe/api_key": "secrets.stripe_key",
"internal/hmac_seed": "secrets.hmac_seed",
},
namespace: "acme/", // Vault Enterprise namespace (optional)
}),
});The second argument to .secret() / the map value is the dot-path at which the secret is
available in the request context and Rego input (see Accessing secrets below).
Kubernetes auth
When running inside a Kubernetes cluster, use the Kubernetes auth method instead of AppRole. CoreSDK reads the pod's service account token from the standard mount path automatically.
use coresdk_engine::secrets::vault::{VaultConfig, VaultAuth};
let sdk = Engine::from_env()?
.tenant("acme")
.secrets_vault(
VaultConfig::builder()
.address("https://vault.acme.internal:8200")
.auth(VaultAuth::Kubernetes {
role: "orders-service",
// token_path defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
token_path: None,
})
.secret("db/password", "secrets.db_password")
.secret("stripe/api_key", "secrets.stripe_key")
.build(),
)
.build()
.await?;# Kubernetes auth — set environment variables; sidecar handles token exchange automatically.
# CORESDK_VAULT_TYPE=hashicorp
# CORESDK_VAULT_ADDR=https://vault.acme.internal:8200
# CORESDK_VAULT_K8S_ROLE=orders-service
# Token path defaults to /var/run/secrets/kubernetes.io/serviceaccount/tokencfg := sdk.Config{
Tenant: "acme",
SecretsVault: &vault.Config{
Address: "https://vault.acme.internal:8200",
Auth: vault.KubernetesAuth{
Role: "orders-service",
// TokenPath defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
},
Secrets: map[string]string{
"db/password": "secrets.db_password",
"stripe/api_key": "secrets.stripe_key",
},
},
}import { KubernetesAuth } from "@coresdk/sdk/secrets/vault";
const sdk = new CoreSDK({
tenant: "acme",
secretsVault: new VaultConfig({
address: "https://vault.acme.internal:8200",
auth: new KubernetesAuth({
role: "orders-service",
// tokenPath defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
}),
secrets: {
"db/password": "secrets.db_password",
"stripe/api_key": "secrets.stripe_key",
},
}),
});Sidecar YAML (HashiCorp Vault)
# coresdk-sidecar.yaml
tenant: acme
secrets:
vault:
address: https://vault.acme.internal:8200
namespace: acme/ # Vault Enterprise only — omit for OSS
# AppRole auth
auth:
method: approle
role_id: ${VAULT_ROLE_ID}
secret_id: ${VAULT_SECRET_ID}
# Kubernetes auth (alternative — comment out approle block above)
# auth:
# method: kubernetes
# role: orders-service
secrets:
- path: db/password
as: secrets.db_password
- path: stripe/api_key
as: secrets.stripe_key
- path: internal/hmac_seed
as: secrets.hmac_seedAWS Secrets Manager
Phase 2.
AwsSecretsManagerships Phase 2. SetCORESDK_VAULT_TYPE=awsto opt in when available.
CoreSDK authenticates to AWS Secrets Manager using the standard AWS credential chain: environment
variables, ~/.aws/credentials, EC2 instance profile, ECS task role, or EKS IRSA — in that order.
No additional auth configuration is required if your workload already has an IAM role attached.
use coresdk_engine::secrets::aws::AwsSecretsConfig;
let sdk = Engine::from_env()?
.tenant("acme")
.secrets_aws(
AwsSecretsConfig::builder()
.region("us-east-1")
.secret("acme/prod/db_password", "secrets.db_password")
.secret("acme/prod/stripe_api_key", "secrets.stripe_key")
.secret("acme/prod/hmac_seed", "secrets.hmac_seed")
// Optional: pin a specific version stage
.version_stage("AWSCURRENT")
.build(),
)
.build()
.await?;# AWS Secrets Manager ships Phase 2.
# Set CORESDK_VAULT_TYPE=aws and AWS_REGION; uses standard AWS credential chain.import "github.com/coresdk/sdk/secrets/aws"
cfg := sdk.Config{
Tenant: "acme",
SecretsAWS: &aws.SecretsConfig{
Region: "us-east-1",
Secrets: map[string]string{
"acme/prod/db_password": "secrets.db_password",
"acme/prod/stripe_api_key": "secrets.stripe_key",
"acme/prod/hmac_seed": "secrets.hmac_seed",
},
VersionStage: "AWSCURRENT", // optional
},
}import { AwsSecretsConfig } from "@coresdk/sdk/secrets/aws";
const sdk = new CoreSDK({
tenant: "acme",
secretsAws: new AwsSecretsConfig({
region: "us-east-1",
secrets: {
"acme/prod/db_password": "secrets.db_password",
"acme/prod/stripe_api_key": "secrets.stripe_key",
"acme/prod/hmac_seed": "secrets.hmac_seed",
},
versionStage: "AWSCURRENT", // optional
}),
});Secrets stored as JSON objects in Secrets Manager are automatically unpacked. For example, if
acme/prod/db contains {"host":"db.acme.internal","password":"s3cr3t"}, you can map individual
keys:
AwsSecretsConfig::builder()
.region("us-east-1")
.secret_key("acme/prod/db", "host", "secrets.db_host")
.secret_key("acme/prod/db", "password", "secrets.db_password")
.build()# AWS JSON key extraction ships Phase 2.&aws.SecretsConfig{
Region: "us-east-1",
SecretKeys: []aws.SecretKey{
{Name: "acme/prod/db", Key: "host", As: "secrets.db_host"},
{Name: "acme/prod/db", Key: "password", As: "secrets.db_password"},
},
}new AwsSecretsConfig({
region: "us-east-1",
secretKeys: [
{ name: "acme/prod/db", key: "host", as: "secrets.db_host" },
{ name: "acme/prod/db", key: "password", as: "secrets.db_password" },
],
})Sidecar YAML (AWS Secrets Manager)
# coresdk-sidecar.yaml
tenant: acme
secrets:
aws:
region: us-east-1
version_stage: AWSCURRENT # optional
secrets:
- name: acme/prod/db_password
as: secrets.db_password
- name: acme/prod/stripe_api_key
as: secrets.stripe_key
# JSON object key extraction (optional)
secret_keys:
- name: acme/prod/db
key: host
as: secrets.db_host
- name: acme/prod/db
key: password
as: secrets.db_passwordAzure Key Vault
Phase 2.
AzureKeyVaultintegration ships Phase 2.
CoreSDK authenticates to Azure Key Vault using DefaultAzureCredential, which resolves in order:
environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET), workload
identity, managed identity, and the Azure CLI. No extra configuration is needed when running on
Azure with a managed identity.
use coresdk_engine::secrets::azure::AzureKeyVaultConfig;
let sdk = Engine::from_env()?
.tenant("acme")
.secrets_azure(
AzureKeyVaultConfig::builder()
.vault_url("https://acme-prod.vault.azure.net/")
.secret("db-password", "secrets.db_password")
.secret("stripe-api-key", "secrets.stripe_key")
.secret("hmac-seed", "secrets.hmac_seed")
// Optional: pin a specific secret version
// .secret_version("db-password", "3d2a1bc4e5f6...", "secrets.db_password")
.build(),
)
.build()
.await?;# Azure Key Vault ships Phase 2.
# Set CORESDK_VAULT_TYPE=azure and AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET.import "github.com/coresdk/sdk/secrets/azure"
cfg := sdk.Config{
Tenant: "acme",
SecretsAzure: &azure.KeyVaultConfig{
VaultURL: "https://acme-prod.vault.azure.net/",
Secrets: map[string]string{
"db-password": "secrets.db_password",
"stripe-api-key": "secrets.stripe_key",
"hmac-seed": "secrets.hmac_seed",
},
// SecretVersions: map[string]string{"db-password": "3d2a1bc4e5f6..."},
},
}import { AzureKeyVaultConfig } from "@coresdk/sdk/secrets/azure";
const sdk = new CoreSDK({
tenant: "acme",
secretsAzure: new AzureKeyVaultConfig({
vaultUrl: "https://acme-prod.vault.azure.net/",
secrets: {
"db-password": "secrets.db_password",
"stripe-api-key": "secrets.stripe_key",
"hmac-seed": "secrets.hmac_seed",
},
// secretVersions: { "db-password": "3d2a1bc4e5f6..." },
}),
});Sidecar YAML (Azure Key Vault)
# coresdk-sidecar.yaml
tenant: acme
secrets:
azure:
vault_url: https://acme-prod.vault.azure.net/
secrets:
- name: db-password
as: secrets.db_password
- name: stripe-api-key
as: secrets.stripe_key
- name: hmac-seed
as: secrets.hmac_seed
# Pin a specific version (optional)
# secret_versions:
# db-password: 3d2a1bc4e5f6...Accessing secrets
Once configured, secrets are available in your handler code through the request context object that CoreSDK populates for every inbound request.
use coresdk_engine::RequestContext;
use axum::{Extension, Json};
async fn create_order(
Extension(ctx): Extension<RequestContext>,
Json(payload): Json<CreateOrderRequest>,
) -> Result<Json<Order>, AppError> {
// Retrieve a secret value — returns None if not configured
let stripe_key = ctx.secrets.get("secrets.stripe_key")
.ok_or(AppError::MissingSecret)?;
let db_password = ctx.secrets.get("secrets.db_password")
.ok_or(AppError::MissingSecret)?;
// Use the values — they are plain &str, never logged
let charge = stripe::charge(stripe_key, payload.amount).await?;
let conn = db::connect("db.acme.internal", db_password).await?;
// ...
Ok(Json(order))
}from fastapi import Request
async def create_order(request: Request, payload: CreateOrderRequest):
# Secret values injected by the sidecar are available on request.state
ctx = getattr(request.state, "coresdk_ctx", {})
stripe_key = ctx.get("secrets.stripe_key")
db_password = ctx.get("secrets.db_password")
if not stripe_key or not db_password:
raise MissingSecretError()
# Use the values — they are plain strings, never logged
charge = await stripe.charge(stripe_key, payload.amount)
conn = await db.connect("db.acme.internal", db_password)
return orderimport (
"net/http"
"github.com/coresdk/sdk"
)
func CreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := sdk.ContextFrom(r.Context())
// Retrieve a secret value — returns "", false if not configured
stripeKey, ok := ctx.Secrets.Get("secrets.stripe_key")
if !ok {
http.Error(w, "missing secret", http.StatusInternalServerError)
return
}
dbPassword, ok := ctx.Secrets.Get("secrets.db_password")
if !ok {
http.Error(w, "missing secret", http.StatusInternalServerError)
return
}
// Use the values — they are plain strings, never logged
charge, err := stripe.Charge(stripeKey, payload.Amount)
conn, err := db.Connect("db.acme.internal", dbPassword)
// ...
}import { getRequestContext } from "@coresdk/sdk";
import type { Request, Response } from "express";
export async function createOrder(req: Request, res: Response) {
const ctx = getRequestContext(req);
// Retrieve a secret value — returns undefined if not configured
const stripeKey = ctx.secrets.get("secrets.stripe_key");
const dbPassword = ctx.secrets.get("secrets.db_password");
if (!stripeKey || !dbPassword) {
res.status(500).json({ error: "missing secret" });
return;
}
// Use the values — they are plain strings, never logged
const charge = await stripe.charge(stripeKey, req.body.amount);
const conn = await db.connect("db.acme.internal", dbPassword);
// ...
}Secret accessor safety
ctx.secrets.get() always returns the live value as of the last rotation cycle. CoreSDK refreshes
secrets in the background on a configurable interval (default: 15 minutes) and swaps the in-memory
value atomically. Your handler always reads the current value without restarting.
Using secrets in Rego policies
Every secret is injected into the Rego policy input object under its configured dot-path. This
lets policies make decisions that depend on secret-backed configuration without any application
code changes.
Given the configuration above, the Rego input document looks like:
{
"user": { "id": "usr_2xK9", "role": "member", "tenant_id": "acme" },
"request": { "method": "POST", "path": "/orders", "resource": "orders" },
"secrets": {
"db_password": "[REDACTED]",
"stripe_key": "[REDACTED]",
"hmac_seed": "[REDACTED]"
}
}Secret values in the policy input are always "[REDACTED]" in the OTEL trace and log record, but
the actual plaintext value is present inside the Rego evaluation context and can be used in policy
logic. For example, to gate access based on a feature flag stored as a secret:
package acme.orders
import future.keywords.if
default allow = false
# Allow the request if the shared HMAC seed is configured (non-empty)
allow if {
input.secrets.hmac_seed != ""
input.user.role == "member"
input.request.method == "POST"
}To use a secret value to validate a caller-supplied token in policy:
package acme.webhooks
import future.keywords.if
import future.keywords.in
default allow = false
# Validate the X-Webhook-Secret header against the vault-backed value
allow if {
provided := input.request.headers["x-webhook-secret"]
expected := input.secrets.hmac_seed
provided == expected
}Note: Rego policy evaluation happens entirely inside the CoreSDK Rust core — secret values never leave the process during policy evaluation.
Secret rotation
CoreSDK polls for updated secret values on a background timer. The default interval is 15 minutes. You can tune it per provider:
VaultConfig::builder()
.address("https://vault.acme.internal:8200")
.auth(VaultAuth::Kubernetes { role: "orders-service", token_path: None })
.secret("db/password", "secrets.db_password")
.refresh_interval(std::time::Duration::from_secs(300)) // 5 minutes
.build()VaultConfig(
address="https://vault.acme.internal:8200",
auth=KubernetesAuth(role="orders-service"),
secrets={"db/password": "secrets.db_password"},
refresh_interval=300, # seconds
)&vault.Config{
Address: "https://vault.acme.internal:8200",
Auth: vault.KubernetesAuth{Role: "orders-service"},
Secrets: map[string]string{"db/password": "secrets.db_password"},
RefreshInterval: 5 * time.Minute,
}new VaultConfig({
address: "https://vault.acme.internal:8200",
auth: new KubernetesAuth({ role: "orders-service" }),
secrets: { "db/password": "secrets.db_password" },
refreshIntervalSeconds: 300,
})Sidecar equivalent:
secrets:
vault:
address: https://vault.acme.internal:8200
auth:
method: kubernetes
role: orders-service
refresh_interval: 300 # seconds (default: 900)
secrets:
- path: db/password
as: secrets.db_passwordSecurity properties
| Property | Detail |
|---|---|
| Transport | TLS 1.3 enforced; TLS 1.2 and below are rejected |
| Storage | Values held in process heap only — no disk writes, no swap |
| Logging | Secret values are replaced with [REDACTED] before any log or trace export |
| Policy evaluation | Values are accessible inside Rego but are not serialised to external sinks |
| Rotation | Background refresh with atomic in-memory swap; zero downtime |
| Startup failure | If a configured secret cannot be fetched at startup, CoreSDK refuses to start and logs the secret path (not the value) |
Next steps
- Rego policy authoring — write policies that reference
input.secrets - OpenTelemetry — understand what is and is not exported in spans
- Configuration reference — full list of secrets options
- Deployment overview — environment variables, sidecar setup, and Helm chart
Self-Hosted Control Plane
Run the CoreSDK control plane on your own infrastructure for full data sovereignty and enterprise compliance.
Pluggable Caching Layer
Configure in-memory and Redis cache adapters for JWKS, policy results, tenant context, and config — with HMAC-SHA256 integrity and full OTel metrics.