Hardcoded API URLs in scripts. Production passwords in environment configs. If you’ve inherited a ForgeRock deployment, you’ve probably seen these anti-patterns.
ESVs (Environment Secrets and Variables) are how PingOne Advanced Identity Cloud wants you to handle this—externalize configuration so the same journey works in dev, staging, and prod without code changes. The trick is managing ESVs at scale without losing your mind.
Here’s how to do it with Frodo CLI.
ESVs in 30 Seconds
ESVs separate configuration from sensitive data:
| Type | Purpose | Security | Example |
|---|---|---|---|
| Variables | Non-sensitive configuration | Visible in exports | API endpoints, feature flags |
| Secrets | Sensitive data | Never exposed in plaintext | API keys, passwords, certificates |
ESV Architecture:
graph TB
subgraph "Application Layer"
JOURNEY[Journeys]
SCRIPT[Scripts]
OAUTH[OAuth Clients]
end
subgraph "ESV Layer"
VAR[Variables]
SEC[Secrets]
end
subgraph "Environment"
DEV[Development Values]
PROD[Production Values]
end
JOURNEY --> VAR
SCRIPT --> VAR
SCRIPT --> SEC
OAUTH --> SEC
VAR --> DEV
VAR --> PROD
SEC --> DEV
SEC --> PROD
style VAR fill:#667eea,color:#fff
style SEC fill:#e53e3e,color:#fff
ESV Variable Management
List Variables
# List all variables
frodo esv variable list -h https://openam-dev.forgeblocks.com/am
# Output:
# ┌──────────────────────────────────────┬─────────────────────────────────────┐
# │ Name │ Value │
# ├──────────────────────────────────────┼─────────────────────────────────────┤
# │ esv-api-base-url │ https://api-dev.example.com │
# │ esv-feature-mfa-enabled │ true │
# │ esv-session-timeout-minutes │ 30 │
# └──────────────────────────────────────┴─────────────────────────────────────┘
Create Variables
# Create a new variable
frodo esv variable create \
-i "esv-api-base-url" \
-v "https://api.example.com" \
-d "Base URL for backend API calls" \
-h https://openam-dev.forgeblocks.com/am
# Create with expression type (for complex values)
frodo esv variable create \
-i "esv-allowed-origins" \
-v '["https://app1.example.com", "https://app2.example.com"]' \
--expression \
-h https://openam-dev.forgeblocks.com/am
Update Variables
# Update an existing variable
frodo esv variable set \
-i "esv-api-base-url" \
-v "https://api-v2.example.com" \
-h https://openam-dev.forgeblocks.com/am
Export Variables
# Export all variables
frodo esv variable export -a -D ./esv -h https://openam-dev.forgeblocks.com/am
# Export specific variable
frodo esv variable export -i "esv-api-base-url" -h https://openam-dev.forgeblocks.com/am
Import Variables
# Import all variables from a directory
frodo esv variable import -a -D ./esv -h https://openam-prod.forgeblocks.com/am
# Import specific variable file
frodo esv variable import -f ./esv/esv-api-base-url.variable.json -h https://openam-prod.forgeblocks.com/am
ESV Secret Management
List Secrets
# List all secrets (values are never shown)
frodo esv secret list -h https://openam-dev.forgeblocks.com/am
# Output:
# ┌──────────────────────────────────────┬──────────────┬─────────────────────┐
# │ Name │ Status │ Last Modified │
# ├──────────────────────────────────────┼──────────────┼─────────────────────┤
# │ esv-smtp-password │ loaded │ 2024-12-20T10:00:00 │
# │ esv-api-key │ loaded │ 2024-12-19T15:30:00 │
# │ esv-db-connection-string │ loaded │ 2024-12-18T09:00:00 │
# └──────────────────────────────────────┴──────────────┴─────────────────────┘
Create Secrets
# Create a new secret
frodo esv secret create \
-i "esv-api-key" \
-v "sk-prod-abc123..." \
-d "Production API key for risk service" \
-h https://openam-dev.forgeblocks.com/am
# Create secret with encoding (for certificates, etc.)
frodo esv secret create \
-i "esv-signing-key" \
-v "$(cat private-key.pem | base64)" \
--encoding base64 \
-h https://openam-dev.forgeblocks.com/am
Version Secrets (Rotation)
Secrets support versioning for zero-downtime rotation:
# Create a new version of an existing secret
frodo esv secret version create \
-i "esv-api-key" \
-v "sk-prod-newkey456..." \
-h https://openam-dev.forgeblocks.com/am
# List secret versions
frodo esv secret version list \
-i "esv-api-key" \
-h https://openam-dev.forgeblocks.com/am
# Delete old secret version after rotation
frodo esv secret version delete \
-i "esv-api-key" \
--version 1 \
-h https://openam-dev.forgeblocks.com/am
Delete Secrets
# Delete a secret (use with caution!)
frodo esv secret delete \
-i "esv-old-api-key" \
-h https://openam-dev.forgeblocks.com/am
Applying ESV Changes
After creating or modifying ESVs, changes must be applied to take effect:
# Apply all pending ESV changes
frodo esv apply -h https://openam-dev.forgeblocks.com/am
# Check pending changes before applying
frodo esv status -h https://openam-dev.forgeblocks.com/am
Heads up: The apply command restarts services. Don’t run this in production during peak hours—schedule it during a maintenance window.
Referencing ESVs in Configuration
In Scripts
// Access a variable
var apiUrl = systemEnv.getProperty("esv.api.base.url");
// Access a secret
var apiKey = systemEnv.getProperty("esv.api.key");
// With fallback
var timeout = systemEnv.getProperty("esv.request.timeout") || "5000";
In OAuth2 Client Configuration
{
"clientSecret": "&{esv.oauth.client.secret}",
"redirectUris": ["&{esv.oauth.redirect.uri}"]
}
In Journey Configuration
ESVs can be referenced in journey node configurations using the &{esv.name} syntax.
Cross-Environment ESV Strategy
Variable Promotion
Variables can be exported and imported between environments:
#!/bin/bash
# promote-variables.sh - Promote variables from dev to prod
DEV="https://openam-dev.forgeblocks.com/am"
PROD="https://openam-prod.forgeblocks.com/am"
# Export from dev
frodo esv variable export -a -D ./esv-export -h "$DEV"
# Review exported variables
echo "Variables to promote:"
ls -la ./esv-export/
# Import to prod (values may need adjustment)
# WARNING: Review values before importing to prod!
frodo esv variable import -a -D ./esv-export -h "$PROD"
# Apply changes
frodo esv apply -h "$PROD"
Secret Handling
Never export secrets to Git repositories. Secrets should be:
- Created directly in each environment
- Stored in a secure vault (HashiCorp Vault, AWS Secrets Manager, etc.)
- Deployed via secure CI/CD pipelines
# GitHub Actions - Secure secret deployment
- name: Deploy Secrets
env:
FRODO_HOST: ${{ secrets.FRODO_PROD_HOST }}
FRODO_USER: ${{ secrets.FRODO_PROD_USER }}
FRODO_PASSWORD: ${{ secrets.FRODO_PROD_PASSWORD }}
run: |
# Secrets come from GitHub Secrets, never from repo
frodo esv secret create \
-i "esv-api-key" \
-v "${{ secrets.PROD_API_KEY }}" \
--force
frodo esv apply
Environment-Specific Values
Same Variable, Different Values
# Development
frodo esv variable create \
-i "esv-api-base-url" \
-v "https://api-dev.example.com" \
-h https://openam-dev.forgeblocks.com/am
# Staging
frodo esv variable create \
-i "esv-api-base-url" \
-v "https://api-staging.example.com" \
-h https://openam-staging.forgeblocks.com/am
# Production
frodo esv variable create \
-i "esv-api-base-url" \
-v "https://api.example.com" \
-h https://openam-prod.forgeblocks.com/am
Configuration Matrix
Document your ESV configuration across environments:
| ESV Name | Dev | Staging | Prod |
|---|---|---|---|
esv-api-base-url |
api-dev.example.com | api-staging.example.com | api.example.com |
esv-feature-mfa-enabled |
false | true | true |
esv-session-timeout |
120 | 60 | 30 |
esv-api-key |
dev-key-*** | staging-key-*** | prod-key-*** |
Automated ESV Deployment Pipeline
# .github/workflows/deploy-esv.yml
name: Deploy ESV Configuration
on:
push:
branches: [main]
paths:
- 'esv/**'
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- dev
- staging
- production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment || 'dev' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install Frodo CLI
run: npm install -g @rockcarver/frodo-cli
- name: Deploy Variables
env:
FRODO_HOST: ${{ secrets.FRODO_HOST }}
FRODO_USER: ${{ secrets.FRODO_USER }}
FRODO_PASSWORD: ${{ secrets.FRODO_PASSWORD }}
run: |
# Import variables (safe to version control)
frodo esv variable import -a -D ./esv/variables
- name: Deploy Secrets
env:
FRODO_HOST: ${{ secrets.FRODO_HOST }}
FRODO_USER: ${{ secrets.FRODO_USER }}
FRODO_PASSWORD: ${{ secrets.FRODO_PASSWORD }}
# Secrets from GitHub Secrets
API_KEY: ${{ secrets.API_KEY }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
run: |
# Create/update secrets from GitHub Secrets
frodo esv secret create -i "esv-api-key" -v "$API_KEY" --force || true
frodo esv secret create -i "esv-smtp-password" -v "$SMTP_PASSWORD" --force || true
- name: Apply Changes
env:
FRODO_HOST: ${{ secrets.FRODO_HOST }}
FRODO_USER: ${{ secrets.FRODO_USER }}
FRODO_PASSWORD: ${{ secrets.FRODO_PASSWORD }}
run: |
echo "Applying ESV changes..."
frodo esv apply
# Wait for services to restart
sleep 30
# Verify
frodo esv variable list
frodo esv secret list
Best Practices
1. Naming Convention
Use a consistent naming pattern:
esv-{category}-{name}
Examples:
esv-api-base-url
esv-smtp-host
esv-feature-mfa-enabled
esv-oauth-client-secret
2. Documentation
Maintain an ESV registry:
# ESV Registry
## Variables
| Name | Description | Default | Used By |
|------|-------------|---------|---------|
| esv-api-base-url | Backend API URL | - | Scripts, OAuth clients |
| esv-session-timeout | Session timeout in minutes | 30 | AM configuration |
## Secrets
| Name | Description | Rotation Schedule | Used By |
|------|-------------|-------------------|---------|
| esv-api-key | Risk API key | Quarterly | Risk evaluation script |
| esv-smtp-password | Email service password | Annually | Email notifications |
3. Secret Rotation Workflow
#!/bin/bash
# rotate-secret.sh - Zero-downtime secret rotation
SECRET_NAME="esv-api-key"
NEW_VALUE="$1"
TENANT="$2"
# Create new version
echo "Creating new secret version..."
frodo esv secret version create \
-i "$SECRET_NAME" \
-v "$NEW_VALUE" \
-h "$TENANT"
# Apply changes
echo "Applying changes..."
frodo esv apply -h "$TENANT"
# Wait for propagation
sleep 60
# Verify new secret is working (application-specific test)
# ./test-api-connection.sh
# Delete old version
echo "Deleting old version..."
frodo esv secret version delete \
-i "$SECRET_NAME" \
--version 1 \
-h "$TENANT"
echo "Rotation complete!"
Troubleshooting
Pending Changes Not Applying
# Check status
frodo esv status -h $TENANT
# Force apply
frodo esv apply --force -h $TENANT
Secret Not Found in Script
Verify the ESV exists and is loaded:
# Check secret status
frodo esv secret list -h $TENANT | grep "esv-api-key"
# Ensure status is "loaded"
Variable Value Not Updating
ESV changes require service restart:
# Apply triggers restart
frodo esv apply -h $TENANT
# Check AM health after restart
curl https://openam-dev.forgeblocks.com/am/json/health
Related Resources
Frodo CLI Series
Official Resources
Lessons from the Field
A few things we learned the hard way:
-
Always run
frodo esv applyafter changes - Forgot this once, spent an hour debugging why the new API URL wasn’t being picked up. The changes were sitting in “pending” status. -
Secrets cannot be exported - This is by design, but it means you need a separate process for secret management. We use GitHub Secrets as the source of truth, with the CI pipeline pushing to each environment.
-
The naming convention matters - We started with random names like
api-key-1,prod-url. Six months later, nobody knew what was what. Switched toesv-{service}-{purpose}and life got better. -
Document everything in a registry - Keep a spreadsheet or markdown file mapping ESV names to their purpose and which components use them. Future you will be grateful.
ESV management isn’t glamorous, but getting it right early saves countless hours of “why does this work in dev but not prod?” debugging sessions.