fix(auth): throttle clients behind a named proxy separately, and do not count expired tokens - #1035
Merged
Merged
Conversation
…ot count expired tokens The failure throttle keyed on the TCP peer, which behind a reverse proxy is the proxy for every user, and it counted every 401, an expired token included. Ordinary expiry across users reached the limit and locked all of them out. KPUBDATA_BUILDER_TRUSTED_PROXIES names the proxies whose X-Forwarded-For is read, from the right, skipping trusted hops; without it the header is still never read. An expired token verified and is only old, so it no longer moves the count. Closes #1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL flagged the warning for writing the setting's text to the log. The value is an address, but a variable set by mistake to something else would be logged too. Log which entry was dropped instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL still flagged the warning: it reads a variable whose name contains 'trusted' as sensitive, and the name constant was a log argument. Only the entry's position is an argument now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yeongseon
approved these changes
Oct 5, 2026
yeongseon
left a comment
Collaborator
There was a problem hiding this comment.
diff 를 읽었고 CLEAN 이라 승인합니다.
X-Forwarded-For를 TCP peer 가 이름을 댄 프록시일 때에만, 그리고 오른쪽부터 읽습니다. 클라이언트가 써 보낸 왼쪽 부분에는 닿지 않고, 실제 소켓 테스트가 "접두를 바꿔도 새 버킷을 얻지 못한다" 와 "설정이 없으면 헤더가 아무것도 바꾸지 않는다" 를 고정합니다.- 만료 토큰은 세지 않되 실패 기록을 지우지도 않습니다. 판정은
AuthError.expired플래그로 하고 문장을 읽지 않습니다. - 잘못된 설정 항목은 값이 아니라 위치로만 로그에 남깁니다. code scanning 의 두 지적은 앞선 커밋에 달린 것으로 보이고 지금 체크는 통과합니다.
본문이 "정해 달라" 고 한 두 가지:
- compose 에 기본값을 넣지 않은 것은 맞습니다. 대역은 배포마다 다르고, 틀린 기본값은 헤더를 아무나 믿게 만듭니다. 배포 대상이 정해지면 그때
.env에 적습니다. - Cloudflare → Caddy 의
trusted_proxies는 문서에 적어 둔 것으로 충분합니다. Caddyfile 은 배포 대상이 정해질 때 같이 봅니다.
알아 둘 것으로 적은 비용 — 만료된 토큰 하나를 가진 쪽이 서명 검증을 스로틀 없이 반복시킬 수 있다 — 은 받아들입니다. 그 토큰은 IdP 가 서명한 것이어야 하므로 실제 사용자였던 쪽으로 한정됩니다.
…le-trusted-proxy # Conflicts: # CHANGELOG.md
…le-trusted-proxy # Conflicts: # CHANGELOG.md
yeongseon
pushed a commit
that referenced
this pull request
Oct 6, 2026
…token (#1058) ## 무엇을 `OIDC_ISSUER` 가 설정되고 `KPUBDATA_BUILDER_API_KEY` 가 없는 배포(다중 사용자 프로필)에서, 토큰 없는 요청의 401 문구를 고칩니다. - 전: `{"error": "api key not configured", "code": "unauthorized"}` - 후: `{"error": "sign-in required: send a bearer token", "code": "unauthorized"}` ## 왜 #1057 의 기동 스모크를 손으로 띄워 보다가 봤습니다. 그 배포에는 설정할 API 키가 없는데, 문구는 읽는 사람을 키 설정 쪽으로 보냅니다. `authenticate()` 가 Bearer 가 없으면 그대로 API 키 경로로 내려가기 때문입니다. ## 바뀌지 않는 것 - 상태 코드(401)와 `code`(`unauthorized`). Studio 는 `code` 로 분기합니다. 문구에 의존하는 곳은 Builder 계약·테스트·문서와 Studio `src` 에서 찾지 못했습니다(`grep "api key not configured"`). - OIDC 와 API 키를 함께 쓰는 배포: 키 경로가 그대로 판정합니다. - OIDC 가 없는 배포: `api key not configured` 그대로. - 인증 실패 집계(#1035)에는 손대지 않았습니다. ## 테스트 `tests/unit/test_oidc_auth.py::TestRequestWithoutAToken` 4건 — OIDC 전용에서 새 문구, API 키를 보내도 같은 답, 부정 2건(키도 받는 배포 / OIDC 없는 배포는 종전대로). ## 검증 - `test_oidc_auth.py`, `test_auth_throttle.py`, `test_signup_ledger.py`, `test_ephemeral_credentials.py` 124건 통과 - `ruff`, `mypy src` 통과 - 전체 단위 스위트는 로컬에서 돌리지 않았습니다. CI 에 맡깁니다. 인증 응답의 문구를 바꾸는 것이라 직접 머지하지 않고 리뷰를 기다립니다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1031
문제
인증 실패 스로틀은 클라이언트를 TCP peer 주소로 가린다. compose 가 싣는 리버스 프록시(Caddy) 뒤에서는 모든 사용자가 프록시의 주소로 도착해 한 버킷을 공유한다. 거기에 만료된 토큰도 401 이라 실패로 셌다. 그래서 사용자 몇 명의 평범한 토큰 만료만으로 한도에 닿고, 윈도가 지날 때까지 모두가 429
auth_throttled를 받는다.변경
1. 만료된 토큰은 세지 않는다.
app.py의 게이트가principal.status_code == 401 and not principal.expired일 때만 실패를 기록한다.expired는 #1023 에서AuthError가 만들어지는 자리(jwt.ExpiredSignatureError)에서 세운 플래그다 — 문장을 읽지 않는다. 만료 토큰은 실패 기록을 지우지도 않는다(성공이 아니므로).2. 이름을 댄 프록시 뒤에서만
X-Forwarded-For를 읽는다.KPUBDATA_BUILDER_TRUSTED_PROXIES(주소·CIDR 블록, 콤마 구분, 기본 미설정).::ffff:a.b.c.d)는 IPv4 로 맞춘다.auth_throttle.py의client_identity한 곳에 있고http.py는 그것을 부른다.3. 문서와 배선.
docs/deploy.md에 "리버스 프록시 뒤에서" 절,docs/deployment.md표,.env.app.example.docker-compose.prod.app.yml은 변수를 값 없이 통과만 시킨다.운영 설정에 대해 — 정해 주셔야 하는 것
.env.app.example은 "Cloudflare proxied DNS → Caddy" 구성을 말한다. Caddy 는 기본적으로 앞단을 신뢰하지 않고X-Forwarded-For를 자기가 본 peer(Cloudflare 주소)로 쓴다. 그 상태에서는 Builder 가 사용자 대신 Cloudflare 엣지 주소로 묶는다.ops/caddy/Caddyfile에는trusted_proxies가 없다. 이것은 문서에 적어 두기만 했고 Caddyfile 은 건드리지 않았다. (Caddy 의 이 기본 동작은 제 지식에 따른 것이고 이 저장소에서 프록시를 띄워 확인한 것은 아니다.)알아 둘 것
만료된(서명이 유효했던) 토큰 하나를 가진 쪽은 이제 그것을 스로틀 없이 반복해 보낼 수 있고, 그때마다 서명 검증 한 번의 CPU 가 든다. 모듈 docstring 이 스로틀의 두 번째 목적으로 든 바로 그 비용이다. 이슈의 결정(만료는 세지 않는다)을 따랐고, 위조·무효 토큰은 여전히 센다.
테스트
tests/unit/test_auth_throttle_proxy.py(새 파일):client_identity: 설정 없음 / peer 가 프록시가 아님 / 프록시가 알려 준 주소 / 클라이언트가 써 보낸 접두 무시 / 신뢰하는 프록시 여러 겹 / 쓸 수 없는 헤더 네 가지 / IPv4-mapped peer / peer 없음.127.0.0.1): 한 프록시 뒤 두 클라이언트가 따로 스로틀됨 / 헤더 왼쪽을 바꿔도 새 버킷을 얻지 못함 / 설정이 없으면 헤더가 아무것도 바꾸지 않음.test_prod_oidc_plumbing.py의 compose 통과 목록에 새 변수를 더했다.검증
pytest tests/unit/test_auth_throttle_proxy.py tests/unit/test_auth_throttle.py tests/unit/test_oidc_auth.py tests/unit/test_stable_error_codes.py→117 passed.pytest tests/unit -k "doc or env or compose or deploy"→110 passed.ruff check src tests,ruff format --check src tests,mypy src통과.mkdocs build --strict통과.🤖 Generated with Claude Code