Devlog #1 | The best-laid plans of mice and men

Last time we talked a lot about the WHY. This post is all about the HOW.

Let’s get down to business.

If a task is done and no one marks it in a ticketing system, does it actually work?

Like I said last time, the first task was to start dumping tasks into a board; lo and behold. It’s still rather empty, but at least there’s something tangible to look at.

project dev board

Let’s do high-level requirements first

Here is the main “User Story”. This is the first time I’m writing it down. Super helpful to talk about this out loud and write it down, it really makes ideas more concrete. So:

Here’s how a user will start playing the CTF

  1. They open CTF main page, which explains the rules and gives them a link to ssh to.
  2. They log on to the server. behind the scenes, they get a new docker just for you using docker-tcp-switchboard.
  3. They clone the repo. behind the scenes, the local git server is already initialized with all the hooks etc. and ready with their ssh key
  4. They read the README, which directs them to checkout start-here and read it again.

Here’s the general idea for how a user plays a single level

  • They read the README to understand how to solve this level. There’s a web page link with hints, as well.
  • They solve the level. For example, create two files at the root of the repo, git add them, git commit. This is equivalent to the second level of the original CTF.
  • git push is the confirmation. That’s how they say “I’m done”. How do we check the solutions?
    • A master pre-receive hook that checks what level they’re in and runs the appropriate solution checker (different for each level). pre-receive will pass all of it’s arguments to the solution checker and the solution checker return 0 or 1 on success/failure with a message. the pre-receive normally always fail (unless we want to allow push for a specific level).
    • If the player didn’t win yet:
      • Print an appropriate error to indicate what went wrong (like “too many commits” or “I’m missing file X”)
    • If the player won:
      • Print the flag(s) 🍾🏆


In very broad strokes, it seems like the CTF will have three main “moving parts”.

  1. The repo itself. It will contain the levels’ beginning state in their respective branches. There’s an assumption here that we’re playing against a single repo, but I couldn’t find any reason to play against multiples.
  2. The level database: hooks file, solution checkers, and level structure. This will be comprised of:
    1. Data
    2. Scripts that parse that data and create the required resources, such as the main hook file, testing suite, and a level browser web page.
  3. Build system. Build will be to a docker.

OK, so now that we know more precisely how the CTF will behave, we need to start to work on ONE of the components: Let’s move to the level DB first since it seems like the real “core” of the CTF. I’ll have to program around how this DB is built.

Planning the level database

What defines “Level”


  • Title. Like “start”, “branching-1”, “merging-1”, etc. This is the human-readable version of the level, unlike…
  • Branch. This is this level’s actual branch in the repo.
  • Solution checker. This is an executable file. Most likely a .sh script, but I’d like to keep any executable as an option.
  • Flags. This is a List of branches this level unlocks.
  • Level page. This is a .md file that will be uploaded to the challenge site, which accompanies the level’s README in the repo. Mostly hints and flavour text.
  • Tests. This describes how to win this level. As much as I can, I’d like for this to be something automatic.

Given this definition of “Level”, what’s the structure of the DB

First, we’ll have one game_config.toml file that looks like this:

# Generic stuff
generic = "stuff"

# Dunno. Some server configs?
paths = "asdf"

# Here's the interesting part
  title = "start-1"
  branch = "start-here"
  solutionChecker = "checkers/"
  flags = ["fizzling-vulture-pedial"]
  # Level page is implicitly "pages/"
  # Testing info is implicitly in "tests/"

  title = "merge-1"
  branch = "fizzling-vulture-pedial"
  solutionChecker = "checkers/"
  flags = ["kneel-untinted-names", "upleaped-unprint-odorize"]
  # Level page is implicitly "pages/"
  # Testing info is implicitly in "tests/"

Level DB folder structure

/levels/checkers  # executable per level
/levels/pages  # markdown per level
/levels/tests  # per level. Will start manually

Envisioning the project structure

I think this will be the semi-final directory structure and parts of the project, following the HLD from before:


/levels  # See above.

/scripts  # This is standalone code. It might rely on the data in `levels`, but these will be separate scripts, that will be developed as independently as possible from the real data.
/scripts/generate_level  # CLI wizard to creates a new  level
/scripts/generate_graph  # Generate the level browsing page. Should be a Markdown file
/scripts/deploy_git_server  # When running inside the docker, set up the repo and the hook
/scripts/test_levels  # When running inside the docker, run all the tests

/build/package_for_docker  # Takes the levels + scripts and packages them for the docker

Back on planet earth… 🌍

I “wasted” some time on installations and reading.

Downloaded Rust and started learning it, since I think it’ll make a good fit for all the /scripts code that I need to write, and I’m growing a little tired of Python.

I also made Docker work on my WSL using this guide. I would have preferred to get WSL2 but I can’t join the Windows Insider Program on this PC, so I’ll have to wait patiently 😪

Next time

Next time I’m going to go in a totally different direction: Now that the plan feels solid and grounded, I’m going to work on getting just the first two levels done and working. This should be a pretty big task since I want a LOT of automation here, but once this infrastructure is laid down, adding more levels should be a walk in the park.