A comprehensive CLI toolkit for CTF challenge development, deployment, and management.
The Challenge Toolkit streamlines the entire CTF challenge lifecycle, from bootstrapping new challenges with proper directory structures to building Docker images and generating Kubernetes deployment manifests. Built to work seamlessly with CTF Pilot's infrastructure, it enforces standardized schemas and automates repetitive tasks, letting you focus on creating great challenges instead of managing boilerplate.
| CTF Pilot Component | Supported Version |
|---|---|
| CTF Pilot's CTF Platform (CTFp) | v1.0 |
| CTF Pilot's Challenge Schema | v1.0 |
| CTF Pilot's Page Schema | v1.0 |
| CTF Pilot's CTFd Manager | v1.0 |
| kube-ctf | v1.0 |
Note
We are currently working on making it easier to use the tool.
The current tool is only provided as the raw python files.
Therefore, in order to run the tool, first clone this repository:
git clone https://github.com/ctfpilot/challenge-toolkitIn order to install required dependencies, run:
pip install -r challenge-toolkit/src/requirements.txtImportant
The tool assumes, that the current working directory is the root of a challenge repository.
Read more about the expected structure of a challenge repository in the Challenge repository structure documentation section.
You can then run the tool using python:
python challenge-toolkit/src/ctf.py <command> [arguments] [options]Important
Deployment templates are essential for a number of commands to work properly.
In order to use create, template, and page you need to copy the deployment templates into the template/ directory of your challenge repository (In accordance with the Template structure section).
This can be done by running:
cp -r challenge-toolkit/template/ .The toolkit supports the following optional environment variables:
| Variable | Description | Used By |
|---|---|---|
GITHUB_REPOSITORY |
GitHub repository in format owner/repo (e.g., ctfpilot/challenges) |
template, page |
Currently, the following dependencies are required:
- Python 3.8 or higher
pyyamlPython packagepython-slugifyPython package- Docker (for building challenge images with the
pipelinecommand)
pyyaml and python-slugify are defined in the requirements.txt file.
One way to include it into your own project is to add it as a git submodule:
git submodule add https://github.com/ctfpilot/challenge-toolkitTo then clone your own project with the submodule included, run:
git clone --recurse-submodules <your-repo-url>Or if you already have cloned your repository, run:
git submodule update --init --recursiveThe tool is typically used in three scenarios:
- Creating a new challenge using the
createcommand.
Theslugifycommand may be used to create the slug for the challenge, based on the name. - Building resources for a challenge. This includes:
- Building Docker images using the
pipelinecommand. - Rendering Kubernetes deployment files using the
templatecommand, for each type of render, in the order ofclean,k8s,configmap,handout.
- Building Docker images using the
- Rendering CTFd pages using the
pagecommand.
The toolkit can be configured, by configuring the src/library/config.py file.
This is important, if you have a custom challenge schema or page schema.
Default values:
# Path to the root of the challenge repository
CHALLENGE_REPO_ROOT = Path.cwd() # Default to the directory where the command is run from
# Challenge and Page schema URLs
CHALLENGE_SCHEMA = "https://raw.githubusercontent.com/ctfpilot/challenge-schema/refs/heads/main/schema.json"
PAGE_SCHEMA = "https://raw.githubusercontent.com/ctfpilot/page-schema/refs/heads/main/schema.json"
# Allowed values for schema fields
CHALL_TYPES = [ "static", "shared", "instanced" ]
DIFFICULTIES = [ "beginner", "easy", "easy-medium", "medium", "medium-hard", "hard", "very-hard", "insane"]
CATEGORIES = [ "web", "forensics", "rev", "crypto", "pwn", "boot2root", "osint", "misc", "blockchain", "mobile", "test" ]
INSTANCED_TYPES = [ "none", "web", "tcp" ] # "none" is the default. Defines how users interact with the challenge.
# Regex patterns for tag and flag validation
TAG_FORMAT = "^[a-zA-Z0-9-_:;? ]+$"
FLAG_FORMAT = "^(\\w{2,10}\\{[^}]*\\}|dynamic|null)$"
# Default challenge configuration values
DEFAULT = {
"enabled": False,
"name": None,
"slug": None,
"author": None,
"category": None,
"difficulty": None,
"type": None,
"tags": [],
"instanced_name": None,
"instanced_type": "none",
"instanced_subdomains": [],
"connection": None,
"flag": {"flag": "null", "case_sensitive": False},
"points": 1000,
"decay": 75,
"min_points": 100,
"description_location": "description.md",
"handout_dir": "handout"
}The toolkit provides several commands to manage CTF challenges throughout their lifecycle. All commands follow the format:
python challenge-toolkit/src/ctf.py <command> [arguments] [options]| Command | Purpose | Key Arguments |
|---|---|---|
create |
Bootstrap a new challenge | Options for name, category, difficulty, etc. |
template |
Generate K8s files, ConfigMaps, or handouts | <renderer> <challenge> |
pipeline |
Build and tag Docker images | <challenge> <registry> <image_prefix> |
page |
Generate ConfigMaps for CTFd pages | <page> |
slugify |
Convert strings to URL-safe slugs | <name> |
Bootstrap a new challenge with the proper directory structure and template files.
Usage:
Important
The challenge will be created in the current working directory, following the challenge repository structure defined in the Challenge repository structure section.
The new challenge will then be located in challenges/<category>/<slug>/.
python challenge-toolkit/src/ctf.py create [options]Options:
| Option | Description | Default |
|---|---|---|
--no-prompts |
Skip interactive prompts and use default/provided values | Interactive mode |
--name <name> |
Name of the challenge | Prompted |
--slug <slug> |
URL-safe identifier for the challenge | Prompted |
--author <author> |
Challenge author name | Prompted |
--category <category> |
Challenge category | Prompted |
--difficulty <difficulty> |
Challenge difficulty | Prompted |
--type <type> |
Challenge type: static, shared, or instanced |
Prompted |
--instanced-type <type> |
For instanced challenges: none, web, or tcp. When web or tcp is provided for a non-static challenge, deployment templates will be generated. |
none |
--flag <flag> |
Challenge flag (format: FLAG{...} or dynamic or null) |
Prompted |
--points <points> |
Initial points for the challenge | 1000 |
--min-points <points> |
Minimum points (for dynamic scoring) | 100 |
--description-location <path> |
Path to the challenge description file | description.md |
--dockerfile-location <path> |
Path to the Dockerfile (relative to challenge directory) | src/Dockerfile |
--dockerfile-context <path> |
Docker build context path | src/ |
--dockerfile-identifier <id> |
Identifier for multiple Dockerfiles | None |
--handout_location <path> |
Directory containing files to hand out to participants | handout |
Examples:
# Interactive mode (recommended for first-time users)
python challenge-toolkit/src/ctf.py create
# Non-interactive mode with all parameters
python challenge-toolkit/src/ctf.py create \
--no-prompts \
--name "SQL Injection 101" \
--slug "sql-injection-101" \
--author "John Doe" \
--category web \
--difficulty easy \
--type instanced \
--instanced-type web \
--flag "FLAG{sql_1nj3ct10n_1s_fun}" \
--points 500 \
--min-points 100Generate Kubernetes deployment files, ConfigMaps, or handout archives for challenges.
Usage:
Important
The command should be run from the root of a challenge repository, as it relies on the challenge directory structure defined in the Challenge repository structure section.
python challenge-toolkit/src/ctf.py template <renderer> <challenge> [options]Arguments:
| Argument | Description | Required |
|---|---|---|
<renderer> |
Type of rendering: k8s, configmap, clean, or handout |
Yes |
<challenge> |
Challenge path in format category/slug (e.g., web/sql-injection-101) |
Yes |
Options:
| Option | Description | Default |
|---|---|---|
--expires <seconds> |
Time in seconds until challenge instance expires | 3600 (1 hour) |
--available <seconds> |
Time in seconds until challenge becomes available | 0 (immediately) |
--repo <owner/repo> |
GitHub repository in format owner/repo |
$GITHUB_REPOSITORY env or empty (see note) |
Note
The --repo option defaults to the GITHUB_REPOSITORY environment variable. If neither is set, the command will fail. This is typically set automatically in GitHub Actions workflows.
Renderer Types:
-
k8s- Generate Kubernetes deployment YAML files for the challenge.If the challenge is of type
instanced, it will template from thetemplate/k8s.ymlfile, into thek8s/challenge/k8s.ymlfile. It will wrap the challenge template into thekube-ctfdeployment template.If the challenge is of type
sharedorstatic, it will template from thetemplate/k8s.ymlfile, into thek8s/challenge/template/k8s.ymlfile, along with a full helm chart located ink8s/challenge/.It will template the following fields:
CHALLENGE_NAME- Challenge slugCHALLENGE_CATEGORY- Challenge categoryCHALLENGE_TYPE- Challenge typeCHALLENGE_VERSION- Challenge versionCHALLENGE_EXPIRES- Expiry time in secondsCHALLENGE_AVAILABLE_AT- When the challenge becomes availableDOCKER_IMAGE- Category and slug combined to docker image. Will not follow the format produced by thepipelinecommand.
Templating is done using
{{ VARIABLE_NAME }}syntax. -
configmap- Generate helm chart containing challenge metadata and description, which produces a ConfigMap for the CTF Pilot's CTFd Manager.This will render the
challenge-configmap.ymlfrom the global template directory, into thek8s/config/templates/k8s.ymlfile, along with a full helm chart located ink8s/config/.It will template the following fields:
CHALLENGE_NAME- Challenge slugCHALLENGE_CATEGORY- Challenge categoryCHALLENGE_REPO- GitHub repository in formatowner/repo, uses the--repooption orGITHUB_REPOSITORYenv variableCHALLENGE_PATH- Challenge path in formatchallenges/<category>/<slug>CHALLENGE_TYPE- Challenge instanced typeCHALLENGE_VERSION- Challenge versionCHALLENGE_ENABLED- Whether the challenge is enabledHOST- Hostname of challenge. Will be replaced with helm template variable{{ .Values.kubectf.host }}CURRENT_DATE- Current date in%Y-%m-%d %H:%M:%Sformat
Templating is done using
{{ VARIABLE_NAME }}syntax. -
clean- Remove all generated Kubernetes files from thek8s/directory -
handout- Create a ZIP archive of files in the handout directory.
The created archive is stored in thek8s/files/directory as<category>_<slug>.zip. It will ignore the files.gitkeepand.gitignore.
Examples:
# Generate Kubernetes deployment files
python challenge-toolkit/src/ctf.py template k8s web/sql-injection-101
# Generate ConfigMap with custom expiry time (2 hours) and repo
python challenge-toolkit/src/ctf.py template configmap web/sql-injection-101 \
--expires 7200 \
--repo ctfpilot/ctf-challenges
# Create handout archive
python challenge-toolkit/src/ctf.py template handout web/sql-injection-101
# Clean generated files
python challenge-toolkit/src/ctf.py template clean web/sql-injection-101Build Docker images for challenges and tag them appropriately for container registry deployment.
Usage:
Important
The command should be run from the root of a challenge repository, as it relies on the challenge directory structure defined in the Challenge repository structure section.
python challenge-toolkit/src/ctf.py pipeline <challenge> <registry> <image_prefix> [options]Arguments:
| Argument | Description | Required |
|---|---|---|
<challenge> |
Challenge path in format category/slug (e.g., web/example) |
Yes |
<registry> |
Container registry URL (e.g., ghcr.io, docker.io) |
Yes |
<image_prefix> |
Prefix for Docker image names, such as the name of the repository | Yes |
Options:
| Option | Description | Default |
|---|---|---|
--image_suffix <suffix> |
Suffix to append to image names | None |
Behavior:
- Automatically increments the challenge version
- Builds Docker images using the Dockerfile locations specified in
challenge.yml - Tags images with both
:latestand:versiontags - Image naming:
<registry>/<prefix>-<category>-<slug>[-identifier][-suffix]
Examples:
# Build and tag Docker image
python challenge-toolkit/src/ctf.py pipeline \
web/sql-injection-101 \
ghcr.io \
ctfpilot/ctf-challenges
# Build with custom suffix (e.g., for staging)
python challenge-toolkit/src/ctf.py pipeline \
web/sql-injection-101 \
ghcr.io \
ctfpilot/ctf-challenges \
--image_suffix staging
# Result: ghcr.io/ctfpilot/ctf-challenges-web-sql-injection-101:latest
# ghcr.io/ctfpilot/ctf-challenges-web-sql-injection-101:1Generate Kubernetes ConfigMaps pages, following the CTF Pilot's Page Schema.
Usage:
Important
The command should be run from the root of a challenge repository, as it relies on the challenge directory structure defined in the Challenge repository structure section.
python challenge-toolkit/src/ctf.py page <page> [options]Arguments:
| Argument | Description | Required |
|---|---|---|
<page> |
Page path (e.g., rules, about) |
Yes |
Options:
| Option | Description | Default |
|---|---|---|
--repo <owner/repo> |
GitHub repository in format owner/repo |
$GITHUB_REPOSITORY env or empty (see note) |
Note
The --repo option defaults to the GITHUB_REPOSITORY environment variable. If neither is set, the command will fail. This is typically set automatically in GitHub Actions workflows.
Examples:
# Render a custom page
python challenge-toolkit/src/ctf.py page rules --repo ctfpilot/ctf-challenges
# Render about page
python challenge-toolkit/src/ctf.py page aboutUtility command to convert challenge names into URL-safe slugs following the toolkit's conventions.
Usage:
python challenge-toolkit/src/ctf.py slugify <name>Arguments:
| Argument | Description | Required |
|---|---|---|
<name> |
String to convert to slug | Yes |
Examples:
# Convert challenge name to slug
python challenge-toolkit/src/ctf.py slugify "SQL Injection 101"
# Output: sql-injection-101
# Convert with special characters
python challenge-toolkit/src/ctf.py slugify "Web: XSS & CSRF"
# Output: web-xss-csrfImportant
The tool works based on the Challenge repository structure defined below. Working outside a structure like this is not supported.
The tools expect a specific directory structure, where challenges are stored in a challenges directory.
Inside the challenges directory, challenges are divided into categories.
Each challenge is stored in its own directory, named identically to the challenge slug.
Besides the challenges directory, there is a template directory, which contains the base templates for kubernetes deployment files.
The structure is as follows:
.
├── challenges/
│ ├── web
│ ├── forensics
│ ├── rev
│ ├── crypto
│ ├── pwn
│ ├── boot2root
│ ├── osint
│ ├── misc
│ ├── blockchain
│ └── beginner/
│ └── challenge-1
├── pages/
│ └── page-1/
├── template/
├── challenge-toolkit/
└── <other files>pages may be split into their own repository, if desired.
Tip
Challenge source code is located in the src/ directory.
The main files are challenge.yml, description.md and README.md.
Each challenge is stored in its own directory, named identically to the challenge slug. Within the challenge directory, there are several subdirectories and files that make up the challenge.
The subdirectory structure of a challenge is as follows:
.
├── handout/
├── k8s/
├── solution/
├── src/
├── template/
├── challenge.yml
├── description.md
├── README.md
└── versionhandout/contains the files that are handed out to the user. This may be the binary that needs to be reversed, the pcap file that needs to be analyzed, etc. The files in this directory are automatically zipped and stored in thek8s/files/directory as<category>_<slug>.zip.k8s/contains the kubernetes deployment files for the challenge. This is automatically generated and used for deploying to the CTF platform. This directory should not be modified manually, but instead use thechallenge.ymlfile to specify the deployment files.solution/contains the script that is used to solve the challenge. This is filled out by the challenge creator. No further standard for the content is enforced.src/contains the source code for the challenge. It contains all the code needed for running the challenge. It may also contain any copies that needs to be handed out. Dockerfiles, python scripts, etc. lives here.template/contains the template files for the challenge. For example the kubernetes deployment files, or similar, that are rendered with the data from thechallenge.ymlfile.challenge.ymlcontains the metadata for the challenge. This must be filled out by the challenge creator. Follows a very strict structure, which can be found in the schema file provided in the file.
The file may be replaced by a JSON file, aschallenge.json.description.mdcontains the description of the challenge. This is the text that is shown to the user, when they open the challenge. It should be written in markdown.README.mdcontains the base idea and information of the challenge. May contain inspiration or other internal notes about the challenge. May also contain solution steps.versioncontains the version of the challenge. This is automatically updated by thepipelinecommand. Contains a single number, which is the version number of the challenge.
To learn more about the challenge.yml file, see the CTF Pilot's Challenge Schema.
It is very common to use Docker for challenges, as it is the core for shared and instanced challenges.
Docker images are built using the pipeline command.
They are built based on the Dockerfiles provided in the challenge.yml file.
Each Dockerfile location is relative to the individual challenge directory.
The following should be described in the challenge.yml file for dockerfiles, under the dockerfile_locations key:
location: The location of the Dockerfile relative to the challenge directory. Example:src/Dockerfile.context: The context of the Dockerfile relative to the challenge directory. Example:src/.
Context controls where Docker looks for files to include in the build process.identifier: The identifier of the Dockerfile to suffix the docker image with. Example:web,db,app,bot. The identifier is used when multiple Docker images are needed for a challenge. This may be left out, if only a single Dockerfile is described.
This format follows the CTF Pilot's Challenge Schema.
Click to expand example
An example of multiple Dockerfiles, with one for the app and one for the database:
dockerfile_locations:
- location: src/app/Dockerfile
context: src/app/
identifier: app
- location: src/db/Dockerfile
context: src/db/
identifier: dbThe folder structure for this example would be:
.
└── src/
├── app/
│ ├── Dockerfile
│ └── <All other files, for the app>
└── db/
├── Dockerfile
└── <All other files for the DB>The Docker image naming convention is described in the pipeline command section above.
Tip
The template directory contains global templates for the challenge deployment.
Default templates are provided in the challenge-toolkit/template/ directory, however they must be moved to the repository template/ directory in order to be used.
The template/ directory contains the base templates for the challenge deployment files.
These templates are used to generate the actual deployment files in the k8s/ directory, when running the template and page command.
They are also used in the initial challenge creation, when running the create command.
The following templates are required:
- ConfigMap templates:
challenge-configmap.ymlpage-configmap.yml
- Challenge deployment templates:
- Instanced web:
instanced-web-k8s.yml - Instanced TCP:
instanced-tcp-k8s.yml - Shared Web:
shared-web-k8s.yml - Shared TCP:
shared-tcp-k8s.yml
- Instanced web:
- kube-ctf deployment template:
instanced-k8s-challenge.yml
Configmap templates are used to generate ConfigMaps for challenges and pages.
Challenge deployment templates are used to generate the Kubernetes deployment files for challenges.
The kube-ctf deployment template is used to generate the deployment file for instanced challenges, when using the kube-ctf platform. Within this template, the challenge deployment template is embedded.
Note
Pages may be stored in their own repository, if desired.
Tip
Page content is located in the root of the page directory (e.g., page.html or page.md).
The main files are page.yml (or page.json) and the content file.
Each page is stored in its own directory under the pages/ directory in the repository root.
Pages are used to create custom pages in CTFd, such as rules, about pages, or other informational content.
The subdirectory structure of a page is as follows:
.
├── k8s/
├── page.html (or page.md, page.txt)
├── page.yml (or page.json)
└── versionk8s/contains the Kubernetes ConfigMap file for the page. This is automatically generated by thepagecommand and should not be modified manually.page.html(orpage.md,page.txt) contains the actual content of the page. The filename is specified inpage.ymlvia thecontentfield. The content can be in HTML or Markdown format.page.ymlcontains the metadata for the page. This must be filled out by the page creator. Follows a strict structure defined by the CTF Pilot's Page Schema.
The file may be replaced by a JSON file, aspage.json.versioncontains the version of the page. This is automatically updated by thepagecommand and contains a single number representing the version.
We welcome contributions of all kinds, from code and documentation to bug reports and feedback!
Please check the Contribution Guidelines (CONTRIBUTING.md) for detailed guidelines on how to contribute.
To maintain the ability to distribute contributions across all our licensing models, all code contributions require signing a Contributor License Agreement (CLA).
You can review the CLA here. CLA signing happens automatically when you create your first pull request.
To administrate the CLA signing process, we are using CLA assistant lite.
A copy of the CLA document is also included in this repository as CLA.md.
Signatures are stored in the cla repository.
This tool and repository is licensed under the EUPL-1.2 License.
You can find the full license in the LICENSE file.
We encourage all modifications and contributions to be shared back with the community, for example through pull requests to this repository.
We also encourage all derivative works to be publicly available under the EUPL-1.2 License.
At all times must the license terms be followed.
For information regarding how to contribute, see the contributing section above.
CTF Pilot is owned and maintained by The0Mikkel.
Required Notice: Copyright Mikkel Albrechtsen (https://themikkel.dk)
We expect all contributors to adhere to our Code of Conduct to ensure a welcoming and inclusive environment for all.