kAIzen Coach Documentation

Multi-environment deployment, architecture, and operational guides

View the Project on GitHub smurphin/my-personal-coach

Secrets Management Guide

This guide explains every secret used in kAIzen Coach, what it’s for, where it comes from, and how to generate it.

Overview

All application secrets are stored in AWS Secrets Manager and loaded at runtime by the Flask application. Terraform creates the secret placeholder, but you must manually populate it.

Secret Name Pattern:

Required Secrets

1. STRAVA_CLIENT_ID

What it is: Your Strava API application’s client ID (public identifier)

Where it comes from: Strava API Settings page

How to get it:

  1. Go to https://www.strava.com/settings/api
  2. Create a new API application (or use existing)
  3. The “Client ID” is shown on the page (numeric value)

Format: Numeric string

"STRAVA_CLIENT_ID": "176694"

Used for:


2. STRAVA_CLIENT_SECRET

What it is: Your Strava API application’s client secret (private key)

Where it comes from: Strava API Settings page

How to get it:

  1. Same page as Client ID: https://www.strava.com/settings/api
  2. The “Client Secret” is shown (hexadecimal string)
  3. Keep this secret! Don’t commit to git or share publicly

Format: Hexadecimal string

"STRAVA_CLIENT_SECRET": "a1b2c3d4e5f6789012345678901234567890abcd"

Used for:


3. STRAVA_VERIFY_TOKEN

What it is: A secret token YOU CREATE to verify webhook requests are from Strava

Where it comes from: YOU GENERATE THIS YOURSELF

How to generate it:

# Generate a random 40-character hex string
openssl rand -hex 20

# Example output:
# cb4fda9a37786db2cbfc7905e5458fe75874ed5a

Format: Any random string (recommend 40+ characters)

"STRAVA_VERIFY_TOKEN": "cb4fda9a37786db2cbfc7905e5458fe75874ed5a"

Used for:

Important:


4. FLASK_SECRET_KEY

What it is: Flask’s session encryption key

Where it comes from: YOU GENERATE THIS YOURSELF

How to generate it:

# Generate a random 64-character hex string
openssl rand -hex 32

# Or use Python:
python3 -c "import secrets; print(secrets.token_hex(32))"

# Example output:
# 8f7d6e5c4b3a2918e7f6d5c4b3a29180f7e6d5c4b3a29180e7f6d5c4b3a2918

Format: Long random string (recommend 64+ characters)

"FLASK_SECRET_KEY": "8f7d6e5c4b3a2918e7f6d5c4b3a29180f7e6d5c4b3a29180e7f6d5c4b3a2918"

Used for:

Security:


5. GOOGLE_APPLICATION_CREDENTIALS_JSON

What it is: GCP service account credentials (entire JSON key file)

Where it comes from: Google Cloud Console

How to get it:

  1. Go to GCP Console → IAM & Admin → Service Accounts
  2. Find your service account (e.g., vertex-ai-sa@kaizencoach-staging.iam.gserviceaccount.com)
  3. Click → Keys → Add Key → Create New Key → JSON
  4. Download the JSON file

How to format for Secrets Manager:

# Convert to single-line JSON (no newlines)
cat .keys/kaizencoach-staging-sa-key.json | jq -c '.'

# This produces a compact single-line JSON string

Format: Complete JSON key file as a single-line string

"GOOGLE_APPLICATION_CREDENTIALS_JSON": "{\"type\":\"service_account\",\"project_id\":\"kaizencoach-staging\",\"private_key_id\":\"abc123...\",\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\"client_email\":\"vertex-ai-sa@kaizencoach-staging.iam.gserviceaccount.com\",\"client_id\":\"123456789\",\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"token_uri\":\"https://oauth2.googleapis.com/token\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\",\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-sa%40kaizencoach-staging.iam.gserviceaccount.com\"}"

Used for:

Critical Notes:


6. GARMIN_ENCRYPTION_KEY

What it is: Symmetric encryption key for storing Garmin credentials

Where it comes from: YOU GENERATE THIS YOURSELF

How to generate it:

# Generate a 32-byte (256-bit) key, base64 encoded
openssl rand -base64 32

# Or use Python:
python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"

# Example output:
# XyZ9AbC8dEf7GhI6JkL5MnO4PqR3StU2VwX1YzA0BcD=

Format: Base64-encoded 32-byte key

"GARMIN_ENCRYPTION_KEY": "XyZ9AbC8dEf7GhI6JkL5MnO4PqR3StU2VwX1YzA0BcD="

Used for:

Security:


7. GCP_PROJECT_ID (Optional - Can be derived)

What it is: The GCP project ID for this environment

Where it comes from: Set when creating GCP project

Format: String

"GCP_PROJECT_ID": "kaizencoach-staging"

Used for:

Note: This is technically optional as config.py can determine it from the environment variable or parse it from the service account JSON.


8. GCP_LOCATION (Optional - Can be hardcoded)

What it is: GCP region for Vertex AI

Format: String

"GCP_LOCATION": "europe-west1"

Note: This is typically hardcoded in config.py and doesn’t need to be in secrets.


Complete Secrets Template

