A decoy AWS access key with zero permissions. Plant it somewhere tempting —
a fake ~/.aws/credentials, a repo .env, a CI secret. The moment anyone uses
it, from any account or machine anywhere on Earth, you get a high-confidence
alert. The key can do nothing; the use attempt itself is the intrusion signal.
Built entirely on AWS Always-Free tier. Idle cost: $0.
Attackers who find credentials test them — reflexively, almost always with
aws sts get-caller-identity as the first move. That reflex is the trap. A
zero-permission key produces a CloudTrail record the instant it's used, even
though it can't touch a single resource. You detect the intrusion at the
attacker's very first step, before they can do anything.
This is the same primitive Thinkst Canary commercialized. This repo is a from-scratch, self-hostable implementation you fully control.
Decoy IAM key (no perms)
│ attacker authenticates with it (anywhere)
▼
CloudTrail ── logs the call in YOUR account (global events on)
▼
EventBridge ── rule matches the canary's accessKeyId
▼
Lambda ── extracts IP / UA / key / principal
▼
SNS email + DynamoDB trip log
Every component sits inside AWS Always-Free at demo volume: Lambda (1M req/mo), SNS (1k emails/mo), DynamoDB (25GB), CloudTrail (first trail mgmt-events free), IAM (free).
- Terraform installed (
terraform -version) - AWS CLI installed and authenticated. If
aws sts get-caller-identitydoesn't return your account, runaws configurefirst (Access Key ID, Secret, region, output format). - An IAM user/role with enough permissions to create the stack. This deploys
across several services, so a tightly-scoped user will fail mid-apply with
AccessDeniedand leave a half-built stack. You need create/delete rights on:iam(user + access key + role + role policy),cloudtrail,s3,events(EventBridge),lambda,sns,dynamodb. An admin user works; otherwise scope a policy to those actions. If apply fails partway, run./destroy.shto clean up before retrying.
git clone https://github.com/longmun/aws-canary aws-canary && cd aws-canary
./deploy.sh you@example.com us-east-1The script checks prereqs, runs Terraform, applies an S3 log-expiry lifecycle, and prints your decoy key. Then confirm the SNS email AWS sends you — no confirmation, no alerts.
AWS_ACCESS_KEY_ID=<canary_id> AWS_SECRET_ACCESS_KEY=<canary_secret> \
aws sts get-caller-identityAlert email arrives in ~1–5 min (CloudTrail delivery latency).
./destroy.sh you@example.com us-east-1A live trip, captured end to end:
ALERT published for <attacker_ip> using AKIA-XXXXXXXXXXXX
DynamoDB trip record:
| field | value |
|---|---|
| event | GetCallerIdentity |
| ip | <attacker_ip> |
| key | AKIA-XXXXXXXXXXXX |
| time | 2026-06-01T10:23:21Z |
The alert email carried timestamp, API call, source IP, User-Agent, access key, and principal.
1. The User-Agent leaks more than the IP. My test trip's alert exposed the
full caller environment — aws-cli/2.x … kali-amd64 … python/3.x … command#sts.get-caller-identity. For a defender that's gold: UA is harder to
spoof than an IP and attackers rarely bother. For a red-teamer, it's a
warning — your reflexive get-caller-identity from a stock Kali box brands you
instantly to any canary. The tool that catches attackers also documents the
exact mistake attackers make.
2. Detection ≠ prevention. CloudTrail's 1–5 min lag means by the time you're alerted, the key's already been used. This is a tripwire, not a blocker. Know its job: it tells you you're breached.
3. The IP is a lead, not proof. My test logged my own egress IP. A real attacker uses the key from behind a VPN/Tor/throwaway cloud box. Weight the "key was used at all" signal far above the source IP for attribution.
4. Terraform state holds secrets in plaintext. terraform.tfstate contains
the canary's secret key. The .gitignore here blocks it — the most common
credential-leak-on-GitHub mistake.
5. Cost guardrails are not circuit breakers. An AWS budget alert lags actual spend by hours and only notifies; it never prevents a charge. The only true hard stop is tearing resources down.
- All serverless → no idle cost
- 30-day S3 lifecycle caps log growth (auto-applied by
deploy.sh) - A zero-spend budget alert is recommended (see RUNBOOK)
deploy.sh one-shot deploy (prereq checks + terraform + lifecycle)
destroy.sh teardown
terraform/main.tf all infra (IAM, CloudTrail, EventBridge, Lambda, SNS, DynamoDB)
lambda/handler.py alert handler (SNS publish + DynamoDB log)
docs/RUNBOOK.md operational runbook
MIT — do whatever, no warranty. This is a learning/portfolio project.