A planted SSH private key that nobody legitimate ever uses. If it is ever used, you get an email naming which planted copy fired, where it was used from, and what the intruder asked for — while they get nothing at all.
This is a tripwire, not a honeypot. It grants no shell, runs nothing the client asks for, and exposes no service to play with. Its whole job is to tell you that a secret you believed was private has been found.
You are not defending the canary host. You are finding out that someone has been through a different machine — a laptop, a backup share, a CI secret store — and picked up credentials they should never have had. The bait lives there; this repo is the trap on the other end.
Two independent detectors feed one alert path:
| Detector | Fires on | Blind to |
|---|---|---|
Forced command (command= in authorized_keys) |
Any client that opens a session channel: ssh host, ssh host cmd, sftp |
A client that only authenticates — ssh -N, connect-and-drop |
| Auth-log watcher (systemd service) | Every successful authentication as the canary account | Nothing, while the service runs |
Both call the same alert code, which rate-limits per label — so a normal login trips both detectors but still produces exactly one email.
They fail differently on purpose. The forced command depends on sshd's
command= mechanism; the watcher depends on parsing sshd's log output. A
change that silences one leaves the other working.
intruder canary host
| |
|-- ssh -i stolen_key -------->| sshd: publickey accepted
| | |
| | +--> auth-log watcher ---+
| | |
| | $SHELL -c trigger.sh LABEL |
| | | |
| | +--> canary_alert.py <---+
| | | (one mail, rate-limited)
|<-- nothing. session hangs. --| +--> syslog
Canary host
- Linux with systemd and journald — the watcher reads
journalctl -u <unit> -f - OpenSSH 7.2+ (for
restrictinauthorized_keys) - Python 3.7+, standard library only. No venv, no packages.
- An MTA or SMTP relay it can reach
Developed and tested on Ubuntu 24.04 with OpenSSH 9.6p1 and Python 3.12. See docs/design.md for exactly which assumptions are load-bearing and which are cosmetic.
Your machine
bash,ssh,scp,ssh-keygen- root (or passwordless sudo to root) on the canary host
cp canary.conf.example canary.conf
$EDITOR canary.conf # host, account, notification address, SMTPCreate the account on the canary host once — see docs/setup.md. Then:
bin/new-key.sh workstation-anna # generate a labeled bait key
bin/deploy.sh # install and arm it on the host
bin/selftest.sh workstation-anna # prove the whole chain works
bin/plant-key.sh workstation-anna ~/somewhere/.sshbin/deploy.sh is idempotent — re-run it after any change. It refuses to
proceed if the account is misconfigured in a way that would leave you with a
canary that looks installed but never fires.
canary.conf.example every site-specific value; copy to canary.conf
bin/
new-key.sh generate a labeled keypair
deploy.sh stage + install on the canary host
install-server.sh server-side installer (invoked by deploy.sh)
plant-key.sh place a bait key and print its ssh_config stanza
selftest.sh end-to-end regression test
server/
trigger.sh the forced command
canary_alert.py rate-limiting, mail, syslog
canary_authwatch.py the auth-log watcher
canary-authwatch.service
keys/ generated keys and derived files — gitignored
bin/selftest.sh uses a real bait key against the real host and checks that
both detectors reported it. Run it after every deploy, and periodically — it
is the only thing that catches a silently broken detector.
The installer also sets up a monthly cron on the host that mails a selftest
alert. That proves the alert path (config readable, SMTP reachable); it
proves nothing about detection. Both matter.
- docs/setup.md — one-time host setup, and what the installer does
- docs/planting.md — the bait side: making a key findable, plausible, and safe from false alarms
- docs/design.md — why no real login is possible, what is deliberately not done, portability limits
The idea is not new, and this is not the first implementation of it. If one of these fits your situation better, use it.
-
m4rkw/canary-ssh — the closest prior art, and the same core idea: plant SSH keys, watch sshd's log for their fingerprints, mail on a hit. It needs no server-side account and no
authorized_keysentry at all, because it alerts on keys that are rejected — which in turn requires sshdDEBUGlogging, since the offered fingerprint is not in the defaultINFOline. Smaller and less invasive than this project; in exchange it has one detector, runs as root, and cannot tell you what the client asked for. -
rondilley/sshcanary — despite the nearly identical name, a different category: a low-interaction honeypot with its own fake SSH daemon on its own port, which records the username/password pairs brute-force bots try. Useful for studying attacker behaviour. It fires constantly by design; this project should fire never.
-
thinkst/canarytokens — the reference implementation of the general concept, hosted at canarytokens.org or self-hostable. Many token types (documents, URLs, DNS, cloud credentials). Start here if you want breadth rather than one deep SSH-shaped tripwire.
-
GitGuardian/ggcanary — the same pattern applied to AWS credentials, deployed with Terraform. A good companion: leaked cloud keys and leaked SSH keys tend to leak together.
One property worth knowing when you choose: document-based canary tokens can be found offline — there are scanners for them. A canary SSH key cannot. It is a normal, valid Ed25519 keypair, indistinguishable from a real one until somebody connects with it, at which point you already know.
MIT — see LICENSE. Do what you like with it; keep the copyright notice.
- Never commit
keys/. A published private key makes every planted copy worthless and lets anyone trigger your alerts..gitignorecovers it; check before you push. canary.confmay hold SMTP credentials. Also gitignored.- The canary account must be used by nothing else, ever. The watcher treats every successful login as it as a hit.