IntegrationCLI Secrets Management

CLI Secrets Management

Learn how to inject secrets at runtime using ReAPI CLI for maximum security. This guide covers secrets file format, CI/CD integration, and best practices for production environments.

Prerequisites: Familiarity with CLI Usage and Secrets Management concepts.

Why CLI Secrets Injection?

When you package secrets in a deployment, they’re encrypted and stored in ReAPI’s cloud. While secure, some organizations require secrets to never leave their infrastructure.

CLI secrets injection solves this by:

  • Zero-knowledge deployment: Deployments contain no secrets at all
  • Centralized secret management: Use your existing vault (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault)
  • Instant rotation: Update secrets in your vault without redeploying tests
  • Compliance: Meet requirements for secrets to stay within your infrastructure
  • Audit trail: Leverage your vault’s access logging and rotation policies

How It Works

┌─────────────────────────────────────────────────────┐
│ 1. Store Secrets in Your Vault                     │
│    (AWS Secrets Manager, Vault, K8s Secrets, etc.) │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 2. CI/CD Pipeline Retrieves Secrets                │
│    - Saves to temporary file (secrets.json)         │
│    - Sets restrictive permissions (0600)            │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 3. ReAPI CLI Runs Tests                            │
│    - Injects secrets via --secrets flag             │
│    - Secrets available as {{$secrets.KEY}}          │
│    - No secrets sent to cloud                       │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 4. Cleanup                                         │
│    - Delete secrets file immediately                │
│    - Secrets never persisted to disk long-term     │
└─────────────────────────────────────────────────────┘

Basic Usage

Command Syntax

# Inject secrets from JSON file
npx @reapi/cli -y test run ./deployment.json --secrets ./secrets.json
 
# Inject secrets from YAML file
npx @reapi/cli -y test run ./deployment.json --secrets ./secrets.yaml
 
# Short form
npx @reapi/cli -y test run ./deployment.json -s ./secrets.json
 
# With remote deployment
npx @reapi/cli -y test run remote://deployment-id \
  --api-key "$REAPI_API_KEY" \
  --secrets ./secrets.json

Secrets File Format

JSON format (secrets.json):

{
  "API_KEY": "sk_live_abc123xyz789",
  "DB_PASSWORD": "super-secret-password",
  "AUTH_TOKEN": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "STRIPE_SECRET": "sk_test_51A...",
  "ADMIN_PASSWORD": "complex-password-here"
}

YAML format (secrets.yaml):

API_KEY: sk_live_abc123xyz789
DB_PASSWORD: super-secret-password
AUTH_TOKEN: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
STRIPE_SECRET: sk_test_51A...
ADMIN_PASSWORD: complex-password-here

Using Secrets in Tests

In your test flows, access secrets using the {{$secrets.KEY}} syntax:

API Node - Request Headers:

Authorization: Bearer {{$secrets.API_KEY}}
X-API-Secret: { { $secrets.STRIPE_SECRET } }

API Node - Request Body:

{
  "username": "admin",
  "password": "{{$secrets.ADMIN_PASSWORD}}",
  "database": "{{$secrets.DB_NAME}}"
}

JSONata Expressions:

// In assertions
response.status = 200 and response.key = $secrets.EXPECTED_KEY
 
// In context operations
{
  "connectionString": "postgresql://user:" & $secrets.DB_PASSWORD & "@localhost/db"
}

Merge Priority

When secrets are provided from multiple sources, they merge with the following priority (highest to lowest):

┌────────────────────────────────────┐
│ 1. CLI --secrets (Highest)         │  ← Overrides everything
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 2. Packaged Deployment Secrets     │  ← From variable groups
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 3. Mock/Default Values (Lowest)    │  ← Test placeholders
└────────────────────────────────────┘

Example:

Deployment has:

{
  "API_KEY": "dev-key-123",
  "DB_HOST": "dev.example.com"
}

CLI provides (--secrets secrets.json):

{
  "API_KEY": "prod-key-789",
  "DB_PASSWORD": "secret-pass"
}

Final merged secrets:

{
  "API_KEY": "prod-key-789", // ← CLI override
  "DB_HOST": "dev.example.com", // ← From deployment
  "DB_PASSWORD": "secret-pass" // ← CLI addition
}

Deployment Configuration

To use CLI secrets injection, you must disable packaging in your variable groups.

