Connect Google Cloud
Workload identity federation with no key file, the reader service account, the custom metadata-only role and its exact permission list — rendered from the file that defines it.
Connecting a project
A Google Cloud project is an account of its own cloud. Estate reads its BigQuery datasets, service accounts, Firebase apps and Cloud Functions as the project's reader service account. The platform holds no key file: its own AWS credentials, IAM Roles Anywhere on the platform host, are exchanged at Google's Security Token Service through workload identity federation, and the federated token impersonates the reader. For an operator who knows AWS: a project is an account, a service account is a role, a custom role is a customer-managed policy, and the binding that lets the platform impersonate the reader is the trust policy.
Once per deployment, in a project the operator holds: a workload identity
pool whose provider accepts only the platform's home identity, and
GCP_WORKLOAD_IDENTITY_PROVIDER set on work and serve to the provider's
name,
//iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>.
Under an AWS home (the default) it is an AWS provider that accepts only the
platform's AWS role, its attribute.aws_role mapped to the role's name — the
binding below names it so (Google's default AWS mapping would make it the
assumed-role ARN instead):
gcloud iam workload-identity-pools create panorama --project=POOL_PROJECT --location=global
gcloud iam workload-identity-pools providers create-aws panorama-aws --project=POOL_PROJECT \
--location=global --workload-identity-pool=panorama --account-id=PLATFORM_ACCOUNT_ID \
--attribute-mapping="google.subject=assertion.arn,attribute.aws_role=assertion.arn.extract('assumed-role/{role}/')" \
--attribute-condition="attribute.aws_role=='PLATFORM_ROLE'"
Under an Azure home it is an OIDC provider for the
directory's issuer (--issuer-uri=https://sts.windows.net/TENANT_ID/ --allowed-audiences=api://CLIENT_ID). Under a Google home it is an X.509
provider, and no AWS is involved at all:
# trust_store.yaml: the CA that issued the host's certificate
# trustStore:
# trustAnchors:
# - pemCertificate: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
gcloud iam workload-identity-pools create panorama-x509 --project=POOL_PROJECT --location=global
gcloud iam workload-identity-pools providers create-x509 panorama-host --project=POOL_PROJECT \
--location=global --workload-identity-pool=panorama-x509 \
--trust-store-config-path=trust_store.yaml \
--attribute-mapping="google.subject=assertion.subject.dn.cn" \
--attribute-condition="assertion.subject.dn.cn=='HOST_CN'"
with PLATFORM_HOME_CLOUD=gcp, GCP_CERTIFICATE_FILE (the host's
certificate, PEM, leaf first) and GCP_PRIVATE_KEY_FILE (its key). The
exchange is made over mutual TLS with that certificate at
sts.mtls.googleapis.com; the key never leaves the host. The certificate is
at most 390 days, P-256 or RSA, issued by a CA kept off the platform host.
Until serve has it, the portal does not offer Google Cloud: GET /auth/me
lists the clouds this deployment reads, and Admin → Accounts has no
project to connect. A project registered while it was set stays listed in
its own block, which says "This deployment does not read Google Cloud." and
counts none of its accounts as being read.
Per project, in the customer's project. The role file is in the deploy/
the install copied out of the image (from platform 0.1.2; the README's
Quickstart), so run these from the directory that holds it:
gcloud iam service-accounts create panorama-reader --project=PROJECT_ID
gcloud iam roles create panoramaMetadataReader --project=PROJECT_ID \
--file=deploy/gcp/reader-role.yaml
gcloud projects add-iam-policy-binding PROJECT_ID \
--member=serviceAccount:panorama-reader@PROJECT_ID.iam.gserviceaccount.com \
--role=projects/PROJECT_ID/roles/panoramaMetadataReader --condition=None
gcloud iam service-accounts add-iam-policy-binding \
panorama-reader@PROJECT_ID.iam.gserviceaccount.com --project=PROJECT_ID \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/POOL_PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/attribute.aws_role/PLATFORM_ROLE"
That member is the AWS home's. Under a Google home it is the host's subject,
principal://iam.googleapis.com/projects/POOL_PROJECT_NUMBER/locations/global/workloadIdentityPools/panorama-x509/subject/HOST_CN;
under an Azure home, principal://.../workloadIdentityPools/POOL/subject/SERVICE_PRINCIPAL_OBJECT_ID.
Then register it, verify it, and enable it:
panorama admin add-gcp-project --tenant-slug acme --project-id PROJECT_ID \
--name "Acme production" --reader panorama-reader@PROJECT_ID.iam.gserviceaccount.com
The registration is written to the audit log as estate.account.register,
with the system as the actor; POST /admin/accounts/projects writes
the same row with the administrator as the actor.
Google applies new IAM bindings within a few minutes: a verification run
straight after the two bindings above can fail with Permission 'iam.serviceAccounts.getAccessToken' denied, and passes when run again a
little later.
Verification (POST /admin/accounts/{accountId}/verify) signs in as the
reader, reads the reader account and the project, then runs each Google
collector's probe; each check names the permission it needs. It then
discovers the project's regions once and adds the ones in use to the
account, so the first collection is already regional, and lists them. A
tenant without Estate is not probed and nothing is discovered: the answer
lists those checks as skipped, with why, rather than as passed. Enabling
(PUT /admin/accounts/{accountId}) is the same entitlement-checked step as an
AWS account's, and a project counts toward the licence's account cap.
The same three steps from the command line, for an operator on the host or a
deployment whose administrators have no portal session to hand, and one
collection on a deployment that runs no schedule:
panorama admin verify-account --tenant-slug acme --account-id PROJECT_ID
panorama admin enable-account --tenant-slug acme --account-id PROJECT_ID
panorama admin collect-now --tenant-slug acme --account-id PROJECT_ID
verify-account runs the API's verification and prints each check; it exits
non-zero when one fails. It works for an AWS account too, without the cost
report's checks, which are the API's alone. It reads entitlements as serve
does: with ENTITLEMENTS_UNRESTRICTED=true a tenant with no entitlement runs
every probe, and the flag beside an installed licence is refused. enable-account is the API's
enable under the same account cap (--disable switches it off); the change is
written to the audit log as estate.account.update, with the system as the
actor. collect-now
queues one job for each inventory module of the account's cloud, and a worker
collects them; a module with a job already waiting keeps that one. It is
written to the audit log as estate.account.collect, with the system as the
actor, in the transaction that queues the jobs; the Admin area's
Collect now is the same row with the administrator as the actor.
An AWS account that moves to a renamed reader role is repointed the same way:
panorama admin set-role-arn --tenant-slug acme --account-id 111122223333 \
--role-arn arn:aws:iam::111122223333:role/panorama-reader
It is the API's roleArn update, under the API's rules. The ARN must be an
IAM role in this account, in a partition AWS has. The external id does not
change. The change is written to the audit log as estate.account.update,
with the system as the actor. Run verify-account after it.
deploy/cloudformation/platform-identity.md §5 gives the whole move.
An account's regions change the same way:
panorama admin set-regions --tenant-slug acme --account-id 111122223333 \
--regions eu-west-1,eu-north-1
It is the API's regions update, under the API's rules: no region may be
blank. The list replaces the old one, so name every region to keep. The change
is written to the audit log as estate.account.update, with the system as the
actor. Run collect-now after it. A new AWS region also wants
ec2-lifecycle-events.yaml deployed there (deploy/cloudformation/README.md,
"EC2 instances between two reads"); until it is, that region's lifecycle read
reports that the queue is not set up, and the inventory is collected regardless.
What is read, and what is not: reader-role.yaml grants thirty-three
permissions. None returns table rows, routine bodies, function configuration
or environment, Firestore documents, messages or keys. Eleven would allow more
than the product reads: compute.instances.list returns an instance's
metadata items (startup scripts, SSH keys), container.clusters.list a
cluster's master credentials and node metadata, run.services.list a Cloud
Run service's containers' environment, arguments and command and its build's
configuration, pubsub.topics.list a topic's message transforms
(JavaScript source), pubsub.subscriptions.list the same transforms and a
push subscription's endpoint, the backend services' list and get, global and
regional, a backend service's custom header values and secrets' identifiers
and hashes, and the URL maps' lists, global and regional, the header values a
route adds and matches (the load balancers). Every request carries a
field mask that keeps those off the wire, and an answer that carries them
anyway is refused whole — the run is incomplete, with Google's answer in its
message, and nothing of it is stored (MILESTONE_1 §13.3).
Load balancers. A forwarding rule is the row — what a person
knows as the load balancer — with the chain behind it and its backends'
health. gcp.compute.forwardingrule reads a region's rules, and
gcp.compute.globalforwardingrule the project's global ones once under
global; each reads the target proxies, URL maps and backend services beside
its rules, and only when a rule names them, so a project with no load
balancer costs one read per run. Health is backendServices.getHealth, a POST
that reads, asked once per backend group Google checks: instance groups and
zonal endpoint groups. A rule's state says what it came to: healthy,
unhealthy-backends (some), no-healthy-backends (none, or a group with
nothing in it), and three unknowns — no-backends (an HTTP-to-HTTPS
redirect), health-not-checked (serverless, internet and bucket backends,
which Google does not health-check) and backends-not-read (a target pool or
instance, whose own health this does not read). chain-unread means a
listing or health read behind the rule failed — a permission the role lacks
shows here with Google's message under Not read — and the rule is still
listed. A Private Service Connect endpoint takes its connection's state.
Google reports each endpoint's IPv4 and IPv6 health apart: a passthrough rule
(one naming its backend service) is judged in its own address family, so an
IPv6 one by its backends' IPv6 health; a proxy rule, whatever its frontend's
version, by the IPv4 health of the connections it opens to its backends. Cloud Functions are read from the
Functions API's listing of every location, masked to each function's
name, state, labels and times; an answer carrying its environment, secrets,
build or trigger is refused whole. A function is listed under its own name
whether or not it has ever run, carries its own labels, takes its status from
its state (active, deploying, failed, …), and goes when the listing stops
naming it. Its triggers come from the execution_count metrics of the last two
weeks, matched by that name; when those cannot be read the function is still
listed, Triggers not read. A second-generation function's Cloud Run service is
listed as well, labelled Managed by cloudfunctions, under the
service's own name, which Google lowercases and hyphenates. The reader role
must carry cloudfunctions.functions.list: an existing project's role takes it
with the gcloud iam roles update command at the head of
deploy/gcp/reader-role.yaml; until then the function collection is denied.
Regions. Instances, GKE clusters, subnets, Cloud Run
services and regional forwarding rules are keyed by the region their zone or
location is in, and read one region at a time; datasets, service accounts,
apps, functions, Cloud SQL instances, buckets, networks, topics,
subscriptions and global forwarding rules are global in the
resource key, their location — where they have one — a property the region
filter reads. A project registers with no regions. Region discovery
(gcp.discovery.region, daily per project, and once at verification) reads
one names-only listing per regional service and adds the regions in use to
the account; the scheduler then runs the regional collectors there. The
subnets Google creates in every region for an auto-mode network (default)
are not use: a region holding only those is not added, and they appear on
their network's row, not as rows of their own. Cloud Run's listing of every
location names Google's restricted regions as unreachable on every call: discovery asks each such location on its own, and one that refuses
the project with LOCATION_POLICY_VIOLATED — a region Google has not opened
to it — is not applicable and is skipped (a debug log line). A location that
still cannot be read is recorded in the discovery's message, not as a
failure, and fails verification's check with Google's answer. It only adds: an admin removes a region
(PUT /admin/accounts/{accountId} with regions), and may add one by hand the
same way. A resource in a region the project had never used appears after the
next discovery; Estate → Accounts says when that last ran and how many regions
it found. A discovery Google refused part of — an API the project has not
enabled — is recorded as finished, with the refusal in its message, and adds
what it could read; any other failure fails the run and is retried. A regional
collector in a region where the project has not enabled that service's API
fails its run with Google's message until the API is enabled.
What the reader role may do
panoramaMetadataReader
- bigquery.datasets.get
- cloudfunctions.functions.list
- cloudsql.instances.list
- compute.backendServices.get
- compute.backendServices.list
- compute.forwardingRules.list
- compute.globalForwardingRules.list
- compute.instances.list
- compute.networks.list
- compute.regionBackendServices.get
- compute.regionBackendServices.list
- compute.regionTargetHttpProxies.list
- compute.regionTargetHttpsProxies.list
- compute.regionTargetTcpProxies.list
- compute.regionUrlMaps.list
- compute.regions.get
- compute.subnetworks.list
- compute.targetGrpcProxies.list
- compute.targetHttpProxies.list
- compute.targetHttpsProxies.list
- compute.targetSslProxies.list
- compute.targetTcpProxies.list
- compute.urlMaps.list
- container.clusters.list
- firebase.clients.list
- firebase.projects.get
- iam.serviceAccounts.get
- iam.serviceAccounts.list
- monitoring.timeSeries.list
- pubsub.subscriptions.list
- pubsub.topics.list
- resourcemanager.projects.get
- run.services.list
- storage.buckets.list
Generated from observability/docs/runbook.md (78aada3d0ac7), observability/deploy/gcp/reader-role.yaml (84df9f50026d) by www/tools/docs; the value in brackets is a hash of the source's content, so a page changes only when its source does.