Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

149 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CTFd Infrastructure

The purpose of this repository is to propose a simple setup and challenge management tool for CTFd infrastructures. It has been tested and validated on PolyCyber's CTFd infrastructures (PolyPwn2025, PolyPwn2026, as well as the internal CTFd for PolyCyber).

Available Scripts

1. CTFd Installation Script (setup.sh)

Bash script that automates the installation and configuration of a CTFd server using the Zync plugin and its dedicated instancer Galvanize.

2. Challenge Management Tool (challenges.sh)

Bash script for building, ingesting, and synchronizing CTF challenges with support for Docker containers and Docker Compose.

Prerequisites

For the CTFd installation script

  • Operating System: Tested and verified on:
    • Ubuntu Server 24
    • Ubuntu Server 25
    • Debian 12
  • Privileges: The script must be executed as root (automatically uses sudo if necessary)

For the challenge management tool

  • Docker: Installed and functional
  • curl, jq, yq: For API calls and YAML/JSON processing (automatically verified)
  • Challenge repository: Folder structure with challenge.yml files

CTFd Server Installation

  1. Clone this repository:

    git clone https://github.com/CorentinFelten/infra
    cd infra
  2. Run the installation script and follow the instructions:

    ./setup.sh --domain <your-domain.com>
  3. Go to the configured server URL

    • Configure the CTF event
    • Navigate to the admin configuration panel: Admin Panel --> Plugins --> Zync Config
    • Enter your Galvanize instancer's URL and JWT secret generated by the setup

    JWT secret: if you use the provided setup workflow, the generated JWT secret is injected automatically into the CTFd container as the ZYNC_JWT_SECRET environment variable (from deploy/.env), and the same secret is written to the Galvanize config. You therefore do not need to enter the JWT secret manually in the Zync Config panel.

Installation Script Options

Option Description Required
--domain domain/IP URL/domain of your CTFd server ✅ Yes
--working-folder DIR Working directory (default: /home/$USER) ❌ No
--theme DIR/URL Enables the use of a personalised theme ❌ No
--backup-schedule TYPE Database backup frequency (daily (default), hourly, 10min) ❌ No
--instancer-url URL Use an external Galvanize instancer instead of deploying one locally ❌ No
--no-instancer Skip Galvanize setup entirely (deploy it separately later) ❌ No
--dns-provider NAME DNS provider for wildcard TLS certs (default: cloudflare) ❌ No
--acme-email EMAIL Email address for Let's Encrypt certificates (required for HTTPS) ✅ HTTPS
--no-https Deployment without HTTPS (automatically enabled for IP addresses) ❌ No
--help Display help ❌ No

--instancer-url and --no-instancer are mutually exclusive.

Installation Examples

# Basic installation with domain (includes local Galvanize instancer by default)
./setup.sh --domain example.com --acme-email admin@example.com

# Basic installation with IP address - automatically uses the --no-https option
./setup.sh --domain 192.168.123.123

# Installation with custom directory
./setup.sh --domain example.com --working-folder /opt/ctfd

# Installation with custom theme
./setup.sh --domain example.com --theme /home/user/my-custom-theme

# Installation with custom theme downloading a theme directly from github
./setup.sh --domain example.com --theme https://github.com/user/theme.git

# Custom ACME email for Let's Encrypt
./setup.sh --domain example.com --acme-email admin@example.com

# Hourly backup
./setup.sh --domain example.com --backup-schedule hourly

# Backup every 10 minutes
./setup.sh --domain example.com --backup-schedule 10min

# Use an external Galvanize instancer
./setup.sh --domain example.com --instancer-url https://instancer.example.com

# Skip Galvanize entirely (deploy it independently later)
./setup.sh --domain example.com --no-instancer

# Display help
./setup.sh --help

Custom theme configuration

If you use the --theme option, the script will automatically mount the custom theme folder in the docker-compose.yml.

Galvanize Instancer Deployment

By default, setup.sh deploys Galvanize as part of the CTFd Docker Compose stack. Two alternatives are available:

  • --instancer-url URL — point CTFd at an already-running Galvanize instance; no local container is started.
  • --no-instancer — skip Galvanize entirely during setup. You can deploy it independently later by running the instancer service manually with its own config (see config/galvanize/config.yaml for the template).

Deployment Directory Structure

After running setup.sh, the following layout is created under <working-folder>/deploy/:

