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:

  1. Created directly in each environment
  2. Stored in a secure vault (HashiCorp Vault, AWS Secrets Manager, etc.)
  3. 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

Frodo CLI Series

Official Resources


Lessons from the Field

A few things we learned the hard way:

  1. Always run frodo esv apply after 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.

  2. 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.

  3. The naming convention matters - We started with random names like api-key-1, prod-url. Six months later, nobody knew what was what. Switched to esv-{service}-{purpose} and life got better.

  4. 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.