Disable Secrets Packaging

  1. Open your variable group in ReAPI Studio
  2. Navigate to Secured Variable Group settings
  3. Disable the packageSecretsInDeployment toggle
  4. Save the variable group
  5. Create/update deployment

Result: Deployment contains test logic but no secrets. Secrets must be provided via CLI.

Validation

When creating a deployment with packaging disabled:

# This will fail - no secrets provided
npx @reapi/cli -y test run ./deployment.json
# ❌ Error: Secrets required but not provided
 
# This works - secrets injected
npx @reapi/cli -y test run ./deployment.json --secrets ./secrets.json
# ✅ Tests run successfully

CI/CD Integration

AWS Secrets Manager

GitHub Actions example:

# .github/workflows/api-tests.yml
name: API Tests
 
on:
  push:
    branches: [main]
  pull_request:
 
jobs:
  test:
    runs-on: ubuntu-latest
 
    permissions:
      id-token: write # Required for AWS OIDC
      contents: read
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GithubActionsRole
          aws-region: us-east-1
 
      - name: Retrieve secrets from AWS Secrets Manager
        run: |
          aws secretsmanager get-secret-value \
            --secret-id reapi/production/api-secrets \
            --query SecretString \
            --output text > secrets.json
 
          # Set restrictive permissions
          chmod 600 secrets.json
 
      - name: Run API tests
        env:
          REAPI_API_KEY: ${{ secrets.REAPI_API_KEY }}
        run: |
          npx @reapi/cli -y test run \
            remote://your-deployment-id \
            --secrets secrets.json \
            --output ./test-results \
            --throw-on-failure
 
      - name: Cleanup secrets
        if: always()
        run: |
          rm -f secrets.json
          echo "Secrets file cleaned up"
 
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: test-results/

HashiCorp Vault

GitLab CI example:

