The REF framework intends to provide students with an interactive, practical learning environment: For pre-defined tasks, each student can work in an individual Docker container with automated goal verification aiding their progress. The framework consists of multiple components that have to be built the first time REF is set up and when it is updated. The framework heavily relies on Docker for the runtime environment itself as well as for deploying the different exercises to the students.
The following describes how to build REF, how to run it, and how to upgrade it. To learn more about creating new exercises, head to exercises.md.
All configuration lives in settings.yaml, the single source of truth. On first use, ./ctrl.sh build auto-runs ./prepare.py which generates settings.yaml with cryptographically random secrets and renders two downstream artifacts from it:
settings.env— environment variables consumed by docker-compose.docker-compose.yml— rendered fromdocker-compose.template.yml.
To inspect or edit settings.yaml before the first build, run ./prepare.py manually. After editing an existing settings.yaml, re-run ./prepare.py to propagate changes to the downstream files. The script backfills new fields and prunes obsolete ones automatically, so it is safe to re-run on upgrades. Use ./prepare.py --fresh to regenerate everything from scratch (the old settings.yaml is backed up first).
| Section | Key | Default | Description |
|---|---|---|---|
ports |
ssh_host_port |
2222 | SSH reverse-proxy listen port |
http_host_port |
8080 | HTTP port (plain or redirect) | |
https_host_port |
8443 | HTTPS port | |
tls |
mode |
off |
off (plain HTTP), internal (self-signed), or acme (Let's Encrypt) |
domain |
— | Required for internal and acme modes |
|
redirect_http_to_https |
false |
Redirect HTTP port to HTTPS (internal and acme modes only) |
|
paths |
data |
./data |
Persistent data directory on the host |
exercises |
./exercises |
Exercise definitions directory | |
runtime |
binfmt_support |
false |
Enable multi-architecture container support |
admin |
password |
(random) | Admin password (username is 0); printed on first run |
ssh_key |
— | Optional SSH public key for the admin account |
Secrets (secrets section) are auto-generated and should not normally be edited. They include the Flask session key, the HMAC key shared between the SSH proxy and the web API, and the PostgreSQL password.
The tls.mode setting controls how the frontend-proxy serves traffic:
| Mode | Ports | Behavior |
|---|---|---|
off |
http_host_port only |
Plain HTTP on a single port. No TLS. |
internal |
http_host_port + https_host_port |
Self-signed TLS certificate (generated by Caddy). HTTPS on https_host_port, plain HTTP on http_host_port. Both serve the full site independently by default. Set redirect_http_to_https: true to redirect HTTP to HTTPS instead. Accessible by domain and by IP. |
acme |
http_host_port + https_host_port |
Let's Encrypt certificate via ACME. HTTP automatically redirects to HTTPS. Requires domain to resolve to the server and host ports 80 + 443 to be publicly reachable. |
After changing tls.mode or tls.domain, run ./prepare.py && ./ctrl.sh build && ./ctrl.sh restart.
All HTTP traffic is served through a single Caddy reverse proxy
(frontend-proxy/). Students reach /spa/register; admins reach
/admin/ (redirects to the exercise view). SSH connections go through
the SSH reverse proxy on the configured SSH port.
In production the Vue SPA is baked into the frontend-proxy image as a
static bundle — rebuild with ./ctrl.sh build after any SPA change.
./ctrl.sh up --hot-reloading starts an extra Vite dev server for the SPA and enables Flask auto-reload. Do not use this on a publicly reachable host — Vite's dev server is not hardened (see docs/ARCHITECTURE.md for details).
The build process is split into two parts. While the first part is mandatory and entails building the framework itself, the second part is only required if you plan to host exercises where ASLR is disabled for setuid binaries.
Building the framework is always required, and is described in the following.
Clone the source and the submodules:
git clone [email protected]:remote-exercise-framework/ref.git
cd ref
git submodule update --init --recursive
# Optional: run prepare.py manually to inspect or edit settings.yaml
# before building. If skipped, ctrl.sh build auto-runs it on first use
# with secure random defaults.
# ./prepare.py
./ctrl.sh buildAfter successfully building REF, the database has to be initialized:
# First, you have to start all services
./ctrl.sh up
# Then, the database has to be initialized
./ctrl.sh flask-cmd db upgrade
# Next, the framework must be shutdown, in order to pick up the changes applied to the db.
./ctrl.sh downBuilding the custom Linux kernel is only required if you need the no-randomize attribute for some exercises. This attribute allows you to disable ASLR for a specific binary, even if it is a setuid binary. This is not allowed for unmodified kernels. The following assumes that your system is based on Debian and uses GRUB as a bootloader. For other systems or bootloaders, the instructions have to be adapted accordingly.
# Switch into the custom kernel source tree
cd ref-linux
# Install the dependencies needed for building the Linux kernel
sudo apt install build-essential bison flex bc lz4 libssl-dev debhelper libelf-dev pahole
# Copy the current kernel config and use it as a starting point for the new kernel.
cp /boot/config-<...> .config
# Disable kernel signing
scripts/config --disable CONFIG_SYSTEM_TRUSTED_KEYS
scripts/config --disable CONFIG_SYSTEM_REVOCATION_LIST
scripts/config --disable MODULE_SIG_KEY
scripts/config --disable CONFIG_MODULE_SIG
# Reduce build time by disabling debug info
scripts/config --disable DEBUG_INFO
# Set default values for config attributes not found in the copied config.
make olddefconfig
# Add custom suffix to the kernel's name
scripts/config --set-str CONFIG_LOCALVERSION 'ref'
# Build the kernel as .deb package. The files will be located in the parent directory.
make -j$(nproc) bindeb-pkgAfter the kernel has been built (the artifact is located in the parent directory), it needs to be installed. This can happen via the following command:
sudo dpkg -i linux-*.debEventually, the bootloader must be configured to boot the desired kernel. If you have access to the boot menu, it is sufficient to select the new kernel (with -ref suffix) during booting. If this is no option, the process is a bit more involved:
- First, sub-menus in GRUB have to be disabled. For this add (or set)
GRUB_DISABLE_SUBMENU=yin/etc/default/grub - Then update Grub via
sudo update-grub. - Run
sudo grep 'menuentry ' /boot/grub/grub.cfg | cut -f 2 -d "'" | nl -v 0which gives you the boot-id for each installed kernel. - Execute
sudo grub-reboot <id>with theidset to the one of the REF kernel. This will temporary set the selected kernel for the next boot. - Reboot the system, and check via
uname -aif currently used kernel is the REF kernel (recognizable by the -ref suffix). - If the kernel has been loaded successfully, the kernel can be configured as default via
sudo grub-set-default <id>.
REF can be started via one of the following commands:
# This will start all services and remain attached to the terminal
# while printing debug information to the console. Closing the terminal,
# or sending SIGTERM will cause all services to be terminated. Hence, this
# command should be run in a `tmux` session.
./ctrl.sh up --debug
# Alternatively, omitting --debug will do the same but will not attach
# to the current terminal.
./ctrl.sh upIn order to shutdown all services, use the following commands:
# This will stop all services but will not remove them. This is typically
# sufficient if no changes to the images (e.g., by running `./ctrl.sh build`)
# or to the compose file have been made.
./ctrl.sh stop
# This command will delete all services that have then to be recreated from
# scratch if `./ctrl.sh up` is issued. This will not cause any data to be lost
# but requires to recreate all containers of REF itself and all user instances.
# Thus, if no changes to the system have been applied, using `./ctrl.sh stop`
# should be preferred for performance reasons.
./ctrl.sh downTo upgrade to a new version of REF, perform the following steps:
# Shutdown all running services.
./ctrl.sh down
# Make a backup of the `data` directory and note down the current commit
# of the main repository. Adapt the following exemplary command.
git rev-parse HEAD > current_commit.backup
sudo cp -ra data data-$(date "+%Y-%m-%d").backup
# Update to the most recent commits.
git pull && git submodule update --init --recursive
# Rebuild all services.
./ctrl.sh build
# Migrate the database to the new version.
./ctrl.sh flask-cmd db upgrade
# Now REF can be started again and should operate normally.
./ctrl.sh upIn case the update fails, remove the data directory and move the backup to data.
After starting the application, the following services are running on the host:
The entry server for all SSH connections to the exercises. Based on the client's username and public key, incoming SSH connections are forwarded to a container of the respective exercise.
Hostname: ssh-reverse-proxy
Port: See settings.yaml (ports.ssh_host_port, default 2222)
The web interface for managing exercises and users. Students also use it to register. All HTTP traffic is served through the frontend-proxy (Caddy), which reverse-proxies to the web (Flask) service internally.
Hostname: frontend-proxy (host-facing), web (internal Flask app)
Port: See settings.yaml (ports.http_host_port / ports.https_host_port)
User: 0
Password: See settings.yaml (admin.password)
The database used to store all information.
Hostname: db
Port: Not exposed to the host
User: ref
Database name: ref
Password: See settings.yaml (secrets.postgres_password)
The following features are disabled by default and can be enabled from the admin UI at /admin/system/settings/.
Allows students to be organized into named groups with a configurable maximum size. Students pick a group during registration, and admins can manage the available groups and reassign students afterwards. Enable via the GROUPS_ENABLED setting and configure the per-group capacity via GROUP_SIZE.
A public leaderboard at /spa/scoreboard that ranks students based on their exercise submissions. Exercises can be grouped into assignments. Enable via SCOREBOARD_ENABLED; optionally set LANDING_PAGE to scoreboard to use it as the default landing page.