# CTFNote: a shared board for organising a CTF team

> CTFNote gives a capture the flag team one place to track challenges, notes and results, served as three Docker containers, with an optional Discord bot that mirrors the state into channels and voices.

**TFNS/CTFNote** — CTFNote is a collaborative tool aiming to help CTF teams to organise their work.

- Repository: https://github.com/TFNS/CTFNote
- Stars: 598 · Forks: 74
- Language: TypeScript
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tfns-ctfnote

## One place for the tasks a team is chewing on

CTFNote is a collaborative tool aiming to help CTF teams organise their work, and it is best understood as a shared board rather than a tool for attacking anything. A team running a capture the flag juggles two kinds of content. There is the working content, which is a pad of notes, scripts and half finished reasoning, and there is the state of the event, which is a list of challenges with an owner, a status and eventually a flag. CTFNote covers the second category directly, with a task list where each challenge can be tracked, and it links the first category to a HedgeDoc pad, since the compose file points the pad creation URL at a HedgeDoc service and the environment offers a choice of uploading images to the filesystem rather than to imgur. The project is written in TypeScript, is licensed under GPL-3.0, and the last push was on 2026-05-09 with the repository not archived.

## Three services and one pad behind compose

The deployment is three containers on a shared network named ctfnote. The api service is built from the ./api directory and runs with a PostgreSQL connection on port 5432, the db service is built from ./db and creates a second database for HedgeDoc through the POSTGRES_MULTIPLE_DATABASES setting, and the front service is built from ./front and serves the web interface. All three are also published as pre-built images under the ghcr.io/tfns/ctfnote namespace, with the api, db and front variants tagged latest. Persistent state lives in two named volumes, ctfnote-uploads mounted at /app/uploads for the images and documents that are stored on disk, and ctfnote-db mounted at the PostgreSQL data directory. The environment block is where the behaviour is configured, with the pad URLs, the database credentials, the domain and protocol flags, a document length cap of 100000, the Discord switches and the timezone and locale.

## Building needs 3 GB of RAM, and there is a pre-built path

The installation instructions separate two routes, and the difference is memory. Building CTFNote requires at least 3 GB of RAM. If you want to host it on a server with less than 3 GB of RAM, you can use the pre-built images from the GitHub Container Registry instead of building locally. With the pre-built images you download the docker-compose.yml, for example by cloning the repository, and run:

```shell
sudo docker compose up -d --pull always
```

The pull always flag is what keeps the instance current, since the images are tagged latest and the registry is where new builds land. If you do want to build the images yourself, the command is the same with a build flag:

```shell
sudo docker compose up -d --build
```

The default configuration is described as making it easy to start a new instance, and the practical entry point is that the instance spawns a web server on port 127.0.0.1:8080. It binds to the loopback address only, so nothing is exposed until you put a reverse proxy in front, and the first account created has administrative privileges, which is the thing to get right on the very first visit.

## nginx in front, TLS decided by the environment file

The documentation assumes you want to serve CTFNote over HTTPS and says to use nginx to make it available that way. The example configuration on the host proxies to the local port and passes through the headers a modern front end needs, including the WebSocket upgrade headers, the host header, the real IP and the forwarded for header, and it sets a client_max_body_size of 5M. The TLS decision itself lives in the environment file rather than only in nginx, with CMD_PROTOCOL_USESSL and CMD_DOMAIN both commented out by default and uncommented to switch the pad onto HTTPS. The documentation shows the change as a diff, since the two lines are shipped disabled. After deploying the configuration you run certbot to get the certificate, and the linked guide is a DigitalOcean tutorial on securing nginx with Let's Encrypt on Ubuntu 20.04. Two other environment switches are worth knowing about while you are in the file, CMD_CSP_ENABLE for the content security policy and CMD_IMAGE_UPLOAD_TYPE, which chooses between filesystem storage and imgur.

## The Discord bot mirrors the state, with documented sharp edges

The optional Discord integration is the most interesting part of the deployment, and the documentation is unusually frank about its limits. Three variables switch it on, USE_DISCORD, DISCORD_BOT_TOKEN and DISCORD_SERVER_ID, with DISCORD_VOICE_CHANNELS controlling how many voice channels you get per CTF and DISCORD_BOT_NAME defaulting to CTFNote. The bot needs a Discord bot created and added to your server, with the Presence Intent, Server Members Intent and Message Content Intent granted, and the Administrator permission. The commands cover the whole lifecycle: /link with a token from your CTFNote profile links your Discord account, /create makes categories, channels and roles for a CTF, /archive converts channel messages into a stored task, /delete removes them, and /start, /stop and /solve manage the task linked to a channel, with solving allowed only once and flags that cannot be removed through Discord.

