Skip to content

eHacking-WebSec/e-Hacking.de

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

107 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

e-hacking.de

This project provides an example configuration how we deploy our eHacking platform on e-hacking.de.

Layout

.
├── .env                          # committed (hostnames, ports, base paths)
├── docker-compose.yml            # committed (base — works on docker & podman)
├── compose.podman.yml            # committed (podman overlay: cap_add, DOCKER_HOST)
├── Justfile                      # committed (operator recipes)
├── bin/                          # committed (helper scripts)
│   ├── compose                   # runtime-agnostic compose wrapper
│   ├── runtime-env.sh            # detects podman/docker, emits ENV
│   ├── make-auth.sh
│   ├── make-bot-env.sh
│   ├── make-credentials.sh
│   ├── make-flags.sh
│   ├── backup.sh                 # snapshot secrets + volumes → tarball
│   ├── restore.sh                # restore a snapshot (runtime-agnostic)
│   ├── init-podman.sh            # bare-server prep for rootless podman
│   └── update.sh
├── traefik/dynamic/basicauth.yml # gitignored — bcrypt hashes
├── cloudflare.env                # gitignored — DNS-01 token
├── bot.env                       # gitignored — recruiting bot secret
├── credentials.env               # gitignored — WildFly + catcher passwords
├── modules.env                   # gitignored, optional — COMPOSE_PROFILES override
├── flags_<module>.env            # gitignored — per-module CTF flags
├── flag_xxe1.txt / xxe2.txt / xslt1.xml  # gitignored — bind-mounted into xml-sec
└── (letsencrypt is a named docker volume, not a host directory)

Secret + config files

Hostnames, ports and paths live in .env (committed). Everything else is gitignored; here's how to create each:

File Purpose How to create
cloudflare.env Cloudflare API token used by Traefik for DNS-01 ACME. echo 'CF_DNS_API_TOKEN=<token with Zone:DNS:Edit on your zone>' > cloudflare.env
traefik/dynamic/basicauth.yml Shared basicauth middleware (recruiting-instructor + the dashboard router). Hot-reloaded by Traefik's file provider — rotate without restarting. just add-basicauth-user <name> (just init calls this for the first user)
bot.env Internal secret for recruiting-bot ↔ recruiting-challenge auth. ./bin/make-bot-env.sh
credentials.env WildFly application principals (attacker, victim, admin, oemmes) and catcher access gates (/__catcher signup + /__instructor). ./bin/make-credentials.sh
flags_*.env Per-module challenge flags. Format: FLAG_<KEY>=<value> matching the ENV FLAG_* lines in each module's Dockerfile. Modules in scope: json-sec, oidc, rest-api-sec, saml, soap-sec, xml-sec, axis2-flag. crawling-maze is a separate project — its flags_crawling-maze.env is hand-managed. ./bin/make-flags.sh (reads each published image's FLAG_* defaults, swaps each _dummy marker for a random token; same script also writes flag_xslt1.xml, flag_xxe1.txt, flag_xxe2.txt which xml-sec reads as bind-mounted files)

Container runtime

The stack runs on either Podman or Docker. just and the helper scripts auto-detect via bin/runtime-env.sh, in this preference order:

  1. Rootless Podman (best — no daemon, no root)
  2. Rootful Podman (acceptable fallback)
  3. Docker (last resort)

The detection probes for podman compose (the compose-go-based wrapper). The legacy Python podman-compose is rejected because it groups every service into a single pod, which collapses Traefik's hostname-based routing.

Setup once per host:

# Rootless podman (recommended):
systemctl --user enable --now podman.socket
# Let traefik bind 80/443 without root:
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/podman-lowports.conf
sudo sysctl --system
sudo loginctl enable-linger "$USER"     # keep the user manager alive past logout

# Rootful podman (only if rootless is impractical):
sudo systemctl enable --now podman.socket

# Docker: nothing extra — used automatically if no podman socket is reachable.

Force a specific runtime/socket by exporting before just:

RUNTIME=docker just up
CONTAINER_SOCKET=/run/podman/podman.sock just up   # e.g. force rootful when both are running

Selecting modules

just up brings up every CTF module by default. To run only a subset, create a gitignored modules.env listing the profiles you want:

# modules.env — skip soap, maze, passkeys
COMPOSE_PROFILES=json,oidc,rest,saml,web,xml,rookies,recruiting,catcher

Available profiles (omit to skip):

Profile Services
json json-sec
oidc oidc, victim-bot
rest rest-api-sec, couchdb
saml saml
soap soap-sec, axis2-flag, axis2-fake
web web-sec
xml xml-sec
passkeys passkeys-app, passkeys-mongo
maze crawling-maze
rookies rookies
recruiting recruiting-challenge, recruiting-bot, recruiting-instructor
catcher catcher (also auto-starts when oidc or saml is enabled)

