This document describes the secrets management strategy for aphiria.com, including GitHub repository secrets, Pulumi ESC environments, and rotation procedures.
Audience: Repository maintainers only
This project uses a three-tier secrets architecture:
GitHub Secrets (CI/CD) → Pulumi ESC (Infrastructure Config) → Kubernetes Secrets (Runtime)
-
GitHub Secrets: CI/CD workflow authentication
PULUMI_ACCESS_TOKEN- Authenticate to Pulumi CloudWORKFLOW_DISPATCH_TOKEN- Trigger workflows from other workflows
-
Pulumi ESC Environments: Infrastructure configuration secrets
- Environment:
aphiria.com/Preview(preview-base, preview-pr-*) - Environment:
aphiria.com/Production(production) - Stores: DigitalOcean tokens, GHCR credentials, PostgreSQL passwords, cert-manager DNS tokens
- Environment:
-
Kubernetes Secrets: Application runtime secrets
- Created by Pulumi components
- Populated from Pulumi ESC values
- Scoped to namespaces (preview-pr-123, default)
Data Flow Example:
postgresql:password (Pulumi ESC)
↓ (accessed by Pulumi TypeScript)
db-env-var-secrets (Kubernetes Secret)
↓ (mounted as env var)
PostgreSQL container
Benefits:
- ✅ Centralized infrastructure secret management
- ✅ Automatic injection into Pulumi stacks (
pulumi config env add) - ✅ Encrypted at rest in Pulumi Cloud
- ✅ Environment composition (Preview and Production can inherit from base)
- ✅ Secret versioning and audit logs
Free Tier Limitations:
- 5 environments max
- 1 team member
- Current usage: 2 environments (
aphiria.com/Preview,aphiria.com/Production)
| Secret Name | Purpose | Rotation Schedule | Used By |
|---|---|---|---|
PULUMI_ACCESS_TOKEN |
Manage infrastructure state in Pulumi Cloud | Annually | preview-deploy.yml, preview-cleanup.yml |
WORKFLOW_DISPATCH_TOKEN |
Trigger preview deployment workflow from build workflow | Annually | build-preview-images.yml |
Note: GITHUB_TOKEN is automatically provided by GitHub Actions for pushing Docker images to ghcr.io. No manual secret configuration required.
These secrets are stored in Pulumi ESC and automatically injected into Pulumi stacks when you run pulumi config env add <environment>.
Environments:
aphiria.com/Preview- Used by preview-base and preview-pr-* stacksaphiria.com/Production- Used by production stack
| Secret Name | Purpose | Rotation Schedule | Environment |
|---|---|---|---|
digitalocean:token |
DigitalOcean API access for cluster management | Annually | Both |
gateway:digitaloceanDnsToken |
DNS API access for cert-manager wildcard TLS (DNS-01 challenges) | Annually | Both |
namespace:imagePullSecret:token |
Pull private Docker images from ghcr.io (Kubernetes imagePullSecrets) | Annually | Both |
namespace:imagePullSecret:username |
GitHub username for GHCR authentication | N/A | Both |
postgresql:user |
PostgreSQL admin user | N/A | Both |
postgresql:password |
PostgreSQL admin password | Quarterly | Both |
grafana:githubClientId |
GitHub OAuth App Client ID for Grafana authentication | When OAuth app is rotated | Both |
grafana:githubClientSecret |
GitHub OAuth App Client Secret for Grafana authentication | Annually | Both |
grafana:githubOrg |
GitHub organization for access control (e.g., "aphiria") | N/A | Both |
grafana:adminUser |
GitHub username with Grafana admin privileges | N/A | Both |
grafana:smtpHost |
SMTP server hostname for alert emails | When SMTP provider changes | Both (unused in preview) |
grafana:smtpPort |
SMTP server port (typically 587) | N/A | Both (unused in preview) |
grafana:smtpUser |
SMTP authentication username | When SMTP credentials rotate | Both (unused in preview) |
grafana:smtpPassword |
SMTP authentication password | Quarterly | Both (unused in preview) |
grafana:smtpFromAddress |
Email "From" address for alerts | N/A | Both (unused in preview) |
grafana:alertEmail |
Email address to receive Grafana alerts | N/A | Both (unused in preview) |
prometheus:authToken |
Bearer token for authenticating Prometheus to scrape /metrics endpoint | Annually | Both |
How Pulumi ESC works:
- Secrets are stored in Pulumi Cloud at https://app.pulumi.com/[org]/settings/environments
- Stack config files reference the environment:
pulumi config env add aphiria.com/Preview - When you run
pulumi up, Pulumi automatically injects ESC secrets as stack config - TypeScript code accesses them:
new pulumi.Config("postgresql").requireSecret("password") - Values are passed to Kubernetes resources (Secrets, ConfigMaps, Deployments)
namespace:imagePullSecret:token and namespace:imagePullSecret:username (Pulumi ESC - Kubernetes Image Pulling)
Why this is needed: Kubernetes clusters need credentials to pull private Docker images from ghcr.io. These values are configured in Pulumi ESC and injected into Kubernetes imagePullSecrets at runtime.
Generate new token:
- https://github.com/settings/tokens
- "Generate new token (classic)"
- Name:
GHCR Package Read/Write (aphiria.com) - Scopes:
read:packages,write:packages - Expiration: No expiration (or 1 year)
- Copy the token
Configure in Pulumi ESC environments:
Option 1: Via Pulumi ESC UI (Recommended):
- Navigate to https://app.pulumi.com/[your-org]/settings/environments
- Select
aphiria.com/Previewenvironment - Click "Edit"
- Update the
pulumiConfigsection:values: pulumiConfig: "namespace:imagePullSecret:token": fn::secret: "ghp_YOUR_NEW_TOKEN_HERE" "namespace:imagePullSecret:username": "your-github-username"
- Save
- Repeat for
aphiria.com/Productionenvironment
Option 2: Via Pulumi CLI:
# Configure for Preview environment
pulumi env set aphiria.com/Preview pulumiConfig."namespace:imagePullSecret:token" "ghp_YOUR_TOKEN" --secret
pulumi env set aphiria.com/Preview pulumiConfig."namespace:imagePullSecret:username" "your-github-username"
# Configure for Production environment
pulumi env set aphiria.com/Production pulumiConfig."namespace:imagePullSecret:token" "ghp_YOUR_TOKEN" --secret
pulumi env set aphiria.com/Production pulumiConfig."namespace:imagePullSecret:username" "your-github-username"Note: The registry URL (ghcr.io) is configured in stack-specific YAML files (Pulumi.production.yml, Pulumi.preview-pr.yml), not in ESC.
Cleanup: Delete old token at https://github.com/settings/tokens (ensure new token works first)
Why this is needed: The default GITHUB_TOKEN cannot trigger workflow_dispatch events (GitHub security policy to prevent infinite loops). A Personal Access Token with workflow scope is required to trigger the preview deployment workflow from the build workflow.
Generate new token:
- https://github.com/settings/tokens
- "Generate new token (classic)"
- Name:
Workflow Dispatch (aphiria.com) - Scopes:
workflow(orpublic_repo+workflowfor public repos) - Expiration: No expiration (or 1 year)
- Copy the token
Update repository secret:
- https://github.com/aphiria/aphiria.com/settings/secrets/actions
- Click
WORKFLOW_DISPATCH_TOKEN(or "New repository secret") - Paste new token value
- Save
Cleanup: Delete old token at https://github.com/settings/tokens
Generate new token:
- https://app.pulumi.com/settings/tokens
- "Create token"
- Name:
GitHub Actions - aphiria.com - Copy the token
Update repository secret:
- https://github.com/aphiria/aphiria.com/settings/secrets/actions
- Click
PULUMI_ACCESS_TOKEN(or "New repository secret") - Paste new token value
- Save
Cleanup: Delete old token at https://app.pulumi.com/settings/tokens
Why this is needed: cert-manager requires DigitalOcean DNS API access to create TXT records for ACME DNS-01 challenges when provisioning wildcard TLS certificates (*.pr.aphiria.com, *.pr-api.aphiria.com). Wildcard certificates cannot use HTTP-01 validation and must use DNS-01.
Generate new token:
- https://cloud.digitalocean.com/account/api/tokens
- Click "Generate New Token"
- Token name:
cert-manager DNS-01 (aphiria.com) - Scopes (REQUIRED - select these exact scopes):
- Under "Scopes" dropdown, select "Custom Scopes"
- Expand "domain" section
- ✅ Enable "domain:read" (allows cert-manager to query existing DNS records)
- ✅ Enable "domain:create" (allows cert-manager to create TXT records for ACME challenge)
- ✅ Enable "domain:delete" (allows cert-manager to cleanup TXT records after validation)
- Leave all other scopes disabled (droplet, kubernetes, etc. are not needed)
- Expiration: No expiration (or set custom expiration date)
- Click "Generate Token"
- Copy the token (starts with
dop_v1_)
Alternative: If Custom Scopes not available, use Full Access:
- Select "Full Access" (read + write to all resources)
- Note: This grants broader permissions than needed, but is acceptable for cert-manager use case
Configure in Pulumi ESC:
# Navigate to Pulumi directory
cd infrastructure/pulumi
# Configure for Preview environment
pulumi env set aphiria.com/Preview pulumiConfig."gateway:digitaloceanDnsToken" "dop_v1_YOUR_TOKEN_HERE" --secret
# Verify it's set
pulumi env get aphiria.com/Preview
# Should show: gateway:digitaloceanDnsToken: [secret]
# Configure for Production environment
pulumi env set aphiria.com/Production pulumiConfig."gateway:digitaloceanDnsToken" "dop_v1_YOUR_TOKEN_HERE" --secretAlternative: Via Pulumi ESC UI:
- Navigate to https://app.pulumi.com/[your-org]/settings/environments
- Edit
aphiria.com/Previewandaphiria.com/Production - Update
pulumiConfig."gateway:digitaloceanDnsToken"with new token (mark as secret)
Security Notes:
- Minimum required scopes:
domain:read,domain:create,domain:delete - This token grants DNS write access to your entire
aphiria.comdomain - Store securely in Pulumi ESC (never commit to git)
- Rotate annually or if compromised
Cleanup: Delete old token at https://cloud.digitalocean.com/account/api/tokens
Why this is needed: Grafana uses GitHub OAuth for authentication, restricting access to members of the aphiria GitHub organization. This eliminates the need for separate username/password management.
Generate new OAuth App:
- Navigate to https://github.com/organizations/aphiria/settings/applications
- Click "New OAuth App"
- Application name:
Grafana - aphiria.com - Homepage URL:
https://grafana.aphiria.com - Authorization callback URL:
https://grafana.aphiria.com/login/github - Click "Register application"
- Copy the Client ID (visible immediately)
- Click "Generate a new client secret"
- Copy the Client Secret (only shown once)
Configure in Pulumi ESC:
# Navigate to Pulumi directory
cd infrastructure/pulumi
# Configure for Preview environment
pulumi env set aphiria.com/Preview pulumiConfig."grafana:githubClientId" "YOUR_CLIENT_ID" --secret
pulumi env set aphiria.com/Preview pulumiConfig."grafana:githubClientSecret" "YOUR_CLIENT_SECRET" --secret
# Configure for Production environment
pulumi env set aphiria.com/Production pulumiConfig."grafana:githubClientId" "YOUR_CLIENT_ID" --secret
pulumi env set aphiria.com/Production pulumiConfig."grafana:githubClientSecret" "YOUR_CLIENT_SECRET" --secretAlternative: Via Pulumi ESC UI:
- Navigate to https://app.pulumi.com/[your-org]/settings/environments
- Edit
aphiria.com/Previewandaphiria.com/Production - Update
pulumiConfig."grafana:githubClientId"andpulumiConfig."grafana:githubClientSecret"(mark as secret)
Security Notes:
- Callback URL must match exactly (including
https://and domain) - For preview environments, the callback URL pattern is
https://{PR}.pr-grafana.aphiria.com/login/github(wildcard not supported, each PR gets unique Grafana instance) - Client secret only shown once - store securely in Pulumi ESC
- Rotate annually or if compromised
Cleanup: Delete old OAuth App at https://github.com/organizations/aphiria/settings/applications
Why this is needed: githubOrg restricts Grafana access to organization members only. adminUser grants one GitHub user full admin privileges (manage dashboards, users, data sources).
Configure in Pulumi ESC:
# Navigate to Pulumi directory
cd infrastructure/pulumi
# Configure for Preview environment
pulumi env set aphiria.com/Preview pulumiConfig."grafana:githubOrg" "aphiria"
pulumi env set aphiria.com/Preview pulumiConfig."grafana:adminUser" "your-github-username"
# Configure for Production environment
pulumi env set aphiria.com/Production pulumiConfig."grafana:githubOrg" "aphiria"
pulumi env set aphiria.com/Production pulumiConfig."grafana:adminUser" "your-github-username"Security Notes:
- Organization name is case-sensitive (must match GitHub exactly)
- Admin user must be a member of the specified organization
- These are not secrets (plain text values)
Why this is needed: Grafana sends email alerts when metrics exceed thresholds (e.g., high CPU usage, pod crashes). Uses Google Workspace SMTP with app-specific password for production monitoring.
Note: SMTP is configured in preview environments but alerts are disabled (preview uses short-lived infrastructure).
Generate Google Workspace App Password:
- Navigate to https://myaccount.google.com/apppasswords
- Sign in with your
[email protected]Google Workspace account - App name:
Grafana Alerts - aphiria.com - Click "Create"
- Copy the 16-character app password (shown once)
- Google displays it with spaces (e.g.,
abcd efgh ijkl mnop) - Remove all spaces when using it (e.g.,
abcdefghijklmnop)
- Google displays it with spaces (e.g.,
Configure in Pulumi ESC:
# Navigate to Pulumi directory
cd infrastructure/pulumi
# Configure for Preview environment (SMTP configured but unused)
pulumi env set aphiria.com/Preview pulumiConfig."grafana:smtpHost" "smtp.gmail.com" --secret
pulumi env set aphiria.com/Preview pulumiConfig."grafana:smtpPort" "587"
pulumi env set aphiria.com/Preview pulumiConfig."grafana:smtpUser" "[email protected]" --secret
pulumi env set aphiria.com/Preview pulumiConfig."grafana:smtpPassword" "YOUR_APP_PASSWORD" --secret
pulumi env set aphiria.com/Preview pulumiConfig."grafana:smtpFromAddress" "[email protected]"
pulumi env set aphiria.com/Preview pulumiConfig."grafana:alertEmail" "[email protected]"
# Configure for Production environment
pulumi env set aphiria.com/Production pulumiConfig."grafana:smtpHost" "smtp.gmail.com" --secret
pulumi env set aphiria.com/Production pulumiConfig."grafana:smtpPort" "587"
pulumi env set aphiria.com/Production pulumiConfig."grafana:smtpUser" "[email protected]" --secret
pulumi env set aphiria.com/Production pulumiConfig."grafana:smtpPassword" "YOUR_APP_PASSWORD" --secret
pulumi env set aphiria.com/Production pulumiConfig."grafana:smtpFromAddress" "[email protected]"
pulumi env set aphiria.com/Production pulumiConfig."grafana:alertEmail" "[email protected]"Alternative: Via Pulumi ESC UI:
- Navigate to https://app.pulumi.com/[your-org]/settings/environments
- Edit
aphiria.com/Previewandaphiria.com/Production - Update SMTP configuration values (mark
smtpHost,smtpUser,smtpPasswordas secret)
Security Notes:
- Use Google Workspace app password (not main account password)
- App passwords bypass 2FA (revoke immediately if compromised)
- SMTP port 587 uses STARTTLS encryption
- Gmail SMTP rate limits: 100 emails/day for free accounts, 2000/day for Workspace
- Rotate app password quarterly or if compromised
Cleanup: Revoke old app password at https://myaccount.google.com/apppasswords
Why this is needed: Prometheus needs to authenticate to the API's /metrics endpoint using Bearer token authentication. This prevents unauthorized access to application metrics.
Generate new token:
# Generate a secure random token (32 bytes = 64 hex characters)
openssl rand -hex 32
# Example output: a1b2c3d4e5f6...
# Alternative: Use base64 encoding (43 characters)
openssl rand -base64 32
# Example output: Xy9ZaBc...==Configure in Pulumi ESC:
# Navigate to Pulumi directory
cd infrastructure/pulumi
# Configure for Preview environment
pulumi env set aphiria.com/Preview pulumiConfig."prometheus:authToken" "YOUR_GENERATED_TOKEN" --secret
# Verify it's set (shows [secret])
pulumi env get aphiria.com/Preview
# Configure for Production environment
pulumi env set aphiria.com/Production pulumiConfig."prometheus:authToken" "YOUR_GENERATED_TOKEN" --secretAlternative: Via Pulumi ESC UI:
- Navigate to https://app.pulumi.com/[your-org]/settings/environments
- Edit
aphiria.com/Previewandaphiria.com/Production - Update
pulumiConfig."prometheus:authToken"with new token (mark as secret)
How it's used:
- Pulumi injects token into Kubernetes Secret
PROMETHEUS_AUTH_TOKEN(API environment variable) - API's
PrometheusTokenHandlervalidates Bearer token fromAuthorizationheader - Prometheus ServiceMonitor configured with
bearerTokenfrom same Secret - Only Prometheus can scrape
/metricsendpoint with valid token
Security Notes:
- Token is NOT hashed - stored as plaintext in Pulumi ESC, injected as-is into Kubernetes Secret
- API compares token using
hash_equals()for timing-attack protection - Use minimum 32 bytes (256 bits) of entropy for cryptographic strength
- Token transmitted over HTTPS only (TLS encryption in cluster)
- Rotate annually or if compromised
Cleanup: No cleanup needed (token is just a random value, not tied to external service)
If a secret is compromised:
- Revoke the compromised credential immediately
- Generate a new credential (see procedures above)
- Update the GitHub repository secret
- Test workflows still function
- Audit logs for unauthorized access
| Error | Cause | Solution |
|---|---|---|
| Authentication failed (build-preview-images.yml) | GITHUB_TOKEN permissions issue |
Verify packages: write permission in workflow |
| ImagePullBackOff / 401 Unauthorized (Kubernetes pods) | Invalid namespace:imagePullSecret:token in Pulumi ESC |
Add/update token in Pulumi ESC environments |
| Pulumi login failed | Invalid PULUMI_ACCESS_TOKEN |
Rotate token |
| workflow_dispatch trigger fails (403 error) | Invalid WORKFLOW_DISPATCH_TOKEN |
Rotate token |
| DNS records not created | Invalid digitalocean:token in Pulumi ESC |
Add/update token in Pulumi ESC environments |
| cert-manager DNS-01 challenge fails | Invalid gateway:digitaloceanDnsToken in Pulumi ESC |
Add/update token in Pulumi ESC environments |
Last Updated: 2025-12-23