opsec

OPSEC for developers: the digital trails we publish without noticing

Emails and time zones in Git commits, secrets left in repo history, GPS coordinates in photos, subdomains in Certificate Transparency logs, and .git folders that get deployed: how to check for and close the information leaks developers create most often.

  • opsec
  • privacy
  • osint

OPSEC (operations security) was born in the Vietnam War. In 1966 the US military formed a team code-named Purple Dragon to investigate why the enemy seemed to know about operations before they began. The conclusion was surprising: the biggest leaks were not classified documents but ordinary pieces of information that anyone could observe, such as schedules, routines, and communication patterns. Each piece was harmless on its own. Put together, they revealed the operation.

Developers produce pieces like that every day. Commits, photos, TLS certificates, and HTTP headers are not secret, but together they can reveal an identity, a location, and an infrastructure. This post covers the most common leaks, how to check for them yourself, and how to close them.

Start with a threat model

OPSEC is not a list of tools but a five-step process, distilled from the Purple Dragon experience and later formalized in the US national OPSEC program (NSDD 298, 1988):

StepQuestionDeveloper example
Identify critical informationWhat needs protecting?the identity behind a username, a home address, API keys, internal infrastructure
Analyze threatsWho might be looking for it?key-scanning bots, doxxers, attackers targeting a client
Analyze vulnerabilitiesWhere can it be seen?commit metadata, photo EXIF, CT logs, files that get deployed
Assess riskHow likely, and how damaging?a leaked cloud key is far more urgent than a commit time zone
Apply countermeasuresWhat is proportionate?a noreply email, key rotation, stripping metadata, authentication on staging

The second step decides everything. A developer who wants to stay anonymous behind a username needs far stricter measures than one who only wants to keep API keys safe, so pick the sections below that match your threat model.

Git commits: name, email, and working hours

Every commit stores the name and email of its author and committer, along with timestamps and time zones. All of it is published when the repo is pushed somewhere public. Check what has been recorded so far:

Identities in the repo history
git log --format='%an <%ae>' | sort | uniq -c | sort -rn
Commit hours and time zones
git log --format='%ad' --date=format:'%H:00 %z' | sort | uniq -c | sort -rn

The first often turns up a work email or a forgotten old address. The second shows the time zone (+0700 is Western Indonesia Time) and active hours, which is enough to guess someone's location and routine. Others don't even need to clone the repo: appending .patch to a GitHub commit URL shows a From: line with the author's name and email.

https://github.com/<user>/<repo>/commit/<sha>.patch

How to close it:

  • On GitHub, open Settings → Emails, then enable Keep my email addresses private and Block command line pushes that expose my email. GitHub then provides an address of the form ID+username@users.noreply.github.com.
  • Use that address as your Git email. To keep work and personal identities apart, includeIf makes repos in a given folder use a different email automatically.
  • The time zone comes from your system settings. If it counts as critical information, you can commit in UTC with TZ=UTC git commit. Active-hour patterns still show in push times and other activity.
~/.gitconfig
[user]
    name = public-name
    email = 12345+username@users.noreply.github.com
 