Infrastructure (traefik, root, watchtower) is untagged and always runs.

Preview which services would start without doing it:

./bin/compose config --services

Quickstart

# One-off: drop in cloudflare.env, then bootstrap the rest.
echo 'CF_DNS_API_TOKEN=<token>' > cloudflare.env
just init
just up

Rotate every flag + credential (new semester etc.):

just reset      # wipes managed secret + flag files, re-runs init
just up

cloudflare.env, the letsencrypt docker volume, and flags_crawling-maze.env are preserved by reset.

Day-to-day:

just update                # git pull + compose pull + up -d
just add-basicauth-user X  # append a user to traefik/dynamic/basicauth.yml
just logs                  # tail logs for all services
just logs catcher
just ps
just restart oidc

If just is not installed, the equivalent flat script works: ./bin/update.sh is the legacy one-shot of just update.

Backup & migration

Everything that makes a deployment unique lives outside git: the gitignored secret/flag files and the stateful named volumes. Two recipes bundle and restore all of it, so moving e-hacking.de to a new server is copy-one-file-and-go.

just backup     # → backups/ehacking-backup-<UTC>.tar.gz

The archive contains:

Group Contents
Files cloudflare.env, bot.env, credentials.env, flags_*.env, flag_*.{txt,xml}, traefik/dynamic/basicauth.yml, optional modules.env / auth.env / .envrc
Volumes catcher-data, recruiting-data, letsencrypt (certs — kept to dodge ACME rate limits), passkeys-instance-data, passkeys-mongo-data

crawling-maze-sessions is deliberately excluded — ephemeral per-visitor crawl state, regenerated on demand. Adjust the lists at the top of bin/backup.sh if the deployment grows new stateful volumes.

Restore on the target host (idempotent; prompts before clobbering). It lists the archives in backups/ and asks which one to use — and if the host already holds deployment data, it offers to snapshot that first before overwriting:

just restore

Volume data is streamed through a throwaway container on both ends, so a backup taken under Docker restores cleanly under rootless Podman and vice-versa — the in-container uids are reapplied via the user namespace on the target, not copied raw off the host.

Bare-server bootstrap

On a fresh server with neither podman nor docker, init-podman automates the whole "Container runtime" setup below (rootless podman, the compose provider, subuid/linger/low-ports) and then restores a backup if one is sitting in backups/:

git clone <repo> e-Hacking.de && cd e-Hacking.de
# drop your backup in: scp ehacking-backup-*.tar.gz server:e-Hacking.de/backups/
just init-podman            # interactive; may ask for sudo
just up

With no backup present it stops short and tells you what a first-time bring-up still needs (chiefly cloudflare.env, then just init).

Catcher-specific notes

The catcher service is mapped onto e-attacker.de — both the bare host and any *.e-attacker.de subdomain. DNS-01 issues a wildcard cert in one go, so no per-subdomain configuration is required. The wildcard A-record (*.e-attacker.de → server IP) must exist in Cloudflare for the wildcard cert to issue.

The legacy mendhak/http-https-echo:31 attacker container has been replaced by the catcher; the bare e-attacker.de host still serves a mendhak-compatible echo response so old attacker_url-style consumers keep working.

Operator passwords

bin/make-credentials.sh writes random passwords for oemmes, the WildFly management user, and the catcher's three access gates (CATCHER_SIGNUP_PASSWORD, CATCHER_SUPERUSER_PASSWORD, CATCHER_INSTRUCTOR_PASSWORD). Look them up in credentials.env when you need to log into /__instructor.

Student-facing CTF accounts (attacker, victim) keep intentional weak defaults so the same login forms work as in the local-dev stack. admin gets a random password (only used internally).

Migrating from a pre-catcher deployment

cd ~/e-Hacking.de
git pull                  # picks up bin/, Justfile, new compose

# (Once) migrate the old ./letsencrypt host directory into the named volume.
# Use whichever runtime your stack uses (docker or podman):
docker run --rm -v letsencrypt:/dst -v "$PWD/letsencrypt":/src \
  alpine cp -a /src/. /dst/
# podman equivalent:
# podman run --rm -v letsencrypt:/dst -v "$PWD/letsencrypt":/src \
#   alpine cp -a /src/. /dst/

# Old auth.env is unused; create the new basicauth file:
just add-basicauth-user admin

# Add a flags_oidc.env (OIDC has flag ENVs since feat-catcher-midp):
./bin/make-flags.sh

# Optional: rotate everything in one go
# just reset

just update               # pulls catcher + victim-bot, recreates stack

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors