Skip to content

Scoring backends

A backend connects KIMO to your scoring platform. It does two jobs:

  1. Authenticate REST API callers — KIMO has no user database; the backend decides who a request belongs to.
  2. Receive lifecycle events — every instance phase change is pushed to it, so the scoreboard can show players their instance URL, warn them before expiry, and so on.

The active backend is chosen at deploy time (integration.backend in Helm, or the KIMO_BACKEND env var) and configured with an opaque JSON blob only that backend understands.

Generic (HMAC webhooks)

The default. Works with any platform that can send a bearer token and receive webhooks.

integration:
  backend: generic
  config:
    apiKey: "choose-a-strong-key"

Auth — requests must carry Authorization: Bearer <apiKey>.

Events — POSTed as JSON to every registered webhook, signed with X-KIMO-Signature: <hex HMAC-SHA256 of the body> so the receiver can verify authenticity. Register webhooks at runtime:

curl -X POST localhost:8080/api/v1/webhooks/configure \
  -H "Authorization: Bearer $KIMO_API_KEY" \
  -d '{"url": "https://scoreboard.example.com/kimo-events", "secret": "webhook-secret"}'

CTFd

integration:
  backend: ctfd
  config:
    baseUrl: "https://ctf.example.com"      # your CTFd instance
    webhookUrl: "https://ctf.example.com/plugins/kimo/events"  # optional
    apiKey: "<ctfd admin access token>"

Auth — the caller's token is validated against CTFd's /api/v1/users/me; the resolved user and team become the request's principal. The resolved team is authoritative: instance creation ignores any team in the request body, so a player can only ever spawn instances for their own CTFd team (teams mode required).

Events — POSTed to webhookUrl (if set) as:

{
  "type": "instance.running",
  "challenge_id": "web-sqli-101",
  "team_id": "team-alpha",
  "instance_url": "web-sqli-101-team-alpha.default.svc:8080"
}

The typical player flow: a "Launch instance" button on the CTFd challenge page calls KIMO's create endpoint with the player's CTFd token, and the page shows the instance URL from the instance.running event (or by polling the get-instance endpoint).

The challenge-page plugin

hack/ctfd-plugin/ is a small CTFd plugin that puts a Launch instance panel on challenge pages — launch button, endpoint, TTL countdown, and stop, all inside the challenge modal. For each action it mints a short-lived CTFd token for the logged-in player and calls KIMO with it, so KIMO's ctfd backend resolves the player and team from CTFd itself; the token is deleted right after.

A challenge maps to the KIMO template with the same name, or to an explicit one via a kimo:<template> tag. Challenges KIMO doesn't know simply get no panel.

Testing locally

The repo ships a throwaway CTFd (with the plugin mounted) for exercising this backend on a local cluster — SQLite-backed, everything in emptyDirs:

kubectl create namespace ctfd --dry-run=client -o yaml | kubectl apply -f -
kubectl -n ctfd create configmap kimo-plugin \
  --from-file=__init__.py=hack/ctfd-plugin/__init__.py \
  --from-file=kimo.js=hack/ctfd-plugin/assets/kimo.js \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f hack/ctfd-dev.yaml

kubectl -n ctfd port-forward svc/ctfd 8000:8000 &
hack/ctfd-setup.sh   # wizard + demo challenge + player + team, prints an API token

kubectl -n workspace-system set env deploy/workspace-controller-manager \
  KIMO_BACKEND=ctfd \
  'KIMO_BACKEND_CONFIG={"baseUrl":"http://ctfd.ctfd.svc.cluster.local:8000"}'

Then log in at http://localhost:8000 as the printed player, open the demo-web challenge, and hit Launch. Direct API calls with Authorization: Token <printed token> authenticate against that CTFd too.

Writing your own backend

Implement the Backend interface and register a factory:

package myplatform

import (
    "context"
    "encoding/json"
    "net/http"

    "github.com/hermannchristopher/kimo/internal/integrations"
)

type backend struct{ /* your config */ }

func init() {
    integrations.Register("myplatform", func(cfg json.RawMessage) (integrations.Backend, error) {
        b := &backend{}
        if err := json.Unmarshal(cfg, b); err != nil {
            return nil, err
        }
        return b, nil
    })
}

func (b *backend) Name() string { return "myplatform" }

func (b *backend) Notify(ctx context.Context, e integrations.Event) error {
    // push e to your platform
    return nil
}

func (b *backend) Authenticate(r *http.Request) (integrations.Principal, error) {
    // resolve the caller from r (token header, session, ...)
    return integrations.Principal{Subject: "alice", Team: "team-alpha"}, nil
}

Import the package for side effects in cmd/manager/main.go, rebuild, and select it with KIMO_BACKEND=myplatform. A backend can optionally also implement WebhookRegistrar to support the /api/v1/webhooks/configure endpoint.

Event reference

Field Meaning
Type One of instance.creating, instance.running, instance.unhealthy, instance.expiring, instance.expired, instance.failed, instance.deleted.
Instance Instance name.
Challenge Template name.
Team / Player Who the instance belongs to.
Endpoint In-cluster address of the instance's service.
Reason Human-readable detail for the transition.