Self-hosted service
GZTimeWalker/GZCTF avatar
GZTimeWalker/GZCTF

GZ::CTF hands out per-team containers and flags with ASP.NET Core

The GZ::CTF project, an open source CTF platform.

1,618 stars201 forksC#AGPL-3.0

At a glance

What is it?
GZ::CTF is an AGPL-3.0 CTF platform built on ASP.NET Core with a React frontend, using Docker or Kubernetes for challenge instances, Redis for cluster cache and PostgreSQL for storage. The upgrade path is one-way, and the README says so five times over.
Who is it for?
GZ::CTF is the right platform if you are running a Jeopardy-style CTF with Docker or Kubernetes available and you want dynamic scoring, per-team flags and live event monitoring without writing the orchestration yourself. It is the wrong tool for an attack-defence competition, since the model here is static and dynamic containers handed to teams rather than services teams attack.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Four challenge types, and only two of them issue per-team flags

The feature list is organised around challenges, and the challenge types are the part that determines whether the platform fits your event.

Static Attachment shares attachments across everyone, and any configured flag is accepted. Dynamic Attachment requires the number of flags and attachments to be at least the number of teams, distributing them so teams receive different ones.

Static Container shares container templates, issues no dynamic flag, and accepts any configured flag. Dynamic Container generates and issues flags automatically through container environment variables, giving each team a unique flag.

So two of the four are for teams with Docker or Kubernetes capacity and two are for events that cannot provision instances. That is the first decision: which kind of event you are running.

The README lists the challenge lifecycle controls alongside these: challenges can be disabled or enabled during a competition, and new ones released at any time, which is how a multi-phase event is run from one deployment. There is also dynamic flag sharing detection, an optional flag template, and leet flag obfuscation.

Dynamic scoring is a formula rather than a fixed table. The README gives it as `f(S, r, d, x)`, the floor of `S times [r plus (1 - r) times exp((1 - x) / d)]`, where S is the original score, r the minimum score ratio, d the difficulty coefficient and x the submission count. The first three parameters are customizable. On top of that, the first three solves receive a bonus of 5%, 3% and 1% of the current score, which is what pushes teams to attempt an unsolved challenge early rather than saving it.

Instance management is the reason to use it rather than a spreadsheet

Dynamic container distribution and management, with multiple port mapping methods on Docker or Kubernetes, is the feature that separates a platform like this from a scoreboard with a flag field.

The README also describes challenge traffic forwarding based on a TCP over WebSocket proxy, with configurable traffic capture. That solves a problem specific to containerised CTF challenges: the container is bound to a high internal port, contestants need to reach it on a known external port, and some platforms need to run behind an HTTP-only ingress. Wrapping TCP in WebSocket gets the traffic through that path.

Live monitoring during the event is real-time, not polled: competition notifications, competition events, flag submission monitoring and log monitoring based on SignalR. For a multi-day event that is the difference between an admin watching a refresh button and seeing submissions as they happen.

The supporting infrastructure is named in the same list. Cluster cache based on Redis. Database storage backend based on PostgreSQL. Storage backend based on local disk or object storage such as MinIO or S3. Support for metrics and distributed tracing, which matters when a cluster is serving several games and you need to know which node is unhealthy.

Teams get a score timeline and a scoreboard, and can be grouped into divisions. There is optional team review, invitation codes and registration email restriction, plus SMTP email verification and malicious registration protection based on Cloudflare Turnstile.

Writeup collection, review and batch download happen inside the platform, and the scoreboard can be exported along with all submission records.

The upgrade section is the most honest part of the README

Five numbered warnings, in the README's own emphasis, and they are worth quoting rather than paraphrasing.

One: to upgrade, pull the latest image and restart, and the database migration runs automatically.

Two: downgrade operations are generally not supported. Upgrading across a long time span may cause data incompatibility, so make sure to back up your data.

Three: after an upgrade there may be new configuration items and changes in file structure, so consult the documentation or the community.

Four: if you are migrating to a fork project, check whether the database structure changed, because the database after the change does not support rollback to the original version.

Five: the community and the official maintainers are not responsible for data loss or incompatibility, and fork issues go to that fork's maintainer.

Point five is the one that defines the relationship between GZ::CTF and the forks that exist in this ecosystem. A fork with schema changes is a different database, and the upstream project declines responsibility for it. That is a reasonable boundary and it is much better to read it before an event than after one.

