Writing and running A/D checkers
Attack & Defence checkers are organiser-operated trusted infrastructure. Competing teams must never run, configure, or gain access to them. Run each checker on an organiser-controlled host or isolated part of the service environment that the team cannot access.
The packaged biterra checker is the starting point for non-professional events. It probes one service with one published default flag store. Serious events should use an organiser-written checker against the API so they can implement service-specific placement and retrieval, multiple flag stores, and public attack information. In both cases, the organiser—not a competing team—operates the checker.
The current checker protocol has two credentials:
- An enrollment token created by a world admin.
- A runtime token issued to one specific team/service checker instance.
Enrollment tokens are short-lived and are only used to connect a new checker. Runtime tokens are what an enrolled checker uses afterwards when it fetches work and submits results.
Overview
A checker loop normally does this:
- Exchange an enrollment token for a runtime token.
- Fetch the current assignment for its team/service.
- Probe the target instance.
- Submit the result for that assignment.
- Repeat while the checker stays enrolled.
Each runtime token is scoped to one configured team/service instance. It cannot be reused for another team, another service, or a second live checker for the same instance.
Environment
At minimum, a custom checker usually needs:
export BITERRA_API_URL="https://your-world.example"
export BITERRA_ENROLLMENT_TOKEN="btr_ad_..."
export BITERRA_ATTACK_URL="https://team-a.example"
After enrollment, it will receive a runtime token:
export BITERRA_RUNTIME_TOKEN="btr_adi_..."
Create the enrollment token from Admin → Attack Defence → Checker API in your world admin. That token is an organiser secret and should only be used to bootstrap a checker container.
Running the biterra CLI
The biterra CLI already implements this flow:
- use the enrollment token
- let the checker pick a team and service
- exchange for a runtime token
- fetch assignments
- submit results
The CLI repository includes organiser-owned examples under examples/attack-defence for all supported probe types:
webtcpbinarycommandcustomgrpc
Every result can include a short diagnostic. Diagnostics must not contain flags, tokens, credentials, request headers, or sensitive response bodies.
Example:
export BITERRA_API_URL="https://your-world.example"
export BITERRA_ENROLLMENT_TOKEN="btr_ad_..."
export BITERRA_PROBE_TYPE="web"
export BITERRA_PROBE_WEB_URL="http://127.0.0.1:8080/health"
biterra init
biterra run
Checker API flow
Custom checkers use five public endpoints:
GET /api/ad/v1/checker/enroll/optionsPOST /api/ad/v1/checker/enrollPUT /api/ad/v1/checker/teams/instancesGET /api/ad/v1/checker/work/currentPOST /api/ad/v1/checker/results
1. Discover available team/service options
Use the enrollment token:
curl -H "Authorization: Bearer $BITERRA_ENROLLMENT_TOKEN" \
"$BITERRA_API_URL/api/ad/v1/checker/enroll/options"
Response shape:
{
"success": true,
"data": {
"protocol_version": 1,
"teams": [
{ "uid": "team-a", "name": "Attackers", "color": "#ef4444" }
],
"services": [
{ "uid": "svc-web", "name": "Web", "slug": "web" }
]
}
}
2. Exchange the enrollment token for a runtime token
Use the selected team_uid and service_uid:
curl -X POST "$BITERRA_API_URL/api/ad/v1/checker/enroll" \
-H "Authorization: Bearer $BITERRA_ENROLLMENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"team_uid": "team-a",
"service_uid": "svc-web"
}'
Response shape:
{
"success": true,
"data": {
"protocol_version": 1,
"runtime_token": "btr_adi_...",
"team_uid": "team-a",
"service_uid": "svc-web",
"round_duration_seconds": 30
}
}
This runtime token is now the credential for the checker. The original enrollment token is no longer used for work submission.
3. Publish service addresses
Use the runtime token to check in the player-facing address:
curl -X PUT "$BITERRA_API_URL/api/ad/v1/checker/teams/instances" \
-H "Authorization: Bearer $BITERRA_RUNTIME_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instances": [{
"team_uid": "team-a",
"service_uid": "svc-web",
"attack_url": "https://team-a.example",
"checker_url": "http://team-a-web.internal",
"defend_url": "http://team-a-web.internal"
}]
}'
Address selection is explicit: opponents receive attack_url; defenders receive defend_url with an attack_url fallback; checkers receive checker_url with an attack_url fallback. The CLI performs this check-in from BITERRA_ATTACK_URL when biterra run starts.
4. Fetch current work
Use the runtime token:
curl -H "Authorization: Bearer $BITERRA_RUNTIME_TOKEN" \
"$BITERRA_API_URL/api/ad/v1/checker/work/current"
When a round is active and the service is live, Biterra returns an assignment:
{
"success": true,
"data": {
"protocol_version": 1,
"round_duration_seconds": 30,
"assignment": {
"uid": "assignment-uid",
"round_uid": "round-uid",
"round_index": 12,
"result_deadline": "2026-08-01T12:00:05.000Z",
"require_placement_for_sla": true,
"require_retrieval_for_sla": true,
"recovering_sla_percent": 50,
"team_uid": "team-a",
"service_uid": "svc-web",
"service_name": "Web",
"service_slug": "web",
"scoring": true,
"target": "http://team-a-web.internal",
"stores": [{
"uid": "store-default",
"slug": "default",
"name": "Default",
"current": { "generation_uid": "generation-uid", "flag": "BTR{...}" },
"previous": []
}]
}
}
}
If no round is active, or the service is not currently checkable, assignment is null.
5. Submit the result
Use the runtime token and the returned assignment.uid:
curl -X POST "$BITERRA_API_URL/api/ad/v1/checker/results" \
-H "Authorization: Bearer $BITERRA_RUNTIME_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"assignment_uid": "assignment-uid",
"status": "up",
"diagnostic": "HTTP 200 matched expected body",
"stores": [{
"generation_uid": "generation-uid",
"placed": true,
"state": "optional private checker state",
"retrievals": [],
"attack_info": { "path": "/records/42" }
}]
}'
Allowed statuses are:
uprecoveringdownfaultyflag_missing
placed: true means the checker successfully placed that store generation. Previous-generation retrieval evidence belongs in retrievals.
Public attack_info versus private state
These fields have different consumers and must not be confused:
| Field | Written by | Read by | Purpose |
|---|---|---|---|
attack_info | Organiser's custom checker | Competing teams through GET /api/ad/v1/player/targets | Public identifiers an exploit needs to locate the flag-bearing account or object |
state | Organiser's custom checker | That checker only, in later work | Private data needed to retrieve or verify the flag, such as credentials or an internal record ID |
For example, a checker could create public username alina_schneider, store the flag in record 42, and keep a generated password privately:
{
"generation_uid": "generation-uid",
"placed": true,
"attack_info": {
"username": "alina_schneider",
"record_id": "42"
},
"state": "private retrieval data; never returned to players",
"retrievals": []
}
An attacker receives the target address plus the public username and record ID, then supplies them to their exploit. The attacker never receives state. attack_info is optional and is not an exploit tutorial: publish only the minimal public lookup values required to select the object. It must never contain flags, credentials, tokens, vulnerability details, private checker state, or sensitive response data. Biterra limits the encoded object to 4096 characters, rejects issued Biterra flags embedded anywhere in it, and exposes it only after successful placement while that generation remains in the six-generation validity window.
This pattern is commonly called a flag ID in Attack & Defence systems. attack_info is Biterra's structured equivalent, allowing one or more named public identifiers per store generation.
The packaged CLI refuses biterra check and biterra run when a service has anything other than one published store named default. It reports only the service-level probe evidence it produced; it does not claim placement or retrieval. Use a custom checker for multi-store services.
The checker does not submit a points value. Biterra calculates SLA points from the effective world and service settings. A submitted up result receives full SLA when its configured placement and retrieval requirements are satisfied. A submitted recovering result, or an up result missing required evidence, receives the configured recovering percentage. Results are accepted through result_deadline; after the round is finalised they are rejected. The response includes submitted_status, effective_status, the evidence outcome, and awarded_points. Retrying the same assignment cannot award points twice.
The packaged checker keeps the default behavior: placement and retrieval are not required unless an organiser enables them, and it sends only evidence its configured probe actually produced.
Minimal loop
This is the shape of a custom checker loop. Real checkers should add logging, retries, and service-specific validation.
const apiUrl = process.env.BITERRA_API_URL;
const enrollmentToken = process.env.BITERRA_ENROLLMENT_TOKEN;
async function adFetch(path, init = {}) {
const response = await fetch(`${apiUrl}${path}`, {
...init,
headers: {
"content-type": "application/json",
...init.headers,
},
});
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
return response.json();
}
async function enroll() {
const options = await adFetch("/api/ad/v1/checker/enroll/options", {
headers: { authorization: `Bearer ${enrollmentToken}` },
});
const teamUid = options.data.teams[0].uid;
const serviceUid = options.data.services[0].uid;
const enrolled = await adFetch("/api/ad/v1/checker/enroll", {
method: "POST",
headers: { authorization: `Bearer ${enrollmentToken}` },
body: JSON.stringify({
team_uid: teamUid,
service_uid: serviceUid,
}),
});
return { runtimeToken: enrolled.data.runtime_token, teamUid, serviceUid };
}
async function checkerFetch(runtimeToken, path, init = {}) {
return adFetch(path, {
...init,
headers: {
authorization: `Bearer ${runtimeToken}`,
...init.headers,
},
});
}
async function isServiceUp(target) {
const response = await fetch(`${target}/health`, {
signal: AbortSignal.timeout(5000),
});
return response.ok;
}
const { runtimeToken, teamUid, serviceUid } = await enroll();
await checkerFetch(runtimeToken, "/api/ad/v1/checker/teams/instances", {
method: "PUT",
body: JSON.stringify({
instances: [{
team_uid: teamUid,
service_uid: serviceUid,
attack_url: process.env.BITERRA_ATTACK_URL,
checker_url: process.env.BITERRA_CHECKER_URL,
}],
}),
});
for (;;) {
const work = await checkerFetch(runtimeToken, "/api/ad/v1/checker/work/current");
const assignment = work.data.assignment;
const roundDurationSeconds = work.data.round_duration_seconds || 30;
if (assignment) {
const up = await isServiceUp(assignment.target).catch(() => false);
const store = assignment.stores[0];
await checkerFetch(runtimeToken, "/api/ad/v1/checker/results", {
method: "POST",
body: JSON.stringify({
assignment_uid: assignment.uid,
status: up ? "up" : "down",
diagnostic: up ? "health check passed" : "health check failed",
stores: [{
generation_uid: store.current.generation_uid,
placed: false,
retrievals: [],
}],
}),
});
}
await new Promise((resolve) => setTimeout(resolve, roundDurationSeconds * 1000));
}
Important auth rules
- Enrollment tokens only work with
enroll/optionsandenroll. - Runtime tokens work with instance check-in,
work/current, andresults. - A runtime token is bound to one configured team/service instance.
- If an admin revokes checker access, the runtime token immediately stops working.
- If a checker is re-enrolled, it receives a new runtime token.
Retries and idempotency
Result submission is idempotent per checker instance and assignment. Retrying the same assignment result does not double-award points.
Retry guidance:
- Retry
429and5xxresponses with backoff and jitter. - Retry network timeouts for the same assignment.
- Do not loop on
401or403; fix the credential instead. - Treat
409during enrollment as “that team/service already has an active checker”. - Keep probe timeouts shorter than the round duration so checks do not overlap indefinitely.
Checker-managed flags
Biterra issues the current flag in the checker assignment. For rotating-flag services:
- the checker fetches the current assignment
- places the returned flag on the target
- reports placement, private state, retrieval evidence, and optional public attack information for each store
Each store's previous array is included so a checker can verify recently valid generations and report retrieval evidence.
Checker-facing APIs never reveal other teams’ admin secrets or stored flag history beyond the assigned current/recent values needed for that checker instance.