# .gitlab-ci.yml
api-tests:
  stage: test
  image: node:20
 
  before_script:
    # Install Vault CLI
    - apt-get update && apt-get install -y vault
 
    # Authenticate with Vault
    - export VAULT_ADDR="https://vault.example.com"
    - export VAULT_TOKEN="$VAULT_CI_TOKEN"
 
  script:
    # Retrieve secrets from Vault
    - |
      vault kv get -format=json secret/reapi/production | \
        jq '.data.data' > secrets.json
 
    # Set restrictive permissions
    - chmod 600 secrets.json
 
    # Run tests
    - |
      npx @reapi/cli -y test run \
        remote://$DEPLOYMENT_ID \
        --api-key "$REAPI_API_KEY" \
        --secrets secrets.json \
        --output ./test-results \
        --report junit \
        --throw-on-failure
 
  after_script:
    # Always cleanup
    - rm -f secrets.json
 
  artifacts:
    when: always
    reports:
      junit: test-results/*.xml

Azure Key Vault

Azure DevOps Pipeline:

# azure-pipelines.yml
trigger:
  - main
 
pool:
  vmImage: "ubuntu-latest"
 
steps:
  - task: AzureCLI@2
    displayName: "Retrieve secrets from Azure Key Vault"
    inputs:
      azureSubscription: "Azure-Subscription"
      scriptType: "bash"
      scriptLocation: "inlineScript"
      inlineScript: |
        # Get secrets from Key Vault
        API_KEY=$(az keyvault secret show --vault-name my-keyvault --name api-key --query value -o tsv)
        DB_PASS=$(az keyvault secret show --vault-name my-keyvault --name db-password --query value -o tsv)
 
        # Create secrets JSON
        cat > secrets.json <<EOF
        {
          "API_KEY": "$API_KEY",
          "DB_PASSWORD": "$DB_PASS"
        }
        EOF
 
        chmod 600 secrets.json
 
  - script: |
      npx @reapi/cli -y test run \
        remote://$(DEPLOYMENT_ID) \
        --api-key "$(REAPI_API_KEY)" \
        --secrets secrets.json \
        --output ./test-results \
        --throw-on-failure
    displayName: "Run API Tests"
 
  - script: rm -f secrets.json
    displayName: "Cleanup secrets"
    condition: always()
 
  - task: PublishTestResults@2
    displayName: "Publish test results"
    condition: always()
    inputs:
      testResultsFormat: "JUnit"
      testResultsFiles: "test-results/*.xml"

Kubernetes Secrets

Jenkins Pipeline with K8s:

// Jenkinsfile
pipeline {
    agent {
        kubernetes {
            yaml """
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: node
    image: node:20
    command: ['sleep']
    args: ['infinity']
    volumeMounts:
    - name: secrets
      mountPath: /secrets
      readOnly: true
  volumes:
  - name: secrets
    secret:
      secretName: reapi-test-secrets
"""
        }
    }
 
    stages {
        stage('Run API Tests') {
            steps {
                container('node') {
                    script {
                        // Convert K8s secret to JSON format
                        sh '''
                            echo '{' > secrets.json
                            echo "  \\"API_KEY\\": \\"$(cat /secrets/api_key)\\"," >> secrets.json
                            echo "  \\"DB_PASSWORD\\": \\"$(cat /secrets/db_password)\\"" >> secrets.json
                            echo '}' >> secrets.json
 
                            chmod 600 secrets.json
                        '''
 
                        // Run tests
                        sh '''
                            npx @reapi/cli -y test run \
                              remote://${DEPLOYMENT_ID} \
                              --api-key "${REAPI_API_KEY}" \
                              --secrets secrets.json \
                              --output ./test-results \
                              --throw-on-failure
                        '''
                    }
                }
            }
 
            post {
                always {
                    container('node') {
                        sh 'rm -f secrets.json'
                    }
                }
            }
        }
    }
}

Configuration File

You can specify secrets file in your reapi.yaml configuration:

# reapi.yaml
target: remote://deployment-id
secrets: ./secrets.json # Path to secrets file
output: ./test-results
report: junit,html
throwOnFailure: true

Usage:

# Secrets path loaded from config
npx @reapi/cli -y test run --config reapi.yaml
 
# Override config with command-line parameter
npx @reapi/cli -y test run --config reapi.yaml --secrets ./prod-secrets.json

Security Best Practices

File Permissions

Always set restrictive permissions on secrets files:

# Linux/macOS - owner read/write only
chmod 600 secrets.json
 
# Windows PowerShell - remove inheritance and grant only current user
icacls secrets.json /inheritance:r
icacls secrets.json /grant:r "%USERNAME%:(R,W)"

Immediate Cleanup

Delete secrets files as soon as tests complete:

# Bash with trap for cleanup on exit
cleanup() {
  rm -f secrets.json
  echo "Secrets cleaned up"
}
trap cleanup EXIT
 
# Retrieve and run tests
aws secretsmanager get-secret-value ... > secrets.json
npx @reapi/cli -y test run --secrets secrets.json
 
# cleanup() runs automatically even if script fails

Python example:

import subprocess
import os
import json
 
try:
    # Retrieve secrets
    secrets = retrieve_from_vault()
 
    # Write to temporary file
    with open('secrets.json', 'w') as f:
        json.dump(secrets, f)
 
    # Set restrictive permissions
    os.chmod('secrets.json', 0o600)
 
    # Run tests
    subprocess.run([
        'npx', '@reapi/cli', '-y', 'test', 'run',
        'remote://deployment-id',
        '--secrets', 'secrets.json'
    ], check=True)
 
finally:
    # Always cleanup
    if os.path.exists('secrets.json'):
        os.remove('secrets.json')
        print("Secrets cleaned up")

Never Commit Secrets Files

.gitignore:

# Secrets files
secrets.json
secrets.yaml
*-secrets.json
*-secrets.yaml
*.secrets.*

# Environment files with secrets
prod-*.json
production-*.json
.env.production
.env.prod

Use Temporary Directories

For extra security, write secrets to a temporary directory:

#!/bin/bash
set -e
 
# Create temporary directory
TEMP_DIR=$(mktemp -d)
SECRETS_FILE="$TEMP_DIR/secrets.json"
 
# Cleanup function
cleanup() {
  rm -rf "$TEMP_DIR"
  echo "Temporary directory cleaned up"
}
trap cleanup EXIT
 
# Retrieve secrets
aws secretsmanager get-secret-value \
  --secret-id reapi/production \
  --query SecretString \
  --output text > "$SECRETS_FILE"
 
# Set restrictive permissions
chmod 600 "$SECRETS_FILE"
 
# Run tests
npx @reapi/cli -y test run \
  remote://deployment-id \
  --secrets "$SECRETS_FILE" \
  --throw-on-failure
 
# cleanup() runs automatically

Rotate Secrets Regularly

Establish a rotation schedule:

# Example rotation workflow
# 1. Generate new secrets in your vault
aws secretsmanager rotate-secret --secret-id reapi/production
 
# 2. Update secrets in tests (no code changes needed!)
# Tests automatically pick up new secrets on next run
 
# 3. Run tests to verify new secrets work
npx @reapi/cli -y test run remote://id --secrets <(
  aws secretsmanager get-secret-value \
    --secret-id reapi/production \
    --query SecretString \
    --output text
)
 
# 4. If tests pass, rotation successful!

Recommended rotation schedule:

Secret TypeRotation FrequencyReason
Production API keysEvery 90 daysCompliance requirement
Database passwordsEvery 60 daysHigh-risk credential
Service tokensEvery 30 daysFrequently accessed
Test environmentEvery 180 daysLower risk tolerance

Audit Access

Enable audit logging in your secret management system:

AWS Secrets Manager CloudTrail:

{
  "eventName": "GetSecretValue",
  "requestParameters": {
    "secretId": "reapi/production/api-secrets"
  },
  "userIdentity": {
    "principalId": "AIDAJ45Q7YJFXYZ123456",
    "arn": "arn:aws:iam::123456789012:user/ci-user"
  },
  "eventTime": "2024-01-15T10:30:00Z"
}

Review audit logs regularly:

  • Who accessed secrets?
  • From which IP addresses?
  • At what times?
  • Were there any failed access attempts?

Troubleshooting

Secrets File Not Found

Error:

❌ Error: Secrets file not found: ./secrets.json

Solutions:

  1. Verify file path is correct (relative or absolute)
  2. Check file was created successfully
  3. Ensure current directory is correct
# Debug: print absolute path
echo "Secrets file: $(pwd)/secrets.json"
ls -la secrets.json
 
# Use absolute path
npx @reapi/cli -y test run --secrets "$(pwd)/secrets.json"

Invalid Secrets Format

Error:

❌ Error: Invalid secrets file format

Solutions:

  1. Validate JSON syntax
# Validate JSON
cat secrets.json | jq .
# If error, JSON is malformed
  1. Check YAML syntax
# Validate YAML (requires yq)
cat secrets.yaml | yq .
  1. Ensure file is UTF-8 encoded
# Check encoding
file -I secrets.json
# Should show: charset=utf-8

Secrets Not Applied

Problem: Tests run but don’t use injected secrets

Solutions:

  1. Verify syntax in tests:
# ✅ Correct
Authorization: Bearer {{$secrets.API_KEY}}
 
# ❌ Wrong - missing $secrets prefix
Authorization: Bearer {{API_KEY}}
  1. Check secret key names match:
// secrets.json
{
  "API_KEY": "value" // ← Key name
}
# Test must use exact key name
Authorization: Bearer {{$secrets.API_KEY}}
  1. Enable debug logging:
# Set log level to debug
export LOG_LEVEL=debug
npx @reapi/cli -y test run --secrets secrets.json

Permission Denied

Error:

❌ Error: Permission denied reading secrets file

Solutions:

# Check file permissions
ls -la secrets.json
 
# Grant read permission
chmod 600 secrets.json
 
# Verify you own the file
stat secrets.json
 
# Run as file owner
sudo -u owner npx @reapi/cli -y test run --secrets secrets.json

Secrets Leak in CI Logs

Problem: Secrets visible in CI/CD logs

Solutions:

  1. Never echo secrets directly:
# ❌ BAD - secret visible in logs
echo "Using API key: $API_KEY"
 
# ✅ GOOD - masked output
echo "API key loaded successfully"
  1. Use CI secret masking:
# GitHub Actions - secrets are automatically masked
- name: Run tests
  env:
    SECRET_KEY: ${{ secrets.SECRET_KEY }}
  run: echo "Secret is: $SECRET_KEY"  # Will show: Secret is: ***
  1. Disable debug mode in CI:
# Don't use debug logging in CI
# ❌ export LOG_LEVEL=debug
 
# ✅ Use default log level
npx @reapi/cli -y test run --secrets secrets.json

Advanced Patterns

Dynamic Secret Selection

Choose different secrets based on environment:

#!/bin/bash
 
ENVIRONMENT=${1:-staging}
 
case $ENVIRONMENT in
  production)
    SECRET_ID="reapi/production"
    DEPLOYMENT_ID="prod-deployment-id"
    ;;
  staging)
    SECRET_ID="reapi/staging"
    DEPLOYMENT_ID="staging-deployment-id"
    ;;
  development)
    SECRET_ID="reapi/development"
    DEPLOYMENT_ID="dev-deployment-id"
    ;;
  *)
    echo "Unknown environment: $ENVIRONMENT"
    exit 1
    ;;
esac
 
# Retrieve appropriate secrets
aws secretsmanager get-secret-value \
  --secret-id "$SECRET_ID" \
  --query SecretString \
  --output text > secrets.json
 
# Run tests for environment
npx @reapi/cli -y test run \
  "remote://$DEPLOYMENT_ID" \
  --secrets secrets.json \
  --throw-on-failure

Multiple Secret Sources

Merge secrets from multiple vaults:

#!/bin/bash
set -e
 
# Retrieve from multiple sources
aws secretsmanager get-secret-value \
  --secret-id reapi/common \
  --query SecretString \
  --output text > common.json
 
aws secretsmanager get-secret-value \
  --secret-id reapi/production \
  --query SecretString \
  --output text > prod.json
 
# Merge JSON files (prod overrides common)
jq -s '.[0] * .[1]' common.json prod.json > secrets.json
 
# Cleanup intermediate files
rm -f common.json prod.json
 
# Run tests
npx @reapi/cli -y test run --secrets secrets.json
 
# Cleanup final secrets
rm -f secrets.json

Secrets with Environment Variables

Combine secrets file with environment variables:

# reapi.yaml
target: remote://deployment-id
secrets: ./secrets.json
env: ./runtime-env.json # Non-secret environment variables

Use case: Secrets contain credentials, environment file contains configuration:

// secrets.json (from vault)
{
  "API_KEY": "sk_prod_abc123",
  "DB_PASSWORD": "secret-pass"
}
// runtime-env.json (from config/git)
{
  "API_BASE_URL": "https://api.example.com",
  "TIMEOUT": 30000,
  "RETRY_COUNT": 3
}

Parallel Test Runs

Run multiple test suites in parallel with different secrets:

#!/bin/bash
 
# Run staging and production tests in parallel
(
  aws secretsmanager get-secret-value \
    --secret-id reapi/staging \
    --query SecretString \
    --output text > staging-secrets.json
 
  npx @reapi/cli -y test run \
    remote://staging-deployment \
    --secrets staging-secrets.json \
    --output ./staging-results
 
  rm -f staging-secrets.json
) &
 
(
  aws secretsmanager get-secret-value \
    --secret-id reapi/production \
    --query SecretString \
    --output text > prod-secrets.json
 
  npx @reapi/cli -y test run \
    remote://prod-deployment \
    --secrets prod-secrets.json \
    --output ./prod-results
 
  rm -f prod-secrets.json
) &
 
# Wait for both to complete
wait
 
echo "All test suites completed"

Comparison: Packaged vs Injected

AspectPackaged SecretsCLI Injected Secrets
SecurityEncrypted in cloudNever leave your infrastructure
RotationRequires redeploymentInstant (update vault)
SetupSimple (UI workflow)More complex (vault + CI)
ConvenienceHigh (one-time setup)Medium (script required)
ComplianceGood for most casesBest for strict requirements
Secret ManagementReAPI handlesYour vault handles
Audit TrailReAPI logsYour vault’s audit logs
CostIncludedDepends on vault provider
Use CaseInternal testing, stagingProduction, compliance-heavy

Best Practices Summary

DO ✅

  • Use CLI injection for production environments
  • Store secrets in a dedicated vault (AWS, Vault, Azure)
  • Set restrictive file permissions (600)
  • Delete secrets files immediately after use
  • Add secrets files to .gitignore
  • Use temporary directories for secrets
  • Rotate secrets regularly (60-90 days)
  • Enable audit logging in your vault
  • Test secret rotation process
  • Document secret rotation procedures

DON’T ❌

  • Commit secrets files to version control
  • Echo secrets in CI logs
  • Leave secrets files on disk
  • Use weak file permissions (644, 755)
  • Share secrets files via email/chat
  • Store secrets in plain text config files
  • Skip cleanup steps in error cases
  • Use same secrets for all environments
  • Forget to test after secret rotation
  • Ignore audit logs

Next Steps


Questions? Contact us at support@reapi.com or join our Discord community