← All articles

Securing Secrets in Serverless Functions: The Credential Risks Most Teams Overlook

June 19, 2026

Why Serverless Is a Different Kind of Secret Risk

Serverless computing strips away a lot of infrastructure complexity, but it doesn't strip away the problem of credential management. If anything, it makes it worse. Developers moving fast in Lambda, Google Cloud Functions, or Azure Functions often reach for the most convenient solution—pasting a secret into an environment variable via a UI or CLI—without considering how that value propagates, where it's stored, and who can read it.

This article walks through the specific ways secrets leak in serverless environments and gives you concrete steps to close each gap.

How Secrets Actually End Up in Serverless Functions

The attack surface looks different from a traditional application server, but the underlying patterns are familiar:

  • Hardcoded in function source: A developer embeds a key directly in the handler file. The code gets pushed to a repo, reviewed in a PR, and suddenly your database password is in five engineers' local git caches.
  • Environment variables set via IaC: Terraform, Serverless Framework, or AWS SAM templates store the plaintext value of a secret in a environment: block, which ends up committed alongside the rest of the stack definition.
  • Console-set variables captured in snapshots: Cloud provider audit logs, deployment packages, and version snapshots can preserve the state of environment variables at deploy time. In AWS Lambda, prior function versions retain their own environment variable configuration—indefinitely, unless you explicitly delete them.
  • Bundled into deployment packages: Build pipelines that zip and upload function code sometimes include .env files, local config files, or node_modules sub-packages that carry their own embedded tokens.

The Lambda-Specific Risks You Should Know

Environment Variables Are Encrypted at Rest—But Not From Your Own IAM

AWS encrypts Lambda environment variables using a default KMS key, and that's often where the security story ends in documentation. What it doesn't tell you prominently: any principal with lambda:GetFunctionConfiguration permission can retrieve those values in plaintext. That includes overly permissive CI/CD roles, developer IAM users, and any compromised credential that has been granted access to your Lambda namespace.

Mitigation steps:

  1. Audit who holds lambda:GetFunctionConfiguration and scope it down aggressively.
  2. Use a customer-managed KMS key and restrict its kms:Decrypt policy separately from the Lambda execution role.
  3. Prefer fetching secrets at runtime from AWS Secrets Manager or Parameter Store (SecureString) rather than baking them into environment variables at all.

Old Function Versions Are a Secret Graveyard

Every time you deploy a new version of a Lambda function, AWS preserves the prior version—including its environment variables. If you rotated a secret last month and updated the environment variable, version $LATEST - 1 still holds the old value. If an attacker or an overly curious employee enumerates your published versions, those stale credentials are readable.

To address this, build a cleanup step into your deployment pipeline:

aws lambda list-versions-by-function \
  --function-name my-function \
  --query 'Versions[?Version!=`$LATEST`].[Version]' \
  --output text | xargs -I{} \
  aws lambda delete-function --function-name my-function --qualifier {}

This keeps only the current version and removes historical snapshots that may carry rotated-but-not-revoked credentials.

Serverless Framework and IaC: Where the Config Becomes the Vulnerability

The Serverless Framework's serverless.yml and AWS SAM's template.yaml are frequently the actual source of a credential leak. A pattern like this appears in countless repos:

functions:
  api:
    handler: src/handler.main
    environment:
      STRIPE_SECRET_KEY: sk_live_abc123xyz   # ← this is now in git forever
      DATABASE_URL: postgres://user:pass@host/db

Even if the developer later switches to a parameter reference, the original plaintext value lives in git history. Tools like git log -S "sk_live" will surface it instantly.

The correct pattern is to reference a secret store, never inline a value:

environment:
  STRIPE_SECRET_KEY: ${ssm:/myapp/prod/stripe_secret_key~true}
  DATABASE_URL: ${ssm:/myapp/prod/database_url~true}

The ~true suffix tells Serverless Framework to decrypt a SecureString parameter. The actual secret value never touches your repository.

Google Cloud Functions and Azure: Similar Patterns, Different Controls

Google Cloud Functions uses runtime environment variables set via the gcloud CLI or the console. These are stored in Cloud Functions service metadata and are accessible to anyone with the cloudfunctions.functions.get IAM permission. The recommended pattern is to use Secret Manager and grant only the function's service account access to the specific secret version it needs.

Azure Functions has App Settings, which are encrypted but accessible to anyone with the Contributor role on the Function App resource. Prefer Azure Key Vault references in App Settings, which store only the vault reference string—not the secret value—in the function configuration:

@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/MySecret/)

This way, a misconfigured IAM role that exposes App Settings metadata reveals nothing actionable.

Scanning Serverless Codebases: What to Look For

A targeted scan of a serverless project should cover:

  • All IaC definition files (serverless.yml, template.yaml, main.tf) for inline secret patterns
  • Deployment packages and zip artifacts before they're uploaded
  • Git history for secrets that were removed from current files but remain in prior commits
  • Environment variable exports in CI/CD pipeline definitions (.github/workflows/, buildspec.yml, cloudbuild.yaml)
  • Any .env, .env.production, or config/secrets.* files inadvertently included in the function bundle

If you haven't audited your serverless repos recently, run a free GhostCred scan to surface exposed keys and misconfigurations across your codebase in under a minute.

Runtime Secret Fetching: The Right Mental Model

The goal is to ensure that no secret value ever exists in a static, readable artifact—not in source code, not in a deployment package, not in a version snapshot. The architecture that achieves this looks like:

  1. Store secrets in a managed vault (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault).
  2. Grant the function's execution identity (IAM role, service account) the minimum permission to read only the specific secrets it needs.
  3. Fetch at cold start, cache in memory for the function lifetime, and refresh on a reasonable TTL. Most SDKs for Secrets Manager make this straightforward.
  4. Never log the fetched value. Instrument your logging to mask values that match known secret patterns—this is a separate problem from storage but an equally important one.

A Quick Pre-Deploy Checklist

  • ☐ No plaintext secrets in serverless.yml, template.yaml, or *.tf files
  • .env files are in .gitignore and excluded from the deployment zip
  • ☐ Lambda function versions are cleaned up on each deploy
  • ☐ IAM permissions for GetFunctionConfiguration and equivalents are scoped to a minimal set of principals
  • ☐ Secrets Manager / Parameter Store references are used instead of inline environment variable values
  • ☐ CI/CD pipeline variables containing secrets are masked and never echoed to build logs

The Bottom Line

Serverless abstracts away the server, but it does not abstract away the responsibility for credential hygiene. The convenience of inline environment variables is a trap that's caught more teams than it should. Building a habit of never inline, always reference from the start of a project costs almost nothing—retrofitting it after a breach costs considerably more.

Start with what you have. Audit your IaC files, check your function version history, and tighten the IAM permissions around configuration reads. The attack surface is manageable once it's visible.

See what's exposed in your own code.

Run a free scan