The default branch is `1.7.0` while the current release is v1.9.0, tagged on 2026-10-03, after v1.8.7 from 2026-07-05. So branches in this project are named after release lines rather than `main` or `develop`, and picking the wrong branch is the sort of thing the upgrade warnings are about.

The last push was on 2026-10-15, the day before the current date.

AGPL-3.0 with an addendum, a notice and a proprietary components list

The licence situation is more involved than a single identifier, and the root files explain it.

The metadata reports AGPL-3.0, and the repository carries `LICENSE.txt`, `LICENSE_ADDENDUM.txt`, `NOTICE`, `TRADEMARKS.md`, `PROPRIETARY_COMPONENTS.md` and a `license/` directory.

An addendum to a copyleft licence is worth reading on its own terms, since AGPL already requires source availability for network use and an addendum can narrow or extend that. `PROPRIETARY_COMPONENTS.md` exists to record components that are not under the same terms, which is the file to check before redistributing a deployment to an organisation that has a policy review.

`TRADEMARKS.md` is separate from licensing and is the usual control on using the project name and logo. If you fork it and rebrand, that document is what tells you where the line is.

For a CTF platform this is the expected arrangement: organisers run it internally and rarely redistribute, but a school or a company that wants to run an internal competition may be hosting a modified version, which is exactly the case AGPL addresses.

Also in the root: `cliff.toml` for changelog generation from conventional commits, `renovate.json` for dependency updates, `codecov.yml` and `.deepsource.toml` for static analysis, `crowdin.yml` pointing at the Crowdin project, and `_typos.toml`.

The default language of the interface is English (en-US), with Simplified Chinese, Traditional Chinese, Japanese, Indonesian, Korean, Russian and Vietnamese fully supported by community translators. German, French and Spanish are machine and AI translated, which the README explicitly says are not perfect.

Permissions, ban and the three-level user model

Administration features are listed compactly and cover what an organiser actually needs on event day.

You can ban a specific user and there is three-level user permission management, which is enough structure for an organiser, an admin and a viewer without building a full RBAC system.

Challenges can be enabled or disabled during a competition and released at any time, so a staged release does not require a redeployment.

There is monitoring of submissions and of major event logs during the competition, which is what you consult when a team disputes a flag or a score.

Customisation is broad enough to make the deployment look like part of your event: global configuration, platform title, record information, dark mode, custom themes, and a customisable website footer, favicon and HTML description for SEO.

That last item is worth noting. Configuring SEO metadata means the platform is intended to be reachable publicly, at least in part, which raises the bar on the registration protections the README lists: SMTP email verification, Cloudflare Turnstile against malicious registration, invitation codes, and optional team review.

Editorial conclusion

GZ::CTF is the right platform if you are running a Jeopardy-style CTF with Docker or Kubernetes available and you want dynamic scoring, per-team flags and live event monitoring without writing the orchestration yourself. It is the wrong tool for an attack-defence competition, since the model here is static and dynamic containers handed to teams rather than services teams attack. Before your first upgrade, take the database backup the README insists on and read the configuration diff, because migrations run automatically and a branch-project migration cannot be rolled back to the original version.

Frequently asked questions

How do I upgrade GZ::CTF?

Pull the latest image and restart; the database migration runs automatically. The README states that downgrades are not supported and that upgrading across a long time span may cause data incompatibility; after changing branch projects, the database cannot be rolled back to the original version.

What challenge types does GZ::CTF support?

Four: Static Attachment and Dynamic Attachment for file-based challenges, and Static Container and Dynamic Container for instance-based ones. Only the dynamic variants issue per-team flags, with the container variant generating them through environment variables.

What infrastructure does GZ::CTF need?

Docker or Kubernetes for challenge instances, Redis for cluster cache, PostgreSQL as the database, and local disk or object storage such as MinIO or S3. Contestant traffic to containers is forwarded through a TCP over WebSocket proxy.

How does dynamic scoring work in GZ::CTF?

Score decays with submission count using a curve parameterised by original score, minimum score ratio and difficulty coefficient, all three customizable. The first three solves also receive a bonus of 5%, 3% and 1% of the current score.

Which languages does the GZ::CTF interface support?

English (en-US) is the default, with Simplified Chinese, Traditional Chinese, Japanese, Indonesian, Korean, Russian and Vietnamese fully translated by the community. German, French and Spanish are machine and AI translated and the README says they are not perfect.

Official sources

  1. GZTimeWalker/GZCTF on GitHub
  2. License: AGPL-3.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gztimewalker-gzctf.svg)](https://hysenlabs.com/projects/gztimewalker-gzctf)

Was this useful?