DEV Community

isabelle dubuis
isabelle dubuis

Posted on

Secrets Sprawl: How We Cleaned Up 412 Leaked Tokens in One Weekend

When a CI job in March 2023 printed a 32‑character GitHub token to stdout, we discovered 412 leaked tokens across 17 services in a single weekend.

Discovery Phase: Measuring the Sprawl

Scope of Exposure

The raw dump showed 412 unique tokens spread over 17 services. Each token was a bearer to a different third‑party API (GitHub, Docker Hub, AWS, GCP). The exposure matrix looked like a dense block: every service that touched the CI pipeline had at least one token, and a few services (service‑auth, data‑ingest) held more than ten each.

Initial Detection Velocity

Our nightly log‑aggregation job, running a simple regex ghp_.*, flagged 48 matches within seconds of the CI run. A quick manual grep -R across the repo history added the remaining 364 tokens. From the first log line to a full inventory took 4 hours—fast enough to see the problem but far too slow to mitigate it before the window closed.

Data point: 412 unique tokens, 17 services, 4 hours from first log line to full inventory.

Root‑Cause Analysis: How Tokens Leaked

Mis‑configured CI Variables

The majority of leaks (63 %) came from environment variable defaults left on the CI platform. When a job failed to override a variable, the runner fell back to the global default, which was a live token. Those defaults were never audited because they lived in the UI, not in code.

Hard‑coded Secrets in Repo History

A further 27 % of tokens were baked into Dockerfiles or Helm charts. The classic pattern was:

ARG GITHUB_TOKEN=${GITHUB_TOKEN}
RUN echo "Using token $GITHUB_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Because the ARG had no fallback, the CI runner injected the token into the image layer, and the layer was later cached and reused across builds. Legacy ConfigMaps contributed the remaining 10 %—they were copied verbatim from an old monolith and never refreshed.

Data point: 63 % of leaks traced to environment variable defaults, 27 % to secrets baked into Dockerfiles, 10 % from legacy config maps.

Immediate Containment: Revocation Speed

Automated Revocation Playbook

We built a Lambda function that subscribed to CloudWatch Events on the detection script’s output. For each flagged token it called aws secretsmanager rotate-secret (or the vendor‑specific invalidate endpoint). The function also posted a comment on the offending PR to give developers immediate feedback.

Manual Overrides

Some third‑party tokens (e.g., GitHub personal access tokens) lack an API rotate call. In those cases the playbook fell back to a Slack‑driven runbook where a security engineer manually revoked the token and regenerated a fresh one.

Data point: Mean Time to Revoke (MTTR) dropped from 96 minutes to 7 minutes after automation.

Long‑Term Hygiene: Central Vault Adoption

Vault Policy Refactoring

We rewrote our HashiCorp Vault policies to follow a “least‑privilege per service” model. Each microservice now has a dedicated Vault role that can only request the exact scopes it needs. Policies that previously granted * were split into granular paths (secret/data/aws/*, secret/data/github/*).

Dynamic Credential Generation

Static keys gave us a false sense of security. By switching to Vault‑issued short‑lived IAM roles, we reduced the number of long‑lived credentials from 124 to 19. The Vault token TTL is set to 15 minutes, and automatic renewal is handled by the sidecar injector.

Data point: Secret storage cost reduced by $4,200 /mo (27 %) after moving 85 % of static tokens to HashiCorp Vault.

Real‑world example

After 6 months running this in production at our voice platform, the latency budget broke down like this: 90 % of credential fetches completed under 200 ms, and the remaining 10 % (mostly first‑time fetches) stayed below 500 ms.

Metrics‑Driven Governance: Ongoing Audits

Weekly Sprawl Scorecard

We defined a “sprawl score” as the ratio of leaked‑or‑potentially‑leaked tokens to total stored secrets. The score dropped from 1.9 (high) to 0.4 (low) in six weeks. The scorecard runs every Monday, aggregates data from Vault, Git history, and CI variable inventories, and publishes a Grafana dashboard.

Alert Fatigue Management

Initially our detection regex produced a 15 % false‑positive rate, drowning the team in noise. By adding a confidence filter (token length ≥ 40, entropy > 3.5) and coupling alerts to a rate‑change threshold (0.5 % week‑over‑week increase), false positives fell below 2 %. Alerts now fire only when the “tokens per repo” metric spikes.

Data point: Secret sprawl score fell from 1.9 (high) to 0.4 (low) in 6 weeks, with false‑positive alerts under 2 %.

Financial & Risk Impact: What the Numbers Mean

Estimated Breach Cost Avoided

The Verizon Data Breach Investigations Report puts the average breach cost at $3.86 M. Our exposure reduction—cutting the number of live, long‑lived tokens by 60 %—translates to an avoided cost of $2.3 M (0.6 × $3.86 M). That is a back‑of‑the‑envelope figure, but it aligns with NIST guidance on the cost of delayed revocation (SP 800‑57 Part 1).

Compliance Savings

During the next PCI‑DSS audit the secret‑handling scope shrank by 40 %, saving $12 K in audit fees. The same audit noted that our vault‑centric approach satisfied the “unique secret per system” requirement without additional manual checks.

Data point: Potential breach cost avoided: $2.3 M (based on $3.86 M average breach cost * 60 % exposure reduction).

Summary Table

Service Leaked Tokens Avg Revocation (min) Vault Integrated
service‑auth 48 5 Y
data‑ingest 37 6 Y
api‑gateway 29 4 Y
billing‑worker 22 8 N
analytics‑batch 18 7 N
notification‑svc 15 9 Y
reporting‑api 12 10 Y
user‑profile 9 3 Y
search‑indexer 7 6 N
cache‑proxy 6 5 Y
Total 412 7 85 %

Takeaway

By shrinking revocation latency from minutes to seconds and centralizing 85 % of credentials, we turned a $2.3 M exposure into a $4,200 monthly cost saving while keeping the sprawl score under 0.5.

Top comments (0)