Skip to main content

Writing and running A/D checkers

Attack & Defence checkers are intentionally distributed. You can run one checker beside each challenge service, or run a larger process that checks several services, but Biterra does not require a central ticker.

The current checker protocol has two credentials:

  1. An enrollment token created by a world admin.
  2. 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:

  1. Exchange an enrollment token for a runtime token.
  2. Fetch the current assignment for its team/service.
  3. Probe the target instance.
  4. Submit the result for that assignment.
  5. 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_INSTANCE_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:

  1. use the enrollment token
  2. let the checker pick a team and service
  3. exchange for a runtime token
  4. fetch assignments
  5. submit results

The CLI repository includes customer-owned examples under examples/attack-defence for all supported probe types:

  • web
  • tcp
  • binary
  • command
  • custom
  • grpc

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/options
  • POST /api/ad/v1/checker/enroll
  • PUT /api/ad/v1/checker/teams/instances
  • GET /api/ad/v1/checker/work/current
  • POST /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 the instance URL

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",
"instance_url": "https://team-a.example"
}]
}'

The CLI performs this check-in from BITERRA_INSTANCE_URL when biterra run starts. The checker can probe a different Docker-local address, but it must publish an address players can reach.

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,
"tick_index": 4,
"team_uid": "team-a",
"service_uid": "svc-web",
"service_name": "Web",
"service_slug": "web",
"scoring": true,
"target": "http://team-a-web.internal",
"flag": "BTR{...}",
"previous_flags": []
}
}
}

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",
"state": "optional checker state",
"placed": true
}'

Allowed statuses are:

  • up
  • down
  • faulty
  • flag_missing

placed: true means the checker successfully placed or refreshed the assignment flag for that generation.

The checker does not submit a points value. Biterra calculates SLA points from the service configuration when it accepts an up result for a scoring assignment. The result response includes awarded_points, and retrying the same assignment cannot award those points twice.

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,
instance_url: process.env.BITERRA_INSTANCE_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);

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",
placed: true,
}),
});
}

await new Promise((resolve) => setTimeout(resolve, roundDurationSeconds * 1000));
}

Important auth rules

  • Enrollment tokens only work with enroll/options and enroll.
  • Runtime tokens work with instance check-in, work/current, and results.
  • 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 429 and 5xx responses with backoff and jitter.
  • Retry network timeouts for the same assignment.
  • Do not loop on 401 or 403; fix the credential instead.
  • Treat 409 during 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 whether placement happened with placed: true

previous_flags is included so a checker can tolerate propagation lag and still recognise recently valid flags.

Checker-facing APIs never reveal other teams’ admin secrets or stored flag history beyond the assigned current/recent values needed for that checker instance.