A self-hostable workshop environment inspired by OWASP Juice Shop, but focused on GitHub Actions attack vectors. Participants exploit intentionally vulnerable workflows to extract tokens and simulate lateral movement across a GitHub organization.
Each challenge is a GitHub repository with a deliberately vulnerable Actions workflow. Hidden inside the workflow (via a Docker image pulled at runtime) is a short-lived GitHub App installation token. The token is scoped to a single "flag" repository — participants must extract the token and use it to open an issue there, simulating lateral movement. A scoreboard tracks progress across all challenges.
Tokens are automatically rotated on schedule (and on manual trigger), so a burned token never permanently stops the workshop.
See docs/CHALLENGES.md for the full challenge summary.
ws-setup/
├── README.md # This file
├── copilot-instructions.md # GitHub Copilot context for this project
├── setup.sh # Creates all repos, pushes files, sets secrets
├── docs/
│ ├── FACILITATOR.md # Workshop running order and facilitation notes
│ ├── CHALLENGES.md # Challenge summary table
│ └── challenges/
│ ├── challenge-01.md # Vulnerability, exploit, fix, facilitator nudges
│ ├── challenge-02.md
│ ├── challenge-03.md
│ ├── challenge-04.md
│ ├── challenge-05.md
│ ├── challenge-06.md
│ └── challenge-07.md
└── repos/ # Mirrors target org repo structure
├── token-dispenser/ # Token rotation workflows and Dockerfile
├── challenge-1/ # Vulnerable repo files
├── challenge-2/
├── challenge-3/
├── challenge-4/
├── challenge-5/
├── challenge-6/
├── challenge-7/
├── flag/ # Template pushed to all flag-N repos
└── scoreboard/ # GitHub Pages scoreboard
- A GitHub organization (free tier is fine). You can create one here.
- Remember to go to https://github.com/organizations/YOUR_ORG/settings/actions and select
Require approval for first-time contributors who are new to GitHub, otherwise PRs will require approval the first time. ghCLI installed and authenticated:gh auth loginjqinstalledpython3withpip
This must be done manually via the GitHub UI. The app is what generates short-lived, scoped tokens for each challenge.
Go to your organization's settings:
https://github.com/organizations/YOUR_ORG/settings/apps/new
Fill in:
- GitHub App name: anything, e.g.
ws-token-distributor - Homepage URL: your org URL, e.g.
https://github.com/YOUR_ORG - Webhooks: uncheck "Active" — not needed
- Permissions (Repository, not Organization):
Issues: Read & WriteContents: Read-only
- Where can this app be installed: Only on this account
Click Create GitHub App.
On the app's settings page, scroll to Private keys and click Generate a private key.
A .pem file will be downloaded — keep it safe.
At the top of the app settings page you'll see App ID — note this down.
Go to:
https://github.com/organizations/YOUR_ORG/settings/apps
Click Edit next to your app, then Install App in the left sidebar. Install it on your org and select Only select repositories. You'll come back to add repositories here after creating them.
Clone this repo and run the setup script. It will:
- Create all repositories in your org (
token-dispenser,challenge-1throughchallenge-7,flag-1throughflag-7,scoreboard) - Push the vulnerable workflows and READMEs to each challenge repo
- Set the required secrets on
token-dispenser - Set required permissions for GitHub packages. (challenge-1-env should only be accessible from challenge-1 etc.)
git clone https://github.com/YOUR_ORG/ws-setup
cd ws-setup
export ORG="your-org-name"
export APP_ID="123456"
export APP_PRIVATE_KEY="$(cat /path/to/your-app.pem)"
bash setup.shNote: The script uses
ghCLI, so make sure you're authenticated with an account that has org admin access.
After the setup script creates all repos, go back to the app installation settings:
https://github.com/organizations/YOUR_ORG/settings/installations/INSTALLATION_ID
Click Configure and under Repository access, add all flag-N repositories and the scoreboard repository:
flag-1throughflag-7scoreboard
The flag repos are internal, so the scoreboard workflow must use the GitHub App to generate a read token for them — a plain GITHUB_TOKEN from the scoreboard repo cannot access internal repos in the org. The scoreboard repo is included in the installation so that actions/create-github-app-token can run there.
Go to token-dispenser → Actions → Rotate Workshop Tokens → Run workflow.
This generates fresh installation tokens (scoped to their respective flag repos), bakes them into Docker images, and pushes them to GHCR. The challenge workflows pull these images at runtime — participants extract the token from there.
Verify it worked by checking that the packages challenge-env-1 through challenge-env-7 appear under your
org's packages tab on GitHub.
The challenge images are private GHCR packages. Each challenge repo's GITHUB_TOKEN needs explicit read access
to pull its image. This must be done manually after the first rotation (the packages don't exist before that).
For each challenge, navigate to the package settings and add the corresponding repo under Manage Actions access:
| Package settings URL | Repo to add |
|---|---|
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-1/settings |
challenge-1 |
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-2/settings |
challenge-2 |
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-3/settings |
challenge-3 |
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-4/settings |
challenge-4 |
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-5/settings |
challenge-5 |
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-6/settings |
challenge-6 |
https://github.com/orgs/YOUR_ORG/packages/container/challenge-env-7/settings |
challenge-7 |
This only needs to be done once. The access persists across token rotations since the package name stays the same.
In token-dispenser, edit .github/workflows/rotate-token.yml and update the cron to match your workshop window.
Tokens expire after 1 hour, so rotate at least every 45 minutes within your session window.
Note that for a free github organization, the schedule might not run as expected. In that case, you can trigger manual rotations from the Actions tab in
token-dispenser&scoreboardas needed..
Example for a workshop running 10:00–13:00 UTC on March 4th:
on:
schedule:
- cron: "*/45 10-12 4 3 *"
workflow_dispatch:Commit and push this change before the workshop starts.
The following secrets must be set on token-dispenser (the setup script handles this):
| Secret | Description |
|---|---|
APP_ID |
Numeric App ID from the GitHub App settings page |
APP_PRIVATE_KEY |
Full PEM content including -----BEGIN RSA PRIVATE KEY----- headers |
See docs/FACILITATOR.md for the full running order and facilitation notes.
- Add an entry to
repos/token-dispenser/challenges.json - Create
docs/challenges/challenge-0N.mdwith vulnerability, exploit, fix, and facilitator nudges - Add the challenge row to
docs/CHALLENGES.md - Create the challenge repo files under
repos/challenge-N/ - Run
setup.shto create the repo and push the files - Grant the GitHub App access to
flag-Nin the org installation settings - Trigger a manual rotation in
token-dispenser
GitHub App installation tokens have a maximum lifetime of 1 hour — this cannot be changed. Adjust the rotation cron to be shorter than 1 hour to ensure tokens never expire mid-workshop.
The setup script is safe to run against an existing org — it only creates new repos and will not
touch any existing ones. Repo names are hardcoded as challenge-N and flag-N to avoid collisions.