작년 한 해의 상당 부분을 레이트 리미터 만드는 데 썼다. 장난감이 아니라 진짜 — 원자적 카운터에 슬라이딩 윈도우, 그리고 Retry-After를 실제로 존중하는 429 Too Many Requests까지 갖춘 물건. 머신별 카운터는 합쳐지지 않아서 캐시 계층에서 떼어냈고, 강한 일관성 저장소에 올렸고, 실제 트래픽에 맞춰 임계치를 튜닝했다. 잘 돈다. 자랑스럽다.
그리고 사람들이 레이트 리밋을 까는 이유인 그 공격들을 거의 하나도 못 막는다.
이 문장을 소리 내어 말하기까지 시간이 좀 걸렸다. 업계 전체가 “레이트 리밋 추가”를 보안 통제로 다루니까 — 하드닝 체크리스트의 불릿, “MFA 켜기”와 “시크릿 로테이션” 바로 밑에 붙는 항목. 아니다. 레이트 리밋은 가용성·비용 통제이고, 어쩌다 가장 멍청한 공격자를 불편하게 만들 뿐이다. 우리는 이걸 엉뚱한 서랍에 넣었고, 그 분류 오류에는 대가가 따른다.
레이트 리밋이 실제로 지키는 것
정말 잘하는 게 하나 있다: 시끄러운 클라이언트 하나가 모두의 하루를 망치는 걸 막는 것.
초당 500번씩 단일 IP에서 로그인 엔드포인트를 두드리는 스크립트는 연결을 고갈시키고, DB를 터뜨리고, 요금 폭탄을 낸다. 레이트 리밋이 이걸 깔끔하게 끈다. 오후 내내 카탈로그 전체를 긁으려는 스크레이퍼 — 스로틀. 재시도 루프에 빠진 버그투성이 연동 — 봉쇄. 한 계정에 모든 비밀번호를 시도하는 머신 한 대 — 그래, 그것도.
이 전부가 공유하는 속성: 단일 출처, 대량. 그게 레이트 리밋이 다루는 위협 모델의 전부다. 밸브다. 밸브는 유용하다. 나라면 내일 다시 만든다.
근데 이 예시 하나하나가 공통으로 가진 걸 봐라. 공격자들은 몇 년 전에 이미 알아챘으니까.
못 건드리는 공격
계정을 실제로 비우는 공격은 크리덴셜 스터핑이고, 이건 레이트 리밋을 우회하도록 — 의도적으로, 엔지니어링적 대응으로 — 설계됐다.
경제 구조가 잔인하다. 누군가 A 사이트를 털어 이메일/비밀번호 쌍 1억 개를 덤프하면, 그 쌍들이 B, C, 그리고 당신 사이트에 재생된다. 사람들이 비밀번호를 재사용하니까. 공격자는 추측할 필요가 없다. 어딘가에서 먹히는 자격증명을 이미 손에 쥐고, 그중 어느 게 당신한테도 먹히는지 확인만 하는 거다. 그리고 그들도 당신이 본 그 하드닝 체크리스트를 읽었다.
그래서 단일 출처에서 대량으로 보내지 않는다. 사방에서 소량으로 보낸다. 문서화된 캠페인들은 단일 공격을 수십만 개의 residential IP — 프록시 서비스를 통해 기가바이트 단위로 빌린 진짜 가정용 브로드밴드와 모바일 회선 — 에 분산시켰고, 개별 IP는 로테이션되기 전에 겨우 몇 번만 시도했다. 여기서 열 번. 저기서 여섯 번. 각 주소는 그 자체로만 보면, 비밀번호를 두 번 잘못 치고 세 번째에 맞춘 사람과 정확히 똑같아 보인다.
당신의 IP별 레이트 리미터는 아무것도 못 본다. 볼 게 없으니까. 모든 출처가 임계치 아래에 있다. 공격의 구조 자체가 당신의 임계치 아래에 머무는 것이기 때문이다. 당신은 소방 호스용 밸브를 만들었는데, 그들은 빨대 이십만 개를 들고 나타났다.
아무도 안 짚는 부분: 당신이 그들을 가르치고 있다
이게 내 생각을 바꾼 디테일이다.
레이트 리밋은 조용하지 않다. 되받아친다. 클라이언트가 선을 넘으면 알려준다 — 429, 때로는 착한 클라이언트가 언제 돌아올지 알도록 친절하게 Retry-After: 60까지 붙여서. 그 헤더는 정상 연동에 대한 배려다. 동시에 공격자에게는 공짜 오라클이다.
제대로 된 크리덴셜 스터핑 운영자는 당신 로그인을 몇 번 돌려보고, 당신이 429를 반환하기 시작하는 정확한 순간을 지켜본다. 이제 당신의 임계치를 안다. 이제 IP별 속도를 그 바로 한 칸 아래에 영구히 맞춘다. 당신은 그들을 막지 못했다. 투명해지는 데 필요한 보정 데이터를 넘겨줬고, 그것도 툴링이 이미 파싱하는 표준 헤더로 건넸다.
멍청한 브루트포서는 멈춘다. 유능한 자는 정보를 얻는다. 그리고 계정을 비우는 건 그 유능한 쪽이다.
오분류가 왜 중요한가
이게 단순한 트집이라면 — “사실 그건 가용성 통제야” — 굳이 적지 않았다. 중요한 이유는, 레이트 리밋을 보안 전략이라 부르면 팀이 너무 일찍 멈추기 때문이다.
지켜봤다. 누가 “로그인 엔드포인트를 어떻게 보호하고 있죠?”라고 물으면 답이 돌아온다: “레이트 리밋 있습니다.” 체크. 회의 끝. 그런데 그 로그인 엔드포인트는 이제 딱 한 종류의 적 — 아마추어 — 에게만 방어되고, 계정 대량 탈취가 사업 모델의 전부인 프로에게는 활짝 열려 있다. 진짜 일이 시작됐어야 할 바로 그 순간에, 체크리스트가 모두를 다 끝난 기분으로 만든 거다.
진짜 일은 더 어렵고 슬라이드에 넣기엔 덜 만족스럽다. 크리덴셜 스터핑 탐지는 IP별이 아니라 전체를 가로질러 상관관계를 봐야 한다: ASN별 실패 속도, 휴면 계정을 향한 로그인 시도의 급증, 불가능한 이동(impossible travel) 패턴, 이력과 안 맞는 디바이스 지문, 비밀번호 하나를 수천 개 사용자명에 시도하는 password-spray 형태. 개별적으로는 무해한 요청의 폭발을 하나의 조율된 사건으로 다뤄야 한다는 뜻이고, IP별 카운팅은 그 구조상 이걸 할 수 없다. 그리고 크리덴셜 스터핑을 실제로 무너뜨리는 건 탐지도 아니다 — 훔친 비밀번호를 쓸모없게 만드는 것이고, 그게 바로 MFA와 패스키가 존재하는 이유다. 다들 레이트 리밋을 넣는 그 서랍에 진짜로 들어가야 할 통제.
밸브는 남겨둬라
이 전부가 레이트 리미터를 뜯어내라는 뜻은 아니다. 가용성은 진짜다. 비용도 진짜다. 스크립트 하나 든 아마추어도 진짜고, 막기 싸고, 막아야 한다.
그냥 정체를 정확히 불러라. 레이트 리밋은 당신의 인프라가 압도당하지 않게 지킨다. 사용자의 계정이 털리지 않게 지키지는 않고, 계정을 터는 공격자들은 그걸 배포하는 대부분의 사람보다 오래 알고 있었다. 제 값을 하는 곳 — 신뢰성 체크리스트에 올려라. 그리고 이미 끝냈다고 스스로에게 말했던 그 보안 작업을 하러 가라.
작년에 좋은 밸브를 만들었다. 그게 자물쇠가 아닐 뿐이고, 나는 둘을 같은 거라고 우기는 걸 그만뒀다.