Skip to content

Authoring challenges

A challenge is packaged as a container image plus a ChallengeTemplate describing how to run it.

Image requirements

KIMO runs challenge containers hardened, so the image must:

  • run as a non-root user (runAsNonRoot is enforced — the image needs a numeric non-root USER, or one resolvable by the runtime),
  • tolerate a read-only root filesystem — only /tmp is writable (an emptyDir mount),
  • not need privilege escalation or a service-account token — both are off.

Most purpose-built challenge images already fit. For off-the-shelf bases, prefer unprivileged variants (e.g. nginxinc/nginx-unprivileged instead of nginx).

A complete template

apiVersion: v1
kind: Secret
metadata:
  name: web-sqli-101-flag
type: Opaque
stringData:
  flag: "FLAG{sql1_1nj3ct10n_1s_fun}"
---
apiVersion: kimo.kimo.io/v1alpha1
kind: ChallengeTemplate
metadata:
  name: web-sqli-101
spec:
  category: web
  difficulty: easy
  points: 100
  flagSecretRef:
    name: web-sqli-101-flag
  instanceMode: perTeam
  ttl: 30m
  maxInstances: 200
  pow:
    enabled: true
    difficulty: 20
  container:
    image: registry.example.com/challenges/web-sqli:v1
    ports:
      - name: http
        containerPort: 8080
        expose: true
    resources:
      requests: { cpu: 100m, memory: 128Mi }
      limits: { cpu: 500m, memory: 256Mi }
    env:
      - name: FLAG
        valueFrom:
          secretKeyRef: { name: web-sqli-101-flag, key: flag }
    readiness:
      type: tcp
      port: 8080
    unhealthyThreshold: 3

Flags

The flag lives in a Kubernetes Secret referenced by flagSecretRef; the template controller refuses to mark a template ready until it exists. Inject it into the container however your challenge expects it — the common pattern is an env var from the secret, as above. Per-instance dynamic flags aren't built in yet; a challenge can derive one inside the container from instance-specific data if needed.

Readiness

The readiness check is what moves an instance from Creating to Running, and what flags it Unhealthy later:

  • type: tcp — port connect check (default when ports exist).
  • type: http — HTTP GET on path/port; anything 2xx/3xx passes.
  • type: none — no probe; the instance is Running as soon as the pod is.

Pick a check that actually exercises the challenge. unhealthyThreshold (default 3) is how many consecutive failed observations it takes before the instance is marked Unhealthy and your scoreboard is notified.

Instance scoping and limits

  • instanceModeshared (one instance for everyone), perTeam, or perPlayer. This is how the REST API names instances (<template>-<team>), which naturally dedupes per team.
  • maxInstances — cap on concurrent instances of this template.
  • ttl — instance lifetime (Go duration syntax: 30m, 2h). Players can be granted more time via the extend endpoint.

Restart policy

Instances are Deployment-managed, and Kubernetes requires restartPolicy: Always for Deployment pods — so Always (or omitting the field) is the only accepted value. The template controller rejects OnFailure/Never at validation time with a clear message rather than letting the Deployment fail later.

Proof of work

Set pow.enabled: true to require solving a SHA-256 puzzle before each instance launch — see Proof of work. difficulty is the number of leading zero bits (20 ≈ a million hashes, about a second of client CPU).

Grouping into rounds

Bundle templates into a ChallengeSet to open and close them on a schedule:

apiVersion: kimo.kimo.io/v1alpha1
kind: ChallengeSet
metadata:
  name: round-1
spec:
  challenges: [web-sqli-101, web-xss-201]
  schedule:
    startAt: "2026-09-01T10:00:00Z"
    endAt: "2026-09-01T18:00:00Z"

A set with no schedule is always active.