# Secrets Management Best Practices for Deployments
A leaked database password or cloud API key is one of the most common — and most damaging — security failures in software. The good news: nearly all of it is preventable with a handful of disciplined practices. This guide covers how to handle secrets across your deployment lifecycle.
What counts as a secret
A secret is any credential that grants access or proves identity: database passwords, API keys, OAuth client secrets, JWT signing keys, TLS private keys, encryption keys, webhook signing secrets. The defining trait is that exposure causes harm. Treat the list broadly — when in doubt, it's a secret.
Rule 1: Never commit secrets to version control
This is the cardinal rule, and it's violated constantly. Git history is permanent: once a secret is committed, removing it later doesn't help — it's in the history, in forks, in clones, and automated scrapers find committed keys within minutes of a push.
Defenses, layered:
# 1. Gitignore secret files
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
# 2. Scan commits before they land (pre-commit hook)
# tools: gitleaks, trufflehog
gitleaks protect --stagedIf a secret *was* committed, the only correct response is to rotate it immediately — assume it's compromised. Scrubbing history is secondary and insufficient on its own.
Rule 2: Separate config from secrets
Not all environment variables are equal. LOG_LEVEL=info is config; DATABASE_PASSWORD is a secret. They both arrive as environment variables, but secrets need encryption at rest, access controls, and audit logging that ordinary config doesn't.
| Config | Secrets | |
|---|---|---|
| Example | PORT, LOG_LEVEL | DB password, API key |
| Encrypted at rest | Optional | Required |
| Access logged | No | Yes |
| In version control | Templates OK | Never |
Rule 3: Apply least privilege
A secret should grant the minimum access needed, and no more. A read-only reporting service should have read-only database credentials. An API key for sending email shouldn't also be able to manage billing. Scoped credentials limit the blast radius when (not if) one leaks.
Rule 4: Prefer short-lived credentials
The best secret is one that expires before an attacker can use it. Long-lived static keys are a standing liability; short-lived, automatically-rotated credentials shrink the window of exposure dramatically.
The modern pattern is OIDC / workload identity: instead of storing a static cloud key in CI, your pipeline exchanges a short-lived signed token for temporary credentials at runtime. No static secret exists to leak.
# GitHub Actions assuming a cloud role via OIDC — no stored keys
permissions:
id-token: write
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/deployer
aws-region: us-east-1Rule 5: Rotate regularly and have a plan
Secrets should be rotatable without downtime, and you should rotate them on a schedule and immediately on any suspected exposure or team-member departure. Rotation is much easier if your app reads secrets from a central store and you can update them in one place. Design for rotation before you need it — discovering you have a key hardcoded in twelve places during an incident is a bad time.
Rule 6: Encrypt secrets at rest and in transit
Secrets sitting in a database or config store should be encrypted at rest (e.g. with a KMS-managed key or Fernet-style symmetric encryption), and always transmitted over TLS. A secrets store that keeps plaintext is barely better than a committed .env.
Rule 7: Don't leak secrets through the side door
Secrets escape in subtle ways even when stored correctly:
- Logs — never log full config objects or request headers containing tokens.
- Error messages — stack traces sometimes include connection strings.
- Client bundles — anything in frontend JavaScript is public; never put server secrets there.
- CI output — mask secrets so they don't print in build logs.
// Bad: logs the whole config including secrets
logger.info('Starting with config', config);
// Good: log only non-sensitive fields
logger.info('Starting', { port: config.port, env: config.env });Secrets management on PandaStack
PandaStack handles secrets as part of the platform rather than something you bake into your image or commit to Git. You set environment variables (including secrets) per app, and they're delivered to your container's runtime — they don't live in your source code or your built image. Platform-reserved variables are protected so a stray value can't break runtime wiring.
Two platform behaviors reinforce good practice. First, when you attach a managed database, PandaStack auto-injects DATABASE_URL — so the single most commonly-leaked secret (a database connection string) never needs to pass through your repo or a config file at all. Second, team access uses RBAC (owner/admin/member) and SSO, so you can control who can view and change configuration. Internally, the platform applies encryption at rest for sensitive data. The practices above — rotation, least privilege, not logging secrets — remain your responsibility in application code, as they are on any platform.
Checklist
- ✅ No secrets in version control (scan to enforce)
- ✅ Config and secrets handled distinctly
- ✅ Least-privilege, scoped credentials
- ✅ Short-lived credentials / OIDC where possible
- ✅ Regular rotation, plus immediate rotation on exposure
- ✅ Encrypted at rest and in transit
- ✅ Never logged, never in client bundles
References
- [OWASP: Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
- [GitHub: About secret scanning](https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning)
- [NIST SP 800-57: Key Management Recommendations](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final)
- [The Twelve-Factor App: Config](https://12factor.net/config)
Keep secrets out of your repo with platform-managed env vars and auto-injected database credentials on PandaStack — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).