- render_service_token_result no longer masks the token field; plaintext tokens are SHA-256 hashed in DB and irrecoverable once lost, so issue/ rotate must reveal them once at issuance (matches interface-contract.md) - Remove now-unused _mask_payload helper; keep mask_token_text for the auxiliary masked_token identifier line - Add one-time reveal warning to issue/rotate table output - 3 regression tests: JSON plaintext assertion, render unit test, table warning - Hotfix Exception Path record: docs/hotfixes/hotfix-001 - Total tests: 369 -> 371 passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.5 KiB
5.5 KiB
HOTFIX-001: service-token issue/rotate 평문 토큰 마스킹 결함
- Status: verified (배포 대기)
- Date: 2026-05-22
- Severity: High
- Area: CLI / 인증 (
pfhub service-token) - Source: portfolio-hub 운영팀 이슈티켓 (대상 버전 0.1.0, commit
58899b1) - Path: Hotfix Exception Path (CLAUDE.md — Requirements/Delivery 경량화, 4개 필수 항목 충족)
1. 최소 요구사항 정의
증상
pfhub service-token issue/rotate가 새 토큰을 발급하지만 출력 시 평문 토큰을 항상 마스킹(ySusw9****Wa3w)한다.--format json을 지정해도 마스킹되며, 마스킹 해제 옵션이 없다.- 결과적으로 새로 발급된 토큰의 평문을 확인할 방법이 전혀 없다.
원인
app/cli/core/output.pyrender_service_token_result()가 출력 포맷 분기 이전에 무조건_mask_payload()로token필드를 마스킹._mask_payload()는token키를 가진 응답에만 동작 →token필드를 가진 응답은issue/rotate둘뿐 → 마스킹이 토큰이 반드시 필요한 두 명령에서만 작동.--format분기보다 마스킹이 먼저 일어나table/json모두 마스킹됨.
영향
- 신규 토큰 사용 불가: 발급/회전 후 평문을 알 수 없어 클라이언트·데몬에 설정 불가.
- 데이터 소실(복구 불가): 토큰은
app/core/security.py:45hash_token()(SHA-256) +token_prefix만 DB 저장. 평문은 명령 실행 중 메모리에만 존재. - 운영 리스크:
rotate는 기존 토큰을 폐기하고 새 토큰을 발급 → 이 버그 상황에서 회전 시 이전 토큰 무효화 + 새 토큰 미상 상태. - 명세 정합성:
docs/interface-contract.mdL83/L494 는 "토큰은 발급 시 1회 노출, 이후 마스킹"을 이미 명시 — 현 구현이 baseline 계약을 위반. 명세 결함이 아닌 구현 버그.
성공 조건
- 결정된 노출 방식: 기본 1회 노출 (운영팀 제안 채택, 2026-05-22 확정).
- SC-1:
pfhub --format json service-token issue …→ JSONtoken필드에 평문 토큰이 그대로 포함된다 (마스킹 문자*없음). - SC-2:
pfhub --format json service-token rotate …→ 동일하게 평문token포함. - SC-3:
table출력(issue/rotate) → 평문 토큰 1회 출력 + "이 토큰은 다시 표시되지 않습니다" 경고 표기. - SC-4:
masked_token:보조 표시는 식별용으로 유지(table 경로).mask_token_text()자체는 보존. - SC-5:
list/rebind/revoke출력은 회귀 없이 기존 동작 유지(이들 응답에는token필드 없음).
2. 최소 검증
- 재현/회귀 테스트:
tests/test_cli.py::test_cli_service_token_issue_supports_json(확장) — JSONtoken필드가 평문임을 단언 ("*" not in token+token.startswith(stored.token_prefix)) (SC-1). ✅tests/test_cli.py::test_render_service_token_result_does_not_mask_token(신규 단위) —render_service_token_result()가token을 평문으로 렌더링 (json/table 양쪽, 출력에*부재). rotate 경로는 동일 함수 호출이므로 본 단위 테스트가 SC-2 커버. ✅tests/test_cli.py::test_cli_service_token_issue_table_shows_reveal_warning(신규) — table 출력에 token_prefix + "다시 표시되지 않습니다" 경고 문구 (SC-3). ✅- 회귀: 전체 pytest suite —
list/rebind/revoke테스트 무변동 (SC-5). ✅
- 결과:
.venv/bin/python -m pytest tests/test_cli.py -k "service_token or render_service_token"→ 15 passed.venv/bin/python -m pytest(전체) → 371 passed in 11.60s (BAT-013 직후 369 baseline 대비 +2 신규, 회귀 0)
- 적용 변경:
app/cli/core/output.py—_mask_payload()함수 제거(발급 경로 외 사용처 없음),render_service_token_result()가 마스킹 없이 렌더링.mask_token_text()는 보존(masked_token:보조 표시에서 계속 사용).app/cli/commands/service_tokens.py—issue/rotatenon-json 출력에 "이 토큰은 다시 표시되지 않습니다" 1회 노출 경고 추가.
3. 배포 전 롤백 계획
- 변경 범위:
app/cli/core/output.py,app/cli/commands/service_tokens.py2개 파일. DB 스키마·마이그레이션 변경 없음, HTTP API 변경 없음. - 롤백 방식: 코드 전용 변경이므로 직전 이미지/커밋(
58899b1)으로 재배포하면 즉시 원복. 데이터 마이그레이션 없음 → 롤백 시 데이터 리스크 없음. - 롤백 트리거: 배포 후
service-token계열 명령 비정상 동작, CLI 기동 실패. - 주의: 롤백은 버그 동작으로 복귀를 의미하므로, 롤백 시
issue/rotate사용 보류 권고가 재적용된다.
4. 배포 후 Postmortem
- (배포 후 작성 — 운영팀)
- 환류 항목 후보:
- 발급 명령 출력에 대한 자동 회귀 부재 → 본 hotfix로 추가됨. 다른 secret-노출 경로(login 토큰 등) 동일 패턴 점검 후속.
interface-contract.mdbaseline 계약과 구현 간 drift 감지 수단 부재 → 후속 Discovery 후보.- 이미 회전되어 평문을 잃은 운영
admin토큰(540e005a…)은 본 hotfix 배포 후 재회전 필요.
Change Log
- 2026-05-22: HOTFIX-001 착수. 운영팀 이슈티켓 접수 → 코드 검증 → Hotfix Exception Path 채택 → 노출 방식 "기본 1회 노출" 확정.
- 2026-05-22: 코드 수정 + 회귀 테스트 3건 반영, 전체 371 passed. Status → verified (배포 대기). 배포는 운영팀이 수행하며 Postmortem은 배포 후 작성.