Hey there! π Badger turns any link into a tripwire. Create innocent-looking URLs, plant them wherever you want to keep an eye on things, and the moment someone opens one you'll know exactly who showed up and with what. π΅οΈββοΈβ¨
Tip
π Prefer the pretty version? There's a landing page (English & German) with the full tour.
Think of it as a canary token you fully control: leaked-credential docs, fake internal links, "confidential" attachments, tracking pixels in emails. Drop a Badger link and watch the metadata roll in.
Warning
Badger is for authorized security testing and monitoring of assets you own or are allowed to
watch. The dashboard API is gated by a shared token (see
Authentication) and the custom trap renders raw HTML, so still treat
the dashboard as sensitive: keep the token strong, run it behind a VPN or IP allowlist if you
can, and never reuse the token elsewhere. Using tracking or cloaked links against third parties
without consent may break privacy law (GDPR & friends), and impersonating a brand you don't
control is plain phishing. Be a good human. π
Glad you asked! Here's the good stuff:
- πͺ€ Four Trap Flavors: tracking pixels, redirect links, preview clones, and fully custom fake pages
- π Deep Metadata Capture: IP chain, geo/ISP/ASN, parsed browser & OS, referer, language, raw headers
- π€ Bot vs. Human Verdict: a built-in heuristic flags automated clients (with the reason why!)
- πͺ Link Preview Cloning (MITM): copy any URL's OpenGraph card so your trap unfurls identically in Slack/Discord/X
- π¨ Custom Preview Builder: design your own fake link preview (image, title, description) with a live preview
- π§ Your Choice of Payload: send humans onward with a redirect, or show them your own custom HTML page
- π‘ Live Feed: every hit across every trap, streaming in and auto-refreshing
- π Geo Enrichment: turn raw IPs into country / city / ISP (toggleable, stays local if you want)
- πΎ Zero-Setup Storage: file-based persistence, no database to babysit
- π³ One-Command Docker: prebuilt image on GHCR, ships as a tiny ~170 MB Alpine container
- 𦑠Actually Nice to Look At: a dark, calm dashboard with warm honey accents (no scary hacker terminal here)
Each trap is just a URL you plant somewhere. What happens when it's opened depends on the type:
| Type | What the visitor sees | Great for |
|---|---|---|
| ποΈ pixel | An invisible 1Γ1 image | Emails & documents: embed <img src="β¦/p/<slug>.png"> |
| βͺοΈ redirect | Gets forwarded to a real URL | Looks like a totally normal short link |
| πͺ clone | The cloned preview of a target, then forwarded to it | Making a link unfurl exactly like the real thing |
| π¨ custom | Your hand-crafted preview plus a redirect or your own HTML | Suggesting a believable fake page |
| π decoy | A friendly "loadingβ¦" page | A soft landing that reveals nothing |
Note
π Whatever the type, every single open is logged with the full metadata, and the visitor just never notices a thing.
Every hit records the juicy details:
- π Full IP chain:
X-Forwarded-For,CF-Connecting-IP,X-Real-IP, and the socket address - π Geo & network: country, city, region, ISP, org, ASN (via ip-api.com, can be turned off)
- π§ Client fingerprint: browser, version, OS, device, engine (parsed from the User-Agent)
- π£οΈ Headers & hints: referer,
Accept-Language, and the complete raw request headers - π€ Bot verdict: human or bot, plus the heuristic reason it decided that
Two easy ways to get going: grab the container, or run it from source. π
The image is built and pushed to GitHub Container Registry automatically on every push to the default branch and on version tags. Just pull and run:
docker run -d --name badger \
-p 3000:3000 \
-e NUXT_PUBLIC_BASE_URL=https://badger.example.com \
-e BADGER_DASHBOARD_TOKEN="$(openssl rand -hex 32)" \
-v badger-data:/app/.data \
ghcr.io/disane87/badger:latestThen open http://localhost:3000 and start setting traps! π¦‘
- π Listens on
:3000(change with-e PORT=), binds0.0.0.0, runs as a non-root user - πΎ Trap & hit data lives in
/app/.data, so mount a volume to keep it across restarts - β€οΈ Built-in healthcheck hits
/api/trapsso your orchestrator knows when it's ready - π·οΈ Tags available:
latest,sha-<short>, and semver (1.2.3,1.2) onv*releases
Prefer to build it yourself?
docker build -t badger .
docker run -d -p 3000:3000 -v badger-data:/app/.data badgerReady to roll? Here's the dev setup:
npm install
npm run devThat's it! π Open http://localhost:3000.
Building for production without Docker?
npm run build
node .output/server/index.mjsImportant
Put Badger behind a reverse proxy (nginx / Caddy / Traefik) so X-Forwarded-For carries the
real client IP. Otherwise every hit looks like it came from your proxy. π€·
A couple of environment variables, that's all:
| Variable | Default | What it does |
|---|---|---|
NUXT_PUBLIC_BASE_URL |
(empty) | The public base URL for your tracking links. Set it to your domain (e.g. https://badger.example.com) so copied URLs point at the right place. If empty, it falls back to the browser's current origin. |
NUXT_GEO_LOOKUP |
true |
Outbound IP to geo enrichment via ip-api.com. Set false to stay 100% local with zero outbound calls. |
BADGER_DASHBOARD_TOKEN |
(unset) | Shared secret guarding every /api/* dashboard endpoint. Required β if unset the server returns 503 for all API calls. See Authentication. |
BADGER_AUTH_DISABLED |
(unset) | Set to 1 to bypass BADGER_DASHBOARD_TOKEN entirely. Local development only β never set in production. |
PORT |
3000 |
Port the server listens on. |
HOST |
0.0.0.0 |
Bind address. |
There's a ready-to-copy .env.example too. π
Every /api/* endpoint streams sensitive honeypot data β visitor IPs, request headers, the cookies
their browser leaked, the full trap configuration. So Badger gates the entire dashboard API behind
a single shared token. It's deliberately simple: one secret, no user accounts, no session store. ποΈ
Generate something long and random, then hand it to the server via BADGER_DASHBOARD_TOKEN:
# Generate
openssl rand -hex 32
# β put the result in your .env, docker-compose, or -e flagImportant
If BADGER_DASHBOARD_TOKEN is unset, the server fails closed and answers every dashboard
request with 503 Dashboard auth not configured. That's intentional β there is no "default open"
mode for production. πͺ
The middleware accepts the token from any of these (first match wins):
| Where | How it looks |
|---|---|
| π·οΈ Header | Authorization: Bearer <token> |
| πͺ Cookie | badger_token=<token> |
| π Query | ?token=<token> (handy for quick curl, avoid in URLs you share) |
Quick smoke test against a running instance:
curl -H "Authorization: Bearer $BADGER_DASHBOARD_TOKEN" http://localhost:3000/api/trapsThe cleanest way to use the dashboard from a browser today is to let your reverse proxy attach the header for you. Example with nginx:
location / {
proxy_pass http://badger:3000;
proxy_set_header Authorization "Bearer $HONEY_TOKEN_FROM_NGINX_ENV";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}β¦or Caddy:
badger.example.com {
reverse_proxy badger:3000 {
header_up Authorization "Bearer {env.BADGER_DASHBOARD_TOKEN}"
}
}That way the browser never has to know the secret. Pair this with VPN access or an IP allowlist if you want defense-in-depth. π‘οΈ
npm run dev against a fresh checkout would 503 on every API call. Two options:
# 1. Run with a throwaway token (mirrors production behavior)
BADGER_DASHBOARD_TOKEN=dev-only npm run dev
# 2. Skip auth entirely. Quick, but never use this in production.
BADGER_AUTH_DISABLED=1 npm run devCaution
𧨠The dashboard's Vue frontend doesn't have a login form yet β opening http://localhost:3000
in a browser only works when you're behind a reverse proxy that injects the header, or when
BADGER_AUTH_DISABLED=1. A proper login flow is a planned follow-up.
Badger ships with three friendly screens:
- πͺ€ Traps: your command center. Create traps, copy their URLs, see hit counts at a glance.
- π‘ Live Feed: every recent visitor across all traps, auto-refreshing so you don't have to.
- π Trap Detail: the full hit timeline with expandable metadata, plus the link-preview card for clone/custom traps.
| π Trap detail | π‘ Live feed |
|---|---|
![]() |
![]() |
Want to design a fake preview? The Custom Preview Builder lets you type a title, description and image URL and watch the social-card preview update live, then pick whether humans get redirected or shown your own HTML. πͺ
Note
πΈ The screenshots above use fabricated demo data, no real IPs or people.
Built with the good stuff:
- β‘ Nuxt 4 + Vue 3: SSR app & Nitro server in one
- π @nuxt/icon with the Lucide set (bundled locally, no CDN)
- π§© unstorage: file-based persistence, nothing else to install
- π³ Multi-stage Docker plus GitHub Actions to GHCR
Badger is a defensive instrument. Only deploy it on assets and networks you own or are
explicitly authorized to monitor. The geo lookup sends visitor IPs to a third-party API (disable it
if that's a concern), the clone and custom traps can reproduce or fabricate link previews, and
the custom trap renders operator-authored HTML verbatim. Don't use any of this to deceive or
impersonate third parties, because that crosses into phishing and is illegal in most places. π
Versioning is fully automated with semantic-release. Every
push to main is analyzed, and your Conventional Commits
decide what happens next:
- π
fix:β patch release (1.0.1) - β¨
feat:β minor release (1.1.0) - π₯
feat!:or aBREAKING CHANGE:footer β major release (2.0.0) - π§Ή
chore:,docs:,refactor:,test:and friends β no release
When a release is cut, the pipeline automatically:
- π·οΈ Tags the commit and creates a GitHub Release with generated notes
- π Updates
CHANGELOG.mdand the version inpackage.json - π³ Builds and pushes the Docker image to GHCR as
:x.y.z,:x.y, and:latest
So just write good commit messages and let the robots do the boring part! π€β¨
MIT. Do good things with it.



