Git Sync Guide
Manage your challenges as code and keep them in sync with a Git repository.
Overview
Git Sync connects a GitHub repository to your event workspace. When enabled, U-CTF continuously watches the repository and applies challenge manifests found at the root. Every push to the default branch triggers a reconciliation that creates, updates, or removes challenges to match your repository content.
The repository root must contain a kustomization.yaml file that references the manifests you want to deploy. This is how U-CTF discovers which files to process.
Repository structure
A typical repository looks like this:
my-ctf-challenges/
├── kustomization.yaml
├── web-sqli.yaml
├── crypto-rsa.yaml
├── pwn-bof.yaml
└── challenges/
└── web-sqli/
├── description.md
└── exploit.pyThe kustomization.yaml at the root lists every manifest that should be applied:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- web-sqli.yaml
- crypto-rsa.yaml
- pwn-bof.yamlAllowed resource types
Git Sync only accepts three resource types. Any other kind in the repository is ignored.
| Kind | Scope | Use case |
|---|---|---|
| Challenge | CTFd only | A challenge with no running containers. Good for trivia, OSINT, or any task that only needs a description and a flag. |
| SharedChallenge | Shared deployment | A single deployment shared by all participants. Ideal for services everyone connects to, like a web application with a common flag. |
| InstancedChallenge | Per-team instances | Each team gets their own isolated instance with a unique flag. Use this for challenges that require a fresh, writable environment per team. |
All three types use apiVersion: ctfd.uctf.io/v1.
Linking files from the repository
Challenges can reference files stored in the same repository. These files are versioned alongside your manifests and synced to storage automatically. Linked files appear as downloadable attachments in CTFd.
Use the files field inside the challenge spec:
files:
- type: "git"
value: "/challenges/web-sqli/exploit.py"
- type: "git"
value: "/challenges/web-sqli/description.md"
name: "README.md"- type must be
"git". - value is the absolute path from the repository root, starting with
/. - name (optional) overrides the filename shown to participants in CTFd.
Updating linked files
Add or increment a version annotation in the challenge metadata:
metadata:
name: web-sqli
annotations:
version: "2"Every time a linked file changes, increment the version number. This tells U-CTF that the files need to be re-uploaded even if the rest of the manifest has not changed.
Example: Challenge (CTFd only)
A minimal challenge with no infrastructure. This creates a challenge entry in CTFd with a static flag, a description, and a downloadable file.
apiVersion: ctfd.uctf.io/v1
kind: Challenge
metadata:
name: trivia-history
annotations:
version: "1"
spec:
name: "History of CTFs"
description: "When was the first DEF CON CTF held? Submit the year."
category: "Trivia"
author: "admin"
value:
type: static
staticValue: 50
flag:
type: static
staticValue: "1996"
mountType: none
state:
type: static
staticState: active
tags:
- trivia
- beginner
files:
- type: "git"
value: "/challenges/trivia/hints.pdf"
name: "hints.pdf"Example: SharedChallenge
A web application shared by all participants. Everyone connects to the same instance. The flag is injected as an environment variable inside the container.
apiVersion: ctfd.uctf.io/v1
kind: SharedChallenge
metadata:
name: web-sqli
annotations:
version: "1"
spec:
challenge:
name: "SQL Injection 101"
description: "Find the admin password through the login form."
category: "Web"
author: "admin"
value:
type: dynamic
initialValue: 500
decayFunction: logarithmic
decayValue: 25
minimumValue: 100
flag:
type: static
staticValue: "uctf{sq1_1nj3ct10n_b4s1cs}"
mountType: env
mountEnv:
envVar: "FLAG"
state:
type: static
staticState: active
files:
- type: "git"
value: "/challenges/web-sqli/source.zip"
name: "source.zip"
infra:
replicas: 1
pods:
- name: app
flagContainers:
- web
spec:
containers:
- name: web
image: registry.example.com/ctf/web-sqli:latest
ports:
- name: http
containerPort: 8080
exposedPorts:
- slug: http
name: "Web UI"
podName: app
portName: http
protocol: HTTP
type: PublicExample: InstancedChallenge
Each team receives their own isolated instance with a unique, randomly generated flag. Instances are automatically destroyed after the TTL expires.
apiVersion: ctfd.uctf.io/v1
kind: InstancedChallenge
metadata:
name: pwn-buffer-overflow
annotations:
version: "1"
spec:
challenge:
name: "Buffer Overflow 101"
description: "Exploit the buffer overflow to get a shell and read the flag."
category: "Pwn"
author: "admin"
value:
type: static
staticValue: 300
flag:
type: per_instance
perInstancePrefix: "uctf"
mountType: file
mountFile:
filePath: "/flag.txt"
fileMode: "0444"
state:
type: static
staticState: active
files:
- type: "git"
value: "/challenges/pwn-bof/vuln.c"
infra:
replicas: 1
pods:
- name: target
flagContainers:
- vuln
spec:
containers:
- name: vuln
image: registry.example.com/ctf/pwn-bof:latest
ports:
- name: tcp
containerPort: 1337
exposedPorts:
- slug: tcp
name: "TCP"
podName: target
portName: tcp
protocol: TCP
type: Public
instanceTTL: 30mTips
- Keep manifests at the repository root or in a flat structure referenced from
kustomization.yaml. Nested Kustomizations are supported if needed. - Use
state.type: staticwithstaticState: hiddento push a challenge without making it visible to participants yet. - Remove a challenge from
kustomization.yamland push to delete it from the event. - The
versionannotation only affects file uploads. Changing the challenge spec fields (name, flag, value, etc.) is picked up automatically without bumping the version. - Use
state.type: time-basedwithactiveAfterto schedule a challenge to become visible automatically at a specific date and time (UTC). This is useful for releasing challenges in waves during a competition:yamlstate: type: time-based activeAfter: "2026-06-15T14:00:00Z"