Skip to content

Secrets Management β€” Lantern ​

Lantern uses GCP Secret Manager as its single source of truth for all runtime secrets. No third-party secret broker (Infisical, Vault, etc.) is needed because the entire backend already runs on Google Cloud (Firebase Cloud Functions, Cloud Run, Cloud IAM).


Why GCP Secret Manager? ​

ConcernAnswer
Already on GCP?Yes β€” Firebase, Cloud Run, Cloud Functions all live in GCP projects.
Keyless auth from CIWorkload Identity Federation (WIF) lets GitHub Actions authenticate without a stored JSON key.
Audit trailEvery secret access is logged in Cloud Audit Logs.
Version historySecrets are versioned; roll back or rotate without redeploying.
CostNegligible β€” free tier covers normal usage.

Adding a third-party broker like Infisical would introduce an extra hop, another vendor dependency, and another credential to manage. GCP Secret Manager already satisfies the requirement.


Secrets by layer ​

1. Firebase Cloud Functions (server-side) ​

All sensitive values consumed by Cloud Functions are stored in Secret Manager and referenced via defineSecret from firebase-functions/params.

js
// services/functions/firebase/config.js
import { defineSecret } from 'firebase-functions/params'

const githubToken      = defineSecret('GITHUB_TOKEN')
const discordWebhookUrl = defineSecret('DISCORD_WEBHOOK_URL')
const resendApiKey     = defineSecret('RESEND_API_KEY')
const cloudflareApiToken  = defineSecret('CLOUDFLARE_API_TOKEN')
const cloudflareZoneId    = defineSecret('CLOUDFLARE_ZONE_ID')
const cloudflareAccountId = defineSecret('CLOUDFLARE_ACCOUNT_ID')
const railwayApiToken  = defineSecret('RAILWAY_API_TOKEN')

Each function that needs a secret must declare it in its options:

js
export const myFunction = onCall(
  { ...callableOptions, secrets: [githubToken, discordWebhookUrl] },
  async (request) => {
    // Firebase injects the value into process.env at runtime:
    const token = process.env.GITHUB_TOKEN
  }
)

Firebase handles the Secret Manager IAM grant automatically when you firebase deploy β€” the Cloud Run service account for that function is granted roles/secretmanager.secretAccessor for the declared secrets.

⚠️ If a function calls getConfig() or reads process.env.GITHUB_TOKEN without declaring secrets: [githubToken], the value will be undefined at runtime (Secret Manager values are not in the environment by default).

Adding a new secret to Cloud Functions ​

bash
# 1. Create the secret (dev project)
echo -n "my-value" | gcloud secrets create MY_SECRET \
  --data-file=- \
  --project=lantern-app-dev

# 2. Create the secret (prod project)
echo -n "my-value" | gcloud secrets create MY_SECRET \
  --data-file=- \
  --project=lantern-app-prod

# 3. Register it in config.js
const mySecret = defineSecret('MY_SECRET')
export { mySecret }

# 4. Declare it on every function that needs it
{ ...callableOptions, secrets: [mySecret] }

# 5. Deploy
firebase deploy --only functions --project lantern-app-dev

2. Cloud Run services (server-side) ​

Cloud Run services authenticate to GCP using the attached service account (Workload Identity Federation from CI, or the default compute SA on Cloud Run).

For secrets needed at runtime, use --update-secrets when deploying:

bash
gcloud run deploy my-service \
  --update-secrets=MY_SECRET=MY_SECRET:latest \
  --region us-central1 \
  --project lantern-app-dev

This mounts the secret as an environment variable without it appearing in the Cloud Run configuration UI or deployment logs.

Current Cloud Run services (venue-api, analytics-api, lanterns-api, docs-api) only require non-sensitive env vars (FIREBASE_PROJECT_ID, NODE_ENV). Add --update-secrets when a sensitive value is needed.


3. GitHub Actions CI/CD (build-time) ​

GitHub repository secrets (Settings β†’ Secrets and variables β†’ Actions) are used only for values that must be present at build time (e.g. VITE_* Firebase web config keys baked into the Vite bundle).

Secret categoryWhere storedNotes
Firebase web config (VITE_*)GitHub SecretsPublic keys β€” safe in bundle; Firebase restricts by domain/security rules
Cloudflare deploy tokenGitHub SecretsUsed by wrangler-action during deployment
Firebase CLI tokenGitHub SecretsFIREBASE_TOKEN for deploying rules/functions
GCP auth (Cloud Run deploys)WIF β€” no stored keyWIF_PROVIDER_DEV/PROD + WIF_SERVICE_ACCOUNT_DEV/PROD
Runtime secretsGCP Secret ManagerNever passed through GitHub Actions

WIF (Workload Identity Federation) means there is no JSON service account key stored in GitHub Secrets for any GCP operation performed during deployment. This is already the pattern in all deploy-dev.yml and deploy-prod.yml workflows.


