클라우드 브라우저가 localhost를 열 때, 실제로 접속하는 쪽은 누구일까?

클라우드에서 실행되는 브라우저에게 http://localhost:3000을 열라고 하면 누구의 3000번 포트에 접속할까요? 사용자의 노트북일까요, 브라우저 provider의 원격 머신일까요, 아니면 에이전트가 실행되는 호스트일까요? localhost는 장소의 고유 이름이 아니라 요청을 보내는 프로세스 자신을 가리키므로, 브라우저가 어디에서 실행되느냐에 따라 답이 달라집니다.

이 모호함을 그대로 두면 로컬 개발 페이지는 열리지 않고, 더 나쁘게는 공개 URL을 열었다가 사설 주소로 이동하는 경로를 안전한 탐색으로 오인할 수 있습니다. Hermes의 하이브리드 라우팅 계약은 공개 URL과 private·loopback·LAN URL을 분류해 서로 다른 backend로 보낼 수 있게 하고, 공개 페이지에서 private 주소로 redirect되는 경우를 별도 guard 대상으로 다룹니다.[1][2][3][4][5]

localhost는 ‘내 컴퓨터’가 아니라 ‘지금 접속하는 쪽’이다

개발자는 브라우저 주소창의 localhost를 자연스럽게 자기 컴퓨터라고 생각합니다. 평소 브라우저가 자기 컴퓨터에서 실행되기 때문입니다. 그러나 브라우저 자동화가 클라우드 provider에서 실행된다면 그 주소의 loopback은 원격 브라우저 환경을 가리킵니다. 사용자의 로컬 개발 서버와는 다른 네트워크 공간입니다.

이 문제는 주소 문자열만 보고 해결되지 않습니다. 실제 접속 주체가 누구인지 알아야 합니다.

로컬 Chromium → localhost → 로컬 Chromium이 있는 호스트
클라우드 브라우저 → localhost → 클라우드 브라우저가 있는 원격 호스트

따라서 공개 웹사이트에는 원격 browser backend가 적합할 수 있지만, 로컬 개발 서버나 사설망 페이지에는 같은 backend를 그대로 쓰면 의도한 대상에 도달하지 못할 수 있습니다. 하이브리드 라우팅은 이 차이를 URL 분류 단계에서 다룹니다. 공개 URL은 cloud 쪽으로, loopback·private·LAN 성격의 URL은 local Chromium sidecar 쪽으로 보낼 수 있다는 것이 공개 문서와 코드 계약의 핵심입니다.[1][2]

여기서 “local”은 언제나 사용자의 개인 노트북이라는 뜻이 아닙니다. 에이전트와 로컬 브라우저 sidecar가 실제로 배치된 호스트를 뜻합니다. 컨테이너, 가상 머신, 원격 개발 환경을 쓰면 그 경계는 다시 달라질 수 있습니다. 그러므로 운영 문서에는 localhost라는 단어보다 “어느 프로세스가 어느 네트워크 namespace에서 접속하는가”를 적는 편이 정확합니다.

URL 하나를 분류하는 일이 왜 보안 경계가 되는가

라우팅은 단지 접속 성공률을 높이는 편의 기능이 아닙니다. public과 private 주소를 구분하는 일은 보안 경계이기도 합니다. 외부에서 받은 URL을 클라우드 browser로 열다가 내부 주소에 접근하거나, 공개 주소가 redirect를 통해 loopback·사설망으로 이동하면 원래의 신뢰 가정이 바뀝니다.

가령 처음에는 https://public.example/guide처럼 보였는데 서버가 http://127.0.0.1:9000/admin으로 이동시키는 상황을 생각해 볼 수 있습니다. 최초 문자열만 검사했다면 public 탐색으로 분류될 수 있지만 최종 목적지는 private입니다. 그래서 공개 URL에서 private 주소로 redirect되는 경로는 별도 guard 대상입니다.[5]

이때 위험한 대응은 “어차피 local backend는 private 페이지를 열 수 있으니 redirect 뒤에도 자동으로 넘겨주자”입니다. 동결 구현은 그렇게 하지 않습니다. cloud 경로에서 시작한 public URL이 private 주소로 redirect되면 탐색 결과를 실패로 바꾸고 about:blank로 이탈합니다.[5] 반면 처음부터 private URL로 분류되어 local sidecar로 간 탐색은 이 cloud SSRF guard의 자동 보호 범위 밖입니다. backend 선택과 cloud redirect 차단을 분리해야 하는 이유입니다. 별도 회귀 계약은 guard가 활성인 browser mode에서 private 페이지의 click·type·press를 막지만, local backend처럼 guard가 비활성인 경로에서는 action을 그대로 진행한다고 명시합니다.[3][4]