{
  "STRAVA_CLIENT_ID": "your_strava_app_client_id",
  "STRAVA_CLIENT_SECRET": "your_strava_app_client_secret",
  "STRAVA_VERIFY_TOKEN": "random_40_char_token_you_generated",
  "FLASK_SECRET_KEY": "random_64_char_key_you_generated",
  "GOOGLE_APPLICATION_CREDENTIALS_JSON": "{\"type\":\"service_account\",\"project_id\":\"kaizencoach-staging\",...}",
  "GARMIN_ENCRYPTION_KEY": "base64_encoded_32_byte_key"
}

Secrets Per Environment

Production

Staging

Demo Instances

Populating Secrets in AWS

Step 1: Create Secrets JSON File

# DON'T commit this file!
cat > /tmp/staging-secrets.json << 'EOF'
{
  "STRAVA_CLIENT_ID": "188207",
  "STRAVA_CLIENT_SECRET": "abc123def456...",
  "STRAVA_VERIFY_TOKEN": "cb4fda9a37786db2cbfc7905e5458fe75874ed5a",
  "FLASK_SECRET_KEY": "8f7d6e5c4b3a2918e7f6d5c4b3a29180...",
  "GOOGLE_APPLICATION_CREDENTIALS_JSON": "{\"type\":\"service_account\"...}",
  "GARMIN_ENCRYPTION_KEY": "XyZ9AbC8dEf7GhI6JkL5MnO4PqR3StU2VwX1YzA0BcD="
}
EOF

Step 2: Upload to Secrets Manager

# For staging
aws secretsmanager put-secret-value \
  --secret-id staging-kaizencoach-app-secrets \
  --secret-string file:///tmp/staging-secrets.json \
  --region eu-west-1

# For prod
aws secretsmanager put-secret-value \
  --secret-id my-personal-coach-app-secrets \
  --secret-string file:///tmp/staging-secrets.json \
  --region eu-west-1

Step 3: Clean Up Local File

rm /tmp/staging-secrets.json

Step 4: Restart App Runner

# Force App Runner to reload secrets
aws apprunner start-deployment \
  --service-arn <your-service-arn> \
  --region eu-west-1

Verifying Secrets Loaded

Check application logs after restart:

aws logs tail /aws/apprunner/staging-kaizencoach-service/service --since 5m --region eu-west-1 | grep "Secrets loaded"

Should show:

✅ Secrets loaded - STRAVA_CLIENT_ID: 188207...

Rotating Secrets

When to Rotate

How to Rotate

# 1. Generate new secrets (don't overwrite existing yet!)
openssl rand -hex 32  # New Flask key
openssl rand -base64 32  # New Garmin key

# 2. Update secrets in Secrets Manager
# (follow same steps as populating)

# 3. For Garmin key rotation:
# WARNING: Rotating Garmin key will invalidate all stored passwords!
# Users will need to re-authenticate with Garmin

# 4. Restart App Runner
aws apprunner start-deployment --service-arn <arn> --region eu-west-1

# 5. Verify new secrets loaded in logs

Security Best Practices

  1. Never commit secrets to git
    • Add to .gitignore: *secrets*.json, *.key, .keys/
  2. Use different secrets per environment
    • Prod and staging should never share Flask keys or Garmin keys
    • Can reuse Strava verify tokens if desired (but not required)
  3. Restrict AWS Secrets Manager access
    • Only App Runner IAM role should read secrets
    • Use principle of least privilege
  4. Audit secret access
    • Enable CloudTrail logging for Secrets Manager
    • Monitor for unusual access patterns
  5. Backup secrets securely
    • Store in password manager (1Password, LastPass, etc.)
    • Don’t rely solely on AWS Secrets Manager
  6. Document secret generation
    • Keep record of when secrets were created
    • Note which environments use which Strava apps

Troubleshooting

App Can’t Load Secrets

Symptom: App crashes on startup with “SECRET_KEY not found” Fix: Verify secrets exist in Secrets Manager and App Runner IAM role has secretsmanager:GetSecretValue permission

Strava OAuth Fails

Symptom: “Invalid client_id” error Fix: Check STRAVA_CLIENT_ID matches the Strava app for this environment’s domain

Webhook Verification Fails

Symptom: Webhook subscription fails with 403 Fix: Ensure STRAVA_VERIFY_TOKEN in secrets matches the token you’re using in the API call

Garmin Credentials Won’t Decrypt

Symptom: Error decrypting Garmin password Fix: GARMIN_ENCRYPTION_KEY may have changed - users need to re-authenticate

GCP Service Account Errors

Symptom: “Could not load default credentials” or 403 from Vertex AI Fix:

Quick Reference Commands

# Generate Flask secret
openssl rand -hex 32

# Generate Garmin encryption key
openssl rand -base64 32

# Generate Strava verify token
openssl rand -hex 20

# Format GCP service account JSON
cat key.json | jq -c '.'

# View secrets (sanitized)
aws secretsmanager get-secret-value \
  --secret-id staging-kaizencoach-app-secrets \
  --region eu-west-1 \
  --query SecretString --output text | jq .

# Update secrets
aws secretsmanager put-secret-value \
  --secret-id staging-kaizencoach-app-secrets \
  --secret-string file:///tmp/secrets.json \
  --region eu-west-1

# Force app to reload secrets
aws apprunner start-deployment --service-arn <arn> --region eu-west-1