CoursesDetection engineeringHoneytokens & deception

Honeytokens & deception

Decoys nothing legitimate ever touches.

Advanced25 min · lesson 13 of 15

Banks keep dye packs in the cash drawer. A dye pack looks like a stack of twenties, sits with the real money, and no honest transaction ever moves it. The moment it leaves the building it bursts and paints the thief bright red. Deception does the same thing to your infrastructure. You plant something that looks valuable (a credential, a user account, a document, a whole fake server) that no person and no process has any legitimate reason to touch, then you wire it so that *any* touch at all sets off an alarm. Every other lesson in this course fights the signal-to-noise war. Deception steps around the fight, because you engineer the rate of harmless, boring interactions down to zero.

Three words first, because people use them loosely. A honeytoken is a decoy piece of *data*: a fake AWS (Amazon Web Services) access key, a bait row in a database, a document that phones home when someone opens it. A honeypot is a decoy *service or machine*: an SSH (Secure Shell, the standard remote-login protocol) daemon that exists only to be broken into. Canary is the umbrella word, popularised by the security company Thinkst, for any tripwire of this kind. What makes them so strong is arithmetic, not cleverness. An alert's precision, the share of its firings that turn out to be real, depends on how often harmless activity trips it, and for a well-placed canary that number is approximately never. A hit means somebody is inside. You usually catch them early, while they are still hunting for credentials or mapping what they can reach, before real damage.

What every tripwire is made of

