Map-based CTF/cyber-range platform.
MapCTF is a map-based CTF and cyber-range platform. Players log in, pick a country on a world map, submit a flag for the challenge hosted there, and capture it for their team. It ships with a full admin console, scoreboard, activity feed, in-game chat and 20 languages.
Warning
MapCTF is under active development and has not yet reached a stable release. Read Known limitations and SECURITY.md before running a real competition.
Two Go services share one PostgreSQL database and one Redis instance:
| Service | Default port | Role |
|---|---|---|
mapctf-map |
8082 | The application. Serves the entire UI — gameboard, admin console, login — as Go html/template pages with static JS/CSS. All gameplay lives here. |
mapctf-api |
8081 | Experimental REST API, intended as the seam for a future single-page frontend. Not required to run a competition. See The API service. |
- PostgreSQL (or MySQL) holds all persistent data.
- Redis backs sessions and response caching. Both services require it.
Everything in one instance is namespaced by a single instance UUID (map.uuid). Every user-facing URL
looks like /{uuid}/..., and every database row is scoped to it. It must be set and must stay stable across
restarts.
There is no separate frontend to build or run. Earlier versions of this README described a React/Vite frontend; it does not exist in this repository.
The fastest way to get a working instance. Brings up PostgreSQL, Redis, both services, and creates the admin user for you.
cp .env.example .env
docker compose -f docker-compose-dev.yml up --buildThen get the generated admin password:
docker logs mapctf-cli-dev | grep -A1 PasswordOpen http://localhost:8082/local-dev/ and log in as admin.
local-dev is the MAP_UUID from .env.example. Browsing to http://localhost:8082/ without the UUID
returns 403 by design — only the namespaced path serves the application.
To choose your own admin password instead of a generated one, set MAPCTF_PASSWORD in .env before
starting.
Download an archive for your platform from the releases page and extract it. It contains both binaries and everything the map service loads at runtime:
mapctf-map-<os>-<arch> mapctf-api-<os>-<arch>
templates/ static/ config/ LICENSE README.md
Run the binaries from the extracted directory so the default asset paths resolve.
1. Provide PostgreSQL and Redis. Both must be reachable before the services start.
2. Generate a configuration file. This writes a complete, commented config including a freshly generated instance UUID:
./mapctf-map-linux-amd64 config-generate -o config/mapctf.yamlEdit config/mapctf.yaml for your database and Redis details. Note the map.uuid value — it is part of
every URL. Validate it at any time:
./mapctf-map-linux-amd64 config-validate -f config/mapctf.yaml3. Create the first admin user. There is no default account. Omit -p and a strong password is
generated and printed once:
./mapctf-map-linux-amd64 create-admin-user -u admin-U defaults to the map.uuid from your configuration, which is almost always what you want. Run the same
command again to reset the password of an existing user.
Database tables are created automatically on first start, and the country and team-logo seed data in
config/ is loaded then too.
4. Start the map service:
./mapctf-map-linux-amd645. Open https://your-host:9000/<your-map-uuid>/ and log in.
Important
The session cookie is always marked Secure, so logging in only works over HTTPS. For anything other
than a local trial, terminate TLS in front of MapCTF or set tls.termination with a certificate and key.
Once logged in as an admin you land on the admin console at /{uuid}/admin:
- Challenges — create categories and challenges, assign each to a country, set points and hints. Bulk import/export as JSON.
- Teams and users — create or import teams, assign logos, enable/disable accounts.
- Game controls — open registration, enable scoring, set start and end times, pause the game.
- Settings — branding, language, leaderboard size, chat limits. Import/export as JSON.
Important
A new instance accepts no flags until you set game_started, even with scoring enabled. It defaults
to off so you can load challenges and open registration without leaking points to players who are already
logged in. Turn it on in Admin → Settings when the competition begins.
Flag and hint submissions are accepted only when all of these hold:
| Condition | Setting | Default |
|---|---|---|
| The game has been started | game_started |
off — you must turn this on |
| Scoring is enabled | scoring_enabled |
off |
| The game is not paused | game_paused |
not paused |
| Now is not before the start | game_start_time |
unset, so no lower bound |
| Now is not after the end | game_end_time |
unset, so no upper bound |
Start and end times are optional; leave them unset to run the competition manually with game_started and
game_paused. All of this is enforced server-side, so the clock is not merely cosmetic.
Each challenge has an optional Bonus and Bonus Decay. The bonus starts at Bonus and loses
BonusDecay for every team that has already solved the challenge, so the first capture is worth the most
and the value settles at the challenge's base points:
awarded = Points + max(0, Bonus - BonusDecay × prior solves)
Leave Bonus at 0 (the default) for flat scoring. The gameboard shows the value a solve is worth now,
not the undecayed base.
Every setting can come from a YAML file, an environment variable, or a CLI flag. Run
mapctf-map help for the full list.
deploy/config/mapctf.example.yamldocuments every field.config-generateproduces the same thing with a UUID filled in..env(see.env.example) drives the Docker development stack only.
The values worth knowing:
| Setting | Env | Notes |
|---|---|---|
map.uuid |
MAP_UUID |
Required. Namespaces every route and all data. Keep it stable. |
db.* |
DB_* |
postgres or mysql. PostgreSQL is what the project is developed and tested against. |
redis.* |
REDIS_* |
Required by both services. |
map.staticDir / map.templatesDir |
STATIC_FILES / TEMPLATES_DIR |
Default to ./static and ./templates, matching the release layout. |
jwt.secret |
JWT_SECRET |
Only used by mapctf-api. Set a strong value if you expose it. |
# Dependencies only, services run on the host
make up-backend
# In separate terminals
make run_map
make run_apiBoth need MAP_UUID and database/Redis settings in the environment, or a config file.
make test # backend tests with coverage
make lint # golangci-lint
make build # build both binariesMost of the suite is self-contained, using in-memory SQLite and a mock Redis as test fixtures. (SQLite is
a test fixture only — it is not a supported database backend; see below.) The tests that open a real
connection, including the AutoMigrate tests covering the concurrent first-boot race between the two
services, need a real PostgreSQL and skip silently without one. To run them, point MAPCTF_PG_TEST_DSN at
a throwaway database:
cd backend
MAPCTF_PG_TEST_DSN="postgres://mapctf:mapctf@127.0.0.1:5432/mapctf-test?sslmode=disable" \
go test ./... -count=1 -raceCI runs exactly this on every push and pull request to main and develop, with a PostgreSQL service
container, and fails if those integration tests skip.
Pre-commit hooks (.pre-commit-config.yaml) run staticcheck, golangci-lint and the unit tests:
pre-commit installmapctf/
├── backend/
│ ├── cmd/
│ │ ├── api/ # REST API service
│ │ └── map/ # Map service: the application
│ │ └── templates/ # All UI: Go templates + static/ assets
│ └── pkg/
│ ├── backend/ # Database connection and migrations
│ ├── cache/ # Redis
│ ├── challenges/ # Challenges and categories
│ ├── chat/ # In-game chat
│ ├── config/ # Configuration, flags, validation
│ ├── countries/ # Map country data
│ ├── i18n/ # Embedded locale catalogues
│ ├── logs/ # Activity, scoreboard, hints, failures
│ ├── settings/ # Platform settings
│ ├── teams/ # Teams, scores, logos
│ └── users/ # Users and authentication
├── deploy/
│ ├── config/ # Example configuration
│ └── docker/ # Development Dockerfiles
├── docker-compose-dev.yml
└── .goreleaser.yml
mapctf-api is an experimental REST surface, published as the seam for a future single-page frontend. It is
not needed to run a competition — the map service is self-contained.
Today it exposes read-only teams, challenges and settings, plus admin create endpoints. It has no scoring,
registration or scoreboard surface, and no API stability promise. If you expose it, set a strong
jwt.secret.
Worth knowing before you run an event:
- HTTPS is required for login; the session cookie is always
Secure. - Rate limiting is per instance. Login and registration are limited per client IP, but the counter is in-process, so running several replicas multiplies the effective limit.
- Challenges have no file attachments — only a title, description and an optional external URL.
- Players cannot join an existing team themselves; team membership is assigned by an admin.
- Game export is not a backup. It captures configuration, not competition progress.
- PostgreSQL or MySQL only. SQLite is not a supported backend: two services share one database, and SQLite allows a single writer. It remains in use as an in-process test fixture.
mapctf-apiis experimental.
Security reports are welcome — see SECURITY.md.
- ARCHITECTURE.md — design decisions, data models, service interactions.
- CHANGELOG.md — what changed between releases.
- SECURITY.md — reporting vulnerabilities and operator guidance.
GPL-3.0. See LICENSE.
