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.py

The kustomization.yaml at the root lists every manifest that should be applied:

yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - web-sqli.yaml
  - crypto-rsa.yaml
  - pwn-bof.yaml

Allowed resource types

Git Sync only accepts three resource types. Any other kind in the repository is ignored.

KindScopeUse case
ChallengeCTFd onlyA challenge with no running containers. Good for trivia, OSINT, or any task that only needs a description and a flag.
SharedChallengeShared deploymentA single deployment shared by all participants. Ideal for services everyone connects to, like a web application with a common flag.
InstancedChallengePer-team instancesEach 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:

yaml
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

Note: When you change a linked file in the repository, you must also bump the challenge's version annotation so that U-CTF uploads the new file to storage.

Add or increment a version annotation in the challenge metadata:

yaml
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.

yaml
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.

yaml
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: Public

Example: InstancedChallenge

Each team receives their own isolated instance with a unique, randomly generated flag. Instances are automatically destroyed after the TTL expires.

yaml
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: 30m

Tips

  • Keep manifests at the repository root or in a flat structure referenced from kustomization.yaml. Nested Kustomizations are supported if needed.
  • Use state.type: static with staticState: hidden to push a challenge without making it visible to participants yet.
  • Remove a challenge from kustomization.yaml and push to delete it from the event.
  • The version annotation 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-based with activeAfter to 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:
    yaml
    state:
      type: time-based
      activeAfter: "2026-06-15T14:00:00Z"