Configuration
Complete reference for all rCTF configuration options including file-based config and environment variables.
rCTF is configured through YAML or JSON files in a rctf.d/ directory and optional environment variable overrides.
Configuration loading
By default, rCTF searches upward from packages/config/ for a directory named rctf.d/. Set RCTF_CONF_PATH to use another directory.
rCTF loads .yaml, .yml, and .json files alphabetically, merging each file over the previous ones. This lets you separate base settings, providers, and deployment-specific overrides:
rctf.d/
- 01-base.yaml Core settings
- 02-providers.yaml Provider configuration
- 03-overrides.yaml Environment-specific overrides
Environment variables are applied last, overriding any file-based values.
Environment variables
The following environment variables are supported. They override values from config files.
Core
| Variable | Type | Description |
|---|---|---|
RCTF_NAME |
string |
CTF display name |
RCTF_ORIGIN |
string |
CTF origin URL (e.g., https://ctf.example.com) |
RCTF_TOKEN_KEY |
string |
Base64-encoded 32-byte key for token encryption |
RCTF_INSTANCE_TYPE |
string |
all, frontend, or leaderboard |
RCTF_SHUTDOWN_TIMEOUT |
integer |
Graceful-shutdown cap in milliseconds before force-exit. 0 disables the cap. |
RCTF_IDLE_TIMEOUT |
integer |
Idle connection timeout in seconds (0-255) |
RCTF_MAX_REQUEST_BODY_SIZE |
integer |
Maximum accepted request body size in bytes |
RCTF_UPLOAD_PROVIDER |
string |
uploads/local, uploads/s3, or uploads/gcs. See Uploads for each provider’s variables. |
RCTF_CONF_PATH |
string |
Path to config directory (overrides search) |
Database
| Variable | Type | Description |
|---|---|---|
RCTF_DATABASE_URL |
string |
PostgreSQL connection string |
RCTF_DATABASE_HOST |
string |
PostgreSQL host (if not using URL) |
RCTF_DATABASE_PORT |
integer |
PostgreSQL port |
RCTF_DATABASE_USERNAME |
string |
PostgreSQL user |
RCTF_DATABASE_PASSWORD |
string |
PostgreSQL password |
RCTF_DATABASE_DATABASE |
string |
PostgreSQL database name |
RCTF_DATABASE_MIGRATE |
string |
before, only, or never |
RCTF_REDIS_URL |
string |
Redis connection string |
RCTF_REDIS_HOST |
string |
Redis host (if not using URL) |
RCTF_REDIS_PORT |
integer |
Redis port |
RCTF_REDIS_PASSWORD |
string |
Redis password |
RCTF_REDIS_DATABASE |
integer |
Redis database number |
Timing and auth
| Variable | Type | Description |
|---|---|---|
RCTF_START_TIME |
integer |
Competition start time (Unix milliseconds) |
RCTF_END_TIME |
integer |
Competition end time (Unix milliseconds) |
RCTF_LOGIN_TIMEOUT |
integer |
Verification token expiry in milliseconds |
RCTF_USER_MEMBERS |
boolean |
Enable team members feature |
RCTF_CTFTIME_CLIENT_ID |
string |
CTFtime OAuth client ID |
RCTF_CTFTIME_CLIENT_SECRET |
string |
CTFtime OAuth client secret |
UI and meta
| Variable | Type | Description |
|---|---|---|
RCTF_HOME_CONTENT |
string |
Home page markdown content |
RCTF_FAVICON_URL |
string |
Favicon URL |
RCTF_META_DESCRIPTION |
string |
Meta description |
RCTF_IMAGE_URL |
string |
Meta image URL |
RCTF_GLOBAL_SITE_TAG |
string |
Google Analytics tag. Deprecated and auto-converted to analytics.provider at startup. See Upgrading from v1. |
| Variable | Type | Description |
|---|---|---|
RCTF_EMAIL_FROM |
string |
Email sender address |
RCTF_EMAIL_LOGO_URL |
string |
Logo URL in email templates |
Leaderboard
| Variable | Type | Description |
|---|---|---|
RCTF_LEADERBOARD_MAX_LIMIT |
integer |
Max teams per leaderboard page |
RCTF_LEADERBOARD_MAX_OFFSET |
integer |
Max leaderboard offset |
RCTF_LEADERBOARD_UPDATE_INTERVAL |
integer |
Leaderboard recalc interval (ms) |
RCTF_LEADERBOARD_GRAPH_MAX_TEAMS |
integer |
Max teams on score graph |
RCTF_LEADERBOARD_GRAPH_SAMPLE_TIME |
integer |
Graph sample interval (ms) |
Note
Boolean environment variables accept true, yes, y, or 1 as truthy values. Anything else is treated as false.
Configuration reference
Below is the complete reference of all configuration options available in rctf.d/ files.
Core settings
ctfName: My CTF # CTF display name (required)origin: https://ctf.example.com # Public origin URL (required)tokenKey: <base64-32-byte-key> # Token encryption key (required)instanceType: all # all | frontend | leaderboardshutdownTimeout: 30000 # Graceful-shutdown cap in msidleTimeout: 65 # Idle connection timeout in secondsmaxRequestBodySize: 1073741824 # Maximum request body size in bytes| Field | Type | Default | Description |
|---|---|---|---|
ctfName |
string |
- | Display name of your CTF |
origin |
string |
- | Public URL of your CTF (no trailing slash) |
tokenKey |
string |
- | Base64-encoded 32-byte key for AES-GCM token encryption |
instanceType |
string |
all |
Controls which components run: all (API + leaderboard worker), frontend (API only), leaderboard (worker only). See Scaling before splitting roles. |
shutdownTimeout |
number |
30000 (30s) |
Time in milliseconds to drain in-flight requests and stop the leaderboard worker on SIGTERM/SIGINT before the process force-exits. 0 disables the force-exit cap. See Scaling. |
idleTimeout |
number |
65 |
How long in seconds an idle connection stays open before the server closes it. Set it higher than your reverse proxy’s keep-alive timeout. Bun limits this value to 255, and rCTF rejects larger values at startup. Set it to 0 to disable the timeout. |
maxRequestBodySize |
number |
1073741824 (1 GiB) |
Maximum request body size accepted by the Bun API server. This is the effective backend cap for upload routes when the bundled nginx config sets client_max_body_size to 0 for streaming uploads. |
Warning
The tokenKey is critical for security. All authentication tokens are encrypted with this key. If it is lost or changed, all existing tokens become invalid and users will need to re-authenticate.
Database
database: sql: postgres://user:password@localhost:5432/rctf redis: redis://localhost:6379 migrate: neverThe sql field accepts either a connection string or an object:
database: sql: host: localhost port: 5432 user: rctf password: secret database: rctf maxPoolSize: 100 # Default: 100 idleTimeout: 30000 # Default: 30000 (ms) connectTimeout: 3000 # Default: 3000 (ms)The redis field accepts either a connection string or an object:
database: redis: host: localhost port: 6379 password: secret # Optional database: 0 # Optional, default 0| Field | Type | Default | Description |
|---|---|---|---|
database.sql |
string | object |
- | PostgreSQL connection (required) |
database.redis |
string | object |
- | Redis connection (required) |
database.migrate |
string |
never |
before runs migrations on startup, only runs migrations and exits, never skips |
Timing
startTime: 1735689600000 # January 1, 2025 00:00 UTCendTime: 1735776000000 # January 2, 2025 00:00 UTC| Field | Type | Default | Description |
|---|---|---|---|
startTime |
number |
- | Competition start time in Unix milliseconds (required) |
endTime |
number |
- | Competition end time in Unix milliseconds (required) |
Tip
To convert a date to Unix milliseconds: date -d "2025-01-01T00:00:00Z" +%s000new Date('2025-01-01T00:00:00Z').getTime() in JavaScript.
Divisions
divisions: open: Open student: StudentdefaultDivision: opendivisionACLs: - match: domain value: example.edu divisions: [student, open] - match: any value: '' divisions: [open]| Field | Type | Default | Description |
|---|---|---|---|
divisions |
object |
{ open: "Open" } |
Map of division ID to display name |
defaultDivision |
string |
- | Default division for new users (optional) |
divisionACLs |
array |
- | Access control rules for divisions (optional) |
Division ACLs
ACLs control which divisions a user can register for based on their email. Each ACL entry has:
| Field | Description |
|---|---|
match |
Match type: domain, email, regex, or any |
value |
Value to match. For a domain rule, use the full domain after @, such as example.edu. Email rules take the full address, regex rules take a pattern, and any uses an empty value. |
divisions |
Array of division IDs this rule grants access to |
A user receives the divisions from every matching rule, regardless of order. A domain rule matches the full domain exactly. For example, example.edu matches alice@example.edu but not alice@sub.example.edu.
Warning (Email provider required, disable CTFtime auth)
Division ACLs require an email provider. Disable CTFtime authentication when using them because CTFtime sign-ins bypass the ACLs and can select any division.
Authentication
registrationsEnabled: trueuserMembers: truemaxMembers: 50loginTimeout: 3600000ctftime: clientId: '12345' clientSecret: your-secret| Field | Type | Default | Description |
|---|---|---|---|
registrationsEnabled |
boolean |
true |
Whether new registrations are allowed |
userMembers |
boolean |
true |
Enable team members feature |
maxMembers |
number |
50 |
Maximum members per team |
loginTimeout |
number |
3600000 |
Verification/CTFtime token expiry in milliseconds (1 hour) |
ctftime.clientId |
string |
- | CTFtime OAuth client ID (numeric string) |
ctftime.clientSecret |
string |
- | CTFtime OAuth client secret |
Note
Auth tokens (used for logging in) never expire. Only verification and CTFtime tokens expire according to loginTimeout.
Providers
Providers are pluggable integrations, each selected by name with provider-specific options. Names follow a <category>/<provider> scheme where the category is a plural or mass noun. The Providers section documents every provider in detail.
Uploads
uploadProvider: name: uploads/s3 options: bucketName: my-bucket awsKeyId: AKIAIOSFODNN7EXAMPLE awsKeySecret: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY awsRegion: us-east-1| Field | Type | Default | Description |
|---|---|---|---|
uploadProvider |
object |
{ name: "uploads/local" } |
File upload provider |
See Upload Providers for provider-specific options.
Scoring
scoreProvider: name: scores/classic| Field | Type | Default | Description |
|---|---|---|---|
scoreProvider |
object |
{ name: "scores/classic" } |
Scoring algorithm provider |
See Scoring Providers for provider-specific options.
Captcha
captcha: provider: name: captcha/turnstile options: siteKey: your-site-key secretKey: your-secret-key protectedEndpoints: - register - recover| Field | Type | Default | Description |
|---|---|---|---|
captcha.provider |
object |
- | Captcha provider config (name + options) |
captcha.protectedEndpoints |
array |
- | List of actions requiring captcha |
Available captcha actions: register, recover, setEmail, instancerStart, instancerExtend, avatarUpload, adminBotSubmit.
See Captcha Providers for provider-specific options.
Instancers
instancers: docker: name: instancers/docker options: apiUrl: http://localhost:8000 authToken: secret k8s: name: instancers/k8s options: apiUrl: https://k8s.example.com authToken: <service-account-token> k8s-arm: name: instancers/k8s options: apiUrl: https://k8s-arm.example.com authToken: <service-account-token>defaultInstancer: k8s| Field | Type | Default | Description |
|---|---|---|---|
instancers |
object |
- | Map of named challenge instancer providers (optional). Each key is an instancer name a challenge can target. |
defaultInstancer |
string |
- | Name of the instancer used when a challenge doesn’t pick one. Required when more than one instancer is defined. When there is only one, rCTF selects it automatically. |
See Instancer for the available instancers and their options.
Admin bot
adminBot: provider: name: admin-bots/rctf-ts options: {} maxLogsPerUserChallenge: 5| Field | Type | Default | Description |
|---|---|---|---|
adminBot.provider |
object |
- | Admin bot provider config |
adminBot.maxLogsPerUserChallenge |
integer |
5 |
Max stored log entries per user per challenge |
email: provider: name: emails/smtp options: smtpUrl: smtp://user:pass@mail.example.com:587 from: noreply@example.com logoUrl: https://example.com/email-logo.png| Field | Type | Default | Description |
|---|---|---|---|
email.provider |
object |
- | Email provider config |
email.from |
string |
- | Sender email address (required if email enabled) |
email.logoUrl |
string |
- | Logo URL for email templates. Falls back to the top-level light and dark logos when unset. |
See Email Providers for provider-specific options.
UI
homeContent: | Welcome to My CTF!
**Markdown** is supported.sponsors: - name: Sponsor Name iconLight: https://example.com/sponsor-light.png iconDark: https://example.com/sponsor-dark.png description: Sponsor description url: https://sponsor.commeta: description: A cool CTF competition imageUrl: https://example.com/banner.pngfaviconUrl: https://example.com/favicon.icologoLightUrl: https://example.com/logo-light.svglogoDarkUrl: https://example.com/logo-dark.svgflagFormatPlaceholder: 'flag{[\x20-\x7e]+}'| Field | Type | Default | Description |
|---|---|---|---|
homeContent |
string |
"Home content. Markdown supported." |
Home page content (Markdown) |
sponsors |
array |
[] |
List of sponsors (name, iconLight, iconDark, description, url). When only one mode’s icon is set, the home page renders it color-inverted in the other mode; legacy icon is accepted as the light-mode icon |
meta.description |
string |
"rCTF event description" |
HTML meta description |
meta.imageUrl |
string |
"" |
HTML meta image URL |
faviconUrl |
string |
rCTF default | Favicon URL |
logoLightUrl |
string |
"" |
Logo for light mode |
logoDarkUrl |
string |
"" |
Logo for dark mode |
flagFormatPlaceholder |
string |
"flag{[\\x20-\\x7e]+}" |
Flag format hint shown to participants |
Tip
The admin settings API can update ctfName, event timing, homepage content, sponsors, metadata, favicon, and logos without restarting rCTF.
Analytics
analytics: provider: name: analytics/google options: siteTag: G-XXXXXXXXX| Field | Type | Default | Description |
|---|---|---|---|
analytics.provider |
object |
- | Analytics provider (analytics/google, analytics/cloudflare) |
Limits
maxAvatarSize: 1048576 # 1 MBleaderboard: maxLimit: 100 maxOffset: 4294967296 updateInterval: 30000 graphMaxTeams: 10 graphSampleTime: 300000 graphWithListLimit: 100| Field | Type | Default | Description |
|---|---|---|---|
maxAvatarSize |
number |
1048576 (1 MB) |
Maximum avatar upload size in bytes |
leaderboard.maxLimit |
number |
100 |
Max teams returned per leaderboard request |
leaderboard.maxOffset |
number |
4294967296 |
Max pagination offset |
leaderboard.updateInterval |
number |
30000 (30s) |
Leaderboard recalculation interval in ms |
leaderboard.graphMaxTeams |
number |
10 |
Max teams displayed on score graph |
leaderboard.graphSampleTime |
number |
300000 (5min) |
Time between graph data points in ms |
leaderboard.graphWithListLimit |
number |
100 |
Max teams for combined leaderboard + graph endpoint |
Moderation
avatarsModeration: provider: name: moderation/openai options: apiKey: sk-... allowOnInternalError: true| Field | Type | Default | Description |
|---|---|---|---|
avatarsModeration.provider |
object |
- | Moderation provider for avatar uploads |
avatarsModeration.allowOnInternalError |
boolean |
true |
Allow avatar upload if moderation API fails |
See Moderation Providers for details.
Proxy
proxy: cloudflare: true trust: 2| Field | Type | Default | Description |
|---|---|---|---|
proxy.cloudflare |
boolean |
false |
Trust Cloudflare CF-Connecting-IP header for client IP |
proxy.trust |
boolean | string | string[] | number |
2 |
Proxy trust setting for X-Forwarded-For. true trusts all, a number trusts exactly that many proxy hops in front of the API, a string or array specifies trusted CIDR ranges or the named subnets loopback, linklocal, uniquelocal |
The bundled nginx inside the container counts as the first hop, so the default of 2 matches the standard deployment: the bundled nginx plus one reverse proxy on the host. With it, logs and rate limits use the participant’s IP as reported by the host proxy.
Warning
A hop count must match your topology exactly. Setting it too high lets participants spoof X-Forwarded-For to evade per-IP rate limits, while setting it too low attributes all traffic to one of your proxies. Increase it if you add another layer (such as a load balancer), set it to 1 if the container is exposed directly, and behind Cloudflare set proxy.cloudflare to true instead.
Blood bot
bloodBot: bloodsCount: 3 destinations: - provider: name: messages/discord options: url: https://discord.com/api/webhooks/... bloodEmojis: - '<:rank1:1008801500736266261>' - '<:rank2:1008801501776449738>' - '<:rank3:1008801503080874056>' messageTemplate: '{{bloodEmoji}} {{bloodNumTitle}} solve for challenge "**{{challengeName}}**" goes to **{{teamName}}**!' - provider: name: messages/telegram options: botToken: '123456:ABC-DEF...' chatId: '-1001234567890'| Field | Type | Default | Description |
|---|---|---|---|
bloodBot.bloodsCount |
number |
1 |
Number of blood tiers to announce (1-3) |
bloodBot.destinations |
array |
- | At least one destination (required) |
bloodBot.destinations[].provider |
object |
- | Messages provider config |
bloodBot.destinations[].bloodEmojis |
string[] |
[] |
Optional blood emoji for this destination |
bloodBot.destinations[].messageTemplate |
string |
- | Custom message template (optional) |
Available template variables: {{teamName}}, {{teamUrl}}, {{bloodEmoji}}, {{bloodNumber}}, {{bloodNumOrdinal}}, {{bloodNumSentence}}, {{bloodNumWord}}, {{bloodNumTitle}}, {{challengeCategory}}, {{challengeName}}.
See Blood Bot for more details.
Complete example
ctfName: Example CTF 2025origin: https://ctf.example.comtokenKey: dGhpcyBpcyBhIGJhc2U2NCBrZXkgZXhhbXBsZSE=
database: sql: postgres://rctf:password@localhost:5432/rctf redis: redis://localhost:6379 migrate: before
startTime: 1735689600000endTime: 1735776000000
divisions: open: Open student: StudentdefaultDivision: opendivisionACLs: - match: domain value: edu divisions: [student, open] - match: any value: '' divisions: [open]
captcha: provider: name: captcha/turnstile options: siteKey: 0x4AAAAAAA... secretKey: 0x4AAAAAAA... protectedEndpoints: - register - recover
email: provider: name: emails/smtp options: smtpUrl: smtp://user:pass@mail.example.com:587 from: noreply@example.com
uploadProvider: name: uploads/s3 options: bucketName: ctf-uploads awsKeyId: AKIAIOSFODNN7EXAMPLE awsKeySecret: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY awsRegion: us-east-1
proxy: cloudflare: true
bloodBot: bloodsCount: 3 destinations: - provider: name: messages/discord options: url: https://discord.com/api/webhooks/123/abc bloodEmojis: - '<:rank1:1008801500736266261>' - '<:rank2:1008801501776449738>' - '<:rank3:1008801503080874056>' messageTemplate: '{{bloodEmoji}} {{bloodNumTitle}} solve for challenge "**{{challengeName}}**" goes to **{{teamName}}**!'