rCTF Docs

Konata

CLI tool and CI actions for syncing CTF challenges to rCTF, building images, and rolling out Kubernetes manifests.

Konata is a CTF challenge-management tool, invoked as kona on the command line. It walks a repository of challenge directories, builds and pushes Docker images, applies Kubernetes manifests, and syncs the resulting challenge metadata to rCTF (and CTFd, when configured).

The whole workflow is driven by a single root kona.yml for global settings, plus a per-challenge kona.yml in each challenge directory.

This page covers Konata as it relates to rCTF. For the upstream tool and source, check the Konata repository.

Note (Pre-1.0 status)

Konata is pre-1.0. The schema is settled enough that we’ve used it to run public CTFs end-to-end (the DiceCTF Quals 2026 challenge repository is a good real-world reference), but breaking changes are still on the table until 1.0.

Install#

Konata is published on PyPI:

Terminal window
pip install konata
kona --help

Python 3.12 or newer is required. The kona binary exposes two commands. The sync command is the main workflow, and compress is a helper for packaging attachment folders ahead of time.

Repository layout#

Konata assumes a directory tree with one root config at kona.yml and one config per challenge somewhere underneath. The challenge folder structure itself is arbitrary. Konata walks up to discovery.challenge_folder_depth levels deep (default 3), looking for files named kona.yml or kona.yaml.

A typical layout (taken from the DiceCTF Quals 2026 deployment repository):

  • ctf-challenges/
    • kona.yml Global config
    • web/
      • dicewallet/
        • kona.yml Per-challenge config
        • Dockerfile
        • challenge/
      • mirror-temple/
        • kona.yml Per-challenge config
    • pwn/
      • garden/
        • kona.yml Per-challenge config
    • sanity/
      • survey/
        • kona.yml Skipped because discovery.skip: true

Global config#

The root kona.yml configures credentials, clusters, registries, and shared template overrides. Everything below is optional. A minimum config just needs an rctf block.

kona.yml
secrets:
token:
env: RCTF_TOKEN
clusters:
prod:
gcloud:
cluster_name: kctf-cluster
project: dicectf-2026-quals
zone: europe-west1-b
main:
alias_to: prod
registries:
challenges: europe-west1-docker.pkg.dev/dicectf-2026-quals/challenges
instancer-challenges: us-central1-docker.pkg.dev/dicectf-2026-quals/challenge-registry
domains:
kctf: chals.dicec.tf
rctf:
base_url: 'https://ctf.example.com'
team_token:
secret: token

secrets#

Named references that the rest of the config can pull from. Each entry must specify exactly one of:

FieldBehavior
valueInline literal. Useful for non-secret values you still want to centralize.
file_pathPath read from disk at load time. Relative paths resolve against the directory containing the root kona.yml.
envRead from the environment. Konata fails fast if the variable is unset.

The reference is then used wherever the schema accepts a secret or value field:

rctf:
team_token:
secret: token # resolves to secrets.token

rctf#

rCTF API credentials. Konata calls into rCTF over the public admin API.

FieldPurpose
base_urlPublic origin of the rCTF instance (no trailing slash).
team_tokenAdmin team token. Accepts either secret: <name> or value: <literal>.
extra_headersOptional headers added to every request. Useful for a deploy-token gate at the reverse proxy.

A ctfd block with the same shape exists for CTFd deployments, and both can coexist if you mirror the same challenge repo to multiple platforms.

clusters#

Named Kubernetes clusters that challenge kubernetes_manifests / kubernetes_inline_manifests deployments target. Each entry picks exactly one auth backend:

BackendUse when
gcloudTargeting GKE. Konata runs gcloud container clusters get-credentials under the hood, so the workflow needs a logged-in gcloud and the gke-gcloud-auth-plugin.
kindLocal Kind cluster. cluster_name defaults to kind.
kubeconfigInline kubeconfig pulled from a secret or value.
use_default: trueUse $KUBECONFIG or ~/.kube/config from the host (the default).
incluster: trueUse the in-pod service-account credentials when Konata itself runs inside the cluster.

alias_to lets a challenge reference one cluster name while routing to another. The DiceCTF example uses this so per-challenge configs say cluster_name: main, while the operator swaps main → prod at the root level.

registries#

Aliases for container registry prefixes. A challenge’s deployment.images[].registry_name looks up an entry here, then prepends the resolved value to the image name. Splitting registries (one for static-hosted images, one for instancer images) is common when the rCTF instancer cluster and the kCTF cluster live in different GCP projects.

