Self-hosted service
thinkst/canarytokens avatar
thinkst/canarytokens

Canarytokens and the configuration that decides whether an intruder hears back

Canarytokens helps track activity and actions on your network

2,186 stars296 forksPythonNOASSERTION

At a glance

What is it?
Canarytokens is a self-hosted Python server that hands out decoys and reports each use by email or webhook. Two env files, a per-IP alert throttle and an AWS key path that now depends on a private repository shape most of what it does.
Who is it for?
For defenders who can expose one hostname and want an email or webhook the moment a decoy is touched, this is serious infrastructure with its switchboard details written down, and far more of the moving parts documented than most services of this size. Decoy files watched by an endpoint agent cost nothing to run but vanish with the agent, which suits estates with no inbound path.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One alert per calling IP, and what the console still keeps

Canarytokens is a server you host so that decoys planted inside your estate report back. Its one line summary is that it helps track activity and actions on your network, and everything underneath that line is configuration rather than an agent you push onto endpoints. A token is a value that looks inert until somebody uses it, at which point the server sends an alert.

Throttling decides what a real intrusion gets you in practice. By default, unless running in DEBUG mode, no more than 1 alert per unique calling IP per minute is permitted. Activity is still recorded in the database and visible in the token management console, while the email and webhook are not generated. That split is the first design decision worth internalising: the console is the record, the alert is only the notification, and lowering the notification rate does not reduce what the server stores.

Two env files, one mail provider, and a port that must speak StartTLS

Settings live in `settings.py` and reach the two processes through separate files. `frontend.env` covers token creation and the console: `CANARY_DOMAINS=mytesttokensdomain.com` is the domain tokens sit under, `CANARY_NXDOMAINS=pdf.demo.canarytokens.net` names tokens that exist only as records which must fail to resolve, and `CANARY_WEB_IMAGE_UPLOAD_PATH=/uploads` with `CANARY_GOOGLE_API_KEY` back the image and map parts of the interface.

`switchboard.env` covers delivery, and the provider choice is exclusive. Supplying `CANARY_MAILGUN_API_KEY` means removing `CANARY_SENDGRID_API_KEY` and `CANARY_MANDRILL_API_KEY`, because the switchboard treats them as alternatives rather than a chain of fallbacks. Outgoing SMTP substitutes its own four keys, and the port has to be a StartTLS enabled one such as 587 in the example below, since anonymous SMTP is not supported:

code
CANARY_SMTP_SERVER=smtp.yourserver.com
CANARY_SMTP_PORT=587
CANARY_SMTP_USERNAME=<your smtp username>
CANARY_SMTP_PASSWORD=<your smtp password>
[email protected]
CANARY_ALERT_EMAIL_SUBJECT="Canary Alert via SMTP"

All six lines go into `switchboard.env`. The sender display name sits in the same file, and for AWS SES it is written as `CANARY_ALERT_EMAIL_FROM_DISPLAY=CanaryAlert <[email protected]>`.

AWS key tokens switch off when one value is missing

AWS key tokens are the one type whose backing infrastructure is no longer public. The AWS side that allocates IAM users and runs the safety-net checks moved to a private repo, and this repository keeps only the integration points facing the token service. Four values in `frontend.env` drive them, with the frontend sending `domain`, `token`, `auth`, and `guid` when creating one:

code
CANARY_AWSID_URL=<custom awsid url>
CANARY_AWSID_AUTH=<shared awsid auth token>
CANARY_AWSID_GUID=<registered awsid console guid>
CANARY_AWSID_CONTROL_ACCOUNT_ID=<awsid control account id>

`CANARY_AWSID_URL` is the API Gateway and Lambda URL for the `LinkAWSIDTokenUserToCanaryConsole` endpoint, `CANARY_AWSID_AUTH` must match the token held in the AWS ID secret, `CANARY_AWSID_GUID` identifies this instance while tokens are allocated and deleted, and `CANARY_AWSID_CONTROL_ACCOUNT_ID` names the account owning the deletion queue. If either the URL or the auth value is unset, AWS key token creation is disabled for that instance rather than failing loudly on the request. A separate `CANARY_LAMBDA_AWS_CRED_REPORT_AUTH` in `switchboard.env` enables the `/a/cr` callback that the credential report checker posts to.

The install instructions point at a separate Docker repository

The install path is deliberately kept elsewhere. The README recommends the Docker image installation process in a separate project, canarytokens-docker, and gives no pip, apt or virtualenv command for running this tree directly. Anyone who wants the bare Python assembles the environment from `pyproject.toml`, which pins the interpreter narrowly and builds through hatchling:

toml
[project]
name = "canarytokens"
version = "0.3.0"
license = "GPL-3.0-only"
requires-python = ">=3.13,<3.14"

Two lines in that block reward a close look. The package version is still 0.3.0 while the default branch is master, so pinning by version string buys you one revision of a project that moves by commit. And `requires-python = ">=3.13,<3.14"` admits 3.13 and nothing else, which leaves a host on any earlier CPython with no supported path short of a fork. The root directory backs up the same story: a makefile, `.flake8`, `mypy.ini` and a `.pre-commit-config.yaml` sit next to a dev group where ruff and modernize do the work.

Two rate limits in this project do not mean the same thing

The `switchboard.env` table lists `CANARY_MAX_ALERTS_PER_MINUTE` with a value of 1000, which reads like the ceiling on alert volume the switchboard will emit. The throttling section describes the opposite behaviour: one alert per unique calling IP per minute, with the data still recorded behind it. A single host that walks an entire token list therefore produces one email rather than hundreds, while the console shows every hit that happened.