## Race conditions, channel limits and privilege escalation

Three warnings in the documentation deserve to be read before you enable the bot. First, do not use it if untrusted members can create channels or categories in your server, or can edit topics of channels, because that can cause privilege escalations within CTFNote. Second, syncing the CTFNote state to Discord is prone to race conditions, so some actions such as importing tasks are now serialised, which is how the fifty channel limit per Discord category is handled correctly, and you are told to wait for the bot to finish importing before deleting or editing a CTF, with the bot confirming through a private reply and extra loading animations appearing in the interface while it syncs. Third, Discord imposes modification timeouts, so if you rename a task or CTF several times in a row the sync cannot be performed and you have to rename the categories and channel topic manually to restore it. The scale limits are spelled out too, with the bot creating more categories when you hit the fifty channel limit, and a server channel limit of five hundred, counting categories as channels, which is your own responsibility to stay under.

## Roles decide what a player can see

The privilege model is simple and the first rule catches people out. When other players register on your CTFNote instance, they will not be able to see any CTF or tasks, because CTFNote uses different roles to restrict access per CTF. You manage the roles of other players in the Users tab of the Admin panel, and you can add a guest to a CTF by ticking their badge. The default role is Guest, meant for guests and friends helping sporadically on a CTF. There is also a secret you can generate that lets users create an account with a different privilege, found in the Registration with password menu of the Admin panel, which is the mechanism for pre-registering a team roster with a specific role. Permissions in the bot follow the same split, since /create, /archive and /delete are only available to Discord administrators, which is not the same as the CTFNote administrator rights, while /start, /stop and /solve require a linked Discord account.

## GPL-3.0, an old release line and a migration guide

The licence is GPL-3.0, which is the right thing to check if you plan to run a modified instance for a team or an event, since the copyleft terms attach to redistribution of the code rather than to the data your team stores. The project structure is a monorepo with the api, front and db directories, a docker-compose.yml for production and a docker-compose.dev.yml for development, plus a yarn lockfile and a packageManager field pinning yarn 4.1.1, and Husky for git hooks through the prepare script. The front end is a separate application in its own directory, which is why the compose file builds it as its own service. The release history is a series of major versions, v3.0.0 on 2024-04-28, v3.1.0 on 2024-07-14 and v3.2.0 on 2024-11-12, so an instance running an older major is not a trivial upgrade. That is what the MIGRATION.md file in the repository root is for, and it is the first thing to read before you plan an update.

## Conclusion

Adopt CTFNote if your team runs a capture the flag and needs a shared place for tasks, notes and flags rather than a channel full of links. Do not enable the Discord bot on a server where untrusted members can create channels or edit topics, because the documentation says that leads to privilege escalation inside CTFNote.

## FAQ

### How do I run CTFNote with Docker?

Copy .env.example to .env and fill in the values, then run sudo docker compose up -d --build to build the images, or sudo docker compose up -d --pull always to use the pre-built images from the GitHub Container Registry. Building requires at least 3 GB of RAM, and the instance serves on 127.0.0.1:8080.

### How do I put CTFNote behind HTTPS?

Run nginx as a reverse proxy to the local port and enable CMD_PROTOCOL_USESSL and CMD_DOMAIN in the .env file, then use certbot to obtain the certificate. The documentation includes an example nginx server block and links a DigitalOcean guide on securing nginx with Let's Encrypt.

### What are the limitations of the CTFNote Discord bot?

Do not use it if untrusted members can create channels or categories, or edit channel topics, because that can cause privilege escalations within CTFNote. Syncing is prone to race conditions, some actions are serialised, and repeated renames can block the sync until you rename the categories and topics manually.

### Why can other players not see my CTFs in CTFNote?

CTFNote uses different roles to restrict access to each CTF, so newly registered players see nothing until you add them. You manage their roles in the Users tab of the Admin panel, and Guest is the default role for friends helping sporadically.

### How do I upgrade an existing CTFNote instance?

The repository ships a MIGRATION.md guide for instances running an earlier version. The release line has moved through v3.0.0, v3.1.0 and v3.2.0, the last of them from 2024-11-12, so read the guide before updating rather than pulling new images.

## Sources

- [Issues](https://github.com/TFNS/CTFNote/issues)
- [License: GPL-3.0](https://github.com/TFNS/CTFNote/blob/main/LICENSE)
- [README](https://github.com/TFNS/CTFNote/blob/main/README.md)
- [Releases](https://github.com/TFNS/CTFNote/releases)
- [TFNS/CTFNote on GitHub](https://github.com/TFNS/CTFNote)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tfns-ctfnote
