Deployment guide

CTFd 3.8.8

Overview

CTFd is the widely used open-source Capture The Flag platform for security training, workshops and competitions. It gives you a challenge board, a live scoreboard, individual or team play, hints, file attachments, flag checking and a full administration panel for authoring challenges and running the event.

This guide covers launching and using the SecureAnyCloud CTFd 3.8.8 on Ubuntu 24.04 AMI on Amazon EC2. It is one version of the SecureAnyCloud security operations tools listing on AWS Marketplace, and you select it as the software version when you subscribe and launch. The image runs CTFd 3.8.8, the current security release, served by gunicorn and backed by MySQL 8.0 and Redis.

What you get:

  • No open setup wizard. On a fresh CTFd, the first visitor to /setup becomes the administrator. This image completes the wizard itself on your instance's first boot, over the loopback interface, before anything is reachable from the network. nginx then refuses /setup from that point on.
  • An administrator password unique to your instance. First boot creates the CTFd user admin with a generated password (CTFd stores only a hash) and writes it to /root/ctfd-credentials.txt. The database password and CTFd's Flask secret key are also generated on your instance. Nothing secret is baked into the image: it ships no MySQL data directory at all, so your instance creates its own on first boot, with no password in it until CTFd's database account gets yours.
  • HTTPS from the first boot. nginx serves CTFd on port 443 with a certificate generated on your instance. Port 80 redirects to HTTPS, and the session cookie is marked Secure.
  • A safe default for a new event. Self-registration is off, and challenges are visible to signed-in users only, until you choose otherwise in the admin panel (Step 7). The scoreboard is public.
  • Fail closed. CTFd cannot start until first boot has written its per-instance secrets, and nginx cannot start until first boot has finished, so a half-configured instance never serves the web.
  • A tight network surface. CTFd (gunicorn) listens on 127.0.0.1:8000 behind nginx. MySQL and Redis listen on local sockets only, not on any TCP port. The only open ports are 22, 80 and 443. CTFd's address comes from your configuration, never from a request, and requests for a host name that the instance does not serve are dropped.
  • Hardened defaults. CTFd runs as its own unprivileged user in a systemd sandbox that can write only its uploads and logs, and its code belongs to root. CTFd, nginx, MySQL and Redis are blocked from the instance metadata service, and the AMI requires IMDSv2.
  • A fully patched base. Ubuntu 24.04 LTS is patched at build time, with unattended security upgrades switched on.

What is on the image: CTFd 3.8.8 (the official release, checksum-verified) in a Python 3.12 virtual environment, gunicorn 23, MySQL 8.0, Redis 7, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. CTFd starts with one practice challenge, Welcome, so the board is not empty.

Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.

Prerequisites

Before you deploy, make sure you have:

  • An AWS account subscribed to the listing, an EC2 key pair, and a VPC subnet with a route to the internet (or a private route that you use for management)
  • A security group that allows TCP 22 (SSH), TCP 443 (CTFd) and TCP 80 (redirects to HTTPS). Allow SSH from your own networks only, and open ports 443 and 80 to the players who should reach the event.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick CTFd 3.8.8 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2.

Choose m5.large (or larger for big events), your key pair and a security group as described above. CTFd's database and uploads live on the root volume.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ctfd}]'
2

Connect to your instance

Version SSH user

CTFd 3.8.8 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot takes about a minute. MySQL creates this instance's data directory, then first boot creates the database, starts CTFd and completes the setup. When it finishes, all five units are active:

systemctl is-active ctfd-firstboot mysql redis-server ctfd nginx
active active active active active

CTFd (gunicorn) listens on loopback only, and nginx serves ports 80 and 443. MySQL and Redis do not appear, because they use local sockets instead of TCP:

sudo ss -tlnp | awk 'NR==1 || /gunicorn|nginx|sshd/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer LISTEN 0.0.0.0:22 users:(("sshd"),("systemd")) LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) LISTEN 127.0.0.1:8000 users:(("gunicorn"),("gunicorn"),("gunicorn")) LISTEN [::]:22 users:(("sshd"),("systemd")) LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))

The versions on the image:

grep '^__version__' /opt/CTFd/CTFd/__init__.py mysqld --version redis-server --version | cut -d' ' -f1-3 /opt/CTFd/venv/bin/gunicorn --version
__version__ = "3.8.8" /usr/sbin/mysqld Ver 8.0.46-0ubuntu0.24.04.4 for Linux on x86_64 ((Ubuntu)) Redis server v=7.0.15 gunicorn (version 23.0.0)
4

Get your URL and password

sudo cat /root/ctfd-credentials.txt

