Server guide

This section is only relevant for administrators that want to run an instance of github:marcel-scherzinger/bast3st-web#bast3st-backend to host specifications as exercises.

Installation

Cargo

As the server is entirely written in Rust, you can compile the application by using cargo:

$ cargo install --git https://github.com/marcel-scherzinger/bast3st-web

Nix

The repository also declares a Flake for the Nix package manager, so you can add it to your system packages or run the application directly with Nix:

$ nix run github:marcel-scherzinger/bast3st-web#bast3st-backend

Setup and configuration

When you run the application, it will probably output nothing, as the default log level is error. To make it more verbose, set the RUST_LOG environment variable.

Attention

It is highly adviced to at least lower the logging level to warn for logs that humans look at and set it to debug when you finally let it run alone on a server 24/7. Logs with level warn can help you during setup and will also warn you if someone uses the admin API.

$ RUST_LOG=warn bast3st-backend

Without any arguments, the server will default to a configuration that you’ll likely want to change. For this, create a new file (e. g. named bast3st.toml) where the following options can be set:

# the section itself is required, even if it is empty
[server]
# port = ...                   # defaults to 42139
# workers = ...                # defaults to 4

# if section missing, count as `enable = false`
[admin]
# BE CAREFUL: read the chapter about the admin API
enable = true
# host = ...                   # defaults to "localhost"
# port = ...                   # defaults to 42039
# workers = ...                # defaults to 2


[server.cors]
allowed_origins = [
    "http://127.0.0.1",  # local testing
    "http://localhost",  # local testing
    "https://127.0.0.1", # local testing
    "https://localhost", # local testing
    # allowed frontends
]
# allowed_headers = []         # defaults to []
# max_age = ...                # defaults to unspecified

# important for limiting size of program- and spec- uploads
[server.limits]
json = 102400
form = 102400

[database]
host = "/var/run/postgresql"
database = "bast3st"
username = "bast3st" # same as Linux username here
# not necessary if host is unix domain socket
# password =
# port = 5432

# see the network section for details
[policy.network.first-script]
command = "false"
args = []                      # defaults to []

# [policy.network.second-script]
# command = "dummy-command-2"
$ RUST_LOG=warn bast3st-backend bast3st.toml

You will probably see something like this:

$ RUST_LOG=warn bast3st-backend bast3st.toml
[2026-10-03T13:33:55Z WARN  bast3st_backend::settings] reading configuration from bast3st.toml
[2026-10-03T13:33:56Z WARN  bast3st_backend] [admin-api] start admin server api on (localhost, 42039)
[2026-10-03T13:33:56Z WARN  bast3st_backend] [admin-api] note that no network selector of bast3st-eval will be able to contact servers on port 42039

Note

CORS only limits what the browser of a user will allow the frontend that tries to communicate with the API and what frontends the browser will allow at all. So setting this to your own domain will not prevent anyone else communicating with the server, it will only ask the browsers of your users to not allow other sites to use your API to prevent e. g. malicious frontends that do extra stuff.

Network

As the rules for allowing specific network requests and others not can be complicated, the decision is made by calling configurable scripts from bast3st.toml.

If you don’t configure any scripts, all network requests will be forbidden.

You can configure multiple named scripts that will be processed in alphabetical order, where all scripts have to agree with a network request to let it through:

# ...

# this policy completly forbids any network requests by calling the UNIX false-command
[policy.network.never]
command = "false"

[policy.network.my-script]
command = "/srv/bast3st-scripts/my-script.py"
args = ["--test", "3"] # cli flags

# ...

This configuration will first try my-script (as m comes for n) and if it succeeds will try never, which will always fail and forbid the request, so this configuration is not that useful.

Each script will be called with one line of input via standard in (stdin), that is guaranteed to be JSON containing the request data. The program then needs to exit with code 0 for allowing and with any other exit code for forbidding the request. Output from the scripts can be found in the DEBUG-logs of the server.

The json can best be described with following example script:

#!/usr/bin/env python3
from typing import TypedDict
import json


class AllowNetworkCmdInput(TypedDict):
    # not set for "debug" requests
    user: str | None
    # not set for "debug" requests
    slot: str | None
    # http/https/...
    scheme: str
    # domain/ip/...
    host: str | None
    port: int | None
    path: str
    query: list[tuple[str, str]]
    # the original url
    url: str
    # request data like the json that should be sent
    data: dict


