PCSalt
YouTube GitHub
Back to Security
Security · 3 min read

Secrets Management — Environment Variables, Vault & Sealed Secrets

How to manage secrets in production — environment variables, HashiCorp Vault, Kubernetes Sealed Secrets, and patterns for keeping credentials out of code.


Every application has secrets — database passwords, API keys, JWT signing keys, encryption keys. The question is where they live and who can access them.

The wrong answer: committed to Git, hardcoded in source, or shared in Slack. The right answer depends on your infrastructure, but the principles are universal.

The hierarchy of secrets management

From simplest to most secure:

Level 0: Hardcoded in source code        ← NEVER do this
Level 1: .env files (gitignored)          ← Local development only
Level 2: Environment variables            ← Simple deployments
Level 3: Encrypted config files           ← CI/CD, Kubernetes
Level 4: Secrets manager (Vault, AWS SM)  ← Production systems

Move up the hierarchy as your system grows. Starting at Level 2 is reasonable for most projects.

Level 1: .env files for local development

# .env (NEVER commit this file)
DATABASE_URL=postgresql://localhost:5432/mydb
DATABASE_USER=admin
DATABASE_PASSWORD=local_dev_password
JWT_SECRET=dev-secret-not-for-production
STRIPE_API_KEY=sk_test_...

.gitignore:

.env
.env.local
.env.*.local

Provide a template so teammates know what’s needed:

# .env.example (commit this)
DATABASE_URL=postgresql://localhost:5432/mydb
DATABASE_USER=
DATABASE_PASSWORD=
JWT_SECRET=
STRIPE_API_KEY=

Spring Boot

# application.yml
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}

jwt:
  secret: ${JWT_SECRET}

Spring Boot reads environment variables automatically. ${DATABASE_URL} resolves from the environment.

Ktor

val dbUrl = System.getenv("DATABASE_URL")
    ?: throw IllegalStateException("DATABASE_URL not set")

Level 2: Environment variables

Set secrets in your deployment environment — not in code.

Docker

# Don't put secrets in Dockerfile
# Use environment variables at runtime
CMD ["java", "-jar", "app.jar"]
docker run \
  -e DATABASE_URL=postgresql://prod:5432/mydb \
  -e DATABASE_PASSWORD=prod_password \
  myapp:latest

Docker Compose

# docker-compose.yml
services:
  app:
    image: myapp:latest
    env_file:
      - .env.production
    environment:
      - DATABASE_URL=postgresql://db:5432/mydb

CI/CD (GitHub Actions)

Store secrets in GitHub repository settings → Secrets:

# .github/workflows/deploy.yml
jobs:
  deploy:
    steps:
      - name: Deploy
        env:
          DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
        run: deploy.sh

Secrets are masked in logs. They’re available to the workflow but never printed.

Level 3: Encrypted config files

Kubernetes Secrets

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  database-password: cHJvZF9wYXNzd29yZA==  # base64 encoded
  jwt-secret: bXktand0LXNlY3JldA==

Mount as environment variables:

# deployment.yaml
spec:
  containers:
    - name: app
      env:
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-password

Important: Kubernetes Secrets are base64-encoded, not encrypted. Anyone with access to the cluster can read them. For actual encryption, use Sealed Secrets or an external secrets manager.

Sealed Secrets (Kubernetes)

Bitnami Sealed Secrets encrypts secrets so they can be safely committed to Git:

# Encrypt a secret
kubeseal --format yaml < secret.yaml > sealed-secret.yaml

The sealed secret is encrypted with the cluster’s public key. Only the Sealed Secrets controller in the cluster can decrypt it:

# sealed-secret.yaml (safe to commit)
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: app-secrets
spec:
  encryptedData:
    database-password: AgBy8h3k...encrypted...
    jwt-secret: AgCf9d2l...encrypted...

Commit sealed-secret.yaml to Git. The controller decrypts it into a regular Kubernetes Secret at deploy time.