The paragraph that would name the tuning variable breaks off partway through a `CANARY_` prefixed name, so the switch controlling the per-IP limit is not readable from the page. What is documented is the behaviour, not the knob that moves it. Read the 1000 as the outer guard on the switchboard process, and the per-IP rule as the reason a well-mapped estate stays quiet through a week of scanning.

Callback path, log files, and a Slack token that closed

Callbacks have to find their way back, and the configuration is blunt about it. `CANARY_PUBLIC_IP` and `CANARY_PUBLIC_DOMAIN` are listed as alternatives in `switchboard.env`, one standing in for a domain and the other for an address, so a server reached by address rather than name can publish tokens without owning a hostname. `CANARY_FORCE_HTTPS` forces the https scheme on generated token URLs, and leaving it unset means token links carry whatever scheme the deployment was configured with.

Error visibility is a separate channel from token alerts. `ERROR_LOG_WEBHOOK` takes a URI that receives error logs, and with it absent those errors stay in the file named by `LOG_FILE`, which is `frontend.log` for the frontend process and `switchboard.log` for the switchboard. One delivery path has closed to new deployments: the Slack API Token is deprecated and no new ones can be created, while old tokens still work, so a server configured years ago keeps posting to Slack and a fresh install has no supported way to obtain one.

Two exact pins among a dependency list that floats

`pyproject.toml` shows the pinning policy plainly. Almost everything floats: `fastapi`, `uvicorn`, `twisted[tls]`, `redis`, `boto3`, `sendgrid`, `segno` and `cryptography` carry no version constraint at all. Two do, `pydantic[email]==2.13.4` and `pydantic-settings==2.14.2`, each pinned with exact equality while its neighbours are not, which is the shape left behind by a dependency that broke something once.

The build side is similarly specific. `[tool.pytest.ini_options]` sets `asyncio_mode = "auto"` so the asynchronous tests need no marker, and `[tool.uv]` sets `exclude-newer = "7 days"` next to a committed `uv.lock`, a 7 day window that lets a resolved environment sit behind whatever the index currently serves. Two maintenance facts frame the rest. GitHub's licence field reports Other for this repository with a pointer to the `LICENSE` file, while `pyproject.toml` declares `license = "GPL-3.0-only"`, so the file itself is the only place that settles what you agree to. The last push was on 2026-09-25 and nothing is tagged on GitHub, which leaves master as the thing worth pinning.

Where a decoy server is the wrong shape of answer

A canary service needs a way in, and the configuration names every door it uses: a DNS name under `CANARY_DOMAINS`, a public address or hostname the server owns, an AWS account for the key tokens. In a segment with no inbound path, or where token callbacks are not allowed to leave the site, the remaining option is a decoy file, decoy account or decoy share watched by the endpoint agent already installed on the machine. The approaches fail in opposite directions, since an agent disappears with whatever removed the agent, while a token on infrastructure the intruder has not touched keeps reporting after they work out what it is.

There is a ceiling on scale as well. Every token is a database row, a console entry and a potential alert, so an estate planting them by the thousand inherits the throttle described earlier: the second hit from one address in a minute produces a record and no message.

Editorial conclusion

For defenders who can expose one hostname and want an email or webhook the moment a decoy is touched, this is serious infrastructure with its switchboard details written down, and far more of the moving parts documented than most services of this size. Decoy files watched by an endpoint agent cost nothing to run but vanish with the agent, which suits estates with no inbound path. Verify first that CANARY_DOMAINS resolves from the networks you plant tokens in, and pick one mail provider before minting the first token.

Frequently asked questions

How do I install the Canarytokens server?

The README points installation at the Docker image installation process in a separate repository, canarytokens-docker, and gives no command for running this tree directly. The package metadata requires Python >=3.13,<3.14 and builds through hatchling, so a manual install means assembling that environment yourself.

Why do I not get an email when my Canarytokens alert fires?

Unless you run in DEBUG mode, the switchboard allows no more than 1 alert per unique calling IP per minute. The hit is still written to the database and shown in the token management console, so a missing email does not mean the token was untouched.

Can I create AWS key tokens on my own Canarytokens server?

Only with the AWS ID values filled in, because if either CANARY_AWSID_URL or CANARY_AWSID_AUTH is unset, AWS key token creation is disabled for that instance. The infrastructure those values point at has moved to a private repository, which is where most self-hosters stop.

Which email provider should I put in switchboard.env?

One only. Providing CANARY_MAILGUN_API_KEY means removing CANARY_SENDGRID_API_KEY and CANARY_MANDRILL_API_KEY, and European Mailgun infrastructure also needs CANARY_MAILGUN_BASE_URL=https://api.eu.mailgun.net.

Does Canarytokens still send alerts to Slack?

Existing deployments do, because the Slack API Token is deprecated but old tokens still work. No new tokens can be created, so a server built today has no supported way to obtain one.

Which Python versions does Canarytokens support?

The package metadata sets requires-python to >=3.13,<3.14, so 3.13 is the only CPython version in range. The same metadata still carries version 0.3.0 while development happens on the default branch, master.

Official sources

  1. Issues
  2. Project website
  3. README
  4. thinkst/canarytokens on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thinkst-canarytokens.svg)](https://hysenlabs.com/projects/thinkst-canarytokens)