Files
portfolio-hub/docs/hotfixes/hotfix-001_20260522_service-token-plaintext-masking.md
T
genichinandClaude Opus 4.7 3a78e5c138 fix: reveal plaintext service token on issue/rotate (HOTFIX-001)
- 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>
2026-05-22 17:36:05 +09:00

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.py render_service_token_result() 가 출력 포맷 분기 이전에 무조건 _mask_payload() 로 token 필드를 마스킹.
  • _mask_payload() 는 token 키를 가진 응답에만 동작 → token 필드를 가진 응답은 issue/rotate 둘뿐 → 마스킹이 토큰이 반드시 필요한 두 명령에서만 작동.
  • --format 분기보다 마스킹이 먼저 일어나 table/json 모두 마스킹됨.

영향

  • 신규 토큰 사용 불가: 발급/회전 후 평문을 알 수 없어 클라이언트·데몬에 설정 불가.
  • 데이터 소실(복구 불가): 토큰은 app/core/security.py:45 hash_token()(SHA-256) + token_prefix 만 DB 저장. 평문은 명령 실행 중 메모리에만 존재.
  • 운영 리스크: rotate 는 기존 토큰을 폐기하고 새 토큰을 발급 → 이 버그 상황에서 회전 시 이전 토큰 무효화 + 새 토큰 미상 상태.
  • 명세 정합성: docs/interface-contract.md L83/L494 는 "토큰은 발급 시 1회 노출, 이후 마스킹"을 이미 명시 — 현 구현이 baseline 계약을 위반. 명세 결함이 아닌 구현 버그.

성공 조건

  • 결정된 노출 방식: 기본 1회 노출 (운영팀 제안 채택, 2026-05-22 확정).
  • SC-1: pfhub --format json service-token issue … → JSON token 필드에 평문 토큰이 그대로 포함된다 (마스킹 문자 * 없음).
  • 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 (확장) — JSON token 필드가 평문임을 단언 ("*" 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/rotate non-json 출력에 "이 토큰은 다시 표시되지 않습니다" 1회 노출 경고 추가.

3. 배포 전 롤백 계획

  • 변경 범위: app/cli/core/output.py, app/cli/commands/service_tokens.py 2개 파일. DB 스키마·마이그레이션 변경 없음, HTTP API 변경 없음.
  • 롤백 방식: 코드 전용 변경이므로 직전 이미지/커밋(58899b1)으로 재배포하면 즉시 원복. 데이터 마이그레이션 없음 → 롤백 시 데이터 리스크 없음.
  • 롤백 트리거: 배포 후 service-token 계열 명령 비정상 동작, CLI 기동 실패.
  • 주의: 롤백은 버그 동작으로 복귀를 의미하므로, 롤백 시 issue/rotate 사용 보류 권고가 재적용된다.

4. 배포 후 Postmortem

  • (배포 후 작성 — 운영팀)
  • 환류 항목 후보:
    • 발급 명령 출력에 대한 자동 회귀 부재 → 본 hotfix로 추가됨. 다른 secret-노출 경로(login 토큰 등) 동일 패턴 점검 후속.
    • interface-contract.md baseline 계약과 구현 간 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은 배포 후 작성.