AI Frontier

2026년 macOS에서 OpenClaw 게이트웨이 CORS와 OPTIONS 프리플라이트: 허용 출처, 자격 증명, nginx 엣지 헤더, 클라우드 Mac mini 리허설

MacHTML Lab2026.05.15약 33분 읽기

제품 팀이 브라우저 대시보드나 내부 SPA를 TCP 8787에서 대기하는 OpenClaw 게이트웨이에 연결할 때 2026년 가장 흔한 “미스터리 장애”는 모델 API가 아니라 CORS인 경우가 많다.curl은 통과하지만 Safari와 Chrome이 blocked by CORS policy를 보여주는 흔한 이유는 브라우저가 보낸 OPTIONS 프리플라이트가 엣지에서 처리되지 않았거나, 응답에 Access-Control-Allow-Origin: *가 있는데 프런트가 credentials: 'include'를 사용했기 때문이다. 이 글은 명시적 Origin 허용 목록, 단순 요청과 자격 증명 요청의 차이, nginx에서 헤더 순서와 중복 헤더 함정, 티켓에 붙일 수 있는 curl 프로브를 정리하고 첫 실행 PATH·Node·LaunchAgent 스모크, 포트 점유와 헬스 프로브 정렬, doctor 게이트웨이 진단과 연결해 팀 간 왕복을 줄인다.

결정 표, 헤더 레시피, 수치 가드레일(204 또는 200 OPTIONS 허용, 자격 증명 시 와일드카드 금지), MacHTML 공개 가격에 가까운 하루 약 16.9달러 대여 기준까지 얻을 수 있어 고객과 동일한 Safari 빌드에서 재현하기 쉽다.

브라우저와 curl의 차이

curl https://gateway.example/health는 연결 가능성만 증명하며 브라우저 정책은 증명하지 않는다. 사용자 에이전트는 교차 사이트 호출에 Origin을 붙이고, 사용자 정의 헤더가 있으면 단순해 보이는 GET도 프리플라이트가 필요한 POST로 격상될 수 있으며, Access-Control-Allow-Origin이 시작 출처와 일치하거나(비자격 증명의 경우 *) JavaScript가 응답 본문을 읽지 못하게 막는다. CORS를 두 팀의 계약으로 보라: 프런트엔드는 정확한 Origin 문자열을 소유하고, 플랫폼은 TLS를 종료하는 홉에서 헤더를 올바른 순서로 보낸다.

스킴·호스트·포트로 각 호출자를 문서화한다——https://app.corp.internal:8443https://app.corp.internal는 다른 출처다. 허용 목록에서 포트를 빼먹으면 비표준 TLS 포트의 스테이징에서만 간헐적으로 실패한다. 임원 노트북과 같은 Safari로 시험하려고 전용 Mac mini를 빌리는 이유가 여기에 있다.

단순 fetch와 자격 증명 fetch

사용자 정의 헤더가 없는 단순 GET은 프리플라이트를 건너뛸 수 있지만, 교차 출처로 본문을 읽으려면 여전히 올바른 Access-Control-Allow-Origin이 필요하다.Authorization, Cookie, fetch(..., { credentials: 'include' }) 중 하나라도 추가되면 사양은 구체적인 Origin 에코를 요구하고 와일드카드는 금지된다.Access-Control-Allow-Credentials: true를 함께 둔다.

익명 지표만 공개한다면 *로 Cookie를 피할 수 있다. 일부 경로만 자격 증명인 혼합 모드에서는 nginx location을 나누어 마케팅 픽셀이 관리 콘솔의 CORS를 실수로 물려받지 않게 한다.

OPTIONS 프리플라이트 동작

프리플라이트는 OPTIONS와 함께 Access-Control-Request-Method 및 선택적 Access-Control-Request-Headers를 보낸다. 게이트웨이는 Access-Control-Allow-Methods로 허용 동사(보통 GET,POST,OPTIONS)를 나열하고, Access-Control-Allow-Headers로 요청된 이름을 대소문자 구분 없이 에코하며, Access-Control-Max-Age로 브라우저 캐시를 제어한다——개발 중에는 300초, 운영에서는 헤더 회전 빈도에 따라 600–86400이 일반적이다.

