CTFdの初期設定まとめ

## はじめに
誕生日にCTFを開催したりTsukuCTFのインフラを担当したりしていたtask4233です。

さて、CTFのコンテストページを簡単にサーブできる[CTFd](https://github.com/CTFd/CTFd)を使ったことはあるでしょうか？
設定方法はドキュメントに書かれているのですが、未経験の人にとっては取っ掛かりづらいかもしれません。

そこで、本記事では[CTFd](https://github.com/CTFd/CTFd)のセッティング方法に関してセッティング方法を簡単に共有します。過去の開催記録は末尾にまとめてあるので、興味があれば参照してください。

今回のゴールは、

- CTFdサーバの構築
- HTTPS化
- メールサーバの構築

です。環境はDebian系のLinuxインスタンス内で実施する想定です。他のディストリビューションを利用する場合は、適宜内容を読み替えてください。実行環境は、GCPのGCEインスタンスやAWSのEC2インスタンスなど、お好みのインスタンスどうぞ。

AWSを利用して構築したい場合は、のみぞうさんの記事が参考になるかもしれません。

https://yawaraka-sec.com/building-a-ctfd-server/

## 構成
このような構成を想定しています。


![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238364/4f24033b-b818-5a66-694b-c21e29bd5e1e.png)


- 外部からの通信 ↔ Linuxインスタンス内のNginx: HTTPS
- Linuxインスタンス内のNginx ↔ Dockerコンテナ内のCTFd: HTTP

## 初期設定

まずは初期設定をしましょう。次のコマンドで設定出来ます。Dockerと基本的なツールをインストールするためのコマンドです。

```bash
#!/bin/bash

# install Docker
sudo apt-get update
sudo apt-get install -y \
    ca-certificates \
    curl \
    git \
    gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin
sudo groupadd docker
sudo gpasswd -a $USER docker
sudo systemctl restart docker
exit
```

## CTFdの導入

次は、CTFdを導入して起動していきましょう。

```
git clone https://github.com/CTFd/CTFd.git
cd CTFd
docker compose up -d
```

この状態でインスタンスに割り振られているグローバルIPアドレスの80番ポートにアクセスし、CTFdの初期設定ページが開かれることを確認してください。繋がらない場合は、インスタンスのファイアウォールによって通信が遮断されている可能性があります。設定を見直してください。

確認できたら、以下コマンドでサーバを一旦落としてHTTPS化していきます。

```bash
docker compose down
```

## HTTPS化

まずはSSL証明書を発行しましょう。Lets Encryptを利用します。

```bash
sudo apt install -y nginx certbot python3-certbot-nginx
sudo certbot --nginx
```

僕は `.dev` ドメインを使っていたので、こちらのサイトを参考にしました。

https://web.archive.org/web/20210413045452/https://dylanjones.dev/posts/devsite-nginx-domain-le/

結果、以下のような文字列が出ればOKです。

```
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://ctf.task4233.dev
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Subscribe to the EFF mailing list (email: ***@gmail.com).

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/ctf.task4233.dev/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/ctf.task4233.dev/privkey.pem
   Your certificate will expire on 2024-03-23. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again with the "certonly" option. To non-interactively
   renew *all* of your certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
```

次に、こちらのファイルをNginxで設定してHTTPS化していきましょう。

`/etc/nginx/sites-enabled/default` を作成/編集して、最終的にこんな感じの設定ファイルになるはずです。`ctf.task4233.dev` は自分のドメインに置き換えてください。

```nginx
server {
	listen 80;
	server_name ctf.task4233.dev
	return 301 https://ctf.task4233.dev$request_uri;
}

server {
 	location / {
 		# First attempt to serve request as file, then
 		# as directory, then fall back to displaying a 404.
 		# try_files $uri $uri/ =404;
		# auth_basic "Restricted";
		# auth_basic_user_file /etc/nginx/.htpasswd; # Basic認証を使いたい場合は、こちらに.htpasswdファイルのパスを設定する
		proxy_pass http://127.0.0.1:8000;
        # proxy_redirect off;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Host $server_name;
	}

    listen [::]:443 ssl http2; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/ctf.task4233.dev/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ctf.task4233.dev/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    client_max_body_size 30m;
}

server {
    if ($host = ctf.task4233.dev) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


	listen 80 default_server;
 	listen [::]:80 default_server;
 
 	server_name ctf.task4233.dev;
    return 404; # managed by Certbot
}
```

最後にNginxを再起動します。

```bash
sudo systemctl restart nginx
```

## メールサーバの設定

Postfixを使います。設定を変える時に個別に再起動すると思ったので、今回はNginx同様Dockerコンテナの外に出しています。

まずはPostfixをインストールしていきましょう。

```bash
sudo apt update
sudo apt install -y postfix
```

そして、`/etc/postfix/main.cf` にある設定ファイルを編集していきます。次のような設定になるはずです。

```tf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version

# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname

smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h

readme_directory = no

# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on
# fresh installs.
compatibility_level = 3.6

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_tls_security_level=may

smtp_tls_CApath=/etc/ssl/certs
smtp_tls_security_level=may
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = ctf.task4233.dev # ここを自分のドメインに置き換える
mydomain = ctf.task4233.dev # ここを自分のドメインに置き換える
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = $mydomain
mydestination = $myhostname, ctf.task4233.dev, localhost.localdomain, localhost # ここを自分のドメインに置き換える
# relayhost = 
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 192.168.0.0/16 # ここにDocker用のネットワークのサブネットマスクを追加する
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
```

設定を上書きしたらリロードしてください。設定ファイルの変更だけならば、こちらのコマンドによってダウンタイムなしで更新できます。

```bash
/etc/init.d/postfix reload
```

## 動作確認
ここまで終わったら、CTFdの設定をしていきましょう。

設定はポチポチするだけなので、省略します。

設定に関しては、CTFdのドキュメントが参考になるかもしれません。

https://docs.ctfd.io/tutorials/getting-started

## 余談

### ctfcliの利用

CTFdに対してぽちぽち問題を追加するのは面倒ですよね？そこで、[ctfcli](https://github.com/CTFd/ctfcli)というCTFdに問題追加をしてくれる便利ツールがあります。設定ファイルをわざわざ書くのが面倒なので、私はこちらのようなgeneratorを利用していました。forkするなりcloneするなりして、よしなに使っていただければ幸いです。

https://github.com/task4233/ctfd-config-generator

### コンテナの雑な監視スクリプト

変なログが出た時にDiscordに雑にメッセージを飛ばしてくれる君です。

```bash
#!/bin/bash

CONTAINER_NAME="hoge" # app server of CTFd
WEBHOOK_URL="https://discord.com/api/webhooks/..." # WebHook URL

TARGET_LOG=$(docker inspect ${CONTAINER_NAME} | grep LogPath | awk '{print $2}' | tr -d ',"')

_error_conditions="Exception" # フィルタしたい条件をここに書く

alert() {
	while read message
	do
		echo $message | grep -q "${_error_conditions}"
		if [ $? = "0" ]; then
			curl -H "Content-Type: application/json" -X POST -d "{\"content\":\"\`\`\`\\n$(echo $message | tr -d '\"\n')\\n\`\`\`\"}" "$WEBHOOK_URL"
		fi
	done
}

tail -n 0 --follow=name --retry $TARGET_LOG | alert
```

## おわりに

こういう知見は調べれば分かるしな、と思っていたのですが、誰かのためになるかもしれないので一応公開しました。参考になれば幸いです。

皆さんのCTF開催を楽しみにしています :+1:

## references

- [Install Docker Engine on Debian](https://docs.docker.com/engine/install/debian/)
- [Dev Site - Buying a domain name, setting up nginx, and configuring Let's Encrypt with Certbot](https://web.archive.org/web/20210413045452/https://dylanjones.dev/posts/devsite-nginx-domain-le/)
- [Getting Started - CTFd Docs](https://docs.ctfd.io/tutorials/getting-started)
- [taskctf22 運営の振り返り](https://qiita.com/task4233/items/74c6a8a3347f79fb6ba9)
- [誕生日CTFにてコードゴルフを裏開催した話](https://qiita.com/task4233/items/3460e60a65aa49de661c)
- [誕生日に開催したCTFの環境構築と振り返り](https://qiita.com/task4233/items/09f112076eb5855eeed3)