← 개발 로그 목록

website: nginx 로그인 rate limit 구멍 메꾸고 보안 헤더 문서에 반영

/ 3분 분량 / 개발 로그

이 레포는 `infra/nginx.conf`가 gitignore에 걸려 있어서 실제 파일이 레포에 안 올라간다. 그래서 nginx 설정을 바꿀 때마다 `infra/CLAUDE.md`에 실제 구조를 기록해두는 게 관례인데, 이번에도 서버에 먼저 적용하고 헬스체크·429 실측까지 끝낸 다음 문서만 뒤늦게 맞추는 순서로 진행했다.

#403 보안 현황을 점검하다가 로그인 엔드포인트의 rate limit이 생각보다 느슨하게 걸려있는 걸 발견해서, 이미 서버에 적용해둔 nginx 변경사항 두 가지를 infra/CLAUDE.md에 정리한 PR이다.

발견한 첫 번째 문제는 rate limit 존 배치였다. /api/admin/auth/login, /api/member/auth/login이 sensitive 존(1r/s)이 아니라 general 존(10r/s)에 걸려있었다. 비밀번호 찾기(password/forgot)는 sensitive로 막아뒀으면서 정작 무차별 대입 공격의 1순위 대상인 로그인 자체는 10배 느슨하게 열려있던 셈이라, 순서가 뒤바뀐 걸 확인하고 sensitive 존으로 옮겼다.

두 번째는 보안 헤더가 아예 없었다는 점이다. X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security 네 개를 추가했는데, 이 중 앞의 두 개는 Spring Security가 기본으로 이미 보내고 있어서 nginx에 추가하면 응답에 중복으로 찍힌다. 값이 같으니 무해하긴 한데, 굳이 백엔드 쪽 헤더를 끄지 않고 nginx에도 남겨둔 이유가 있다. 나중에 이 서버 앞에 다른 백엔드가 붙거나 nginx가 정적 응답을 직접 내보낼 일이 생겨도 방어선이 유지되게 하려는 판단이었다. X-Content-Type-Options는 특히 최근에 처리한 업로드 매직바이트 검증(#406)이랑 짝을 이루는 헤더라, 업로드된 파일이 스크립트로 오인 실행되는 경로를 서버 검증 + 브라우저 강제 양쪽에서 막아두는 셈이다.

배포하면서 예상 못 한 삽질도 하나 있었다. 새 nginx.conf를 mv로 갈아끼우고 nginx -t, reload를 돌렸는데 둘 다 성공으로 나왔다. 그런데 실제로 헤더가 안 붙는 걸 보고 이상해서 docker compose exec nginx cat nginx.conf로 컨테이너 안 파일을 직접 열어보니 옛날 설정 그대로였다. Docker 바인드마운트가 파일을 inode 기준으로 물고 있어서, mv(rename)로 파일을 바꿔치기하면 호스트에서는 새 파일이지만 컨테이너 입장에서는 여전히 옛 inode를 보는 상황이었다. nginx -t나 reload는 그 시점에 마운트된(옛) 파일을 대상으로 문법 검사를 하는 거라 "성공"이라고 나온 것도 당연했던 거고, 결국 docker compose up -d --force-recreate nginx로 컨테이너를 재생성해야 새 바인드마운트가 다시 맺어지면서 반영됐다. 앞으로는 mv로 교체하지 말고 기존 파일에 직접 덮어쓰거나, 교체 후 반드시 컨테이너 내부 파일을 재확인하기로 문서에 못박아뒀다.

커밋은 두 개인데 두 번째는 Copilot Autofix가 붙인 커밋이고, 실제로 이 PR에서 바뀐 파일은 infra/CLAUDE.md 하나, 13줄 추가에 1줄 삭제로 끝났다. 서버 쪽 실제 변경은 이미 다 적용되고 검증된 상태에서 문서만 따라잡은 거라 리뷰나 병합 자체는 20분 만에 끝났다. 코드 변경치고는 작지만, gitignore로 가려진 설정을 다루는 만큼 이 문서가 틀어지면 다음에 nginx를 만지는 사람이 실제 서버 상태를 아예 모르고 작업하게 된다는 게 이 PR의 실질적인 이유였다.