CTFd 3.8.8 on AWS User Guide
Overview
This guide covers launching and using the CTFd 3.8.8 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg security operations tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
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 image runs CTFd 3.8.8, the current security release, served by gunicorn and backed by MySQL 8.0 and Redis.
The cloudimg image adds:
- No open setup wizard. On a fresh CTFd the first visitor to
/setupbecomes 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, and nginx refuses/setupfrom then on. - An administrator password unique to your instance. First boot creates the CTFd user
adminwith 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. - 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:8000behind 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 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; its code belongs to root. CTFd, nginx, MySQL and Redis are blocked from the instance metadata service, and the AMI requires IMDSv2.
- Fully patched Ubuntu 24.04 LTS 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
- 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 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; open 443 and 80 to the players who should reach the event.
Step 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 and your Region, then Launch through EC2. Choose m5.large (or larger for big events), your key pair and a security group as 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}]'
Step 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>
Step 3: Check the services
First boot takes about half a minute: it creates the database, starts CTFd and completes the setup. Then 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, not 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)
Step 4: Get your URL and password
sudo cat /root/ctfd-credentials.txt
The file holds the address CTFd is served as (your instance's public DNS name by default), the administrator user and password, and the CTFd database login, all 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>
Step 5: Sign in from your browser
Open CTFD_URL in your browser. The certificate is self signed (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:

Step 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, 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 (it starts as My CTF) under Config, General.
Step 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). Change them under Config, Visibility, then 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, then the plus button.
- Teams: CTFd runs in users mode. To switch to teams mode, use Config, Accounts, before players register (changing the mode later resets accounts).
Step 8: Security checks
Port 80 sends browsers to HTTPS, CTFd answers over HTTPS, the setup wizard and self registration are closed, signed out visitors are sent to the login page for challenges, and nginx closes the connection for a host name 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 (read from the file, never from a command line), and also proves 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 = cloudimg
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. 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
nginx passes CTFd a fixed host name and the real client address from the connection, and CTFd trusts exactly one proxy hop for the client address and scheme only, so 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/cloudimg-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
Step 9: Your own DNS name and certificate
Point a DNS record at the instance, then set the name CTFd is served as. This mints 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 it 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
Step 10: The administrator password and email
ctfd-set-password sets a new password for admin using CTFd's own password hashing (the password is passed on standard input, never on a command line) and records it in /root/ctfd-credentials.txt. Run it without arguments for a new random password, or pass your own (24 to 64 letters and digits) on stdin. You can also change it 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 once 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).
Step 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, use Admin Panel, Config, Backup, Export (a zip of the database and uploads that CTFd can import again), or 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
Snapshotting the root volume is the simplest full backup. The image ships the CTFd release named in the listing; 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 and follow CTFd's upgrade notes.
Step 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-cloudimg.cnf |
| 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/cloudimg-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) |
Step 13: Security recommendations
- Allow SSH (port 22) from your own networks only, and open 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 your browsers trust (Step 9).
- Treat challenge files and services you host for players as untrusted: run anything 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 upgradeapplies everything else. - Export a backup before and after each event (Step 11).
Support and licensing
CTFd is open source under the Apache License 2.0 (its licence is at /usr/share/doc/ctfd/LICENSE); there is no per user or per CPU licence fee. CTFd is a trademark of its respective owner; this image is not affiliated with or endorsed by CTFd LLC. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA