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:
- Encrypted at rest using AES-256-GCM with a master key.
- Versioned so that old secrets remain available during rotation.
- Labeled with stage identifiers (e.g.,
current,pending,previous) for seamless rotation. - Accessible in Grove code through the
credentialsnamespace.
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
| Stage | Description |
|---|---|
pending | The next version, not yet active. Used for pre-deployment verification. |
current | The active version. This is what credentials.name resolves to. |
previous | The 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:
pending= new keycurrent= existing key (still active)previous= old key
Phase 2: Promote
manzano credential promote \
--module order \
--name stripe_api_key
This shifts all labels forward:
current= what waspending(the new key is now active)previous= what wascurrent- The old
previousversion is deleted
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
- Must be exactly 256 bits (32 bytes), provided as a base64-encoded string.
- Must be set before the application starts.
- If the master key is not set, the credential store is unavailable and any access to
credentials.*produces a runtime error. - The master key itself is never stored on disk by Grove.
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
| Property | Detail |
|---|---|
| Encryption algorithm | AES-256-GCM |
| Key derivation | HKDF-SHA256 from master key + secret name |
| Storage format | Encrypted blob in the credential store database |
| Access control | Secrets are accessible only within their module's runtime scope |
| Audit | All credential access is logged in the audit trail |
| Transmission | Secrets are never logged, never included in error messages, never serialized to API responses |
CLI Command Reference
| Command | Description |
|---|---|
credential set | Create or update a secret |
credential get | Retrieve a secret value (for debugging) |
credential list | List all secrets in a module with versions |
credential delete | Delete a secret and all its versions |
credential promote | Promote pending to current |
credential rekey | Re-encrypt all secrets with a new master key |
See Also
- Authorization Hooks -- using secrets in authorization contexts
- Webhook Security -- webhook signing secrets
- Field Classifications -- encryption keys for field-level encryption
- Configuration Reference -- environment variable configuration