Level 4: Secrets managers

HashiCorp Vault

Vault is a dedicated secrets management tool. Secrets are stored encrypted, access is audited, and secrets can be rotated automatically.

# Store a secret
vault kv put secret/myapp/database password=prod_password

# Read a secret
vault kv get secret/myapp/database

Application integration

// Spring Cloud Vault
@Configuration
class VaultConfig {
    @Value("\${database.password}")
    lateinit var databasePassword: String
}
# bootstrap.yml
spring:
  cloud:
    vault:
      host: vault.example.com
      port: 8200
      authentication: token
      token: ${VAULT_TOKEN}
      kv:
        enabled: true
        backend: secret
        application-name: myapp

Spring Cloud Vault fetches secrets from Vault at startup and injects them as properties.

AWS Secrets Manager

val client = SecretsManagerClient.builder()
    .region(Region.US_EAST_1)
    .build()

val secret = client.getSecretValue(
    GetSecretValueRequest.builder()
        .secretId("myapp/database")
        .build()
)

val credentials = Json.decodeFromString<DbCredentials>(secret.secretString())

When to use a secrets manager

  • You have multiple services that need shared secrets
  • You need audit trails for secret access
  • You need automatic secret rotation
  • Compliance requires it (SOC2, HIPAA, PCI)

Secret rotation

Secrets should change regularly. A leaked secret with a 90-day rotation window limits the damage to 90 days max.

Database password rotation

  1. Generate a new password
  2. Update the database user’s password
  3. Update the secret in Vault/AWS SM
  4. Applications pick up the new secret (dynamic or on restart)

With Vault, this can be automatic:

vault write database/config/mydb \
    plugin_name=postgresql-database-plugin \
    allowed_roles="myapp" \
    connection_url="postgresql://{{username}}:{{password}}@db:5432/mydb" \
    username="vault_admin" \
    password="admin_password"

vault write database/roles/myapp \
    db_name=mydb \
    creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
    default_ttl="1h" \
    max_ttl="24h"

Vault creates a new database user with a temporary password for each application connection. No shared password, automatic expiration.

Common mistakes

1. Secrets in Git history

Even if you delete the secret file, it’s in the Git history:

# This doesn't help — the secret is in previous commits
git rm .env
git commit -m "Remove secrets"

If secrets were committed, rotate them immediately. Use git filter-branch or BFG Repo Cleaner to remove from history, but always assume the secret is compromised.

2. Secrets in Docker images

# WRONG — secret baked into the image
ENV DATABASE_PASSWORD=prod_password

Anyone who pulls the image can read the secret. Pass secrets at runtime, not build time.

3. Secrets in logs

// WRONG — logs the password
logger.info("Connecting to database with password: $password")

// RIGHT — log that you're connecting, not the credentials
logger.info("Connecting to database at $dbHost:$dbPort")

Review logging to ensure secrets aren’t printed.

4. Overly broad access

If every developer can read production secrets, the blast radius of a compromise is huge. Apply least privilege:

  • Developers access dev/staging secrets only
  • Production secrets are accessible only by CI/CD and production systems
  • Use short-lived credentials where possible

Checklist

  • No secrets in source code or Git history
  • .env files gitignored, .env.example committed
  • Production secrets in environment variables or secrets manager
  • Secrets rotated on a schedule
  • Audit trail for secret access
  • Least-privilege access to secrets
  • Secrets not logged or exposed in error messages
  • Different secrets per environment (dev ≠ staging ≠ production)

Summary

LevelToolBest for
Local dev.env filesIndividual developers
Simple deployEnvironment variablesSmall teams, simple infrastructure
KubernetesSealed SecretsGitOps-based deployments
EnterpriseVault / AWS Secrets ManagerMulti-service, compliance requirements

Start with environment variables and .env files. Move to a secrets manager when you need rotation, auditing, or cross-service secret sharing. The one constant: secrets never belong in code.