204 무본문 또는 200 빈 본문 모두 널리 허용된다. 실패 사례로는 405 Method Not Allowed(nginx가 OPTIONS를 잘못된 업스트림으로 보냄), 동적 에코 시 Vary: Origin 누락으로 CDN이 잘못된 ACAO를 모든 테넌트에 캐시하는 경우가 있다.

Origin 허용 목록과 localhost 함정

http://localhost:5173(Vite)와 http://127.0.0.1:5173은 다른 출처다. 로컬 개발에서는 둘 다 넣거나 dev 명령을 하나의 호스트명으로 통일한다. SSH 로컬 포워딩으로 127.0.0.1:8787을 열어도 브라우저 Origin은 공개 SPA URL 그대로이므로 허용 목록에는 SPA 출처가 필요하고 http://127.0.0.1만으로는 부족하다.

들어오는 Origin을 그대로 Access-Control-Allow-Origin에 반사하는 동적 방식은 편하지만 서버에서 고정 집합으로 검증하지 않으면 위험하다. 맵 조회가 안전하다: 집합 S에 있으면 에코하고 아니면 헤더를 생략해 브라우저가 실패 종료하도록 한다. 거부된 Origin은 INFO로 샘플링 로그해 보안이 남용을 감사할 수 있게 한다.

nginx와 중복 헤더

nginx가 TLS를 종료하고 루프백의 OpenClaw로 전달할 때 CORS를 nginx와 Node 중 어디가 담당할지 정한다——둘 다 붙이면 중복 헤더가 되어 일부 브라우저는 거부한다. 실무적으로 nginx는 정적·마케팅 페이지, OpenClaw는 CLI 호환 API 헤더를 이미 내는 경우 API 라우트만 Node에 맡기는 식의 분할이 흔하다. 어떤 선택이든 우아한 종료와 같은 운영 런북에 적어 인시던트 중 이중 수정을 피한다.

nginx에서 HTTP/2를 종료한다면 오래된 설정에서 add_header2xx에만 붙고 always가 필요할 수 있다. OPTIONS가 잘못된 server 블록에 떨어지는 것은 와일드카드와 자격 증명 불일치 다음으로 SPA 미스터리 장애의 주요 원인이다.

Vary: Origin, CDN 캐시, 오래된 ACAO

요청별로 Origin을 에코하면 응답은 Origin에 따라 변하는 변형이 된다.Vary: Origin이 없으면 중간 캐시가 고객 A의 Access-Control-Allow-Origin을 고객 B 세션에 제공할 수 있다——은밀한 정보 유출이자 기능 버그다. 일부 CDN은 Vary를 제거하는 공격적으로 정규화한다; 엣지 규칙이 CORS를 다시 쓰면 벤더에 티켓을 연다.

CORS 디버깅 중에는 오류 본문 캐시 TTL을 60초 미만으로 두어 잘못된 배포가 엣지에 몇 시간 붙는 것을 막는다. 캐시 무효화는 구조화 로그의 상관 ID와 짝을 이뤄 사용자가 스크린샷을 가려도 HAR과 게이트웨이 로그를 맞출 수 있게 한다.

JWT, 사용자 정의 헤더, 도구별 CORS

OpenClaw 도구 경로는 X-Request-IdX-OpenClaw-Profile 스타일 헤더를 자주 추가한다. 브라우저 안전 목록 밖의 헤더는 모두 프리플라이트를 유발하므로 Access-Control-Allow-Headers에 명시적으로 나열한다——자격 증명 흐름에서 *에 의존하지 말 것. JWT가 Authorization: Bearer라면 프리플라이트와 실제 POST 모두에서 ACAO 줄이 일치하는지 확인한다. 불일치면 서버에서 POST가 성공해도 “Access-Control-Allow-Origin 없음”이라는 유명한 오류가 난다.

서명 키 순환 시에는 프리플라이트 캐시 창을 일시적으로 두 배로 늘려 브라우저가 새 헤더 이름을 15분 안에 집어들게 한 뒤 max-age를 다시 조인다. TLS 인증서 갱신 캘린더와 같은 운영 보드에 적어 동기화가 깨지지 않게 한다.

