Instancer
is a simple API to provision and deploy containerized web-based CTF challenges on a per-team basis.
Instancer
requires authentication to both launch instances and create challenges for deployments. In routes/auth.js, this is implemented according to the ctfjs /self
route, but it can be modified as necessary, as long as the contest route returns the user and team id, and whether the user should have administrative privileges or not.
Token-bucket ratelimiting is also supported using a Redis store, with configurable settings through environment variables.
Additionally, instancer
automatically terminates launched instances that live past a configurable amount of time, saving resources when unused.
Certain capture-the-flag challenges require getting privilege escalation. For native applications, solutions such as redpwn/jail exist to provide secure and isolated sandboxes for competitors. However, it is not usable for web challenges.
Instancer
fills this gap, and allows teams to provision web challenges individually.
For example, I wrote a challenge named filestore for Γ₯ngstromCTF 2023. As it was intended to require a bit of bruteforce as well as remote privilege escalation, it would have benefited greatly from running as an instanced challenge.
The following environment variables are required:
Name | Description | Required? |
---|---|---|
DATABASE_URI | URI to the database to store instance and challenge information | β |
PORT | Port to run the server on | β |
JWT_SECRET | Secret to sign JWTs with | β |
GCP_PROJECT_ID | Google Cloud project id | β |
GCP_LOCATION | Google Cloud location to deploy challenges on | β |
GCP_CLUSTER_NAME | Google Cloud cluster name | β |
INSTANCE_LIFETIME | The length of time an instance will stay alive after being provisioned (seconds) | β |
CLEANUP_INTERVAL | How often the cleanup utility runs to purge instances (seconds) | β |
ORACLE_URL | Competition platform route to authenticate a user token | β |
REDIS_URI | Ratelimit - URI to the Redis database | |
RL_TOKEN_CAP | Ratelimit - capacity of a token bucket | |
RL_TOKEN_ADD | Ratelimit - number of tokens to add to a bucket every minute |
Instancer
is meant to be deployed on GCP as a Cloud Run instance.
First, build and push the instance to Artifact Registry. Then, create an IAM role with Cluster Admin permissions. Finally, run instancer
with the appropriate IAM role.
Additionally, if you want to deploy a Redis instance on the same cloud, you can use this guide to set it up.
A Challenge object contains information about the deployment details of a CTF challenge.
field | type | description |
---|---|---|
competition | number | id of the competition that the challenge belongs to |
image_uri | string | uri to the Docker image to deploy |
yaml | string | additional deployment configurations |
id | id | id of the challenge information |
created | Date | creation time |
updated | Date | last updated time |
An Instance object contains information about a specific deployment of a Challenge.
field | type | description |
---|---|---|
challenge_id | number | id of the associated deployed challenge |
team_id | number | id of the team that launched the instance |
host | string | public-facing web endpoint to access the deployed challenge |
status | string | the status of the underlying kubernetes deployment |
created | Date | creation time |
updated | Date | last updated time. this is used to determine if an instance should be auto-terminated |
There are three main services:
A single lock (π) means that user-level authentication is required to access the route. A double lock (ππ) means that admin-level authentication is required to access the route.
POST /auth/
name | type | required | notes |
---|---|---|---|
token | string | yes | must be a CTFjs token |
code | description | content |
---|---|---|
200 | authenticated | JWT with fields {"admin", "user_id", "team_id", "competition_id"} |
403 | not authenticated | none |
Requires admin authentication.
GET /challenges/
code | description | content |
---|---|---|
200 | list of challenges | list containing challenges |
Requires admin authentication.
GET /challenges/:id
code | description | content |
---|---|---|
200 | challenge id found | a challenge |
404 | challenge id not found | none |
Requires admin authentication.
PUT /challenges/:id
name | type | required | notes |
---|---|---|---|
competition | number | yes | competition id |
image_uri | string | yes | uri to where challenge image is stored |
yaml | string | yes | challenge manifest yaml |
code | description | content |
---|---|---|
200 | challenge updated | saved challenge |
400 | invalid values | {"message":"invalid_values"} |
404 | challenge not found | {"message":"challenge_not_found"} |
Requires admin authentication.
DELETE /challenges/:id
code | description | content |
---|---|---|
204 | challenge deleted, if it existed | none |
Requires admin authentication.
POST /challenges/new
name | type | required | notes |
---|---|---|---|
competition | number | yes | competition id |
image_uri | string | yes | uri to where challenge image is stored |
yaml | string | yes | challenge manifest yaml |
code | description | content |
---|---|---|
201 | created successfully | the new challenge |
400 | invalid values | {"message":"invalid_values"} |
Requires admin authentication.
GET /instances
code | description | content |
---|---|---|
200 | list of all instances | list containing instances |
Requires user authentication.
GET /instances/by_id/:id
code | description | content |
---|---|---|
200 | an instance | an instance |
Requires user authentication. Gets an instance for a challenge belonging to a team.
GET /instances/by_challenge/:challenge_id
code | description | content |
---|---|---|
200 | an instance | an instance |
Requires user authentication. The user's team_id
must be the same as the instance's owner team.
DELETE /instances/:id
code | description | content |
---|---|---|
204 | successfully deleted | none |
403 | unauthorized | {"message":"unauthorized"} |
404 | instance not found | {"message":"instance_not_found"} |
Requires user authentication. It will delete a team's instance corresponding to a challenge.
DELETE /instances/by_challenge/:challenge_id
code | description | content |
---|---|---|
204 | successfully deleted | none |
403 | unauthorized | {"message":"unauthorized"} |
404 | instance not found | {"message":"instance_not_found"} |
Requires user authentication.
POST /instances/new
name | type | required | notes |
---|---|---|---|
challenge_id | number | yes | challenge id to create an instance for |
code | description | content |
---|---|---|
200 | successfully created | the running instance |
403 | unauthorized | {"message":"user_not_on_team"} or {"message":"unauthorized"} |
404 | challenge id not found | {"message":"challenge_not_found"} |
409 | challenge instance for team is already running | the running instance |
Requires user authentication. The user's team_id
must be the same as the instance's owner team.
Renewing an instance resets its lifespan.
PATCH /instances/renew
name | type | required | notes |
---|---|---|---|
id | number | yes | id of the instance to renew |
code | description | content |
---|---|---|
204 | successfully renewed | none |
403 | unauthorized | {"message":"user_not_on_team"} or {"message":"unauthorized"} |