Credential Store

Grove includes a built-in credential store for managing secrets such as API keys, database passwords, encryption keys, and webhook signing secrets. The credential store provides encrypted storage, versioning, rotation support, and stage-based labels so that secrets can be promoted through environments.

Prerequisites: Familiarity with Grove project structure, Configuration Reference. What you'll learn: How to store, retrieve, version, and rotate secrets in a Grove application.


Overview

The credential store is an encrypted key-value store that lives alongside your Grove application. Secrets are:

Storing Secrets

Secrets are managed through the Grove CLI:

# Set a secret
manzano credential set \
  --module order \
  --name stripe_api_key \
  --value "sk_live_abc123..."

# Set a secret from a file
manzano credential set \
  --module order \
  --name tls_certificate \
  --file ./certs/server.pem

# Set a secret with a specific stage label
manzano credential set \
  --module order \
  --name stripe_api_key \
  --value "sk_live_xyz789..." \
  --stage pending

Retrieving Secrets in Code

Secrets are accessed through the credentials namespace. The runtime resolves the current-labeled version by default:

activity charge_payment(order_id: Id) -> PaymentResult {
  let api_key = credentials.stripe_api_key
  return payment_service.charge(order_id, api_key)
}

Accessing Specific Stages

To access a secret at a specific stage (e.g., during rotation), use the stage qualifier:

let current_key = credentials.stripe_api_key              // defaults to "current"
let pending_key = credentials.stripe_api_key.pending       // the next key
let previous_key = credentials.stripe_api_key.previous     // the prior key

Versioning

Every secret maintains a version history. When you set a new value, a new version is created:

stripe_api_key
  version 1: sk_live_abc123...  (stage: previous)
  version 2: sk_live_def456...  (stage: current)
  version 3: sk_live_xyz789...  (stage: pending)

Version Lifecycle

StageDescription
pendingThe next version, not yet active. Used for pre-deployment verification.
currentThe active version. This is what credentials.name resolves to.
previousThe prior version. Retained for rollback and for decrypting data encrypted with the old key.

Only the three most recent versions are retained. Older versions are permanently deleted.

Listing Versions

manzano credential list \
  --module order \
  --name stripe_api_key

# Output:
# stripe_api_key
#   v3  pending    2025-03-15T10:00:00Z
#   v2  current    2025-03-01T10:00:00Z
#   v1  previous   2025-02-15T10:00:00Z

Rotation

Secret rotation is a two-phase process: promote and clean up.

Phase 1: Set the New Secret

manzano credential set \
  --module order \
  --name stripe_api_key \
  --value "sk_live_new..." \
  --stage pending

At this point, the secret layout is:

Phase 2: Promote

manzano credential promote \
  --module order \
  --name stripe_api_key

This shifts all labels forward:

Dual-Read During Rotation

During rotation, your code may need to try both the current and previous secrets (e.g., when verifying webhook signatures signed with either key):

webhook stripe_events {
  provider: "stripe"
  secret: credentials.stripe_webhook_secret
  fallback_secret: credentials.stripe_webhook_secret.previous
  path: "/webhooks/stripe"
  handler: handle_stripe_event
}

Master Key

The credential store is encrypted with a master key. The master key is provided through an environment variable:

export GROVE_MASTER_KEY="base64-encoded-256-bit-key"

Master Key Requirements

Generating a Master Key

# Generate a random 256-bit key
openssl rand -base64 32

Master Key Rotation

To rotate the master key:

manzano credential rekey \
  --module order \
  --old-master-key "old-base64-key" \
  --new-master-key "new-base64-key"

This re-encrypts all secrets with the new master key. Both keys must be provided; the old key is needed to decrypt existing secrets.

Scoping

Secrets are scoped to modules:

// In the "order" module:
let key = credentials.stripe_api_key        // resolves to order/stripe_api_key

// In the "customer" module:
let key = credentials.stripe_api_key        // resolves to customer/stripe_api_key (different secret)

To share a secret across modules, set the same secret in each module or use a shared configuration approach.

Security Properties

PropertyDetail
Encryption algorithmAES-256-GCM
Key derivationHKDF-SHA256 from master key + secret name
Storage formatEncrypted blob in the credential store database
Access controlSecrets are accessible only within their module's runtime scope
AuditAll credential access is logged in the audit trail
TransmissionSecrets are never logged, never included in error messages, never serialized to API responses

CLI Command Reference

CommandDescription
credential setCreate or update a secret
credential getRetrieve a secret value (for debugging)
credential listList all secrets in a module with versions
credential deleteDelete a secret and all its versions
credential promotePromote pending to current
credential rekeyRe-encrypt all secrets with a new master key

See Also