The Host's Casebook / The developer and the leaked key

The developer and the leaked key

CASEBOOK

5 min read · 1,170 words

A note on authenticity. This is a composite story written by the editors, built from situations that come up again and again. It is not the account of a particular named person or business. Real reader stories go through the submission page and are marked as reader-submitted.

A freelance developer was building a small booking system for a client, a physiotherapy practice. Late one evening he pushed his work to a public code repository so that a friend could look at a bug. In the push was a configuration file holding two things that should never have left his machine: the database password for the staging server and the API key for the email service that sent booking confirmations.

He noticed about an hour later, while rereading the commit. He deleted the file, committed the deletion, pushed again, and went to bed relieved that the exposure had been short. By the next morning the email account had sent several thousand messages he had not written, and the provider had suspended it. This is a fictional composite, but the speed of the sequence is accurate to how these incidents unfold.

It is useful to go through it carefully, because most people's instinct in that first hour is the same as his, and it is the wrong one.

What was in the file

The file was an ordinary application config, the sort every framework has: database host, name, user and password, then a block of third-party settings including the email service's API key. Developers write these files to make local setups easy, and the habit of committing them comes from the same convenience. Normally the file is listed in .gitignore so it never reaches the repository. This one had been created before the ignore rule was added, and Git does not forget a file it already tracks.

DB_HOST=db.example.com
DB_USER=booking
DB_PASSWORD=...
MAIL_API_KEY=...

Two secrets, two different kinds of damage. The database password allows reading and changing the practice's appointment records, which include names and contact details. The API key allows sending email as the practice, and any holder of the key can use it from anywhere, with no further proof of identity.

How fast the scanners were

Public repositories are watched. Some of the watchers are helpful: code hosting platforms scan pushes for known key formats and notify the owner or the provider. Others are not. Criminal scanners follow the stream of public pushes in near real time, pull out strings that look like credentials, test them against the relevant service, and put working ones to use. The gap between publication and first use can be a few minutes.

22:00Push withsecrets22:05Scanner findsboth keys23:00File deleted,history unchangedOvernightBulk mail sentwith the keyMorningProvidersuspendsTimes illustrative
The credentials were taken within minutes; deleting the file an hour later changed nothing about that.

Why deleting the file did not help

Git is a history database. Every commit is a permanent snapshot, and a commit that deletes a file only adds a new snapshot in which the file is absent. The earlier snapshot, with the secrets inside it, stays in the history. Anyone can run git log, check out the older commit, or view it on the hosting site, and see exactly what was there.

Several copies existed by morning. The hosting platform keeps the old commits reachable by their hash even after a branch is rewritten. Anyone who had cloned or forked the repository in that hour had their own copy. Search engines and archive services may have kept pages. A leak on the public internet cannot be recalled; it can only be made useless.

Treat a secret that has been public for one minute as public forever. Removing it from the repository is housekeeping. Rotating it is the actual fix.

The order of response

When the developer woke up to the suspension notice, he did the sequence a support engineer would have recommended, though in a more panicked order than ideal. The right order is below.

  1. Revoke the email key. In the provider's dashboard, delete the exposed key and create a new one with only the permissions needed to send. The old key stops working immediately.
  2. Change the database password, then update the application's configuration. Check the database's own users for any he did not create, and check whether it accepted connections from the internet at all. It should only accept them from the application server.
  3. Look for damage. Review the email provider's logs for what was sent and to whom. Check the database's recent queries or logs for unusual access, and compare table counts to a recent backup.
  4. Tell the client, with facts: what was exposed, for how long, what has been rotated, and what was or was not accessed. If personal data may have been read, the client may have legal duties to report it, and that decision is theirs, with advice.
  5. Clean the history, after the secrets are dead, so the repository stops advertising them.

The history rewrite uses a purpose-built tool, because plain Git commands are awkward for this. Something like git filter-repo --path config.env --invert-paths removes the file from every commit, after which a forced push replaces the remote branches. Everyone who cloned has to re-clone, and the hosting platform's support may need to purge cached views. It cleans the record; it does not restore secrecy.

The aftermath

The mail provider had suspended the account for abuse, and getting it reinstated took three days, a call with their trust team and a plain statement of what had happened. In the meantime the practice's appointment reminders did not go out, which was the visible harm. Some of the several thousand messages were phishing, sent as the practice's name, and a few recipients replied to the practice asking what they were. The messages' return addresses also damaged the sending reputation of the practice's domain, and its normal mail was filtered more aggressively for about a fortnight.

The client was understandably unhappy, and then, to his credit, practical. The developer wrote a short incident note: timeline, exposure, actions, and changes. The client kept him on. A frank account, delivered the same day, did more for the relationship than any attempt to soften it would have.

BeforeAfterRepositorycode + config + keysPublic hosting siteRepositorycode onlyServer envkeys live hereScanner blocks keys at commit
Keeping secrets out of the repository, and checking before every commit, removes the opening entirely.

The habits he adopted

He now keeps secrets in environment variables set on the server, or in a secrets file outside the project directory, and the repository holds only a template such as config.example.env with empty values. The real file is listed in .gitignore before the project's first commit. A pre-commit hook runs a secret scanner, such as gitleaks, on every commit and refuses it if it finds anything that looks like a key. He also turned on the hosting platform's own push protection, which stops a push containing a recognised key format.

The less visible changes matter just as much. API keys are now scoped to the least permission they need, so a send-only key cannot read account settings. The database user used by the application cannot drop tables. The database is not reachable from the internet. Keys have owners, and rotation is a habit instead of an emergency.

The takeaway

A pushed secret is a spent secret. Rotate first, rewrite history second, tell the client third, and set things up so the next commit cannot contain a key in the first place.

PreviousThe photographer and the 4 MB hero imageNextThe nonprofit and the unclaimed subdomain

More from The Host's Casebook

Composite case

Two sites on one account, one infection

A web designer hosted two client sites and her own on a single hosting account to save money. One client's...

Composite case

The translator who lost her portfolio to a lapsed plan

A freelance translator paid for hosting annually. The renewal notice went to an old email address she no...

Composite case

The local news site and the comment spam

A volunteer-run local news site enabled comments on every story. For a year it was lively and polite. Then...