Layered Secrets and Environment Variable Management for Cloud Functions / CloudBase Run
In one sentence: Separate the environment variables for Cloud Functions / CloudBase Run / frontend into dev / staging / prod configurations — local development uses
.env+.gitignore, CI usestcb login --apiKeyId/--apiKeyto inject permanent keys + pipeline variables, production uses Console environment variables — ensuring no secret ever enters git history or a frontend bundle.Estimated time: 30 minutes | Difficulty: Intermediate
Applicable Scenarios
- You have deployed Cloud Functions / CloudBase Run services containing LLM API keys, database passwords, or third-party credentials.
- You want to upgrade from "scattered local
.envfiles and a Console that someone updates manually for production" to a standardized workflow. - You are setting up a CI pipeline (Tencent's internal Git / CODING / GitHub Actions) and need the pipeline to access Secrets without putting them in the repository.
Not applicable:
- Solo personal projects with no compliance requirements —
.env+.gitignoreis sufficient. - Projects using a dedicated third-party secret manager (HashiCorp Vault, AWS Secrets Manager) — those have their own established workflows.
What Not to Do
Starting with what you must not do is more important than how to do it right:
| Anti-pattern | Why it's wrong |
|---|---|
| Hardcoding secrets in source code | They live in git history forever — rotating once doesn't save you; even private repositories carry this risk |
Putting secrets in package.json or package-lock.json | Same as above, and these files are almost always committed |
Leaving real values in .env.example "as reference" | Someone copies it and forgets to change the values — real secrets enter git |
| Putting API keys in WeChat Mini Program / web frontend code | Frontend code is accessible to end users via DevTools or decompilation; treat every frontend variable as public |
Debugging with console.log(process.env.OPENAI_KEY) and not removing it before commit | Logs enter monitoring systems; anyone with log access has the key |
| Sharing one production key across the whole team | When anyone leaves, the entire key set must be rotated |
| Pasting a key in a commit message or PR description "for reference" | Searching sk- in history exposes it |
Committing .env to a private repository thinking "it's not public" | Team membership changes, repository migrations, and backup leaks are all attack vectors; private does not mean secure |
Three-Layer Environment Model
┌─────────────────────┐
│ Local Dev │ .env (local) + .gitignore + .env.example (template)
└─────────────────────┘
↓
┌─────────────────────┐
│ CI Pipeline │ Pipeline variables (Tencent's internal Git / CODING) + tcb login --apiKeyId/--apiKey
└─────────────────────┘
↓
┌─────────────────────┐
│ Production (prod) │ CloudBase Console "Environment Variables"
└─────────────────────┘
Each layer explained below.
Layer 1: Local Development
.env + .gitignore
Create these in the project root:
# Local dev config (never commit this)
touch .env
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo "*.env" >> .gitignore
.env content (adjust key names to your project):
# LLM gateway proxy token for local dev
PROXY_ACCESS_TOKEN=local-dev-token-xxxxxxxxxxxxxxxx
# Real upstream LLM key
UPSTREAM_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
# Local dev PostgreSQL connection (local Docker instance)
PG_HOST=127.0.0.1
PG_PASSWORD=local-pg-password
Commit a template for teammates — with no real values:
touch .env.example
.env.example:
# Copy this file to .env and fill in real values
PROXY_ACCESS_TOKEN=
UPSTREAM_API_KEY=
PG_HOST=
PG_PASSWORD=
.env.example is safe to commit — it's just a skeleton with no values. New team members clone the repo, run cp .env.example .env, then fill in values from internal documentation.
Reading .env in Node.js code
Install dotenv in Cloud Function and CloudBase Run Node.js projects:
npm install --save-dev dotenv
At the very top of the entry file:
// Only load .env in local dev; in CloudBase, environment variables are injected by the platform
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
const apiKey = process.env.UPSTREAM_API_KEY;
When deployed to CloudBase, NODE_ENV=production is set by the platform by default, so it won't attempt to read .env. This prevents accidentally bundling .env into the deployment and having it loaded.
.gitignore Checklist
Before any commit, verify .gitignore includes at least these (adjust as needed):
# Environment variables
.env
.env.*
!.env.example
*.env
# CloudBase CLI cache
.cloudbase/
cloudbaserc.json.local
# Auto-generated key files
tcb_custom_login.json
*.pem
*.key
firebase-adminsdk-*.json
service-account*.json
!.env.example is the exception that allows the template file into git.
Layer 2: CI Pipeline
CI cannot use a local .env — it runs on a separate machine with no .env in the repository. Use pipeline variables instead.
Configuring pipeline variables
Using Tencent's internal Git (CODING / GitLab works the same) as an example:
- Repository → "Settings → Pipeline → Pipeline Variables"
- Add variables, e.g.
TCB_DEPLOY_TOKEN,PROD_UPSTREAM_API_KEY,PROD_PG_PASSWORD - Enable "Protected" + "Masked" (Protected limits the variable to protected branches; Masked shows
***in logs)
Non-interactive login for CloudBase CLI in CI
CloudBase CLI cannot use the browser login flow in CI (no one to click Confirm). Use tcb login --apiKeyId/--apiKey to explicitly inject Tencent Cloud permanent keys (distinct from the local browser-based tcb login):
# 1. Non-interactive login with permanent keys
tcb login --apiKeyId $TENCENT_SECRET_ID --apiKey $TENCENT_SECRET_KEY
# 2. Deploy
tcb fn deploy llm-proxy --httpFn -e your-env-id
Create TENCENT_SECRET_ID / TENCENT_SECRET_KEY in the Tencent Cloud CAM Console. Use these only for CI deployment — developers should use the browser-based tcb login locally (temporary credentials, following least privilege).
Never echo $TENCENT_SECRET_KEY in CI steps — if logs are ever exposed, the key is public. Pipeline masking is a last line of defense, not a primary control.
Pushing application-layer secrets to Cloud Functions in CI
Deploy code and inject business secrets:
# 1. Log in CLI with permanent keys
tcb login --apiKeyId $TENCENT_SECRET_ID --apiKey $TENCENT_SECRET_KEY
# 2. Deploy function code
tcb fn deploy llm-proxy --httpFn -e your-env-id
# 3. Update environment variables via CloudBase OpenAPI / @cloudbase/manager-node
# (See manager-node's functions.updateFunctionConfig for the specific API)
Alternatively, write environment variables into cloudbaserc.json's functions.envVariables field to have them updated during deployment. Do not commit secrets from cloudbaserc.json to git — use cloudbaserc.local.json + .gitignore, or replace placeholder values with real ones in CI using envsubst / sed before deployment.
Refer to the current version of @cloudbase/cli for exact commands — the environment variable management subcommands vary across CLI versions; check tcb fn --help for actual options.
Layer 3: Production Environment
Production secrets ultimately live in CloudBase Console Environment Variables:
| Resource | Configuration path |
|---|---|
| Cloud Functions (event / Web) | Console → Cloud Functions → Function Details → Function Config → Environment Variables |
| CloudBase Run services | Console → Cloud Hosting → Service Details → Service Settings → Version Management → configure when creating a new version |
Cloud Hosting and Cloud Functions handle environment variables differently:
| Dimension | Cloud Functions | CloudBase Run |
|---|---|---|
| Effect after change | Takes effect on next invocation (hot-updated on instance) | Requires publishing a new version and shifting traffic |
| Bound to version | No (function-level) | Yes (per service version, each version is independent) |
| Dockerfile default | N/A | ENV KEY=VALUE sets defaults; Console variables of the same name take precedence |
Cross-environment (dev / staging / prod) naming convention
If production, staging, and dev each have their own CloudBase Environment, use the same variable name in each — the same key UPSTREAM_API_KEY holds different values in prod vs. staging automatically.
If all three environments share one CloudBase Environment (not recommended but common), use prefixes:
PROD_UPSTREAM_API_KEY=...
STAGING_UPSTREAM_API_KEY=...
DEV_UPSTREAM_API_KEY=...
Select the right one in code based on process.env.STAGE:
const stage = process.env.STAGE || 'dev';
const apiKey = process.env[`${stage.toUpperCase()}_UPSTREAM_API_KEY`];
Recommended: use three separate Environments. Resource isolation is always more reliable than naming conventions.
Special note on frontend secrets
Any environment variable that appears in frontend code is not a secret, including:
- Next.js
NEXT_PUBLIC_* - Vite
VITE_* - Any config value in a WeChat Mini Program
These are statically embedded into JS bundles at build time. Anyone can read them with browser DevTools or the WeChat developer tool.
The correct approach: let the frontend only hold a public endpoint URL; keep secrets in Cloud Function / CloudBase Run backends. For example:
Wrong: const OPENAI_KEY = 'sk-xxx'; // in Mini Program code
Correct: const PROXY_URL = 'https://your-env.service.tcloudbase.com/llm-proxy'; // in Mini Program code
// OPENAI_KEY stays in Cloud Function environment variables
If you have already hardcoded a key in frontend code, that key is compromised. No code change undoes this — you must rotate it.
What to Do If a Key Is Already Leaked
Rotation procedures differ by service. The most common ones:
OpenAI
- Log in to platform.openai.com → API Keys
- Find the leaked key and click Revoke (immediately invalidated — all calls return 401)
- Create a new secret key
- Update the key in every Cloud Function / CloudBase Run Environment Variable that uses it
- Check the billing page for anomalous usage during the exposure window
Tencent Cloud CAM Permanent Keys
- CAM Console → Access Keys
- Find the leaked SecretId, click "Disable", then delete it
- Create a new SecretId / SecretKey pair
- Update CI pipeline variables
- Immediately check Operation Logs for anomalous API calls during the exposure window
WeChat Mini Program AppSecret
- WeChat Open Platform → Development Settings → Reset AppSecret
- The old AppSecret is immediately invalidated (no grace period, unlike OpenAI — this is a hard cut)
- Immediately update all Cloud Function environment variables that use it and redeploy; otherwise production breaks instantly
Database Password
- Log in via DMC tool → Account Management → Reset Password
- Update all Cloud Function / CloudBase Run environment variables that use it
- Existing sessions remain connected until they close, but new connections will fail immediately
After every rotation, do one more thing: investigate how the key ended up in git. Was it a missed commit check? Did someone accidentally treat .env as .env.example? Was CI log masking not enabled? The root cause matters more than the rotation — otherwise it will happen again.
Pre-production Checklist
Run through this before any production deployment:
-
.gitignoreincludes.env,.env.*(except.env.example),tcb_custom_login.json,*.pem - Searched repository history for
sk-,AKID,-----BEGIN PRIVATE— confirmed no secrets remain (git log -S "sk-" -- '*.js') - CI uses permanent keys + masking; local dev uses
tcb loginweb-flow - No API keys in frontend code (grep for
sk-,Bearer,apiKey:) - All Cloud Function / CloudBase Run environment variables are injected from Console or CI — none in code
- Team has shared documentation covering which variable lives under which CAM sub-account and who owns rotation
- Critical keys (database / payment) have usage alerts configured to trigger on anomalous volume
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
process.env.X is undefined after deployment | Console environment variable not added, or function not redeployed | After adding a variable in the Console, trigger a redeployment (Cloud Functions pull new values, but redeploying is the safe path) |
Changed NEXT_PUBLIC_*, restarted service, frontend still has old value | These variables are injected at build time — restart does not update them | Re-run npm run build and redeploy |
.env accidentally committed to git | .gitignore was misconfigured | git rm --cached .env, add to .gitignore, then rotate all exposed keys (they remain visible in git history — adding to .gitignore does not erase history) |
CI logs show *** but deployed function returns 401 | CI variable name was mistyped — masking hides the mistake | Add a diagnostic step like `printenv |
cloudbaserc.json placeholder not replaced before deploy | Placeholder went directly into Cloud Function environment variables | Add a grep step in CI to check for \${.*} unresolved markers before deploying |
| Production Mini Program login breaks immediately after AppSecret reset | Cloud Function environment variables not updated first | Enforce the order: "update environment variables first, then reset AppSecret" |
| Same key used in both staging and production | No environment isolation | Use three separate CloudBase Environments for proper isolation |
Related Documentation
- Cloud Function Environment Variables — Configuration and limits
- CloudBase Run Environment Variables — Service-level env settings
- CloudBase CLI Temporary Keys — Using
tcb secrets getto initialize the SDK in local scripts - CloudBase CLI Installation and Login —
tcb login/ login methods - Tencent Cloud CAM Access Keys — Creating and revoking permanent keys
Next Steps
Once this baseline is in place, consider:
connect-openai-api-cloud-function— The best-practice reference for LLM key isolation; all rules from this recipe apply directly.deploy-nextjs-to-cloudbase-run— Environment variable layering for Next.js (NEXT_PUBLIC_*vs. server-side).add-rag-with-pgvector-cloudbase— A compound scenario managing both database passwords and LLM keys simultaneously.