Cloudflare 뒤의 오리진 IP가 deploy 서브도메인으로 새고 있다 — 세 저장소를 14초 만에 한꺼번에 수정
그날 오후 잠시 GitHub을 응시했다. 세 저장소가 각각 16:20:31, 16:20:38, 16:20:45에 PR을 닫았다 — 14초 차이.
smarts #38: deploy.smarts.md → deploy.smartshow2claude #13: deploy.how2claude.com → deploy.how2claudepickful #118: deploy.pickful.ai / deploy.pickful.xyz → deploy.pickful / deploy.pickful-alpha세 저장소 모두 같은 종류의 버그를 고친 것이다. 원래는 how2claude 저장소에서 무관한 작은 배포 이슈에 대해 Claude와 이야기하던 중이었다. Claude가 지나가는 길에 config/deploy.yml을 한번 보고는 알려줬다 — 이 머신의 오리진 IP가 공개 DNS에 그대로 노출되어 있다고.
내 모든 프로젝트는 Cloudflare 뒤에 있다. Cloudflare 콘솔의 그 주황색 작은 구름 아이콘은 "프록시 통과" 의미로, HTTP 요청은 먼저 Cloudflare 엣지 노드에 도달한 다음 내 오리진으로 간다. 오리진 IP는 DNS 응답에 나타나지 않고, DNS가 반환하는 건 Cloudflare의 anycast IP다. 여기까지는 맞다.
하지만 나는 Kamal로 배포한다. Kamal은 SSH로 서버에 연결해서 docker를 돌린다. SSH는 HTTP 프록시를 통과할 수 없으므로, Cloudflare에 프록시되지 않은 호스트네임이 SSH용으로 필요했다. 당시 내 설정은 이랬다:
# config/deploy.yml
servers:
web:
- deploy.how2claude.com # ← 오리진 IP로 직접 향하는 A 레코드, 회색 구름 (DNS only)
Cloudflare 콘솔에서 how2claude.com과 www.how2claude.com은 주황색이었다. deploy.how2claude.com은 회색이었다. 회색이어야 했다 — 그렇지 않으면 SSH가 연결되지 않으니까.
회색은 공개 DNS를 의미한다. 누구든 다음을 실행할 수 있다:
$ dig deploy.how2claude.com +short
<내 오리진 IP>
그리고 deploy.<domain> 같은 명명 규칙 자체가 분명한 표지다 — 흔한 SaaS 도메인 목록의 deploy.* 서브도메인을 일괄 스캔하면, 본래 Cloudflare 뒤에 숨어 있어야 했던 오리진 IP를 무더기로 수확할 수 있다.
Cloudflare WAF 우회, 속도 제한 우회, DDoS 방어 우회. dig 한 번이면 끝이다.
그날의 주제는 사실 다른 것이었다. 내가 막 작성한 배포 문서를 검토해 달라고 했고, Claude는 대조하기 위해 config/deploy.yml을 열어 7번째 줄 - deploy.how2claude.com까지 읽다가 잠시 멈추더니 대략 이런 말을 했다:
이 호스트네임은 공개 DNS로 해석되죠? 그러면 누구든 DNS 쿼리로 그 오리진 IP를 가져갈 수 있고, Cloudflare의 엣지 프록시는 우회당하는 셈이에요.
나는 잠깐 굳었다. 이건 내가 진작 알아챘어야 할 일이다 — Cloudflare 설정할 때 회색 구름 아이콘을 한번 더 들여다보고 그게 뭘 의미하는지 물어봤더라면 — 하지만 그러지 않았다. 나는 그 호스트네임을 어떤 면에서 "내부적인 것"으로 다루고 있었다. 단지 접두사가 deploy.라는 이유만으로. 내 머리가 조용히, 실제로는 존재하지 않는 사적인 성질을 부여하고 있었다.
Claude는 그 편향을 공유하지 않는다. yaml 필드의 문자열을 읽고 기계적으로 묻는다: 이 문자열은 어떻게 IP로 해석되는가? 답: 공개 DNS. 결론: 오리진 IP는 공개되어 있다.
/etc/hosts 별칭수정은 의외로 간단했다. Kamal은 로컬 SSH 클라이언트에게 호스트네임 해석을 맡긴다. 그러니 그 호스트네임은 내 머신에서만 해석되면 된다 — 인터넷 전체에서 해석될 필요가 없다.
yaml의 호스트네임을 짧게 줄인다:
servers:
web:
- deploy.how2claude # ← 주의: .com 없음
그리고 내 /etc/hosts에 한 줄 추가:
198.51.100.42 deploy.how2claude
CI에서 deploy를 실행할 때, 러너에도 같은 줄을 추가한다 (환경 변수나 워크플로우에서 직접 echo >> /etc/hosts).
결과:
deploy.how2claude.* 레코드가 어디에도 존재하지 않는다kamal deploy / app exec / app logs 명령어는 그대로 동작한다. /etc/hosts가 DNS보다 먼저 조회되니까deploy.* 같은 어색한 회색 구름 서브도메인이 그냥 사라진다데이터베이스 액세서리도 같이 바꿔야 한다:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← 같은 호스트네임
이게 내가 이걸 쓰고 싶었던 이유다.
how2claude 수정을 마치고 나서 다음 일로 넘어가려는데, Claude가 멈춰 세웠다: "smarts랑 pickful도 Kamal 쓰시죠? 거기도 확인해 볼게요."
확인했다.
deploy.smarts.md — 같은 문제, A 레코드 공개deploy.pickful.ai (production), deploy.pickful.xyz (alpha), 그리고 오래전에 죽은 deploy-test1.pickful.ai와 폐기된 blockgeek.com 도메인을 참조하던 deploy.staging.ymlpickful은 다른 둘보다 좀 더 복잡했다. destination이 여러 개(production / alpha / test2)고 역사적 잔재가 있다. Claude는 김에 죽은 staging과 test1 destination도 정리했다 — 어차피 아무도 안 쓰던 것, 그저 노이즈였다.
최종 커밋:
smarts 6482472 2026-04-27 16:20:31 Use private deploy.smarts alias instead of public deploy.smarts.md (#38)
how2claude ccc0344 2026-04-27 16:20:38 Use private deploy.how2claude alias instead of public deploy.how2claude.com (#13)
pickful e6bf9af 2026-04-27 16:20:45 Deploy config hygiene + privatize hostnames (#118)
14초 간격. 물론 이건 GitHub의 머지 큐가 비워진 시각일 뿐이고, 실제 작업은 앞선 두 시간에 걸쳐 있었다 — 하지만 효과적인 모양은: 한 번의 발견, 세 저장소 모두 수정 완료.
내가 혼자 했다면, 현실적인 시나리오는: how2claude에서 고치고, "smarts와 pickful도 같은 수정"이라고 TODO에 적고, 그 TODO가 목록에서 3개월을 누워 있다. 그 전개를 내 눈으로 여러 번 봤다.
Kamal, Capistrano, 또는 SSH 기반의 어떤 배포 도구라도:
config/deploy*.yml을 열고 host:와 servers:를 grep. 등장하는 모든 호스트네임을 리스트업.dig +short 실행. 빈 값이 아니라 오리진 IP가 돌아오면 누수다.deploy.<project>)으로 이름을 바꾸고, IP를 /etc/hosts에 넣는다.echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.여러 프로젝트가 같은 인프라 패턴을 공유한다면, 한 줄짜리 grep 스크립트로 모든 저장소의 deploy*.yml을 훑는 게 하나씩 확인하는 것보다 믿을 만하다.
Cloudflare가 실패한 게 아니다, Kamal의 설계가 잘못된 것도 아니다 — 두 도구 모두 자신이 해야 할 일을 정확히 하고 있었다.
교훈은: 기본값이 조용히 당신을 속인다. deploy.<your-domain>.com은 내부 이름처럼 들리지만, DNS에는 "내부"라는 개념이 없다. A 레코드는 A 레코드고, 공개된 순간부터 공개된 것이다. 이름이 사적이라는 착각을 줄 수 있고, Cloudflare 대시보드의 그 작은 회색 구름 아이콘은 같은 착각의 시각적 형태일 뿐이다 — 화면 위에서 조용히, 빨간 경고도 없이 앉아 있지만, 실제로는 "이 레코드는 나를 통과하지 않는다"라고 말하고 있다.
Claude에게 당신의 deploy 설정을 한 번 읽게 해보라. Claude는 그 착각을 공유하지 않는다.