Konata

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

Edit this page View Markdown

Konata deploys challenges from a repository. The kona command can package attachments, build and push images, apply Kubernetes manifests, and update rCTF or CTFd.

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:

Terminal window
pip install konata
kona --help

Konata 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.

kona.yaml
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.token

rctf

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-credentials, so the workflow needs an authenticated gcloud session and the gke-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:

misc/survey/kona.yaml
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: false

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

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: true

For 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.

kona.yaml
secrets:
minions-rctf-secret:
env: MINIONS_RCTF_SECRET
game/minions-in-16k/kona.yaml
challenges:
- overrideId: game_minions-in-16k
category: game
name: Minions in 16k
author: mixy1
scoring:
rctf:
kind: dynamic
dynamic:
secret:
secret: minions-rctf-secret

transport 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.gz

A 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-components. Defaults to 0. 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: 1337

Deployment

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.

misc/pwnable-document-fabricator/kona.yaml
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/amd64

For 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):

web/lt_w/kona.yaml
challenges:
- category: web
name: '&lt;\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 form
adminBot:
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.

Terminal window
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.

Terminal window
kona compress ./challenge/dist --format zip --output handout.zip
kona 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>. Unchanged challenges are not redeployed.

The SekaiCTF 2026 workflow is a good reference:

.github/workflows/sync.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
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: 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.
  • docker/setup-qemu-action is only needed when some challenge builds target non-native platforms such as arm64.
  • RCTF_TOKEN is the admin team token referenced by the root kona.yaml (secrets.token.env: RCTF_TOKEN). Extra env vars map to other root-level secrets entries.

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.

Type to search.