data: AllowNetworkCmdInput = json.loads(input())

if data["scheme"] == "https":
    exit(0)
else:
    exit(1)

Caution

As the data you receive was originally from a possibly malicious user, you should be careful when accepting data and even more careful when making assumptions about what values might be or what they’ll probably never be. A good first step is to check with very specific logic what to allow and not what to forbid, so that stuff in weird formats that you didn’t thought of will be forbidden.

Allow only as much as really needed, as malicious network requests coming from your server can have legal consequences for you.

Note

The server only allows the schemes http and https and it will always forbid to communicate with servers on the port that is used by the admin API if it is active and there is nothing such a script could do about this.

Admin API

You should never make the admin API publicly available! Everyone who can send messages to the API, can create new users and reset the password of any other user without authentication.

By default, it will listen only on localhost and you shouldn’t change that. This way, only local programs can send requests to it, adding an important level of security. You can use the port-mapping-capabilities of SSH to access the API as an administrator with SSH access to the server:

$ ssh -L 42039:localhost:42039 user@server

where user@server can also be an alias in your SSH config. This will make the admin API available to your localhost as long as the session is open. You can also consider deactivating the admin API after you created all required users, what is definitly the safest option.

Jailbreaking

As a badly configured server allows users to do nearly arbitrary network requests, one could also try to write the following test specification:

from bast3st import Bast3StSpec, main
from bast3st.actions import set_flag
from bast3st.decisions import NetworkRequest


register = True
part = "register" if register else "pwdreset"

spec = Bast3StSpec("Test if the network config allows to contact the admin api")
spec.run_action(
    set_flag(
        "flag",
        value=NetworkRequest(
            server="http://localhost:42039",
            route=f"/v2/api/admin/{part}",
            method="POST",
            allowed_status=(400, 200),
            json={"user": "jailbreak"},
        )["text"],
    )
)


if __name__ == "__main__":
    main(spec)

As the normal server typically is on the same host as the admin API, it would be allowed to communicate to it on a network-level. To prevent this jailbreak, the server will block all requests from user specifications that try to access the same port that is used to run the admin API. Without this protection, anyone with access to the main server could run this jailbreak successfully.

Note

This means that you make it more complicated for your users if you pick a port that is typically needed, but as the server permits only http and https connections, there shouldn’t be a problem. The default port is a good choice and shouldn’t ever cause problems.

Never let the admin API run on one of the ports 80 or 443. That is not a good idea but could also allow bypassing the safety guard from above.

Using the admin API

You can interact with the server like in Jailbreaking by sending manual network requests (from your computer), but an easier way might be to use github:marcel-scherzinger/bast3st:

$ uvx bast3st -- admin users -h
usage: bast3st admin users [-h] [-a {register,reset}] username

positional arguments:
  username

options:
  -h, --help            show this help message and exit
  -a, --action {register,reset}

To tell the CLI which server you want, you can explicitly set the environment variable BAST3ST_ADMIN_SERVER. By default it will just use http://localhost:42039, this should always be ok, right?

$ uvx bast3st -- admin users -a register ferris
Registered user 'ferris' with password: XuLLAtq3WcykatGZmDzkjEsFbWygccNz3qE8PgEKJ3
$ uvx bast3st -- admin users -a reset ferris
The password of 'ferris' is now: gc2VVLQyw3MTgP2fGd22FCp9We4enNtdHZD85vsWMF

The passwords are selected randomly by the server and you have no way to influence their value or length. The passwords you see here were reset before this documentation went live. Never share your real passwords like this!

If you see the following error, there is no server you can talk to:

$ uvx bast3st -- admin users -a register ferris
ERROR:root:Admin server at 'http://localhost:42039' misbehaved: HTTPConnectionPool(host='localhost', port=42039): Max retries exceeded with url: /v2/api/admin/health (Caused by NewConnectionError("HTTPConnection(host='localhost', port=42039): Failed to establish a new connection: [Errno 111] Connection refused"))
$ ssh -L 42039:localhost:42039 user@server # this could help