Running a CTF means coordinating organizers and challenge authors while keeping the infrastructure stable and players informed. These guides cover the work from early planning through teardown, including mistakes that tend to repeat.
Before building challenges or deploying infrastructure, decide who is responsible for each part of the event, set a schedule, and establish how participants will get information and support.
Organizational structure
A small team can run a strong event, but every important job needs a clear owner.
Organizers own the schedule, final decisions, sponsor relationships, merchandise, and prizes.
Challenge authors build challenges and playtest one another’s work.
Infrastructure leads deploy the platform and challenges, monitor them during the event, and respond to security or capacity problems. For a long event, rotate this responsibility so someone with access is always available.
Support team members answer participant tickets and bring challenge or infrastructure problems to the right person.
Tip (Team size)
Five to ten organizers can usually cover a 24 to 48 hour CTF with 20 to 30 challenges. People can hold more than one role, but plan shift coverage before assuming a small team is enough.
Timeline
Start at least two or three months before the event. One workable schedule is:
Three months out, choose the date, outline the challenges, contact sponsors, and start the CTFtime registration.
Two months out, begin writing challenges.
Two weeks out, freeze new challenge ideas and focus on playtesting, revisions, platform setup, and infrastructure testing.
One week out, finalize every challenge, open registration, announce the event, and prepare Discord for participants.
CTFtime
CTFtime is where most teams discover upcoming CTFs and track their results. Check its calendar before choosing a date, then list the event early enough for the registration to be approved.
Note (Regarding conflicts)
Avoid overlapping major events, especially qualifiers for onsite finals and competitions with large prizes.
Listing a CTF takes three steps:
Create a team. The same team can compete in and organize events.
Register the event series, such as “My CTF.” The CTFtime team reviews this manually, so submit it well before the event.
Add an edition for the specific competition, such as “My CTF 2026.”
After the event, upload the final scoreboard so teams receive CTFtime ranking points. Competitors also vote on the event’s weight, which can increase as the event establishes a history.
Logo placement on the CTF homepage and merchandise, if applicable
Shoutouts on social media and announcement channels
A challenge written by the sponsor or built around one of its tools, when that fits the event
A talk or booth at an onsite event
Communication
Discord setup
Most CTFs use Discord as the main communication channel for announcements, support, and post-event discussion.
A typical Discord server configuration for a CTF includes the following channels:
Channel
Purpose
#rules
Discord and CTF-specific rules
#announcements
Registration status, CTF start/end times, challenge updates, and feedback requests
#first-bloods
Recognition of teams who achieved the first solve on a challenge
#support
Support ticket creation for issues such as broken challenges (e.g., via Tickets v2)
#general
General discussion
#looking-for-team
Team formation for participants seeking teammates
#web/#rev/#pwn/…
Category-specific discussion channels, if desired
Keep support in one place instead of answering direct messages. A ticket channel gives the team a shared record of platform problems, broken challenges, incorrect flags, and hint requests.
Hints
Caution (No-hint policy)
Do not give private hints to individual teams. Even a well-intended nudge can look unfair in a competition.
When a participant reports a challenge problem, confirm that the live service and reference solution still work. Do not reveal anything about the intended solution.
If you need to publish a hint or clarification, follow the hint policy.
Challenge design
Guidelines for planning, authoring, and testing CTF challenges, including difficulty distribution, flag formats, and quality assurance.
Aim for two to six challenges per category and avoid leaving one category noticeably thinner than the others.
Include a real range of difficulty. One or two easy, two or three medium, and one or two hard challenges per category is a useful starting point.
Challenge authoring
Good challenges are what make a CTF worth playing. Keep these guidelines in mind during development.
General principles
Write original challenges. Reused problems are often searchable or recognizable.
Give solvers enough information to make reasoned progress. If the only way forward is to guess what the author had in mind or try arbitrary inputs, the challenge is unclear rather than difficult. An unsolved challenge is usually a reason to revisit the design.
Make sure the intended solution fits the length of the event.
Do not use fake clues or red herrings.
Give each challenge a technical idea worth learning or practicing.
Challenge components
Each challenge should include the following:
Component
Comments
Necessity
Title
A memorable name. Prefer something distinctive over keygen, pwnme, warmup, or crackme.
Required
Description
Context and necessary information for the challenge. Descriptions are often themed and playful.
Required
Hints
Usually released during the event when an unsolved challenge needs clarification. See Hint policy.
Optional, uncommon
Category
The category that best matches the main technique.
Required
Author
The challenge author. Meta challenges, such as survey or sanity checks, should be authored by “Team” or some other umbrella name.
Optional, common
Tags
Extra labels for bounties, challenge series, or secondary categories.
Optional, common
Difficulty
A rough rating. Because difficulty is subjective, many events omit it.
Optional, uncommon
Flag
The solution in the established format.
Required
Attachments
Downloadable files, packaged cleanly and given stable names instead of names such as dist(4).tar.gz.
Optional, common
Remote
Connection details for a hosted challenge. Use rCTF’s [!CONNECTION] callout.
Optional, common
Instancer
Per-team instance configuration. Keep it private during the event and publish it with the challenge afterward.
Optional, uncommon
Solution
A working solve script or writeup for playtesting, support, and post-event release.
Optional, recommended
Dockerization
Package remote challenges as containers so the same build can be tested, deployed, and reproduced after the event.
Best practices
Keep one challenge per container so each challenge stays self-contained. If a challenge requires multiple services (e.g., a web app and a database), use Docker Compose.
Reach for minimal base images like alpine or debian-slim to reduce build times and attack surface.
Build artifacts separately with multi-stage builds, compiling binaries in one stage and copying only the final artifact into the runtime image.
Run challenge services as non-root users inside the container to limit the impact of container escapes.
Use a read-only filesystem where possible by mounting the root filesystem as read-only (readOnly: true or --read-only) and using a tmpfs for writable directories.
Security defaults
When deploying challenges, apply the following security settings:
Memory and PID limits prevent resource exhaustion (e.g., fork bombs).
Network isolation uses internal networks for multi-container challenges where services should not be directly reachable.
Example Dockerfile
Dockerfile
1
FROM python:3.12-slim
2
3
RUN useradd -m ctf
4
WORKDIR /home/ctf
5
6
COPY requirements.txt .
7
RUN pip install --no-cache-dir -r requirements.txt
8
9
COPY app.py flag.txt ./
10
RUN chmod 444 flag.txt
11
12
USER ctf
13
EXPOSE 5000
14
CMD ["python", "app.py"]
Using with rCTF instancer
With the rCTF Docker instancer, describe the challenge services in instancerConfig. The instancer creates the containers and network for each team, then removes them when the instance expires.
Pre-event checklist
Before the event, verify the following for each challenge:
The challenge is solvable with the provided materials. Where it fits, ship a single solve.py that outputs the flag.
The challenge has been playtested internally. Two playtesters per challenge is the safer bet.
The flag is correct and matches the expected format.
The files are correctly packaged and downloadable. Double-check that the files are the right versions, since authors sometimes update source code without re-packaging the distribution zip.
The remote services are accessible and stable.
The Docker containers build and run correctly.
The solution or writeup is documented internally and accessible to the support team.
Recreate the service after changing configuration:
Terminal window
cd /opt/rctf &&docker compose up -d--force-recreate
Setting up the web server
rCTF listens only on the VPS loopback address. Nginx terminates TLS and forwards public traffic to it. Cloudflare can sit in front of Nginx for proxying and DDoS protection.
Configure Cloudflare DNS
Under DNS > Records, create a proxied A record for 123.123.123.123. Then set SSL/TLS encryption to Full (strict).
Create origin certificate
Under SSL/TLS > Origin Server, create a certificate for ctf.example.com. Save the certificate as /etc/ssl/cf_fullchain.pem and its private key as /etc/ssl/cf_privkey.pem.
Configure UFW firewall
Allow SSH and Cloudflare traffic, then enable UFW:
Terminal window
ufw allow 22
for cfip in `curl-sw'\n' https://www.cloudflare.com/ips-v{4,6}`; do ufw allow proto tcp from $cfip comment 'Cloudflare IP'; done
ufw enable # answer `y` when prompted regarding enabling firewall
Install and configure Nginx
Install Nginx with apt update &&apt install -y nginx, then create /etc/nginx/sites-available/rctf.conf. Update the marked hostname.
/etc/nginx/sites-available/rctf.conf
1
# Redirect HTTP -> HTTPS
2
server {
3
listen 80;
4
server_name ctf.example.com; # TODO: update me to CTF platform domain name
5
return301 https://$host$request_uri;
6
}
7
8
map $http_upgrade $connection_upgrade {
9
default upgrade;
10
'' close;
11
}
12
13
server {
14
listen 443 ssl;
15
server_name ctf.example.com; # TODO: update me to CTF platform domain name
16
ssl_certificate /etc/ssl/cf_fullchain.pem;
17
ssl_certificate_key /etc/ssl/cf_privkey.pem;
18
19
# NOTE: It is *very* important to only whitelist Cloudflare IPs for the webserver (which we'll do in the following steps),
20
# as this otherwise lets you spoof the IP.
21
set_real_ip_from 0.0.0.0/0;
22
real_ip_header CF-Connecting-IP;
23
24
location/ {
25
# Uncomment the following if you want to only allow yourself to access the platform for now.
Use the same command after later Nginx changes. rCTF should now be available at https://ctf.example.com.
Configure DNS
Create an A record pointing to 123.123.123.123, then follow the Certbot instructions for Nginx.
Install and configure Nginx
Install Nginx with apt update &&apt install -y nginx, then create /etc/nginx/sites-available/rctf.conf. Update the marked hostname and certificate paths.
/etc/nginx/sites-available/rctf.conf
1
# Redirect HTTP -> HTTPS
2
server {
3
listen 80;
4
server_name ctf.example.com; # TODO: update me to CTF platform domain name
5
return301 https://$host$request_uri;
6
}
7
8
map $http_upgrade $connection_upgrade {
9
default upgrade;
10
'' close;
11
}
12
13
server {
14
listen 443 ssl;
15
server_name ctf.example.com; # TODO: update me to CTF platform domain name
16
ssl_certificate /etc/letsencrypt/live/ctf.example.com/fullchain.pem; # TODO: update path to CTF platform domain name
17
ssl_certificate_key /etc/letsencrypt/live/ctf.example.com/privkey.pem; # TODO: update path to CTF platform domain name
18
19
location/ {
20
# Uncomment the following if you want to only allow yourself to access the platform for now.
Hosted challenges use either a shared remote, which every team connects to, or an instanced remote, which gives each team a separate environment.
Tip
Every hosted challenge should include a local setup, such as a Docker Compose file. Authors need it to reproduce deployment problems and test the reference solution against the same build.
Admin bots should run separately from challenge services. This keeps browser traffic outside the challenge network and lets bot workers scale independently.
Shared remote
In a shared remote, every team connects to the same service. Use one when solves do not alter shared state, or when each connection runs inside its own sandbox. A cryptographic oracle is a common example.
kCTF runs shared challenges on GCP with autoscaling, health checks, automatic restarts, and per-connection sandboxing. A dedicated VPS running Docker is simpler, but you must provide the isolation and resource limits yourself.
Warning (Wrap shared remotes in a jail)
Any shared remote where the intended solve gives a competitor code execution must run the vulnerable process inside a per-connection sandbox. Concretely:
pwn.red/jail runs each connection in a separate nsjail with a read-only root filesystem, no network, dropped capabilities, and CPU, memory, and time limits. It is a practical default for shared pwn and RCE services.
nsjail is the lower-level building block both kCTF and pwn.red/jail wrap. kCTF runs every challenge inside an nsjail by default. If you’re not on kCTF and not using pwn.red/jail, configure nsjail yourself.
Use a jail whenever the intended solution provides code execution. Without per-connection isolation, one solver can replace the binary, leave a backdoor in /tmp/, or stop the service for everyone.
kCTF
kCTF has its own documentation, so this section focuses on the things that aren’t immediately obvious, especially for first-time users:
Set the challenge domain with --domain-name when you run kctf cluster create. To use *.example.com, use Google nameservers for the domain. To use *.subdomain.example.com, add NS records that delegate the subdomain to Google.
By default, the created nodes are spot (preemptible) instances. This means the nodes rotate every 24 hours, with brief downtime each time. To avoid this, resize the cluster without the spot instance flag (e.g., kctf cluster resize --min-nodes 1 --max-nodes 3 --num-nodes 1 --machine-type n2-standard-4), at slightly higher cost.
If a health check fails, the challenge restarts automatically, which can look like a connectivity issue from the outside. Run kctf chal status to see what’s actually going on.
Watch your GCP project’s quotas. The dashboard shows which limits have been hit or are getting close. Deploying a lot of challenges can fail because of insufficient IP addresses or other quota constraints. Setting up kCTF early is a practical way to verify your quotas are sufficient, and it may improve your account’s standing for automatic quota increase approvals.
Some challenges deployed to kCTF need extra changes because of the security hardening. Standing up kCTF early lets you catch and fix these before the competition starts. The Kubernetes Pods documentation covers the configurable options for kCTF challenge instances (for example, additional volumes, since the file system is read-only by default).
Docker
For a smaller setup, run Docker on one or more dedicated hosts. Do not put intentionally vulnerable challenges on the server that holds the rCTF database or participant data. Challenge traffic and a container escape should not be able to take down or expose the platform.
Docker Compose is enough for a small set of shared services, with each challenge on its own port. A container alone is not a safe boundary for an RCE challenge, especially if it has extra privileges. Put the vulnerable process inside pwn.red/jail or nsjail and set storage and process limits.
Caution
Block access to cloud metadata services such as http://169.254.169.254. They can expose credentials for the cloud account.
VM
Another option is to give each challenge instance its own VM or VPS. This costs more, but a compromised challenge is less likely to affect the rest of the event through disk exhaustion, a container escape, or a host failure. Block access to the cloud metadata service on these hosts as well. Choose the level of isolation based on the risks and resource needs of each challenge.
Instanced remote
An instanced remote gives each team its own environment. Use it when a solve changes persistent state, writes files, or gives broad code execution that cannot be safely reset after every connection.
rCTF provides two instancer backends:
Docker instancer is a bundled Python FastAPI service that runs on a standalone Docker host alongside Traefik and Redis. It’s lightweight and a good fit for small-to-medium events.
Kubernetes instancer runs each team instance in its own namespace with network policies and Traefik routing. It includes a Terraform deployment for GKE and is intended for events that need several challenge nodes.
The instancer overview explains the shared instancerConfig fields, participant controls, and admin UI.
Existing systems such as Klodd can still integrate with rCTF.
Warning
Instances can be deployed before the CTF begins. Be careful when deploying challenges in advance if their names are predictable.
Admin bot
The rCTF admin bot runs trusted TypeScript handlers in Chrome or Firefox. It validates participant input, queues browser visits, and returns logs for each job. Run the worker separately from the challenge so browser capacity can scale without exposing the worker service publicly.
Keeping challenges in sync
Deploy source, attachments, images, and instancerConfig from the same revision. Updating them separately makes it easy to publish a binary that no longer matches the remote service or reference solution.
Tip (Use Konata + CI as the single source of truth)
Commit each challenge’s kona.yml beside its source and deploy it with Konata from CI. The Konata GitHub Action can build and push the image, regenerate attachments, update rCTF, and roll out the Kubernetes deployment from one commit.
Generate attachments from the challenge build and reference the output through attachments.files. Konata archives that output on each sync, while rCTF skips the upload when its contents have not changed.
During the event, the team needs to watch the platform, respond to failures, and make consistent decisions about broken challenges and hints.
Monitoring
Watch the services participants depend on, and make sure alerts reach someone who can act:
For the rCTF API, watch error rates, latency, CPU, and memory.
For PostgreSQL, watch connection usage, slow queries, and disk space. A steadily rising connection count can point to a leak.
For Redis, watch memory and connected clients. If Redis fails, leaderboard caching and rate limiting are affected.
For challenge containers, watch CPU, memory, network traffic, restarts, and disk use.
For the reverse proxy, watch request volume, 5xx responses, and latency. If Cloudflare is in front, include its traffic and DDoS dashboards.
Tip
Use an external uptime monitor such as UptimeRobot or Hetrix Tools so a total outage still produces an alert.
Staffing and shift coverage
For events longer than 12 hours, rotate shifts and keep someone with infrastructure access on call at all times. When a challenge author is offline, the support team should still have the reference solution and enough notes to investigate tickets.
Incident response
Decide who can change infrastructure, challenges, and the event schedule before an incident happens.
Challenge outages
If a challenge becomes unavailable, the response depends on the deployment method:
For Docker containers, check container status with docker ps and logs with docker logs <container>. Restart with docker compose up -d--force-recreate<service> if needed.
For instancer-managed challenges, verify the instancer service is running and reachable. Check the instancer logs for errors. If individual instances are failing, the issue may be with the challenge image itself rather than the instancer.
For static challenges (no remote), verify that challenge files are accessible through the upload provider. If using S3/GCS, check bucket permissions and CDN status.
After the fix is tested, announce that the challenge is available again and say whether its files or behavior changed.
Platform issues
If the rCTF platform itself becomes unresponsive, work through the following steps:
Check API server logs for crash traces, out-of-memory errors, or unhandled exceptions. If using Docker, run docker logs rctf-rctf-1.
Verify database connectivity by ensuring PostgreSQL is running and accepting connections. Check for connection pool exhaustion or long-running queries with docker exec -it rctf-postgres-1 psql-U rctf -c"SELECT count(*) FROM pg_stat_activity;".
Verify Redis connectivity by ensuring Redis is running. Run docker exec -it rctf-redis-1 redis-cli--pass"$RCTF_REDIS_PASSWORD" ping.
Restart the rCTF service if the issue is not immediately identifiable. Run cd /opt/rctf &&docker compose restart rctf.
Check resource usage to verify the host has sufficient CPU, memory, and disk space. High leaderboard update frequency or large team counts can increase resource usage.
If an outage materially reduces playing time, extend the event and announce the new end time clearly.
Security incidents
For security incidents:
For suspected flag sharing, preserve submission times, IP records, and the shared values before deciding on a penalty.
During a denial-of-service attack, take the affected challenge offline if necessary, preserve logs, and add rate limits or blocks before restoring it.
If a flag is leaked, rotate it in rCTF and every affected service immediately, then announce any file or service changes participants need to know about.
Challenge issues
Broken challenges
If a challenge is reported as broken, verify it first. Have the author run the intended solution against the live deployment. If it’s actually broken, work through the following steps:
Post an announcement acknowledging the issue and that a fix is in progress.
Apply the fix to the challenge.
Test the fix before redeploying.
Announce when the challenge is back online.
Warning (Mid-competition modifications)
Be very careful when modifying challenges during the competition, since changes can invalidate progress made by participants. If any team has already solved the challenge, consider it solvable and avoid further modifications unless the challenge is fundamentally broken.
Unintended solutions
Accept unintended solutions unless they rely on attacking shared infrastructure or another team. Finding a valid path the author missed is part of solving the challenge.
If a deployment mistake exposes the flag directly, fix the original only when necessary for fairness or stability. A separate patched variant can preserve the intended idea, but its release may reveal what was wrong with the first version.
Attachment updates
If challenge attachments need updates (e.g., typo fixes or missing files), update the attachment and post a clear announcement spelling out what changed. Avoid changes that would invalidate existing progress.
Hint policy
Do not give private hints. If an unsolved challenge needs help, release the same information to everyone:
Around the halfway point of the competition, ask interested participants to open support tickets describing their current progress on unsolved challenges.
The challenge author reviews the submitted progress and decides whether a hint is warranted and what to provide.
Any approved hints must go out publicly in the #announcements channel so all participants have equal access.
Caution (Post-solve hints)
Once a challenge has been solved, don’t release any more hints for it. Doing so would be unfair to teams that solved it without help.
Tip (Timing considerations)
Account for time zones when scheduling a hint. A release during one region’s night gives teams elsewhere more useful playing time with it.
Note (Beginner-friendly challenges)
Beginner challenges can allow more guidance when their purpose is teaching rather than ranking. State that policy in advance and apply it consistently.
Announcements
Keep communication with participants clear and timely throughout the competition. Always announce the following:
Competition start and end reminders
Challenge releases (if challenges are released in waves)
Challenge fixes or updates
Delays or extensions of the competition
Hint releases
Infrastructure issues and resolutions
Survey release
Writeups release
Keep announcements concise and informative. For time-related announcements, Discord’s timestamp formatting handles relative times across time zones.
After the CTF
Post-CTF tasks including uploading scoreboards to CTFtime, distributing prizes, collecting feedback, and tearing down infrastructure.
After the event, submit the scoreboard, contact prize winners, collect feedback and writeups, publish an archive, and remove infrastructure that no longer needs to run.
Uploading scoreboard to CTFtime
Submitting the final scoreboard to CTFtime is what gives participating teams their ranking points. The procedure below exports the scoreboard in the format CTFtime expects.
Update admin permissions
The CTFtime export requires leaderboardRead (4). A full admin with permissions 63 already has it. Otherwise, grant the permission as described under Administration › Permissions:
1
UPDATE users SET perms =7WHERE email ='[...]';
Retrieve authentication token
Log in to the CTF platform as the admin account and run the following snippet in the browser’s developer console:
Browser console
localStorage.getItem('token')
Copy the returned authentication token, removing the surrounding quotation marks.
Export the scoreboard
Use the following command to retrieve the scoreboard export, replacing TOKEN with the authentication token from the previous step:
Submit the command output through the CTFtime event dashboard.
Note
Point distribution can take around an hour for established events. For first-time events, points are awarded only after the one-week weight voting period ends.
Prize distribution
Contact winners through the email in the admin teams view, not through an unverified Discord account. The same view can filter teams by division for divisional prizes.
Feedback collection
A short post-event form can show what to keep and what to change. Ask about:
Category
Topics to address
Infrastructure
Platform stability, challenge availability, response times
Challenges
Difficulty balance, category distribution, quality of problem design
Organization
Communication clarity, support responsiveness, overall experience
Tip
Read the responses before planning the next edition, while the incidents and challenge discussions are still fresh.
Writeup collection
Writeups make the challenges useful after the scoreboard closes. To encourage them:
Announce a deadline for writeup submissions (typically one to two weeks after the event).
Offer incentives such as prizes for outstanding writeups or recognition in official channels.
Compile and share links to community writeups in the Discord server or on the event website.
Release official writeups for challenges that received few or no community solutions.
Archive the platform
Before decommissioning the platform, use the static export tool to preserve challenges, profiles, solves, and the final leaderboard. Host the resulting read-only site wherever you plan to keep permanent links and writeups.
Infrastructure teardown
Scale expensive infrastructure down soon after the event. If the budget allows, keep challenge services available for a few days so participants can finish writeups and verify solutions.
Cloud credits are not a reason to leave vulnerable or forgotten services running. Keep them only while someone is still monitoring them and no sensitive data or credentials remain exposed.
Warning (Teardown checklist)
Decommission the following resources so you don’t get hit with unexpected costs:
Publish the static archive (see Archiving) before deleting anything else.
Take down the CTF platform, and back up the database and configuration before deletion to simplify setup for future events.
Remove challenge infrastructure: kCTF clusters and any VPS instances used for shared remotes, plus the Docker instancer or Kubernetes instancer deployment (including the GKE cluster, Traefik, and the ACME wildcard secret).
Remove the admin bot worker(s) and any queue/scraper sidecars if you deployed an autoscaled fleet.
For cloud services, delete static IP addresses, DNS records, load balancers, and any remaining storage buckets or artifacts (including the S3/GCS upload bucket if you migrated off the local provider).
Rotate or revoke any API keys, tokens, or credentials used during the event. This covers rCTF’s tokenKey, the instancer authToken, the admin bot service token, captcha secrets, registry push credentials, and any deploy tokens, such as Konata’s RCTF_TOKEN.