주소 분류에는 대표적으로 다음 범주가 있습니다.

  • 일반적인 공개 http·https URL
  • localhost와 loopback 주소
  • 사설 IP 범위와 LAN 호스트
  • 탐색 도중 public에서 private로 바뀌는 redirect
  • 해석 결과에 따라 private가 될 수 있는 호스트

정확한 판정은 코드와 버전에 따라 달라질 수 있으므로 임의의 문자열 규칙을 새로 만들기보다 제공되는 라우팅 계약을 사용해야 합니다.[2]

backend 선택과 페이지 행동 허용은 별도 질문이다

“어떤 browser backend로 열 것인가?”와 “열린 페이지에서 어떤 행동을 허용할 것인가?”는 같은 질문이 아닙니다.

첫 번째는 연결 가능성의 문제입니다. 로컬 개발 페이지라면 local Chromium이 네트워크상 도달 가능한 주체일 수 있습니다. 두 번째는 권한과 안전의 문제입니다. 현재 private-page action guard는 cloud/비로컬 browser mode에서 활성일 때 click·type·press를 차단하지만, local backend나 allow_private_urls처럼 guard가 비활성인 경로에서는 private-looking 현재 URL을 검사하지 않고 action을 진행합니다.[4] 따라서 local sidecar로 열렸다는 사실 자체가 사용자 승인이나 action 안전정책을 대신하지 않습니다.

다음과 같은 상태 모델이 유용합니다.

입력 URL 분류
  → backend 선택
  → 실제 탐색
  → 최종 URL 재분류
  → guard가 활성인 browser mode라면 페이지 action 차단 판정

최종 URL 재분류가 빠지면 redirect가 최초 판정을 우회할 수 있습니다. action 판정이 빠지면 “읽기 위해 열기”와 “페이지에서 변경 작업 수행”이 같은 승인으로 취급될 수 있습니다. 특히 내부 관리 페이지에서는 읽기 전용 snapshot과 버튼 클릭의 위험도가 다릅니다.

외부 네트워크 없이 만드는 최소 재현

실제 사내망이나 클라우드 provider의 내부 네트워크를 건드리지 않아도 라우팅 계약을 안전하게 확인할 수 있습니다. URL 분류 함수와 mock backend를 사용하고, redirect는 로컬 fixture 서버 또는 합성 응답으로 재현합니다. 목표는 특정 provider의 네트워크를 탐사하는 것이 아니라 입력별 backend 선택과 guard 판정을 기록하는 것입니다.

1. 세 종류의 fixture를 준비한다

  • PUBLIC_URL: 문서 전용 가짜 공개 주소 또는 테스트에서 public으로 분류되는 합성 URL
  • LOOPBACK_URL: 테스트 fixture가 듣는 loopback 주소
  • PUBLIC_TO_PRIVATE: 첫 응답은 public으로 분류되지만 Location이 loopback fixture를 가리키는 합성 redirect

실제 사설 장비 주소, 관리자 페이지, 클라우드 metadata endpoint는 사용하지 않습니다. 테스트 서버는 읽기 전용 고정 문자열만 반환하고 테스트가 끝나면 종료합니다.

2. browser backend를 mock으로 바꾼다

cloud와 local 두 backend가 실제 페이지를 열지 않고 호출 기록만 남기게 합니다.

cloud.navigate(url) → {backend: "cloud", url: ...}
local.navigate(url) → {backend: "local", url: ...}

이렇게 하면 외부 호출이나 비용 없이 라우터의 결정을 볼 수 있습니다. 공개 URL은 cloud, loopback·private URL은 local로 분리되는지 확인합니다.[2][3]

3. redirect 뒤 최종 주소를 다시 판정한다

PUBLIC_TO_PRIVATE를 cloud 경로에서 탐색했을 때 최초 public 판정만으로 private 목적지가 노출되지 않는지 봅니다. 기대 결과는 post-navigation 검사에서 탐색 자체가 실패하고 브라우저가 about:blank로 이동해, 이후 snapshot이나 action이 private 목적지에 이어지지 않는 것입니다.[5] local sidecar처럼 guard가 비활성인 경로의 action 통제는 이 차단이 아니라 별도 승인·운영 정책의 책임입니다.[4]