# Repos inside ~/work/ use the work email from a separate file
[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

Secrets left in history

Deleting a .env file in a new commit does not remove it from history; its contents can still be read from earlier commits. Bots constantly scan public repos for keys, and various experiments have shown that leaked keys can be tried by others within minutes. Handle it in this order:

  1. Rotate first. Revoke the key with its provider and issue a new one. A key that has been published must be treated as known, whatever happens to the repo history.
  2. Look for other leaks across the whole history, for example with gitleaks: gitleaks git -v.
  3. Clean the history with git filter-repo in a fresh clone, then push again with --force (example below).
  4. Keep it from happening again. Add .env* to .gitignore (except examples such as .env.example) and enable secret scanning and push protection in the repo's security settings.
Cleaning history with git filter-repo
# remove a file from the entire history
git filter-repo --invert-paths --path .env
 
# or replace secret strings (one per line in secrets.txt)
git filter-repo --replace-text secrets.txt

Forks, other people's clones, and caches (including pull request refs on GitHub) are not cleaned. That is why rotation always comes first.

For Next.js specifically, environment variables prefixed with NEXT_PUBLIC_ are inlined into the JavaScript sent to the browser at build time. Never put secret keys there. This site, for example, only reads its SMTP credentials on the server through a Server Action, and the values are set in the hosting panel, not in the repo.

Metadata in photos and documents

Photos from a phone usually carry EXIF data: the device model, when the photo was taken, and often GPS coordinates. In December 2012, a photo of John McAfee published by Vice magazine still contained GPS coordinates, which immediately revealed that he was in Guatemala. Check and strip it with ExifTool:

exiftool -gps:all -datetimeoriginal photo.jpg   # show location and time
exiftool -all= -overwrite_original photo.jpg    # remove all metadata

Many social networks strip EXIF on upload, but files sent as documents, such as email attachments or the "send as file" option in chat apps, usually arrive intact, metadata included.

PDF and Office documents store the author's name, organization, software version, and creation time. exiftool report.pdf shows them, and mat2 makes a clean copy: mat2 report.pdf produces report.cleaned.pdf.

Screenshots need a check too. Terminal screenshots often show user@hostname in the prompt, paths like /home/real-name/, internal IP addresses, or client names in open tabs. Images can even hold more than metadata: data can also be hidden in the pixels themselves, as covered in the post on the Chi-Square Attack and RS Analysis.

What gets deployed along with the site

A production server is the easiest place for others to inspect, and also the easiest to inspect yourself:

Quick checks from the outside
curl -sI https://your-domain.com | grep -iE '^(server|x-powered-by):'
curl -s https://your-domain.com/.git/HEAD
curl -s -o /dev/null -w '%{http_code}\n' https://your-domain.com/.env
  • An accessible .git/ folder lets anyone download the entire source code, history included, with tools such as git-dumper. If the second command prints something like ref: refs/heads/main, that is a serious problem.
  • Version headers such as Server and X-Powered-By tell attackers which software to look up vulnerabilities for. Next.js sends X-Powered-By: Next.js by default; this site turns it off with poweredByHeader: false.
  • Source maps (*.js.map) in production give back the original source code, comments included. Next.js does not generate them for the browser in production builds unless productionBrowserSourceMaps is enabled.
  • Overly detailed error messages, such as stack traces, server paths, or SQL queries, belong in the logs, not on the page.

Some leaks cannot be switched off. Every TLS certificate from a public CA is recorded in Certificate Transparency logs that anyone can read, so subdomain names like staging. or old-admin. are public from the moment their certificate is issued:

https://crt.sh/?q=%25.your-domain.com

Don't rely on hard-to-guess subdomain names for protection. Put staging environments behind authentication, or use a wildcard certificate when the subdomain name itself is sensitive.

One identity, one compartment

Using the same username across platforms makes it easy for others to link your accounts; OSINT tools such as Sherlock check a single username on hundreds of sites at once. The classic case is Ross Ulbricht, the founder of Silk Road. Investigators found an old post by the username "altoid" promoting Silk Road, then another post by "altoid" on a Bitcoin forum that listed a Gmail address under his real name.

Clean compartments mean:

  • separate email addresses for different purposes, or aliases (for example from Proton Pass, SimpleLogin, or Firefox Relay) so your main address doesn't spread;
  • separate browser profiles or containers, so cookies and sessions for work, personal, and research accounts don't mix;
  • different usernames and profile photos for identities you want to keep apart, because photos can also be matched with a reverse image search.

A compartment is only as strong as its weakest moment. Hector Monsegur ("Sabu") of LulzSec was reportedly identified after logging into an IRC server without Tor just once, which recorded his real IP address.

Leaks through habits

Not every leak comes from code:

  • In January 2018, Strava's global heatmap showed the running routes of military personnel around bases in conflict zones, because GPS data from their smartwatches was uploaded automatically.
  • In 2020, former Australian Prime Minister Tony Abbott posted a photo of his boarding pass on Instagram. The booking code on it was enough for a security researcher to log into the airline's booking page and find his passport number.
  • Photos of a desk often also show sticky notes with passwords, internal dashboards on screen, or an office ID badge.

The lesson is the same each time: information that feels trivial to us, like a running route or a plane ticket, is a valuable piece for someone building a profile.

Checklist

  • The threat model is clear: what you are protecting, and from whom.
  • Commits use a noreply email, and pushes that expose your email are blocked.
  • The repo history has been scanned, and any key that ever leaked has been rotated.
  • No secrets live in NEXT_PUBLIC_* variables or in the JavaScript bundle.
  • Photos and documents are stripped of metadata before they are shared.
  • /.git/, .env, and source maps cannot be reached from outside, and version headers are off.
  • Subdomains on crt.sh have been reviewed, and staging is behind authentication.
  • Identities you want to keep apart use different emails, usernames, and browser profiles.

OPSEC is not a one-off job. Repeat the five steps whenever something changes: a new job, a new public repo, or a new habit. If you find a leak on this site, please report it via security.txt.