The file holds the address that CTFd is served as (your instance's public DNS name by default), the administrator user and password, and the CTFd database login. All of these are generated on this instance:

CTFD_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ CTFD_ADMIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/admin CTFD_ADMIN_USER=admin CTFD_ADMIN_PASSWORD=<24 letters and digits> CTFD_DB_NAME=ctfd CTFD_DB_USER=ctfd CTFD_DB_PASSWORD=<48 hex characters>
5

Sign in from your browser

Open CTFD_URL in your browser. The certificate is self-signed and generated on your instance, so accept the browser warning once, or install your own certificate (Step 9). Choose Login and sign in with admin and the password from Step 4.

Open Challenges to see the board. The practice challenge Welcome in the Getting Started category shows its flag in the description, so you can try a submission end to end.

6

Run your event from the admin panel

Choose Admin Panel in the top bar. Statistics summarises users, IP addresses, points and challenges. Challenges is where you create challenges, flags, hints and files. Config holds the event name, theme, start and end times, and access settings.

To add a challenge, open Challenges, choose the plus button, and pick the standard type. Fill in the name, category, description and points, then add one or more flags.

Delete or hide Welcome once you have your own challenges. Rename the event, which starts as My CTF, under Config, General.

7

Let players in

The image starts with Registration Visibility: Private (nobody can sign up) and Challenge Visibility: Private (players must sign in to see challenges). To change them, open Config, Visibility, adjust the settings, then select Update.

  • Open registration: Set Registration Visibility to Public. To admit only invited players, also set a Registration Code under Config, Registration Code, and give the code to your players.
  • Create accounts yourself instead: Leave registration private, and add players under Users with the plus button.
  • Teams: CTFd runs in users mode. To switch to teams mode, open Config, Accounts before players register. Changing the mode later resets accounts.
8

Security checks

These checks confirm the following. Port 80 sends browsers to HTTPS, and CTFd answers over HTTPS. The setup wizard and self-registration are closed. Signed-out visitors are sent to the login page for challenges. nginx closes the connection for a host name that the instance does not serve (000 means no answer).

H=$(sudo sed -n 's#^CTFD_URL=https://\([^/]*\)/.*#\1#p' /root/ctfd-credentials.txt)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'CTFd home page: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'setup wizard: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/setup"
curl -sk -o /dev/null -w 'self registration: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/register"
curl -sk -o /dev/null -w 'challenges (signed out): %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/challenges"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ CTFd home page: 200 setup wizard: 404 self registration: 404 challenges (signed out): 302 unknown host name: 000

A built-in check signs in as the administrator through nginx with the password from the credentials file. It reads the password from the file, never from a command line, and also shows that a wrong password is refused:

sudo /usr/local/sbin/ctfd-cred-roundtrip.sh
OK https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/: 80->301, /healthz 200, /setup 404, anonymous challenges + /register refused, wrong password rejected, admin signed in (Secure HttpOnly session), admin panel 200, demo challenge listed

The certificate was generated on this instance and names its own addresses:

H=$(sudo sed -n 's#^CTFD_URL=https://\([^/]*\)/.*#\1#p' /root/ctfd-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

CTFd runs unprivileged in a sandbox, and none of the four services can reach the instance metadata service:

systemctl show ctfd -p User -p ProtectSystem -p NoNewPrivileges -p IPAddressDeny for u in nginx mysql redis-server; do
systemctl show "$u" -p IPAddressDeny; done
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 User=ctfd ProtectSystem=strict NoNewPrivileges=yes IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

MySQL accepts no TCP connections and keeps no binary log. Its root account signs in through the local socket only (sudo mysql, no password), and the only application account is ctfd. The three mysql.* accounts are MySQL's own locked system accounts, which cannot sign in. Redis answers on its socket only:

sudo mysql -e "SELECT @@skip_networking AS tcp_disabled, @@log_bin AS binary_log, @@local_infile AS local_infile; SELECT user, host, plugin FROM mysql.user ORDER BY user;"
sudo redis-cli -s /run/redis/redis-server.sock ping
tcp_disabled binary_log local_infile 1 0 0 user host plugin ctfd localhost caching_sha2_password mysql.infoschema localhost caching_sha2_password mysql.session localhost caching_sha2_password mysql.sys localhost caching_sha2_password root localhost auth_socket PONG

The image ships an empty MySQL data directory. Before MySQL first starts, /usr/local/sbin/sac-mysql-datadir-init creates this instance's own directory, with root on socket authentication from the start and no password anywhere. MySQL then generates its own server ID and keys. The marker that the script leaves shows when this happened on your instance:

cat /var/lib/sac/data/mysql-datadir-initialised
MySQL datadir initialised on this instance at 2026-10-20T09:36:11Z (the image ships none)

nginx passes CTFd a fixed host name and the real client address from the connection. CTFd trusts exactly one proxy hop, for the client address and scheme only. As a result, a client cannot make CTFd build links (such as password reset links) for another host or log a forged address:

grep -E 'proxy_set_header (Host|X-Forwarded-For|X-Real-IP)|location \^~ /setup' /etc/nginx/sites-available/sac-ctfd
sudo grep '^REVERSE_PROXY=' /etc/ctfd/ctfd.env
location ^~ /setup { return 404; } proxy_set_header Host $ctfd_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; REVERSE_PROXY=1,1,0,0,0
9

Your own DNS name and certificate

Point a DNS record at the instance, then set the name that CTFd is served as. This creates a new self-signed certificate for the name, updates nginx and the URLs in /root/ctfd-credentials.txt, and keeps the name across reboots:

sudo ctfd-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx. Running ctfd-set-hostname again replaces your certificate with a self-signed one.

sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/ctfd.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/ctfd.key
sudo nginx -t && sudo systemctl reload nginx
10

The administrator password and email

ctfd-set-password sets a new password for admin using CTFd's own password hashing, and records it in /root/ctfd-credentials.txt. The password is passed on standard input, never on a command line.

Run the command without arguments for a new random password, or pass your own (24 to 64 letters and digits) on stdin. You can also change the password in the browser under Settings.

sudo ctfd-set-password printf '%s\n' '<your-new-password>' | sudo ctfd-set-password --stdin

CTFd sends password reset and confirmation emails only after you configure a mail server under Admin Panel, Config, Email (SMTP or Mailgun). Until then, Forgot your password? cannot send anything, and only the administrator can reset players' passwords (under Users).

11

Your data and backups

CTFd keeps its event data (users, challenges, flags, submissions and settings) in the MySQL database ctfd, and uploaded challenge files under /var/lib/ctfd/uploads:

sudo ls /var/lib/ctfd /var/log/ctfd
/var/lib/ctfd: uploads /var/log/ctfd: logins.log registrations.log submissions.log

To back up, open Admin Panel, Config, Backup, then choose Export. The export is a zip of the database and uploads that CTFd can import again. Alternatively, dump the database and copy the uploads:

sudo mysqldump --single-transaction ctfd > ctfd-$(date +%F).sql
sudo tar -czf ctfd-uploads-$(date +%F).tar.gz -C /var/lib/ctfd uploads

A snapshot of the root volume is the simplest full backup. The image ships the CTFd release named in the listing, and Ubuntu's unattended upgrades keep the operating system, MySQL, Redis and nginx patched. To move to a newer CTFd release later, take a snapshot and an export first, then follow CTFd's upgrade notes.

12

Server components

Component Path

CTFd application (read only to the service) /opt/CTFd (virtual environment in /opt/CTFd/venv)

Per instance configuration (secret key, database URL) /etc/ctfd/ctfd.env (mode 0640, root:ctfd)

gunicorn settings /etc/ctfd/gunicorn.conf.py

Uploaded files /var/lib/ctfd/uploads

CTFd logs /var/log/ctfd (requests: journalctl -u ctfd)

MySQL settings /etc/mysql/mysql.conf.d/zz-sac.cnf

MySQL data directory /var/lib/mysql (created on first boot by /usr/local/sbin/sac-mysql-datadir-init)

Redis socket /run/redis/redis-server.sock

Service units /etc/systemd/system/ctfd.service, /etc/systemd/system/ctfd-firstboot.service

nginx site /etc/nginx/sites-available/sac-ctfd

Certificate and key /etc/nginx/ssl/

Host name and password helpers /usr/local/sbin/ctfd-set-hostname, /usr/local/sbin/ctfd-set-password

Credentials /root/ctfd-credentials.txt (mode 0600)

13

Security recommendations

  • Allow SSH (port 22) from your own networks only, and open ports 80 and 443 only to the people who should reach the event.
  • Keep registration private, or use a registration code, until the event opens (Step 7).
  • Keep the administrator password secret, and rotate it if it may have been shared (Step 10).
  • Use your own DNS name and a certificate that your browsers trust (Step 9).
  • Treat challenge files and services that you host for players as untrusted. Run anything that players can attack on separate instances, not on the CTFd server.
  • Keep the operating system patched. Unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.
  • Export a backup before and after each event (Step 11).

Support and licensing

CTFd is open-source software under the Apache License 2.0 (its licence is at /usr/share/doc/ctfd/LICENSE), and there is no per-user or per-CPU licence fee. CTFd is a trademark of its respective owner, and this image is not affiliated with or endorsed by CTFd LLC. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24-hour response SLA

Deploy on AWS

Launch CTFd 3.8.8 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS