Skip to content
isdnetworks
Go back

인증 없이 열린 API 와 데이터베이스 규칙을 한 번에 닫지 않고 좁은 판부터 닫았다

넘겨받은 커머스에는 인증 없이 인터넷에서 닿는 내부 API 와, 로그인 없이 읽히거나 쓰이는 데이터베이스 경로가 있었다. 닫아야 한다는 데에는 이견이 없었고, 어려운 것은 닫는 방법이었다. 열린 문을 그냥 잠그면, 그 문으로 정상적으로 드나들던 화면과 서버가 같이 멈춘다.

Table of contents

Open Table of contents

배경 — 막으면 끊기는 호출처들

첫 대상은 문자 발송 API 였다. 보내는 번호·받는 번호·본문을 요청에서 그대로 받는 경로가 인증 없이 열려 있었고, 인증 헤더는 있으면 관리자 메일을 붙일 뿐 없어도 발송했다. 공개 주소 앞의 Apache 가 제한 없이 넘겨 주고, Cloudflare 도 출발지를 막지 않았다.

닫기 전에 누가 부르는지부터 셌다. 요청은 하나도 보내지 않고 컨테이너 로그·코드·설정만 읽었다. 로그의 접근 줄과 요청 머리를 요청 ID 로 이어 집계만 했고, 출발지는 Cloudflare 가 붙인 원 IP 로 갈라 우리 외부 IP 31개, Google 공개 대역표, 사무실 허용 IP 와 대조했다. 30일 동안 발송 요청은 3,319건이었고 그중 2,646건에 인증이 없었다. 모르는 제3자의 발송 흔적은 찾지 못했다.

문제는 부르는 곳이 넓다는 점이었다. 클러스터 안의 서비스, 고정 IP 가 없는 Cloud Functions, 공개 주소로 돌아 들어오는 GKE 파드, 작업용 VM, 백오피스 브라우저 JavaScript, 공장 Android 앱, 데이터 스크립트가 모두 이 API 를 부른다. 인증을 바로 필수로 바꾸면 이 가운데 무엇이 끊길지 미리 알 수 없다. 그래서 문자 API 는 호출처별 수정표와 속도 제한 초안부터 만들었고, 같은 조사에서 나온 데이터베이스 규칙의 열린 자리는 좁은 판부터 배포했다.

판단 — 한 번에 닫을지, 좁은 판부터 닫을지

닫는 방식은 다음 두 가지 방법 가운데서 골랐다.

  1. 규칙과 인증을 한 판으로 고쳐 한 번에 배포한다
  2. 가장 위험한 자리만 고친 좁은 판을 먼저 내고, 나머지는 호출처별 수정표에 맞춰 단계로 나눈다

2번이 맞다고 본 이유는 실패했을 때 원인을 가를 수 있어야 하기 때문이다. 큰 판은 무엇이 끊겼을 때 어느 줄 때문인지 알 수 없다. 반면 좁은 판은 바뀐 곳이 하나라 끊긴 것이 생기면 그 한 곳으로 되짚을 수 있다. 대가는 배포 횟수가 늘고, 그동안 다른 열린 자리가 남는다는 점이다.

데이터베이스 규칙은 라이브 300줄 중 290줄을 그대로 두는 후보판을 먼저 만들었다. 후보판을 만드는 스크립트는 바뀐 줄을 모두 찍고, 라이브 원문이 예상과 달라 절이 맞지 않으면 멈춘다. 그중에서도 가장 급한 자리 하나만 고친 좁은 판을 따로 뽑았다. 텍스트 diff 는 한 곳(−1줄 +5줄)이고, 바뀐 규칙 줄이 둘이 아니면 스크립트가 rc 1 로 실패한다.

검증 — 대조군이 갈라야 시험이다

배포 전 검증은 Firebase 에뮬레이터로 했다. 시험 사례는 {무인증 · 고객 · 다른 고객 · 멤버 · 직원} × 경로 × {맞는 모양 · 틀린 모양} 의 조합으로 짰다. 핵심은 라이브 규칙을 대조군으로 같이 돌리는 것이다. 지금 뚫린 곳이 시험에서도 뚫린 것으로 나와야, 그 시험이 무언가를 가른다고 말할 수 있다.

좁은 판의 18사례를 라이브 규칙과 배포용 규칙에 각각 돌렸다. 두 판 모두 기대값과 어긋난 것이 0건이었고, 두 판의 결과는 정확히 6사례에서 갈렸다. 「가입한 아무 계정이 남의 항목을 덮거나 지우는」 사례들이 라이브에서는 통과하고 배포용에서는 거부됐다. 둘이 같은 결과를 냈다면 그 시험은 아무것도 측정하지 못한 것이다. 에뮬레이터 jar 는 내려받은 뒤 크기와 SHA256 이 firebase-tools 기대값과 같은지부터 확인했다.

해결 — 배포·되읽기·라이브 대조·관측

배포는 다섯 단계로 했다.

  1. 직전 라이브 규칙을 다시 받아, 해시가 후보판을 만들 때의 원본과 같은지 본다. 다르면 배포용을 다시 만든다
  2. 바뀌는 diff 를 보이고, 그 diff 그대로 배포 승인을 받는다
  3. REST 의 PUT /.settings/rules.json 으로 규칙 전체를 한 번에 배포한다
  4. 되읽은 규칙의 sha256 이 배포용과 바이트까지 같은지 본다
  5. 라이브에서 대조한다. 직원 권한이 없는 시험 계정 토큰으로 쓰기를 시도해 401 Permission denied 와 값 그대로(null)를 확인하고, 서버 경로 쓰기는 200 으로 통과한 뒤 원복까지 되읽는다

되돌림은 1단계에서 받은 직전 라이브 원문을 다시 PUT 하는 것이다. 좁은 판을 낸 지 11분 뒤에 다음 판 네 묶음을 배포했고, 배포 뒤 20분·82분·215분에 이상 여부를 관측했다. 세 번 모두 이상이 없었으므로 규칙을 그대로 유지했다. 관측을 한 번이 아니라 간격을 두고 여러 번 한 것은, 간헐적으로 쓰이는 화면이 늦게 깨질 수 있기 때문이다.

결과 — 좁은 판으로 닫은 노출

정리


Share this post on:

Previous Post
인수 과업의 완료는 증빙이 있는 것만 적고, 남은 것은 성격별로 다섯 갈래로 나눠 넘겼다
Next Post
dev 환경을 「쓸 때만 띄우는 구조」로 바꾸자는 안은 실측해 보니 끌 것이 없어 하지 않기로 했다