Contents
API docs

Quickstart

Issue a canary, place it, and see the first alert, in about five minutes.

View as Markdown

You will create an API key, issue one canary, put its secret somewhere an intruder would look, and watch the alert arrive. Every command is copy-paste; only the placeholders change.

1. Create an API key#

In the console, open API keys and create a key with all three scopes: tokens:write to issue the canary, tokens:read to look it up, alerts:read to see the alert. The secret starts with ak_ and is shown once. Keep it in the environment rather than in a file:

export FEINT_API_KEY=ak_…

Admins and members of the organization can create keys; a read-only seat cannot. Authentication has the details.

2. Issue a canary#

placement is the one field you must supply: where the secret is about to live, precisely enough that the alert tells you where to look.

curl -s -X POST https://api.feintsecurity.com/v1/tokens \
  -H "Authorization: Bearer $FEINT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"placement": "prod .env on web-01", "metadata": {"owner": "platform"}}'
import os
import requests

API = "https://api.feintsecurity.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FEINT_API_KEY']}"}

r = requests.post(
    f"{API}/tokens",
    headers=HEADERS,
    json={"placement": "prod .env on web-01", "metadata": {"owner": "platform"}},
)
r.raise_for_status()
canary = r.json()
print(canary["id"])
print(canary["secret"])

The response is 201 Created, and it is the only time you will see secret. Abridged:

{
  "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "provider": "gitlab",
  "name": "ci-3f9a1b2c4d",
  "placement": "prod .env on web-01",
  "metadata": {"owner": "platform"},
  "status": "active",
  "secret_prefix": "glpat-Xy7Q2k",
  "expires_at": "2027-09-26T00:00:00Z",
  "created_at": "2026-09-26T09:14:07.412903Z",
  "last_used_at": null,
  "needs_attention": false,
  "secret": "glpat-Xy7Q2k…",
  "secret_notice": "This is the only time the secret is returned. Feint does not store it."
}

Canary tokens lists every field.

Feint keeps only the first 12 characters of the secret and a SHA-256 fingerprint of the whole. If you lose it, rotate the canary and place the new one.

3. Place it#

Put the secret where it looks like it belongs and where nothing legitimate reads it. An entry in a .env file that is never loaded, a CI variable named like a deploy token, a line in an internal runbook:

GITLAB_TOKEN=glpat-Xy7Q2k…
# rotated 2026-09-26 — do not commit

Then let go of the secret. The canary is a real GitLab project access token with the least GitLab allows — a Guest with the create_runner scope — so it is refused on every API call, and each refusal is recorded.

That is the default provider. An AWS decoy ("provider": "aws_iam") is a key pair, placed as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY or as a profile in ~/.aws/credentials; Canary tokens has the details.

4. Watch for the alert#

Feint polls GitLab every few minutes, and GitLab reports a token's use at most once every ten minutes or so, so the first alert lands within roughly a quarter of an hour of the first use. Your team is emailed at the same time, and any webhook endpoint you registered receives it. The API shows the same events:

curl -s "https://api.feintsecurity.com/v1/alerts?limit=20" \
  -H "Authorization: Bearer $FEINT_API_KEY"
r = requests.get(f"{API}/alerts", headers=HEADERS, params={"limit": 20})
r.raise_for_status()
for event in r.json():
    print(event["kind"], event["canary_token_id"], event["occurred_at"], event["source_ips"])

To see one without waiting for an intruder, use the canary yourself:

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "PRIVATE-TOKEN: glpat-Xy7Q2k…" https://gitlab.com/api/v4/user

GitLab refuses the call and records the use anyway. A little later the alert appears, abridged:

[
  {
    "id": "0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f",
    "canary_token_id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
    "kind": "canary.used",
    "occurred_at": "2026-09-26T09:41:00Z",
    "detected_at": "2026-09-26T09:45:12.088311Z",
    "source_ips": ["203.0.113.44"]
  }
]

5. Retire it#

When the exercise is over, revoke the canary. GitLab revokes the token; the record stays in your list as revoked.

curl -s -X DELETE https://api.feintsecurity.com/v1/tokens/8d3e6f1a-2b4c-4d5e-9f01-23456789abcd \
  -H "Authorization: Bearer $FEINT_API_KEY"
r = requests.delete(f"{API}/tokens/{canary['id']}", headers=HEADERS)
r.raise_for_status()

Or keep it. A canary costs nothing to leave in place; the default lifetime is 365 days and can be set per canary.

Next#

  • Authentication — scopes, and giving a CI job only what it needs.
  • Canary tokens — every field, rotation, limits.
  • Alerts — polling without missing or double-counting an event.