Konata
CLI tool and CI actions for syncing CTF challenges to rCTF, building images, and rolling out Kubernetes manifests.
Konata deploys challenges from a repository. The kona
One root kona.yaml holds shared settings. Each challenge directory has its own kona.yaml with the challenge metadata and deployment instructions.
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 has been used for public events, but it is still pre-1.0 and its configuration may change incompatibly before the 1.0 release.
Install
Konata is published on PyPI:
pip install konatakona --helpKonata requires Python 3.12 or newer. Use sync to deploy challenges and compress to create attachment archives separately.
Repository layout
The root and per-challenge files can use either the .yaml or .yml extension. Challenge directories can be arranged however you like. Konata searches to discovery.challenge_folder_depth, which defaults to three levels below the root.
A typical layout (taken from the SekaiCTF 2026 challenge repository):
sekaictf-2026/
- kona.yaml Global config
web/
migurimental/
- kona.yml Per-challenge config
- challenge/
- solution/
game/
minions-in-16k/
- kona.yaml Per-challenge config
- challenge/
- dist/
misc/
survey/
- kona.yaml Per-challenge config
Global config
The root config contains credentials and any shared clusters, registries, domains, or templates. Only the rctf block is required for an rCTF-only setup.
secrets: token: env: RCTF_TOKEN minions-queue-secret: env: MINIONS_QUEUE_SECRET minions-rctf-secret: env: MINIONS_RCTF_SECRET ppfarming-api-ip: env: PPFARMING_API_IP
clusters: prod: gcloud: clusterName: sekaictf-infra project: sekaictf-500215 zone: europe-west1-b instancer: gcloud: clusterName: instancer-cluster project: sekaictf-500215 zone: europe-west1 main: aliasTo: prod
registries: challenges: europe-west1-docker.pkg.dev/sekaictf-500215/sekaictf instancer-challenges: europe-west1-docker.pkg.dev/sekaictf-500215/challenge-registry
domains: static: chals.sekai.team instancer: instancer.sekai.team
rctf: baseUrl: 'https://ctf.sekai.team' teamToken: secret: token
templates: challengeDescription: | {{ challenge.description }}
{{ endpoints_rendered.strip() }}secrets
The secrets map gives sensitive or repeated values a name. Each entry uses exactly one source:
| Field | Behavior |
|---|---|
value |
Inline literal. Useful for non-secret values you still want to centralize. |
file_path |
Path read from disk at load time. Relative paths resolve against the directory containing the root kona.yml. |
env |
Read from the environment. Konata fails fast if the variable is unset. |
Use the name anywhere a field accepts secret:
rctf: team_token: secret: token # resolves to secrets.tokenrctf
rCTF API credentials. Konata calls into rCTF over the public admin API.
| Field | Purpose |
|---|---|
base_url |
Public origin of the rCTF instance (no trailing slash). |
team_token |
Admin team token. Accepts either secret: <name> or value: <literal>. |
extra_headers |
Optional headers added to every request. Useful for a deploy-token gate at the reverse proxy. |
A ctfd block with the same structure is available for CTFd deployments. Both blocks can be used together when the same challenge repository is deployed to multiple platforms.
clusters
Named Kubernetes clusters that challenge kubernetesManifests / kubernetesInlineManifests deployments target. Each entry picks exactly one auth backend:
| Backend | Use when |
|---|---|
gcloud |
Required for GKE. Konata runs gcloud container clusters get-credentialsgcloudgke-gcloud-auth-plugin. |
kind |
Local Kind cluster. clusterName / cluster_name defaults to kind. |
kubeconfig |
Inline kubeconfig pulled from a secret or value. |
use_default: true |
Use $KUBECONFIG or ~/.kube/config from the host (the default). |
incluster: true |
Use the in-pod service-account credentials when Konata itself runs inside the cluster. |
alias_to / aliasTo redirects one cluster name to another. This lets challenge files target a stable name such as main while the root config decides which cluster that means.
registries
The registries map names container registry prefixes. An image selects one with registryName / registry_name, and Konata prepends the mapped prefix to the image name. Separate entries are useful when shared and instanced challenges publish to different registries.
domains
The domains map makes hostnames available to Jinja templates, for example {{ config.domains['static'] }}. Changing a deployment domain then requires one root edit instead of changes to every challenge.
templates
Templates control the final challenge description and connection block. They use Jinja2 and can read challenge, config, and models.
| Field | Purpose |
|---|---|
challenge_description |
Top-level description template. Receives endpoints_rendered already filled in by the per-provider endpoints template. |
endpoints_text.rctf |
Endpoints 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.ctfd |
Endpoints template used when syncing to CTFd. The default is plain socat/nc/ncat --ssl/http(s) lines. |
ctfd_attribution |
Suffix appended to the description on CTFd syncs (defaults to **Author**: {{ challenge.author }}). |
rCTF and CTFd have separate endpoint templates, so either format can be changed independently.
templates: endpoints_text: rctf: | {% for endpoint in challenge.endpoints %} ...your override... {% endfor %}discovery
Top-level discovery options.
| Field | Default | Purpose |
|---|---|---|
challenge_folder_depth |
3 |
Max depth from root when scanning for kona.yml / kona.yaml. |
attachment_analysis_depth |
50 |
Per-challenge cap when walking attachment file lists. |
klodd_domain |
- | Klodd domain when using the Klodd integration. |
klodd_endpoint_name |
- | Klodd endpoint identifier. |
attachment_format
Choose tar_gz (the default), zip, or 7z for generated attachments. Password protection always uses 7z.
attachment_wrap_dir
When true, the default, generated archives place their files under a directory named after the archive or challenge. Set it to false to put files at the archive root.
Per-challenge config
A challenge file declares one or more challenges and may include a deployment block. A static challenge needs only its category, name, author, description, and flag:
challenges: - category: misc name: Survey author: SekaiCTF releaseTime: '2026-06-28T20:00:00Z' description: | This year we tried many new things compared to previous iterations, and we would love your feedback
[https://forms.gle/KdEuEWyUxeX5CEe26](https://forms.gle/KdEuEWyUxeX5CEe26) tags: - SEKAI flags: rctf: 'SEKAI{thx_4_play1ng_sekaictf_2026!}' scoring: initialValue: 39 minimumValue: 39 rctf: eligibleForTiebreaks: falsediscovery.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
| Field | Purpose |
|---|---|
category |
Challenge category. Combined with name to form the default challenge ID. |
name |
Challenge name. Becomes the slug under which Konata syncs it. |
author |
Rendered into the default description template. |
description |
Markdown description. Trimmed of leading/trailing whitespace before rendering. |
override_id |
Replaces the default <category>_<name> challenge ID. Useful when renaming a challenge without breaking already-recorded solves. |
tags |
Free-form label list synced to both rCTF and CTFd. |
attachments |
File list or full AttachmentConfig. See below. |
scoring |
Initial / minimum point values plus per-platform overrides (scoring.rctf.eligibleForTiebreaks, scoring.ctfd.decay, …). |
flags |
Per-platform flags. flags.rctf is either a literal string or { file: <path> } / { strContent: <value> }. |
endpoints |
Static endpoints (host/port) rendered into the description by the endpoints template. |
hidden |
When true, the challenge is uploaded but not released. |
releaseTime / release_time |
Optional datetime for delayed release. |
sortWeight / sort_weight |
Numeric sort hint passed through to rCTF. |
instancerConfig / instancer_config |
rCTF instancer config (see Instancer) |
adminBot / admin_bot |
Admin-bot challenge source for the rCTF admin bot. See Admin bot. |
Release scheduling
Use releaseTime / release_time when a challenge should be uploaded ahead of time but hidden from participants until a specific moment:
challenges: - category: crypto name: needLe in a multivariate sekai author: sceleri releaseTime: 2026-06-27T08:00:00Z flags: rctf: SEKAI{example}Konata accepts a datetime value and sends it to rCTF as releaseTime in Unix milliseconds. Use a timestamp with a timezone, such as 2026-02-07T18:00:00Z. Konata treats timestamps without a timezone as UTC.
Scoring
By default, Konata creates a decay challenge. initialValue and minimumValue set its point range, while the rCTF block controls tiebreak eligibility:
scoring: initialValue: 500 minimumValue: 50 rctf: eligibleForTiebreaks: trueFor rCTF dynamic scoring, set scoring.rctf.kind to dynamic and provide a webhook secret. As elsewhere in Konata, the secret can come from either secret or value.
secrets: minions-rctf-secret: env: MINIONS_RCTF_SECRETchallenges: - overrideId: game_minions-in-16k category: game name: Minions in 16k author: mixy1 scoring: rctf: kind: dynamic dynamic: secret: secret: minions-rctf-secrettransport currently defaults to webhook. The resolved secret becomes the rCTF dynamic-scoring webhook secret used to authenticate score submissions to POST /api/v2/challs/:id/scores. See Submit dynamic scores for the request format.
Attachments
Three forms are accepted:
A bare list:
attachments: - dist/bzImage - dist/initramfs.cpio.gzA full AttachmentConfig:
attachments: files: - 'challenge/' exclude: - 'challenge/flag.txt' stripComponents: 1 additional: - path: flag.txt strContent: 'SEKAI{dummy_flag}' preCompressed: - dist/handout.tar.gz| Field | Purpose |
|---|---|
files |
Files relative to the challenge directory. Directory entries (chall/src/) recurse into all files underneath. |
exclude |
Globs filtered out of the resolved file list before archiving. |
additional |
Synthetic files injected into the archive. Each entry specifies path plus exactly one of strContent / str or base64Content / base64. A typical use is shipping a dummy flag file so the build still works. |
preCompressed / pre_compressed |
Archives that are uploaded as-is instead of being repacked. The challenge page shows them under their original filenames. |
archiveName / archive_name |
Base name for the generated archive (and the wrap directory when attachmentWrapDir / attachment_wrap_dir is on). Defaults to the challenge ID. Characters illegal in filenames are replaced with _. |
stripComponents / strip_components |
Number of leading path components to drop from each entry’s archive path, like tar --strip-components0. Use it to flatten a nested dist/ or handout/ prefix out of the downloaded archive. |
password |
Encrypts the generated archive with AES-256 behind this password. Setting a password forces the 7z format regardless of attachmentFormat / attachment_format. Does not apply to preCompressed archives, which are uploaded as-is. |
Note (Attachment matching is strict)
A files glob that matches nothing, a named file that doesn’t exist, or a missing preCompressed archive now fails the sync instead of being silently skipped. Fix the path or drop the entry.
Flags
flags is configured separately for each platform. rCTF accepts exactly one flag, while CTFd accepts a list. A flag can be an inline string, { str: ... } / { strContent: ... }, or { file: ... }. Konata reads file-backed flags during sync and resolves their paths relative to the challenge directory.
# rCTF flag, inline literal. This form is the most common.flags: rctf: SEKAI{example}
# rCTF flag, explicit str form. Equivalent to the inline literal above.flags: rctf: str: SEKAI{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| Field | Type | Notes |
|---|---|---|
flags.rctf |
string | { str } | { strContent } | { file } |
Single flag synced to rCTF. |
flags.ctfd[] |
list | Multiple flags synced to CTFd. |
flags.ctfd[].type |
string |
CTFd flag type. Defaults to static. Pass regex for regex flags. |
flags.ctfd[].flag |
string | { str } | { strContent } | { file } |
Flag value. Same forms as flags.rctf. |
Endpoints
Use endpoints to add connection information to a static challenge’s description. Each entry has a type, a host in endpoint, and an optional port. The supported types are http, https, socat, nc, and ncat-ssl. The host supports Jinja templates:
endpoints: - type: nc endpoint: "{{ challenge.name | lower }}.{{ config.domains['static'] }}" port: 1337Deployment
The optional deployment block tells Konata which images to build and which Kubernetes manifests to apply. Omit it for challenges that need only metadata and attachments.
Building images
deployment: images: - path: challenge dockerfile: deploy/sandbox/rootless.Containerfile # optional, defaults to <path>/Dockerfile name: minions tag: latest registryName: instancer-challenges platform: linux/amd64 buildArgs: ENV: prod noCache: false exports: - stage: out src: /out dst: ./handout| Field | Purpose |
|---|---|
path (alias buildContext / build_context) |
Build-context directory relative to the challenge folder. |
dockerfile |
Path to a non-default Dockerfile. |
name, tag |
Image name and tag. The published image is <registries[registryName]>/<name>:<tag>. |
registryName / registry_name |
Lookup key into the root-level registries map. |
platform |
Optional target platform. linux/amd64 is the common choice for CTF builds. |
buildArgs / build_args |
Build-time --build-arg values. |
noCache / no_cache |
Forces a clean build when true. |
exports |
Multi-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: kubernetesManifests: - paths: - manifests/deployment.yaml - manifests/service.yaml clusterName: main rolloutRestart: image: true…or inline documents:
deployment: kubernetesInlineManifests: - clusterName: main documents: - apiVersion: v1 kind: Namespace metadata: name: 67-hunt - apiVersion: sctf.es3n1n.io/v1alpha1 kind: Challenge metadata: name: 67-hunt namespace: 67-hunt spec: releaseTime: "2026-06-27T08:00:00Z" pods: - name: challenge replicas: 2 spec: containers: - name: challenge image: "{{ images['67-hunt'] }}" ports: - containerPort: 8080 resources: requests: { cpu: 50m, memory: 32Mi } limits: { cpu: 500m, memory: 128Mi } securityContext: runAsNonRoot: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] exposed: - protocol: HTTPS subdomain: 67-hunt targetPort: 8080| Field | Purpose |
|---|---|
clusterName / cluster_name |
Cluster from the root clusters map (or an aliasTo / alias_to) to apply against. |
paths / documents |
YAML files on disk or inline objects. Both render as Jinja templates with challenge, challenges, images, and config available. |
rolloutRestart.image / rollout_restart.image |
When true (the default), triggers a rollout restart of the matching Deployment whenever the resolved image digest changes. |
rolloutRestart.annotationPath / rollout_restart.annotation_path |
Optional JSON-path-style hook for restarting on annotation changes. |
Instanced challenges
For an instanced challenge, add instancerConfig / instancer_config. Its shared fields match the rCTF instancer config. The inner config contains Docker services or Kubernetes pods[]. Konata accepts both snake_case and camelCase keys.
challenges: - category: misc name: pwnable document fabricator author: SekaiCTF description: | yep it's another web challenge. attachments: files: - "challenge/" exclude: - "challenge/readflag.c" additional: - path: readflag.c strContent: | #include <stdio.h> int main() { printf("SEKAI{dummy_flag}"); } stripComponents: 1 flags: rctf: SEKAI{example} instancerConfig: instancer: k8s challengeIntegrationId: pwnable-doc timeoutMilliseconds: 1800000 expose: - kind: https hostPrefix: pwnable-doc containerName: app containerPort: 8080 config: pods: - name: app egress: false ports: - protocol: TCP port: 8080 spec: restartPolicy: Always terminationGracePeriodSeconds: 0 automountServiceAccountToken: false enableServiceLinks: false containers: - name: app image: "{{ images['pwnable-doc'] }}" ports: - containerPort: 8080 volumeMounts: - name: tmp mountPath: /tmp resources: requests: { cpu: 100m, memory: 128Mi } limits: { cpu: 500m, memory: 512Mi } readinessProbe: tcpSocket: { port: 8080 } initialDelaySeconds: 5 periodSeconds: 3 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] volumes: - name: tmp emptyDir: medium: Memory sizeLimit: 512Mi
deployment: images: - path: challenge name: pwnable-doc tag: latest registryName: instancer-challenges platform: linux/amd64For an instanced Kubernetes challenge, provide the pods only. The rCTF operator creates the namespace, services, network policies, and ingress when a team starts an instance.
instancerConfig.instancer names which configured rCTF instancer the challenge runs on, matching a key in rCTF’s instancers map. Omit it to fall back to rCTF’s defaultInstancer. See Instancer for the deployment-side setup.
Admin bot
For a web challenge with an rCTF admin bot, put the handler source under adminBot / admin_bot. Konata uploads it as adminBotConfig, and rCTF reads the handler’s inputs, timeout, and revision from the source.
Provide exactly one of code (inline source) or file (path relative to the challenge directory):
challenges: - category: web name: '<\w+' author: claustra01 adminBot: code: | import { Challenge } from '../src/types'
const APP_HOST = '{{"ltw." + config.domains['static']}}' const APP_URL = `https://${APP_HOST}`
export const challenge = new Challenge({ timeoutMilliseconds: 30_000, inputs: { id: { pattern: '^[0-9a-fA-F-]{36}$', }, }, browser: 'chrome', restrictDomains: { host: { allowRegex: [{ pattern: '^{{("ltw." + config.domains['static']) | re_escape}}$' }], disallowRegex: [{ pattern: '.*' }], }, }, })# Inline formadminBot: code: | // hello| Field | Purpose |
|---|---|
code |
Inline admin-bot source. Rendered as a Jinja2 template with the same challenge, config, images, and models context as descriptions and manifests. |
file |
Path to a source file, read at sync time and resolved relative to the challenge directory. Mutually exclusive with code. |
Note (re_escape filter)
Konata’s Jinja2 environment exposes a re_escape filter ({{ value | re_escape }}) for safely embedding a templated value into a regex, which is handy in admin-bot source and endpoint templates.
CLI
kona sync
sync finds the selected challenges, builds and pushes their images, applies Kubernetes manifests, and updates every configured platform.
kona sync -d ./ctf-challenges| Flag | Behavior |
|---|---|
-d, --deploy-directory |
Root of the deploy repo (the folder containing the root kona.yml). Defaults to the current directory (.). |
--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
compress creates an attachment archive that can be committed and referenced through attachments.preCompressed.
kona compress ./challenge/dist --format zip --output handout.zipkona compress ./challenge/dist --password "$FLAG" --output handout.7z| Flag | Behavior |
|---|---|
<path> (positional) |
Source file or directory. |
-f, --format |
tar_gz (default), zip, or 7z. |
-o, --output |
Output path. Defaults to <src>.{tar.gz,zip,7z} in the current directory, based on the selected or forced archive format. |
-p, --password |
Encrypts the archive. Passing a password forces 7z output, even when --format is tar_gz or zip. |
Generated archives are deterministic. Konata normalizes file metadata and entry order, so the same inputs produce the same archive bytes across machines and runs. It applies the same rules to attachments built during sync.
CI integration
The project-sekai-ctf/konata-deploy-action has two parts. detect builds a matrix of changed challenges, then sync deploys one challenge from each matrix job with kona sync --challenge-path <one>
The SekaiCTF 2026 workflow is a good reference:
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 env: USE_GKE_GCLOUD_AUTH_PLUGIN: "True" 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: project_id: sekaictf-500215 workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.SERVICE_ACCOUNT }}
- name: Activate Google Cloud SDK credentials run: | gcloud auth login --brief --cred-file="${GOOGLE_GHA_CREDS_PATH}" gcloud config set project sekaictf-500215
- 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: docker/setup-qemu-action@v3 with: platforms: arm64
- uses: project-sekai-ctf/konata-deploy-action/sync@main with: challenge-path: ${{ matrix.challenge }} env: RCTF_TOKEN: ${{ secrets.RCTF_TOKEN }} MINIONS_QUEUE_SECRET: ${{ secrets.MINIONS_QUEUE_SECRET }} MINIONS_RCTF_SECRET: ${{ secrets.MINIONS_RCTF_SECRET }} MINIONS_RCTF_API_GSA: ${{ secrets.MINIONS_RCTF_API_GSA }} MINIONS_RCTF_API_IP: ${{ secrets.MINIONS_RCTF_API_IP }} MINIONS_K8S_INSTANCER_API_URL: ${{ secrets.MINIONS_K8S_INSTANCER_API_URL }} MINIONS_K8S_INSTANCER_AUTH_TOKEN: ${{ secrets.MINIONS_K8S_INSTANCER_AUTH_TOKEN }} MINIONS_K8S_INSTANCER_CA_CERTIFICATE: ${{ secrets.MINIONS_K8S_INSTANCER_CA_CERTIFICATE }} MINIONS_GCS_BUCKET: ${{ secrets.MINIONS_GCS_BUCKET }} PPFARMING_API_IP: ${{ secrets.PPFARMING_API_IP }} PPFARMING_API_TOKEN: ${{ secrets.PPFARMING_API_TOKEN }} PPFARMING2_API_IP: ${{ secrets.PPFARMING2_API_IP }} PPFARMING2_API_TOKEN: ${{ secrets.PPFARMING2_API_TOKEN }}Notable bits:
- Workload Identity Federation is preferred over a long-lived JSON key. The
id-token: writepermission is what makesgoogle-github-actions/auth@v3work with WIF. is needed for every Artifact Registry host the matrix shard might push to.gcloud auth configure-dockerdocker/setup-qemu-actionis only needed when some challenge builds target non-native platforms such as arm64.RCTF_TOKENis the admin team token referenced by the rootkona.yaml(secrets.token.env: RCTF_TOKEN). Extra env vars map to other root-levelsecretsentries.
Real-world reference
The SekaiCTF 2026 challenges repository contains a working Konata and rCTF deployment. It includes static Kubernetes services, instanced challenges, dynamic scoring, file-backed flags, multiple registries, and CI that only deploys changed challenges.