<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>pubsub 님의 블로그</title>
    <link>https://pubsub.tistory.com/</link>
    <description>pubsub 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 2 Nov 2026 14:38:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>pubsub</managingEditor>
    <item>
      <title># 흐름 로그를 아직도 서브넷마다 켜고 계십니까</title>
      <link>https://pubsub.tistory.com/8</link>
      <description>&lt;p&gt;VPC 흐름 로그를 켜는 방법이 하나 더 생겼습니다. &lt;strong&gt;조직에 한 번만 걸면 그 아래&lt;br&gt;모든 VPC 에 적용됩니다.&lt;/strong&gt; 서브넷이 계속 늘어나는 랜딩존이라면 눈여겨보실 만합니다.&lt;/p&gt;
&lt;p&gt;다만 기존 방식과 &lt;strong&gt;공존&lt;/strong&gt;하는 구조라, 모르고 둘 다 켜면 로그가 두 배로 쌓입니다.&lt;br&gt;이 글은 두 체계의 차이와 전환할 때 걸리는 지점을 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 서브넷마다 켜는 방식의 한계&lt;/h2&gt;
&lt;p&gt;지금까지는 서브넷을 만들 때 흐름 로그를 켜는 방법뿐이었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute networks subnets create SUBNET \
  --enable-flow-logs --logging-flow-sampling=1.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식에는 구조적인 약점이 둘 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 빠뜨리기 쉽습니다.&lt;/strong&gt; 서브넷을 새로 만들 때마다 챙겨야 합니다. 랜딩존처럼&lt;br&gt;업무 도메인이 늘어날 때마다 서브넷이 추가되는 환경에서는 언젠가 누락이 생깁니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 서브넷만 봅니다.&lt;/strong&gt; VM 트래픽은 잡지만 &lt;strong&gt;VPN 터널이나 인터커넥트 연결을&lt;br&gt;오가는 트래픽 자체는 대상이 아닙니다.&lt;/strong&gt; 온프렘 하이브리드 구성에서 정작 보고 싶은&lt;br&gt;구간이 빠집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 체계가 둘입니다&lt;/h2&gt;