A deception asset has three parts, and the weakest one decides what the whole thing is worth. The bait has to be findable and believable, planted exactly where intruders go rummaging. MITRE ATT&CK (a public catalogue of the techniques real attackers use, maintained by the MITRE Corporation) files that rummaging under T1552, Unsecured Credentials: ~/.aws/credentials, .env files, CI (continuous integration, your automated build system) variables, shell history, a wiki page titled "prod runbook". The sensor is the telemetry a touch unavoidably produces: a CloudTrail record (CloudTrail is the AWS audit log of who called what), a Windows logon event, a DNS (Domain Name System, the internet's phone book) lookup. The alarm is the plumbing that turns that record into somebody's phone ringing. A decoy with a broken alerting path is worse than no decoy at all. It cost you effort and it buys you false confidence.

Plant a decoy AWS key

Cloud keys are the classic honeytoken for two reasons. Attackers actively grep for them, and AWS hands you a perfect sensor for free. You mint a *real* IAM key (IAM, Identity and Access Management, is the AWS service that decides who is allowed to do what), so the key survives any validity check an intruder runs, but you attach it to a user with no policies at all. Then you add an explicit deny-everything policy, so the key stays harmless even if a colleague later attaches permissions by mistake. Name the user like a dull service account. A user called canary-token fools nobody.

terminal — mint the decoy
$ aws iam create-user --user-name s3-backup-svc \
--tags Key=purpose,Value=deception
{
"User": {
"Path": "/",
"UserName": "s3-backup-svc",
"UserId": "AIDAQ3EGA4EXAMPLE7Y2K",
"Arn": "arn:aws:iam::111122223333:user/s3-backup-svc",
"CreateDate": "2026-07-13T08:12:44+00:00",
"Tags": [
{
"Key": "purpose",
"Value": "deception"
}
]
}
}
$ aws iam put-user-policy --user-name s3-backup-svc \
--policy-name DenyAll \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*"}]}'
# (no output on success)
$ aws iam create-access-key --user-name s3-backup-svc
{
"AccessKey": {
"UserName": "s3-backup-svc",
"AccessKeyId": "AKIA3D7EXAMPLECANARY",
"Status": "Active",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"CreateDate": "2026-07-13T08:13:02+00:00"
}
}

Now plant it. An entry in ~/.aws/credentials on your bastion hosts, a variable in an internal repo's .env.example, a "legacy" secret in CI. Keep a private inventory that maps each key ID to the place you planted it, because when the alert fires that mapping tells you *which host or repo the attacker is standing on*. One deliberate AWS quirk works in your favour here. The call sts:GetCallerIdentity (STS is the Security Token Service, and this call answers the question "who am I?") needs no permissions and cannot be denied by any IAM policy, yet AWS still writes it to CloudTrail. Checking that a stolen key is live is the first thing an attacker does, so your deny-all key rings the bell anyway.

Wiring the alarm so the touch reaches you

CloudTrail records every authenticated management-plane API (application programming interface) call, including the ones that failed with an errorcode of AccessDenied. For paging in real time, an EventBridge rule (EventBridge is the AWS event router) matching userIdentity.accessKeyId is the production wiring. There is one trap in it. A rule left in the default ENABLED state matches only *write* management events, and the first calls an attacker makes (GetCallerIdentity, ListBuckets) are read-only. Create the rule with its state set to ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS, or it will sleep through the entire intrusion. For hunting backwards through history, and for checking your own work, query the CloudTrail archive with Athena, which runs SQL over log files sitting in storage.

Athena — hunt the canary key in CloudTrail
SELECT eventtime, eventname, eventsource,
sourceipaddress, useragent, errorcode
FROM cloudtrail_logs
WHERE useridentity.accesskeyid = 'AKIA3D7EXAMPLECANARY'
ORDER BY eventtime DESC
LIMIT 10;
-- eventtime eventname eventsource sourceipaddress useragent errorcode
-- 2026-07-11T02:14:31Z ListBuckets s3.amazonaws.com 185.220.101.45 aws-cli/2.17.5 ... AccessDenied
-- 2026-07-11T02:14:09Z GetCallerIdentity sts.amazonaws.com 185.220.101.45 aws-cli/2.17.5 ... (null)

Read those two rows as a short story. Someone checked the key was live with GetCallerIdentity, then went straight for the S3 buckets (S3, Simple Storage Service, is where most AWS data lives), from a Tor exit node, the last hop of an anonymity network. That is not a misconfigured script. That is a person holding your credentials file. The on-premises equivalent is a decoy Active Directory account (Active Directory is Microsoft's central directory of users and machines). Nobody ever logs into it, so Windows events 4624 (successful logon) and 4625 (failed logon) against it are pure signal. A sharper variant registers an SPN on the decoy, a Service Principal Name being the label that says "this account runs this service". That makes the account a target for Kerberoasting, where an attacker asks for a service ticket and cracks it offline for the password. The attempt lands as event 4769, usually with the weak RC4 encryption type 0x17, and it trips the decoy without a single logon. Here is the query in Microsoft Sentinel, written in KQL (Kusto Query Language):

KQL — decoy account activity in Sentinel
SecurityEvent
| where EventID in (4624, 4625)
| where TargetUserName =~ "svc-veeam-bak" // decoy — zero legitimate use
| project TimeGenerated, EventID, IpAddress, WorkstationName, LogonType
| order by TimeGenerated desc
// TimeGenerated EventID IpAddress WorkstationName LogonType
// 2026-07-12 23:41:07 4625 10.20.8.71 FIN-WKS-114 3
// 2026-07-12 23:40:58 4625 10.20.8.71 FIN-WKS-114 3

Decoy services with OpenCanary

Honeytokens catch credential hunting. Honeypots catch network exploration. OpenCanary is Thinkst's open-source honeypot daemon, and it is low-interaction, meaning it copies only the opening handshake and the login banner of a protocol. It *pretends* to be SSH, RDP (Remote Desktop Protocol, the Windows remote-screen service), Telnet, MySQL or an HTTP admin panel, and it writes every connection and login attempt as JSON (JavaScript Object Notation, plain-text structured data) that you ship straight into your SIEM (Security Information and Event Management, the platform where your logs land and your alerts are built). Because the whole thing is mimicry, an attacker never gets a real shell on it, so the decoy itself carries almost no risk of being turned against you.

terminal — run an SSH decoy with OpenCanary
$ python3 -m venv env && . env/bin/activate
$ pip install opencanary
$ opencanaryd --copyconfig
# [*] A sample config file is ready /etc/opencanaryd/opencanary.conf
# enable "ssh.enabled": true (and set a fake "ssh.version" banner), then:
$ opencanaryd --start --uid=nobody --gid=nogroup
$ tail -1 /var/tmp/opencanary.log
{"dst_host": "10.20.8.9", "dst_port": 22, "local_time": "2026-07-12 23:58:41",
"logdata": {"LOCALVERSION": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6",
"PASSWORD": "P@ssw0rd!", "REMOTEVERSION": "SSH-2.0-libssh_0.9.6",
"USERNAME": "root"},
"logtype": 4002, "node_id": "opencanary-dc1",
"src_host": "10.20.44.17", "src_port": 51988}

logtype: 4002 means an SSH login attempt, and the record carries the username and password the attacker typed. That often tells you *which* stolen credential set they are working from. On a flat internal network, any connection to a machine that advertises no legitimate service is somebody looking for a way to move sideways. There is no tuning conversation to have here. Open an incident and go and look at the source host.

Limits, cost, and production practice

Deception is the cheapest signal in detection engineering. An IAM user costs nothing. OpenCanary runs happily on a leftover VM (virtual machine). Thinkst's hosted canarytokens.org mints document, DNS, URL and AWS-key tokens for free. Set that against the analyst hours a single noisy correlation rule burns every month and the return is absurd. Which is exactly why deception sits alongside your Sigma rules and your UEBA baselines (User and Entity Behaviour Analytics, the models that learn what normal looks like for each account) rather than competing with them.

A careful attacker can spot your canary without ever touching it
An AWS access key ID *contains* the account ID it belongs to, and anybody can decode that offline. No API call, no CloudTrail record, no alert for you. Researchers have published the account IDs behind the popular hosted canary services, so a careful intruder checks any key they find against that list and walks quietly around your dye pack. The fix: mint decoy keys in a dedicated AWS account you own yourself, and make the decoy's name, tags and placement look exactly like your real service accounts.

Know what deception cannot do. It proves presence, not technique. A tripped canary says "somebody is in here", not how they got in. Its coverage is a matter of luck as much as design, because an attacker who never rummages in the drawer you baited stays invisible to it, and that is why deception never replaces rule-based or behaviour-based detection. It also demands housekeeping. Document every decoy *out of band*, somewhere your own admins and red team will find it, so nobody burns incident hours chasing your bait or helpfully "tidies it up". Refresh the file timestamps so the bait does not look abandoned. And fire each token yourself once a quarter, to prove the page still reaches a human at the far end.

The deception kill chain
1Plant the bait
decoy key, SPN'd account, fake SSH box
2Attacker touches it
credential hunting or lateral movement
3Sensor fires
CloudTrail, event 4625/4769, OpenCanary JSON
4Page immediately
precision ≈ 1, no triage queue
Test the whole chain end to end on a schedule. A decoy whose alarm path quietly broke is pure false confidence.

A tripped canary is the one class of alert you escalate *without* triage. Its precision is effectively 1, so near enough every firing is real, and under-reacting to your highest-signal detection throws away the whole point of building it. Be precise about the words, though. The firing is a detection. The incident is confirmed afterwards, when you follow the key ID back to the host or repo you planted it on and find out what else that identity touched. You page first and you confirm second. That precision figure doubles as a benchmark: it is what a perfect alert looks like. The next lesson, Alert quality & tuning, is about dragging the other ninety-nine percent of your alert corpus, the rules that really do have to fight the noise war, as close to that bar as engineering allows.

Good deception is boring by design. Put honeytokens where attackers look: in a .env.example, in a database table called something like "backup", in a cloud secret named as if it guarded production payments. Then write down the legitimate scanners and humans who might stumble across them, as documented exclusions. If your CI secret scanner trips the canary every night, you do not have a canary. You have a pager. Prefer tokens whose use reveals intent. A cloud key that exists only to answer GetCallerIdentity still proves theft the moment anyone calls it.

Try this

Plant a canary AWS key in a lab repo, never a real account key. Watch CloudTrail for that access key ID, then prove to yourself that any use of it pages you.

terminal
$ # Generate a decoy-looking key id/secret for documentation only — do NOT create a real IAM user
$ CANARY_AKIA=AKIAAAAAAAAAAAAAAAAA
$ # Alert rule sketch (CloudTrail / SIEM): any event where userIdentity.accessKeyId == canary
$ cat <<'EOF'
title: Canary Access Key Used
logsource: { product: aws, service: cloudtrail }
detection:
selection:
userIdentity.accessKeyId: AKIAAAAAAAAAAAAAAAAA
condition: selection
level: critical
EOF
# Trigger test: from a lab runner, attempt an AWS call with the decoy creds.
# Expected: auth fails AND your canary alert still fires on the attempt (or on a real canary user).

Takeaway

A canary is a tripwire that nothing legitimate should ever touch. Hold its false-positive rate at zero, keep honeypots walled off from anything real, and treat every hit as hostile until you have proved otherwise.

Next step: put one honeytoken in a non-production secret store, alert on any use of it, and write down the scanner exclusions so your own CI does not burn the signal on night one.

Quick check
01You plant a decoy AWS key and build an EventBridge rule that matches its access key ID so any use pages you. As a test you validate the key with GetCallerIdentity and then run ListBuckets. The rule never fires. What is the fix?
Correct — This is the trap the lesson calls out: in the default ENABLED state the rule ignores read-only calls like GetCallerIdentity and ListBuckets, so it sleeps through the whole intrusion until you enable all management events.
Incorrect — No. A deny-all policy never suppresses logging. Denied calls are still written to CloudTrail with an errorcode of AccessDenied, and that record is exactly what drives the alarm.
Incorrect — No. GetCallerIdentity cannot be denied by any policy and it IS logged in CloudTrail. That undeniable-but-logged behaviour is the reason a deny-all decoy still rings the bell.
Incorrect — No. Once the rule captures all management events, read-only calls do reach EventBridge in near real time. Athena is for hunting backwards through history and checking your work, not a hard ceiling on what you can page on.
02Your decoy AWS key is minted correctly, seeded in a bastion host's credentials file, and wired to a working alarm. An experienced intruder still finds it, ignores it, and never trips a thing. What happened, and what is the mitigation?
Incorrect — No. Reading a policy takes an authenticated API call, which would be logged and would page you, and the attacker's key has no permission to make it. Removing the deny-all also hands a real, usable credential to whoever finds it.
Incorrect — No. The decoy in this lesson is a genuine IAM key from a genuine IAM user, so it looks and validates exactly like any other key. There is no separate canary prefix to fix.
Correct — This is the fingerprinting problem in the warning box. The account ID is sitting inside the key ID, so a careful intruder identifies a hosted canary without touching it, and your own dedicated account plus convincing naming is the way to close that gap.
Incorrect — No. Tags are not broadcast to anyone who finds a key, and reading them would take an authenticated, logged API call the decoy user cannot make. The offline giveaway is the account ID baked into the key ID itself.
03At 23:58 your OpenCanary decoy on 10.20.8.9 writes a record with logtype 4002, src_host 10.20.44.17, USERNAME root and PASSWORD P@ssw0rd!. That decoy host runs no legitimate service and nothing in your estate is supposed to connect to it. What do you do?
Incorrect — No. Low-interaction means there is no real shell behind the banner, so a successful login is never coming. The connection attempt itself is the signal, and waiting for one that cannot happen means waiting forever.
Correct — On a flat internal network, any connection to a host that advertises no legitimate service is lateral-movement reconnaissance, so there is no tuning conversation to have. The username and password the attacker typed also tell you which credential set is in play.
Incorrect — No. That noise argument belongs to internet-facing services. This is an internal decoy that nothing legitimate should ever reach, and tuning the hit away destroys the entire value of the tripwire.
Incorrect — No. Precision on a decoy with no legitimate service is effectively 1, so a single hit is exactly the evidence you built it to collect. Under-reacting to your highest-signal detection wastes the whole investment.

Related