Skip to content

Tokens, Keys, Secrets, and Environment Variables

These four terms are often used interchangeably, but they describe different things with different lifetimes, scopes, and storage requirements. Confusing them leads to real problems: secrets committed to git, tokens stored with too-broad a scope, env vars used where a secret manager is needed.

The definitions

Environment variable

A named string in a process’s environment, inherited by child processes. Not inherently secret — PATH, HOME, and PORT are all environment variables. The confusion arises because env vars are a common delivery mechanism for secrets: you inject a secret into a process at startup via an env var, so the secret never appears in source code.

Terminal window
export DATABASE_URL="postgres://user:pass@host:5432/db"
node server.js # server reads process.env.DATABASE_URL

Env vars have no built-in access control, encryption, or audit log. They are visible to any code running in the process, often appear in crash dumps and debug output, and are inherited by child processes. They are a convenience, not a security primitive.

Secret

Any value that must not be exposed: passwords, private keys, connection strings, cryptographic seeds. “Secret” is the category. The others are types of secret.

Secrets need:

  • Encryption at rest (not just Base64)
  • Access control (only specific services can read them)
  • Audit logging (who read what, when)
  • Rotation (ability to change without redeployment)

A hardcoded database password in source code is a secret that is being handled wrongly. An env var injected at runtime from a secrets manager is the same secret handled correctly.

API key

A long random string that identifies and authenticates a caller to an API. API keys are typically:

  • Stateless: the API server validates the key on every request without session state
  • Long-lived: they do not expire automatically (though they should be rotatable)
  • Coarse-grained: one key usually grants access to an entire API or a predefined permission set
  • Not user-specific: they identify an application or integration, not a human
Authorization: Bearer sk_live_<your_api_key_here>

API keys are secrets. They should be stored in a secrets manager, injected via env var at runtime, never committed to source code, and rotated on any suspected exposure.

Token

A credential that represents a grant of access, usually time-limited. Tokens come in two shapes:

Opaque tokens are random strings. The server validates them by looking them up in a database or cache. Session cookies and OAuth access tokens are often opaque.

Structured tokens encode claims directly. A JWT (JSON Web Token) contains a header, payload (claims like sub, exp, scope), and a signature. The server validates the signature without a database lookup, then reads the claims.

<jwt_header>.<jwt_payload>.<jwt_signature>

Tokens are typically:

  • Short-lived: they expire (minutes to hours for access tokens)
  • Scoped: they encode what the bearer is allowed to do
  • User or session-specific: one token per user session, not per application

A refresh token is a longer-lived credential used to get new access tokens. It is a secret and must be stored securely.

Signing key / secret key

A cryptographic key used to sign or verify data (HMAC, RSA, ECDSA). It is never sent over the wire — it lives server-side and is used to produce or verify signatures.

JWT signing keys are a common example: the server signs tokens with a private key. Clients (or other servers) verify with the corresponding public key. If the signing key leaks, an attacker can forge any token the key would sign.

Signing keys are secrets with an even stricter rotation requirement than API keys.

Personal access token (PAT)

A token that represents a human identity on a platform (GitHub PAT, GitLab PAT, Netlify token). PATs are scoped to specific permissions and are revocable. They are used in place of passwords for API access, CI/CD pipelines, and automation.

PATs are secrets. They should be stored in a secrets manager, not hardcoded in scripts or CI config files.

Comparison table

TypeIdentifiesExpiresScopeWhere to store
API keyApplication/integrationRarely (manually rotated)Usually coarseSecrets manager
Access tokenUser sessionYes (minutes to hours)Fine-grained (scopes)Memory only (never persisted)
Refresh tokenUser sessionYes (days to weeks)ScopedSecrets manager / secure cookie
JWTBearer’s claimsYes (embedded in payload)Claims-encodedMemory only (validate, don’t store)
PATHuman identityConfigurablePlatform-definedSecrets manager
Signing keyServer identityNo (manually rotated)N/A (server-only)Secrets manager
Env varN/A (delivery mechanism)N/AN/ASource is the secret, not the var

Where secrets actually live

Dev machine CI/CD Production
----------- ----- ----------
.env file GitHub Secrets AWS Secrets Manager
(git-ignored) (encrypted) GCP Secret Manager
HashiCorp Vault
Azure Key Vault
Kubernetes Secrets
(base64, not encrypted by default)
All injected as env vars at runtime

The flow is always: secret lives in a manager → injected as env var at startup → read once into memory → used. The env var is a transport layer, not storage.

Common mistakes

Committing secrets to git. The most common exposure. .env files, hardcoded connection strings, API keys in config files. Even a “private” repo is a risk — access spreads, and git history is permanent. Use .gitignore for .env and scan with tools like trufflehog or gitleaks.

Logging secrets. Logging request.headers or process.env in a handler logs every secret the process has. Redact or allowlist what gets logged.

Broad token scopes. An API key or PAT with admin scope is a single point of catastrophic failure. Use the minimum scope needed.

Long-lived tokens where short-lived ones would work. If a token can expire in 15 minutes, it should. An expired token that leaks is useless. An API key with no expiry that leaks is a permanent credential.

Kubernetes Secrets are not secret by default. They are base64-encoded, not encrypted. Anyone with kubectl get secret access can decode them. Enable encryption at rest and use external secrets operators (External Secrets Operator, Sealed Secrets) for production clusters.

Storing tokens in localStorage. Accessible to any JavaScript on the page (XSS risk). Prefer httpOnly cookies for session tokens.

Sharing secrets via Slack, email, or clipboard. Use a secret manager’s sharing mechanism or a one-time-use tool like onetimesecret.com for emergencies.

The injection pattern

The correct runtime pattern for every environment:

Terminal window
# Dev: .env file (git-ignored), loaded by dotenv or equivalent
DATABASE_URL=postgres://localhost/mydb
# CI: platform secrets (GitHub Actions, GitLab CI)
# Set in repository settings UI, referenced as:
${{ secrets.DATABASE_URL }}
# Production: secrets manager + env injection
# AWS example (ECS task definition):
{
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:prod/db-url"
}
]
}

The application code reads process.env.DATABASE_URL (or os.environ["DATABASE_URL"]) in all environments. The source of truth changes per environment. The application does not.

References