
보안 학습 이벤트를 위한 자체 호스팅 CTF 컨트롤 플레인: 팀 등록, 실시간 리더보드, 그리고 patch-to-score, 퀴즈, jeopardy, AI 챌린지 모듈을 하나의 Docker Compose 박스에서 제공합니다.
보안 학습 이벤트를 위한 자체 호스팅 컨트롤 플레인 — 박스 하나, 무료 GitHub 조직 하나.
대학, 고등학교, OWASP 챕터, 밋업을 위해 운영하세요.
코드를 작성하기 전에 AGENTS.md를 읽으세요. 이것이 운영
매뉴얼입니다: CI가 실행하는 정확한 명령, 이 저장소가 이미 겪은 실패
모드, 그리고 docs/reviewing.md의 리뷰
불변 조건이 담겨 있습니다. CLAUDE.md는 같은 파일을 가리키는
포인터입니다.
변경 사항은 CI가 통과하고 그리고 최신 커밋의 모든 실행 가능한 CodeRabbit 스레드가 해결(또는 기록상 거부)되었을 때 준비된 것입니다. 커밋은 Conventional Commits를 따르며 AI 기여 표시를 포함하지 않습니다.
작고 잘 정의된 작업에는
good first issue
태그가 붙습니다. 새 모듈은 PR이 아니라 이슈로 시작합니다 —
CONTRIBUTING.md를 참고하세요.
단일 게임이 아니라 컨트롤 플레인입니다. 이 박스는 이벤트에 공유 척추를 제공합니다 — GitHub 조직, 팀 등록, 실시간 리더보드, 주최자 관리자 패널, 그리고 이를 뒷받침하는 채점 파이프라인. 모듈은 도전 과제 콘텐츠를 그 척추에 연결하며, 어떤 부분집합이든 단독으로 또는 함께 실행될 수 있습니다: 패치-투-스코어 Secure Development, Quiz 문제 은행, Jeopardy 보드, 그리고 외부 호스팅 AI 챌린지. 모듈 계약이 척추와 콘텐츠 사이의 경계이므로, 이 박스는 포렌식, API 보안, 클라우드 등 추가 모듈이 등장하는 대로 호스팅할 수 있도록 만들어졌습니다.
존재 이유. Secure Development 모듈은 공격이 아니라 방어를 가르치며, 보안 코딩을 가르치는 진정으로 좋은 방법입니다. 지금까지 이를 운영하려면 Vercel, Upstash, Lambda, DynamoDB를 세우고, 클라우드 청구서를 감당하고, 비공개 채점 이미지에 접근할 수 있어야 했습니다. 예산이 있는 컨퍼런스에는 합리적인 요구입니다. 대학 보안 과정, 고등학교 동아리, OWASP 챕터의 밤, 또는 주말 워크숍에는 불합리한 요구입니다.
이 키트는 그것을 없앱니다. 모든 것은 이미 가지고 있는 한 대의 머신 — 노트북, 여분의 데스크톱, 작은 VPS — 에서 Docker Compose로 실행되며, 포크를 위한 무료 GitHub 조직 하나만 있으면 됩니다. 여섯 개 대상 모두의 루브릭이 박스 안에 포함되어 있으므로 요청할 비공개 이미지도, 작성할 채점 코드도 없습니다. 청구되는 것도, 외부로 통신하는 것도 없으며, 이벤트가 끝나면 저장소를 아카이브하고 스택을 중지하면 됩니다.
누구를 위한 것인가: 이 이벤트를 운영하고 싶지만 그러기 위해 클라우드 운영자가 되고 싶지 않은 모든 사람 — 과정 강사, 동아리 운영자, OWASP 챕터 리드, 워크숍 진행자, 내부 교육의 날을 운영하는 보안 팀.
배포되어 엔드 투 엔드로 검증됨; 아직 실제 코호트를 대상으로 운영되지는
않음. 전체 채점 경로가 키트 내에 포함되어 있습니다 — 채점기의
bearer 인증 POST /score, 포크를 위한 자체 포함 채점 워크플로,
폴 전송 — 그리고 scripts/smoke.sh가 그 전체 파이프라인을 목(mock)
대상으로 구동합니다. 그뿐만 아니라, 이 키트는 이 저장소가 제공하는
동일한 Compose 파일로 호스팅 박스에서 지속적으로 실행되며,
GET /health는 이를 서비스하는 정확한 리비전을 보고하고, 그 라이브
인스턴스에 대한 엔드 투 엔드 통과는 목 기반 스위트가 볼 수 없는 종류의
실제 결함 무더기가 발견되고 수정된 지점입니다.
일어나지 않은 것은 실제 이벤트입니다: 참가자 코호트가 실제 포크에 대해 실제 PR을 한꺼번에 몇 시간 동안 여는 것. 그것이 "파이프라인이 작동한다"와 "파이프라인이 40명 규모에서 작동한다" 사이의 간극입니다. 두 가지 주의 사항은 묻히지 않고 공개되어 있습니다: Security Shepherd 결과 매처에는 명시된 잔여 한계가 있으며(비정상적으로 표현된 거부가 여전히 해결로 읽힐 수 있음 — 올바른 패치에 점수를 과소 부여할 수는 있어도 공짜 점수를 부여하지는 않음), 전체 코호트의 부하 프로파일은 검증되지 않았습니다. 세부 사항과 현재 상태: 상태 및 업스트림 의존성.
이것이 그들이 하지 않는 것을 하는 부분: GitHub 풀 리퀘스트를 통해 채점되는 패치-투-스코어 방어 훈련, 하나의 리더보드에서 게임 유형을 혼합하기 위한 모듈 계약, 그리고 처음부터 끝까지 소유하는 컨트롤 플레인 — 박스 하나, 무료 조직 하나, 클라우드 청구서 없음, 텔레메트리 없음.
이 프로젝트는 OWASP Foundation과 제휴하거나 그 승인을 받지 않았습니다. 여섯 개 취약 대상 중 네 개는 OWASP 프로젝트입니다 (Juice Shop, WebGoat, Security Shepherd, VulnerableApp). DVWA와 VAmPI는 커뮤니티 프로젝트입니다.
2분 만에 실행되는 모습 보기 — GitHub 조직도, OAuth 앱도, 구성할
것도 없습니다. Compose v2가 있는 Docker와 **openssl**이
필요합니다:```sh
git clone https://github.com/dcotelo/owasp-ctf
cd owasp-ctf
./scripts/dev-stack up
로컬 일회용 시크릿을 작성하고, 스코어러와 앱 이미지를 빌드하며, 스택을 올리고, 스코어러의 실제 스코어링 API를 통해 데모 리더보드를 시딩한 뒤, 열어볼 URL을 출력합니다. 시딩된 팀과 시간별 점수 그래프가 있는 리더보드가 보일 것입니다. `./scripts/dev-stack score <login> juice-shop 3`을 실행하면 세 번의 추가 해결이 실시간으로 반영됩니다. `./scripts/dev-stack down`으로 정리합니다.
**가이드 마법사로 실제 이벤트를 운영하세요.** **[`gh` CLI](https://cli.github.com)**(인증됨)와, 이벤트에서 Secure Development를 운영한다면 **무료 GitHub 조직 하나**를 추가하세요. `./setup/ctf-setup.sh check`가 먼저 도구를 검증합니다:```sh
./setup/ctf-setup.sh # guided, prompts for values, resumable
각 값을 진행하면서 묻습니다 — 박스 URL, 이벤트 조직, 관리자
로그인, Secure Development 운영 여부, GitHub 자격 증명 — .env를 작성하고,
자동화 가능한 모든 단계를 수행하며, GitHub UI에서 해야 하는 단계는 안내하고,
중단했다가 돌아와도 이어서 진행합니다. 그 밖의 모든 것(이벤트 이름, 실행할
모듈, 대상)은 런타임 /admin 설정이므로 편집할 구성 파일이 없습니다. 실제로
필요한 것만 묻습니다: Secure Development가 없는 이벤트는 조직도, 포크도,
스코어러 이미지도 필요 없으며 그것들에 대해 묻지 않습니다. 변경을 일으키는
단계는 --dry-run으로 미리 볼 수 있습니다 — 이미 완성된 .env를 기준으로
4–9단계를 설명하며, 관리자 로그인이 없거나 Secure Development가 켜져 있는데
조직이 없으면 (설계상) 거부합니다. 마법사는 ./setup/ctf-setup.sh doctor를
실행하는 것으로 마무리됩니다 — 언제든 다시 실행할 수 있는 포크별 상태
매트릭스입니다 — 그런 다음 선택적 fly.io 배포(기본값 아니요)를 제안하므로,
같은 이벤트를 공개 호스트명에 올리는 것도 배포 문서를 헤매는 대신 안내된
흐름(호스트명, 미리 보는 배포, 그다음 확인)이 됩니다.
자세한 내용이 궁금하신가요? 모든 개별 하위 명령, 각 UI 전용 단계, 그리고
두 GitHub 앱이 어떻게 다른지:
docs/hosting.md.
클라우드에서 하시겠습니까? docs/aws.md (Terraform: ECS Fargate,
ElastiCache 및 ALB — apply로 올리고 destroy로 내림) 또는
docs/fly.md (Fly 머신 하나).
Secure Development — 의도적으로 취약한 앱을 포크하고, 결함을 찾아 패치한 뒤 PR을 엽니다. 포크의 GitHub Action이 패치에 대해 대상의 루브릭을 실행하고 점수는 리더보드에 반영됩니다(폴 모드에서 약 30초 후). 6개 대상, 321개 챌린지; 기본 상태는 0점, 올바른 패치는 해당 점수를 획득합니다 — 양방향으로 게이트가 적용됩니다. GitHub 조직과 스코어링 파이프라인이 필요합니다.
Quiz — 단일 선택 및 다중 선택 보안 문제로, 답하는 즉시 앱에서 채점되며
(다중 선택은 전부 아니면 전무), 시도 횟수 제한과 재시도 쿨다운이 있습니다.
/admin에서 하나씩 작성하거나 하나의 JSON 번들로 가져오기 및 내보내기가
가능합니다. GitHub도, 포크도, 파이프라인도 필요하지 않습니다.
Jeopardy — 카테고리별로 주최자가 작성한 플래그 보드입니다.
제출은 트림 및 정규화되고, 플래그가 대소문자 구분으로 표시되지 않은 한
대소문자는 무시됩니다(해당 카드에 그렇게 표시됨), 제출 쿨다운과 선택적 유료
힌트가 있습니다. 퀴즈와 동일한 /admin + JSON 번들 작성 방식입니다.
GitHub 역시 필요하지 않습니다.
AI — 박스 외부에서 호스팅되는 프롬프트 인젝션 및 가드레일 챌린지입니다. 각 참가자의 챌린지 페이지는 외부 사이트로의 개인 런치 링크를 발급합니다. 해결은 해당 사이트 자체 콜백을 통하거나 앱에 다시 입력한 플래그를 통해 리더보드에 보고됩니다. GitHub도, 포크도, 파이프라인도 필요하지 않습니다.
활성화한 모듈이 무엇이든, 플랫폼은 다음을 제공합니다: 캡틴이 있는 팀
자체 등록, 참여 코드 및 /join/<code> 링크(솔로 플레이는 1인 팀이며, 여러
팀원이 해결한 플래그는 한 번만 계산됨); 실제 해결별 타임스탬프로부터 만든
CTFd 스타일의 시간별 점수 그래프가 있는 실시간 리더보드; 허용 목록 기반
/admin 패널 — 동결, 채점 및 등록 기간, 힌트와 비용, 팀 상한, 쿨다운,
모듈 콘텐츠, 참가자별 지원 작업, 활동 스트림 및 참여 지표 — 모두 런타임이며
재빌드가 필요 없음; 그리고 모든 관리자 작업에 대한 상한이 있는 감사 로그.
| 참가자 분석 | 챌린지 브라우저 |
|---|---|
![]() | ![]() |
| Jeopardy 플래그 보드 | 퀴즈 |
|---|---|
![]() | ![]() |
시드된 데모 플레이어와 함께 scripts/dev-stack up으로 로컬에서
실행 중인 참가자 앱에서 캡처했습니다. 대상과 포크 링크는 이벤트 구성 기반이며,
이벤트 이름과 나머지 브랜딩은 관리자 패널 설정입니다.
하나의 Docker Compose 스택: Caddy가 Next.js 앱 앞에서 TLS를 종료합니다.
앱은 srh(Upstash 호환 REST 프록시)를 통해서만 Redis와 통신합니다 —
네트워크가 분리되어 인터넷에 노출된 어떤 것도 redis:6379로 가는 경로가
없습니다. Quiz, Jeopardy, AI는 앱 내부에서 채점하고 점수를 곧바로 Redis에
기록합니다. Secure Development는 박스 외부에서 채점됩니다: 참가자의 포크가
GitHub Action을 실행하여 대상을 부팅하고, 패치에 대해 루브릭을 실행하고,
PR에 기계 판독 가능한 점수 댓글을 게시합니다. sync 폴러가 그 댓글을
가져옵니다 — 인바운드 네트워크 표면이 전혀 없으므로 박스는 NAT 뒤와 행사장
와이파이에서도 작동합니다(이것이 유일한 전송 방식입니다: 푸시 수집은 v0.6에서
제거되었습니다, #377 참조).
점수는 단일 감사 기록기를 통해 들어옵니다:
스코어러의 베어러 인증 POST /score이며, 이는 검증하고 단조 증가 방식으로
기록합니다 — 해결은 이후의 실패한 실행으로 되돌려지지 않습니다.
전체 그림 — 구성 요소, 9단계 점수 데이터 흐름, 보안 모델 — 은 docs/architecture.md에 있습니다.
이 모듈의 콘텐츠는 취약한 대상 집합과 그 채점 루브릭입니다.
참가자는 대상을 선택하고, 조직의 복사본을 포크하고, 패치한 뒤 PR을 엽니다.
각 대상의 챌린지는 실행 가능한 node:test 스위트이며, 난이도에 따라
가격이 매겨집니다.
개수는 수동으로 유지 관리되며
apps/web/src/lib/tests/apps-catalogue.test.ts에 의해 벤더링된
루브릭에 고정됩니다 — vendor-rubric.sh 범프 후 다시 확인하세요.
올바른 수정이 점수를 얻음을 증명하는 참조 패치(양방향 게이트의
긍정 방향)는 별도로 patches/ 아래에
있습니다.
루브릭은 scorer/rubric.owasp/에 있으며,
OWASP-CTF/dc34-owasp-secure-development-ctf에서
벤더링되어 scorer/rubric.owasp/PROVENANCE.md에 기록된 단일 업스트림 커밋에
고정됩니다. 더 새로운 커밋에 대해 다시 벤더링하려면:```sh
./scripts/vendor-rubric.sh --all --ref
두 가지 루브릭 형태가 동시에 지원되며, 하나의 루브릭 디렉터리에서 두 형태를 혼용할 수 있습니다. `<target>.yaml` 파일은 선언적 HTTP 요청/기대 프로브 문법을 사용하고, `<target>/tests/challenges/` 디렉터리는 `catalogue.<target>.json`으로 가격이 책정된 실행 가능한 테스트를 사용합니다. 작성 가이드:
[docs/scorer.md](https://github.com/owasp/owasp-ctf/blob/main/docs/scorer.md).
**루브릭 비밀성에 관하여.** 이 루브릭들은 공개되어 있습니다. 대상들은 오픈 소스이고 그 해답들도 이미 공개되어 있으므로, 이 키트는 루브릭 비공개를 정답을 모르게 하는 것에 대한 보호가 아니라 검사 게이밍(check-gaming)에 대한 보호로 취급합니다 — 자체 호스팅 이벤트에서 수용되는 트레이드오프입니다. 언제든지 자신만의 비공개 루브릭으로 재정의할 수 있습니다:```sh
cp -r /path/to/private-rubric scorer/rubric
docker build -t ghcr.io/<org>/score:latest --build-arg RUBRIC_DIR=rubric scorer/
scorer/rubric/는 gitignore 처리되어 있으며 바로 이 용도를 위해 예약되어 있습니다.
스택이 EVENT_URL에서 실행된 후:
/admin을 운영합니다: 리더보드 고정, 등록 개시 및 마감, 일정
설정, 퀴즈 문제 작성, 클래식 챌린지 및 ai 챌린지 — 그리고 한 참가자가 막혔을 때
이벤트를 초기화하는 대신 그 참가자만 고쳐줍니다.docker compose logs -f sync로 합니다 (secure-development가
활성화된 상태로 실행됩니다). 모든 상태는 명명된 Docker 볼륨에 저장되므로
박스를 재부팅해도 아무것도 잃지 않습니다../setup/ctf-setup.sh teardown이 대상 리포지토리를
아카이브합니다 — 그런 다음 직접 GitHub App을 제거하고 조직의 Actions 시크릿을
삭제하세요. secure-development가 없는 이벤트는 아카이브할 포크가 없습니다.팀, 관리자 패널, 당일 전 키트 검증, 로컬 개발 스택은 모두 docs/operations.md에서 다룹니다. 사전 요구 사항, 점수 전송, OAuth 설정 및 이벤트 구성은 docs/hosting.md에 있습니다.
전체 근거, 대안 및 트레이드오프는 docs/decisions.md에 번호가 매겨진 ADR로 기록되어 있습니다.
**dcotelo.github.io/owasp-ctf**에서 렌더링됩니다.
기여를 환영합니다 — CONTRIBUTING.md는 개발 환경, CI 게이트, 모듈 제안 방법을 다룹니다. CODE_OF_CONDUCT.md가 적용됩니다.
에이전트는 AGENTS.md를 따라야 합니다. 아래 명령은 CI와 일치합니다.
make help는 동일한 대상을 나열합니다.
각 서비스는 독립적으로 테스트됩니다 (전부 Node 22):```sh (cd sync && npm ci && npm test) (cd scorer && npm ci && npm test && node tools/vacuous-sweep.mjs) ./scripts/acceptance-scorer.sh # from the repo root — the script lives in scripts/ (cd apps/web && corepack pnpm install --frozen-lockfile && corepack pnpm lint && corepack pnpm test) ./scripts/smoke.sh # the full poll pipeline, end to end
킷 자체에서 취약점을 발견하셨나요? **[SECURITY.md](https://github.com/owasp/owasp-ctf/blob/main/SECURITY.md)** — 타겟의 취약점은 의도된 것이며 범위 밖입니다.
## 라이선스 및 크레딧
MIT — [LICENSE](https://github.com/owasp/owasp-ctf/blob/main/LICENSE)를 참조하세요. `scorer/rubric.owasp/` 아래의 루브릭 콘텐츠는 업스트림 [OWASP-CTF](https://github.com/OWASP-CTF/dc34-owasp-secure-development-ctf) 이벤트에서 가져온 것이며, `scorer/rubric.owasp/PROVENANCE.md`의 커밋에 고정되어 있습니다 — 이 킷이 존재하는 이유는 그 이벤트가 한 번 이상 진행할 가치가 있었기 때문입니다. 취약한 타겟들은 벤더링되지 않았습니다: 이벤트들은 각자의 업스트림에서 포크합니다 ([Juice Shop](https://github.com/juice-shop/juice-shop), [WebGoat](https://github.com/WebGoat/WebGoat), [DVWA](https://github.com/digininja/DVWA), [Security Shepherd](https://github.com/OWASP/SecurityShepherd), [VulnerableApp](https://github.com/SasanLabs/VulnerableApp), [VAmPI](https://github.com/erev0s/VAmPI)), 그리고 각각은 자체 라이선스를 유지합니다. OWASP®는 OWASP Foundation의 등록 상표입니다; 이 프로젝트는 이와 제휴하거나 승인받지 않았습니다.
| 대상 | 챌린지 | 점수 | 비고 |
|---|
vulnerableapp | 110 | 187 | 가장 큰 대상; 8방향 병렬 채점 |
webgoat | 69 | 137 | 2단계 빌드: Maven, 그다음 포크의 런타임 전용 Dockerfile |
dvwa | 55 | 108 | MariaDB 형제 컨테이너와 스키마 초기화 필요 |
securityshepherd | 40 | 79 | HTTPS, 3개 컨테이너 스택, 엄격한 직렬 처리 |
juice-shop | 38 | 141 | 난이도가 6성까지 이어지는 유일한 대상 |
vampi | 9 | 16 | 자체 완결형; 가장 빠른 엔드투엔드 검증 |
| 합계 | 321 | 668 | 모든 이벤트가 여섯 개를 모두 프로비저닝함; /admin → Secure Development → Targets에서 하위 집합 선택 |
| 이런 상황일 때 읽으세요… | 문서 |
|---|
| 키트를 세울 때 | docs/hosting.md — 사전 요구 사항, 마법사 및 모든 개별 단계, 점수가 박스에 도달하는 방법, GitHub OAuth 앱, 이벤트 구성 |
| 클라우드에 배포할 때 | docs/aws.md (Terraform: ECS Fargate + ElastiCache + ALB) · docs/fly.md (Fly 머신 하나) |
| 문을 열기 직전일 때 | docs/security-checklist.md — 한 페이지짜리 사전 이벤트 점검 |
| 이벤트를 운영할 때 | docs/operations.md — 팀, 관리자 패널, 퀴즈/클래식/ai 주최자 가이드, 검증, 정리 |
| 시스템을 이해할 때 | docs/architecture.md — 다이어그램, 점수 데이터 흐름, Redis 키, 보안 모델, 테스트 전략 |
| 루브릭을 작성할 때 | docs/scorer.md — serve + judge 모드, 두 가지 루브릭 문법, 작성 및 빌드 |
| 새 모듈을 만들 때 | docs/modules.md — 플랫폼/모듈 계약 |
| "왜 이렇게 되어 있나요?"라고 물을 때 | docs/decisions.md — 번호가 매겨진 ADR |