deploy/
├── docker-compose.yml          # Active compose file (copied from repo)
├── .env                        # Environment variables and generated secrets
├── .secrets                    # Plaintext copy of generated secrets (chmod 600)
├── traefik-config/             # Traefik static & dynamic configs, letsencrypt storage
├── ctfd/                       # CTFd Dockerfile and custom entrypoint
│   └── plugins/zync/           # CTFd instancer plugin clone
├── ansible-ssh/                # Ansible SSH key pair (local instancer only)
├── data/                       # Runtime data (database, uploads, galvanize)
│   ├── CTFd/
│   ├── mysql/
│   ├── redis/
│   └── galvanize/              # Galvanize config and SQLite DB (local instancer only)
└── cron_backup.log             # Backup cron job log

Challenge Management Tool

challenge.yml: the ctfcli standard, without the tool

Challenges are described with the same challenge.yml format used by CTFd's official ctfcli. Existing ctfcli-compatible challenge repositories therefore work here unchanged, and challenges authored for this tool stay portable back to ctfcli.

What this tool does not do is wrap or depend on ctfcli. Ingestion and synchronization are implemented natively in Bash against the CTFd REST API (curl + jq). This keeps the tool dependency-light (no Python/pip environment to manage) and lets a single run build the Docker images, register the challenge in CTFd, and wire up the Galvanize/Zync deployment together.

