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 (
runAsNonRootis enforced — the image needs a numeric non-rootUSER, or one resolvable by the runtime), - tolerate a read-only root filesystem — only
/tmpis writable (anemptyDirmount), - 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 onpath/port; anything 2xx/3xx passes.type: none— no probe; the instance isRunningas 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¶
instanceMode—shared(one instance for everyone),perTeam, orperPlayer. 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.