Skip to main content

Deploying a CTFd Site in Azure

· 7 min read
Joie Llantero

Capture-The-Flag (CTF) in Information Security is a set of challenges or scenarios where participants find the answer called "flag". For instance, one can find a flag by solving programming problems or finding them in vulnerable servers.

This article covers how we can deploy an open-source CTF platform in the cloud.

The CTF platform

There's an open-source CTF platform which you can fork/clone the code in GitHub. It's free but it has a paid version if you don't want to host the site.

Setting up the basic configuration of the site

I created a virtual machine (VM) in my Azure resource group and below are the specifications.

VM Specifications:

  • OS: Ubuntu 20.04
  • Storage: 30GB
  • 2GB RAM
  • 2 vCPU

If you intend to make your CTFd site an internal app, please make sure you set the appropriate VNET before provisioning your VM then check this section for steps on how to proceed.

After provisioning the VM, SSH into it and run the following commands. Also make sure that the ports for HTTP and HTTPS are allowed in your network security group.

# configure user for our CTFd resources
sudo adduser ctfd

# add the user to the sudo group
sudo usermod -aG sudo ctfd

# add the necessary firewalls
sudo ufw allow openssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

# update and install required software
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3-pip python3-dev build-essential libssl-dev libffi-dev python3-setuptools nginx git
pip3 install pipenv

# install CTFd
cd /var/www
sudo git clone

# switch into the ctfd user
su ctfd
sudo chown -R ctfd:www-data /var/www/CTFd
cd /var/www/CTFd

# Create a pipenv to run CTFd in
pipenv install --python 3.12
pipenv shell

Testing the site

sudo ufw allow 5000
gunicorn --bind 'CTFd:create_app()'

# try to access the website
http://<ip_address_here>:5000 or

If you encounter this error: AttributeError: module 'lib' has no attribute 'X509_V_FLAG_CB_ISSUER_CHECK'. Run sudo apt remove python3-openssl.


When you encounter an error that a current process is already running, check gunicorn process: ps ax|grep gunicorn then stop the process kill -9 932.

Further configuring the CTFd site

In this section, we will edit the systemd service unit file to install the workser service and other processes for the site. For deploying the site in single-core CPUs, we will use three workers with worker-class=gevent and set keep-alive=2.

# identify the pipenv virtual environment for use in unit file
pipenv --venv
# Create service unit configuration
sudo vim /etc/systemd/system/ctfd.service

# Copy all below (lines 5-17) and paste it in ctfd.service
Description=Gunicorn instance to serve ctfd

ExecStart=/home/ctfd/.local/share/virtualenvs/CTFd-rOJbThUf/bin/gunicorn --bind unix:app.sock --keep-alive 2 --workers 3 --worker-class gevent 'CTFd:create_app()' --access-logfile '/var/log/CTFd/CTFd/logs/access.log' --error-logfile '/var/log/CTFd/CTFd/logs/error.log'


Setting up Nginx

Create log directories

sudo mkdir -p /var/log/CTFd/CTFd/logs/
sudo chown -R ctfd:www-data /var/log/CTFd/CTFd/logs/

Create Nginx site

sudo systemctl enable ctfd
sudo systemctl start ctfd
sudo systemctl status ctfd

Create Nginx site

# let's encrypt will handle the https later
sudo vim /etc/nginx/sites-available/ctfd