4. 관찰과 기대를 분리해 기록한다

입력                 기대 backend   최종 범주          action 기대
public URL            cloud          public             정책 범위 내
loopback URL          local          private/loopback    private 정책 적용
public→private redirect 최초 cloud    private/loopback    탐색 실패 + about:blank

테스트 결과에는 URL 범주, 선택 backend, 최종 범주, guard 상태만 남깁니다. 실제 내부 호스트 이름이나 응답 본문은 기록할 필요가 없습니다.

5. 실패 조건도 확인한다

라우터가 private 주소를 cloud로 보내거나, cloud의 public→private redirect 뒤 최종 주소를 재평가하지 않거나, 차단 뒤 about:blank로 이탈하지 않는다면 실패로 봅니다.[5] 반대로 private 주소가 local로 갔다는 이유만으로 “보안 검증 완료”라고 쓰지 않습니다. 그것은 backend 선택이 계약대로였다는 뜻일 뿐입니다. local backend에서는 일반 cloud SSRF action guard가 비활성일 수 있으므로, 페이지 action 전체의 안전은 별도 승인·운영 정책으로 닫아야 합니다.

디버깅할 때 먼저 물어야 할 네 가지

로컬 페이지가 열리지 않을 때 포트부터 의심하기보다 다음 순서로 보면 원인을 빨리 좁힐 수 있습니다.

  1. 입력 URL은 어느 범주로 분류됐는가? localhost, loopback IP, 사설 IP, 공개 호스트를 구분합니다.
  2. 어느 backend가 실제 탐색했는가? cloud인지 local sidecar인지 호출 영수증을 봅니다.
  3. 그 backend에서 대상 서비스에 도달 가능한가? 같은 localhost라도 실행 위치가 다르면 대상이 다릅니다.
  4. 탐색 후 최종 URL이 바뀌었는가? redirect 뒤 private가 됐다면 새로운 guard 판정이 필요합니다.[5]

이 네 가지를 기록하면 “브라우저가 localhost를 못 연다”라는 모호한 오류를 “public으로 오분류됨”, “local sidecar가 다른 namespace에 있음”, “redirect 뒤 private guard가 작동함”처럼 구체적인 상태로 바꿀 수 있습니다.

하이브리드 라우팅이 있다고 해서 모든 배치 환경의 네트워크 연결이 자동으로 해결되는 것은 아닙니다. local backend가 실행되는 호스트와 개발 서버가 서로 다른 컨테이너라면 적절한 바인딩과 네트워크 구성이 여전히 필요합니다. 라우터는 어느 쪽이 접속해야 하는지 결정하지만 존재하지 않는 연결 경로를 만들어 내지는 않습니다.

확인 범위와 한계

이 글에서 확인한 범위는 공개 URL과 private·loopback URL이 서로 다른 browser backend로 라우팅될 수 있다는 문서·코드·회귀 계약, 그리고 public에서 private로 redirect되는 경로가 별도 guard 대상이라는 계약입니다.[1][2][3][4][5]

특정 클라우드 browser provider의 내부 네트워크 구현은 확인하지 않았습니다. 어느 데이터센터에서 실행되는지, provider 내부의 DNS와 프록시가 어떻게 구성되는지, 특정 사용자 환경에서 local sidecar가 어느 namespace에 있는지도 단정하지 않습니다. 위 절차는 mock과 fixture로 라우팅·guard 계약을 확인하는 안전한 재현이며, 실제 사설망 스캔이나 내부 서비스 접근 절차가 아닙니다.

Sources

[1] https://github.com/NousResearch/hermes-agent/blob/f51aa6a9b5ce514e15f8e337777f522fd5cc6fa2/website/docs/user-guide/features/browser.md

[2] https://github.com/NousResearch/hermes-agent/blob/f51aa6a9b5ce514e15f8e337777f522fd5cc6fa2/tools/browser_tool.py#L1282-L1401

[3] https://github.com/NousResearch/hermes-agent/blob/f51aa6a9b5ce514e15f8e337777f522fd5cc6fa2/tests/tools/test_browser_hybrid_routing.py

[4] https://github.com/NousResearch/hermes-agent/blob/f51aa6a9b5ce514e15f8e337777f522fd5cc6fa2/tests/tools/test_browser_private_page_action_guard.py

[5] https://github.com/NousResearch/hermes-agent/blob/f51aa6a9b5ce514e15f8e337777f522fd5cc6fa2/tools/browser_tool.py#L3109-L3140

1
1개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.