Local development β€” replacing .env.local ​

.env.local has historically required developers to manually copy tokens from dashboards, shared docs, or colleagues. With the secrets already in GCP Secret Manager (and the public Firebase config available via the Firebase Management API), you can populate .env.local automatically:

bash
# One-time: Google account β†’ GCP Secret Manager
gcloud auth application-default login

# One-time: Google account β†’ Firebase Management API (for VITE_FIREBASE_*)
firebase login

# Pull all shared values into .env.local
npm run env:bootstrap

The bootstrap script (tooling/scripts/bootstrap-env.mjs) connects to the lantern-app-dev GCP project, fetches every secret listed in SECRET_MAP, calls firebase apps:sdkconfig WEB for the public Firebase config, and merges everything into your .env.local.

What gets bootstrapped vs. what's manual ​

CategoryExamplesHow to get
Server-side secretsCloudflare tokens, Resend/Anthropic API keys, GitHub App credentials, reCAPTCHA keys, Railway token, Discord webhook, email encryption keynpm run env:bootstrap β€” pulls from GCP Secret Manager
Public Firebase configVITE_FIREBASE_* (API key, auth domain, project ID, etc.)npm run env:bootstrap β€” auto-fetched via firebase apps:sdkconfig WEB (falls back to manual copy if firebase-tools isn't installed)
Manual (per-developer)GH_PAT (your personal GitHub token), GOOGLE_APPLICATION_CREDENTIALSSet by hand β€” these differ per developer
Other public configAPI origins (VENUE_API_ORIGIN etc.), GITHUB_REPOSet once from .env.local.example β€” project-shape constants

πŸ’‘ The GOOGLE_APPLICATION_CREDENTIALS path can be avoided entirely. Running gcloud auth application-default login creates ADC credentials that Firebase Admin SDK and gcloud CLI discover automatically β€” no explicit path needed in .env.local for most tooling scripts.

Additional bootstrap flags ​

bash
# Preview what would be written (no file changes)
npm run env:bootstrap:dry-run

# Overwrite values that are already set (e.g. after a rotation)
npm run env:bootstrap:force

# Use the prod project instead of dev
node tooling/scripts/bootstrap-env.mjs --project=lantern-app-prod

IAM requirement ​

The bootstrap script needs roles/secretmanager.secretAccessor on the lantern-app-dev project. Ask a project owner to grant your Google account:

bash
gcloud projects add-iam-policy-binding lantern-app-dev \
  --member="user:you@example.com" \
  --role="roles/secretmanager.secretAccessor"

Adding a new secret to the bootstrap flow ​

  1. Create the secret in both GCP projects (see "Adding a new secret" above).
  2. Add an entry to SECRET_MAP in tooling/scripts/bootstrap-env.mjs:
    js
    MY_NEW_VAR: 'MY_SECRET_NAME',   // .env.local key β†’ GCP secret name
  3. Add a [secret β†’ bootstrap fills this] annotation to .env.local.example.
  4. Run npm run env:sync to keep .env.local.example ordered correctly.

Before vs. after ​

Before (manual setup, ~15 min):

  1. Ask a teammate for the Cloudflare token
  2. Log into Resend, copy the API key
  3. Find the Discord webhook URL in the server settings
  4. Copy 7 VITE_FIREBASE_* values from Firebase Console one at a time
  5. Repeat for every other token…

After (with Secret Manager + Firebase Management API, ~2 min):

bash
gcloud auth application-default login   # one-time, for Secret Manager
firebase login                            # one-time, for VITE_FIREBASE_*
npm run env:bootstrap                    # done β€” pulls 25 values

If firebase-tools isn't installed, the script gracefully degrades and prints the npm install -g firebase-tools hint β€” only VITE_FIREBASE_* need manual copy in that case.


Local development ​

Copy .env.local.example to .env.local and fill in your dev values:

bash
cp .env.local.example .env.local

Local Cloud Functions development uses .runtimeconfig.json (gitignored) or the Firebase Emulator with .env files in services/functions/firebase/. See the Cloud Functions emulator docs for details.


Rotation procedure ​

  1. Create a new secret version in GCP Secret Manager (UI or gcloud secrets versions add).
  2. Disable the old version once the new deployment is confirmed healthy.
  3. For GitHub Actions secrets, update the value in Settings β†’ Secrets.
  4. Redeploy the affected function/service if not using :latest auto-resolution.

Threat model notes ​

  • Secrets never leave GCP for Cloud Functions/Cloud Run workloads.
  • VITE_ values are intentionally public* β€” they identify the Firebase project to the browser SDK. Restrict them via Firebase security rules and Authorized Domains in the Firebase console, not by keeping them secret.
  • Audit logs: enable Cloud Audit Logs β†’ Data Access for secretmanager.googleapis.com in both GCP projects to capture every accessSecretVersion call.

Built with VitePress