Cloudflare로 봇을 막아도 vercel.app 주소는 열려 있다
Cloudflare + Vercel 조합에서 놓치기 쉬운 우회 경로와, 그 경로를 막는 방법을 정리했습니다.

시작하며
요즘 도메인은 Cloudflare에 연결하고 서비스는 Vercel에 배포하는 조합을 많이 쓰는 것 같습니다. 봇 차단도 Cloudflare에서 하는 경우가 많습니다.
이 조합에서 놓치기 쉬운 부분이 하나 있습니다.
Vercel이 자동으로 만들어 두는 xxx.vercel.app 주소는 Cloudflare를 거치지 않습니다.
이 글에서는 왜 그런지, 어떻게 막으면 되는지 정리합니다.
같은 봇인데 주소에 따라 갈린다

Cloudflare 봇 차단은 내 도메인을 Cloudflare에 연결해서, 그 도메인으로 들어오는 요청을 거르는 방식입니다. DNS가 Cloudflare를 가리키니 모든 요청이 Cloudflare를 먼저 지납니다.
그런데 Vercel은 따로 설정하지 않아도 주소를 여러 개 만들어 둡니다.
| 종류 | 형태 | 언제 생기나 |
|---|---|---|
| 기본 주소 | 프로젝트명.vercel.app | 프로젝트를 만들 때 1개 |
| 브랜치 주소 | 프로젝트명-git-브랜치명-팀명.vercel.app | 브랜치에 푸시할 때 |
| 배포 주소 | 프로젝트명-해시-팀명.vercel.app | 배포할 때마다 |
이 주소들의 DNS는 Vercel을 바로 가리킵니다.
그래서 내 도메인에서는 403으로 막히는 봇도, xxx.vercel.app으로 들어오면 200을 받습니다.
커스텀 도메인을 연결해도 이 주소들은 사라지지 않고 같이 살아 있습니다.
봇은 이 주소를 어떻게 찾을까
정해진 경로 하나가 있는 건 아니고, 흔히 알려진 경로는 이렇습니다.
- 이름 추측: 기본 주소는
프로젝트명.vercel.app꼴이라 서비스 이름만 알면 쉽게 추측됩니다. - 페이지가 흘리는 링크:
og:image, canonical, sitemap 같은 곳에VERCEL_URL로 만든 절대 주소가 들어가면 봇이 그 링크를 따라옵니다. - 공개된 링크: 공개 저장소의 PR 댓글에 남는 미리보기 주소, 메신저에 붙여 넣은 링크, 검색엔진에 색인된 페이지가 경로가 됩니다.
막는 방법
보통 쓰는 방법은 네 가지입니다.
1. 차단 규칙을 Vercel Firewall로 옮기기
Vercel에 배포한다면 가장 흔한 방법입니다. Cloudflare는 DNS만 쓰고(프록시 끔), 봇 차단 규칙은 Vercel Firewall에 둡니다.
Vercel Firewall은 어느 주소로 들어오든 프로젝트로 오는 모든 요청에 적용됩니다.
xxx.vercel.app이든 내 도메인이든 같은 규칙을 지나니 우회 경로 자체가 사라집니다.
Vercel도 앞단에 리버스 프록시를 두는 것을 권하지 않습니다. 앞에 프록시가 있으면 Vercel의 캐시와 방화벽이 실제 방문자를 제대로 보지 못하기 때문입니다.
2. Cloudflare를 거쳐 왔는지 검사하기
Cloudflare의 캐시나 WAF를 꼭 써야 한다면, 요청이 Cloudflare를 거쳐 왔는지 Vercel에서 확인합니다. 흔히 오리진 잠금이라고 부르는 패턴입니다.
- Cloudflare Transform Rules로 모든 요청에 비밀 헤더를 붙입니다. 예를 들면
x-origin-secret: 긴-랜덤-값입니다. - Vercel Firewall 커스텀 규칙에서 이 헤더가 없거나 값이 다르면 403으로 막습니다.
xxx.vercel.app으로 바로 들어온 요청에는 이 헤더가 없으니 막힙니다.
Cloudflare IP 대역만 허용하는 방법도 있지만, 대역이 바뀔 때마다 목록을 고쳐야 해서 헤더 방식을 더 많이 씁니다.
검사를 미들웨어에서 해도 되지만, 미들웨어는 보통 matcher에서 /api나 정적 파일을 빼 두기 때문에 빈틈이 생기기 쉽습니다.
Firewall 규칙은 모든 요청에 걸려서 더 안전합니다.
3. 정본 도메인으로 리다이렉트
내 도메인이 아닌 주소로 들어오면 내 도메인으로 308 리다이렉트합니다. 리다이렉트된 요청은 Cloudflare를 지나게 되고, 배포 주소가 검색에 색인되어 canonical이 갈리는 것도 막을 수 있습니다.
// next.config.mjs
const CANONICAL_HOST = "example.com"
export default {
async redirects() {
// 프리뷰 배포는 그대로 두고 프로덕션만 리다이렉트
if (process.env.VERCEL_ENV !== "production") return []
return [
{
source: "/:path*",
missing: [{ type: "host", value: CANONICAL_HOST }],
destination: `https://${CANONICAL_HOST}/:path*`,
permanent: true, // 308
},
]
},
}미들웨어 대신 next.config에 둔 이유도 같습니다.
next.config의 리다이렉트는 라우팅 단계에서 먼저 평가되어 /api를 포함한 모든 경로에 걸립니다.
다만 리다이렉트는 봇이 결국 Cloudflare를 지나게 만들 뿐, 직접 막지는 않습니다. 그래서 1번이나 2번과 함께 쓰는 SEO용 보조 수단으로 보는 게 맞습니다.
4. 프리뷰·배포 주소는 Deployment Protection으로
Vercel에는 배포 주소를 로그인한 팀원만 보게 하는 Deployment Protection이 있습니다. 브랜치 주소와 해시가 붙은 배포 주소는 표준 보호(Standard Protection)를 켜는 것으로 막을 수 있습니다.
하지만 표준 보호는 프로덕션 도메인을 제외하고, 기본 주소 프로젝트명.vercel.app은 프로덕션 도메인으로 취급됩니다.
가장 추측하기 쉬운 주소가 보호 대상에서 빠지므로, 이 주소는 1~3번으로 막아야 합니다.
정리
| 상황 | 조합 |
|---|---|
| Vercel에만 의존해도 될 때 | 1번 + 4번, SEO를 위해 3번 추가 |
| Cloudflare 기능을 계속 써야 할 때 | 2번 + 4번, SEO를 위해 3번 추가 |
주기적으로 확인하기
이런 우회는 설정만 봐서는 놓치기 쉽습니다. 접속 로그에서 내 도메인이 아닌 호스트로 들어온 요청이 있는지만 봐도 금방 드러납니다.
AI에게 점검을 맡기고 싶다면 아래 프롬프트를 써 볼 수 있습니다.
내 서비스는 Cloudflare로 봇을 막고 Vercel에 배포돼 있어.
*.vercel.app 같은 배포 주소처럼 Cloudflare를 거치지 않고 들어올 수 있는 경로가 있는지 점검해 줘.
그런 경로로 실제 요청이 들어온 기록이 있는지 로그도 확인하고,
있다면 막는 방법(Vercel Firewall, 오리진 잠금, 리다이렉트 등)을 제안해 줘.마치며
방화벽을 CDN 앞단에 두면, CDN을 거치지 않는 주소가 뒷문이 됩니다.
Cloudflare + Vercel 조합을 쓰고 있다면 xxx.vercel.app 주소도 한 번 확인해 보시길 추천합니다.