--- { "schema": "gitea-agent.requirement/v1", "id": "REQ-001", "title": "Dashboard 관리자 인증", "revision": "2.0", "status": "approved", "depends_on": [], "applies_to": ["dashboard"], "language": "ko" } --- # REQ-001 — Dashboard 관리자 인증 ## 1. 목적 Dashboard의 금융 기능 앞에 단일 fail-closed 관리자 인증 경계를 두고, 유효한 인증 cookie가 같은 browser profile의 새 연결에서 복원되도록 한다. ## 2. 배경 Issue #1의 관리자 인증을 Issue #6에서 persistent signed cookie와 명시적 logout으로 확장한다. 기존 요구사항의 정체성, 승인 범위, Issue #1과 구현 PR #3의 추적성을 유지하며 revision 2.0으로 갱신한다. ## 3. 범위 ### 포함 - Dashboard 전 기능 앞의 인증 gate와 인증 전 DB 차단 - `JARCA_DASHBOARD_ADMIN_ID`, `JARCA_DASHBOARD_ADMIN_PASSWORD`, `JARCA_DASHBOARD_SIGNING_SECRET` 기반 server-only 검증 - constant-time credential 비교, 일반 오류, 실패당 최소 1초 지연 - same-origin HTTPS login, cleanup, logout route - `Secure`/`HttpOnly`/`SameSite=Lax`/`Path=/` signed cookie와 24시간·30일 absolute TTL - 고정된 비민감 role subject와 설정 credential에 묶인 서명 검증 - invalid, expired, tampered, malformed, configuration-failure cookie의 cleanup 및 matching deletion - password, signing-secret, admin-ID 변경에 따른 기존 token 거부 - HTTPS, proxy, Kubernetes/Compose 운영 제약과 민감정보 비노출 ### 제외 - 다중 사용자, RBAC, OAuth/OIDC, SSO, MFA, CAPTCHA, 계정 잠금 - 외부 인증 proxy로 책임 이전 - sliding expiration, DB-backed session/token registry, per-token revocation - MCP endpoint 인증 변경, SQLite schema/data migration - 다른 browser profile·기기 간 로그인 공유 ## 4. 용어와 기준 - **role subject**: token의 `sub`에 고정 기록하는 비민감 값 `dashboard-admin`; 관리자 ID나 credential이 아니다. - **server-only binding**: signing secret과 현재 설정 credential을 서버에서만 읽어 key를 파생하며 browser에 값을 보내지 않는 방식. - **cleanup**: 인증 실패 cookie를 같은 origin의 cleanup route에서 matching name/path/security 속성으로 삭제하는 절차. - **canonical 근거**: Issue #1, Issue #6, 구현 PR #3 및 #7. ## 5. 요구사항 ### REQ-001.01 — 공통 인증 경계 모든 DB path, page dispatch, gateway, upload, import보다 먼저 설정과 cookie를 검증하고, 실패하면 로그인 화면만 표시한다. **영향 범위** - 인터페이스: `dashboard-auth-gate` - 영속성: 없음 - 후속 사용 주체: Dashboard page와 gateway - 마이그레이션: 없음 - 호환성: 인증 후 기존 page/query 동작 유지 - 배포 전제 조건: 유효한 세 환경변수 ### REQ-001.02 — 설정과 server-only binding 세 환경변수만 사용하고 누락·빈 값·read error·짧은 signing secret이면 fail-closed한다. signing key는 서버에서 configured credential과 secret으로만 파생한다. **영향 범위** - 인터페이스: `dashboard-auth-config`, `dashboard-auth-cookie` - 영속성: 없음 - 후속 사용 주체: 인증 verifier - 마이그레이션: 없음 - 호환성: Streamlit secrets/fallback 미사용 - 배포 전제 조건: secret manager 주입 ### REQ-001.03 — Credential과 민감정보 비노출 ID와 password는 constant-time 비교한다. credential, secret, signing material, token, raw cookie와 Set-Cookie 값은 UI, URL, log, 문서, session state에 기록하지 않는다. **영향 범위** - 인터페이스: login form, event log - 영속성: 없음 - 후속 사용 주체: 운영자와 감사 도구 - 마이그레이션: 없음 - 호환성: 비민감 correlation ID만 유지 - 배포 전제 조건: redacted logging ### REQ-001.04 — Login 실패 계약 ID 또는 password가 틀리면 동일한 일반 오류와 최소 1초 지연을 적용하고 상태를 변경하지 않는다. 성공에는 지연을 적용하지 않는다. **영향 범위** - 인터페이스: `POST /api/auth/login` - 영속성: 없음 - 후속 사용 주체: browser - 마이그레이션: 없음 - 호환성: 기존 login form 유지 - 배포 전제 조건: 외부 rate limiting ### REQ-001.05 — Persistent token 성공 login은 기본 24시간, remember-me 선택 시 30일의 absolute TTL token을 발급한다. `iat`와 `exp`는 고정하며 활동·rerun·복원으로 연장하지 않는다. `sub`는 항상 `dashboard-admin`이다. **영향 범위** - 인터페이스: `dashboard-auth-cookie` - 영속성: browser cookie만 - 후속 사용 주체: 새 Streamlit session - 마이그레이션: 기존 session boolean에서 cookie로 전환 - 호환성: 같은 browser profile 복원 - 배포 전제 조건: HTTPS ### REQ-001.06 — Cookie 보안 속성 Cookie 이름은 `__Host-jarca_dashboard_auth`이며 `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`, no `Domain`을 사용한다. 일반 요청은 재발급하지 않는다. **영향 범위** - 인터페이스: HTTP response와 browser - 영속성: browser cookie - 후속 사용 주체: browser/Streamlit handshake - 마이그레이션: 없음 - 호환성: host-only cookie - 배포 전제 조건: 공개 origin HTTPS ### REQ-001.07 — Invalid cookie cleanup 없음·malformed·oversized·tampered·expired·claim-invalid·rotation-invalid 또는 configuration-failure cookie는 보호 자원에 접근하기 전에 거부한다. 같은 origin cleanup route를 통해 동일 name/path/security 속성과 `Max-Age=0` deletion을 반환한 뒤 unauthenticated page를 표시한다. **영향 범위** - 인터페이스: `GET /api/auth/cleanup`, `dashboard-auth-gate` - 영속성: 서버 session/registry 없음 - 후속 사용 주체: browser - 마이그레이션: 없음 - 호환성: stale cookie가 남지 않음 - 배포 전제 조건: same-origin HTTPS ### REQ-001.08 — Login, cleanup, logout Login과 logout은 trusted same-origin HTTPS POST만 허용하고 cleanup GET은 same-origin HTTPS `Referer`를 검증한다. logout은 명시적으로 matching deletion을 반환한다. cleanup과 logout은 금융 DB를 변경하지 않는다. **영향 범위** - 인터페이스: `POST /api/auth/login`, `GET /api/auth/cleanup`, `POST /api/auth/logout` - 영속성: 없음 - 후속 사용 주체: browser - 마이그레이션: 없음 - 호환성: root redirect 유지 - 배포 전제 조건: proxy가 Host/Origin/Proto 보존 ### REQ-001.09 — Rotation과 무상태성 Token claim은 `v`, 고정 `sub`, `iat`, `exp`, `jti`만 허용한다. password, signing secret 또는 admin ID 변경은 기존 token을 거부한다. per-token revocation이나 DB session은 만들지 않는다. **영향 범위** - 인터페이스: token verifier - 영속성: revocation/session 저장소 없음 - 후속 사용 주체: 운영 rotation 절차 - 마이그레이션: 없음 - 호환성: 전체 무효화는 rotation으로 수행 - 배포 전제 조건: 모든 replica 동일 설정 ### REQ-001.10 — 보호 범위와 운영 인증 후 기존 page, query parameter, gateway와 MCP 동작을 유지한다. Compose/Kubernetes는 실제 secret을 저장하지 않으며 HTTPS, forwarded-header allowlist와 rate limit을 적용한다. **영향 범위** - 인터페이스: Dashboard 운영 진입점 - 영속성: 기존 SQLite만 - 후속 사용 주체: 운영 배포 - 마이그레이션: DB migration 없음 - 호환성: MCP endpoint는 별도 계약 - 배포 전제 조건: TLS와 제한된 proxy CIDR ### REQ-001.11 — 관찰 가능성 설정 오류, 인증 필요, token 수락, login 성공/실패/reject, cleanup, logout event와 비민감 correlation ID만 기록한다. 민감 값과 구체적 검증 원인은 기록하지 않는다. **영향 범위** - 인터페이스: event log - 영속성: 로그에 비민감 메타데이터만 - 후속 사용 주체: 운영 관찰 도구 - 마이그레이션: 없음 - 호환성: 일반 오류 메시지 유지 - 배포 전제 조건: redaction 정책 ## 6. 제약과 불변조건 - `sub`는 비민감 고정 role 값이며 credential에서 파생하거나 관리자 ID를 담지 않는다. - credential, secret, token, cookie 값은 저장·출력·문서화하지 않는다. - 인증 성공 전 DB/gateway/page dispatch는 실행하지 않는다. - 만료는 absolute이며 활동으로 연장하지 않는다. - invalid cookie cleanup과 explicit logout의 deletion은 발급 cookie와 matching scope를 사용한다. - 서버는 per-token revocation과 DB-backed session을 유지하지 않는다. ## 7. 인수 기준 ### REQ-001.01-AC01 — 인증 전 차단 **판정 기준** 모든 인증 거부 경로에서 DB, gateway, page dispatch가 실행되지 않는다. **검증** AppTest와 gate ordering 테스트에서 database access sentinel이 호출되지 않음을 확인한다. **검증 분류** - 수준: integration - 백엔드: Streamlit AppTest - 케이스: missing config, no cookie, invalid cookie **상태 불변조건** 거부 시 금융 상태와 DB는 변경되지 않는다. ### REQ-001.02-AC01 — 설정 fail-closed **판정 기준** 세 설정 중 하나라도 유효하지 않으면 일반 설정 오류로 인증을 거부한다. **검증** 설정별 누락·빈 값·짧은 secret 테스트를 실행한다. **검증 분류** - 수준: unit/integration - 백엔드: pytest - 케이스: 각 환경변수 누락, 빈 값, short secret **상태 불변조건** cookie 발급과 보호 자원 접근이 없다. ### REQ-001.03-AC01 — 민감정보 비노출 **판정 기준** credential, secret, token, cookie가 UI·URL·log·session state에 나타나지 않는다. **검증** 로그·응답·session state와 문서를 redaction 검색한다. **검증 분류** - 수준: static/integration - 백엔드: pytest, ruff, repository search - 케이스: 성공·실패 login과 invalid cookie **상태 불변조건** 비민감 correlation ID 외 인증 비밀 상태가 없다. ### REQ-001.03-AC02 — 두 credential의 constant-time 비교 **판정 기준** 제출 ID와 password는 각각 `hmac.compare_digest`로 비교하며, 한 필드가 일치하지 않아도 다른 필드의 비교를 생략하지 않는다. **검증** `tests/test_dashboard_authentication.py::test_verify_credentials_uses_constant_time_comparison_for_both_fields`에서 성공 및 두 필드가 모두 잘못된 경우에 두 비교의 호출 순서와 인자를 계측한다. **검증 분류** - 수준: unit - 백엔드: pytest monkeypatch - 케이스: 정상 credential, 잘못된 ID와 password **상태 불변조건** 일반 동등 비교나 한 필드 실패에 따른 short-circuit를 사용하지 않는다. ### REQ-001.04-AC01 — 실패 지연과 일반 오류 **판정 기준** 잘못된 ID와 잘못된 password가 같은 일반 오류를 반환하고 각각 최소 1초 지연된다. **검증** 두 실패 경로의 elapsed time과 response를 측정한다. **검증 분류** - 수준: integration - 백엔드: pytest/HTTP client - 케이스: wrong ID, wrong password **상태 불변조건** cookie·DB·gateway 상태가 변하지 않는다. ### REQ-001.05-AC01 — Absolute TTL과 고정 subject **판정 기준** 기본 24시간과 remember-me 30일 token의 `iat`, `exp`, `sub=dashboard-admin`이 고정된다. **검증** token claim, TTL, rerun/non-sliding 테스트를 실행한다. **검증 분류** - 수준: unit/integration - 백엔드: pytest - 케이스: normal, remember-me, activity, reconnect **상태 불변조건** 복원은 token을 갱신하지 않는다. ### REQ-001.06-AC01 — Hardened cookie **판정 기준** 발급 cookie가 이름·Secure·HttpOnly·SameSite·Path·Domain 계약을 모두 만족한다. **검증** 실제 HTTP `Set-Cookie`를 파싱하고 속성을 검사한다. **검증 분류** - 수준: integration - 백엔드: Starlette TestClient와 loopback HTTPS - 케이스: login normal/remember **상태 불변조건** Domain attribute가 없고 평문 전송에서 발급되지 않는다. ### REQ-001.07-AC01 — Invalid cookie cleanup **판정 기준** invalid/expired/tampered/config-failure cookie는 보호 자원 전에 거부되고 same-origin cleanup route의 matching deletion 후 unauthenticated page가 표시된다. **검증** 실제 browser/HTTP 흐름에서 각 invalid cookie와 cleanup response를 확인한다. **검증 분류** - 수준: integration/browser - 백엔드: HTTPS probe와 Streamlit browser evidence - 케이스: malformed, expired, tampered, wrong claims, config failure **상태 불변조건** cleanup은 DB를 변경하지 않으며 stale cookie가 남지 않는다. ### REQ-001.08-AC01 — Explicit logout **판정 기준** logout POST가 matching `Max-Age=0` deletion을 반환하고 다음 접근이 unauthenticated다. **검증** login 후 logout response와 fresh request를 확인한다. **검증 분류** - 수준: integration/browser - 백엔드: HTTPS probe - 케이스: same-origin logout, cross-origin rejection **상태 불변조건** 금융 DB와 token registry는 변경되지 않는다. ### REQ-001.09-AC01 — Rotation과 무상태성 **판정 기준** configured credential 변경은 기존 token을 거부하며 per-token revocation/DB session 구현이 없다. **검증** rotation 테스트와 source/schema/diff 검색을 실행한다. **검증 분류** - 수준: unit/static - 백엔드: pytest와 repository search - 케이스: password, signing secret, admin ID rotation **상태 불변조건** 검증은 현재 설정과 서명만 사용한다. ### REQ-001.10-AC01 — 운영 제약 **판정 기준** README와 Kubernetes 문서에 HTTPS, proxy allowlist, rate limit, secret 주입, rotation과 stateless logout이 기록된다. **검증** 문서 검색과 deployment manifest 검토를 실행한다. **검증 분류** - 수준: documentation/static - 백엔드: repository search - 케이스: README, deploy/k8s/README.md **상태 불변조건** 실제 credential·secret 값이 문서와 manifest에 없다. ### REQ-001.10-AC02 — 인증 후 기존 페이지 호환성 **판정 기준** 유효한 Dashboard 인증 cookie로 인증하면 기존 page query parameter를 유지한 채 요약, 자산/마감, 분류 규칙, 확인필요 큐와 월정산 핵심 페이지가 예외 없이 예상 내용을 렌더링한다. **검증** `tests/test_dashboard_app.py::test_streamlit_apptest_core_pages_use_synthetic_fixture_db`와 `tests/test_dashboard_authentication_app.py::test_valid_cookie_allows_dashboard_and_is_not_stored_in_session_state`를 실행한다. **검증 분류** - 수준: regression integration - 백엔드: Streamlit AppTest, synthetic SQLite - 케이스: 인증된 핵심 page query와 기존 gateway **상태 불변조건** 기존 SQLite schema, page query와 gateway contract를 변경하지 않는다. ### REQ-001.10-AC03 — MCP 인증 범위 분리 **판정 기준** Dashboard cookie 인증은 MCP endpoint의 인증을 의미하지 않으며 MCP server 진입점, transport와 port를 변경하지 않는다. **검증** Base-to-head diff에서 MCP 인증·진입점·transport·port를 담당하는 `jarca_ledger_mcp/server.py`, `jarca_ledger_mcp/adapters/mcp/**`, `tests/test_mcp_http_runtime.py`의 불변성을 확인하고 기존 stdio, streamable HTTP와 invalid transport 회귀 test를 실행한다. **검증 분류** - 수준: static/regression - 백엔드: Git diff, pytest - 케이스: MCP source, stdio, streamable HTTP, invalid transport **상태 불변조건** Dashboard와 MCP 인증 경계를 결합하지 않으며 MCP 인증 상태를 변경하지 않는다. ### REQ-001.11-AC01 — 안전한 관찰성 **판정 기준** 허용 event와 correlation ID만 기록되고 민감 값·구체적 검증 원인은 기록되지 않는다. **검증** 로그 테스트와 redaction 검색을 실행한다. **검증 분류** - 수준: unit/static - 백엔드: pytest와 repository search - 케이스: config, login, cleanup, logout success/failure **상태 불변조건** 로그에서 credential·secret·token·cookie가 복원되지 않는다. ## 8. 오류 및 충돌 계약 동일 요청에서 설정 오류가 credential 오류보다 우선하며, 모든 인증 실패는 보호 자원 전에 종료된다. invalid cookie는 cleanup deletion을 시도하고, cleanup route가 거부되거나 실패해도 보호 자원은 열지 않는다. cross-origin/plain HTTP 요청은 403, malformed request는 400이다. 동시 login은 application lock 없이 독립 처리한다. ## 9. 데이터 및 인터페이스 계약 Token은 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti`와 서명만 가지며 browser cookie에만 저장된다. 세 환경변수는 server process에서만 읽는다. 상세 HTTP, cookie, cleanup, logout 계약은 `docs/interface-contract.md`에 정의한다. ## 10. 하위 호환성 Issue #1의 Dashboard-only 범위, 기존 page/query/gateway/MCP 동작과 구현 PR #3의 추적성을 유지한다. session-only 인증은 persistent cookie로 대체하며 SQLite schema와 MCP transport는 변경하지 않는다. ## 11. 추적성 - 근거 이슈: `#1`, `#6` - 구현 PR: `#3`, `#7` - 관련 설계 문서: `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flow.md`, `README.md`, `deploy/k8s/README.md` - 관련 요구사항: 없음