2026-03-22 · Security IncidentDockerCredentials

Hardcoded Secrets in Docker Compose: How Credentials End Up on GitHub

Database passwords hardcoded in docker-compose.yml, committed to public GitHub repos, and harvested by automated scanners within hours. This is the path — and it happens more often than anyone admits.

Hardcoded credentials in Docker Compose files and application configs are a persistent source of credential exposure. GitHub's secret scanning catches some patterns — but database passwords, custom API keys, and internal service tokens are frequently missed, and the exposure often predates the scan by months.

How credentials end up exposed

Pattern 1 — docker-compose.yml committed to public repo

The most common path: a developer creates a docker-compose.yml with hardcoded credentials for local development, adds the project to GitHub, and either forgets to add a .gitignore entry for the secrets or uses a public repo. The file sits in the repository history even after the credentials are removed.

Pattern 2 — .env file committed accidentally

A developer runs git add . and commits a .env file that wasn't in .gitignore. The file contains production database passwords, API keys, and JWT secrets. GitHub's secret scanning may flag some patterns, but database passwords without recognisable prefixes are not detected.

Pattern 3 — CI/CD config with hardcoded tokens

GitHub Actions workflow files with hardcoded tokens (instead of using ${{ secrets.TOKEN }}) are committed to public repositories. Build logs also sometimes echo secret values when debugging is left enabled.

What attackers do with exposed Docker credentials

Exposed database credentials in docker-compose.yml files have a predictable exploitation path:

Hour 0
Automated scanner finds docker-compose.yml with POSTGRES_PASSWORD=mypassword in a public GitHub search.
Hour 0–1
Scanner attempts connection to the server IP associated with the repo owner's other public projects. Tries common ports (5432, 3306, 27017) with the exposed credentials.
Hour 1–24
If the database is accessible (Docker UFW bypass — see related incident), attacker exfiltrates the database. If not accessible, credentials are stored for credential stuffing against other services using the same password.
Day 1–30
Credentials sold or used in targeted attacks against the organisation. GitHub history preserves the original exposure even after the file is updated.

The git history problem

Removing credentials from a file and pushing the update does not remove them from git history. Anyone with access to the repository can still see the credentials in previous commits using git log -p.

# Check if credentials exist in git history:
git log --all -p | grep -i "password|secret|key|token" | head -20

# Remove from history (requires git-filter-repo):
pip install git-filter-repo
git filter-repo --path docker-compose.yml --invert-paths

After removing from history: rotate all exposed credentials immediately. Git history removal doesn't help if the credentials were already harvested.

The correct pattern

docker-compose.yml — use variable references
services:
  postgres:
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}   # Reference, not value
      POSTGRES_USER: ${DB_USER}
.env file — never commit this
DB_PASSWORD=your-actual-password
DB_USER=postgres
.gitignore — add before first commit
.env
.env.local
.env.production
*.env

Related

Paste your docker-compose.yml to detect hardcoded credentials, exposed ports, and missing security controls — nothing leaves your browser.