사내망에서 인터넷을 거치지 않고 Google Cloud API 를 호출해야 할 때가 있습니다.
보안 정책상 아웃바운드가 막혀 있거나, 트래픽이 공중망을 타지 않아야 하는 경우입니다.
Private Service Connect(PSC)로 엔드포인트를 만들면 VPC 안의 내부 IP 하나로
Google API 전체를 부를 수 있습니다. 그런데 막상 curl 을 쳐 보면 생각보다 안 됩니다.000 이 나오거나, 분명 토큰을 넣었는데 401 이 돌아옵니다.
이 글은 그 이유를 실제로 구성해 확인한 기록입니다. Cloud Storage 를 대상으로
세 가지 방식을 모두 실행했고, 결과를 그대로 실었습니다.
VM
│ 이름 해석 → 10.90.100.10
▼
PSC 엔드포인트 ── all-apis ──▶ Google API (GCS · Compute · OAuth2 …)먼저 알아야 할 것 — SNI 와 Host 는 하는 일이 다릅니다
PSC 엔드포인트는 IP 하나로 모든 Google API 를 받습니다. 그래서 서버 입장에서는
「이 요청을 어디로 보낼지」를 따로 알아야 합니다. 여기에 두 가지가 관여합니다.
| 무엇이 | 무엇을 결정하는가 |
|---|---|
| SNI (TLS 핸드셰이크의 서버 이름) | 서버가 어떤 인증서를 내어줄지 |
| Host 헤더 (HTTP) | 요청을 어느 API 로 보낼지 |
SNI 를 바꿔 가며 어떤 인증서가 오는지 확인해 봤습니다.
SNI storage.googleapis.com → CN = storage.googleapis.com
SNI storage-ENDPOINT.p.googleapis.com → CN = *.googleapis.com
SNI gcs.mylab.internal → CN = *.google.com ← 기본 인증서
SNI 없음 → CN = invalid2.invalid
OU = No SNI provided - please fix your client.자체 도메인이나 IP 로 접속하면 Google 이 *.google.com 기본 인증서를 줍니다.
이름이 맞지 않으니 curl 이 연결을 끊습니다. 이것이 뒤의 두 방법에서 손이 더 가는 이유입니다.
방법 1. PSC 전용 도메인
가장 단순합니다. 구성만 해 두면 curl 에 추가 옵션이 없습니다.
PSC 엔드포인트를 만들 때 Service Directory 에 등록하면 p.googleapis.com 비공개 영역이
자동으로 생깁니다. 이름 형식은 <서비스>-<엔드포인트이름>.p.googleapis.com 입니다.
TOKEN=$(gcloud auth print-access-token)
curl -s -H "Authorization: Bearer $TOKEN" \
'https://storage-myep.p.googleapis.com/storage/v1/b/my-bucket/o/hello.txt?alt=media'
hello-psc ← 200. 객체 내용이 그대로 나옵니다함정 — 엔드포인트 이름만으로는 해석되지 않습니다
myep.p.googleapis.com (해석 안 됨)
storage-myep.p.googleapis.com 10.90.100.10 ✔
compute-myep.p.googleapis.com (해석 안 됨)서비스 접두사가 반드시 붙어야 하고, 모든 API 에 레코드가 생기지도 않습니다.
Service Directory 가 흔히 쓰는 API 에만 레코드를 만들기 때문입니다.
쓰려는 API 가 없으면 방법 2 로 가야 합니다.
방법 2. 사설 도메인
자체 도메인(gcs.mylab.internal)을 PSC IP 로 가리키게 한 경우입니다.
그대로는 실패합니다.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" \
'https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt'
000 ← 종료코드 60. 인증서 검증 실패인증서 검증만 끄면 — 여전히 안 됩니다
curl -sk -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" \
'https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt'
401 ← TLS 는 통과했으나 인증 실패이 부분에서 많이 헤맵니다. -k 는 TLS 문제만 덮습니다.
Host 헤더가 gcs.mylab.internal 이라 Google 이 이 요청을 GCS 로 보내지 않습니다.
토큰이 멀쩡해도 401 입니다.
성립하는 조합 — -k 와 Host 헤더를 함께
curl -sk \
-H 'Host: storage.googleapis.com' \
-H "Authorization: Bearer $TOKEN" \
'https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt?alt=media'
hello-psc ← 200다만 인증서 검증을 끄는 것이 전제이므로 운영에 권하지 않습니다.
중간자 공격을 탐지할 수 없게 됩니다.
검증을 끄지 않으려면
이름을 바꾸지 않고 IP 만 바꾸는 방법이 있습니다.
curl -s --resolve 'storage.googleapis.com:443:10.90.100.10' \
-H "Authorization: Bearer $TOKEN" \
'https://storage.googleapis.com/storage/v1/b/my-bucket/o/hello.txt'
200 ← 인증서 검증을 켠 채로 성공SNI 와 Host 가 모두 storage.googleapis.com 이라 정상 인증서가 나오고 라우팅도 됩니다.
방법 3. 순수 IP
도메인 없이 IP 로 직접 호출합니다. 방법 2 와 같은 이유로 그대로는 실패합니다.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" \
'https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt'
000 ← 종료코드 60curl -sk -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" \
'https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt'
401 ← TLS 통과, 라우팅 실패성립하는 조합
curl -sk \
-H 'Host: storage.googleapis.com' \
-H "Authorization: Bearer $TOKEN" \
'https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt?alt=media'
hello-psc ← 200IP 호출에서는 Host 헤더가 필수입니다. 이것이 없으면 어느 API 인지 알 수 없습니다.
결과 한눈에
| 호출 방식 | 추가 옵션 | 결과 | |
|---|---|---|---|
| ① | storage-myep.p.googleapis.com |
없음 | 200 |
| ② | storage.googleapis.com (사설 DNS) |
없음 | 200 |
| ③ | gcs.mylab.internal |
없음 | 000 (exit 60) |
| ④ | gcs.mylab.internal |
-k |
401 |
| ⑤ | gcs.mylab.internal |
-k + Host: |
200 |
| ⑥ | 10.90.100.10 |
없음 | 000 (exit 60) |
| ⑦ | 10.90.100.10 |
-k |
401 |
| ⑧ | 10.90.100.10 |
-k + Host: |
200 |
| ⑨ | storage.googleapis.com |
--resolve |
200 |
응답 코드를 구분해서 읽으십시오
000· 종료코드 60 — TLS 핸드셰이크에서 끊겼습니다. 인증서 문제입니다401— TLS 는 성립했고 HTTP 응답을 받았습니다. 라우팅이나 인증 문제입니다403— 도달했고 인증도 됐으나 권한이 없습니다. 경로는 정상입니다
실제로 Compute API 를 같은 엔드포인트로 호출했더니 403 이 나왔습니다.
서비스 계정에 Compute 권한이 없어서였고, PSC 경유 자체는 성공한 것입니다.
curl -s -o /dev/null -w '%{http_code} via %{remote_ip}\n' \
-H "Authorization: Bearer $TOKEN" \
'https://www.googleapis.com/compute/v1/projects/my-project/zones'
403 via 10.90.100.10 ← PSC 를 거쳤습니다%{remote_ip} 를 찍어 보면 실제로 어느 IP 로 나갔는지 바로 확인됩니다.
공인 IP 가 나오면 DNS 가 적용되지 않은 것입니다.
어느 방법을 쓸 것인가
| 상황 | 권고 |
|---|---|
| 새로 구성하고 코드를 고칠 수 있다 | ① PSC 도메인. 옵션이 필요 없고 인증서 검증도 켜집니다 |
| 기존 코드를 고칠 수 없다 | ② 기본 도메인 + 사설 DNS. 애플리케이션 변경이 없습니다 |
| 일회성 진단 | ⑨ --resolve. DNS 를 건드리지 않고 경로만 바꿔 봅니다 |
| 자체 도메인을 꼭 써야 한다 | ⑤. 다만 인증서 검증을 끄게 되므로 권하지 않습니다 |
운영 환경에는 ② 가 무난합니다. 애플리케이션과 SDK 가 기본 도메인을 그대로 쓰므로
바꿀 것이 없고 인증서 검증도 유지됩니다. 클라이언트 라이브러리는 --resolve 같은
옵션을 받지 않으므로, DNS 로 푸는 것이 사실상 유일한 길이기도 합니다.
구성 명령
PSC 엔드포인트
P=my-project; R=asia-northeast3; NET=psc-lab-vpc
gcloud compute networks create $NET --subnet-mode=custom --project=$P
gcloud compute networks subnets create psc-lab-sub --network=$NET \
--region=$R --range=10.90.0.0/24 --enable-private-ip-google-access --project=$P
# PSC 용 주소는 서브넷이 아니라 글로벌 내부 주소입니다
gcloud compute addresses create psc-lab-ep-ip --global \
--purpose=PRIVATE_SERVICE_CONNECT --addresses=10.90.100.10 \
--network=$NET --project=$P
# 전달 규칙 이름에는 하이픈을 쓸 수 없습니다
gcloud compute forwarding-rules create myep --global \
--network=$NET --address=psc-lab-ep-ip \
--target-google-apis-bundle=all-apis \
--service-directory-registration=projects/$P/locations/$R \
--project=$P
--target-google-apis-bundle 은 all-apis 와 vpc-sc 중에 고릅니다.
VPC SC 경계 안에서만 쓸 것이라면 vpc-sc 입니다.
API 를 먼저 켜십시오
gcloud services enable servicedirectory.googleapis.com dns.googleapis.com --project=$P
이것이 꺼져 있으면 엔드포인트는 만들어지지만 이름으로 접근할 수 없습니다.
저도 꺼진 채로 만들었다가 뒤늦게 켰습니다.
기본 도메인을 PSC 로 보내기 (방법 2)
gcloud dns managed-zones create psc-googleapis --dns-name=googleapis.com. \
--visibility=private --networks=$NET --project=$P
gcloud dns record-sets create googleapis.com. --zone=psc-googleapis \
--type=A --ttl=60 --rrdatas=10.90.100.10 --project=$P
gcloud dns record-sets create "*.googleapis.com." --zone=psc-googleapis \
--type=CNAME --ttl=60 --rrdatas=googleapis.com. --project=$P
영역 자체에 A 레코드를 만들고 나머지는 와일드카드 CNAME 으로 묶습니다.
이렇게 하면 storage.googleapis.com · www.googleapis.com · oauth2.googleapis.com 이
모두 PSC 로 갑니다.
막혔던 지점 정리
| 증상 | 원인 |
|---|---|
p.googleapis.com 영역에 레코드가 0개 |
Service Directory 연동 영역이라 Cloud DNS 에 레코드가 직접 없습니다. 정상입니다 |
myep.p.googleapis.com 해석 실패 |
서비스 접두사가 필요합니다 — storage-myep… |
-k 를 붙였는데 401 |
Host 헤더가 없어 라우팅이 안 됩니다. TLS 와 별개 문제입니다 |
| 403 | 경로는 정상입니다. 서비스 계정 권한을 보십시오 |
마무리
정리하면 이렇습니다.
PSC 엔드포인트는 SNI 로 인증서를 고르고 Host 헤더로 라우팅합니다.
이름을 바꾸면 인증서가 맞지 않고, 인증서를 무시해도 Host 가 맞지 않으면 라우팅이 안 됩니다.
두 가지를 모두 풀어야 200 이 나옵니다.
그래서 가장 깔끔한 선택은 이름을 바꾸지 않는 것입니다.
사설 DNS 로 googleapis.com 을 PSC 쪽으로 돌려 두면, 애플리케이션은 아무것도 모른 채
사설 경로로 나갑니다.