# Nginx config (copy lines 6-15)
# the client_max_body_size enables file uploads over the default of 1MB
server {
listen 80;
server_name <your_domain_here> <your_ip_address_here>;
client_max_body_size 75M;
location / {
include proxy_params;
proxy_pass http://unix:/var/www/CTFd/app.sock;


# Link config file
sudo ln -s /etc/nginx/sites-available/ctfd /etc/nginx/sites-enabled

# Remove defaults
sudo rm /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default

# Test nginx configuration
sudo nginx -t

# Restart nginx if test wasw good
sudo systemctl restart nginx

You may run the following code to view the access and error logs:

  • tail /var/log/CTFd/CTFd/logs/access.log
  • tail /var/log/CTFd/CTFd/logs/error.log

Configuring the site for better security

It's important to secure our website to avoid compromising the confidentiality, integrity, and availability of our site. What's more is cloud service providers are often vulnerable by default and it's our job, as customers, to configure the cloud to fit our needs.

In this section, we discuss some configurions we can do to make our CTFd site more secure.

Adding an SSL certificate

A simple and free way to add an SSL certificate for your site is by using certbot. Visit the certbot website and follow the instructions for installation. Note that you must make sure to select the corresponding OS and web server.


If you have your own certificate, visit this page to learn the steps to store your certificate in Azure Key Vault and use it in your VM.

Changing the SSH Port

Below is the process of changing the SSH port. For instance, changing it from port 22 to port 1234. Make sure that the port you will use is available.

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config_backup
sudo vim /etc/ssh/sshd_config

# uncomment "#Port 22" and change the port number
# example:
Port 1234
# update the firewall
sudo ufw allow 1234

# delete OpenSSH
sudo ufw status numbered
sudo ufw delete <enter_here_the_number_of_OpenSSH>

In Azure, update the NSG inbound rules to allow the port you chose. In the example above, it's port 1234. Remove the SSH port 22 inbound rule as well.


Cannot SSH into the VM after changing the SSH port? You can still access the VM by going to your VM page in Azure portal and selecting the "Run Commands" option at the sidebar. For further info, check out this troubleshooting guide.

Configuring the CTFd platform as an internal site

If you don't want to expose your site to the public internet, remove the public IP and use the private IP as point of access to the site. You may also want to register a domain and configure its DNS record to point to the private IP.


If you need help with setting up DNS records, you can visit this page to learn more about setting up the A records.


  1. After removing the public IP, update the IP address in Nginx.

    # let's encrypt will handle the https later
    sudo vim /etc/nginx/sites-available/ctfd

    # Nginx config (copy lines 6-15)
    # the client_max_body_size enables file uploads over the default of 1MB
    server {
    listen 80;
    server_name <your_domain_here> <your_ip_address_here>;
    client_max_body_size 75M;
    location / {
    include proxy_params;
    proxy_pass http://unix:/var/www/CTFd/app.sock;

  2. Restart Nginx and test the site.

  3. In Azure, configure a virtual network (VNET) that is setup for internal sites or apps and use that VNET for your CTFd server.


    As of writing, Azure doesn't support changing the VNET of VMs (related resource). You may need to redeploy your VM if you want to change your VNET.

  4. Final step is to change your NSG inbound rules and set them to receive sources from your VNET.

Theme Configuration

  1. I will use the Pixio theme as an example (Theme Repository)

  2. Clone the repository in the CTFd theme folder by running the command

    git /var/www/CTFd/CTFd/themes/
  3. Login as admin and change the theme in the config page.

  4. You may also customize the theme header. For instance,

    <style id="theme-color">
    :root {--theme-color: #17377f;}
    .navbar{background-color: var(--theme-color) !important;}
    .jumbotron{background-color: var(--theme-color) !important;}
    .modal-content{background-color: #0E204B !important;}
    .challenge-button{box-shadow: 3px 3px #004CFF !important}
  5. You may also customize the footer content. For instance,

    document.querySelector("footer a").innerText = "Powered by MyOrg";
    .querySelector("footer a")
    .setAttribute("href", "");

Configuring the flicker effect of Pixio theme

I noticed that the flicker effect of this particular theme may be unpleasant for some. To remove the flicker, simply follow the steps below.

  1. cd /var/www/CTFd

  2. go to CTFd/themes/pixo/static/css

  3. do sudo vi main.min.css

  4. find @keyframes flicker

  5. delete everything in it except the 0% and 100%

  6. change the parameter of 0% and 100% to "opacity" to ".5"

    @keyframes flicker{
    0% {
    opacity: 0.5;
    100% {
    opacity: 0.5;