Skip to content

Latest commit

 

History

History
292 lines (218 loc) · 11.8 KB

File metadata and controls

292 lines (218 loc) · 11.8 KB

MapCTF

mapctf

Map-based CTF/cyber-range platform.

Software License Tests Release build Go Report Card

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.

Architecture

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.

Quick start with Docker

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 --build

Then get the generated admin password:

docker logs mapctf-cli-dev | grep -A1 Password

Open 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.

Running a release build

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.yaml

Edit 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.yaml

3. 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-amd64

5. 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.

Setting up a competition

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.

Starting the game

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.

Dynamic scoring

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.

Configuration

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.yaml documents every field. config-generate produces 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.

Development

# Dependencies only, services run on the host
make up-backend

# In separate terminals
make run_map
make run_api

Both 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 binaries

Most 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 -race

CI 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 install

Project structure

mapctf/
├── 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

The API service

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.

Known limitations

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-api is experimental.

Security reports are welcome — see SECURITY.md.

Additional docs

License

GPL-3.0. See LICENSE.