Skip to main content
CoreSDK
Deployment

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_TYPEProvider
hashicorp (default)HashiCorpVault — full implementation, ships Phase 1b
awsAwsSecretsManager — ships Phase 2
azureAzureKeyVault — ships Phase 2

Key environment variables

VariableDescription
CORESDK_VAULT_TYPEProvider selection: hashicorp, aws, or azure
CORESDK_VAULT_ADDRHashiCorp Vault address (e.g. https://vault.acme.internal:8200)
CORESDK_VAULT_TOKENStatic 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/token
cfg := 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_seed

AWS Secrets Manager

Phase 2. AwsSecretsManager ships Phase 2. Set CORESDK_VAULT_TYPE=aws to 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_password

Azure Key Vault

Phase 2. AzureKeyVault integration 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 order
import (
    "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_password

Security properties

PropertyDetail
TransportTLS 1.3 enforced; TLS 1.2 and below are rejected
StorageValues held in process heap only — no disk writes, no swap
LoggingSecret values are replaced with [REDACTED] before any log or trace export
Policy evaluationValues are accessible inside Rego but are not serialised to external sinks
RotationBackground refresh with atomic in-memory swap; zero downtime
Startup failureIf a configured secret cannot be fetched at startup, CoreSDK refuses to start and logs the secret path (not the value)

Next steps

On this page