Skip to content

artifact 배포 파이프라인, MySQL 8.4 judge, 문제 검토 도구, 정답 노출 수정, 시험 대비 ASG 설정을 main에 반영 - #50

Open
pwh9882 wants to merge 108 commits into
mainfrom
dev
Open

artifact 배포 파이프라인, MySQL 8.4 judge, 문제 검토 도구, 정답 노출 수정, 시험 대비 ASG 설정을 main에 반영#50
pwh9882 wants to merge 108 commits into
mainfrom
dev

Conversation

@pwh9882

@pwh9882 pwh9882 commented Sep 1, 2026

Copy link
Copy Markdown

개요

dev에 누적된 커밋 105개(PR 22개)를 main에 반영합니다. 처음 열었을 때는 artifact 기반 배포 파이프라인만 담고 있었지만, 이후 SQL Judge 실행 엔진 교체, 문제 출제 가이드와 검토 파이프라인, 학생에게 정답 SQL이 노출되던 API 수정, 시험에 대비한 ASG 설정, 업로드 파일의 S3 저장, 로그인 방식 전환(Google은 첫 접속의 수강생 확인에만 쓰고 이후 아이디·비밀번호), 시험 무결성 로그(Test 실행 기록과 동시 접속 추적), 첨부 파일 다운로드 링크 수정까지 포함하게 되었습니다. 각 PR에 자동으로 달린 Codex 리뷰 지적은 모두 후속 PR(#58~#61)로 반영했습니다. 병합 시점에 운영 release는 이 PR의 마지막 커밋(#56 포함) 기준으로 다시 빌드해야 합니다.

변경 내용

아티팩트 기반 배포 파이프라인 (#49, #51)

  • Packer가 지정한 commit SHA에서 CTFd와 SQL Judge 이미지를 빌드하고 immutable release manifest를 생성합니다.
  • dev와 main이 서로 다른 SSM channel pointer, Terraform state, 자원 이름, VPC CIDR, 도메인을 사용합니다.
  • EC2는 부팅 시 Secrets Manager에서 애플리케이션 설정을 읽고 manifest의 AMI와 image digest를 사용합니다.
  • Valkey cache 초기화는 SCAN 기반으로 처리하고 Valkey Serverless의 CROSSSLOT 오류를 피합니다.
  • nginx와 alpine 이미지를 digest로 고정하고 AMI에 미리 포함합니다.
  • 사용 중인 AMI를 보존하는 artifact prune 보호 규칙을 적용합니다.
  • CTFd와 SQL Judge를 병렬로 빌드하고 channel별 BuildKit cache를 ECR에서 재사용해 빌드 시간을 줄였습니다. Builder는 AMI 생성 전에 BuildKit 이미지와 빌드 cache를 제거합니다.
  • 절차는 IaC/ARTIFACT_PIPELINE.md에 정리했습니다.

SQL Judge 실행 엔진을 MySQL 8.4로 교체 (#52, #53, #59)

  • 내장형 go-mysql-server를 실제 MySQL 8.4.11(digest 고정, judge-db 내부 네트워크에만 연결)로 교체했습니다. 수업 SQL이 학생 로컬 MySQL 8과 같은 의미로 실행됩니다.
  • 실행마다 ctfd_tmp_<hex> 데이터베이스와 계정 둘을 만듭니다. init 계정은 해당 DB에만 ALL PRIVILEGES, 채점 계정은 SELECT, SHOW VIEW만 가집니다. 정답과 제출은 서로 다른 DB·계정에서 실행하고 끝나면 세션 KILL 후 삭제합니다. 정리 실패분은 5분 주기 스위퍼가 회수합니다.
  • 시간·자원 한도: 요청 전체 8초, 실행당 2.5초, 동시 16(대기 2초), 본문 1MiB, 결과 1,000행·4MiB. MAX_EXECUTION_TIME 힌트 차단, 이벤트 스케줄러 DISABLED.
  • MySQL root 비밀번호는 EC2 첫 부팅 때 .env.judge(0600)에 한 번 생성하고 이후 부팅에서 보존합니다. CTFd 컨테이너는 이 파일과 judge-db 네트워크에 접근하지 않습니다.
  • CTFd는 sql-judge의 /health healthcheck를 기다린 뒤 기동합니다.
  • 문자열 비교·정렬은 MySQL 8 기본값 utf8mb4_0900_ai_ci를 따릅니다.
  • 지난 학기 문제 대부분이 쓰는 MySQL Workbench·mysqldump 템플릿(DROP/CREATE SCHEMA kbo; USE kbo;, SET SQL_MODE='TRADITIONAL')을 받아들입니다. 스키마 문장은 실행하지 않고 그 이름을 임시 DB 별칭으로 치환하며, init 세션의 sql_mode를 채점 세션에 그대로 적용합니다.
  • 저장소에 있던 67MB judge 바이너리를 제거하고 빌드 산출물은 artifact pipeline이 만듭니다.
  • #53의 Codex 지적을 #59로 반영했습니다. 스키마 별칭 치환이 문자열 리터럴과 주석을 건드리지 않고('[email protected]' 보존), USE `kbo-data` 같은 백틱 스키마 이름도 인식합니다.

사용자·관리자가 체감하는 계약 변화는 PR #52 본문의 "계약 변화" 절에 정리되어 있습니다. 요약하면 실행 시간 예산이 실행당 5초에서 요청 8초·실행당 2.5초로 바뀌고, 결과 텍스트가 실제 MySQL 표현(5.0000, 12.5000, 2024-03-23)을 따르며, 채점 문장은 읽기 전용 계정으로 실행됩니다.

문제 출제 가이드와 검토 파이프라인 (#54, #55, #60)

  • platform/CTFd/plugins/sql_challenges/AUTHORING.md: 실행 모델과 한도, init에서 허용·비허용·권장하는 문장, 정답 SQL 규칙(ORDER BY와 tie-break, ROUND), 차단 단어, 게시 전 확인 항목. 관리자 문제 생성·수정 화면에서 링크합니다.
  • REVIEW.mdscripts/export-challengesscripts/review-challenges --review: 관리자 토큰으로 문제 정의를 내보내고, 일회용 MySQL 8.4 + 이 checkout의 judge로 전 문제를 채점한 뒤 문제별 판정(실행 실패, ORDER BY 없음, 정렬 키 동률, ROUND 없는 집계, 0행·500행 이상, 느린 요청)을 출력합니다. 다음 학기 조교가 문서만 보고 그대로 실행할 수 있게 만들었습니다.
  • scripts/regrade-challenges: 두 judge에 같은 입력을 보내 결과를 비교하는 재채점 도구. judge 구조를 바꿀 때 회귀 검증에 씁니다. 숫자 비교는 #60에서 표시 정밀도 기준으로 바꿔, 183.0901183.0900900900901은 표현 차이로 두되 10000001000001은 값 차이로 잡습니다.

학생에게 정답 SQL이 노출되던 API 수정 (#56)

  • 플러그인 read()init_querysolution_query를 항상 응답에 넣어, 로그인한 학생이 GET /api/v1/challenges/<id>로 정답 SQL을 볼 수 있었습니다. 플러그인 초기부터 있던 결함이며 이번 judge 작업과는 무관합니다.
  • 두 필드는 is_admin()일 때만 넣고 deadline은 모든 사용자에게 유지합니다. CTFd 테스트 하네스로 관리자·학생 응답을 고정했습니다.
  • 운영 release는 반드시 이 수정을 포함한 커밋으로 빌드해야 합니다. 현재 main channel의 main-939e48146560-20260901T104354Z에는 포함되어 있지 않습니다.

시험에 대비한 ASG 설정 (#57, #58)

  • health_check_grace_period와 instance refresh instance_warmup을 180초에서 300초로 올렸습니다. CTFd가 MySQL 초기화와 judge healthcheck를 기다리게 되면서 spot t4g.small의 launch→ALB healthy가 3분 50초로 측정됐기 때문입니다.
  • exam_windows 변수(KST, { name, start, end, capacity })로 시험·퀴즈 창을 선언하면 창마다 aws_autoscaling_schedule 두 개를 만듭니다. 시작 시각에 min_size·desired_capacitycapacity로 올리고, 종료 시각에 min_size만 되돌려 target tracking이 서서히 줄이게 합니다. 지난 창은 plan에서 제외됩니다. 지난 학기부터 손으로 하던 시험 전 8대 스케일 관행을 IaC로 옮긴 것입니다.
  • #57의 Codex 리뷰 P1 2건을 #58로 반영했습니다. 예약 작업은 max_size를 건드리지 않고(-1) capacity는 asg_min_size~asg_max_size 범위로 검증하며, 겹치거나 맞닿는 창은 validation이 거부합니다. 연달아 보는 시험은 한 창으로 합칩니다.
  • scale-out 인스턴스를 기본 온디맨드로 바꿨습니다(on_demand_percentage_above_base 0→100). 시험 중 spot 회수를 피하기 위한 것이며 t4g.small 기준 비용 차이는 학기당 몇 달러입니다. spot으로 되돌리려면 tfvars에서 0으로 둡니다.
  • ASG desired_capacity·min_sizeignore_changes로 두어 예약 작업이나 스케일링이 바꾼 값을 시험 중 apply가 줄이지 않게 했습니다.
  • IaC/README.md에 Auto Scaling 설정과 시험·퀴즈 사전 스케일 절을 추가했고 tests/test_asg_exam_readiness.py로 값을 고정했습니다.

업로드 파일의 S3 저장과 ECR 이관 문서 (#61)

  • #50의 Codex 지적 2건을 반영했습니다. 업로드 파일이 인스턴스 root volume에 있어 release마다 instance refresh가 지우던 문제를, 배포별 private·암호화 S3 bucket과 UPLOAD_PROVIDER=s3(인스턴스 role, bucket 한정 권한)로 고쳤습니다. ephemeral(dev) 배포만 bucket을 force_destroy합니다.
  • 이전 구성으로 배포한 환경이 같은 prefix를 쓸 때 foundation apply가 RepositoryAlreadyExists로 실패하는 경우의 이관 절차를 IaC/ARTIFACT_PIPELINE.md에 적었습니다. sql-2026-s2는 새 prefix라 해당되지 않습니다.

로그인 방식 전환: Google 관문 + 온보딩 + 폼 로그인 (#62)

  • 지난 학기 시험 시작 때 Google 로그인이 몰려 429가 났습니다. 원인은 /google/callback의 IP당 60초 10회 rate limit이며, 강의실·교내 NAT 뒤에서는 수십 명이 같은 IP로 보입니다. 이제 Google 로그인은 첫 접속에서 한양 계정을 확인하고 계정을 만드는 용도로만 씁니다.
  • 비밀번호 없는 Google 계정은 어떤 페이지를 열어도 /onboarding/(새 플러그인 CTFd/plugins/onboarding, blueprint + before_request 훅)으로 가서 아이디(자유 선택)·비밀번호·학번을 정하고, 이후에는 CTFd 기본 로그인 폼(아이디 또는 이메일 + 비밀번호)을 씁니다. 비밀번호를 잊으면 Google로 다시 로그인하며, 그 세션에서는 온보딩 페이지가 한 번 "새 비밀번호 설정" 모드로 나옵니다.
  • 콜백이 verified_email, hd == hanyang.ac.kr, 이메일 접미사를 모두 검사합니다. 이전에는 hd가 인가 URL의 힌트일 뿐이라 다른 Google 계정도 가입됐습니다.
  • rate limit은 /login POST 5초 30회, /google/callback 60초 60회로 올렸습니다. 로그인 화면은 폼이 먼저이고 Google 버튼은 보조 경로입니다. Codex 지적 4건을 반영했습니다.

온보딩 약관 동의, 시험 브라우저 제한, 푸터 연도 (#64)

  • 온보딩 페이지가 이용 약관(CTFd Legal 설정 tos_text, 플러그인 초안으로 seed) 동의를 함께 받고, 동의를 필수·비편집 boolean 사용자 필드로 기록합니다. 동의 기록이 없는 계정은 약관 전용 화면을 한 번 거칩니다. Google 재로그인은 반드시 새 비밀번호를 정해야 하며, 비밀번호는 8자 이상에 문자·숫자를 포함해야 합니다. 학생은 이메일을 바꿀 수 없습니다.
  • Exam Mode 관리자 화면에 "Allow the exam browser only" 스위치를 추가했습니다. 켜면 관리자를 제외하고 User-Agent에 Trustlockbrowser가 없는 요청은 403이며, 로그인·로그아웃·비밀번호 복구·정적 파일·healthcheck는 예외입니다. 문제·해설 첨부 파일은 제한 대상입니다.
  • 푸터 연도는 브라우저가 현재 연도로 채우고 약관 링크가 붙습니다. Codex 지적 14건을 모두 반영했습니다.

동시 로그인 차단을 모든 요청에서 검사 (#66)

  • 같은 계정의 새 로그인이 이전 세션을 끊는 검사가 get_current_user() 안에만 있어 순위표처럼 사용자 전체를 읽지 않는 페이지에서는 이전 세션이 통과했습니다. 플러그인 single_sessionbefore_request 훅이 로그인된 모든 요청에서 세션 nonce와 활성 nonce를 비교해 다르면 로그아웃시키고 로그인 페이지로 보냅니다(API는 401). API 토큰 요청은 검사하지 않습니다. 코어 수정은 없습니다.

ALB 상태 검사를 /healthcheck로 변경 (#67)

  • dev에서 #64의 시험 브라우저 스위치를 켠 채 두자 상태 검사 경로 /가 403을 받아 ASG가 6분마다 인스턴스를 교체했습니다(12:24~12:52Z, 교체 5회). 대상 그룹 상태 검사를 CTFd /healthcheck(DB·설정 정상일 때만 200, 두 플러그인 모두 예외)로 바꿔 스위치를 켜도 인스턴스가 내려가지 않게 했습니다. 운영 반영 전 필수입니다.

Test 실행의 execute 이벤트와 동시 접속 기록 (#68, DDPS-962·DDPS-1303)

  • attempt 핸들러의 Test 분기가 채점 직후 execute 이벤트를 행동 로그 파일에 서버 기준(사용자·문제·판정, source: "server")으로 씁니다. 행동 로그 엔드포인트는 이벤트 종류 허용 목록, 존재하는 문제, 텍스트 필드 8,000자, 본문 크기 상한을 검사하고 사용자·문제 필드를 서버 값으로 덮어씁니다. 요청당 이벤트 건수는 제한하지 않습니다. 페이지 tracker가 거부된 배치를 무한 재시도하므로 건수 제한은 일시 장애 뒤 페이지를 영구히 막습니다(검토 중 b0659ff9로 보완).
  • single_session 플러그인이 logins 스트림에 "다른 세션이 살아 있는 상태의 로그인 시도"와 "다른 브라우저 로그인으로 끊김" 두 줄을 남깁니다. 최근 30분 활동 기준으로 로그아웃 뒤 재로그인과 구분합니다. (#69에서 session started 한 줄로 대체)

로그인 기록 한 줄화와 시험 세션 규칙 스위치 (#69)

  • 로그인마다 logins 스트림에 브라우저와 직전 로그인(시각·IP·브라우저)을 담은 session started 줄 하나를 남기고, #68의 활동 키·경고 줄은 제거했습니다.
  • 단일 세션은 Exam Mode 화면의 "Allow only one session per student" 스위치를 켠 동안만 학생에게 적용되고 관리자는 늘 예외입니다(코어 get_current_user()의 nonce 검사도 같은 조건). 기본은 꺼짐이라 평소에는 기기를 오갈 수 있습니다.
  • 시험 브라우저 제한이 켜진 동안 다른 브라우저의 학생 로그인 POST는 403(rate limit에 합산), 로그인 페이지의 Google 버튼은 숨김. 시험 규칙이 하나라도 켜져 있으면 관리자 페이지 상단에 배너가 뜹니다.
  • 폼 로그인 rate limit IP당 5초 120회, Google 콜백 1분 300회. Google 허용 도메인은 GOOGLE_HOSTED_DOMAIN 설정(config.ini 또는 환경변수, 기본 hanyang.ac.kr)입니다.
  • 2026-09-03 dev에 배포해 두 스위치, 배너, 다른 브라우저 로그인 403을 확인했습니다(아래 검증 기록).

S3 첨부 파일 다운로드 링크를 리전 호스트로 서명 (#70)

  • dev에서 첨부 파일 업로드는 됐지만 다운로드가 403이었습니다. CTFd가 보내는 presigned URL이 boto3 기본 addressing에서 글로벌 호스트(<bucket>.s3.amazonaws.com)로 서명되는데, ap-northeast-2 버킷은 S3가 리전 호스트로 리다이렉트해 서명이 어긋납니다(SignatureDoesNotMatch). IaC/ec2/userdata.sh의 CTFd 환경에 AWS_S3_ADDRESSING_STYLE=virtual을 넣어 리전 호스트로 서명하게 했습니다. 코드 변경은 없고, 설정을 고정하는 테스트와 README 설명을 추가했습니다. 운영 uploads 버킷도 새로 만들어지므로 이 수정 없이는 운영에서도 같은 403이 납니다.

온보딩 페이지에서 비밀번호 규칙과 약관 체크를 입력 중에 표시 (#65)

  • 비밀번호 확인 칸 아래 규칙 세 줄(8–128자, 영문자 A–Z와 숫자 0–9 포함, 두 입력 일치)을 입력할 때마다 색과 체크 표시로 바꾸고, 규칙과 약관 체크가 모두 충족될 때만 제출 버튼을 켭니다. 서버 검증은 그대로이며 스크립트가 꺼진 브라우저는 이전처럼 제출 후 오류를 봅니다.
  • 서버와 페이지가 어긋나지 않도록 규칙을 ASCII로 명시했습니다(ASCII 공백만 제거, 영문자와 숫자 각 1개 이상, 코드 포인트 길이). #64의 "문자와 숫자"(유니코드 문자 허용)보다 좁아진 정책입니다. Codex P2 8건은 모두 반영했습니다.

로그인·온보딩 화면의 버튼 너비와 설정 화면의 토큰 탭 (#71)

  • 로그인 화면과 온보딩 화면의 제출 버튼을 폼 너비 전체로 늘리고(아래 Google 버튼과 같은 너비) 폼 열에 아래 여백을 줘 푸터와 붙지 않게 했습니다.
  • 설정 화면의 Access Tokens 탭은 관리자에게만 렌더링합니다. 탭만 숨기면 API로는 토큰을 만들 수 있으므로, 온보딩 플러그인이 학생의 POST /api/v1/tokens를 403으로 거부하고 학생 토큰으로 인증하는 요청도 403으로 거부합니다. 이 거부는 CTFd의 tokens 훅보다 먼저 실행되어 login_user() 자체가 일어나지 않으므로, 단일 세션 규칙 아래에서 옛 토큰이 학생의 시험 브라우저 세션을 끊지 못합니다. 관리자 토큰은 그대로 동작합니다. Codex P1 3건·P2 1건을 반영했습니다.

오래 실패하던 회귀 테스트 4건 정리 (#72)

  • 회귀 세트에서 계속 실패하던 upstream 테스트 4건은 모두 이 포크가 의도적으로 바꾼 동작(기본 테마 ddps, oauth_id 문자열 컬럼, ddps 설정 화면의 필드 구성, 내비바의 사용자 이름 표시)을 반영하지 못한 경우였습니다. 테스트를 포크의 동작에 맞추고, MLC 경로(auth.py)가 id를 문자열로 저장하도록 명시했습니다. 코드 결함은 없었습니다.
  • 회귀 세트 161개 통과, 실패 0입니다(전에는 4 failed).

검증 기록

2026-09-01 main·dev 동시 운영 (아티팩트 파이프라인)

  • dev: sql-2026-s2-dev, sql-dev.ddps.cloud, 10.26.0.0/16
  • main: sql-2026-s2, sql.ddps.cloud, 192.168.0.0/16
  • main release: main-939e48146560-20260901T104354Z, AMI ami-002ad5ac61eb67837
  • main Terraform apply: 45 added, 0 changed, 0 destroyed. main과 dev의 후속 plan: 양쪽 모두 No changes
  • main과 dev ASG 각각 1대 InService/Healthy, main ALB target healthy, CTFd·SQL Judge·nginx 컨테이너 실행 중
  • sql.ddps.cloud: /setup으로 정상 연결, sql-dev.ddps.cloud: HTTP 200

2026-09-02 dev 배포 (MySQL judge)

dev@65f890ca를 release dev-65f890cae7b0-20260902T050048Z(AMI ami-0bbb1474ddbc824b2)로 빌드해 sql-dev.ddps.cloud(t4g.small)에 instance refresh로 반영했습니다. 부팅 후 MySQL 초기화 약 25초, CTFd 기동까지 약 2분 15초, ALB healthy까지 3~3분 50초였습니다. .env.judge 0600, CTFd에서 MySQL 비접근, event_scheduler=DISABLED, utf8mb4_0900_ai_ci, OOM 0을 확인했습니다.

지난 학기 문제 61개 전수 재채점(같은 t4g.small, 옛 judge는 dev@72b1775d):

항목 새 judge (MySQL 8.4) 옛 judge (go-mysql-server)
성공 60 / 61 61 / 61
요청 지연 p50 / p90 / 최대 632ms / 801ms / 2,401ms 561ms / 2,987ms / 5,614ms
61문제 합계 37.6초 76.3초
실행당 2.5초 초과 없음 -
정리 실패, 잔여 임시 DB·계정 0, 0 -

실패한 1건(#32)은 정답 SQL의 ambiguous GROUP BY TEAM_ID로 로컬 MySQL에서도 같은 오류이며 문제 수정 대상입니다. 옛 엔진과의 결과 차이는 숫자 표현 17건, 행 순서 6건(ORDER BY 없음 또는 tie-break 없음: #22·#23·#27·#30·#36·#57), DATE 표현 2건으로, 정답과 제출이 같은 엔진에서 실행되므로 채점에는 영향이 없습니다.

부하 실측(5테이블×100행 init): 동시 16에서 7.2 req/s·p95 2.5초, 동시 32에서는 96건 중 26건이 busy 응답. 5테이블×1000행 init은 동시 16에서 3.9 req/s·p95 4.7초. 인스턴스당 채점 상한은 초당 5~8건입니다.

마무리 검증: 2대 scale-out(spot), 세션 유지 40/40, 두 judge에 걸친 채점 20/20, 인스턴스 재기동 후 .env.judge 보존, CloudWatch sql-judge 스트림 수집, CTFd 웹 경로(setup → 관리자 로그인 → SQL 문제 생성 → Test → Submit) 정상. 검증 후 dev runtime 45개 자원과 dev secret은 삭제했고 release·AMI는 유지했습니다.

2026-09-02 ASG·업로드 설정 plan (#57, #58, #61)

빈 dev state에 샘플 시험 창 2개(하나는 지난 날짜)를 넣고 plan만 실행했습니다. 47 to add, 지난 창은 제외, midterm은 시작 2026-10-19T23:30:00Z·종료 2026-10-20T02:30:00Z(KST 08:30/11:30)로 min·desired 8, 종료 시 min 1·desired 유지. grace 300, warmup 300, 온디맨드 100 확인. #58 이후에는 창 2개(midterm 8, final 10)로 49 to add이고 예약 4개 모두 max_size = -1이며, 맞닿는 창 2개와 capacity 11은 validation 오류로 거부됐습니다. #61 이후 기본 plan은 49 to add(uploads bucket 리소스 4개 추가)입니다. apply는 하지 않았습니다.

2026-09-02 dev 배포와 무중단 갱신 (#62, #64)

빈 dev state에 release dev-9596eb99a24f-20260902T091111Z(#62 병합 커밋)를 apply(49 add)해 sql-dev.ddps.cloud를 올리고 초기 설정·로그인 화면·Google 인가 URL(hd, dev 콜백 URI)·온보딩 게이트를 스크립트로 확인했습니다. 이어 #64를 병합한 release dev-1878e147f44a-20260902T113949Z를 같은 환경에 apply(0 add·2 change: launch template AMI, ASG release 태그)했고, instance refresh(Rolling, min healthy 100%, warmup 300초)가 11:50:37Z에 시작해 11:57:25Z에 끝났습니다. 그동안 2초 간격으로 /healthcheck·관리자 세션 API·/login을 1,353회 확인해 실패 0이었고, 로그인 세션은 인스턴스 교체 뒤에도 유지됐습니다. 새 인스턴스의 스모크 15개(약관 페이지·푸터 링크 포함) 통과. 브라우저에서 한양 계정이 콜백을 통과하고(userinfo에 hd 존재), Google 재로그인 시 비밀번호 재설정이 강제되며, Exam Mode의 Trustlock 스위치가 동작하는 것을 확인했습니다.

2026-09-02 dev 갱신 2회차 (#66, #67)

release dev-ededbbd8e7f9-20260902T123043Z를 apply(0 add·3 change: launch template AMI, ASG 태그, 대상 그룹 상태 검사)했고 instance refresh가 12:55:29Z~13:01:59Z에 끝났습니다. 2초 간격 615회 확인에서 실패 0, 세션 유지. 스모크 19개 통과(두 번째 로그인 뒤 첫 세션의 API 401·페이지 로그인 리다이렉트, 두 번째 세션 유지 포함).

2026-09-02 시험 무결성 로그 (#68)

플러그인·Google 테스트 34개와 upstream 문제 API 테스트 63개 통과. 테스트 환경에는 judge가 없어 execute 이벤트의 판정은 incorrect(judge 연결 오류)로 남았습니다. 실제 판정 값과 CloudWatch 전송은 dev에서 확인해야 합니다.

2026-09-02 dev 갱신 3회차 (#68)

release dev-d0591bc13f57-20260902T134949Z를 apply(0 add·2 change)했고 instance refresh가 14:00:20Z~14:07:28Z에 끝났습니다. 2초 간격 1,368회 확인에서 실패 0, 세션 유지. 스모크 22개 통과: 관리자 Test 실행이 judge correct로 채점되고 CloudWatch behavior 스트림에 execute 이벤트(source: server)가 도착했으며, 두 번 로그인한 뒤 logins 스트림에 동시 접속 줄이 남았습니다.

2026-09-03 dev 재배포와 갱신 4회차 (#69, #70)

빈 dev state에 release dev-3a4f60e46b8b-20260903T012606Z(#69 병합)를 apply(49 add)했습니다. 스모크에서 #69 항목이 모두 통과했습니다: 단일 세션 스위치가 꺼진 동안 학생의 이전 세션이 다음 로그인 뒤에도 유지, 켜면 이전 세션이 API 401·페이지는 로그인으로 이동, 관리자 두 세션은 모두 유지, 관리자 페이지에 배너 스크립트 포함; 시험 브라우저 제한을 켠 동안 /healthcheck 200, 로그인 페이지의 Google 버튼 숨김, 학생 세션 403(API는 JSON), 다른 브라우저의 학생·미등록 이름 로그인 403(기존 학생 세션은 끊기지 않음), Trustlock UA 로그인 통과, 관리자 로그인 통과, 대상 그룹 healthy 유지(#67); Test 실행의 execute 이벤트(judge correct, source: server)와 loginssession started 줄(직전 로그인 포함). 첨부 파일은 업로드는 됐지만 다운로드가 S3 SignatureDoesNotMatch 403이어서 #70으로 고쳤습니다. #70 apply(0 add·2 change) 뒤 instance refresh 02:01:55Z~02:08:25Z 동안 2초 간격 531회 확인에서 실패 0, 세션 유지. 다시 돌린 스모크 50개 모두 통과(첨부 다운로드는 S3 리다이렉트를 거쳐 200). 개인 Gmail 거부는 브라우저에서 확인해야 합니다.

2026-09-03 dev 갱신 5회차 (#65)

release dev-9d5807f084a8-20260903T041247Z를 apply(0 add·2 change)했고 instance refresh 04:22:36Z~04:29:04Z 동안 2초 간격 546회 확인에서 실패 0, 세션 유지. 스모크 50개 통과. 약관에 동의하지 않은 학생 계정으로 연 온보딩 페이지(약관 전용 모드)에서 #65의 폼 속성(data-password-min-length·data-password-max-length), 약관 체크박스 표식 1개, 규칙 목록 없음, 실시간 검사 스크립트를 확인했습니다. 입력 중 반응은 브라우저에서 확인해야 합니다.

2026-09-03 dev 갱신 6회차 (#71, #72)

release dev-429224201e73-20260903T054956Z를 apply(0 add·2 change)했고 instance refresh 05:59:21Z~06:05:50Z 동안 2초 간격 507회 확인에서 실패 0, 세션 유지. 스모크 50개 통과. #71 확인: 학생 설정 화면에 Access Tokens 탭 없음, 관리자에게는 있음, 학생 POST /api/v1/tokens 403, 관리자 토큰 생성·인증·삭제 정상, 로그인 화면 제출 버튼 폼 너비·열 아래 여백 마크업. 화면 모양은 브라우저에서 확인해야 합니다.

저장소 테스트

  • go test, go vet, scripts/test-sql-judge(MySQL 8.4.11 통합 테스트: 격리, 권한, timeout, 스위퍼, 템플릿 호환, HTTP smoke)
  • 저장소 Python 테스트 30개(artifact pipeline, runtime 이미지·compose 설정, 검토 판정 함수, ASG 시험 설정·상태 검사, 업로드 저장소)와 CTFd 하네스 테스트(플러그인 정답 노출, 온보딩 6개, Google 콜백 2개). 회귀 세트(tests/users tests/oauth tests/plugins test_views test_themes)는 오래 실패하던 회귀 테스트 4건을 포크의 의도된 동작에 맞게 정리 #72 이후 161개 통과, 실패 0입니다.
  • judge alias 치환에서 문자열 리터럴을 보존하고 따옴표 스키마 이름을 인식 #59 이후 지난 학기 61문제 재채점: 수정 전과 상태·컬럼·행 수·응답 크기 동일. #60의 새 숫자 규칙으로 옛 judge와 다시 비교한 분류도 이전과 같음(일치 35, 숫자 표현 17, 값 차이 8, 비교 불가 1)
  • terraform validate, 수정 파일 terraform fmt -check

운영 반영 시 확인할 것

  • 운영에서도 Test 한 번 → behavior 스트림에 execute 이벤트, 로그인 → logins 스트림에 session started 줄이 오는지 확인합니다(dev에서는 확인했습니다). 지난 학기 데이터에는 execute 이벤트와 source·received_at 필드가 없습니다.

  • 운영 release를 이 PR의 병합 커밋으로 다시 빌드하고 main channel을 갱신한 뒤 apply합니다. #56이 포함되지 않은 release로 운영을 시작하면 안 됩니다. 같은 apply가 ASG 설정(시험에 대비한 ASG 설정: 첫 부팅 여유 300초, 예약 스케일, 온디맨드 스케일 아웃 #57)을 in-place로 반영하며 instance refresh 트리거는 tag뿐이라 재배포는 일어나지 않습니다.

  • 시험·퀴즈 일정이 정해지면 운영 tfvars의 exam_windows에 창(시험 30분 전 시작, 종료 30분 뒤 끝, capacity 8)을 넣고 plan을 검토한 뒤 apply합니다. 창은 겹치지 않게 하고 capacity는 asg_max_size(10) 이하로 둡니다. 시험이 끝나면 항목을 지웁니다. 인스턴스당 채점 상한이 초당 5~8건이라 target tracking만으로는 시험 시작 burst를 따라가지 못합니다.

  • 온디맨드 스케일 아웃 기본값(100)은 이 PR에서 제안한 값입니다. spot으로 되돌리려면 tfvars에서 on_demand_percentage_above_base = 0으로 둡니다.

  • 문제 내용 수정(조교 작업): #32 GROUP BY, tie-break 없는 6문제의 ORDER BY 보강, #33 상관 서브쿼리 최적화, 지문의 숫자·DATE 예시 출력 갱신. scripts/review-challenges --review로 확인합니다.

  • 운영 환경의 초기 관리자 설정과 로그인 흐름을 브라우저에서 확인합니다. dev의 브라우저 확인(개인 Gmail 거부, #65의 입력 중 규칙 표시, #71의 화면)은 2026-09-03 사용자가 마쳤고, dev는 06:28Z에 destroy했습니다.

  • 지난 학기 Notion 플랫폼 튜토리얼의 로그인 절차를 새 흐름으로 바꿉니다.

  • 운영 반영 뒤 문제 첨부 파일을 하나 올려 관리자와 학생 계정에서 내려받히는지 확인합니다(S3 첨부 파일 다운로드 링크를 리전 호스트로 서명 (다운로드 403 수정) #70, dev에서는 확인했습니다).

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T11:36:37.071143Z 939e481 PR opened
🔒 Security Review Completed 2026-09-01T11:38:41.978753Z 939e481 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 939e481465

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread IaC/ec2/ec2.tf
Comment on lines +335 to +336
instance_refresh {
strategy = "Rolling"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist uploads before triggering instance refreshes

When a production release changes the launch template or ArtifactRelease tag, this rolling refresh terminates the existing instances. However, platform/docker-compose.production.yml bind-mounts uploads from .data/CTFd/uploads, and the launch template stores that directory on a root EBS volume configured with delete_on_termination = true; consequently, every artifact deployment can permanently delete uploaded challenge/user files. Move uploads to shared durable storage or preserve/migrate them before enabling automatic refreshes.

Useful? React with 👍 / 👎.

Comment thread IaC/foundation/main.tf
Comment on lines +6 to +9
resource "aws_ecr_repository" "ctfd" {
name = local.ctfd_repository_name
image_tag_mutability = "IMMUTABLE"
force_delete = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Migrate existing ECR state before creating foundation repositories

For an environment deployed from the parent configuration, the removed module.ami already owns repositories named ${var.prefix}-ctfd and ${var.prefix}-sql-judge; with the default values, these are the exact names this new foundation state attempts to create. Following the documented foundation-first upgrade therefore fails with RepositoryAlreadyExists, while applying the old runtime state removal first schedules the force-deletable repositories and their images for destruction. Add an explicit cross-state move/import migration or use non-conflicting repository names so existing deployments can adopt the pipeline safely.

Useful? React with 👍 / 👎.

pwh9882 and others added 19 commits September 1, 2026 12:19
아티팩트 빌드 시간을 단축한다
Independent review against MySQL 8.4.11 and the built judge container
reproduced three student-reachable problems, all fixed here:

- Event scheduler threads outlived DB and account cleanup (2 of 40
  requests) and the sweeper could not stop them: start MySQL with
  --event-scheduler=DISABLED and make the sweeper kill ct_% sessions by
  name pattern instead of by mysql.user membership.
- The MAX_EXECUTION_TIME optimizer hint overrode the session limit:
  reject the hint in validateSQLQuery.
- DML is not bounded by max_execution_time and its rollback doubled the
  cost of a killed statement: run the graded statement under a separate
  SELECT-only account while a full-privilege init account loads the
  challenge SQL. The graded connection is pinned so the session limit
  applies to that statement.

Also align collation with the MySQL 8 default (utf8mb4_0900_ai_ci) so
ordering matches a student's local MySQL, give sql-judge a /health
healthcheck, and make CTFd wait for it so a fresh instance never accepts
submissions it cannot grade.
scripts/regrade-challenges sends every challenge definition to a judge with
the solution as both solution and submission, records status, latency, row
count and result size, and optionally compares result rows against a
baseline judge (for example the previous go-mysql-server build). Numeric
formatting differences are classified separately from value differences.
The input is a JSON array of challenge definitions only; no user or
submission data is involved.
SQL Judge 실행 엔진을 실제 MySQL 8.4로 교체
Re-grading last semester's 61 challenges on the MySQL judge failed 58 of
them at init: the scripts are MySQL Workbench and mysqldump exports that
manage their own schema and session.

- Skip CREATE/DROP DATABASE|SCHEMA and USE statements and map that
  schema name onto the execution's temporary database, including
  mysqldump's /*!... */ wrapped forms and schema-qualified names such
  as kbo.PLAYER in init and graded statements.
- Drop leading comment lines from each statement; MySQL rejects
  "-----" separators and empty comment-only chunks.
- Carry the init session's sql_mode (for example TRADITIONAL) into the
  graded statement's session, as one local MySQL session would.
- Start MySQL with lower_case_table_names=1 so Salaries and salaries
  match, as on Windows and macOS where the challenges were written.

The re-grade tool now treats float-precision differences as formatting.
judge가 로컬 MySQL 템플릿 형식의 init SQL을 받아들이도록 수정
AUTHORING.md explains how the judge executes init and graded SQL, what a
local MySQL export may contain, how results are compared (unique ORDER BY,
numeric text formats), and which words the filter blocks. The admin create
and update forms link to it, as does the plugin README.
scripts/export-challenges pulls SQL challenge definitions from a CTFd
through the admin API (token from the environment), and
scripts/review-challenges starts a disposable MySQL judge from the checkout,
grades every definition with scripts/regrade-challenges --review, prints
authoring findings, and removes the containers.

The review findings cover execution failures, missing ORDER BY, repeated
sort-key values, ORDER BY expressions the tool cannot check, unrounded
AVG/division results, empty or near-limit results, and slow requests.
REVIEW.md documents the procedure and how to act on each finding so a
future TA or an agent can run it unchanged.
문제 검토 파이프라인 추가 (export, review, 판정)
The plugin's read() always included init_query and solution_query, and
CTFd returns that dict from GET /api/v1/challenges/<id> to every logged-in
user, so any student could fetch the answer key. Only admins receive the
two fields now; the student view never used them. A plugin test in the
CTFd harness locks this in.
학생에게 SQL 문제 정답이 노출되던 API 응답 수정
pwh9882 and others added 30 commits September 2, 2026 13:37
The page's tracker flushes every 5 seconds or 20 events and puts a
rejected batch back in front of its buffer to retry forever. After one
transient failure the buffer passes 50 events, and a count-based 400
then leaves the page stuck, silently losing every later event. Only the
body size is bounded now; the 50-event figure remains as the sizing
basis for that cap.
…ity on sign-out

already_solved on client events is decided at the event's own timestamp
(solves dated before it), so work buffered just before the first
correct submission is not marked as after it; execute events use the
same rule at the moment of the run.

When a session is signed out because an API token replaced the active
nonce, its activity marker is removed too, so the next form login is
not logged as concurrent.
Test 실행을 execute 이벤트로 서버에서 기록하고 동시 접속을 로그에 남김 (DDPS-962, DDPS-1303)
…e exam browser

Since only one session per account is ever alive, the sequence of
logins is the whole record of who held an account and when. Each login
now writes one line with the browser and the previous login of the
account (time, address, browser). The activity marker, the "attempt
while another session is active" line and the "signed out" line from
#68 are removed; they tried to decide concurrency on the server and
needed state that the login trail makes unnecessary.

While the exam-browser restriction is on, a student's login from any
other browser is refused before it happens. The login page stayed open
for admins, but a successful login replaces the account's session and
would have signed the student out of the exam browser.
The login line is written only when the pre-request nonce snapshot
exists; a request refused by an earlier hook has none and is not a
login. During the exam-browser restriction every non-admin name,
existing or not, gets the same 403 from another browser, and refused
attempts count against the login view's rate limit.
…address

A configured preset admin exists only after its first login, so its
name or email is recognised before the database lookup. The previous
login record stores get_ip(), the same resolved address CTFd's log
lines use behind a proxy.
Two TAs may share an admin account during an exam, and an admin's token
script runs beside the browser. Both the plugin hook and the fork's
check in get_current_user() now skip admin accounts; students are
still limited to one session.
…xam-start logins

The one-session rule for students is now a switch on the Exam Mode
page, off by default so students may move between devices outside
exams; the login trail is written either way. While any exam rule is
on, every admin page shows a banner so it is not forgotten. The login
page hides the Google button while the exam-browser rule is on, and the
form login limit rises to 120 per 5 seconds per address for a lecture
hall behind one NAT.

The login trail line now reads "session started" so it is not confused
with CTFd's own "logged in" line.
…, log registrations

The admin banner was inserted as the first child of <body>, where the
admin theme's fixed navbar covers it. It now goes into <main>, which
starts below the navbar, the same place the core update notice uses.

The exam-time login check let any name that matched the preset admin
name or email through. It now looks the account up first: an existing
account is judged by its type, and only a preset admin that does not
exist yet is recognised, by the same name and password auth.login
accepts.

Registration also starts a session, so auth.register is in the login
endpoint map and writes a "session started via registration" line.
…urse domain a setting

The first lecture is where a whole class signs in with Google in the
same minute from one lecture-hall NAT address, so the per-IP callback
limit goes from 60 to 300 per minute. Google has already authenticated
the request by then, so the limit only guards against abuse.

The course domain was a constant in auth.py. It is now
GOOGLE_HOSTED_DOMAIN in config.ini or the environment, hanyang.ac.kr
when unset, and the userdata passes it through from the application
secret when present.
… lookup

The refusal hook looked the submitted account up before checking the
shared login rate limit, so an address already over the limit still
cost one query per attempt. The limit is now checked first, and only a
refused attempt below it is counted.
로그인 기록을 한 줄로 단순화하고 시험 세션 규칙(단일 세션·브라우저 제한)을 스위치와 배너로 관리
CTFd builds attachment download links as S3 presigned URLs. With boto3's
default addressing the URL points at the global host
(bucket.s3.amazonaws.com), which S3 answers with a redirect to the
regional host for a bucket outside us-east-1. The redirected request no
longer matches the signature, so every download ended in 403 on dev.

Set AWS_S3_ADDRESSING_STYLE to virtual so the URL is signed for the
regional host, add a test that pins the setting, and note the reason in
the IaC README.
S3 첨부 파일 다운로드 링크를 리전 호스트로 서명 (다운로드 403 수정)
온보딩 페이지에서 비밀번호 규칙과 약관 체크를 입력 중에 바로 표시
…footer off the form

Both forms placed the submit button in a right-aligned third of the
row, leaving an empty two-thirds to its left. The button now spans the
form like the Google button below it. The form column also gets a
bottom margin so the footer no longer sits directly under the button on
the longer onboarding page.
API tokens are for admin scripts; a student has no use for the tab, so
the settings page renders the tab and its pane only for admins.
Each failure came from a deliberate change of this fork that the
upstream test never learned about:

- the default theme is ddps, so the prefix-loading test compares
  against the default theme's prefix instead of core's;
- oauth_id is a string column (Google accounts store "google_<id>"),
  so the MLC path now stores the ids as strings on every database and
  the test expects strings;
- the ddps settings page has no website or country field, so the test
  checks those two through the API and the rendered fields on the page;
- the ddps navbar names the logged-in user, so the hidden-user test
  searches the page content instead of the whole body.
Hiding the settings tab alone left the token API open to students, and
a student who already held a token could no longer see or revoke it.
The onboarding plugin's account-policy hook now refuses a student's
POST /api/v1/tokens and any request a student signs in with a token,
so no student token can exist or work.
…opened

CTFd's tokens hook signs the request in before the plugin hook runs,
so refusing with 403 alone left a session cookie a client could reuse
without the token. The hook now logs that session out before refusing.
The token refusal sat behind request_is_exempt(), so a student token
sent to an exempt endpoint such as /healthcheck still left the session
CTFd's tokens hook had opened. The check now runs first in the hook.
Refusing after CTFd's tokens hook still let login_user() replace the
account's active nonce, which signs the student's browser out under
the one-session rule. The onboarding plugin now puts its check at the
front of the before_request list and answers 403 before any login;
unknown or expired tokens are left to CTFd's hook.
로그인·온보딩 화면의 제출 버튼을 폼 너비로 늘리고 설정 화면의 토큰 탭을 관리자에게만 보임
오래 실패하던 회귀 테스트 4건을 포크의 의도된 동작에 맞게 정리
SQL 채점 공정성과 시험 운영 안정성 개선
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant