Contents
API docs

Canary tokens

Issuing, the secret you see once, rotating, revoking, and what each field means.

View as Markdown

A canary token is a real credential that grants next to nothing, minted so that its first use is a signal. Two kinds exist today. A GitLab project access token is a Guest on a project Feint controls, with the create_runner scope, which GitLab refuses on every API call while still recording that the call was made. An AWS IAM access key belongs to a user with no permissions in an AWS account Feint owns: every call but sts:GetCallerIdentity is denied, and every call is written to CloudTrail.

Issuing#

POST /v1/tokens with a JSON body. placement is the only required field.

Field Type Meaning
provider string Platform to mint on: gitlab (the default) or aws_iam. Where AWS decoys are not yet enabled, aws_iam is refused with 400.
placement string Where the secret will live: prod .env on web-01, CI variable GITLAB_DEPLOY_TOKEN in group/infra. One line, at most 200 characters. The alert names it, so write it for whoever will be reading the alert.
metadata object Your own notes, kept with the canary and shown on alerts. At most 20 entries; keys up to 64 characters, string values up to 500. Small non-string values are kept as JSON.
ttl_days integer Lifetime, 1–3650 days. Default 365. GitLab enforces the expiry itself; Feint revokes an expired AWS key within minutes. Either way a canary.expired alert says the decoy has gone dark.

Whitespace in placement and in metadata is collapsed to single spaces. A placement of nothing but whitespace is refused with 400.

curl -s -X POST https://api.feintsecurity.com/v1/tokens \
  -H "Authorization: Bearer $FEINT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "placement": "CI variable GITLAB_DEPLOY_TOKEN in group/infra",
    "metadata": {"env": "prod", "owner": "platform"},
    "ttl_days": 90
  }'
r = requests.post(
    f"{API}/tokens",
    headers=HEADERS,
    json={
        "placement": "CI variable GITLAB_DEPLOY_TOKEN in group/infra",
        "metadata": {"env": "prod", "owner": "platform"},
        "ttl_days": 90,
    },
)

The secret#

The 201 response is the only place secret appears. Feint does not store it. It keeps secret_prefix, the first 12 characters, and secret_fingerprint, a SHA-256 of the whole secret. Both are on every read, so a credential found in the wild can be matched to its canary without the secret ever being held again:

printf '%s' "$FOUND_TOKEN" | shasum -a 256

An AWS key is a pair. external_id is the access key ID (AKIA…) and secret the secret access key; place both, as a [default] profile in ~/.aws/credentials, as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or as the JSON aws iam create-access-key prints. The access key ID is not secret (it is in every CloudTrail record, and it is what GuardDuty shows), so secret_prefix carries the whole of it and secret_fingerprint is the SHA-256 of the secret access key alone. Abridged:

{
  "id": "5c1f8e2d-9a7b-4c3e-b0d1-6f7a8b9c0d1e",
  "provider": "aws_iam",
  "name": "deploy-ci-3f9a1c",
  "placement": "~/.aws/credentials on bastion-1",
  "status": "active",
  "secret_prefix": "AKIAEXAMPLEKEY000001",
  "external_id": "AKIAEXAMPLEKEY000001",
  "scopes": [],
  "access_level": null,
  "secret": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "secret_notice": "This is the only time the secret is returned. Feint does not store it."
}

name is the credential's name at the provider: ci- and ten characters for a GitLab token, an IAM user name like deploy-ci-3f9a1c for an AWS key, on purpose. Whoever holds the credential can read its name, and a name that mentioned Feint or the placement would give the game away.

Status#

status Meaning
active Armed: the credential exists at the provider and Feint is watching it.
expired Past expires_at. Derived, so it is reported the moment it becomes true; a canary.expired alert follows within minutes.
rotated Replaced by a newer canary, which names this one in rotated_from_id.
revoked Retired: by you through the API or the console, or found revoked at the provider by someone else, which also raises an alert.
missing The provider has no record of the credential and Feint did not remove it. An alert was raised when this was noticed.

Once it is used#

Field Meaning
last_used_at When the credential was last seen used: GitLab's throttled last-used time, or the latest CloudTrail record for an AWS key. Non-null means the canary has fired.
last_used_ips Every source address reported for it, accumulated over time.
last_checked_at When Feint's monitor last looked.
needs_attention Used, and nobody has acknowledged since. A fresh use after an acknowledgement raises it again.
acknowledged_at, acknowledged_by The last acknowledgement, made in the console.

The events behind these fields are listed by GET /v1/alerts.

Acknowledging a used canary, and editing a canary's placement or metadata, are done in the console for now. The API reads the result.

Rotating#

POST /v1/tokens/{id}/rotate mints a replacement with the same placement, metadata and original lifetime, returns it with its new secret, and revokes the old credential at the provider. The old record becomes rotated; the new one points back to it in rotated_from_id. Rotation does not count against your plan's canary cap.

The replacement is minted before the old token is revoked, so a failure leaves you with a working canary rather than none. If the new one exists but the old could not be revoked, the response is 400 and says so; revoke the old credential by hand at the provider.

Rotate when the secret is lost, or when the placement has been refreshed and the old secret would look stale.

Revoking#

DELETE /v1/tokens/{id} revokes the credential at the provider and marks the record revoked with revoked_at. Revoking an already revoked canary returns the record unchanged. Retired canaries stay in your list and keep their history.

Limits#

An organization on the Free plan can have 5 active canaries — active and not yet expired — at a time; Pro has no cap. Issuing past the cap returns 400 with a message saying so. Revoked, rotated and expired canaries do not count, and rotation never does.