DEV Community

Cover image for Host your CTF using CTFd!
Aditya Kumar
Aditya Kumar

Posted on

Host your CTF using CTFd!

Hosting CTF: A Comprehensive Guide

If you want to jump to the technical details, scroll to the bottom.

Backstory

I hosted and coordinated a Capture The Flag (CTF) competition for around 300 people in my workplace. This was a great learning experience for me. This article may help you decide to host one for your team yourself, and it's pretty easy, too!

Initial Planning

Budget considerations

Well, like how it always is the cheaper, the better, one thing that played a crucial role is that I work in an infrastructure team, so having access to VMs was a no-cost affair. But keeping this aside, a small 4vCPU 2GB RAM machine can handle the load pretty well and is free if you have a student pack for Azure/AWS/GCP.

Timeline development

The most time-consuming part of CTF is forming the questions. Since this was the first time many people were playing, we decided to use medium-level questions. We also included a few non-technical questions so players do not lose interest in the game and drop off.

It is a good idea to survey how many people are interested in playing the CTF. This will help you estimate the traffic on the actual day and the level of the participants, so you can frame the questions accordingly!

Having a team that will form the questions is immensely important, this is the backbone on which your CTF depends, always vet the questions, make sure it is of different categories and most importantly not easily solved by ChatGPT :P

Technical Architecture

We selected to host this on VMs in the OpenStack environment but this really shouldn’t matter much, once you provision the VM it's the same process.

The VM was hosted on the company’s direct network, which is accessible only via VPN, so that isolated the VM from actors outside the company.

Challenge Types

  • Web exploitation
  • Reverse engineering
  • Cryptography
  • Forensics / Packet Inspection
  • Miscellaneous

Challenge Design Principles

  • Realistic scenarios
  • Educational Value
  • Progressive difficulty
  • Clear, unambiguous instructions

Challenge Verification

Always check your questions, create a hidden account on the platform and test your questions from start to end, there is most likely a team that is framing all the questions make sure you test each other’s questions out so that you get the feel from the participant’s side too!

Testing

I wrote a script to test the concurrent connections and average response times and monitored the VM stats using htop

As a failover scenario, I had one more VM in a different DC and network as a spare (I only did this because I had access to free VMs) to continue the contest if something bad happened!

I was also taking exports of the current state of the CTF so that I could easily import it to the new VM and continue it after making a DNS update.

Pre-Event Communication

We used a Webex bot to send communications for example, when new questions dropped, leaderboard updates, and fun stats to keep the enthusiasm going.

We tried to be as clear as possible with the rules in the questions themselves. If a frequent question popped up, we notified everyone to clear it out!

Participant Management

We did not want users to use their IDs and passwords and did not have enough time to understand how we could integrate our company's OAuth into this, so we used the CTFd APIs to create local accounts for the users and send them via the bot to everyone.

Spin up CTFd

Reference form the CTFd Docs

I used a VM with Ubuntu OS, you can follow these steps or modify them according to the package manager your OS uses.

apt install python3-pip -y -q && apt install python3.10-venv -y && apt install python-is-python3 -y -q && apt install docker -y && apt install docker-compose -y
git clone https://github.com/CTFd/CTFd.git
cd CTFd/
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
docker-compose up
Enter fullscreen mode Exit fullscreen mode

docker ps to check if the containers are up and running!

You should now be able to access CTFd at http://localhost:8000

This will spin up the CTFd with wsgi WORKERS=1

To have the better performance it is better to have more than one worker, to set up more than one worker you also need to include SECRET_KEY in the docker-compose.yml

So bring down your setup by docker-compose down and then change the docker-compose.yml file as below

Your docker-compose file is going to look like this once you add that

services:
  ctfd:
    build: .
    user: root
    restart: always
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=<SECRET_HERE>
      - UPLOAD_FOLDER=/var/uploads
      - DATABASE_URL=mysql+pymysql://ctfd:ctfd@db/ctfd
      - REDIS_URL=redis://cache:6379
      - WORKERS=3
      - LOG_FOLDER=/var/log/CTFd
      - ACCESS_LOG=/var/log/CTFd/access.log
      - ERROR_LOG=/var/log/CTFd/error.log
      - REVERSE_PROXY=true
    volumes:
      - .data/CTFd/logs:/var/log/CTFd
      - .data/CTFd/uploads:/var/uploads
      - .:/opt/CTFd:ro
    depends_on:
      - db
    networks:
        default:
        internal:
Enter fullscreen mode Exit fullscreen mode

To enable HTTPS, you need to generate a certificate and then mount it as a volume on the nginx container

We generated the certificate and the private key and stored it on the VM as ./conf/nginx/fullchain.pem and ./conf/nginx/privkey.pem

You can either use Certbot to generate a certificate or follow your organisation's guidelines to generate the certificate and private key.

We referred to this article to set up the certificates

The nginx part of the docker-compose.yml now looks like this

  nginx:
    image: nginx:stable
    restart: always
    volumes:
      - ./conf/nginx/http.conf:/etc/nginx/nginx.conf
      - ./conf/nginx/fullchain.pem:/certificates/fullchain.pem:ro
      - ./conf/nginx/privkey.pem:/certificates/privkey.pem:ro
    ports:
      - 80:80
      - 443:443
    depends_on:
      - ctfd
Enter fullscreen mode Exit fullscreen mode

Script to load test your CTFd deployment

import requests
import time
from concurrent.futures import ThreadPoolExecutor
import logging
from datetime import datetime

def setup_logging():
    """Configure logging to track performance metrics"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(message)s',
        filename=f'load_test_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
    )

def single_request(url, session):
    """Make a single request and measure response time"""
    try:
        start_time = time.time()
        response = session.get(url)
        duration = time.time() - start_time

        return {
            'status_code': response.status_code,
            'duration': duration,
        }
    except Exception as e:
        logging.error(f"Request failed: {str(e)}")
        return None

def load_test(url, num_requests=100, max_workers=10):
    """
    Perform load testing on a website

    Parameters:
        url: The URL to test
        num_requests: Total number of requests to make
        max_workers: Maximum concurrent requests
    """
    setup_logging()
    results = []

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        with requests.Session() as session:
            futures = [
                executor.submit(single_request, url, session)
                for _ in range(num_requests)
            ]

            for future in futures:
                result = future.result()
                if result:
                    results.append(result)
                    logging.info(
                        f"Status: {result['status_code']}, "
                        f"Duration: {result['duration']:.2f}s"
                    )

    # Analyze results
    successful_requests = len([r for r in results if r['status_code'] == 200])
    avg_duration = sum(r['duration'] for r in results) / len(results)

    print(f"\nLoad Test Results:")
    print(f"Total Requests: {len(results)}")
    print(f"Successful Requests: {successful_requests}")
    print(f"Average Response Time: {avg_duration:.2f}s")

test_url = "https://yourdomain/users"  # Use your test environment
load_test(
    url=test_url,
    num_requests=3000,
    max_workers=30
)
Enter fullscreen mode Exit fullscreen mode

A snippet of the VM utilisation during the load test

VM utilisation during load test

If this guide helped you, please consider giving it a like.

Top comments (0)