Connect with a GCP account
Create a dedicated project and a read-only service account for Valkan, grant it access across your projects, then connect and scan. The same steps are given three ways — Cloud Console, Cloud Shell or bash, and PowerShell.
Before choosing Connect Google Cloud in Valkan, create a dedicated project with a service account, grant it read-only access, enable a few APIs, and generate a JSON key. Valkan only reads your Google Cloud resources; it never creates or modifies them.
A single connection covers every project the service account can access. To stop scanning one, untick it under Projects on the integration's page.
You need permission to:
- create a project, and a service account in it (Service Account Admin, or Owner)
- create a service account key (Service Account Key Admin, or Owner)
- in each project you want scanned, define a custom role and grant it: Owner, or both Role Administrator (
roles/iam.roleAdmin) and Project IAM Admin (roles/resourcemanager.projectIamAdmin)
Editor is not enough for the last one: it carries neither iam.roles.create nor resourcemanager.projects.setIamPolicy, so both commands in step 2 fail on a project where Editor is all you hold. That is easy to miss, because such a project still appears in the survey below.
Valkan only reads. Every call it makes to Google Cloud is a list or a get: it never creates, changes or deletes anything in your projects. It lists secret and bucket names, never secret values or bucket contents. The role below is read-only, so Google would refuse a write even if one were attempted. The commands in these steps are ones you run yourself; Valkan never runs them.
First, check what your account has
gcloud projects list --format="table(projectId,name,parent.type,parent.id)"These are your existing projects, the ones holding the resources Valkan reports on. They are not candidates for holding the service account, which is always a new project created in step 1.
Decide now which of them Valkan should cover. Every route below configures only the projects you name, and two of the steps cost money in each one — enabling APIs and turning on DNS query logging are billable. Write the list down before you start: on an organization-wide login this survey can run to hundreds of projects, and there is no reason to touch the ones you are not onboarding.
A project you add later needs step 2 run again for it. Access is granted per project, so a new project is not picked up on its own. Valkan reports a project it can see referenced but not read — specifically, one named as the host of a Shared VPC network a covered project uses — as a coverage gap rather than leaving it absent. That is a floor, not a census: it catches only a project your covered infrastructure points at, so re-run step 2 whenever you add one instead of waiting to be told.
A project awaiting deletion does not appear in this list. In the console the same overview is IAM & Admin → Manage resources (console.cloud.google.com/cloud-resource-manager).
Showing the organization and folder names above your projects needs Organization Viewer and Folder Viewer, granted at the organization. They are optional: without them, projects are still scanned.
Choose one of the setup methods below. All perform the same steps, and the two terminal options differ only in shell syntax.
1. Create the project and service account
Create the project at console.cloud.google.com/projectcreate and name it valkan-discovery. Project IDs are globally unique, so the console may append digits; note the ID it settles on, because step 4 needs it. It needs no billing account.
Now open IAM & Admin → Service Accounts (console.cloud.google.com/iam-admin/serviceaccounts) with the new project selected in the top-left switcher. Click Create service account, name it valkan, and click Create and continue. When it offers to grant roles, click Done: the role comes from step 2, and it is granted in your other projects.
Copy the account's email address from the list. It looks like valkan@<project-id>.iam.gserviceaccount.com, and both later steps need it.
2. Create the read-only role and grant it
Valkan uses its own role definition rather than a broad predefined role. It contains 47 read permissions, every one of them a list or a get, and nothing that can create, change or delete. The nearest predefined role that would also work, Security Reviewer, carries more than two thousand.
{
"title": "ValkanDiscovery Read-Only (single project)",
"description": "Read-only Valkan Discovery for one project: identity inventory, IAM grants and deny policies, AI and compute workloads, Model Armor, data services, and audit metadata. No mutate permissions.",
"stage": "GA",
"includedPermissions": [
"resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy",
"iam.serviceAccounts.list",
"iam.serviceAccounts.getIamPolicy",
"iam.serviceAccountKeys.list",
"iam.denypolicies.list",
"iam.denypolicies.get",
"iam.roles.list",
"iam.workloadIdentityPools.list",
"iam.workloadIdentityPoolProviders.list",
"run.services.list",
"cloudfunctions.functions.list",
"aiplatform.reasoningEngines.list",
"aiplatform.endpoints.list",
"discoveryengine.engines.list",
"discoveryengine.dataStores.list",
"dialogflow.agents.list",
"dialogflow.agents.get",
"dialogflow.webhooks.list",
"dialogflow.fulfillments.get",
"logging.logEntries.list",
"logging.privateLogEntries.list",
"monitoring.timeSeries.list",
"compute.networks.list",
"compute.instances.list",
"compute.firewalls.list",
"compute.subnetworks.list",
"dns.policies.list",
"secretmanager.secrets.list",
"cloudsql.instances.list",
"cloudsql.users.list",
"cloudkms.locations.list",
"cloudkms.keyRings.list",
"cloudkms.cryptoKeys.list",
"cloudkms.cryptoKeys.getIamPolicy",
"modelarmor.floorSettings.get",
"modelarmor.templates.list",
"storage.buckets.list",
"bigquery.datasets.get",
"pubsub.topics.list",
"pubsub.topics.getIamPolicy",
"container.clusters.list",
"container.clusters.get",
"container.deployments.list",
"container.statefulSets.list",
"container.daemonSets.list",
"container.cronJobs.list"
]
}A custom role can only be granted inside the project that defines it, so create the role and grant it once in each project.
Choose the projects first
Work from the list you decided on in the survey above, not from everything the account can see. Steps 4 and 5 are billable in each project you configure, and on an organization-wide login the survey may list far more projects than you intend to onboard. Keep the list beside you: steps 2, 4, 5 and 6 each apply to it.
With the first of those projects selected in the top-left switcher, open IAM & Admin → Roles (console.cloud.google.com/iam-admin/roles) and click Create role:
- Title —
Valkan Discovery Read Only - ID —
valkanDiscoveryReadOnly - Role launch stage —
General Availability
Click Add permissions. That dialog filters a long list, so paste one permission name at a time into its filter box, tick the result, and repeat for all
- A permission whose API is not enabled yet (step 4) shows a "not supported" warning — add it anyway. The role keeps it, and it starts working when the API is on. Then click Create.
If valkanDiscoveryReadOnly already exists in this project from an earlier setup, edit it instead of creating a second role, and check all 47 permissions are present. A role made from an older version of this page keeps that version's permissions, and the ones added since never arrive — the scan then reports permission gaps and lists no virtual machines.
Still in that project, open IAM & Admin → IAM (console.cloud.google.com/iam-admin/iam), click Grant access, put the service account's email address in New principals, choose Valkan Discovery Read Only from the Custom group, and click Save.
Repeat both halves for each remaining project on your list, and no others. The two terminal methods loop over an explicit list instead, which is worth switching to if you have more than a handful.
You still add a single source in Valkan, however many projects it covers: the service account reports back every project it can read.
To undo this later, remove the service account's binding and delete the role in each project. Deleting the project that holds the service account does not remove grants made elsewhere.
3. Create a key
Open the service account, go to its Keys tab, then Add key → Create new key → JSON → Create. A file downloads. You paste its contents into Valkan in step 7, and it is the only copy Google gives you.
4. Enable the required APIs
Open APIs & Services → Library (console.cloud.google.com/apis/library), search each API by name, and click Enable.
Required — connecting fails without it:
- Cloud Resource Manager API (
cloudresourcemanager.googleapis.com). Valkan uses it to find out which projects the service account can read. Without it, Connect returns a permission error and no source is created.
Needed by the areas they cover. Leaving one off does not stop a scan; that area is reported as a coverage gap instead, so you can add them later:
- Identity and Access Management API (
iam.googleapis.com) — service accounts, their keys and their IAM policies. This is the core of what Valkan inventories on Google Cloud, so leaving it off produces a scan that finds almost nothing. - Compute Engine API (
compute.googleapis.com) — virtual machines, their firewall exposure, and the check for networks without DNS logging - Cloud Logging API (
logging.googleapis.com) — audit and activity evidence - Cloud DNS API (
dns.googleapis.com) — DNS query evidence - Cloud Storage API (
storage.googleapis.com) — storage buckets - Secret Manager API (
secretmanager.googleapis.com) — secrets - Cloud Run Admin API (
run.googleapis.com) — Cloud Run services - Cloud Functions API (
cloudfunctions.googleapis.com) — Cloud Functions - Vertex AI API (
aiplatform.googleapis.com) — Vertex endpoints and reasoning engines - Kubernetes Engine API (
container.googleapis.com) — GKE clusters - Cloud SQL Admin API (
sqladmin.googleapis.com) — Cloud SQL instances and their IAM database users - Cloud Key Management Service (KMS) API (
cloudkms.googleapis.com) — KMS keys and who can use them - Model Armor API (
modelarmor.googleapis.com) — whether a Model Armor floor setting covers Vertex AI agents - Cloud Monitoring API (
monitoring.googleapis.com) — Vertex AI model usage counts - Discovery Engine API (
discoveryengine.googleapis.com) — Vertex AI Agent Builder apps and their data stores - Dialogflow API (
dialogflow.googleapis.com) — Dialogflow agents and their webhooks - BigQuery API (
bigquery.googleapis.com) — datasets and who can read them - Cloud Pub/Sub API (
pubsub.googleapis.com) — topics and who can publish to them
Enable these in each project on your list, switching project in the top-left switcher as you go. Granting access to a project does not enable its APIs. Enabling an API is a billable change to that project, so do not sweep projects you are not onboarding.
The project holding the service account needs only the Cloud Resource Manager API, because that is the one call Valkan makes without naming a project. Everything else is read from the project that owns the resource.
If that is more than a handful of projects, the two terminal methods loop over your list in one command.
5. Enable DNS query logging
Google Cloud leaves this off, and Valkan needs it for two things: the DNS tab, and attributing AI use to the machine making it. A VM resolving api.openai.com is how an agent nobody registered gets discovered, and without query logging there is nothing to read. Valkan reports a network with logging switched off as a finding, so skipping this step does not go unnoticed — it just leaves the gap open.
The policy costs nothing. The logs it produces are ingested by Cloud Logging, which is free up to a monthly allowance per project and charged by volume above it, so a large or chatty fleet can reach a bill. Step 6 is there for that case.
Logging attaches to a VPC network, not to a project, and a project can have several. You need Compute Network Admin or Owner on each project, which the person doing the rest of this setup may not have.
In the console this is Network Services → Cloud DNS → DNS server policies → Create policy, with Logs set to On and the network added under Networks. Repeat it for every network in each project on your list — the two terminal methods loop over that list, which is worth switching to here. This is the other billable step, so again, do not include projects you are not onboarding.
A network that already has a policy attached is reported and skipped rather than overwritten, because that policy may be someone else's. Turn logging on for it by editing the existing policy instead. Under a Shared VPC the network lives in the host project, so logging is set there and covers the service projects using it.
6. (Optional) Shorten DNS log retention to reduce storage cost
Skip this unless log volume is high enough for storage cost to matter, because logging already works at the default 30-day retention. This step is riskier than it appears: _Default is not DNS-only, so shortening its retention shortens retention for every log type routed there — data-access logs, application logs, anything else that project sends to it.
This is the one step with no loop at all. Reading is safe, so the check below reports your whole list at once; the change is deliberate and applies to one project, because a loop would quietly shorten retention estate-wide for logs other people rely on, and log data already aged out does not come back.
Per project, Logging → Logs storage shows _Default's retention, and Network Services → Cloud DNS → DNS server policies shows what is logging into it. The two terminal methods report every project in one pass.
Pick a project from that output only if it shows retention=30, the default, and logging-policies=0, meaning nothing has been logging to _Default yet. Anything else means the bucket is already carrying logs something depends on, and shortening it would cut their retention too.
In the console, open Logging → Logs storage, click _Default, choose Edit bucket and set the retention to 1 day.
1. Create the project and service account
Run these in Cloud Shell, or in a terminal where you have already run gcloud auth login. Apart from the project name, nothing needs typing in: the values every later command uses are read back from your session.
# Project IDs are globally unique, so put something of your own on the end.
gcloud projects create valkan-discovery-1234 --name="Valkan Discovery"
gcloud config set project valkan-discovery-1234
PROJECT=$(gcloud config list --format="value(core.project)")
echo "project=$PROJECT"
gcloud iam service-accounts create valkan \
--project="$PROJECT" --display-name="Valkan Discovery"
SA="valkan@$PROJECT.iam.gserviceaccount.com"project= should be the project you just created. It holds the service account and nothing else, so it is not one of the projects Valkan scans.
If your account belongs to an organization, some organizations require a new project to name its parent. Add --organization=$(gcloud organizations list --format="value(ID)" | head -1) to the create command in that case.
2. Create the read-only role and grant it
Valkan uses its own role definition rather than a broad predefined role. It contains 47 read permissions, every one of them a list or a get, and nothing that can create, change or delete. Save it as gcp-iam-policy-project.json:
{
"title": "ValkanDiscovery Read-Only (single project)",
"description": "Read-only Valkan Discovery for one project: identity inventory, IAM grants and deny policies, AI and compute workloads, Model Armor, data services, and audit metadata. No mutate permissions.",
"stage": "GA",
"includedPermissions": [
"resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy",
"iam.serviceAccounts.list",
"iam.serviceAccounts.getIamPolicy",
"iam.serviceAccountKeys.list",
"iam.denypolicies.list",
"iam.denypolicies.get",
"iam.roles.list",
"iam.workloadIdentityPools.list",
"iam.workloadIdentityPoolProviders.list",
"run.services.list",
"cloudfunctions.functions.list",
"aiplatform.reasoningEngines.list",
"aiplatform.endpoints.list",
"discoveryengine.engines.list",
"discoveryengine.dataStores.list",
"dialogflow.agents.list",
"dialogflow.agents.get",
"dialogflow.webhooks.list",
"dialogflow.fulfillments.get",
"logging.logEntries.list",
"logging.privateLogEntries.list",
"monitoring.timeSeries.list",
"compute.networks.list",
"compute.instances.list",
"compute.firewalls.list",
"compute.subnetworks.list",
"dns.policies.list",
"secretmanager.secrets.list",
"cloudsql.instances.list",
"cloudsql.users.list",
"cloudkms.locations.list",
"cloudkms.keyRings.list",
"cloudkms.cryptoKeys.list",
"cloudkms.cryptoKeys.getIamPolicy",
"modelarmor.floorSettings.get",
"modelarmor.templates.list",
"storage.buckets.list",
"bigquery.datasets.get",
"pubsub.topics.list",
"pubsub.topics.getIamPolicy",
"container.clusters.list",
"container.clusters.get",
"container.deployments.list",
"container.statefulSets.list",
"container.daemonSets.list",
"container.cronJobs.list"
]
}A custom role can only be granted inside the project that defines it, so the role is created and granted once in each project.
Choose the projects first
Every command from here on changes the projects you name, and steps 4 and 5 cost money — enabling APIs and turning on DNS logging are billable. So name them explicitly rather than sweeping your whole account. See what you can reach:
gcloud projects list --format="value(projectId)"Then set the list to the projects you want Valkan to cover, and check it before anything changes:
PROJECTS="my-prod-project my-staging-project"
for P in $PROJECTS; do echo " will configure: $P"; doneIf you do intend every project your account can see — reasonable for a small estate, rarely what you want on an organization-wide login — ask for it deliberately, then read the list it prints before continuing:
PROJECTS=$(gcloud projects list --format="value(projectId)")Keep this shell open: $PROJECTS and $SA are used by steps 2, 4, 5 and 6.
Create and grant
for P in $PROJECTS; do
if gcloud iam roles describe valkanDiscoveryReadOnly --project="$P" >/dev/null 2>&1; then
# Already there from an earlier setup. It must be UPDATED: a role created by
# an older version of this page keeps that version's permissions, and the
# ones added since -- compute.instances.list, for instance -- never arrive.
# The scan then reports permission gaps and lists no virtual machines.
# --quiet: without an etag in the file, update can prompt before
# overwriting, and a loop must not depend on how a non-interactive
# shell answers that prompt.
if ! gcloud iam roles update valkanDiscoveryReadOnly --project="$P" --quiet \
--file=gcp-iam-policy-project.json >/dev/null 2>&1; then
echo "FAILED $P (role exists and could not be updated)"
continue
fi
echo "updated $P"
elif ! gcloud iam roles create valkanDiscoveryReadOnly --project="$P" \
--file=gcp-iam-policy-project.json >/dev/null 2>&1; then
echo "skipped $P (no permission to create a role here)"
continue
fi
if gcloud projects add-iam-policy-binding "$P" \
--member="serviceAccount:$SA" \
--role="projects/$P/roles/valkanDiscoveryReadOnly" >/dev/null 2>&1; then
echo "granted $P"
else
echo "FAILED $P (binding refused)"
fi
doneOne line per project. granted is done, and updated means the role was already there and has been brought up to date. skipped means your account cannot create a role there — usually because you hold Editor rather than Owner on it, which is common for a project someone else set up. Check with:
gcloud projects get-iam-policy <PROJECT_ID> --flatten="bindings[].members" --filter="bindings.members:<YOUR_EMAIL>" --format="value(bindings.role)"
A skipped project is never scanned. Valkan reports it as a coverage gap if anything in a project it can read points at it; otherwise its absence is silent. Note the skips and either have someone with Owner make the grant, or accept that those projects stay invisible.
The output is suppressed on purpose. Left alone, each project prints its whole role definition and then its entire IAM policy, every member of every binding included, which buries the one line you need.
Creating the role also warns about permissions whose APIs are not enabled yet (step 4). The role is still created with them, and they start working once the API is on.
You still add a single source in Valkan, however many projects it covers: the service account reports back every project it can read.
To undo this later, remove the service account's binding and delete the role in each project. Deleting the project that holds the service account does not remove grants made elsewhere.
3. Create a key
gcloud iam service-accounts keys create valkan-key.json --iam-account="$SA"You paste the contents of valkan-key.json into Valkan in step 7. Delete the local file once it is in.
4. Enable the required APIs
The first API is required: connecting fails without it, because Valkan uses it to find out which projects the service account can read. The rest are needed by the areas they cover, and leaving one off does not stop a scan; that area is reported as a coverage gap instead.
This enables them in every project your account can see, one API at a time. Enabling an API in one project does not enable it in another, and granting access to a project does not enable anything in it.
Enable them individually, not in one call. gcloud services enable given several APIs is all-or-nothing: if any one of them is refused, none are enabled. Compute Engine and Cloud DNS both require the project to have a billing account, so on a project without one a single batched call fails and takes cloudresourcemanager.googleapis.com down with it — the API that Connect cannot work without. The loop below avoids that, and a project reported as partial is usually missing exactly those two.
The project holding the service account needs the Cloud Resource Manager API, because that is the one call Valkan makes without naming a project. Everything else is read from the project that owns the resource, so the APIs refused there for want of billing do not matter.
APIS="cloudresourcemanager iam compute logging dns storage secretmanager run cloudfunctions aiplatform container sqladmin cloudkms modelarmor monitoring discoveryengine dialogflow bigquery pubsub"
for P in $PROJECTS; do
FAILED=""
for A in $APIS; do
gcloud services enable "$A.googleapis.com" --project="$P" >/dev/null 2>&1 \
|| FAILED="$FAILED $A"
done
if [ -z "$FAILED" ]; then
echo "all on $P"
else
echo "partial $P (could not enable:$FAILED)"
fi
done| API | Covers |
|---|---|
cloudresourcemanager.googleapis.com | Required. Which projects are readable, and project IAM |
iam.googleapis.com | Service accounts, their keys and IAM policies — the core inventory |
compute.googleapis.com | Virtual machines, their firewall exposure, networks without DNS logging |
logging.googleapis.com | Audit and activity evidence |
dns.googleapis.com | DNS query evidence |
storage.googleapis.com | Storage buckets |
secretmanager.googleapis.com | Secrets |
run.googleapis.com | Cloud Run services |
cloudfunctions.googleapis.com | Cloud Functions |
aiplatform.googleapis.com | Vertex AI endpoints and reasoning engines |
container.googleapis.com | GKE clusters |
sqladmin.googleapis.com | Cloud SQL instances and their IAM database users |
cloudkms.googleapis.com | KMS keys and who can use them |
modelarmor.googleapis.com | Whether Model Armor covers Vertex AI agents |
monitoring.googleapis.com | Vertex AI model usage counts |
discoveryengine.googleapis.com | Agent Builder apps and their data stores |
dialogflow.googleapis.com | Dialogflow agents and their webhooks |
bigquery.googleapis.com | Datasets and who can read them |
pubsub.googleapis.com | Topics and who can publish to them |
Expect a few seconds per API per project, so give it a minute on a handful of projects. partial means the rest were enabled and only the listed ones were refused; those areas report a coverage gap rather than failing a scan.
Five of the eleven need the project to have a billing account: compute, dns, secretmanager, run and container. On a project without one they are refused and the other six still go on, which is why they are enabled individually. The remedy is a billing account on that project.
The project holding the service account is the exception worth checking. It needs cloudresourcemanager for the one call Valkan makes without naming a project, and iam if you want its own service accounts inventoried; partial ... (could not enable: compute dns secretmanager run container) there is the expected result, not a problem.
5. Enable DNS query logging
Google Cloud leaves this off, and Valkan needs it for two things: the DNS tab, and attributing AI use to the machine making it. A VM resolving api.openai.com is how an agent nobody registered gets discovered, and without query logging there is nothing to read. Valkan reports a network with logging switched off as a finding, so skipping this step does not go unnoticed — it just leaves the gap open.
The policy costs nothing. The logs it produces are ingested by Cloud Logging, which is free up to a monthly allowance per project and charged by volume above it, so a large or chatty fleet can reach a bill. Step 6 is there for that case.
Logging attaches to a VPC network, not to a project, and a project can have several. You need Compute Network Admin or Owner on each project, which the person doing the rest of this setup may not have.
for P in $PROJECTS; do
for N in $(gcloud compute networks list --project="$P" --format="value(name)" 2>/dev/null); do
echo "=== $P / $N"
gcloud dns policies create "valkan-dns-logging-$N" --project="$P" \
--networks="$N" --enable-logging \
--description="Valkan: log VPC DNS resolutions" || echo "!!! skipped $P/$N"
done
doneA network that already has a policy attached is reported and skipped rather than overwritten, because that policy may be someone else's. Turn logging on for it by editing the existing policy instead. Under a Shared VPC the network lives in the host project, so logging is set there and covers the service projects using it.
6. (Optional) Shorten DNS log retention to reduce storage cost
Skip this unless log volume is high enough for storage cost to matter, because logging already works at the default 30-day retention. This step is riskier than it appears: _Default is not DNS-only, so shortening its retention shortens retention for every log type routed there — data-access logs, application logs, anything else that project sends to it.
This is the one step with no loop at all. Reading is safe, so the check below reports your whole list at once; the change is deliberate and applies to one project, because a loop would quietly shorten retention estate-wide for logs other people rely on, and log data already aged out does not come back.
for P in $PROJECTS; do
R=$(gcloud logging buckets describe _Default --location=global --project="$P" \
--format="value(retentionDays)" 2>/dev/null)
L=$(gcloud dns policies list --project="$P" --filter="enableLogging=true" \
--format="value(name)" 2>/dev/null | wc -l)
printf "%-32s retention=%-4s logging-policies=%s\n" "$P" "${R:-?}" "$L"
donePick a project from that output only if it shows retention=30, the default, and logging-policies=0, meaning nothing has been logging to _Default yet. Anything else means the bucket is already carrying logs something depends on, and shortening it would cut their retention too.
gcloud logging buckets update _Default --location=global --project=<PROJECT_ID> --retention-days=11. Create the project and service account
Run these in a terminal where you have already run gcloud auth login. Apart from the project name, nothing needs typing in: the values every later command uses are read back from your session.
Use the blocks from this route, not the bash ones: VAR=value, $(…), && and do/done are all syntax errors in PowerShell, and it reports them one line at a time rather than as one clear failure. If a multi-line block misbehaves when pasted at the prompt, save it as setup.ps1 and run .\setup.ps1 instead.
# Project IDs are globally unique, so put something of your own on the end.
gcloud projects create valkan-discovery-1234 --name="Valkan Discovery"
gcloud config set project valkan-discovery-1234
$PROJECT = gcloud config list --format="value(core.project)"
"project=$PROJECT"
gcloud iam service-accounts create valkan --project=$PROJECT --display-name="Valkan Discovery"
$SA = "valkan@$PROJECT.iam.gserviceaccount.com"project= should be the project you just created. It holds the service account and nothing else, so it is not one of the projects Valkan scans.
If your account belongs to an organization, some organizations require a new project to name its parent. Run $ORG = @(gcloud organizations list --format="value(ID)")[0] first and add --organization=$ORG to the create command.
2. Create the read-only role and grant it
Valkan uses its own role definition rather than a broad predefined role. It contains 47 read permissions, every one of them a list or a get, and nothing that can create, change or delete. Save it as gcp-iam-policy-project.json in the folder you are working in:
{
"title": "ValkanDiscovery Read-Only (single project)",
"description": "Read-only Valkan Discovery for one project: identity inventory, IAM grants and deny policies, AI and compute workloads, Model Armor, data services, and audit metadata. No mutate permissions.",
"stage": "GA",
"includedPermissions": [
"resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy",
"iam.serviceAccounts.list",
"iam.serviceAccounts.getIamPolicy",
"iam.serviceAccountKeys.list",
"iam.denypolicies.list",
"iam.denypolicies.get",
"iam.roles.list",
"iam.workloadIdentityPools.list",
"iam.workloadIdentityPoolProviders.list",
"run.services.list",
"cloudfunctions.functions.list",
"aiplatform.reasoningEngines.list",
"aiplatform.endpoints.list",
"discoveryengine.engines.list",
"discoveryengine.dataStores.list",
"dialogflow.agents.list",
"dialogflow.agents.get",
"dialogflow.webhooks.list",
"dialogflow.fulfillments.get",
"logging.logEntries.list",
"logging.privateLogEntries.list",
"monitoring.timeSeries.list",
"compute.networks.list",
"compute.instances.list",
"compute.firewalls.list",
"compute.subnetworks.list",
"dns.policies.list",
"secretmanager.secrets.list",
"cloudsql.instances.list",
"cloudsql.users.list",
"cloudkms.locations.list",
"cloudkms.keyRings.list",
"cloudkms.cryptoKeys.list",
"cloudkms.cryptoKeys.getIamPolicy",
"modelarmor.floorSettings.get",
"modelarmor.templates.list",
"storage.buckets.list",
"bigquery.datasets.get",
"pubsub.topics.list",
"pubsub.topics.getIamPolicy",
"container.clusters.list",
"container.clusters.get",
"container.deployments.list",
"container.statefulSets.list",
"container.daemonSets.list",
"container.cronJobs.list"
]
}A custom role can only be granted inside the project that defines it, so the role is created and granted once in each project.
Choose the projects first
Every command from here on changes the projects you name, and steps 4 and 5 cost money — enabling APIs and turning on DNS logging are billable. So name them explicitly rather than sweeping your whole account. See what you can reach:
gcloud projects list --format="value(projectId)"Then set the list to the projects you want Valkan to cover, and check it before anything changes:
$PROJECTS = @("my-prod-project", "my-staging-project")
$PROJECTS | ForEach-Object { " will configure: $_" }If you do intend every project your account can see — reasonable for a small estate, rarely what you want on an organization-wide login — ask for it deliberately, then read the list it prints before continuing:
$PROJECTS = @(gcloud projects list --format="value(projectId)")Keep this session open: $PROJECTS and $SA are used by steps 2, 4, 5 and 6.
Create and grant
foreach ($P in $PROJECTS) {
gcloud iam roles describe valkanDiscoveryReadOnly --project=$P 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
# Already there from an earlier setup. It must be UPDATED: a role created by
# an older version of this page keeps that version's permissions, and the
# ones added since -- compute.instances.list, for instance -- never arrive.
# The scan then reports permission gaps and lists no virtual machines.
# --quiet: update can prompt before overwriting without an etag.
gcloud iam roles update valkanDiscoveryReadOnly --project=$P --quiet --file=gcp-iam-policy-project.json 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { "FAILED $P (role exists and could not be updated)"; continue }
"updated $P"
} else {
gcloud iam roles create valkanDiscoveryReadOnly --project=$P --file=gcp-iam-policy-project.json 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { "skipped $P (no permission to create a role here)"; continue }
}
gcloud projects add-iam-policy-binding $P --member="serviceAccount:$SA" --role="projects/$P/roles/valkanDiscoveryReadOnly" 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { "granted $P" } else { "FAILED $P (binding refused)" }
}One line per project. granted is done, and updated means the role was already there and has been brought up to date. skipped means your account cannot create a role there — usually because you hold Editor rather than Owner on it, which is common for a project someone else set up. Check with:
gcloud projects get-iam-policy <PROJECT_ID> --flatten="bindings[].members" --filter="bindings.members:<YOUR_EMAIL>" --format="value(bindings.role)"
A skipped project is never scanned. Valkan reports it as a coverage gap if anything in a project it can read points at it; otherwise its absence is silent. Note the skips and either have someone with Owner make the grant, or accept that those projects stay invisible.
The output is suppressed on purpose. Left alone, each project prints its whole role definition and then its entire IAM policy, every member of every binding included, which buries the one line you need.
Creating the role also warns about permissions whose APIs are not enabled yet (step 4). The role is still created with them, and they start working once the API is on.
You still add a single source in Valkan, however many projects it covers: the service account reports back every project it can read.
To undo this later, remove the service account's binding and delete the role in each project. Deleting the project that holds the service account does not remove grants made elsewhere.
3. Create a key
gcloud iam service-accounts keys create valkan-key.json --iam-account=$SAYou paste the contents of valkan-key.json into Valkan in step 7. Delete the local file once it is in.
4. Enable the required APIs
The first API is required: connecting fails without it, because Valkan uses it to find out which projects the service account can read. The rest are needed by the areas they cover, and leaving one off does not stop a scan; that area is reported as a coverage gap instead.
This enables them in every project your account can see, one API at a time. Enabling an API in one project does not enable it in another, and granting access to a project does not enable anything in it.
Enable them individually, not in one call. gcloud services enable given several APIs is all-or-nothing: if any one of them is refused, none are enabled. Compute Engine and Cloud DNS both require the project to have a billing account, so on a project without one a single batched call fails and takes cloudresourcemanager.googleapis.com down with it — the API that Connect cannot work without. The loop below avoids that, and a project reported as partial is usually missing exactly those two.
The project holding the service account needs the Cloud Resource Manager API, because that is the one call Valkan makes without naming a project. Everything else is read from the project that owns the resource, so the APIs refused there for want of billing do not matter.
$APIS = "cloudresourcemanager", "iam", "compute", "logging", "dns", "storage", "secretmanager", "run", "cloudfunctions", "aiplatform", "container", "sqladmin", "cloudkms", "modelarmor", "monitoring", "discoveryengine", "dialogflow", "bigquery", "pubsub"
foreach ($P in $PROJECTS) {
$failed = @()
foreach ($A in $APIS) {
gcloud services enable "$A.googleapis.com" --project=$P 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { $failed += $A }
}
if ($failed.Count -eq 0) {
"all on $P"
} else {
"partial $P (could not enable: " + ($failed -join " ") + ")"
}
}| API | Covers |
|---|---|
cloudresourcemanager.googleapis.com | Required. Which projects are readable, and project IAM |
iam.googleapis.com | Service accounts, their keys and IAM policies — the core inventory |
compute.googleapis.com | Virtual machines, their firewall exposure, networks without DNS logging |
logging.googleapis.com | Audit and activity evidence |
dns.googleapis.com | DNS query evidence |
storage.googleapis.com | Storage buckets |
secretmanager.googleapis.com | Secrets |
run.googleapis.com | Cloud Run services |
cloudfunctions.googleapis.com | Cloud Functions |
aiplatform.googleapis.com | Vertex AI endpoints and reasoning engines |
container.googleapis.com | GKE clusters |
sqladmin.googleapis.com | Cloud SQL instances and their IAM database users |
cloudkms.googleapis.com | KMS keys and who can use them |
modelarmor.googleapis.com | Whether Model Armor covers Vertex AI agents |
monitoring.googleapis.com | Vertex AI model usage counts |
discoveryengine.googleapis.com | Agent Builder apps and their data stores |
dialogflow.googleapis.com | Dialogflow agents and their webhooks |
bigquery.googleapis.com | Datasets and who can read them |
pubsub.googleapis.com | Topics and who can publish to them |
Expect a few seconds per API per project, so give it a minute on a handful of projects. partial means the rest were enabled and only the listed ones were refused; those areas report a coverage gap rather than failing a scan.
Five of the eleven need the project to have a billing account: compute, dns, secretmanager, run and container. On a project without one they are refused and the other six still go on, which is why they are enabled individually. The remedy is a billing account on that project.
The project holding the service account is the exception worth checking. It needs cloudresourcemanager for the one call Valkan makes without naming a project, and iam if you want its own service accounts inventoried; partial ... (could not enable: compute dns secretmanager run container) there is the expected result, not a problem.
5. Enable DNS query logging
Google Cloud leaves this off, and Valkan needs it for two things: the DNS tab, and attributing AI use to the machine making it. A VM resolving api.openai.com is how an agent nobody registered gets discovered, and without query logging there is nothing to read. Valkan reports a network with logging switched off as a finding, so skipping this step does not go unnoticed — it just leaves the gap open.
The policy costs nothing. The logs it produces are ingested by Cloud Logging, which is free up to a monthly allowance per project and charged by volume above it, so a large or chatty fleet can reach a bill. Step 6 is there for that case.
Logging attaches to a VPC network, not to a project, and a project can have several. You need Compute Network Admin or Owner on each project, which the person doing the rest of this setup may not have.
foreach ($P in $PROJECTS) {
foreach ($N in gcloud compute networks list --project=$P --format="value(name)") {
"=== $P / $N"
gcloud dns policies create "valkan-dns-logging-$N" --project=$P --networks=$N --enable-logging --description="Valkan: log VPC DNS resolutions"
}
}A network that already has a policy attached is reported and skipped rather than overwritten, because that policy may be someone else's. Turn logging on for it by editing the existing policy instead. Under a Shared VPC the network lives in the host project, so logging is set there and covers the service projects using it.
6. (Optional) Shorten DNS log retention to reduce storage cost
Skip this unless log volume is high enough for storage cost to matter, because logging already works at the default 30-day retention. This step is riskier than it appears: _Default is not DNS-only, so shortening its retention shortens retention for every log type routed there — data-access logs, application logs, anything else that project sends to it.
This is the one step with no loop at all. Reading is safe, so the check below reports your whole list at once; the change is deliberate and applies to one project, because a loop would quietly shorten retention estate-wide for logs other people rely on, and log data already aged out does not come back.
foreach ($P in $PROJECTS) {
$R = gcloud logging buckets describe _Default --location=global --project=$P --format="value(retentionDays)"
$L = @(gcloud dns policies list --project=$P --filter="enableLogging=true" --format="value(name)").Count
"{0,-32} retention={1,-4} logging-policies={2}" -f $P, $R, $L
}Pick a project from that output only if it shows retention=30, the default, and logging-policies=0, meaning nothing has been logging to _Default yet. Anything else means the bucket is already carrying logs something depends on, and shortening it would cut their retention too.
gcloud logging buckets update _Default --location=global --project=<PROJECT_ID> --retention-days=1(Optional) Record who calls Vertex AI models
Valkan counts Vertex AI model use per model from Cloud Monitoring with no extra setup. To see which service account or person made the calls, Google has to record them, and for Vertex AI it does not by default: those records are Data Access audit logs, which are off unless you turn them on. Until they are, Valkan shows the counts and says caller attribution is unavailable for that project, rather than showing no callers.
In each project you want attributed, open IAM & Admin → Audit Logs, find Vertex AI API, tick Data Read, and click Save. Data Access logs are billed as log storage, so turn them on where knowing the caller is worth it. The role you granted in step 2 already includes logging.privateLogEntries.list, which is what reading them requires.
The same switch on the Dialogflow API and Discovery Engine API lets Valkan see when each Dialogflow agent and Agent Builder app was last used. Without it, an agent is marked dormant only by its age, and Valkan says so.
(Optional) Sweep a whole organization with Cloud Asset Inventory
If you granted the role at the organization (or a folder), Valkan can also ask Cloud Asset Inventory for every project under it. Two things follow: every project the service account cannot read is reported by name — not only the ones it happens to find referenced — and roles granted on a folder count toward what each service account can reach in the projects beneath it.
It is off by default. To use it, enable the Cloud Asset API (cloudasset.googleapis.com) in the project that holds the service account, then ask your Valkan administrator to set the integration's asset inventory scope to organizations/<ORG_ID> or folders/<FOLDER_ID>. The organization role in step 2 already carries the two read-only search permissions this needs; the single-project role does not, because the search is only useful above a project.
(Optional) Read the workloads inside GKE clusters
Valkan lists your GKE clusters by default. To also see the Deployments, StatefulSets, DaemonSets and CronJobs running in them — and which Google service account each one runs as through Workload Identity — ask your Valkan administrator to turn on GKE workload reading for the integration. The role from step 2 already carries the four read-only container.*.list permissions this uses; GKE maps them to Kubernetes read access, so nothing is created inside the cluster.
Valkan reaches each cluster's public control-plane endpoint. A cluster with only a private endpoint is reported as not readable rather than left out silently. Environment variables are recorded by name only, and Kubernetes Secrets are never read.
7. Connect and scan in Valkan
Choose Integrations → Sources → Connect → Google Cloud, paste the contents of the JSON key and pick a region.
Test the connection
Before connecting, click Test connection. It tells you now what a first scan would otherwise report later:
- Missing permissions: read-only permissions the service account lacks, per project when they differ.
- Disabled APIs: APIs that are switched off. Enable them in the project.
- Projects it couldn't check: each with the reason Google gave.
- Could not confirm: APIs whose check got no answer. Test again.
- Optional features that are off: organization-level and opt-in access, such as Cloud Asset Inventory and GKE workloads. These are not needed to connect.
It checks up to 5 projects, and your key is used for this request only: Valkan does not store it until you click Connect. The test is advice. Connect works whatever it says, and anything missing is reported by the first scan as a coverage gap.
Click Connect. The first scan starts automatically, and it reports what it could not reach rather than leaving it out.