kevin-cazal/deploy_challenges
GitHub: kevin-cazal/deploy_challenges
Stars: 0 | Forks: 0
# deploy_challenges
Deploy [ctfcli](https://github.com/CTFd/ctfcli)-compatible challenge repositories to a CTFd instance.
## Docker image
Published to [GitHub Container Registry](https://github.com/kevin-cazal/deploy_challenges/pkgs/container/deploy_challenges):
ghcr.io/kevin-cazal/deploy_challenges:latest
Tags: `latest` (default branch), commit SHA, and `v*` semver on git tags.
### Deploy from a git repo (CTFd on the host)
export GPG_PASSPHRASE='…' # required if challenges use private/flag.txt.gpg
docker run --rm \
-e GPG_PASSPHRASE \
--add-host=host.docker.internal:host-gateway \
ghcr.io/kevin-cazal/deploy_challenges:latest \
https://github.com/Manta-Epitech-Academy/osint_investigator_ctf_challenges.git \
--subdir challenges \
--url http://host.docker.internal:9042/ctfd/default \
--token ctfd_YOUR_ADMIN_TOKEN
On macOS/Windows Docker Desktop, `host.docker.internal` is usually available without `--add-host`.
### Deploy from a private git repo (SSH)
The image includes `openssh-client` and GitHub host keys. Mount your SSH key (or agent).
Git clone uses `ssh -F /dev/null` by default so a host-mounted `~/.ssh/config` with
wrong ownership inside Docker does not block the clone.
export GPG_PASSPHRASE='…'
docker run --rm \
-e GPG_PASSPHRASE \
-v "$HOME/.ssh:/root/.ssh:ro" \
ghcr.io/kevin-cazal/deploy_challenges:latest \
git@github.com:kevin-cazal/shell-1-challenges.git \
--url https://YOUR_CTFD_HOST/ctfd/default \
--token ctfd_YOUR_ADMIN_TOKEN \
--force
With **ssh-agent** instead of mounting keys:
docker run --rm \
-e GPG_PASSPHRASE \
-v "$SSH_AUTH_SOCK:/ssh-agent" \
-e SSH_AUTH_SOCK=/ssh-agent \
ghcr.io/kevin-cazal/deploy_challenges:latest \
git@github.com:kevin-cazal/shell-1-challenges.git \
--url https://YOUR_CTFD_HOST/ctfd/default \
--token ctfd_YOUR_ADMIN_TOKEN \
--force
HTTPS private repos work too — embed a GitHub PAT in the URL:
`https://x-access-token:TOKEN@github.com/owner/repo.git`
### Local challenge directory
docker run --rm \
-e GPG_PASSPHRASE \
-v "$PWD/challenges:/challenges:ro" \
--add-host=host.docker.internal:host-gateway \
ghcr.io/kevin-cazal/deploy_challenges:latest \
/challenges --no-clone \
--url http://host.docker.internal:9042/ctfd/default \
--token ctfd_YOUR_ADMIN_TOKEN
### Options
| Flag | Description |
|------|-------------|
| `--url` | CTFd API base URL (required) |
| `--token` | Admin API token (required) |
| `--subdir` | Subdirectory inside the source repo |
| `--no-clone` | Source is a local path |
| `--force` | Re-sync existing challenges |
| `--wait` | Seconds to wait for CTFd (default 60) |
| `--no-sync-flags` | Skip GPG/API flag sync |
| `GPG_PASSPHRASE` | Decrypt `private/flag.txt.gpg` or `flag.yml.gpg` |
| `SHELL1_FLAG_SECRET` | CTFd env for `type: dynamic` flags (plugin) |
### CTFd home page
If the challenge root contains **`index.html`** (HTML fragment), deploy updates the CTFd page with route **`index`** (create or patch). Omit the file to leave the instance home page unchanged.
### Images in challenge descriptions
Markdown like `` is rewritten to CTFd file URLs (`/files/…`) after install, using attachments listed in `files:`. Keep images next to `challenge.yml` and list them under `files:` (see [CHALLENGE_REPOSITORY.md](CHALLENGE_REPOSITORY.md)).
Flag sync supports **`private/flag.yml`** (`static`, `regex`, `dynamic`, `custom`) and legacy **`private/flag.txt`**. See [CHALLENGE_REPOSITORY.md](CHALLENGE_REPOSITORY.md).
## Build locally
docker build -t deploy-challenges .
docker run --rm deploy-challenges --help
## Run without Docker
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
export GPG_PASSPHRASE='…'
.venv/bin/python deploy_challenges.py … --url … --token …
Requires **git**, **gpg**, and **openssh-client** (for `git@…` URLs) on the host for clone and encrypted flags.
## License
See repository defaults.