The following challenge.yml fields are honored: name, category, description, value, type (ctfcli's standard/dynamic, plus the Galvanize-specific zync extension), state, connection_info, attempts, attribution, the dynamic-scoring initial/minimum/decay values, flags, files, hints (including title, cost, and key-based prerequisite gating), tags, topics, requirements (either a bare list of prerequisites or the {prerequisites, anonymize} object form), and an extra map passed through verbatim for CTFd plugin fields.

Authentication

The first time you run an action that talks to CTFd (ingest, sync, or the default all), the tool prompts for your CTFd instance URL and an admin Access Token. Generate this token beforehand from the CTFd web UI, logged in as an administrator: Settings → Access Tokens → Generate. The account must have admin rights, since ingesting and managing challenges uses admin-only API endpoints.

The URL and token are saved to <working-folder>/.ctfd/config and reused on subsequent runs, so you are only prompted once.

Available Actions

Action Description
all Build + ingest (default)
build Build Docker images only
ingest Ingest challenges into CTFd
sync Synchronize existing challenges
status Display status and statistics
cleanup Stop compose stacks and clean up Docker images (respects --categories/--challenges filters)

Main Options

Option Description Required
--repo REPO Name of the challenge repository present in the working directory ✅ Yes
--action ACTION Action to perform (all (default), build, ingest, sync, status, cleanup) ❌ No
--working-folder DIR Working directory (default: /home/$USER) ❌ No
--config FILE Load configuration from a file ❌ No

Filtering Options

Option Description
--categories LIST List of categories to process (comma-separated)
--challenges LIST List of specific challenges to process (comma-separated)

Behavior Options

Option Description
--dry-run Simulation mode (shows actions without executing them)
--force Force operations (rebuild, overwrite)
--parallel-builds N Number of parallel builds (default: 4)

Debug Options

Option Description
--debug Enable debug output
--skip-docker-check Skip Docker daemon check
--help Display help
--version Display version information

Challenge Management Examples

# Full configuration (build + ingest)
./challenges.sh --repo challenge_repo

# Build only for certain categories
./challenges.sh --action build --repo challenge_repo --categories "web,crypto"

# Synchronization with forced update
./challenges.sh --action sync --repo challenge_repo --force

# Simulation mode to see planned actions
./challenges.sh --repo challenge_repo --dry-run

# Processing specific challenges
./challenges.sh --action build --repo challenge_repo --challenges "web-challenge-1,crypto-rsa"

# Parallel build with 8 threads
./challenges.sh --action build --repo challenge_repo --parallel-builds 8

# Display status
./challenges.sh --action status --repo challenge_repo

# Clean up Docker images
./challenges.sh --action cleanup --repo challenge_repo

Configuration File

Create a .env file with KEY=VALUE pairs:

REPO=challenge_repo
WORKING_DIR=/opt/ctf
PARALLEL_BUILDS=8
FORCE=true
DEBUG=false

Usage:

./challenges.sh --config .env

Script Functionality

CTFd Installation Script

1. System Update

  • Update system packages
  • Install dependencies

2. Docker Installation

  • Add official Docker repository
  • Install Docker CE, Docker Compose, etc.
  • Enable Docker service to start on boot
  • Configure user groups

3. Theme configuration (optional)

If the --theme flag is used:

  • Mounts the theme/custom/ folder in the CTFd container
  • Enables the use of custom themes

Challenge Management Tool

1. Dependency Check

  • Verify Docker and daemon availability
  • Check for required system tools (curl, jq, yq)

2. Challenge Discovery

  • Analyze the challenge repository structure
  • Identify Docker and static challenges

3. Docker Image Building

  • Sequential or parallel image building
  • Support for --force mode for complete rebuild
  • Error handling with detailed reports

4. Challenge Ingestion

  • Installation via the CTFd REST API into the CTFd instance
  • Challenges are installed in dependency order: requirements are topologically sorted so a prerequisite is always created before the challenge that needs it. Circular dependencies are detected and reported.
  • Atomic install with rollback: if attaching flags, files, hints, tags, topics, or requirements fails, the partially-created challenge is removed so a failed ingest leaves no half-registered challenge behind.
  • Docker Compose image tag validation before ingest, and duplicate detection (existing challenges are skipped — use sync to update them).

5. Synchronization

  • Updates existing challenges in place: the challenge is PATCHed, so its CTFd ID never changes.
  • Re-pushes owned sub-resources — flags, hints, tags, topics, and files — by clearing and recreating them from challenge.yml, so edits to any of them actually propagate.
  • Requirements are resolved in a second pass, after every challenge has been synced, so a prerequisite referenced by name resolves correctly regardless of processing order.
  • Option to backup before synchronization, and --force mode for overwriting.

6. Dependency-safe deletion

  • CTFd stores requirement prerequisites as raw challenge IDs and does not cascade-clean them: deleting and recreating a challenge would give it a new ID and silently orphan every challenge that required it.
  • To avoid this, sync never deletes-and-recreates a challenge (it patches in place), and the tool refuses to delete a challenge that other challenges list as a prerequisite unless explicitly forced.

7. Cleanup

  • Remove Docker images associated with challenges
  • Dry-run mode available

Challenge Structure

Expected Challenge Repository

challenge_repo/
├── challenges/                    # (optional, detected automatically)
│   ├── web/
│   │   ├── challenge-1/
│   │   │   ├── challenge.yml      # Challenge configuration
│   │   │   ├── Dockerfile         # Docker image (for type: zync)
│   │   │   ├── src/               # Source code
│   │   │   └── files/             # Challenge files
│   │   └── challenge-2/
│   ├── crypto/
│   └── pwn/

Format of the challenge.yml File

name: "MyChallenge"
author: Challenge_Author
category: AI

description: |-
  ## Description (French)

  Petite description en français

  ## Description (English)

  Short description in English

flags:
  - flag{flag_to_find}

tags:
  - AI
  - A:Challenge_Author

# Prerequisites: a bare list of challenge names/IDs...
requirements:
  - "Rules"
# ...or the object form, to show locked challenges as "???" instead of hiding them:
# requirements:
#   prerequisites:
#     - "Rules"
#   anonymize: true

# If files needed
files:
  - "files/hello_world.txt"

# If hints needed, choose the cost
hints:
  - Interesting hint
  - {
    cost: 10,
    content: "Interesting payed hint"
  }
  # Gated hint: stays hidden until the player unlocks the hints it requires.
  # Give the prerequisite hints a `key` and reference those keys in `requirements`.
  - key: step1
    cost: 5
    content: "First step"
  - cost: 20
    content: "Second step (only revealed after step1 is unlocked)"
    requirements:
      - step1

value: 5
type: zync                            # or type: dynamic / static

# Following options are for type: zync only. See https://github.com/28Pollux28/galvanize/blob/master/data/challenges/example/challenge.yml for up-to-date config

playbook_name: http                   # Use 'http' for web challenges, 'tcp' for TCP challenges, or 'custom_compose' for custom Docker Compose setups
deploy_parameters:
  image: nginx:alpine                 # Docker image to deploy
  unique: false                       # Set to true if there needs to be a unique instance for all players
  published_ports:                    # Ports to expose from the container (Only for 'tcp' playbooks)
    - 80                              # Port to expose
  compose_definition: |-              # Docker Compose definition (Only for 'custom_compose' playbooks)
    version: '3'
    services:
      web:
        image: nginx:alpine
        ports:
          - "80:80"
  env:                                # Environment variables passed to the container
    FLAG: "flag{flag_to_find_in_env}"
    TZ: Europe/Zurich
  resource_limits:                    # Override default resource limits (optional)
    cpus: "1"
    memory: "512M"
    pids_limit: 256

Generated Configuration

The setup script automatically generates:

  • CTFd secret key (32 characters)
  • Database password (16 characters)
  • Database root password (16 characters)
  • Galvanize JWT secret (48 characters)

All secrets are written to <deploy-dir>/.secrets (chmod 600) and to .env.

Re-running setup is safe: if secrets already exist in .env, they are preserved. Only missing secrets are generated, so running setup.sh again will not break existing containers.

License

This project is licensed under the MIT License - see the LICENSE file for details.

These scripts were initially developed for the PolyCyber team to automate the installation and management of CTFd servers. They were built to work specifically with the Galvanize instancer and Zync plugin.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages