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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# 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은 배포 후 작성.
|
||||
Reference in New Issue
Block a user