결정 매트릭스

호출자자격 증명ACAO 전략비고
공개 문서 사이트없음* 또는 정적 허용익명 유지, Cookie 없음
내부 관리 SPA있음명시 Origin 에코Allow-Credentials: true와 함께
모바일 WebView경우에 따라사용자 정의 스킴 OriginWebView URL 패턴 검증
서드파티 SaaS 임베드드묾정적 파트너 Origin계약 허용 목록만

보관용 curl 프로브

# 프리플라이트(호스트·경로 치환)
curl -i -X OPTIONS "https://gw.example/v1/chat" \
  -H "Origin: https://app.example" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

# Origin이 있는 GET으로 ACAO 확인
curl -i "https://gw.example/health" \
  -H "Origin: https://app.example"

전체 응답 헤더를 티켓에 저장하고 각 배포 후 diff한다. 엣지 OPTIONS의 p95가 150 ms를 넘어도 GET 헬스가 12 ms대라면 먼저 차가운 업스트림 라우팅을 의심하고 OpenClaw 자체를 바로 탓하지 않는다.

출시 체크리스트

  1. 운영과 스테이징 SPA의 모든 출처를 스킴·호스트·포트까지 나열한다.
  2. CORS 헤더의 단일 소유자(nginx 또는 OpenClaw)를 정하고 중복을 제거한다.
  3. CI에서 개발자가 로컬에서 쓰는 것과 동일한 Origin 문자열로 OPTIONS를 실행한다.
  4. 자격 증명 흐름이 와일드카드 ACAO를 거부함을 자동 테스트로 보장한다.
  5. 포트나 헤더 변경 후 openclaw doctor를 다시 실행하고 헬스가 200인지 확인한다.
  6. 대여한 mini에서 Safari와 Chrome HAR을 수집해 90일 보관한다.

보안 검토에서 동적 Origin 반사가 상한이 있음을 증명하라는 요구가 늘고 있다. 허용 목록 파일을 git으로 관리하고 플랫폼 팀을 CODEOWNERS에 두면 “임시” 와일드카드 커밋이 영구 공격면이 되는 것을 막는다.

FAQ

Safari만 실패하고 Chrome은 성공하는 이유는?

Safari는 중복 CORS 헤더와 혼합 콘텐츠에 더 엄격하다. 실제 macOS 하드웨어에서 두 엔진을 모두 시험한다.

프리플라이트를 영구 캐시해야 하나요?

아니요. 경계가 있는 Access-Control-Max-Age로 헤더 변경이 분~시간 단위로 전파되게 한다.

WebSocket도 같은 CORS 규칙인가요?

핸드셰이크는 Origin 헤더가 있는 HTTP 업그레이드이며 REST와 동일한 허용 로직으로 검증한다.

Apple Silicon Mac mini 대여는 의사결정자가 데모에 쓰는 것과 같은 Safari·WebKit 스택에서 게이트웨이 UI를 검증하게 해준다. MacHTML 클라우드 노드는 스크립트화된 curl 스위트용 SSH와 디자이너가 Network 패널을 라이브로 볼 선택적 VNC를 결합할 수 있다. 유휴 전력은 종종 12 W 전후로, 스프린트 주에 리허설용 mini를 켜 둬도 잘못된 CORS를 운영에 올린 뒤 이사회 중 롤백하는 것보다 저렴한 경우가 많다.

대여는 무거운 자산 조달 주기도 우회한다. 공개 가격 페이지의 대략 하루 16.9달러로 과금하고 통합 후 유휴가 될 금속 상자를 추가로 살 필요가 없다. CORS 작업이 끝나면 인스턴스를 중지하면 되고 헤더는 git에 남으며 하드웨어는 장부에서 36개월에 걸쳐 감가상각되지 않는다.

실제 macOS 게이트웨이에서 OpenClaw CORS 리허설

클라우드 Mac mini를 빌려 게이트웨이 변경을 머지하기 전 Safari로 OPTIONS·자격 증명 fetch·nginx 엣지 헤더를 검증한다.

CORS 준비된 OpenClaw Mac
$16.9/일~