DEV Community

Le Beltagy
Le Beltagy

Posted on

I Almost Leaked My Production Database Password to GitHub (And How I Fixed CI/CD Security)

I Almost Leaked My Production Database Password to GitHub (And How I Fixed CI/CD Security)

Why GitHub Actions secrets aren't enough, and what solo developers actually need to know


The Moment

I'm reviewing a GitHub Actions log.

Something catches my eye in the build output:

Step 5: Running tests
DATABASE_URL=postgresql://root:MySecretPassword123!@prod-db.aws.rds.amazonaws.com:5432/vehicle_metrics
Running pytest...
Enter fullscreen mode Exit fullscreen mode

Wait. My production database password is logged in plain text in a GitHub Actions run.

Not in a secret file. Not hidden. In the public build log.

Anyone with access to the repository could scroll through the logs and find it.

I had 30 seconds of pure panic.


How It Happened

Here's what I did wrong:

# ❌ VULNERABLE
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run tests
        run: pytest backend/tests/
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
Enter fullscreen mode Exit fullscreen mode

This looks safe, but if my test code does this:

# ❌ BAD TEST CODE
def test_database_connection():
    print(f"Connecting to: {os.getenv('DATABASE_URL')}")  # LOGS THE SECRET!
Enter fullscreen mode Exit fullscreen mode

The secret gets printed. GitHub logs it. Everyone sees it.


The Fix

Step 1: Clean Error Messages

# ✅ GOOD
def sanitize_for_logging(connection_string):
    return connection_string.split('@')[1] if '@' in connection_string else "***"

try:
    db_url = os.getenv('DATABASE_URL')
    logger.info(f"Connecting to: {sanitize_for_logging(db_url)}")
    connection = psycopg2.connect(db_url)
except Exception as e:
    logger.error(f"Connection failed: {type(e).__name__}")
    raise
Enter fullscreen mode Exit fullscreen mode

Step 2: Use Temporary Credentials

# ✅ BEST: Use AWS IAM
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      id-token: write

    steps:
      - uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions
          aws-region: us-east-1

      - name: Run tests
        run: pytest backend/tests/
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
Enter fullscreen mode Exit fullscreen mode

Step 3: Scan for Secrets

# ✅ ADD: Secret scanning
jobs:
  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: TruffleHog secret scan
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
Enter fullscreen mode Exit fullscreen mode

The Complete Secure Pipeline

name: Secure CI/CD
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD

  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'
      - uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  test:
    runs-on: ubuntu-latest
    permissions:
      id-token: write

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions
          aws-region: us-east-1

      - run: pip install -r requirements.txt pytest pytest-cov
      - run: pytest backend/tests/ --cov=backend --cov-report=xml
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml

  deploy:
    runs-on: ubuntu-latest
    needs: [secret-scan, dependency-scan, test]
    if: github.ref == 'refs/heads/main'
    permissions:
      id-token: write

    steps:
      - uses: actions/checkout@v3
      - uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1
      - run: |
          aws eks update-kubeconfig --name vehicle-metrics
          kubectl apply -f infrastructure/kubernetes/
Enter fullscreen mode Exit fullscreen mode

Security Checklist

☐ Secrets in GitHub Secrets (not in code)
☐ Secrets masked in logs
☐ No debug logging with sensitive data
☐ Use IAM roles instead of long-lived credentials
☐ Rotate credentials regularly
☐ Scan for secrets on every push (TruffleHog)
☐ Scan dependencies (Trivy)
☐ SAST scanning (SonarQube)
☐ Container scanning (Grype, Snyk)
☐ Limited permissions per job (RBAC)
☐ Audit logs reviewed regularly
☐ Separate test/staging/prod credentials
☐ Encrypted secrets at rest
☐ MFA for manual deployment
☐ Secrets expire automatically
Enter fullscreen mode Exit fullscreen mode

Real World Gotchas

Gotcha 1: Secrets in Environment Variables

# ❌ BAD
export DATABASE_URL="postgresql://root:password@localhost/db"

# ✅ GOOD
export DATABASE_URL=$(aws secretsmanager get-secret-value --secret-id db-password)
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: Secrets in Docker Build

# ❌ BAD: Secret baked into image
ARG DB_PASSWORD=prod_secret_here
RUN echo $DB_PASSWORD > /app/config

# ✅ GOOD: Only runtime secrets
ARG VERSION=1.0
ENV VERSION=$VERSION
Enter fullscreen mode Exit fullscreen mode

Gotcha 3: Secrets in Error Messages

# ❌ BAD
try:
    connect(os.getenv('DATABASE_URL'))
except Exception:
    print(traceback.format_exc())  # Includes the URL!

# ✅ GOOD
except Exception as e:
    logger.error(f"Connection failed: {type(e).__name__}")
Enter fullscreen mode Exit fullscreen mode

TL;DR

  • Almost leaked production database password via GitHub Actions logs
  • Found it in environment variable printed during tests
  • Fixed: sanitized logs, temporary credentials, secret scanning
  • Implemented: TruffleHog + Trivy + SonarQube + Snyk in CI/CD
  • One leaked credential kills your business

GitHub: beltagyy/vehicle-metrics
Author: Mohamed ElBeltagy (@beltagyy)
Topic: CI/CD Security | DevOps | Production Hardening

Top comments (1)

Collapse
 
leob profile image
leob • Edited

That's useful, also the checklist ... just wondering - could AI spot any of these security holes?