domains#

Free-form key/value map of domain names available to the Jinja templates inside challenge configs ({{ config.domains['kctf'] }} in the examples). Keeps the deploy domain out of every per-challenge config.

templates#

Overrides for the rendered challenge description and endpoint block. The defaults wrap the description with a connection-info section followed by **Author**: .... Overrides are Jinja2 templates with challenge, config, and models available.

FieldPurpose
challenge_descriptionTop-level description template. Receives endpoints_rendered already filled in by the per-provider endpoints template.
endpoints_text.rctfEndpoints template used when syncing to rCTF. The default renders a > [!CONNECTION] callout, which the rCTF frontend turns into a styled connection-info box.
endpoints_text.ctfdEndpoints template used when syncing to CTFd. The default is plain socat/nc/ncat --ssl/http(s) lines.
ctfd_attributionSuffix appended to the description on CTFd syncs (defaults to **Author**: {{ challenge.author }}).

endpoints_text is per-provider because rCTF and CTFd render markdown differently. Each provider sees its own rendered endpoints, and then challenge_description is run once per provider with that provider’s endpoints_rendered in scope. To override just one side, set only that key. The other keeps its default.

templates:
endpoints_text:
rctf: |
{% for endpoint in challenge.endpoints %}
...your override...
{% endfor %}

discovery#

Top-level discovery options.

FieldDefaultPurpose
challenge_folder_depth3Max depth from root when scanning for kona.yml / kona.yaml.
attachment_analysis_depth50Per-challenge cap when walking attachment file lists.
klodd_domain-Klodd domain when using the Klodd integration.
klodd_endpoint_name-Klodd endpoint identifier.

attachment_format#

tar_gz (default) or zip. Picks the archive format Konata uses when bundling attachment files for upload.

Per-challenge config#

Each challenge directory has a kona.yml (or kona.yaml) that declares one or more challenges plus an optional deployment block. The simplest static challenge needs nothing but a category, name, author, description, and flag:

