Secrets management: keeping API keys out of your code and git history
By VA2PT Team, . 8 min read

Your team ships fast, and somewhere along the way a database password ended up in a config file, an AWS access key landed in a commit, and a third-party API token got pasted into a Slack message that is now searchable forever. None of this happened because anyone was careless on purpose. It happened because storing a secret the right way used to be harder than storing it the wrong way. This article shows how to flip that, so the easy path is also the safe one, and what to do the day you discover a key has leaked.
The problem
Secrets — API keys, database passwords, private certificates, signing keys, OAuth tokens — are the credentials that unlock everything else you own. When one leaks, an attacker does not need to break your application; they simply log in as it. The most common places they leak are painfully ordinary: a .env file committed by accident, a hard-coded key in a mobile app, a credential in a CI log, a token in a public GitHub repository, or a password shared over chat and never rotated.
The part teams underestimate is that git never forgets. Deleting a secret in a later commit does not remove it; it stays in the history, reachable by anyone who clones the repository. If the repository is or ever was public, assume the secret is already harvested — automated bots scan new commits on public platforms within minutes.
Why it happens
Secrets leak because, for most of a project's life, the convenient option and the secure option point in opposite directions. Hard-coding a key gets the feature working in the next five minutes. Setting up a secret store, wiring it into every environment, and teaching the team to use it is work that pays off later, so under deadline pressure it gets deferred, and "temporary" hard-coded values become permanent.
Three specific mechanisms cause most incidents:
- No single place secrets are supposed to live, so they end up scattered across
.envfiles, CI variables, personal notes and code. - No guardrail that stops a secret reaching git, so a leak depends entirely on a developer noticing before they commit.
- No rotation, so a key that leaked two years ago still works today. A secret with no expiry is a permanent liability.
Store secrets in a manager, not in files
The fix is to give secrets one home: a dedicated secret manager that stores them encrypted, controls who and what can read them, and logs every access. Your application fetches the secret at runtime; it never sits in the code or the image. Each major cloud has a native option, and they behave similarly:
- AWS Secrets Manager (or SSM Parameter Store for simpler needs)
- Azure Key Vault
- Google Cloud Secret Manager
- HashiCorp Vault, if you want one tool across clouds
The pattern is the same everywhere: the workload is given an identity (an IAM role, a managed identity, a service account), and that identity is granted read access to exactly the secrets it needs. Nothing is stored on disk.
# Fetch a secret at runtime instead of hard-coding it.
import boto3, json
def get_secret(name: str) -> dict:
client = boto3.client("secretsmanager", region_name="ap-south-1")
resp = client.get_secret_value(SecretId=name)
return json.loads(resp["SecretString"])
db = get_secret("prod/api/database")
# use db["username"], db["password"] — never written to code or logs
Two rules make this effective. First, grant access by workload identity, not by shared credentials, so a leaked human password cannot read production secrets. Second, keep a hard separation between environments — the staging workload must not be able to read production secrets.
Put a guardrail in front of git
Storing secrets correctly still leaves the risk that someone pastes one into code by mistake. Close that gap with automated scanning that runs before a commit lands, so a secret is caught at the door rather than after it is public.
# .pre-commit-config.yaml — block commits that contain likely secrets
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Run the same scanner in your CI pipeline as a backstop, so the protection does not depend on every developer having the local hook installed. Tools such as gitleaks or trufflehog detect the patterns of common keys and stop the pull request if they find one.
Rotate on a schedule, not after a scare
A secret that never changes is a secret that only gets riskier with time. Set an expiry and rotate automatically where the manager supports it — AWS Secrets Manager, for example, can rotate database credentials on a schedule with no downtime. For third-party keys that cannot rotate automatically, put a recurring calendar task against an owner. The goal is that any credential which leaked in the past has already been replaced by the time anyone tries to use it.
What to do this week
- Pick one secret manager for your main cloud and move production database credentials into it first, since those do the most damage if leaked.
- Add secret scanning (gitleaks or equivalent) as a pre-commit hook and as a CI step, so both the developer's machine and the pipeline catch leaks.
- Grep your repositories and CI configuration for obvious keys, and rotate anything you find — assume it is already compromised.
- Turn on automatic rotation for anything that supports it, and set calendar reminders for the rest.
- Write a one-line rule the whole team knows: secrets live in the manager, never in code,
.envfiles or chat.
When to bring in a partner
If secrets are scattered across services and nobody is confident where they all live, an outside team can inventory them, move them into a manager, wire rotation, and add the pipeline guardrails without stalling your releases. This is core to how we run DevSecOps engagements, and it pairs naturally with a vulnerability assessment that checks whether anything has already leaked. It is usually a short, high-return piece of work, and it removes one of the most common ways companies get breached.
- secrets-management
- devsecops
- api-keys
- git
- cloud-security