Architecture¶
KIMO is a single binary that runs five controllers and an HTTP API inside one manager process.
flowchart TB
subgraph clients [Clients]
PL[Players]
SP[Scoring platform]
end
subgraph kimo [KIMO manager]
API[REST API :8080]
TC[Template controller]
IC[Instance controller]
LC[Lifecycle controller]
SC[Set controller]
NC[NetworkFence controller]
BE[Backend adapter]
end
subgraph cluster [Kubernetes]
CT[ChallengeTemplate]
CI[ChallengeInstance]
CS[ChallengeSet]
NF[NetworkFence]
W[Deployment / Service / Pod]
NP[NetworkPolicy]
end
PL -->|create/list/delete instances| API
SP -->|auth + events| BE
API --> CI
TC --> CT
IC --> CI
IC --> W
IC --> NF
NC --> NF
NC --> NP
LC --> CI
SC --> CS
IC -.->|phase events| BE
LC -.->|expiry events| BE
The controllers¶
Template controller — validates each ChallengeTemplate: the flag secret must exist, the image must be set, and the restart policy must be Deployment-compatible. It publishes status.ready and a live count of instances referencing the template. Instances of a template that is not ready wait in Pending.
Instance controller — the core state machine. For each ChallengeInstance it creates a hardened Deployment, a Service for the exposed ports, and a NetworkFence, all owned by the instance so deletion cascades. It then watches the pod (via a label-based pod watch, since the pod is two ownership hops away) and drives the phase: no pod yet → Creating, pod ready → Running, readiness failing past the template's threshold → Unhealthy. Every phase change is pushed to the scoring backend.
Lifecycle controller — enforces TTLs. It requeues itself to wake exactly at the two boundaries that matter: entering the 60-second Expiring grace window (backend notified, so a scoreboard can warn the player), and expiry itself, where it notifies the backend and deletes the instance — Kubernetes garbage collection then removes the Deployment, Service, and NetworkPolicy.
Set controller — flips a ChallengeSet active/inactive based on its schedule window, requeuing at the next boundary so rounds open and close without external triggers. A set with no schedule is always active.
NetworkFence controller — translates each NetworkFence into a deny-by-default NetworkPolicy selecting the instance's pods. See Network isolation.
The REST API¶
The API server (:8080) runs alongside the controllers and talks to the cluster through the same client. Authentication is fully delegated to the active backend adapter — KIMO has no user database of its own. See the REST API reference.
The backend seam¶
Every lifecycle transition emits an Event to a single Backend interface:
type Backend interface {
Name() string
Notify(ctx context.Context, event Event) error
Authenticate(r *http.Request) (Principal, error)
}
That interface is the only seam between KIMO and the outside scoring world — swap the adapter and nothing else changes. See Scoring backends.
Pod hardening¶
Challenge containers are deliberately locked down:
| Setting | Value |
|---|---|
runAsNonRoot |
true |
readOnlyRootFilesystem |
true (writable /tmp via emptyDir) |
allowPrivilegeEscalation |
false |
automountServiceAccountToken |
false |
Challenge images must tolerate a read-only root filesystem; anything that only needs scratch space under /tmp works unmodified.