sanity/survey/kona.yml
challenges:
- category: sanity
name: survey
author: defund
sort_weight: 98
description: |
Thanks for participating! Fill out this [survey](https://forms.gle/xxx) to get the flag.
flags:
rctf: dice{thanks_for_playing_dicectf!!!!!!}
scoring:
initial_value: 1
minimum_value: 1
rctf:
eligible_for_tiebreaks: false
discovery:
skip: true

discovery.skip: true opts the directory out of discovery, which is useful when the file is committed but the challenge isn’t ready to deploy yet.

Challenge fields#

FieldPurpose
categoryChallenge category. Combined with name to form the default challenge ID.
nameChallenge name. Becomes the slug under which Konata syncs it.
authorRendered into the default description template.
descriptionMarkdown description. Trimmed of leading/trailing whitespace before rendering.
override_idReplaces the default <category>_<name> challenge ID. Useful when renaming a challenge without breaking already-recorded solves.
attachmentsFile list or full AttachmentConfig. See below.
scoringInitial / minimum point values plus per-platform overrides (scoring.rctf.eligible_for_tiebreaks, scoring.ctfd.decay, …).
flagsPer-platform flags. flags.rctf is either a literal string or { file: <path> } / { str: <value> }.
endpointsStatic endpoints (host/port) rendered into the description by the endpoints template.
hiddenWhen true, the challenge is uploaded but not released.
sort_weightNumeric sort hint passed through to rCTF.
instancer_configrCTF instancer config (see Instancer)

Attachments#

Three forms are accepted:

A bare list:

attachments:
- dist/bzImage
- dist/initramfs.cpio.gz

A full AttachmentConfig:

attachments:
files:
- 'build.sh'
- 'Dockerfile'
- 'garden'
- 'garden.c'
- 'ld-linux-x86-64.so.2'
- 'libc.so.6'
exclude:
- '**/__pycache__'
additional:
- path: flag.txt
str: 'dice{dummy_flag}'
pre_compressed:
- dist/handout.tar.gz
FieldPurpose
filesFiles relative to the challenge directory. Directory entries (chall/src/) recurse into all files underneath.
excludeGlobs filtered out of the resolved file list before archiving.
additionalSynthetic files injected into the archive. Each entry specifies path plus exactly one of str or base64. A typical use is shipping a dummy flag file so the build still works.
pre_compressedArchives that are uploaded as-is instead of being repacked. The challenge page shows them under their original filenames.

Flags#

flags is a per-platform map. Each platform has its own shape, where rCTF accepts exactly one flag and CTFd accepts a list. Inside each platform, a flag value can be an inline literal string, { str: ... }, or { file: ... } (read at sync time, with the path resolved relative to the challenge directory).

# rCTF flag, inline literal. This form is the most common.
flags:
rctf: dice{example}
# rCTF flag, explicit str form. Equivalent to the inline literal above.
flags:
rctf:
str: dice{example}
# rCTF flag, read from a file. Pairs well with an `additional` attachment that ships a dummy flag.txt
# (see Attachments) so the build context stays self-contained.
flags:
rctf:
file: flag.txt
FieldTypeNotes
flags.rctfstring | { str } | { file }Single flag synced to rCTF.
flags.ctfd[]listMultiple flags synced to CTFd.
flags.ctfd[].typestringCTFd flag type. Defaults to static. Pass regex for regex flags.
flags.ctfd[].flagstring | { str } | { file }Flag value. Same three forms as flags.rctf.

Endpoints#

Static-hosting deployments declare endpoints so Konata can render connection info into the description. Each entry has a type (one of http, https, socat, nc, ncat-ssl), an endpoint host, and an optional port. Jinja templating works on endpoint, so the host can be built from the challenge name and the global domains map:

endpoints:
- type: nc
endpoint: "{{ challenge.name }}.{{ config.domains['kctf'] }}"
port: 1337

Deployment#

deployment declares what Konata should build and what manifests to apply on top of the challenge sync. Both blocks are optional, so a static challenge with just a flag and an attachment skips deployment entirely.

Building images#

deployment:
images:
- build_context: .
dockerfile: bot/Dockerfile # optional, defaults to <build_context>/Dockerfile
name: '{{ challenges[0].name }}'
tag: latest
registry_name: instancer-challenges
platform: linux/amd64
build_args:
ENV: prod
no_cache: false
exports:
- stage: out
src: /out
dst: ./handout
FieldPurpose
build_context (alias path)Build-context directory relative to the challenge folder.
dockerfilePath to a non-default Dockerfile.
name, tagImage name and tag. The published image is <registries[registry_name]>/<name>:<tag>.
registry_nameLookup key into the root-level registries map.
platformOptional target platform. linux/amd64 is the common choice for CTF builds.
build_argsBuild-time --build-arg values.
no_cacheForces a clean build when true.
exportsMulti-stage build exports. Copies the contents of src in a named stage out to dst on the host. Used for shipping handouts that fall out of the build.

Image references inside Kubernetes manifests can interpolate {{ images[challenge.name] }} to pull in the fully-resolved registry/name:tag for the current challenge.

Kubernetes manifests#

Two equivalent forms:

deployment:
kubernetes_manifests:
- paths:
- manifests/deployment.yaml
- manifests/service.yaml
cluster_name: main
rollout_restart:
image: true

…or inline documents:

deployment:
kubernetes_inline_manifests:
- cluster_name: main
documents:
- apiVersion: kctf.dev/v1
kind: Challenge
metadata:
name: '{{ challenges[0].name }}'
spec:
deployed: true
image: '{{ images[challenges[0].name] }}'
network:
public: true
ports:
- protocol: TCP
port: 1337
targetPort: 5000
FieldPurpose
cluster_nameCluster from the root clusters map (or an alias_to) to apply against.
paths / documentsYAML files on disk or inline objects. Both render as Jinja templates with challenge, challenges, images, and config available.
rollout_restart.imageWhen true (the default), triggers a rollout restart of the matching Deployment whenever the resolved image digest changes.
rollout_restart.annotation_pathOptional JSON-path-style hook for restarting on annotation changes.

Instanced challenges#

For challenges using the rCTF instancer, add instancer_config. The schema mirrors the rCTF instancer config. The outer envelope is the same across providers, and only the inner config differs (Docker Compose-like for docker-instancer, pods[] for k8s-instancer). The whole Konata schema accepts both snake_case and camelCase keys.

web/mirror-temple/kona.yml
challenges:
- category: web
name: mirror-temple
author: arcblroth
description: |
stare long enough at the void and the void stares back
attachments:
files:
- 'Dockerfile'
- 'chall/src/'
flags:
rctf:
file: flag.txt
instancer_config:
challenge_integration_id: '{{ challenge.name }}'
timeout_milliseconds: 1800000
extendable: true
expose:
- kind: https
host_prefix: '{{ challenge.name }}'
container_name: app
container_port: 8080
config:
pods:
- name: app
ports:
- protocol: TCP
name: http-service
port: 8080
spec:
containers:
- name: app
image: '{{ images[challenge.name] }}'
resources:
requests:
cpu: '500m'
memory: '500Mi'
limits:
cpu: '3'
memory: '2Gi'
restartPolicy: Always
terminationGracePeriodSeconds: 0
automountServiceAccountToken: false
egress: true
deployment:
images:
- build_context: .
name: '{{ challenges[0].name }}'
tag: latest
registry_name: instancer-challenges
platform: linux/amd64

The instanced flow only needs pod definitions. The rCTF instancer’s k8s-controller handles namespaces, services, network policies, and ingress at runtime, so no kubernetes_inline_manifests block is required.

CLI#

kona sync#

The main entry point. Walks the deploy directory, builds and pushes any declared images, applies Kubernetes manifests, then syncs challenge metadata to every configured platform.

Terminal window
kona sync -d ./ctf-challenges
FlagBehavior
-d, --deploy-directoryRoot of the deploy repo (the folder containing the root kona.yml). Required.
--only <name>Repeatable. Restricts the run to specific challenge folder names. Discovery still walks the tree, and non-matching challenges are skipped.
--challenge-path <path>Repeatable. Direct paths to challenge directories, bypassing discovery entirely. The CI integration uses this to scope each matrix shard to one challenge.

kona compress#

Helper for producing an attachment archive from a folder or file. Useful when you want to commit a pre-built handout and reference it through attachments.pre_compressed.

Terminal window
kona compress ./challenge/dist --format zip --output handout.zip
FlagBehavior
path (positional)Source file or directory.
-f, --formattar_gz (default) or zip.
-o, --outputOutput path. Defaults to <src>.{tar.gz,zip} in the current directory.

Both formats are deterministic. File mtimes are zeroed (fixed to 1980-01-01 for zip, 0 for tar.gz), uid/gid become 0, uname/gname become kona, permissions are normalized to 0777, the gzip header drops its own filename and mtime, and entries are sorted by archive path. Compressing the same set of files always produces a byte-identical archive (and therefore an identical hash), so attachment dedup and CI cache hits stay stable across machines, runs, and users. Konata applies the same normalization to attachments it builds internally during sync.

CI integration#

The project-sekai-ctf/konata-deploy-action GitHub Action exposes two subactions. The detect action walks the repository and emits a matrix of changed challenges, and sync runs kona sync --challenge-path <one> for one matrix shard. Together they restrict each push to redeploying only the challenges that changed.

The DiceCTF Quals 2026 workflow is a good reference:

.github/workflows/deploy.yaml
name: Deploy challenges
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.detect.outputs.matrix }}
should_sync: ${{ steps.detect.outputs.should-sync }}
steps:
- uses: actions/checkout@v6
- uses: project-sekai-ctf/konata-deploy-action/detect@main
id: detect
sync:
needs: detect
if: needs.detect.outputs.should_sync == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
challenge: ${{ fromJson(needs.detect.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v6
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.SERVICE_ACCOUNT }}
# Doing it like this is faster than anything else
- name: Install gke-gcloud-auth-plugin
run: |
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --batch --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list > /dev/null
sudo apt-get update -o Dir::Etc::sourcelist=/etc/apt/sources.list.d/google-cloud-sdk.list -o Dir::Etc::sourceparts="-"
sudo apt-get install -yq --no-install-recommends google-cloud-cli-gke-gcloud-auth-plugin
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker europe-west1-docker.pkg.dev,us-central1-docker.pkg.dev --quiet
- uses: project-sekai-ctf/konata-deploy-action/sync@main
with:
challenge-path: ${{ matrix.challenge }}
env:
RCTF_TOKEN: ${{ secrets.RCTF_TOKEN }}

Notable bits:

  • Workload Identity Federation is preferred over a long-lived JSON key. The id-token: write permission is what makes google-github-actions/auth@v3 work with WIF.
  • gcloud auth configure-docker is needed for every Artifact Registry host the matrix shard might push to.
  • RCTF_TOKEN is the admin team token referenced by the root kona.yml (secrets.token.env: RCTF_TOKEN).

Real-world reference#

The DiceCTF Quals 2026 challenges repository is the most complete public Konata + rCTF deployment. It covers static-hosted kCTF challenges, rCTF-instancer challenges, file-backed flags, dummy-flag injection via additional attachments, multi-cluster registries, and the change-detected CI matrix.

Esc

Start typing to search the docs.