운영 감사 후속: 보안 패치, call 무결성, 검증된 백업, cron typecheck - #38
Merged
Conversation
2026-08-20 운영 리포트 7건을 코드에 대조해 실제 결함만 고쳤다.
리포트의 "stance enum 미검증" 은 사실이 아니었다 — 화이트리스트가 이미
있었고, 진짜 결함은 순서였다.
보안
- Next 16.2.4 → 16.2.12. 공개 권고 22건(App Router Server Component DoS
포함)의 패치 하한이 16.2.11 이었다.
- 미사용 nanoid 직접 의존 제거.
- 30건(high 17) → 7건. 남은 7건은 전부 next 가 스스로 핀한 postcss·
nanoid3·sharp 이고 이 앱에서 도달 불가(next/image 미사용, remotePatterns
없음, 자체 CSS 만) — 이유와 함께 pnpm-workspace.yaml 에 기록하고
`pnpm audit --audit-level=high` 를 CI 게이트로. --prod 가 아닌 이유:
여기선 devDependencies 가 운영에서 돈다(모든 cron 이 .bin/tsx).
Trackable call 무결성
- 글을 먼저 쓰고 가격을 부른 뒤 call 삽입을 catch{} 로 삼키던 순서를
뒤집었다. reference price 를 모델 호출 *전에* 확보하고, 글과 call 을
한 transaction(createPostWithCall)으로 쓴다. 둘 다 남거나 둘 다 없다.
가격 출처가 죽으면 토큰도 쓰지 않는다.
- callDirectionFor 가 agree|disagree 만 방향으로 인정한다. 이전엔 예상 밖
stance 문자열이 조용히 down call 이 됐다.
- persona-tick 의 priceable quota 를 "후보 2개 예약" 에서 "하루 발행 2건"
으로. 예약된 둘이 쿨다운·429 에 걸리면 그날 call 이 0 이었다.
배포
- pm2 save 실패를 경고로 넘기지 않는다. 재시도 → CRITICAL 알림 → 릴리스
prune 중단(stale dump 가 가리키는 디렉터리를 지우면 재부팅이 안 온다).
배포 자체는 실패로 기록하지 않는다 — 새 릴리스는 살아 있다.
- swap 직전 quiet-hour 재검사. 05:5x 에 시작한 tick 이 06 시 cron 구간에서
swap 하면 pm2 재등록이 그 cron 을 두 번 태운다.
- 릴리스에서 pnpm test 를 돌린다(--force·CI 무판정 경로 대비).
관측
- audit-auto 가 에러 결과를 완료로 세지 않는다 → 다음 실행에서 재시도.
실패한 질의만 골라 재시도하므로 성공한 답변은 유지된다.
- audit cron 의 *실행 여부* 를 health subsystem 으로(인용률은 여전히 제외 —
0% 는 재시작으로 안 고쳐지고 strict 는 모니터에 물려 있다). 94일 정체가
아무에게도 안 알려지던 이유가 이것이었다.
- 일일 요약에 citation rate 한 줄.
재해복구
- scripts/backup-db.ts: 매일 03:00 KST 스냅샷 → PRAGMA integrity_check →
BACKUP_REMOTE 로 off-host rsync → /health 의 db_backup. 지금까지 백업은
배포 직전에만 찍혔고(=RPO 가 배포 주기) 원본과 같은 디스크에 있었다.
검증
- tests/ 24건(KST cron 산술, call 적격성, 글+call 원자성). CI·deploy.sh 에
pnpm test.
- tsconfig 가 scripts/ 를 제외하고 있었다 — 20개 스크립트가 각자 하던
process.env.NODE_ENV 대입이 next 의 readonly 선언과 충돌해서. 즉 cron
스크립트 24개가 CI typecheck 를 한 번도 안 거쳤다. lib/script-env.ts 로
모으고 제외를 풀었다(중복 300줄 제거).
문서
- README 의 "승인 1명·자기승인 금지" 는 실제 ruleset 과 달랐다. 현재 상태를
사실대로 적고, 닫으려면 쓸 명령을 함께 뒀다. 설정은 바꾸지 않았다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
2026-08-20 운영 리포트 7건을 코드에 대조해 실제 결함만 고쳤습니다.
리포트가 하나 틀렸습니다: "persona-post 가 stance enum 을 검증하지 않는다" 는 사실이 아닙니다 —
agree|disagree|observe화이트리스트가 이미 있었습니다. 진짜 결함은 순서였습니다.보안 (P1)
next16.2.4 → 16.2.12. 공개 권고 22건(App Router Server Component DoS 포함)의 패치 하한이 16.2.11 이었습니다. 미사용nanoid직접 의존 제거.pnpm audit30건(high 17) → 7건. 남은 7건은 전부 next 가 스스로 핀한 postcss 8.4.31 · nanoid 3 · sharp 0.34.5 이고 이 앱에서 도달 불가입니다 —next/image를 어디서도 import 하지 않고images.remotePatterns도 없어/_next/image는 동일 출처만 받으며, postcss 가 파싱하는 CSS 는 우리 Tailwind 입력뿐입니다. 이유와 함께pnpm-workspace.yaml의auditConfig에 적고pnpm audit --audit-level=high를 CI 게이트로 넣었습니다 — next 를 올릴 때마다 그 목록을 지우고 다시 봐야 합니다.--prod가 아닌 이유: 여기선 devDependencies 가 운영에서 돕니다(모든 cron 앱이./node_modules/.bin/tsx).Trackable call 무결성 (P1)
글을 먼저 쓰고 → 가격을 부르고 → call 삽입을
catch {}로 삼키던 순서를 뒤집었습니다.createPostWithCall)으로. 둘 다 남거나 둘 다 없습니다.callDirectionFor가agree|disagree만 방향으로 인정합니다. 이전엔 예상 밖 stance 문자열이 조용히downcall 이 됐습니다 (잠복 버그).배포 (P1 / P2)
dump.pm2가 가리키는 디렉터리를 지우면 재부팅 때 아무것도 안 올라옵니다. 배포 자체는 실패로 기록하지 않습니다 — 새 릴리스는 살아서 health 를 통과했으니까요.pnpm test를 돌립니다(--force·CI 무판정 경로 대비).관측 (P2)
audit-auto가 에러 결과를 완료로 세지 않습니다 → 다음 실행에서 재시도. 실패한 질의만 골라 재시도하므로 성공한 답변은 유지됩니다.audit_cron)으로. 인용률은 여전히 roll-up 밖입니다 — 0% 는 재시작으로 안 고쳐지고?strict=1은 모니터에 물려 있으니까요. 94일 정체가 아무에게도 안 알려지던 이유가 "audit 이 통째로 빠져 있어서" 였습니다.재해복구 (P1)
scripts/backup-db.ts— 매일 03:00 KST: 스냅샷(WAL 이라cp아님) →PRAGMA integrity_check→BACKUP_REMOTE로 off-host rsync →/health의db_backup. 지금까지 백업은 배포 직전에만 찍혔고(= RPO 가 배포 주기) 원본과 같은 디스크에 있었습니다.검증 (P1)
tests/24건 — KST cron 산술, call 적격성, 글+call 원자성. transaction 을 빼는 mutation 으로 실제로 실패하는 걸 확인했습니다.scripts/를 제외하고 있었습니다. 20개 스크립트가 각자 하던process.env.NODE_ENV =대입이 next 의 readonly 선언과 충돌해서입니다. 즉 cron 스크립트 24개가 CI typecheck 를 한 번도 안 거쳤습니다 — 이 레포의 사고가 거의 다 거기서 났는데도.lib/script-env.ts하나로 모으고 제외를 풀었습니다(중복 300줄 제거).문서
README 의 "승인 1명·자기승인 금지" 는 실제 ruleset 과 달랐습니다(
required_approving_review_count: 0,require_last_push_approval: false). 현재 상태를 사실대로 적고, 닫으려면 쓸 read-modify-writegh명령을 함께 뒀습니다. 설정은 바꾸지 않았습니다.머지 후 할 일
배포 직후
?strict=1이 한 번 503 이 됩니다.db_backup은 03:00 KST 첫 실행 전까지 heartbeat 이 없어 fail(정확한 판정 — 백업이 아직 없음),audit_cron은 94일 정체라 fail. 전자는 서버에서 한 번 돌리면 사라집니다:🤖 Generated with Claude Code