&lt;p&gt;같은 것을 두 경로로 만드는 것이 아니라 &lt;strong&gt;별개의 체계&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Compute Engine API&lt;/th&gt;
&lt;th&gt;Network Management API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;형태&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;서브넷의 속성&lt;/strong&gt; (&lt;code&gt;logConfig&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;독립된 구성 리소스&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 범위&lt;/td&gt;
&lt;td&gt;서브넷마다 하나씩&lt;/td&gt;
&lt;td&gt;프로젝트 또는 &lt;strong&gt;조직&lt;/strong&gt; 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대상&lt;/td&gt;
&lt;td&gt;서브넷뿐&lt;/td&gt;
&lt;td&gt;네트워크 · 서브넷 · &lt;strong&gt;VPN 터널&lt;/strong&gt; · &lt;strong&gt;인터커넥트 연결&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2차 샘플링 기본값&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;구성을 서브넷에서 떼어내 별도 리소스로 만든 것이 Network Management API 쪽입니다.&lt;br&gt;대상을 네 가지 중 하나로 지정하며, &lt;strong&gt;서로 배타적&lt;/strong&gt;이라 하나를 지정하면 나머지는&lt;br&gt;지워집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;network                  VPC 안의 VM · VPN 터널 · 인터커넥트 연결 전부
subnet                   그 서브넷의 VM 만
vpnTunnel                VPN 터널 하나
interconnectAttachment   인터커넥트 연결 하나&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;network&lt;/code&gt; 대상을 쓰면 &lt;strong&gt;VM 과 하이브리드 연결을 한 번에&lt;/strong&gt; 덮습니다. 1장에서 말씀드린&lt;br&gt;두 번째 약점이 여기서 해소됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 조직 수준 구성&lt;/h2&gt;
&lt;p&gt;조직에 한 번 걸면 그 아래 &lt;strong&gt;모든 VPC 네트워크의 서브넷, VLAN 연결, Cloud VPN&lt;br&gt;터널&lt;/strong&gt;에 적용됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud network-management vpc-flow-logs-configs create CONFIG_NAME \
  --location=global --organization=ORG_ID \
  --network=projects/HOST_PRJ/global/networks/VPC \
  --flow-sampling=1.0 --metadata=INCLUDE_ALL_METADATA&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;새 서브넷에 로그 켜는 것을 잊는 실수가 사라집니다.&lt;/strong&gt; 운영 부담이 눈에 띄게&lt;br&gt;줄어듭니다.&lt;/p&gt;
&lt;h3&gt;GA 입니다 (단, gcloud 를 올리셔야 합니다)&lt;/h3&gt;
&lt;p&gt;이 기능은 한동안 beta 였습니다. &lt;strong&gt;현재는 GA 이지만 낡은 gcloud 에서는 플래그가&lt;br&gt;보이지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;529.0.0 (2025.06)   GA 에 --vpn-tunnel, --interconnect-attachment 만
591.0.0 (2026.10)   GA 에 --network, --subnet, --organization 전부&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;--network&lt;/code&gt; 이나 &lt;code&gt;--organization&lt;/code&gt; 이 없다면 먼저 &lt;code&gt;gcloud components update&lt;/code&gt; 를&lt;br&gt;하십시오. 「지원하지 않는 기능」으로 오해하고 넘어가기 쉽습니다.&lt;/p&gt;
&lt;h3&gt;필요한 권한&lt;/h3&gt;
&lt;p&gt;조직 수준 구성을 만들려면 &lt;strong&gt;조직 수준 &lt;code&gt;Network Management Admin&lt;/code&gt;&lt;/strong&gt; 역할이&lt;br&gt;필요합니다. 프로젝트 권한만으로는 되지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 겹치면 어느 쪽이 이기는가&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;아무 쪽도 이기지 않습니다.&lt;/strong&gt; 이것이 가장 많이 오해하는 지점입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Each configuration generates a separate set of flow logs.&lt;br&gt;(범위가 겹치면) your logging information might contain duplicate logs&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;우선순위라는 개념이 없고, &lt;strong&gt;각 구성이 자기 로그를 따로 만듭니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;조직 구성 (flow_sampling 1.0)    → 로그 세트 A
서브넷 설정 (flow_sampling 0.5)   → 로그 세트 B
                                   둘 다 쌓입니다&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;전환하실 때&lt;/h3&gt;
&lt;p&gt;조직 수준 구성을 도입하면서 기존 서브넷 설정을 그대로 두면 &lt;strong&gt;같은 트래픽이 두 번&lt;br&gt;기록되고 로그 비용도 두 배&lt;/strong&gt;가 됩니다. 조직 구성을 건 뒤 서브넷 쪽을 반드시&lt;br&gt;끄십시오.&lt;/p&gt;
&lt;h3&gt;거꾸로 이용할 수도 있습니다&lt;/h3&gt;
&lt;p&gt;조직 전체에는 낮은 샘플링으로 기본선을 깔고, 민감한 서브넷에만 100% 구성을 따로&lt;br&gt;얹는 구성이 가능합니다. 중복이 의도된 설계라면 문제가 되지 않습니다.&lt;/p&gt;
&lt;h3&gt;중복 확인 방법&lt;/h3&gt;
&lt;p&gt;적용 중인 구성을 전부 보여주는 조회가 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;그 리소스를 직접 대상으로 한 구성
그 리소스가 속한 VPC 네트워크의 구성
소속 조직의 구성
Compute Engine API 로 만든 서브넷 구성       ← 이것도 함께 나옵니다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;두 체계가 한 화면에 나오므로&lt;/strong&gt; 전환 후 서브넷 설정이 남아 있는지 확인하기&lt;br&gt;좋습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 조직 정책으로 강제하기&lt;/h2&gt;
&lt;p&gt;구성이 「켜 주는 것」이라면, 조직 정책은 &lt;strong&gt;「끄지 못하게 막는 것」&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;constraints/compute.requireVpcFlowLogs&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;흐름 로그 활성화를 요구하고 &lt;strong&gt;샘플링 비율의 하한&lt;/strong&gt;까지 정합니다. 필터링은 허용되지&lt;br&gt;않습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;샘플링 비율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ESSENTIAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.1 이상 0.5 미만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LIGHT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.5 이상 1.0 미만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COMPREHENSIVE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;값을 조합해 하한을 정합니다. 셋 다 허용하면 최소 10%, &lt;code&gt;LIGHT&lt;/code&gt;+&lt;code&gt;COMPREHENSIVE&lt;/code&gt; 면&lt;br&gt;최소 50%, &lt;code&gt;COMPREHENSIVE&lt;/code&gt; 만 두면 100% 를 강제합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: organizations/ORG_ID/policies/compute.requireVpcFlowLogs
spec:
  rules:
  - values:
      allowedValues:
      - LIGHT
      - COMPREHENSIVE        # 최소 50% 를 강제합니다&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;가장 쓸모 있는 동작&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아무것도 지정하지 않고 서브넷을 만들면, 흐름 로그가 켜진 채로 정책의 최소 비율이&lt;br&gt;적용됩니다.&lt;/strong&gt; 담당자가 &lt;code&gt;--enable-flow-logs&lt;/code&gt; 를 잊어도 자동으로 켜집니다. 앞서 말한&lt;br&gt;기본값 50% 문제도 여기서 해소됩니다.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;경로에 따라 동작이 다릅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;생성 경로&lt;/th&gt;
&lt;th&gt;아무것도 지정 안 함&lt;/th&gt;
&lt;th&gt;켜기 (비율 충족)&lt;/th&gt;
&lt;th&gt;켜기 (비율 미달)&lt;/th&gt;
&lt;th&gt;끄기&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gcloud · API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;자동으로 켜짐&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;생성됨&lt;/td&gt;
&lt;td&gt;오류&lt;/td&gt;
&lt;td&gt;오류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;콘솔&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;불가&lt;/strong&gt; (선택 필수)&lt;/td&gt;
&lt;td&gt;생성됨&lt;/td&gt;
&lt;td&gt;오류&lt;/td&gt;
&lt;td&gt;오류&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;콘솔은 미지정 상태를 둘 수 없어 켜거나 끄거나를 골라야 하는데, 끄면 오류로&lt;br&gt;막힙니다. 결국 켜는 것 말고는 선택지가 없습니다.&lt;/p&gt;
&lt;h3&gt;걸리는 지점 셋&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;① 기존 서브넷은 영향받지 않습니다.&lt;/strong&gt; 정책을 걸어도 이미 있는 서브넷은&lt;br&gt;그대로이고, 구성을 변경할 때 비로소 적용됩니다. &lt;strong&gt;운영 중인 서브넷을 일괄&lt;br&gt;점검하는 별도 작업이 필요합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② Compute Engine API 쪽에만 적용됩니다.&lt;/strong&gt; Network Management API 로 만든 구성은&lt;br&gt;이 정책의 통제를 받지 않습니다. 문서는 Network Management API 사용을 권하면서,&lt;br&gt;정작 조직 정책은 구 체계만 통제하는 상태입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 부분 병합이 안 됩니다.&lt;/strong&gt; 적용 방식이 &lt;code&gt;Replace&lt;/code&gt; 뿐이라 상위 정책을 하위에서&lt;br&gt;완화하려면 통째로 교체해야 합니다. 계층 설계를 처음부터 신중히 하셔야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;roles/orgpolicy.policyAdmin&lt;/code&gt; 이 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 랜딩존 적용 제안&lt;/h2&gt;
&lt;p&gt;세 가지를 조합하면 빈틈이 없습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;조직 정책 &lt;code&gt;requireVpcFlowLogs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;끄지 못하게 막고, 최소 비율을 보장합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network Management API 조직 구성&lt;/td&gt;
&lt;td&gt;VPN 터널·인터커넥트까지 포함해 일괄로 켭니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;방화벽 규칙 로깅 (catch-all deny)&lt;/td&gt;
&lt;td&gt;어느 규칙이 막았는지 남깁니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;다만 앞의 둘을 동시에 쓰면 중복 로그가 생깁니다.&lt;/strong&gt; 조직 정책이 서브넷 속성을&lt;br&gt;켜고, 조직 구성이 또 자기 로그를 만들기 때문입니다. 비용을 보신다면 둘 중 하나로&lt;br&gt;정하시고, 4장의 조회로 중복 여부를 확인하십시오.&lt;/p&gt;
&lt;p&gt;세 번째 항목은 흐름 로그로는 알 수 없는 영역입니다. 이전 글에서 다루었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 적용 전에 확인하셔야 할 것&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Terraform 과 조직 정책의 궁합&lt;/strong&gt;이 문서에 없습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;google_compute_subnetwork&amp;quot; &amp;quot;this&amp;quot; {
  # log_config 블록 없음 → 미지정일까, 끄기일까?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;프로바이더가 「끄기」로 전송한다면 정책에 걸려 &lt;strong&gt;&lt;code&gt;apply&lt;/code&gt; 가 실패&lt;/strong&gt;합니다. 「미지정」&lt;br&gt;이면 정책이 켜 주지만, 이번에는 &lt;strong&gt;상태 드리프트&lt;/strong&gt;가 생깁니다. Terraform 은 꺼져&lt;br&gt;있다고 알고 있는데 실제로는 켜져 있으니, 다음 &lt;code&gt;plan&lt;/code&gt; 에서 끄려 시도하고 또 실패하는&lt;br&gt;순환에 빠질 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IaC 로 관리하신다면 조직 정책을 켜기 전에 테스트 폴더에서 반드시 확인하십시오.&lt;/strong&gt;&lt;br&gt;파이프라인이 깨지면 복구가 번거롭습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 문서&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;th&gt;문서&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;흐름 로그 구성&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/vpc/docs/using-flow-logs&quot;&gt;Configure VPC Flow Logs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;흐름 로그 개요와 샘플링&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/vpc/docs/flow-logs&quot;&gt;VPC Flow Logs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조직 정책 제약&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/vpc/docs/org-policy-flow-logs&quot;&gt;Configure organization policy constraints for VPC Flow Logs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;콘솔에서는 &lt;code&gt;https://console.cloud.google.com/networking/vpc-flow-logs&lt;/code&gt; 에서 조직&lt;br&gt;수준과 프로젝트 수준 구성을 관리합니다. 서브넷 설정은 VPC 네트워크 페이지의&lt;br&gt;&lt;code&gt;Subnets in current project&lt;/code&gt; 탭에서 &lt;code&gt;Manage flow logs&lt;/code&gt; 로 들어갑니다.&lt;/p&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/8</guid>
      <comments>https://pubsub.tistory.com/8#entry8comment</comments>
      <pubDate>Fri, 30 Oct 2026 09:58:24 +0900</pubDate>
    </item>
    <item>
      <title># 묵시적 방화벽 규칙에 막힌 트래픽 로그 식별해보기</title>
      <link>https://pubsub.tistory.com/7</link>
      <description>&lt;p&gt;GCP 에서 VPC 를 만들면 방화벽 규칙을 하나도 만들지 않아도 통신이 통제됩니다.&lt;br&gt;이 「보이지 않는 규칙」에 막힌 트래픽이 로그 어디에 어떤 형태로 남는지, 실제로&lt;br&gt;트래픽을 흘려 보내며 확인했습니다. &lt;strong&gt;통념과 다른 결과가 나왔습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 흔한 오해부터 바로잡기&lt;/h2&gt;
&lt;p&gt;VPC 를 만들 때 묵시적 규칙이 들어간다는 것은 널리 알려져 있습니다. 그런데 이를&lt;br&gt;「기본적으로 ingress 와 egress 가 모두 차단된다」고 설명하는 경우를 자주 봅니다.&lt;br&gt;&lt;strong&gt;절반만 맞습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;콘솔의 VPC 생성 화면 맨 아래를 보면 답이 나와 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deny-all-ingress    Ingress   0.0.0.0/0   all   Deny    65,535
allow-all-egress    Egress    0.0.0.0/0   all   Allow   65,535&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;egress 는 Allow 입니다.&lt;/strong&gt; 아무것도 설정하지 않은 VPC 의 VM 은 인터넷을 포함한&lt;br&gt;모든 목적지로 나갈 수 있습니다. 랜딩존에서 외부 유출을 통제하시려면 egress deny 를&lt;br&gt;명시적으로 만들어야 합니다. 가만히 두면 열려 있습니다.&lt;/p&gt;
&lt;h3&gt;체크박스가 없다는 것의 의미&lt;/h3&gt;
&lt;p&gt;같은 화면에서 위쪽 네 개(&lt;code&gt;allow-custom&lt;/code&gt;, &lt;code&gt;allow-icmp&lt;/code&gt;, &lt;code&gt;allow-rdp&lt;/code&gt;, &lt;code&gt;allow-ssh&lt;/code&gt;)에는&lt;br&gt;체크박스가 있지만, 아래 두 개에는 없습니다. 우선순위도 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;우선순위&lt;/th&gt;
&lt;th&gt;성격&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;체크박스 있음&lt;/td&gt;
&lt;td&gt;65,534&lt;/td&gt;
&lt;td&gt;선택하면 &lt;strong&gt;실제 규칙 객체&lt;/strong&gt;로 생성되고, 나중에 삭제할 수 있습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;체크박스 없음&lt;/td&gt;
&lt;td&gt;65,535&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;묵시적 규칙&lt;/strong&gt;입니다. 끌 수도 지울 수도 없습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;VPC 를 만든 뒤 방화벽 목록을 조회하면 이 차이가 분명해집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ gcloud compute firewall-rules list --filter=&amp;quot;network:lab-fwlog-vpc&amp;quot;
NAME                     DIRECTION  PRIORITY  ALLOW   DENY  ENABLE
lab-fwlog-allow-iap-ssh  INGRESS    1000      tcp:22        False&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;묵시적 규칙은 목록에 나오지 않습니다.&lt;/strong&gt; 규칙 객체가 아니라 네트워크에 내장된&lt;br&gt;동작이기 때문입니다. 그리고 바로 이 점이 다음 질문으로 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 질문 :: 그렇다면 로그는 어떻게 되는가&lt;/h2&gt;
&lt;p&gt;방화벽 로깅은 &lt;strong&gt;규칙마다 개별적으로 켜는&lt;/strong&gt; 기능입니다. 묵시적 규칙은 규칙 객체가&lt;br&gt;아니니 로깅을 켤 대상 자체가 없습니다. 공식 문서도 미지원 대상으로 못 박고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Implied deny ingress and implied allow egress rules&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;여기에 흐름 로그(VPC Flow Logs) 문서의 다음 문장이 겹칩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;If an ingress firewall rule denies inbound packets, those packets aren&amp;#39;t sampled&lt;br&gt;by VPC Flow Logs.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;두 문장을 이어 붙이면 「묵시적 deny 에 막힌 트래픽은 어디에도 남지 않는다」는 결론이&lt;br&gt;나옵니다. 실제로 많은 글이 그렇게 설명합니다. &lt;strong&gt;그런데 실험해 보니 달랐습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 실험 설계&lt;/h2&gt;
&lt;h3&gt;환경&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;VPC      커스텀 모드 (사전 생성 규칙이 없습니다)
서브넷    10.10.0.0/24, 흐름 로그 100% 샘플링, 5초 집계, 전체 메타데이터
VM       e2-micro × 2 (클라이언트 10.10.0.2, 타겟 10.10.0.3), 외부 IP 없음
방화벽    IAP SSH 용 1개로 시작&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;커스텀 모드로 만드는 것이 중요합니다. 자동 모드나 &lt;code&gt;default&lt;/code&gt; 네트워크는&lt;br&gt;&lt;code&gt;allow-ssh&lt;/code&gt; 같은 규칙이 미리 들어가 있어 실험이 오염됩니다.&lt;/p&gt;
&lt;h3&gt;시나리오&lt;/h3&gt;
&lt;p&gt;같은 트래픽(8080 포트로 &lt;code&gt;curl&lt;/code&gt; 30회)을 방화벽 상태만 바꿔 가며 반복합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;확인하려는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;8080 &lt;strong&gt;허용&lt;/strong&gt; 규칙&lt;/td&gt;
&lt;td&gt;대조군 :: 정상일 때 어떤 로그가 남는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;규칙 없음 (&lt;strong&gt;묵시적 deny&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;핵심 질문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;명시적 deny&lt;/strong&gt; 65534 + 로깅&lt;/td&gt;
&lt;td&gt;권장 해법이 실제로 통하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;ICMP (ping 20회)&lt;/td&gt;
&lt;td&gt;프로토콜에 따른 차이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;egress deny&lt;/strong&gt; (로깅 끔)&lt;/td&gt;
&lt;td&gt;방향에 따른 차이&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A 를 넣은 이유가 있습니다. 흐름 로그는 두 단계로 샘플링되는데, 그중 1차 샘플링은&lt;br&gt;&lt;strong&gt;사용자가 제어할 수 없고&lt;/strong&gt; 물리 호스트 부하에 따라 비율이 달라집니다. 짧은 TCP&lt;br&gt;연결은 이 단계에서 누락될 수 있습니다. 대조군 없이 「로그가 없다」는 결과만 보면&lt;br&gt;차단 때문인지 샘플링 운 때문인지 구분할 수 없습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;샘플링은 2단계입니다&lt;/strong&gt;&lt;br&gt;① 기본 샘플링 :: 제어 불가, 호스트 부하에 따라 동적&lt;br&gt;② 집계 :: 같은 연결의 패킷을 하나의 항목으로 묶음 (기본 5초)&lt;br&gt;③ 2차 샘플링 :: 우리가 &lt;code&gt;1.0&lt;/code&gt; 으로 설정하는 값&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1.0&lt;/code&gt; 은 「①이 건진 것을 버리지 않겠다」는 뜻이지 「모든 패킷을 남기겠다」가&lt;br&gt;아닙니다. 공식 문서도 &lt;code&gt;VPC Flow Logs doesn&amp;#39;t capture every packet&lt;/code&gt; 이라고&lt;br&gt;못 박고 있습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h4&gt;2차 샘플링의 기본값은 50% 입니다&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;콘솔에서 체크박스만 켜고 넘어간 서브넷은 절반만 남고 있습니다.&lt;/strong&gt; 랜딩존을&lt;br&gt;Terraform 으로 구성하셨다면 &lt;code&gt;logging_flow_sampling&lt;/code&gt; 을 명시하지 않는 한 0.5 입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;google_compute_subnetwork&amp;quot; &amp;quot;this&amp;quot; {
  # ...
  log_config {
    aggregation_interval = &amp;quot;INTERVAL_5_SEC&amp;quot;
    flow_sampling        = 1.0              # 생략하면 0.5 입니다
    metadata             = &amp;quot;INCLUDE_ALL_METADATA&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 실험에서는 비교를 위해 1.0 으로 두었습니다. 그런데도 뒤에서 보시듯 전부&lt;br&gt;남지는 않았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;흐름 로그를 켜는 방법은 서브넷 속성 말고 하나 더 있고, 조직 수준에 한 번에&lt;br&gt;걸 수도 있습니다. 그쪽은 이 글의 주제를 벗어나므로 &lt;strong&gt;다음 글&lt;/strong&gt;에서 따로&lt;br&gt;다루겠습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;4. 결과 :: 드롭 전용 레코드가 따로 있었다&lt;/h2&gt;
&lt;p&gt;묵시적 deny 만 있던 구간에서 흐름 로그를 열어 보니 이런 항목이 있었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;시각 07:26:44   (방화벽 규칙은 IAP SSH 용 하나뿐)
  reporter        = DEST
  10.10.0.2:52952 → 10.10.0.3:8080
  disposition     = DROPPED
  drop_reason     = FIREWALL_DENY
  packets_dropped = 4&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;남습니다.&lt;/strong&gt; 그것도 차단 이유까지 명시해서 남습니다.&lt;/p&gt;
&lt;p&gt;핵심은 이 레코드가 &lt;strong&gt;일반 흐름 로그와 필드 구성이 아예 다르다&lt;/strong&gt;는 점입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;일반 레코드&lt;/th&gt;
&lt;th&gt;드롭 레코드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;공통&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connection&lt;/code&gt; &lt;code&gt;reporter&lt;/code&gt; &lt;code&gt;src_vpc&lt;/code&gt; &lt;code&gt;dest_vpc&lt;/code&gt; &lt;code&gt;src_instance&lt;/code&gt; &lt;code&gt;dest_instance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;고유&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bytes_sent&lt;/code&gt; &lt;code&gt;packets_sent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;disposition&lt;/code&gt; &lt;code&gt;drop_reason&lt;/code&gt; &lt;code&gt;packets_dropped&lt;/code&gt; &lt;code&gt;throughput_inclusion&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;앞서 인용한 &amp;quot;ingress deny 는 샘플링되지 않는다&amp;quot;는 문장은 &lt;strong&gt;일반 레코드에만&lt;/strong&gt;&lt;br&gt;해당합니다. 드롭 레코드는 그 경로를 타지 않습니다. 문서에 &amp;quot;VPC Flow Logs generates&lt;br&gt;log records for dropped traffic&amp;quot; 이라는 별도 문장이 있는데, 두 문장이 다른 섹션에&lt;br&gt;떨어져 있어 연결해서 읽기 어렵습니다.&lt;/p&gt;
&lt;h3&gt;방화벽 로그는 어떤가&lt;/h3&gt;
&lt;p&gt;같은 구간을 방화벽 로그에서 조회하면 대비가 선명합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구간&lt;/th&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;방화벽 로그&lt;/th&gt;
&lt;th&gt;흐름 로그 드롭 레코드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;07:26:10~&lt;/td&gt;
&lt;td&gt;묵시적 deny 만&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0건&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FIREWALL_DENY&lt;/code&gt; 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07:29:11~&lt;/td&gt;
&lt;td&gt;명시적 deny 65534 + 로깅&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3건 &lt;code&gt;DENIED&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FIREWALL_DENY&lt;/code&gt; 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;대조군을 넣은 전체 결과&lt;/h3&gt;
&lt;p&gt;같은 트래픽 30회를 다섯 조건에서 반복했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;방화벽 로그&lt;/th&gt;
&lt;th&gt;흐름 로그 일반&lt;/th&gt;
&lt;th&gt;흐름 로그 드롭&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8080 허용&lt;/td&gt;
&lt;td&gt;30건 &lt;code&gt;ALLOWED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SRC=15 &lt;strong&gt;DEST=8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;묵시적 deny&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0건&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SRC=7 &lt;strong&gt;DEST=0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DEST=2&lt;/strong&gt; &lt;code&gt;FIREWALL_DENY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;명시적 deny 65534 + 로깅&lt;/td&gt;
&lt;td&gt;30건 &lt;code&gt;DENIED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;DEST=4 &lt;code&gt;FIREWALL_DENY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ICMP (ping 20회)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0건&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SRC=1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;egress deny (로깅 끔)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0건&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SRC=4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SRC=4&lt;/strong&gt; &lt;code&gt;FIREWALL_DENY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;A 와 B 의 &lt;code&gt;DEST&lt;/code&gt; 칼럼을 비교하십시오.&lt;/strong&gt; 똑같은 트래픽인데 허용일 때는 DEST 일반&lt;br&gt;레코드가 8건 나오고, 차단일 때는 0건이 되면서 드롭 레코드로 바뀌었습니다. 샘플링&lt;br&gt;운으로는 설명되지 않습니다. 대조군을 넣은 보람이 여기에 있습니다.&lt;/p&gt;
&lt;h3&gt;드롭 레코드의 reporter 는 막힌 쪽을 가리킵니다&lt;/h3&gt;
&lt;p&gt;B 와 E 를 나란히 놓으면 규칙성이 드러납니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;차단 방향&lt;/th&gt;
&lt;th&gt;드롭 레코드의 reporter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ingress&lt;/strong&gt; 차단 (B)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DEST&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;egress&lt;/strong&gt; 차단 (E)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SRC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;패킷이 버려진 지점에서 보고하기 때문입니다. &lt;strong&gt;reporter 값만 봐도 어느 방향에서&lt;br&gt;막혔는지 알 수 있습니다.&lt;/strong&gt; 장애 대응에서 쓸 만한 단서입니다.&lt;/p&gt;
&lt;h3&gt;그런데 숫자가 맞지 않습니다&lt;/h3&gt;
&lt;p&gt;30회를 보냈는데 흐름 로그에 남은 것은 그보다 훨씬 적습니다. 2차 샘플링을 &lt;code&gt;1.0&lt;/code&gt; 으로&lt;br&gt;두었는데도 그렇습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A :: 30회 전송 → 일반 레코드 SRC 15건 · DEST 8건
B :: 30회 전송 → 드롭 레코드 2건
C :: 30회 전송 → 드롭 레코드 4건&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;반면 &lt;strong&gt;방화벽 로그는 30건을 빠짐없이 기록했습니다.&lt;/strong&gt; 방화벽 로깅은 샘플링하지 않고&lt;br&gt;연결 단위로 전부 남깁니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;「몇 번 시도했는가」를 세어야 한다면 흐름 로그를 쓰시면 안 됩니다.&lt;br&gt;흐름 로그의 숫자는 추정치이고, 방화벽 로그의 숫자는 실측치입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;차단된 트래픽일수록 덜 남습니다&lt;/h3&gt;
&lt;p&gt;위 숫자를 다시 보시면 규칙성이 하나 보입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시나리오&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;th&gt;흐름 로그에 남은 수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;허용&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23건&lt;/strong&gt; (SRC 15 + DEST 8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;묵시적 deny&lt;/td&gt;
&lt;td&gt;9건 (일반 7 + 드롭 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;명시적 deny&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4건&lt;/strong&gt; (드롭만)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;같은 30회인데 허용일 때가 차단일 때보다 &lt;strong&gt;대여섯 배 많이 남았습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;허용된 연결은 핸드셰이크를 마친 뒤 데이터가 오가므로 기본 샘플링에 걸릴 기회가&lt;br&gt;여러 번 생깁니다. 반면 차단된 연결은 SYN 몇 개가 전부이고, 그마저 재전송으로&lt;br&gt;끝납니다. 샘플링될 확률이 구조적으로 낮습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;보안 관점에서 가장 보고 싶은 트래픽이 가장 적게 남는 셈입니다.&lt;/strong&gt; 차단 추적을&lt;br&gt;흐름 로그에 의존하시면 안 되는 이유가 여기에 하나 더 있습니다.&lt;/p&gt;
&lt;p&gt;연결 시도 자체를 빠짐없이 보셔야 한다면 &lt;strong&gt;TCP 연결 로깅&lt;/strong&gt;을 켜십시오. SYN 과&lt;br&gt;SYN-ACK 을 기록하므로 짧은 연결도 잡힙니다. 다만 2차 샘플링은 이 항목에도&lt;br&gt;적용되고, vCPU 당 초당 100개 · VM 당 초당 800개라는 상한이 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 그래서 두 로그는 무엇이 다른가&lt;/h2&gt;
&lt;p&gt;이것이 이 글의 결론입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;흐름 로그 드롭 레코드&lt;/th&gt;
&lt;th&gt;방화벽 규칙 로그&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;묵시적 규칙에 막힌 트래픽&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;기록됨&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;기록되지 않음 (&lt;strong&gt;켤 수 없음&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;명시적 규칙 + 로깅 &lt;strong&gt;ON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;기록됨&lt;/td&gt;
&lt;td&gt;기록됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;명시적 규칙 + 로깅 &lt;strong&gt;OFF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;기록됨&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;기록되지 않음&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;차단 사실&lt;/td&gt;
&lt;td&gt;알 수 있음&lt;/td&gt;
&lt;td&gt;알 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;어느 규칙이 막았는지&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;알 수 없음&lt;/strong&gt; (&lt;code&gt;FIREWALL_DENY&lt;/code&gt; 뿐)&lt;/td&gt;
&lt;td&gt;알 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;켜는 단위&lt;/td&gt;
&lt;td&gt;서브넷&lt;/td&gt;
&lt;td&gt;규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로토콜&lt;/td&gt;
&lt;td&gt;TCP, UDP, ICMP, ESP, GRE, RDMA&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TCP, UDP 뿐&lt;/strong&gt; (ping 20회 → 0건)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;샘플링&lt;/td&gt;
&lt;td&gt;적용됨 (30회 → 2~15건)&lt;/td&gt;
&lt;td&gt;없음 (30회 → 30건)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;건수의 성격&lt;/td&gt;
&lt;td&gt;추정치&lt;/td&gt;
&lt;td&gt;실측치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;「막혔다」는 사실은 흐름 로그로, 「무엇이 막았다」는 방화벽 로그로 봅니다.&lt;/strong&gt;&lt;br&gt;둘 중 하나만으로는 장애 대응이 되지 않습니다.&lt;/p&gt;
&lt;p&gt;표에서 보시듯 &lt;strong&gt;갈리는 기준은 「묵시적이냐 명시적이냐」가 아니라 「로깅을 켰느냐」&lt;/strong&gt;&lt;br&gt;입니다. 시나리오 E 가 그 증거입니다. 명시적 egress deny 규칙을 만들되 로깅을 끈&lt;br&gt;채로 두었더니 방화벽 로그는 0건이었고, 흐름 로그에만 드롭 레코드가 남았습니다.&lt;br&gt;묵시적 규칙과 결과가 똑같습니다. 묵시적 규칙은 &lt;strong&gt;켤 방법 자체가 없을 뿐&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;drop_reason&lt;/code&gt; 은 &lt;code&gt;FIREWALL_DENY&lt;/code&gt; 라고만 알려 줄 뿐, 묵시적 규칙인지 내가 만든&lt;br&gt;규칙인지 구분해 주지 않습니다. 규칙이 수십 개인 환경에서 「왜 막혔지」를 추적하려면&lt;br&gt;결국 방화벽 로그가 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 실무 권고&lt;/h2&gt;
&lt;h3&gt;묵시적 deny 자리에 똑같은 규칙을 직접 만듭니다&lt;/h3&gt;
&lt;p&gt;동작은 그대로이면서 로그만 생깁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;google_compute_firewall&amp;quot; &amp;quot;deny_all_ingress_logged&amp;quot; {
  name          = &amp;quot;fw-deny-all-ingress-log&amp;quot;
  network       = google_compute_network.this.id
  direction     = &amp;quot;INGRESS&amp;quot;
  priority      = 65534              # 묵시적 규칙보다 1 높습니다
  source_ranges = [&amp;quot;0.0.0.0/0&amp;quot;]

  deny { protocol = &amp;quot;tcp&amp;quot; }
  deny { protocol = &amp;quot;udp&amp;quot; }

  log_config {
    metadata = &amp;quot;INCLUDE_ALL_METADATA&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;우선순위를 65534 로 두면 다른 모든 규칙(보통 1000번대 이하)이 먼저 걸리고,&lt;br&gt;어디에도 해당하지 않은 트래픽만 여기에 떨어집니다.&lt;/p&gt;
&lt;p&gt;다만 이 구성에서 &lt;strong&gt;&lt;code&gt;rule_details&lt;/code&gt; 가 비어 나오는 현상&lt;/strong&gt;을 겪었습니다. 같은&lt;br&gt;&lt;code&gt;INCLUDE_ALL_METADATA&lt;/code&gt; 설정인데 허용 규칙에서는 전부 채워지고 거부 규칙에서는&lt;br&gt;빈 객체였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A (allow, priority 1000)   rule_details = {action, direction, ip_port_info,
                                           priority, reference, source_range}
C (deny,  priority 65534)  rule_details = {}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;두 규칙은 동작, 우선순위, 프로토콜 지정 방식(&lt;code&gt;tcp:8080&lt;/code&gt; 하나 / &lt;code&gt;tcp,udp,icmp&lt;/code&gt; 셋),&lt;br&gt;소스 범위가 모두 달라 원인을 특정하지 못했습니다. 문서에 「&lt;code&gt;ip_protocol&lt;/code&gt; 을 &lt;code&gt;ALL&lt;/code&gt; 로&lt;br&gt;설정한 규칙은 로깅 미지원」이라는 조항이 있어, 프로토콜을 묶어 지정한 것이 영향을&lt;br&gt;주었을 가능성이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;규칙 이름까지 로그에 남기셔야 한다면 적용 후 실제 로그를 열어 확인하시고, 비어&lt;br&gt;있다면 프로토콜별로 규칙을 나눠 만들어 보십시오.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;로깅은 규칙마다 켜야 하고, 기본값은 꺼짐입니다&lt;/h3&gt;
&lt;p&gt;규칙을 잘 만들어 두고 로깅을 안 켜면 아무것도 남지 않습니다. 묵시적 규칙을 그대로&lt;br&gt;둔 것과 결과가 같습니다.&lt;/p&gt;
&lt;p&gt;Terraform 에서는 &lt;strong&gt;&lt;code&gt;log_config&lt;/code&gt; 블록의 유무가 곧 스위치&lt;/strong&gt;입니다. &lt;code&gt;enable = false&lt;/code&gt;&lt;br&gt;같은 인자가 따로 있는 것이 아니라서, 블록을 빠뜨리면 조용히 꺼진 채 배포됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;log_config {
  metadata = &amp;quot;INCLUDE_ALL_METADATA&amp;quot;   # 또는 EXCLUDE_ALL_METADATA
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존 규칙은 재생성 없이 바로 켤 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute firewall-rules update RULE_NAME \
  --enable-logging --logging-metadata=include-all&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 상태는 이렇게 확인합니다. &lt;code&gt;ENABLE&lt;/code&gt; 이 &lt;code&gt;False&lt;/code&gt; 면 꺼진 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute firewall-rules list \
  --format=&amp;quot;table(name,direction,priority,denied[],logConfig.enable)&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;어디에 켤지는 골라야 합니다&lt;/h4&gt;
&lt;p&gt;전부 켜면 로그 비용이 적지 않습니다. 우선순위를 두시면 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;규칙&lt;/th&gt;
&lt;th&gt;로깅&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;catch-all deny (65534)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;반드시&lt;/td&gt;
&lt;td&gt;「왜 막혔는지」가 여기서만 나옵니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개별 deny 규칙&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;td&gt;의도한 차단인지 확인합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인터넷 향 allow&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;td&gt;누가 들어오는지 확인합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내부 통신 allow&lt;/td&gt;
&lt;td&gt;선별&lt;/td&gt;
&lt;td&gt;양이 많고 대부분 정상 트래픽입니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;egress 는 따로 막아야 합니다&lt;/h3&gt;
&lt;p&gt;1장에서 짚었듯 egress 는 기본 허용입니다. 통제가 필요하시면 명시적 deny 를&lt;br&gt;만드십시오.&lt;/p&gt;
&lt;h3&gt;비용을 보고 조정합니다&lt;/h3&gt;
&lt;p&gt;인터넷에 노출된 VPC 에 catch-all deny 로깅을 걸면 스캔 트래픽이 전부 쌓입니다.&lt;br&gt;며칠 관찰한 뒤 &lt;code&gt;_Default&lt;/code&gt; 싱크의 제외 필터를 조정하시기를 권합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 문서&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;th&gt;문서&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;VPC 방화벽 규칙과 묵시적 규칙&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/firewall/docs/firewalls&quot;&gt;VPC firewall rules&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;방화벽 규칙 로깅&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/vpc/docs/firewall-rules-logging&quot;&gt;Firewall rules logging&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;흐름 로그와 샘플링&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/vpc/docs/flow-logs&quot;&gt;VPC Flow Logs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;규칙 평가 순서&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.cloud.google.com/firewall/docs/firewall-policies-rule-eval-order&quot;&gt;Firewall rule evaluation order&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/7</guid>
      <comments>https://pubsub.tistory.com/7#entry7comment</comments>
      <pubDate>Fri, 30 Oct 2026 09:56:58 +0900</pubDate>
    </item>
    <item>
      <title># service-xxx@gcp-sa-&amp;hellip; 는 뭐고 왜 권한을 줘야 하나 :: GCP 서비스 에이전트</title>
      <link>https://pubsub.tistory.com/6</link>
      <description>&lt;p&gt;Shared VPC 에 Cloud Run 을 띄우려다 이런 안내를 보신 적이 있으실 겁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service-123456789@serverless-robot-prod.iam.gserviceaccount.com 에
roles/compute.networkUser 를 부여하십시오&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;만든 적 없는 계정입니다. 서비스 계정 목록에도 안 보입니다. 그런데 권한을 주라고 합니다.&lt;/p&gt;
&lt;p&gt;이것이 &lt;strong&gt;서비스 에이전트&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한 줄로&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Google 이 만들고 Google 이 소유하는 서비스 계정&lt;/strong&gt;입니다. 서비스가 내 자원을 대신&lt;br&gt;만질 때 쓰는 신원입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Google Cloud &lt;strong&gt;creates and manages&lt;/strong&gt; [service agents] ... &lt;strong&gt;You can&amp;#39;t access them directly&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;내가 만든 서비스 계정과는 성격이 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;서비스 에이전트&lt;/th&gt;
&lt;th&gt;내가 만든 서비스 계정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;생성&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;API 켤 때 자동&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;사람이 만듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소유&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;키 발급&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;불가&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;불가 (API 를 끄면 사라짐)&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service-accounts list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;안 나옴&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;나옴&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;왜 필요한가&lt;/h2&gt;
&lt;p&gt;Cloud Run 이 Shared VPC 의 서브넷을 쓰려면 &lt;strong&gt;누군가 그 서브넷에 접근&lt;/strong&gt;해야 합니다.&lt;br&gt;내 애플리케이션이 아니라 &lt;strong&gt;Cloud Run 이라는 서비스 자체&lt;/strong&gt;가 하는 일입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;내 코드          →  런타임 서비스 계정으로 API 호출
Cloud Run 자체   →  서비스 에이전트로 서브넷·이미지·로그 접근&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;둘이 완전히 다른 신원입니다. 런타임 계정에 아무리 권한을 줘도 &lt;strong&gt;서브넷 문제는 풀리지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;내 프로젝트에 몇 개나 있나&lt;/h2&gt;
&lt;p&gt;목록 조회 명령에는 나오지 않습니다. IAM 정책을 뒤져야 보입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects get-iam-policy PROJECT_ID \
  --flatten=&amp;#39;bindings[].members&amp;#39; --format=&amp;#39;value(bindings.members)&amp;#39; \
| grep -E &amp;#39;serverless-robot|gcp-sa-|-system\.|container-engine|gs-project&amp;#39; \
| sed &amp;#39;s/serviceAccount://&amp;#39; | sort -u&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;제가 쓰는 프로젝트 하나에서 &lt;strong&gt;46개&lt;/strong&gt;가 나왔습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service-N@serverless-robot-prod…       Cloud Run
service-N@compute-system…              Compute Engine
service-N@container-engine-robot…      GKE
service-N@gs-project-accounts…         Cloud Storage
service-N@gcp-sa-artifactregistry…     Artifact Registry
service-N@gcp-sa-pubsub…               Pub/Sub
service-N@gcp-sa-cloudkms…             Cloud KMS
service-N@gcp-sa-aiplatform…           Vertex AI
service-N@gcp-sa-eventarc…             Eventarc
…&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;콘솔에서 보시려면 IAM 화면의 &lt;strong&gt;「Google 제공 역할 부여 포함」&lt;/strong&gt; 체크박스를 켜야 합니다.&lt;br&gt;기본값이 꺼져 있어서 안 보입니다.&lt;/p&gt;
&lt;h3&gt;주소 형식이 제각각입니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gcp-sa-&amp;lt;서비스&amp;gt;.iam.gserviceaccount.com      비교적 최근 관례
serverless-robot-prod.iam…                  Cloud Run
compute-system.iam…                         Compute
container-engine-robot.iam…                 GKE
gs-project-accounts.iam…                    Cloud Storage&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;외워서 쓸 수가 없습니다.&lt;/strong&gt; 오래된 서비스일수록 고유한 도메인을 씁니다.&lt;br&gt;매번 문서를 보거나 위 명령으로 확인하는 편이 빠릅니다.&lt;/p&gt;
&lt;p&gt;앞의 &lt;code&gt;service-&lt;/code&gt; 뒤에는 &lt;strong&gt;프로젝트 번호&lt;/strong&gt;가 들어갑니다. 프로젝트 ID 가 아닙니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects describe PROJECT_ID --format=&amp;#39;value(projectNumber)&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;언제 권한을 줘야 하나&lt;/h2&gt;
&lt;p&gt;대부분은 신경 쓸 일이 없습니다. 같은 프로젝트 안에서는 &lt;strong&gt;자동으로 부여&lt;/strong&gt;됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;경계를 넘을 때&lt;/strong&gt; 드러납니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;대상 에이전트&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Cloud Run → Shared VPC 서브넷&lt;/td&gt;
&lt;td&gt;&lt;code&gt;serverless-robot-prod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;compute.networkUser&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run → &lt;strong&gt;다른 프로젝트&lt;/strong&gt; 저장소&lt;/td&gt;
&lt;td&gt;&lt;code&gt;serverless-robot-prod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;artifactregistry.reader&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage → CMEK 키&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gs-project-accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cloudkms.cryptoKeyEncrypterDecrypter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub → 다른 프로젝트 토픽&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcp-sa-pubsub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pubsub.publisher&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eventarc → Cloud Run 호출&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcp-sa-eventarc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run.invoker&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;공통점은 &lt;strong&gt;프로젝트나 서비스의 경계를 넘는다&lt;/strong&gt;는 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;사례 1 :: Shared VPC 에 Cloud Run 띄우기&lt;/h2&gt;
&lt;p&gt;Shared VPC 는 네트워크가 host 프로젝트 소유이고 워크로드는 서비스 프로젝트에 있습니다.&lt;br&gt;Cloud Run 이 그 서브넷을 쓰려면 권한이 필요합니다.&lt;/p&gt;
&lt;h3&gt;방법 A :: host 프로젝트 전체에&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects add-iam-policy-binding HOST_PROJECT_ID \
  --member=&amp;quot;serviceAccount:service-SVC_PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com&amp;quot; \
  --role=&amp;quot;roles/compute.networkUser&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;간단하지만 &lt;strong&gt;host 의 모든 서브넷&lt;/strong&gt;을 쓸 수 있게 됩니다.&lt;/p&gt;
&lt;h3&gt;방법 B :: 서브넷 단위로 (권장)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute networks subnets add-iam-policy-binding SUBNET_NAME \
  --region=REGION --project=HOST_PROJECT_ID \
  --member=&amp;quot;serviceAccount:service-SVC_PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com&amp;quot; \
  --role=&amp;quot;roles/compute.networkUser&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;여기서 한 줄을 빠뜨리기 쉽습니다.&lt;/strong&gt; 서브넷 권한만으로는 네트워크 목록을 읽지 못해&lt;br&gt;host 프로젝트에 조회 권한을 따로 줘야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects add-iam-policy-binding HOST_PROJECT_ID \
  --member=&amp;quot;serviceAccount:service-SVC_PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com&amp;quot; \
  --role=&amp;quot;roles/compute.networkViewer&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;프로젝트 번호는 서비스 프로젝트 것&lt;/strong&gt;입니다. host 프로젝트 번호를 넣으면 조용히&lt;br&gt;아무 일도 일어나지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;사례 2 :: Cloud Run 이 이미지를 당겨올 때&lt;/h2&gt;
&lt;p&gt;Cloud Run 은 배포 시점에 &lt;strong&gt;저장소에서 이미지를 직접 가져옵니다.&lt;/strong&gt; 이때 쓰는 신원도&lt;br&gt;런타임 계정이 아니라 서비스 에이전트입니다.&lt;/p&gt;
&lt;p&gt;같은 프로젝트면 자동으로 되지만, &lt;strong&gt;저장소가 다른 프로젝트&lt;/strong&gt;에 있으면 막힙니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud artifacts repositories add-iam-policy-binding REPO \
  --location=REGION --project=REGISTRY_PROJECT_ID \
  --member=&amp;quot;serviceAccount:service-RUN_PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com&amp;quot; \
  --role=&amp;quot;roles/artifactregistry.reader&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;배포가 「이미지를 찾을 수 없다」로 실패하면 이쪽을 보십시오. 런타임 계정에 권한을&lt;br&gt;아무리 줘도 풀리지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;사례 3 :: CMEK 를 거는 순서&lt;/h2&gt;
&lt;p&gt;버킷에 고객 관리 암호화 키를 적용하려면 &lt;strong&gt;Storage 서비스 에이전트가 키를 쓸 수 있어야&lt;/strong&gt;&lt;br&gt;합니다. 순서를 틀리면 버킷 생성이 실패합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1) 키링과 키를 먼저 만듭니다
gcloud kms keyrings create KEYRING --location=REGION
gcloud kms keys create KEY --keyring=KEYRING --location=REGION --purpose=encryption

# 2) Storage 서비스 에이전트에 키 사용 권한
gcloud kms keys add-iam-policy-binding KEY \
  --keyring=KEYRING --location=REGION \
  --member=&amp;quot;serviceAccount:service-PROJECT_NUMBER@gs-project-accounts.iam.gserviceaccount.com&amp;quot; \
  --role=&amp;quot;roles/cloudkms.cryptoKeyEncrypterDecrypter&amp;quot;

# 3) 그 다음에야 버킷을 만듭니다
gcloud storage buckets create gs://BUCKET --default-encryption-key=...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2번을 건너뛰면 3번이 거부됩니다.&lt;/strong&gt; 키가 있어도 쓸 수 있는 신원이 없기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;걸리기 쉬운 지점&lt;/h2&gt;
&lt;h3&gt;계정이 아직 없다고 나옵니다&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;API 를 켜야 생성됩니다.&lt;/strong&gt; 권한을 주려는데 계정을 찾을 수 없다면 순서가 뒤바뀐 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud services enable run.googleapis.com --project=PROJECT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;API 를 켜고 나서 잠시 뒤에 생기는 경우도 있습니다.&lt;/p&gt;
&lt;h3&gt;강제로 만들 수 있습니다&lt;/h3&gt;
&lt;p&gt;API 를 켰는데도 안 생기면 이 명령으로 생성을 유도할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud beta services identity create \
  --service=run.googleapis.com --project=PROJECT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;지울 수 없습니다&lt;/h3&gt;
&lt;p&gt;삭제 명령이 없습니다. &lt;strong&gt;API 를 끄면 사라집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;기본 서비스 계정과는 다릅니다&lt;/h3&gt;
&lt;p&gt;이름이 비슷해 헷갈리는 것이 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service-N@compute-system.iam…              서비스 에이전트  (Google 소유)
N-compute@developer.gserviceaccount.com    기본 서비스 계정 (내 소유)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;뒤의 것은 &lt;strong&gt;VM 이나 함수가 쓰는 신원&lt;/strong&gt;이고 내가 관리합니다. 조직 정책&lt;br&gt;&lt;code&gt;iam.automaticIamGrantsForDefaultServiceAccounts&lt;/code&gt; 가 통제하는 대상도 이쪽입니다.&lt;br&gt;서비스 에이전트와는 무관합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;서비스 에이전트는 &lt;strong&gt;서비스가 내 자원을 대신 만질 때 쓰는 Google 소유 계정&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;기억해 두면 좋은 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;목록 조회에 안 나옵니다.&lt;/strong&gt; IAM 정책을 뒤지거나 콘솔에서 「Google 제공 역할 포함」을 켜십시오&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경계를 넘을 때만&lt;/strong&gt; 권한을 줍니다. 같은 프로젝트 안에서는 자동입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로젝트 번호&lt;/strong&gt;가 주소에 들어갑니다. 어느 프로젝트 번호인지 혼동하지 마십시오&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;권한 오류를 만났을 때 &lt;strong&gt;「내 서비스 계정 문제인가, 서비스 에이전트 문제인가」&lt;/strong&gt; 를&lt;br&gt;먼저 가르면 헤매는 시간이 줄어듭니다. 내 코드가 호출해서 난 오류면 앞쪽이고,&lt;br&gt;배포나 네트워크 연결 단계에서 났으면 뒤쪽일 가능성이 큽니다.&lt;/p&gt;
&lt;h3&gt;참고 문서&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/iam/docs/service-account-types&quot;&gt;서비스 계정의 종류&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/iam/docs/service-agents&quot;&gt;서비스 에이전트 목록&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/run/docs/configuring/shared-vpc-direct-vpc&quot;&gt;Shared VPC 와 Direct VPC egress&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/6</guid>
      <comments>https://pubsub.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 19 Oct 2026 09:25:07 +0900</pubDate>
    </item>
    <item>
      <title># PSC 로 Google API 사설 호출 :: curl 이 000 과 401 을 뱉는 이유</title>
      <link>https://pubsub.tistory.com/5</link>
      <description>&lt;p&gt;사내망에서 인터넷을 거치지 않고 Google Cloud API 를 호출해야 할 때가 있습니다.&lt;br&gt;보안 정책상 아웃바운드가 막혀 있거나, 트래픽이 공중망을 타지 않아야 하는 경우입니다.&lt;/p&gt;
&lt;p&gt;Private Service Connect(PSC)로 엔드포인트를 만들면 VPC 안의 내부 IP 하나로&lt;br&gt;Google API 전체를 부를 수 있습니다. 그런데 &lt;strong&gt;막상 &lt;code&gt;curl&lt;/code&gt; 을 쳐 보면 생각보다 안 됩니다.&lt;/strong&gt;&lt;br&gt;&lt;code&gt;000&lt;/code&gt; 이 나오거나, 분명 토큰을 넣었는데 &lt;code&gt;401&lt;/code&gt; 이 돌아옵니다.&lt;/p&gt;
&lt;p&gt;이 글은 그 이유를 실제로 구성해 확인한 기록입니다. Cloud Storage 를 대상으로&lt;br&gt;세 가지 방식을 모두 실행했고, 결과를 그대로 실었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VM
 │  이름 해석 → 10.90.100.10
 ▼
PSC 엔드포인트 ── all-apis ──▶ Google API (GCS · Compute · OAuth2 …)&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;먼저 알아야 할 것 :: SNI 와 Host 는 하는 일이 다릅니다&lt;/h2&gt;
&lt;p&gt;PSC 엔드포인트는 &lt;strong&gt;IP 하나로 모든 Google API 를 받습니다.&lt;/strong&gt; 그래서 서버 입장에서는&lt;br&gt;「이 요청을 어디로 보낼지」를 따로 알아야 합니다. 여기에 두 가지가 관여합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;무엇이&lt;/th&gt;
&lt;th&gt;무엇을 결정하는가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SNI&lt;/strong&gt; (TLS 핸드셰이크의 서버 이름)&lt;/td&gt;
&lt;td&gt;서버가 &lt;strong&gt;어떤 인증서&lt;/strong&gt;를 내어줄지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Host 헤더&lt;/strong&gt; (HTTP)&lt;/td&gt;
&lt;td&gt;요청을 &lt;strong&gt;어느 API 로 보낼지&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;SNI 를 바꿔 가며 어떤 인증서가 오는지 확인해 봤습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;자체 도메인이나 IP 로 접속하면 Google 이 &lt;code&gt;*.google.com&lt;/code&gt; 기본 인증서를 줍니다.&lt;/strong&gt;&lt;br&gt;이름이 맞지 않으니 curl 이 연결을 끊습니다. 이것이 뒤의 두 방법에서 손이 더 가는 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 1. PSC 전용 도메인&lt;/h2&gt;
&lt;p&gt;가장 단순합니다. 구성만 해 두면 &lt;strong&gt;curl 에 추가 옵션이 없습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PSC 엔드포인트를 만들 때 Service Directory 에 등록하면 &lt;code&gt;p.googleapis.com&lt;/code&gt; 비공개 영역이&lt;br&gt;자동으로 생깁니다. 이름 형식은 &lt;strong&gt;&lt;code&gt;&amp;lt;서비스&amp;gt;-&amp;lt;엔드포인트이름&amp;gt;.p.googleapis.com&lt;/code&gt;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TOKEN=$(gcloud auth print-access-token)

curl -s -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://storage-myep.p.googleapis.com/storage/v1/b/my-bucket/o/hello.txt?alt=media&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hello-psc                      ← 200. 객체 내용이 그대로 나옵니다&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;함정 :: 엔드포인트 이름만으로는 해석되지 않습니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;myep.p.googleapis.com                  (해석 안 됨)
storage-myep.p.googleapis.com          10.90.100.10   ✔
compute-myep.p.googleapis.com          (해석 안 됨)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;서비스 접두사가 반드시 붙어야 하고, 모든 API 에 레코드가 생기지도 않습니다.&lt;/strong&gt;&lt;br&gt;Service Directory 가 흔히 쓰는 API 에만 레코드를 만들기 때문입니다.&lt;br&gt;쓰려는 API 가 없으면 방법 2 로 가야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 2. 사설 도메인&lt;/h2&gt;
&lt;p&gt;자체 도메인(&lt;code&gt;gcs.mylab.internal&lt;/code&gt;)을 PSC IP 로 가리키게 한 경우입니다.&lt;br&gt;&lt;strong&gt;그대로는 실패합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;000                            ← 종료코드 60. 인증서 검증 실패&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;인증서 검증만 끄면 :: 여전히 안 됩니다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;401                            ← TLS 는 통과했으나 인증 실패&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;이 부분에서 많이 헤맵니다.&lt;/strong&gt; &lt;code&gt;-k&lt;/code&gt; 는 TLS 문제만 덮습니다.&lt;br&gt;Host 헤더가 &lt;code&gt;gcs.mylab.internal&lt;/code&gt; 이라 Google 이 이 요청을 GCS 로 보내지 않습니다.&lt;br&gt;토큰이 멀쩡해도 401 입니다.&lt;/p&gt;
&lt;h3&gt;성립하는 조합 :: &lt;code&gt;-k&lt;/code&gt; 와 Host 헤더를 함께&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk \
  -H &amp;#39;Host: storage.googleapis.com&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt?alt=media&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hello-psc                      ← 200&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;다만 &lt;strong&gt;인증서 검증을 끄는 것이 전제&lt;/strong&gt;이므로 운영에 권하지 않습니다.&lt;br&gt;중간자 공격을 탐지할 수 없게 됩니다.&lt;/p&gt;
&lt;h3&gt;검증을 끄지 않으려면&lt;/h3&gt;
&lt;p&gt;이름을 바꾸지 않고 &lt;strong&gt;IP 만 바꾸는&lt;/strong&gt; 방법이 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s --resolve &amp;#39;storage.googleapis.com:443:10.90.100.10&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://storage.googleapis.com/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;200                            ← 인증서 검증을 켠 채로 성공&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SNI 와 Host 가 모두 &lt;code&gt;storage.googleapis.com&lt;/code&gt; 이라 정상 인증서가 나오고 라우팅도 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 3. 순수 IP&lt;/h2&gt;
&lt;p&gt;도메인 없이 IP 로 직접 호출합니다. 방법 2 와 같은 이유로 &lt;strong&gt;그대로는 실패합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;000                            ← 종료코드 60&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;401                            ← TLS 통과, 라우팅 실패&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;성립하는 조합&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk \
  -H &amp;#39;Host: storage.googleapis.com&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt?alt=media&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hello-psc                      ← 200&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;IP 호출에서는 Host 헤더가 필수입니다.&lt;/strong&gt; 이것이 없으면 어느 API 인지 알 수 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결과 한눈에&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;호출 방식&lt;/th&gt;
&lt;th&gt;추가 옵션&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;&lt;code&gt;storage-myep.p.googleapis.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;②&lt;/td&gt;
&lt;td&gt;&lt;code&gt;storage.googleapis.com&lt;/code&gt; (사설 DNS)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcs.mylab.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;000 (exit 60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;④&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcs.mylab.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑤&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcs.mylab.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt; + &lt;code&gt;Host:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑥&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.90.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;000 (exit 60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑦&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.90.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑧&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.90.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt; + &lt;code&gt;Host:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑨&lt;/td&gt;
&lt;td&gt;&lt;code&gt;storage.googleapis.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--resolve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;응답 코드를 구분해서 읽으십시오&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;000&lt;/code&gt;&lt;/strong&gt; · 종료코드 60 :: TLS 핸드셰이크에서 끊겼습니다. &lt;strong&gt;인증서 문제&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;401&lt;/code&gt;&lt;/strong&gt; :: TLS 는 성립했고 HTTP 응답을 받았습니다. &lt;strong&gt;라우팅이나 인증&lt;/strong&gt; 문제입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;403&lt;/code&gt;&lt;/strong&gt; :: 도달했고 인증도 됐으나 &lt;strong&gt;권한이 없습니다.&lt;/strong&gt; 경로는 정상입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실제로 Compute API 를 같은 엔드포인트로 호출했더니 403 이 나왔습니다.&lt;br&gt;서비스 계정에 Compute 권한이 없어서였고, &lt;strong&gt;PSC 경유 자체는 성공&lt;/strong&gt;한 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code} via %{remote_ip}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://www.googleapis.com/compute/v1/projects/my-project/zones&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;403 via 10.90.100.10           ← PSC 를 거쳤습니다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;%{remote_ip}&lt;/code&gt; 를 찍어 보면 실제로 어느 IP 로 나갔는지 바로 확인됩니다.&lt;br&gt;공인 IP 가 나오면 DNS 가 적용되지 않은 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어느 방법을 쓸 것인가&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;새로 구성하고 &lt;strong&gt;코드를 고칠 수 있다&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;① PSC 도메인.&lt;/strong&gt; 옵션이 필요 없고 인증서 검증도 켜집니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;기존 코드를 고칠 수 없다&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;② 기본 도메인 + 사설 DNS.&lt;/strong&gt; 애플리케이션 변경이 없습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일회성 진단&lt;/td&gt;
&lt;td&gt;⑨ &lt;code&gt;--resolve&lt;/code&gt;. DNS 를 건드리지 않고 경로만 바꿔 봅니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자체 도메인을 꼭 써야 한다&lt;/td&gt;
&lt;td&gt;⑤. 다만 &lt;strong&gt;인증서 검증을 끄게 되므로 권하지 않습니다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;운영 환경에는 &lt;strong&gt;②&lt;/strong&gt; 가 무난합니다. 애플리케이션과 SDK 가 기본 도메인을 그대로 쓰므로&lt;br&gt;바꿀 것이 없고 인증서 검증도 유지됩니다. 클라이언트 라이브러리는 &lt;code&gt;--resolve&lt;/code&gt; 같은&lt;br&gt;옵션을 받지 않으므로, DNS 로 푸는 것이 사실상 유일한 길이기도 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;구성 명령&lt;/h2&gt;
&lt;h3&gt;PSC 엔드포인트&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;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&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--target-google-apis-bundle&lt;/code&gt; 은 &lt;code&gt;all-apis&lt;/code&gt; 와 &lt;code&gt;vpc-sc&lt;/code&gt; 중에 고릅니다.&lt;br&gt;VPC SC 경계 안에서만 쓸 것이라면 &lt;code&gt;vpc-sc&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;h3&gt;API 를 먼저 켜십시오&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud services enable servicedirectory.googleapis.com dns.googleapis.com --project=$P&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;이것이 꺼져 있으면 엔드포인트는 만들어지지만 이름으로 접근할 수 없습니다.&lt;/strong&gt;&lt;br&gt;저도 꺼진 채로 만들었다가 뒤늦게 켰습니다.&lt;/p&gt;
&lt;h3&gt;기본 도메인을 PSC 로 보내기 (방법 2)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;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 &amp;quot;*.googleapis.com.&amp;quot; --zone=psc-googleapis \
  --type=CNAME --ttl=60 --rrdatas=googleapis.com. --project=$P&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;영역 자체에 A 레코드를 만들고 나머지는 와일드카드 CNAME 으로 묶습니다.&lt;br&gt;이렇게 하면 &lt;code&gt;storage.googleapis.com&lt;/code&gt; · &lt;code&gt;www.googleapis.com&lt;/code&gt; · &lt;code&gt;oauth2.googleapis.com&lt;/code&gt; 이&lt;br&gt;모두 PSC 로 갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;막혔던 지점 정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p.googleapis.com&lt;/code&gt; 영역에 레코드가 0개&lt;/td&gt;
&lt;td&gt;Service Directory 연동 영역이라 Cloud DNS 에 레코드가 직접 없습니다. 정상입니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;myep.p.googleapis.com&lt;/code&gt; 해석 실패&lt;/td&gt;
&lt;td&gt;서비스 접두사가 필요합니다 :: &lt;code&gt;storage-myep…&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt; 를 붙였는데 401&lt;/td&gt;
&lt;td&gt;Host 헤더가 없어 라우팅이 안 됩니다. TLS 와 별개 문제입니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;경로는 정상입니다. 서비스 계정 권한을 보십시오&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PSC 엔드포인트는 SNI 로 인증서를 고르고 Host 헤더로 라우팅합니다.&lt;/strong&gt;&lt;br&gt;이름을 바꾸면 인증서가 맞지 않고, 인증서를 무시해도 Host 가 맞지 않으면 라우팅이 안 됩니다.&lt;br&gt;두 가지를 모두 풀어야 200 이 나옵니다.&lt;/p&gt;
&lt;p&gt;그래서 가장 깔끔한 선택은 &lt;strong&gt;이름을 바꾸지 않는 것&lt;/strong&gt;입니다.&lt;br&gt;사설 DNS 로 &lt;code&gt;googleapis.com&lt;/code&gt; 을 PSC 쪽으로 돌려 두면, 애플리케이션은 아무것도 모른 채&lt;br&gt;사설 경로로 나갑니다.&lt;/p&gt;
&lt;h3&gt;참고 문서&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vpc/docs/configure-private-service-connect-apis&quot;&gt;Private Service Connect 로 Google API 에 액세스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vpc/docs/private-service-connect&quot;&gt;Private Service Connect 개요&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/5</guid>
      <comments>https://pubsub.tistory.com/5#entry5comment</comments>
      <pubDate>Fri, 16 Oct 2026 14:11:50 +0900</pubDate>
    </item>
    <item>
      <title># 서비스 계정 키로 gcloud 쓸 때 방화벽에 열어야 할 도메인 직접 찾기</title>
      <link>https://pubsub.tistory.com/4</link>
      <description>&lt;p&gt;폐쇄망이나 프록시 뒤에서 &lt;code&gt;gcloud&lt;/code&gt; 를 쓰려면 &lt;strong&gt;어떤 도메인을 열어야 하는지&lt;/strong&gt; 알아야 합니다.&lt;br&gt;그런데 공식 문서를 뒤져도 전체 목록이 나오지 않습니다.&lt;/p&gt;
&lt;p&gt;프록시 설정 문서는 &lt;code&gt;http_proxy&lt;/code&gt; 환경변수 쓰는 법만 알려 주고,&lt;br&gt;「무엇으로 나가는가」는 말해 주지 않습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;gcloud CLI respects the &lt;code&gt;http_proxy&lt;/code&gt;, &lt;code&gt;https_proxy&lt;/code&gt;, and &lt;code&gt;no_proxy&lt;/code&gt; environment variables&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그래서 &lt;strong&gt;직접 찾아야 합니다.&lt;/strong&gt; 이 글은 그 방법 네 가지와, 서비스 계정 키로 인증할 때의&lt;br&gt;특수성을 정리한 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결론부터 :: 최소 허용 목록&lt;/h2&gt;
&lt;p&gt;서비스 계정 키로 인증하고 API 를 호출하는 데 필요한 것은 이것뿐입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;oauth2.googleapis.com          토큰 발급
&amp;lt;서비스&amp;gt;.googleapis.com        실제 API 호출&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;accounts.google.com&lt;/code&gt; 은 필요 없습니다.&lt;/strong&gt; 브라우저 로그인을 하지 않기 때문입니다.&lt;br&gt;사용자 계정으로 &lt;code&gt;gcloud auth login&lt;/code&gt; 을 할 때만 쓰입니다.&lt;/p&gt;
&lt;p&gt;여기에 운영 편의를 더하면 이렇게 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도메인&lt;/th&gt;
&lt;th&gt;언제 필요한가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oauth2.googleapis.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;필수.&lt;/strong&gt; 키로 토큰을 받습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.googleapis.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;필수.&lt;/strong&gt; 호출하는 서비스마다 다릅니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accounts.google.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용자 계정 로그인·취소 시에만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dl.google.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcloud components update&lt;/code&gt; 시에만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metadata.google.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GCE VM 안에서 쓸 때 (키 없이)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;문제는 &lt;strong&gt;&lt;code&gt;*.googleapis.com&lt;/code&gt; 을 통째로 열 수 없는 환경&lt;/strong&gt;입니다.&lt;br&gt;그래서 「내가 쓰는 명령이 정확히 어디로 가는가」를 알아내야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 1. &lt;code&gt;--log-http&lt;/code&gt; :: 가장 확실합니다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gcloud&lt;/code&gt; 의 모든 명령에 붙일 수 있는 옵션입니다. 실제로 보낸 HTTP 요청이 전부 찍힙니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects describe my-project --log-http 2&amp;gt;&amp;amp;1 | grep &amp;#39;^uri:&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;uri: https://cloudresourcemanager.googleapis.com/v1/projects/my-project&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여러 명령을 한 번에 돌려 모으면 목록이 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;: &amp;gt; hosts.txt
collect() {
  gcloud &amp;quot;$@&amp;quot; --log-http 2&amp;gt;&amp;amp;1 \
    | grep -oE &amp;#39;^uri: https://[^/]+&amp;#39; | sed &amp;#39;s|uri: https://||&amp;#39; &amp;gt;&amp;gt; hosts.txt
}

collect projects describe my-project
collect compute instances list --limit=1
collect storage buckets list --limit=1
collect iam service-accounts list --limit=1
collect run services list --region=asia-northeast3 --limit=1
collect services list --enabled --limit=1
collect logging logs list --limit=1

sort -u hosts.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 결과입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;asia-northeast3-run.googleapis.com
cloudresourcemanager.googleapis.com
compute.googleapis.com
iam.googleapis.com
logging.googleapis.com
serviceusage.googleapis.com
storage.googleapis.com&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;여기서 놓치기 쉬운 것 :: 리전 접두사&lt;/h3&gt;
&lt;p&gt;맨 위를 보십시오.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;asia-northeast3-run.googleapis.com&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;run.googleapis.com&lt;/code&gt; 이 아닙니다.&lt;/strong&gt; Cloud Run 처럼 리전별 엔드포인트를 쓰는 서비스가 있고,&lt;br&gt;허용 목록에 &lt;code&gt;run.googleapis.com&lt;/code&gt; 만 넣으면 막힙니다.&lt;/p&gt;
&lt;p&gt;리전을 여럿 쓴다면 그만큼 늘어납니다. 서비스 목록만 보고 작성하면 반드시 빠뜨리는 부분입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 2. 서비스 계정 키 파일을 열어 봅니다&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;키 파일 안에 토큰 엔드포인트가 그대로 적혀 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m json.tool &amp;lt; sa-key.json | grep -E &amp;#39;uri|url&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;service_account&amp;quot;,
  &amp;quot;token_uri&amp;quot;: &amp;quot;https://oauth2.googleapis.com/token&amp;quot;,
  &amp;quot;auth_uri&amp;quot;: &amp;quot;https://accounts.google.com/o/oauth2/auth&amp;quot;,
  &amp;quot;auth_provider_x509_cert_url&amp;quot;: &amp;quot;https://www.googleapis.com/oauth2/v1/certs&amp;quot;,
  &amp;quot;client_x509_cert_url&amp;quot;: &amp;quot;https://www.googleapis.com/robot/v1/metadata/x509/...&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;키를 가진 쪽이 부르는 것은 &lt;code&gt;token_uri&lt;/code&gt; 하나뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;누가 부르는가&lt;/th&gt;
&lt;th&gt;무엇에 쓰는가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;token_uri&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;클라이언트(gcloud)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;토큰을 &lt;strong&gt;받는&lt;/strong&gt; 곳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth_uri&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브라우저&lt;/td&gt;
&lt;td&gt;사용자 동의 화면. 서비스 계정은 쓰지 않습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth_provider_x509_cert_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;토큰을 &lt;strong&gt;받는 서버&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google 이 서명한&lt;/strong&gt; 토큰 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;client_x509_cert_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT 를 &lt;strong&gt;받는 서버&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;이 서비스 계정이 서명한&lt;/strong&gt; JWT 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;뒤의 두 개를 헷갈리기 쉬운데, &lt;strong&gt;키 파일을 가진 쪽이 쓰는 주소가 아닙니다.&lt;/strong&gt;&lt;br&gt;상대방이 서명을 확인할 때 쓰는 공개키 주소입니다. 둘 다 열어 보면 인증서 묶음이 나옵니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s https://www.googleapis.com/oauth2/v1/certs | python3 -m json.tool | head -3&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;f10f87405a979c1df36df26606734f33cd85c271&amp;quot;: &amp;quot;-----BEGIN CERTIFICATE-----\nMIIDJjCC...&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;두 인증 흐름을 나란히 놓으면&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;auth_uri&lt;/code&gt; 가 왜 서비스 계정에서 쓰이지 않는지가 여기서 드러납니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;사용자 계정    동의 단계  →  토큰 교환  →  API 호출
서비스 계정                  토큰 교환  →  API 호출&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;차이는 앞의 동의 단계 하나뿐이고, 거기서만 &lt;code&gt;accounts.google.com&lt;/code&gt; 이 쓰입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;동의 단계 :: 사용자 계정에만 있습니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gcloud  →  브라우저를 auth_uri 로 보냄
           accounts.google.com 에서 로그인하고 동의
gcloud  ←  authorization code 수신&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;사람에게 「이 앱이 계정에 접근해도 됩니까」를 묻는 과정입니다.&lt;br&gt;&lt;code&gt;gcloud auth login&lt;/code&gt; 을 칠 때 브라우저가 열리는 그 화면입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;서비스 계정에는 물어볼 사람이 없습니다.&lt;/strong&gt; 키 파일을 가진 것 자체가&lt;br&gt;「권한을 위임받았다」는 증거라서 이 단계가 통째로 빠집니다.&lt;/p&gt;
&lt;h3&gt;토큰 교환 :: 양쪽 다 있습니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;사용자 계정   code 를 token_uri 로
서비스 계정   private_key 로 JWT 서명 (로컬) → token_uri 로
                        ↓
                 access_token 수신&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;보내는 재료가 다를 뿐 &lt;strong&gt;목적지는 같습니다.&lt;/strong&gt; 서비스 계정 쪽은 JWT 를 만드는 과정이&lt;br&gt;로컬에서 끝나므로, 여기서도 외부 통신은 &lt;code&gt;oauth2.googleapis.com&lt;/code&gt; 한 곳뿐입니다.&lt;/p&gt;
&lt;h3&gt;API 호출 :: 양쪽 다 같습니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Bearer &amp;lt;access_token&amp;gt;
  →  &amp;lt;서비스&amp;gt;.googleapis.com&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Google 이 자기가 발급한 토큰을 내부에서 확인합니다. 공개키 URL 을 부르지 않습니다.&lt;/p&gt;
&lt;h3&gt;그런데 왜 키 파일에 &lt;code&gt;auth_uri&lt;/code&gt; 가 들어 있을까&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;형식을 공유하기 때문입니다.&lt;/strong&gt; 서비스 계정 키 JSON 은 OAuth 클라이언트 설정과 같은 구조를&lt;br&gt;쓰고, 라이브러리가 두 가지를 같은 코드로 읽습니다. 자리는 있지만 서비스 계정 흐름에서는&lt;br&gt;&lt;strong&gt;채워져 있을 뿐 호출되지 않습니다.&lt;/strong&gt; &lt;code&gt;client_id&lt;/code&gt; 나 &lt;code&gt;universe_domain&lt;/code&gt; 도 비슷합니다.&lt;/p&gt;
&lt;p&gt;정리하면 &lt;strong&gt;서비스 계정 키는 브라우저도, &lt;code&gt;accounts.google.com&lt;/code&gt; 도 쓰지 않습니다.&lt;/strong&gt;&lt;br&gt;그래서 폐쇄망에서 다루기가 훨씬 쉽습니다.&lt;/p&gt;
&lt;h3&gt;「토큰 검증」이 둘 중 무엇인지 구분하십시오&lt;/h3&gt;
&lt;p&gt;이 공개키 주소가 필요한지는 &lt;strong&gt;어떤 토큰을 다루느냐&lt;/strong&gt;에 달려 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;생김새&lt;/th&gt;
&lt;th&gt;공개키로 검증&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;access token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ya29.a0AfB_...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;불가.&lt;/strong&gt; 서명이 없는 불투명한 문자열입니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ID token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eyJhbGci...eyJpc3M...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;가능.&lt;/strong&gt; JWT 라 서명이 들어 있습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;gcloud auth print-access-token&lt;/code&gt; 이 주는 값은 &lt;strong&gt;access token&lt;/strong&gt; 입니다.&lt;br&gt;JWT 가 아니라서 공개키로 검증할 수 없고, Google 이 내부 저장소를 조회해 확인합니다.&lt;/p&gt;
&lt;p&gt;공개키 주소가 실제로 필요한 경우는 이렇습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auth_provider_x509_cert_url&lt;/code&gt; :: &lt;strong&gt;내 서버가 Google 이 발급한 ID 토큰을 검증&lt;/strong&gt;할 때.&lt;br&gt;Cloud Run 앞에 자체 게이트웨이를 두고 &lt;code&gt;Authorization: Bearer &amp;lt;ID token&amp;gt;&lt;/code&gt; 을&lt;br&gt;직접 확인하거나, IAP 헤더를 검증하는 경우입니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_x509_cert_url&lt;/code&gt; :: &lt;strong&gt;서비스 계정이 직접 서명한 JWT&lt;/strong&gt;(self-signed JWT)를&lt;br&gt;제3자가 검증할 때. Google 에 토큰을 받으러 가지 않고 개인키로 JWT 를 만들어&lt;br&gt;상대 서비스에 바로 보내는 방식입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;둘 다 &lt;code&gt;www.googleapis.com&lt;/code&gt; 안에 있으므로, 그 도메인을 이미 열어 두셨다면&lt;br&gt;따로 챙길 것이 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 3. gcloud 내부 설정에서 후보를 뽑습니다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gcloud&lt;/code&gt; 는 서비스별 엔드포인트를 바꿀 수 있는 설정을 갖고 있습니다.&lt;br&gt;&lt;strong&gt;이 키 목록이 곧 gcloud 가 아는 서비스 목록&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud config list --all | sed -n &amp;#39;/api_endpoint_overrides/,/^\[/p&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[api_endpoint_overrides]
accessapproval (unset)
accesscontextmanager (unset)
aiplatform (unset)
apigee (unset)
artifactregistry (unset)
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;저는 110개가 나왔습니다. 대부분 &lt;code&gt;&amp;lt;이름&amp;gt;.googleapis.com&lt;/code&gt; 에 대응합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이것은 「가능한 전체」이지 「내가 쓰는 것」이 아닙니다.&lt;/strong&gt; 허용 목록을 이걸로 만들면&lt;br&gt;쓰지도 않는 도메인을 잔뜩 열게 됩니다. 방법 1 로 실제 사용분을 추리고,&lt;br&gt;이 목록은 &lt;strong&gt;빠진 것이 없는지 대조하는 용도&lt;/strong&gt;로 쓰는 편이 맞습니다.&lt;/p&gt;
&lt;h3&gt;SDK 소스에서 인증 관련 주소도 확인됩니다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SDK=$(gcloud info --format=&amp;#39;value(installation.sdk_root)&amp;#39;)
grep -rhoE &amp;#39;https://(accounts\.google\.com|oauth2\.googleapis\.com)[a-z0-9/._-]*&amp;#39; \
  &amp;quot;$SDK/lib/googlecloudsdk/core/credentials/&amp;quot; | sort -u&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;https://accounts.google.com/o/oauth2/auth
https://accounts.google.com/o/oauth2/revoke
https://oauth2.googleapis.com&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;revoke&lt;/code&gt; 는 &lt;code&gt;gcloud auth revoke&lt;/code&gt; 를 쓸 때만 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 4. 실제 트래픽을 관찰합니다&lt;/h2&gt;
&lt;p&gt;앞의 방법들은 &lt;strong&gt;gcloud 가 알려 주는 것&lt;/strong&gt;입니다.&lt;br&gt;확실하게 하려면 나가는 트래픽을 직접 봅니다.&lt;/p&gt;
&lt;h3&gt;DNS 조회로&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS
sudo tcpdump -i any -n port 53 2&amp;gt;/dev/null | grep -oE &amp;#39;A\? [a-z0-9.-]+&amp;#39; &amp;amp;
gcloud compute instances list&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;프록시 로그로&lt;/h3&gt;
&lt;p&gt;프록시를 거치는 환경이라면 그쪽 액세스 로그가 가장 정확합니다.&lt;br&gt;&lt;code&gt;CONNECT&lt;/code&gt; 메서드로 나간 호스트를 모으면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export https_proxy=http://proxy.example:3128
gcloud compute instances list
# 프록시 로그에서 CONNECT 대상 확인&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;막아 놓고 확인하는 방법&lt;/h3&gt;
&lt;p&gt;역으로, &lt;strong&gt;일부러 막아 두고 무엇이 실패하는지&lt;/strong&gt; 보는 것도 확실합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export https_proxy=http://127.0.0.1:1   # 존재하지 않는 프록시
gcloud projects describe my-project&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;오류 메시지에 접속하려던 호스트가 그대로 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;허용 목록 만들기&lt;/h2&gt;
&lt;h3&gt;1단계 :: 쓰는 명령을 전부 적습니다&lt;/h3&gt;
&lt;p&gt;파이프라인이나 스크립트에서 실제로 실행되는 &lt;code&gt;gcloud&lt;/code&gt; 명령을 모읍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -rhoE &amp;#39;gcloud [a-z-]+ [a-z-]+&amp;#39; ./scripts/ | sort -u&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2단계 :: 각각을 &lt;code&gt;--log-http&lt;/code&gt; 로 돌립니다&lt;/h3&gt;
&lt;p&gt;읽기 명령(&lt;code&gt;list&lt;/code&gt; · &lt;code&gt;describe&lt;/code&gt;)으로 바꿔 돌리면 안전합니다.&lt;br&gt;같은 서비스는 같은 엔드포인트를 쓰므로 &lt;code&gt;create&lt;/code&gt; 를 실제로 실행할 필요가 없습니다.&lt;/p&gt;
&lt;h3&gt;3단계 :: 리전형이 있는지 확인합니다&lt;/h3&gt;
&lt;p&gt;결과에 &lt;code&gt;&amp;lt;리전&amp;gt;-&amp;lt;서비스&amp;gt;.googleapis.com&lt;/code&gt; 형태가 있으면,&lt;br&gt;&lt;strong&gt;쓰는 리전을 모두 넣었는지&lt;/strong&gt; 확인하십시오.&lt;/p&gt;
&lt;h3&gt;4단계 :: 인증과 부가 항목을 더합니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;oauth2.googleapis.com                      필수
dl.google.com                              components update 를 쓴다면
accounts.google.com                        사용자 계정도 쓴다면
metadata.google.internal / 169.254.169.254 GCE VM 안에서 쓴다면&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;검증&lt;/h2&gt;
&lt;p&gt;허용 목록을 적용한 뒤 실제로 되는지 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 인증이 되는가
gcloud auth activate-service-account --key-file=sa-key.json
gcloud auth print-access-token &amp;gt; /dev/null &amp;amp;&amp;amp; echo &amp;quot;토큰 발급 OK&amp;quot;

# API 호출이 되는가
gcloud projects describe my-project &amp;gt; /dev/null &amp;amp;&amp;amp; echo &amp;quot;API 호출 OK&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;막히면 어디서 막혔는지 바로 보입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects describe my-project --log-http 2&amp;gt;&amp;amp;1 | tail -20&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;인증에서 막히면&lt;/strong&gt; &lt;code&gt;oauth2.googleapis.com&lt;/code&gt; 이 닫힌 것이고,&lt;br&gt;&lt;strong&gt;API 호출에서 막히면&lt;/strong&gt; 그 서비스 도메인이 빠진 것입니다. 둘을 구분해 보십시오.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;공식 문서에 전체 목록이 없으므로 &lt;strong&gt;쓰는 명령에서 거꾸로 뽑아내는 것&lt;/strong&gt;이 유일한 방법입니다.&lt;/p&gt;
&lt;p&gt;가장 빠른 길은 &lt;code&gt;--log-http&lt;/code&gt; 입니다. 한 줄로 끝납니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud &amp;lt;명령&amp;gt; --log-http 2&amp;gt;&amp;amp;1 | grep &amp;#39;^uri:&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 &lt;strong&gt;서비스 계정 키는 &lt;code&gt;oauth2.googleapis.com&lt;/code&gt; 하나면 인증이 끝납니다.&lt;/strong&gt;&lt;br&gt;브라우저 로그인이 없어 &lt;code&gt;accounts.google.com&lt;/code&gt; 이 필요 없고, 이것이 폐쇄망에서&lt;br&gt;사용자 계정보다 다루기 쉬운 이유입니다.&lt;/p&gt;
&lt;p&gt;마지막으로 &lt;strong&gt;리전 접두사를 조심하십시오.&lt;/strong&gt; &lt;code&gt;asia-northeast3-run.googleapis.com&lt;/code&gt; 같은&lt;br&gt;형태를 놓치면, 다른 명령은 다 되는데 특정 서비스만 안 되는 상황이 생깁니다.&lt;/p&gt;
&lt;h3&gt;참고 문서&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/sdk/docs/proxy-settings&quot;&gt;gcloud CLI 프록시 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/docs/authentication/provide-credentials-adc&quot;&gt;서비스 계정 키로 인증&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/identity/protocols/oauth2/service-account&quot;&gt;OAuth 2.0 서비스 계정 흐름&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/4</guid>
      <comments>https://pubsub.tistory.com/4#entry4comment</comments>
      <pubDate>Fri, 16 Oct 2026 14:08:35 +0900</pubDate>
    </item>
    <item>
      <title># Cloud Run ingress=internal 이 404 를 뱉을 때 :: 판정 단위는 VPC 가 아니라 프로젝트</title>
      <link>https://pubsub.tistory.com/3</link>
      <description>&lt;p&gt;Cloud Run 서비스를 &lt;code&gt;ingress=internal&lt;/code&gt; 로 막아 두고 사내망에서 호출하려는 구성은 흔합니다.&lt;br&gt;Private Service Connect(PSC) 엔드포인트를 허브 VPC 에 두고 VPN 으로 들어오면 될 것 같은데,&lt;br&gt;막상 호출하면 &lt;strong&gt;404&lt;/strong&gt; 가 돌아옵니다.&lt;/p&gt;
&lt;p&gt;경로가 끊긴 걸까요? 아닙니다. &lt;strong&gt;요청은 도달했고 Cloud Run 이 거부한 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이 글은 그 판정 기준을 실제로 바꿔 가며 확인한 기록입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결론 먼저&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ingress=internal&lt;/code&gt; 은 요청이 들어온 VPC 가 「어느 프로젝트 것이냐」로 판정합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;진입점이 서비스와 &lt;strong&gt;다른 프로젝트&lt;/strong&gt;의 VPC 면 → &lt;strong&gt;404&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;진입점이 &lt;strong&gt;같은 프로젝트&lt;/strong&gt;의 VPC 면 → &lt;strong&gt;403&lt;/strong&gt; (통과, 인증만 요구)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문서의 표현도 이렇습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;requests from &lt;strong&gt;VPC networks in the same project&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;PSC 엔드포인트냐 아니냐는 상관이 없습니다.&lt;/strong&gt; PSC 도 결국 VPC 안의 리소스이므로,&lt;br&gt;그 VPC 가 어느 프로젝트에 있느냐만 봅니다. 아래 실험에서 진입점을 PSC 로 바꿔 가며&lt;br&gt;비교한 것도 &lt;strong&gt;프로젝트를 바꾸기 위한 수단&lt;/strong&gt;이었을 뿐입니다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;strong&gt;VPC 가 서비스와 연결돼 있는지도 보지 않습니다.&lt;/strong&gt; 같은 프로젝트이기만 하면&lt;br&gt;서비스와 아무 관계없는 VPC 에서 불러도 통과합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실험 구성&lt;/h2&gt;
&lt;p&gt;같은 VM, 같은 서비스, 같은 &lt;code&gt;ingress=internal&lt;/code&gt; 에서 &lt;strong&gt;엔드포인트만 교체&lt;/strong&gt;해 비교했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;project-a  (Cloud Run 이 여기 있음)
 ├ onprem-vpc  10.99.0.0/24 ── VM 10.99.0.3
 │                             run.app 전달영역 → 허브 창구
 ├ PSC 엔드포인트 ❷  10.88.88.4
 └ other-vpc   10.97.0.0/24 ── PSC 엔드포인트 ❸  10.66.66.4
                                (서비스와 아무 연결 없는 새 VPC)

project-b  (허브)
 └ hub-vpc  10.98.0.0/24
    ├ DNS 인바운드 창구  10.98.0.2
    └ PSC 엔드포인트 ❶  10.77.77.4&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Cloud Run 서비스는 &lt;code&gt;asia-northeast3&lt;/code&gt; 에 있고 &lt;code&gt;ingress=internal&lt;/code&gt; 입니다.&lt;br&gt;VPC 커넥터나 Direct VPC egress 는 붙이지 않았습니다. &lt;strong&gt;서비스 쪽에는 지정된 VPC 자체가 없습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;확인한 범위를 밝혀 둡니다&lt;/h3&gt;
&lt;p&gt;세 경로 모두 &lt;strong&gt;PSC 엔드포인트를 거쳤고&lt;/strong&gt;, 바뀐 것은 그 엔드포인트가 어느 VPC·프로젝트에&lt;br&gt;있느냐였습니다. PSC 를 쓰지 않고 VPC 에서 직접 호출하는 경우는 확인하지 않았습니다.&lt;/p&gt;
&lt;p&gt;문서상 기준이 「VPC 네트워크」이므로 결론은 같을 것으로 보지만, 아래 수치는&lt;br&gt;&lt;strong&gt;PSC 경유 세 가지를 직접 돌린 결과&lt;/strong&gt;입니다. 뒤에 나오는 Shared VPC 절도&lt;br&gt;실측이 아니라 &lt;strong&gt;공식 문서를 근거로 정리한 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결과 — 진입점만 바꿨습니다&lt;/h2&gt;
&lt;p&gt;진입점으로 PSC 엔드포인트를 쓴 것은 &lt;strong&gt;어느 VPC 에서 들어왔는지를 깔끔하게 바꾸기 위해서&lt;/strong&gt;입니다.&lt;br&gt;&lt;code&gt;--resolve&lt;/code&gt; 로 목적지 IP 만 갈아 끼우면 같은 VM 에서 세 경로를 비교할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;URL=myservice-123456789.asia-northeast3.run.app

curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; --resolve $URL:443:10.77.77.4 https://$URL/
# 404   ← project-b · hub-vpc        (다른 프로젝트)

curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; --resolve $URL:443:10.88.88.4 https://$URL/
# 403   ← project-a · onprem-vpc     (같은 프로젝트)

curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; --resolve $URL:443:10.66.66.4 https://$URL/
# 403   ← project-a · other-vpc      (같은 프로젝트, 서비스와 무관한 VPC)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;세 번째가 결정적입니다.&lt;/strong&gt; &lt;code&gt;other-vpc&lt;/code&gt; 는 Cloud Run 서비스와 아무 연결이 없는 새 VPC 입니다.&lt;br&gt;그런데도 통과했습니다. 판정 단위가 VPC 라면 거부되어야 했습니다.&lt;/p&gt;
&lt;h3&gt;404 와 403 을 구분해서 읽으십시오&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;요청은 도달했으나 Cloud Run 이 &lt;strong&gt;허용 네트워크로 인정하지 않음&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;네트워크 판정은 &lt;strong&gt;통과&lt;/strong&gt;했고 IAM 인증만 요구&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;403 은 사실상 성공입니다. 토큰을 붙이면 200 이 나옵니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $(gcloud auth print-identity-token)&amp;quot; \
  --resolve $URL:443:10.88.88.4 https://$URL/
# 200&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;경로 자체는 처음부터 정상이었습니다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ingress=all&lt;/code&gt; 로 바꾸면 &lt;strong&gt;❶ 도 403 이 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud run services update myservice --ingress=all --region=asia-northeast3
# 이후 10.77.77.4 로 호출 → 403&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 404 는 네트워크가 끊겨서가 아니라 &lt;strong&gt;Cloud Run 의 ingress 판정에서 걸린 것&lt;/strong&gt;입니다.&lt;br&gt;VPN 도 DNS 도 라우팅도 모두 정상이었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;함정 — 만들자마자 판단하지 마십시오&lt;/h2&gt;
&lt;p&gt;같은 프로젝트에 엔드포인트를 만들고 바로 호출했더니 &lt;strong&gt;처음엔 404&lt;/strong&gt; 가 나왔습니다.&lt;br&gt;약 2분 뒤 다시 호출하니 403 으로 바뀌었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;엔드포인트와 네트워크 연결이 반영되는 데 시간이 걸립니다.&lt;/strong&gt;&lt;br&gt;생성 직후의 404 를 보고 「안 되는구나」로 결론 내면 잘못 읽는 것입니다.&lt;/p&gt;
&lt;p&gt;반면 &lt;strong&gt;다른 프로젝트 엔드포인트는 1시간 뒤에도 404 로 유지&lt;/strong&gt;됐습니다.&lt;br&gt;시간이 지나도 바뀌지 않으면 그때는 진짜 거부입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Shared VPC 에서는 의미가 또 다릅니다&lt;/h2&gt;
&lt;p&gt;지금까지는 VPC 와 Cloud Run 이 같은 프로젝트에 있는 구성이었습니다.&lt;br&gt;그런데 &lt;strong&gt;Shared VPC 는 VPC 가 host 프로젝트 소유이고 워크로드는 서비스 프로젝트에 있습니다.&lt;/strong&gt;&lt;br&gt;「같은 프로젝트」라는 기준이 그대로 적용되지 않습니다.&lt;/p&gt;
&lt;p&gt;공식 문서는 Shared VPC 트래픽이 &lt;code&gt;internal&lt;/code&gt; 로 인정되는 경우를 &lt;strong&gt;두 가지로 한정&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Shared VPC traffic is only recognized as &amp;quot;internal&amp;quot; in the following situations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Cloud Run resource is &lt;strong&gt;running in the Shared VPC host project&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Shared VPC ingress: the Cloud Run resource is &lt;strong&gt;attached to a Shared VPC network&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성&lt;/th&gt;
&lt;th&gt;&lt;code&gt;internal&lt;/code&gt; 인정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Cloud Run 이 &lt;strong&gt;host 프로젝트&lt;/strong&gt;에 있음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;인정&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run 이 서비스 프로젝트에 있고 &lt;strong&gt;Shared VPC 에 연결됨&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;인정&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run 이 서비스 프로젝트에 있고 &lt;strong&gt;VPC 연결이 없음&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;인정되지 않습니다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;세 번째가 함정입니다. 서비스 프로젝트에 Cloud Run 을 올리고 &lt;code&gt;ingress=internal&lt;/code&gt; 로 두면&lt;br&gt;&lt;strong&gt;Shared VPC 쪽에서 불러도 404&lt;/strong&gt; 가 납니다. VPC 연결(Direct VPC egress 또는 커넥터)이&lt;br&gt;있어야 그 네트워크가 「내 것」으로 인정됩니다.&lt;/p&gt;
&lt;h3&gt;방향에 따라 경로가 다릅니다&lt;/h3&gt;
&lt;p&gt;헷갈리기 쉬운 부분이 하나 더 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Requests sent &lt;strong&gt;from Cloud Run to the Shared VPC network&lt;/strong&gt; are routed by Direct VPC egress or the connector.&lt;br&gt;However, requests sent &lt;strong&gt;from the Shared VPC network to Cloud Run&lt;/strong&gt; use the standard ingress path.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;나가는 길과 들어오는 길이 다릅니다.&lt;/strong&gt; 커넥터나 Direct VPC egress 는 &lt;strong&gt;이그레스&lt;/strong&gt; 설정이고,&lt;br&gt;들어오는 요청은 그 경로를 타지 않습니다. 그런데도 &lt;code&gt;internal&lt;/code&gt; 판정에는 그 연결의 존재가&lt;br&gt;영향을 줍니다. 「커넥터를 붙였으니 들어오는 것도 그리로 오겠지」라고 읽으면 틀립니다.&lt;/p&gt;
&lt;h3&gt;VPC SC 를 쓰신다면&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;You have placed the Shared VPC &lt;strong&gt;host and all service projects inside the same&lt;br&gt;VPC Service Controls perimeter&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;host 와 서비스 프로젝트가 &lt;strong&gt;같은 경계 안&lt;/strong&gt;에 있어야 합니다. 경계가 갈려 있으면&lt;br&gt;&lt;code&gt;internal&lt;/code&gt; 판정도 함께 어긋납니다.&lt;/p&gt;
&lt;h3&gt;그래서 Shared VPC 에서는&lt;/h3&gt;
&lt;p&gt;프로젝트가 여럿으로 갈리는 환경이라면 &lt;strong&gt;내부 ALB 를 앞에 두는 구성&lt;/strong&gt;이 훨씬 단순합니다.&lt;br&gt;어느 프로젝트에서 부르든 &lt;strong&gt;ALB VIP 는 평범한 내부 IP&lt;/strong&gt; 라 ingress 판정에 걸리지 않습니다.&lt;/p&gt;
&lt;p&gt;접근할 프로젝트를 골라서 열어야 한다면 문서가 이 방법을 권합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;host it with an internal Application Load Balancer and publish the service by using&lt;br&gt;&lt;strong&gt;Private Service Connect&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;ALB 뒤에 두고 PSC 로 게시하면, 소비하는 쪽은 자기 프로젝트에 PSC 엔드포인트를 만들어&lt;br&gt;붙습니다. 프로젝트 단위로 접근을 통제할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그럼 어떻게 할 것인가&lt;/h2&gt;
&lt;p&gt;같은 VM 에서 &lt;strong&gt;내부 ALB 를 거치는 경로&lt;/strong&gt;도 함께 확인했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# A. 내부 ALB VIP 로
curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; http://10.97.0.100/
# 403     ← 통과. 응답 헤더에 via: 1.1 google

# B. PSC 로 run.app 직접 (다른 프로젝트 엔드포인트)
curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; --resolve $URL:443:10.77.77.4 https://$URL/
# 404     ← 거부

# C. PSC 로 Google API
curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  --resolve storage.googleapis.com:443:10.77.77.4 \
  https://storage.googleapis.com/storage/v1/b?project=...
# 401     ← API 가 응답. 토큰만 없는 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ALB 를 쓰려면 ingress 를 바꿔야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud run services update myservice \
  --ingress=internal-and-cloud-load-balancing --region=asia-northeast3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ALB VIP 는 앱 서브넷의 평범한 내부 IP&lt;/strong&gt; 라 피어링과 VPN 경로를 그대로 탑니다.&lt;br&gt;PSC 엔드포인트처럼 프로젝트 판정에 걸리지 않습니다.&lt;/p&gt;
&lt;h3&gt;선택지 정리&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;내부 ALB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ingress=internal-and-cloud-load-balancing&lt;/code&gt;. VIP 가 일반 내부 IP 라 경로 제약이 없습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ingress=all&lt;/code&gt; + IAM&lt;/td&gt;
&lt;td&gt;네트워크 경계 대신 인증으로 막습니다. 실측에서 보듯 인증은 그대로 걸립니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;엔드포인트를 서비스 프로젝트에&lt;/td&gt;
&lt;td&gt;같은 프로젝트면 통과합니다. 다만 &lt;strong&gt;PSC 는 피어링을 넘지 않으므로&lt;/strong&gt; 그 VPC 에 VPN 직결이 필요합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC Service Controls&lt;/td&gt;
&lt;td&gt;허브 VPC 를 경계에 포함시켜 허용 네트워크로 만듭니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;허브 엔드포인트를 별도 프로젝트에 두는 한 &lt;strong&gt;VPC 를 어떻게 바꿔도 결과는 같습니다.&lt;/strong&gt;&lt;br&gt;판정 단위가 프로젝트이기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;구성 명령&lt;/h2&gt;
&lt;h3&gt;PSC 엔드포인트&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute addresses create psclabip --global \
  --purpose=PRIVATE_SERVICE_CONNECT --addresses=10.77.77.4 \
  --network=hub-vpc --project=project-b

gcloud compute forwarding-rules create psclabep --global \
  --network=hub-vpc --address=psclabip \
  --target-google-apis-bundle=all-apis --project=project-b&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전달 규칙 &lt;strong&gt;이름에 하이픈을 넣으면 거부됩니다.&lt;/strong&gt; 영숫자 20자 이하입니다.&lt;/p&gt;
&lt;h3&gt;DNS — 와일드카드 하나로 충분합니다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud dns managed-zones create psclab-runapp --dns-name=run.app. \
  --visibility=private --networks=hub-vpc --project=project-b

gcloud dns record-sets create run.app. --zone=psclab-runapp \
  --type=A --ttl=60 --rrdatas=10.77.77.4 --project=project-b

gcloud dns record-sets create &amp;quot;*.run.app.&amp;quot; --zone=psclab-runapp \
  --type=CNAME --ttl=60 --rrdatas=run.app. --project=project-b&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서비스마다 레코드를 만들 필요가 없습니다. &lt;code&gt;*.run.app.&lt;/code&gt; 와일드카드는 라벨이 둘 이상&lt;br&gt;앞에 붙어도(&lt;code&gt;myservice-….asia-northeast3.run.app&lt;/code&gt;) 걸립니다.&lt;br&gt;&lt;strong&gt;구분은 DNS 가 아니라 TLS SNI 와 Host 헤더가 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;온프렘에서 이름을 풀려면 — 전달영역&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 허브에 인바운드 창구
gcloud dns policies create psclab-inbound --networks=hub-vpc \
  --enable-inbound-forwarding --project=project-b

gcloud compute addresses list --project=project-b \
  --filter=&amp;quot;purpose=DNS_RESOLVER&amp;quot; --format=&amp;quot;table(address,subnetwork.basename())&amp;quot;
# 10.98.0.2    ← 창구 IP 는 서브넷마다 1개씩 자동 생성됩니다

# 온프렘 쪽은 run.app 만 허브로 전달
gcloud dns managed-zones create psclab-runapp-fwd --dns-name=run.app. \
  --visibility=private --networks=onprem-vpc \
  --forwarding-targets=10.98.0.2 --project=project-a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;정책(&lt;code&gt;dns policies&lt;/code&gt;)이 아니라 영역 단위로 거십시오.&lt;/strong&gt; 정책으로 전체 질의를 넘기면&lt;br&gt;VM 의 다른 이름 해석까지 허브로 가 버립니다. 영역으로 걸면 &lt;code&gt;run.app&lt;/code&gt; 만 넘어갑니다.&lt;/p&gt;
&lt;h3&gt;PSC IP 를 VPN 너머로 광고&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute routers update hub-router --region=asia-northeast3 \
  --advertisement-mode=CUSTOM --set-advertisement-groups=ALL_SUBNETS \
  --set-advertisement-ranges=10.77.77.4/32 --project=project-b&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PSC 주소는 &lt;strong&gt;서브넷 경로가 아니므로 &lt;code&gt;/32&lt;/code&gt; 커스텀 광고가 필요합니다.&lt;/strong&gt;&lt;br&gt;&lt;code&gt;ALL_SUBNETS&lt;/code&gt; 를 함께 주지 않으면 &lt;strong&gt;기본 서브넷 광고가 통째로 사라집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;조용히 실패하던 것 — DNS 전달 경로&lt;/h2&gt;
&lt;p&gt;광고를 하기 전에는 이름 해석이 &lt;strong&gt;오류 없이 타임아웃&lt;/strong&gt;만 났습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm$ getent hosts myservice-123456789.asia-northeast3.run.app
(해석 실패)

vm$ curl -sv https://myservice-….run.app/
* Resolving timed out after 12000 milliseconds&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;원인은 &lt;strong&gt;질의 출발지&lt;/strong&gt;였습니다. Cloud DNS 전달영역이 질의를 보낼 때 출발지가&lt;br&gt;&lt;code&gt;35.199.192.0/19&lt;/code&gt; 가 되는데, 그 대역으로 돌아올 경로를 허브가 몰랐던 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute routers update onprem-router --region=asia-northeast3 \
  --advertisement-mode=CUSTOM --set-advertisement-groups=ALL_SUBNETS \
  --set-advertisement-ranges=35.199.192.0/19 --project=project-a&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;다만 이것은 랩 특유의 조건입니다&lt;/h3&gt;
&lt;p&gt;이번 구성의 「온프렘」은 실제 IDC 가 아니라 GCP VPC 였습니다. 그래서 전달 질의를&lt;br&gt;사내 DNS 서버가 아니라 &lt;strong&gt;Cloud DNS 가 대신&lt;/strong&gt; 보냈고, 출발지가 &lt;code&gt;35.199.192.0/19&lt;/code&gt; 가 됐습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;이 랩 (온프렘 = GCP VPC)&lt;/th&gt;
&lt;th&gt;실제 IDC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;질의 주체&lt;/td&gt;
&lt;td&gt;Cloud DNS 전달영역&lt;/td&gt;
&lt;td&gt;사내 DNS 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;질의 출발지&lt;/td&gt;
&lt;td&gt;&lt;code&gt;35.199.192.0/19&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사내 DNS 서버 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;허브에 필요한 경로&lt;/td&gt;
&lt;td&gt;&lt;code&gt;35.199.192.0/19&lt;/code&gt; 커스텀 광고&lt;/td&gt;
&lt;td&gt;VPN 으로 이미 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;실제 IDC 라면 사내 DNS 서버가 직접 질의하므로 따로 광고할 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;반대 방향에서는 실제 환경에서도 걸립니다.&lt;/strong&gt; GCP 의 전달영역이 사내 DNS 로 질의를&lt;br&gt;내보내는 구성이라면, 출발지가 &lt;code&gt;35.199.192.0/19&lt;/code&gt; 이므로 &lt;strong&gt;사내 방화벽에서 그 대역의&lt;br&gt;udp/tcp 53 을 열고 응답 경로를 VPN 으로 잡아 주어야&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ingress=internal&lt;/code&gt; 은 요청이 들어온 VPC 의 프로젝트로 판정합니다.&lt;/strong&gt;&lt;br&gt;진입점이 PSC 든 아니든 상관없습니다. 허브처럼 &lt;strong&gt;별도 프로젝트를 거쳐 들어오는 구조&lt;/strong&gt;라면&lt;br&gt;그 안에서 VPC 를 어떻게 구성해도 404 입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared VPC 에서는 기준이 하나 더 붙습니다.&lt;/strong&gt; 서비스 프로젝트에 올린 Cloud Run 은&lt;br&gt;&lt;strong&gt;VPC 에 연결돼 있어야&lt;/strong&gt; 그 네트워크를 「내 것」으로 인정합니다.&lt;/p&gt;
&lt;p&gt;404 를 받았을 때 확인할 순서는 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;2분쯤 기다렸다 다시&lt;/strong&gt; 호출합니다. 전파 지연일 수 있습니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ingress=all&lt;/code&gt; 로 잠깐 바꿔 봅니다. &lt;strong&gt;403 이 나오면 경로는 정상&lt;/strong&gt;이고 ingress 판정 문제입니다&lt;/li&gt;
&lt;li&gt;요청이 들어온 VPC 가 &lt;strong&gt;서비스와 같은 프로젝트&lt;/strong&gt;인지 봅니다&lt;/li&gt;
&lt;li&gt;Shared VPC 라면 &lt;strong&gt;host 프로젝트에 있는지, 아니면 VPC 에 연결돼 있는지&lt;/strong&gt; 봅니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그리고 사내망에서 Cloud Run 을 부르는 일반적인 답은 &lt;strong&gt;내부 ALB&lt;/strong&gt; 입니다.&lt;br&gt;VIP 가 평범한 내부 IP 라 경로 제약이 없고, PSC 는 Google API 용으로 두는 편이 깔끔합니다.&lt;br&gt;프로젝트가 여럿으로 갈리는 Shared VPC 환경일수록 이 편이 단순합니다.&lt;/p&gt;
&lt;h3&gt;참고 문서&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/run/docs/securing/ingress&quot;&gt;Cloud Run 인그레스 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/run/docs/securing/private-networking&quot;&gt;Cloud Run 과 비공개 네트워킹&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/run/docs/configuring/shared-vpc-direct-vpc&quot;&gt;Shared VPC 와 Direct VPC egress&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vpc/docs/configure-private-service-connect-apis&quot;&gt;Private Service Connect 로 Google API 에 액세스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/dns/docs/zones/forwarding-zones&quot;&gt;Cloud DNS 전달 영역&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/3</guid>
      <comments>https://pubsub.tistory.com/3#entry3comment</comments>
      <pubDate>Fri, 16 Oct 2026 09:12:45 +0900</pubDate>
    </item>
    <item>
      <title># PSC 로 Google API 사설 호출 &amp;mdash; curl 이 000 과 401 을 뱉는 이유</title>
      <link>https://pubsub.tistory.com/2</link>
      <description>&lt;p&gt;사내망에서 인터넷을 거치지 않고 Google Cloud API 를 호출해야 할 때가 있습니다.&lt;br&gt;보안 정책상 아웃바운드가 막혀 있거나, 트래픽이 공중망을 타지 않아야 하는 경우입니다.&lt;/p&gt;
&lt;p&gt;Private Service Connect(PSC)로 엔드포인트를 만들면 VPC 안의 내부 IP 하나로&lt;br&gt;Google API 전체를 부를 수 있습니다. 그런데 &lt;strong&gt;막상 &lt;code&gt;curl&lt;/code&gt; 을 쳐 보면 생각보다 안 됩니다.&lt;/strong&gt;&lt;br&gt;&lt;code&gt;000&lt;/code&gt; 이 나오거나, 분명 토큰을 넣었는데 &lt;code&gt;401&lt;/code&gt; 이 돌아옵니다.&lt;/p&gt;
&lt;p&gt;이 글은 그 이유를 실제로 구성해 확인한 기록입니다. Cloud Storage 를 대상으로&lt;br&gt;세 가지 방식을 모두 실행했고, 결과를 그대로 실었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VM
 │  이름 해석 → 10.90.100.10
 ▼
PSC 엔드포인트 ── all-apis ──▶ Google API (GCS · Compute · OAuth2 …)&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;먼저 알아야 할 것 — SNI 와 Host 는 하는 일이 다릅니다&lt;/h2&gt;
&lt;p&gt;PSC 엔드포인트는 &lt;strong&gt;IP 하나로 모든 Google API 를 받습니다.&lt;/strong&gt; 그래서 서버 입장에서는&lt;br&gt;「이 요청을 어디로 보낼지」를 따로 알아야 합니다. 여기에 두 가지가 관여합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;무엇이&lt;/th&gt;
&lt;th&gt;무엇을 결정하는가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SNI&lt;/strong&gt; (TLS 핸드셰이크의 서버 이름)&lt;/td&gt;
&lt;td&gt;서버가 &lt;strong&gt;어떤 인증서&lt;/strong&gt;를 내어줄지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Host 헤더&lt;/strong&gt; (HTTP)&lt;/td&gt;
&lt;td&gt;요청을 &lt;strong&gt;어느 API 로 보낼지&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;SNI 를 바꿔 가며 어떤 인증서가 오는지 확인해 봤습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;자체 도메인이나 IP 로 접속하면 Google 이 &lt;code&gt;*.google.com&lt;/code&gt; 기본 인증서를 줍니다.&lt;/strong&gt;&lt;br&gt;이름이 맞지 않으니 curl 이 연결을 끊습니다. 이것이 뒤의 두 방법에서 손이 더 가는 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 1. PSC 전용 도메인&lt;/h2&gt;
&lt;p&gt;가장 단순합니다. 구성만 해 두면 &lt;strong&gt;curl 에 추가 옵션이 없습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PSC 엔드포인트를 만들 때 Service Directory 에 등록하면 &lt;code&gt;p.googleapis.com&lt;/code&gt; 비공개 영역이&lt;br&gt;자동으로 생깁니다. 이름 형식은 &lt;strong&gt;&lt;code&gt;&amp;lt;서비스&amp;gt;-&amp;lt;엔드포인트이름&amp;gt;.p.googleapis.com&lt;/code&gt;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TOKEN=$(gcloud auth print-access-token)

curl -s -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://storage-myep.p.googleapis.com/storage/v1/b/my-bucket/o/hello.txt?alt=media&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hello-psc                      ← 200. 객체 내용이 그대로 나옵니다&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;함정 — 엔드포인트 이름만으로는 해석되지 않습니다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;myep.p.googleapis.com                  (해석 안 됨)
storage-myep.p.googleapis.com          10.90.100.10   ✔
compute-myep.p.googleapis.com          (해석 안 됨)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;서비스 접두사가 반드시 붙어야 하고, 모든 API 에 레코드가 생기지도 않습니다.&lt;/strong&gt;&lt;br&gt;Service Directory 가 흔히 쓰는 API 에만 레코드를 만들기 때문입니다.&lt;br&gt;쓰려는 API 가 없으면 방법 2 로 가야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 2. 사설 도메인&lt;/h2&gt;
&lt;p&gt;자체 도메인(&lt;code&gt;gcs.mylab.internal&lt;/code&gt;)을 PSC IP 로 가리키게 한 경우입니다.&lt;br&gt;&lt;strong&gt;그대로는 실패합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;000                            ← 종료코드 60. 인증서 검증 실패&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;인증서 검증만 끄면 — 여전히 안 됩니다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;401                            ← TLS 는 통과했으나 인증 실패&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;이 부분에서 많이 헤맵니다.&lt;/strong&gt; &lt;code&gt;-k&lt;/code&gt; 는 TLS 문제만 덮습니다.&lt;br&gt;Host 헤더가 &lt;code&gt;gcs.mylab.internal&lt;/code&gt; 이라 Google 이 이 요청을 GCS 로 보내지 않습니다.&lt;br&gt;토큰이 멀쩡해도 401 입니다.&lt;/p&gt;
&lt;h3&gt;성립하는 조합 — &lt;code&gt;-k&lt;/code&gt; 와 Host 헤더를 함께&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk \
  -H &amp;#39;Host: storage.googleapis.com&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://gcs.mylab.internal/storage/v1/b/my-bucket/o/hello.txt?alt=media&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hello-psc                      ← 200&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;다만 &lt;strong&gt;인증서 검증을 끄는 것이 전제&lt;/strong&gt;이므로 운영에 권하지 않습니다.&lt;br&gt;중간자 공격을 탐지할 수 없게 됩니다.&lt;/p&gt;
&lt;h3&gt;검증을 끄지 않으려면&lt;/h3&gt;
&lt;p&gt;이름을 바꾸지 않고 &lt;strong&gt;IP 만 바꾸는&lt;/strong&gt; 방법이 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s --resolve &amp;#39;storage.googleapis.com:443:10.90.100.10&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://storage.googleapis.com/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;200                            ← 인증서 검증을 켠 채로 성공&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SNI 와 Host 가 모두 &lt;code&gt;storage.googleapis.com&lt;/code&gt; 이라 정상 인증서가 나오고 라우팅도 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;방법 3. 순수 IP&lt;/h2&gt;
&lt;p&gt;도메인 없이 IP 로 직접 호출합니다. 방법 2 와 같은 이유로 &lt;strong&gt;그대로는 실패합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;000                            ← 종료코드 60&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;401                            ← TLS 통과, 라우팅 실패&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;성립하는 조합&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sk \
  -H &amp;#39;Host: storage.googleapis.com&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://10.90.100.10/storage/v1/b/my-bucket/o/hello.txt?alt=media&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hello-psc                      ← 200&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;IP 호출에서는 Host 헤더가 필수입니다.&lt;/strong&gt; 이것이 없으면 어느 API 인지 알 수 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결과 한눈에&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;호출 방식&lt;/th&gt;
&lt;th&gt;추가 옵션&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;&lt;code&gt;storage-myep.p.googleapis.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;②&lt;/td&gt;
&lt;td&gt;&lt;code&gt;storage.googleapis.com&lt;/code&gt; (사설 DNS)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcs.mylab.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;000 (exit 60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;④&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcs.mylab.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑤&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcs.mylab.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt; + &lt;code&gt;Host:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑥&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.90.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;000 (exit 60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑦&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.90.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑧&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.90.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt; + &lt;code&gt;Host:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑨&lt;/td&gt;
&lt;td&gt;&lt;code&gt;storage.googleapis.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--resolve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;응답 코드를 구분해서 읽으십시오&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;000&lt;/code&gt;&lt;/strong&gt; · 종료코드 60 — TLS 핸드셰이크에서 끊겼습니다. &lt;strong&gt;인증서 문제&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;401&lt;/code&gt;&lt;/strong&gt; — TLS 는 성립했고 HTTP 응답을 받았습니다. &lt;strong&gt;라우팅이나 인증&lt;/strong&gt; 문제입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;403&lt;/code&gt;&lt;/strong&gt; — 도달했고 인증도 됐으나 &lt;strong&gt;권한이 없습니다.&lt;/strong&gt; 경로는 정상입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실제로 Compute API 를 같은 엔드포인트로 호출했더니 403 이 나왔습니다.&lt;br&gt;서비스 계정에 Compute 권한이 없어서였고, &lt;strong&gt;PSC 경유 자체는 성공&lt;/strong&gt;한 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s -o /dev/null -w &amp;#39;%{http_code} via %{remote_ip}\n&amp;#39; \
  -H &amp;quot;Authorization: Bearer $TOKEN&amp;quot; \
  &amp;#39;https://www.googleapis.com/compute/v1/projects/my-project/zones&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;403 via 10.90.100.10           ← PSC 를 거쳤습니다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;%{remote_ip}&lt;/code&gt; 를 찍어 보면 실제로 어느 IP 로 나갔는지 바로 확인됩니다.&lt;br&gt;공인 IP 가 나오면 DNS 가 적용되지 않은 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어느 방법을 쓸 것인가&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;새로 구성하고 &lt;strong&gt;코드를 고칠 수 있다&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;① PSC 도메인.&lt;/strong&gt; 옵션이 필요 없고 인증서 검증도 켜집니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;기존 코드를 고칠 수 없다&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;② 기본 도메인 + 사설 DNS.&lt;/strong&gt; 애플리케이션 변경이 없습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일회성 진단&lt;/td&gt;
&lt;td&gt;⑨ &lt;code&gt;--resolve&lt;/code&gt;. DNS 를 건드리지 않고 경로만 바꿔 봅니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자체 도메인을 꼭 써야 한다&lt;/td&gt;
&lt;td&gt;⑤. 다만 &lt;strong&gt;인증서 검증을 끄게 되므로 권하지 않습니다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;운영 환경에는 &lt;strong&gt;②&lt;/strong&gt; 가 무난합니다. 애플리케이션과 SDK 가 기본 도메인을 그대로 쓰므로&lt;br&gt;바꿀 것이 없고 인증서 검증도 유지됩니다. 클라이언트 라이브러리는 &lt;code&gt;--resolve&lt;/code&gt; 같은&lt;br&gt;옵션을 받지 않으므로, DNS 로 푸는 것이 사실상 유일한 길이기도 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;구성 명령&lt;/h2&gt;
&lt;h3&gt;PSC 엔드포인트&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;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&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--target-google-apis-bundle&lt;/code&gt; 은 &lt;code&gt;all-apis&lt;/code&gt; 와 &lt;code&gt;vpc-sc&lt;/code&gt; 중에 고릅니다.&lt;br&gt;VPC SC 경계 안에서만 쓸 것이라면 &lt;code&gt;vpc-sc&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;h3&gt;API 를 먼저 켜십시오&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud services enable servicedirectory.googleapis.com dns.googleapis.com --project=$P&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;이것이 꺼져 있으면 엔드포인트는 만들어지지만 이름으로 접근할 수 없습니다.&lt;/strong&gt;&lt;br&gt;저도 꺼진 채로 만들었다가 뒤늦게 켰습니다.&lt;/p&gt;
&lt;h3&gt;기본 도메인을 PSC 로 보내기 (방법 2)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;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 &amp;quot;*.googleapis.com.&amp;quot; --zone=psc-googleapis \
  --type=CNAME --ttl=60 --rrdatas=googleapis.com. --project=$P&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;영역 자체에 A 레코드를 만들고 나머지는 와일드카드 CNAME 으로 묶습니다.&lt;br&gt;이렇게 하면 &lt;code&gt;storage.googleapis.com&lt;/code&gt; · &lt;code&gt;www.googleapis.com&lt;/code&gt; · &lt;code&gt;oauth2.googleapis.com&lt;/code&gt; 이&lt;br&gt;모두 PSC 로 갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;막혔던 지점 정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p.googleapis.com&lt;/code&gt; 영역에 레코드가 0개&lt;/td&gt;
&lt;td&gt;Service Directory 연동 영역이라 Cloud DNS 에 레코드가 직접 없습니다. 정상입니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;myep.p.googleapis.com&lt;/code&gt; 해석 실패&lt;/td&gt;
&lt;td&gt;서비스 접두사가 필요합니다 — &lt;code&gt;storage-myep…&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt; 를 붙였는데 401&lt;/td&gt;
&lt;td&gt;Host 헤더가 없어 라우팅이 안 됩니다. TLS 와 별개 문제입니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;경로는 정상입니다. 서비스 계정 권한을 보십시오&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PSC 엔드포인트는 SNI 로 인증서를 고르고 Host 헤더로 라우팅합니다.&lt;/strong&gt;&lt;br&gt;이름을 바꾸면 인증서가 맞지 않고, 인증서를 무시해도 Host 가 맞지 않으면 라우팅이 안 됩니다.&lt;br&gt;두 가지를 모두 풀어야 200 이 나옵니다.&lt;/p&gt;
&lt;p&gt;그래서 가장 깔끔한 선택은 &lt;strong&gt;이름을 바꾸지 않는 것&lt;/strong&gt;입니다.&lt;br&gt;사설 DNS 로 &lt;code&gt;googleapis.com&lt;/code&gt; 을 PSC 쪽으로 돌려 두면, 애플리케이션은 아무것도 모른 채&lt;br&gt;사설 경로로 나갑니다.&lt;/p&gt;
&lt;h3&gt;참고 문서&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vpc/docs/configure-private-service-connect-apis&quot;&gt;Private Service Connect 로 Google API 에 액세스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vpc/docs/private-service-connect&quot;&gt;Private Service Connect 개요&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>GCP</category>
      <author>pubsub</author>
      <guid isPermaLink="true">https://pubsub.tistory.com/2</guid>
      <comments>https://pubsub.tistory.com/2#entry2comment</comments>
      <pubDate>Wed, 14 Oct 2026 15:17:46 +0900</pubDate>
    </item>
  </channel>
</rss>