HMM 포털 URL 토큰 제거와 HttpOnly 쿠키 인증 설계 (#709)
상태: 구현·배포·검증 완료 대상:
https://hmm.cloud-handson.com브랜치:hmm-backoffice
문제
현재 Streamlit 로그인 유지 기능은 서명된 토큰을 poc4_remember query parameter에 저장한다.
토큰이 암호화된 비밀번호는 아니더라도 유효 기간 동안 인증 수단으로 작동하므로 다음 위치에 남을 수
있다.
- 브라우저 주소와 방문 기록
- Nginx·상위 프록시 access log
- 사용자가 복사한 링크와 화면 캡처
- 외부 링크 이동 시 Referer
인증 수단은 URL에 포함하지 않는다. 기존 query-token 코드를 삭제하고 기존 서명 secret을 회전해 과거 URL을 즉시 무효화한다.
목표
- 로그인은
POST /auth/login으로만 처리한다. - 인증 상태는
Secure,HttpOnly,SameSite=Lax,Path=/쿠키에만 둔다. - Nginx가 모든 Streamlit HTTP·WebSocket 요청 전에 쿠키를 검증한다.
- Streamlit은 외부 요청 헤더가 아니라 Nginx가 덮어쓴 내부 사용자 헤더만 사용한다.
- 로그아웃은 쿠키를 만료시키고 로그인 화면으로 돌아간다.
- 로그인·로그아웃 이후 주소에 토큰이나 자격 증명이 남지 않는다.
구성
Browser
├─ GET /auth/login ────────────────┐
├─ POST /auth/login (ID/password) │
└─ Cookie: __Host-HMM_PORTAL_SESSION
▼
Nginx :443
├─ /auth/* ───────────────► auth_gateway.py :8621
└─ /* + auth_request ──────► /auth/check
├─ 204 + X-Auth-User ─► Streamlit :8622
└─ 401 ───────────────► /auth/login
인증 서비스와 Streamlit은 모두 127.0.0.1에만 바인딩한다. 외부에서 인증 사용자 헤더를
보내더라도 Nginx가 auth_request 결과로 값을 덮어쓴다.
쿠키
| 항목 | 값 |
|---|---|
| 이름 | __Host-HMM_PORTAL_SESSION |
| 속성 | Secure; HttpOnly; SameSite=Lax; Path=/ |
| 기본 로그인 | 브라우저 세션 쿠키, 서버 토큰 만료 12시간 |
| 로그인 유지 | Max-Age=604800, 서버 토큰 만료 7일 |
| 형식 | version, user, issued-at, expiry, nonce를 담은 base64url payload + HMAC-SHA256 |
| 서명키 | POC4_LOGIN_COOKIE_SECRET, Git·로그 미기록 |
__Host- 접두사는 Secure, Path=/, Domain 미지정 조건을 강제해 하위 도메인의 쿠키
주입 범위를 줄인다.
로그인 보호
- PBKDF2 비밀번호 해시는 기존
POC4_LOGIN_PASSWORD_PBKDF2를 사용한다. - 로그인 GET에서 10분 유효한 일회용 CSRF 쿠키와 hidden 값을 발급한다.
- 로그인 POST는 CSRF 두 값을 상수 시간 비교한 뒤 자격 증명을 확인한다.
- 오류 메시지는 사용자 존재 여부와 비밀번호 실패를 구분하지 않는다.
- 요청 body와 필드 길이를 제한한다.
- 실패 횟수는 IP별 짧은 시간 창에서 제한한다.
- 인증 응답에는
Cache-Control: no-store와 보안 헤더를 설정한다. - 서비스 로그에는 query string, 쿠키, 비밀번호, 토큰을 기록하지 않는다.
Streamlit 변경
poc4_remember상수, 생성, 복원, query 정리 코드를 삭제한다.- Streamlit 내부 로그인 유지 로직을 삭제한다.
st.context.headers["X-HMM-Authenticated-User"]가 설정된 경우에만 포털 세션을 활성화한다.- 기대 사용자와 프록시 사용자 값은 상수 시간 비교한다.
- 로그아웃 UI는
/auth/logout으로 이동해 쿠키를 만료시킨다. - 신뢰 헤더가 없으면 자격 증명 폼 대신 인증 게이트웨이 설정 오류만 표시한다.
배포
- 인증 서비스 소스와 systemd unit을
/opt/hmm-poc4에 배포한다. - 새
POC4_LOGIN_COOKIE_SECRET을 root 소유 환경 파일에 추가하고 기존POC4_LOGIN_REMEMBER_SECRET은 제거한다. - 인증 서비스를
127.0.0.1:8621에서 시작한다. - Nginx 설정에
/auth/*, 내부/auth/check,auth_request를 적용한다. - Streamlit 소스를 배포하고 서비스를 재시작한다.
nginx -t, 서비스 상태, 로그인·쿠키·WebSocket·로그아웃을 검증한다.
완료 검증
- 기존
?poc4_remember=<old-token>요청이 인증되지 않고 로그인 화면으로 이동한다. - 로그인 POST 응답의
Location은/이고 URL에 토큰이 없다. - 세션 쿠키에
Secure,HttpOnly,SameSite=Lax,Path=/가 모두 있다. - 조작·만료 쿠키는
/auth/check에서 401이다. - 인증 쿠키가 없으면 Streamlit asset·WebSocket을 포함한 보호 경로를 사용할 수 없다.
- 로그인 후 포털 주요 탭, MCP 설정, 사용자 전환이 정상 동작한다.
- 로그아웃 후 쿠키가 만료되고 보호 경로가 다시 로그인 화면으로 이동한다.
롤백
변경 전 Nginx 설정, Streamlit 소스, 환경 파일을 타임스탬프 백업한다. 장애 시 이 세 파일을 복구하고 인증 서비스를 중지한다. 롤백을 해도 query-token 구현은 재활성화하지 않으며, 임시로 포털 접근을 차단하는 쪽을 우선한다.
배포 검증 결과
2026-07-23 운영 배포에서 다음을 확인했다.
hmm-portal-auth.service,poc4-streamlit.service,nginx모두active- 기존 query-token 서명키 제거·회전, 환경 백업의 이전 서명키도 제거
/미인증 요청:/auth/login으로 이동/?poc4_remember=retired-token: 인증되지 않고/auth/login으로 이동하며 query 제거- 로그인 페이지: URL token 없음, CSRF cookie는
Secure; HttpOnly; SameSite=Strict - 포털 session cookie:
Secure; HttpOnly; SameSite=Lax; Path=/ - 브라우저
document.cookie에서 session cookie를 읽을 수 없음 - 인증 후 URL:
https://hmm.cloud-handson.com/, query 없음 - 아키텍처·시나리오·감사로그·보안관리 탭 및 MCP endpoint 설정 표시 정상
- 로그아웃 후 session cookie 제거와 로그인 화면 복귀 확인
- Python 단위·HTTP 통합 테스트 21건 통과, 선택적 Streamlit runtime 테스트 1건 skip
- 브라우저 page error 0건, console error 0건
상세 증거는 docs/reports/2026-07-23-hmm-portal-cookie-auth-verification.md에 기록한다.