본문 바로가기

GCP

# PSC 로 Google API 사설 호출 — curl 이 000 과 401 을 뱉는 이유

사내망에서 인터넷을 거치지 않고 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                            ← 종료코드 60
curl -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                      ← 200

IP 호출에서는 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 쪽으로 돌려 두면, 애플리케이션은 아무것도 모른 채
사설 경로로 나갑니다.

참고 문서