Connect with a GCP account
Create a read-only Google Cloud service account for Valkan, give it access to your projects, then connect it and run a scan. Follow the steps in the Cloud Console, in bash, or in PowerShell.
Valkan reads your Google Cloud projects through a service account that you create. The account can only read. This page shows how to create it, give it access, and connect it to Valkan.
In short, you will:
- Create a new project that holds the service account.
- Give the account a read-only role in each project you want scanned.
- Create a key for the account.
- Turn on the APIs Valkan reads from.
- Turn on DNS query logging.
- Paste the key into Valkan.
One connection covers every project the service account can read. To stop scanning a project later, untick it under Projects on the integration's page.
Before you start
You need permission to do three things:
- Create a project, and a service account inside it (Service Account Admin, or Owner).
- Create a key for the service account (Service Account Key Admin, or Owner).
- In each project you want scanned, create a custom role and give it to the account. You need Owner, or both Role Administrator (
roles/iam.roleAdmin) and Project IAM Admin (roles/resourcemanager.projectIamAdmin).
Editor is not enough for the last one. Editor cannot create roles or change who has access. Step 2 fails in any project where Editor is the most you have, even though that project shows up in your project list.
Valkan only reads. Every call it makes to Google Cloud is a list or a get. It never creates, changes or deletes anything. It lists the names of secrets and buckets, never their contents. The role below can only read, so Google would refuse a write even if Valkan tried one. You run the commands on this page yourself. Valkan never runs them.
Pick one way to follow the steps below. All three do the same thing. The two terminal options differ only in shell syntax.
See your projects first
In the console, open IAM & Admin → Manage resources (console.cloud.google.com/cloud-resource-manager). It lists the projects your account can see.
These are the projects Valkan will scan. The service account does not live in any of them. You create a separate project for it in step 1.
By default, the steps below cover all of these projects. Steps 2, 4 and 5 show how to pick only some. Check the list before you go on, because turning on APIs and DNS logging in a project can add cost there.
Adding a project later, or projects Valkan cannot read
Access is given one project at a time. If you add a project later, run step 2 again for it.
Valkan also tells you about a project it cannot read when a project it can read points to it. One example is the host project of a shared VPC network. This finds only some unread projects, so do not wait for it. Run step 2 yourself whenever you add a project.
Projects that are waiting to be deleted do not appear in the list.
To show organization and folder names above your projects, Valkan needs Organization Viewer and Folder Viewer at the organization. They are optional. Projects are scanned without them.
1. Create the project and service account
Goal: a new project that holds the service account.
- Go to
console.cloud.google.com/projectcreateand create a project namedvalkan-discovery. Google may add digits to make the ID unique. Write down the final ID. The project does not need a billing account. - Open IAM & Admin → Service Accounts (
console.cloud.google.com/iam-admin/serviceaccounts). Make sure the new project is selected in the project picker at the top left. - Click Create service account. Name it
valkan. Click Create and continue. - When Google asks you to give the account roles, click Done. You give it a role in step 2, in your other projects.
- Copy the email address of the account from the list. It looks like
valkan@<project-id>.iam.gserviceaccount.com. You need it in step 2.
2. Create the read-only role and give it to the service account
Goal: in each project, a role that can only read, given to the service account.
The role contains 47 read permissions. Each one only lists or gets something. None can create, change or delete. Google's closest ready-made role, Security Reviewer, has far more permissions than Valkan needs, so Valkan uses its own.
{
"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 works only in the project where you create it. So you create the role and grant it once in each project.
Which projects?
Do this step in every project in your list. To use only some, open the section below.
Only some projects? Choose them
Write down the projects you want Valkan to cover. Do steps 2, 4 and 5 in those projects only, and skip the rest.
Create the role and grant it
- Select the first project in the project picker at the top left.
- Open IAM & Admin → Roles (
console.cloud.google.com/iam-admin/roles) and click Create role. - Set Title to
Valkan Discovery Read Only, ID tovalkanDiscoveryReadOnly, and Role launch stage toGeneral Availability. - Click Add permissions. The dialog filters a long list. Paste one permission name at a time into the filter box, tick the result, and repeat for all 47 permissions. If a permission shows a "not supported" warning, add it anyway. The role keeps it, and it starts working when you turn on its API in step 4.
- Click Create.
- Open IAM & Admin → IAM (
console.cloud.google.com/iam-admin/iam) and click Grant access. - Paste the service account's email address into New principals. Choose Valkan Discovery Read Only from the Custom group, and click Save.
Then repeat these seven steps in each of your other projects.
If valkanDiscoveryReadOnly already exists in a project, edit it instead of creating a second one, and check that all 47 permissions are there. A role made from an older version of this page keeps its old permissions. The scan then reports permission gaps and lists no virtual machines.
You still add one source in Valkan, however many projects it covers. The service account reports back every project it can read.
How to undo this later
In each project, remove the service account's access and delete the role. Deleting the project that holds the service account does not remove access you gave in other projects.
3. Create a key
Goal: a JSON key file for the service account.
Open the service account and go to its Keys tab. Click Add key → Create new key → JSON → Create. A file downloads. You paste its contents into Valkan in step 7. Google gives you this file only once.
The key works like a password. Do not commit it to a repository or share it. Delete the file after you paste it into Valkan.
4. Turn on the APIs
Goal: turn on the APIs Valkan reads from, in each project in your list.
Open APIs & Services → Library (console.cloud.google.com/apis/library). Search for each API by name and click Enable.
Required. Connecting fails without this one:
- 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 for the areas they cover. If you leave one off, the scan still runs and reports that area as a coverage gap. You can turn it on later. APIs marked "needs billing" work only in projects that have a billing account.
- Identity and Access Management API (
iam.googleapis.com). Service accounts, their keys and their access policies. This is the core of what Valkan finds on Google Cloud. Without it, a scan finds almost nothing. - Compute Engine API (
compute.googleapis.com, needs billing). Virtual machines, their firewall exposure, and networks without DNS logging. - Cloud Logging API (
logging.googleapis.com). Audit and activity evidence. - Cloud DNS API (
dns.googleapis.com, needs billing). DNS query evidence. - Cloud Storage API (
storage.googleapis.com). Storage buckets. - Secret Manager API (
secretmanager.googleapis.com, needs billing). Secrets. - Cloud Run Admin API (
run.googleapis.com, needs billing). 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, needs billing). GKE clusters. - Cloud SQL Admin API (
sqladmin.googleapis.com). Cloud SQL instances and their database users. - Cloud Key Management Service (KMS) API (
cloudkms.googleapis.com). Encryption keys and who can use them. - Model Armor API (
modelarmor.googleapis.com). Whether Model Armor protects your Vertex AI agents. - Cloud Monitoring API (
monitoring.googleapis.com). Vertex AI model usage counts. - Discovery Engine API (
discoveryengine.googleapis.com, needs billing). 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.
Turn them on in each project in your list. Switch projects in the project picker as you go. Giving access to a project does not turn on its APIs.
A project without a billing account cannot use the six APIs marked "needs billing". Skip them there. Such a project almost certainly has no resources of those kinds.
The project that holds the service account needs only the Cloud Resource Manager API. Valkan calls it without naming a project. Everything else is read from the project that owns the resource.
If you have more than a handful of projects, the two terminal options do this for all of them in one command.
5. Turn on DNS query logging
Goal: log DNS lookups on each VPC (virtual private cloud) network.
Google Cloud leaves DNS logging off. Valkan needs it for the DNS tab, and to see which machine calls an AI service. A VM that looks up api.openai.com is how Valkan finds an agent nobody registered. Without logging there is nothing to read. Valkan reports each network that has logging off as a finding.
The DNS policy itself is free. The logs go to Cloud Logging, which is free up to a monthly limit per project and charged by volume above it. Step 6 shows how to reduce that cost.
Logging belongs to a network, not a project, and a project can have several networks. You need Compute Network Admin or Owner in each project.
- Open Network Services → Cloud DNS → DNS server policies and click Create policy.
- Set Logs to On.
- Under Networks, add the network.
- Repeat for each network in each project in your list. Skip a project that has no networks.
A network can have only one DNS policy. If the network already has one, open that policy and set Logs to On instead of creating a new one. The existing policy may belong to someone else, so check with them first.
With a shared VPC, the network lives in the host project. Turn logging on there. It covers the service projects that use the network.
6. (Optional) Shorten DNS log retention to reduce storage cost
Skip this unless log storage costs matter to you. Logging already works with the default 30-day retention.
Be careful. The _Default log bucket holds more than DNS logs. Shortening its retention shortens retention for every log type sent to it, such as data-access logs and application logs.
To find out what is in a bucket, look at Logging → Logs storage for the retention, and Network Services → Cloud DNS → DNS server policies for what logs into it. Change a project only if its _Default bucket has 30-day retention and no DNS policy is logging to it. Otherwise something else may depend on those logs.
To change one project, open Logging → Logs storage, click _Default, choose Edit bucket, and set the retention to 1 day.
See your projects first
Run this to list the projects your account can see:
# List the projects your account can see.
gcloud projects list --format="table(projectId,name,parent.type,parent.id)"These are the projects Valkan will scan. The service account does not live in any of them. You create a separate project for it in step 1.
By default, the steps below cover all of these projects. Steps 2, 4 and 5 show how to pick only some. Check the list before you go on, because turning on APIs and DNS logging in a project can add cost there.
Adding a project later, or projects Valkan cannot read
Access is given one project at a time. If you add a project later, run step 2 again for it.
Valkan also tells you about a project it cannot read when a project it can read points to it. One example is the host project of a shared VPC network. This finds only some unread projects, so do not wait for it. Run step 2 yourself whenever you add a project.
Projects that are waiting to be deleted do not appear in the list. In the console, the same list is under IAM & Admin → Manage resources (console.cloud.google.com/cloud-resource-manager).
To show organization and folder names above your projects, Valkan needs Organization Viewer and Folder Viewer at the organization. They are optional. Projects are scanned without them.
1. Create the project and service account
Goal: a new project that holds the service account.
Run these commands in Cloud Shell, or in a terminal where you have run gcloud auth login. You only type the project name. Every later command reads its values back from your session.
# Create the project and make it the default for this 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
# Read the project ID back, so later commands never need it typed again.
PROJECT=$(gcloud config list --format="value(core.project)")
echo "project=$PROJECT"
# Create the service account Valkan signs in as.
gcloud iam service-accounts create valkan \
--project="$PROJECT" --display-name="Valkan Discovery"
# Keep its email for the steps that follow.
SA="valkan@$PROJECT.iam.gserviceaccount.com"You should see project= followed by the project you just created. If Google says the ID is already in use, choose another and change it in both lines. This project holds the service account and nothing else.
Some organizations require a new project to name its parent. If that applies to you, add --organization=$(gcloud organizations list --format="value(ID)" | head -1) to the create command.
2. Create the read-only role and give it to the service account
Goal: in each project, a role that can only read, given to the service account.
The role contains 47 read permissions. Each one only lists or gets something. None 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 works only in the project where you create it. So the commands below create the role and grant it once in each project.
Which projects?
By default, this covers every project your account can see. Run this to set the list and print it:
# Scan every project your account can see.
PROJECTS=$(gcloud projects list --format="value(projectId)")
# Print the list to check it.
for P in $PROJECTS; do echo " will configure: $P"; doneThe list includes the project you made in step 1. That is fine. It is empty, so a scan finds nothing in it.
Only some projects? Choose them
Replace the list with the projects you want. Steps 4, 5 and 6 use the same list.
# Scan only the projects you name here.
PROJECTS="my-prod-project my-staging-project"
# Print the list to check it.
for P in $PROJECTS; do echo " will configure: $P"; doneKeep this shell open. Steps 2, 4, 5 and 6 use $PROJECTS and $SA.
Create the role and grant it
# In each project, create the role (or update it if it exists), then grant it
# to the service account. Prints one line per project.
for P in $PROJECTS; do
if gcloud iam roles describe valkanDiscoveryReadOnly --project="$P" >/dev/null 2>&1; then
# The role exists from an earlier setup. Update it so it gets any permissions added
# since. --quiet stops gcloud asking before it overwrites.
if ! ERR=$(gcloud iam roles update valkanDiscoveryReadOnly --project="$P" --quiet \
--file=gcp-iam-policy-project.json 2>&1 >/dev/null); then
echo "FAILED $P ($(echo "$ERR" | grep ERROR | head -1))"
continue
fi
echo "updated $P"
elif ! ERR=$(gcloud iam roles create valkanDiscoveryReadOnly --project="$P" \
--file=gcp-iam-policy-project.json 2>&1 >/dev/null); then
echo "skipped $P ($(echo "$ERR" | grep ERROR | head -1))"
continue
fi
if ERR=$(gcloud projects add-iam-policy-binding "$P" \
--member="serviceAccount:$SA" \
--role="projects/$P/roles/valkanDiscoveryReadOnly" 2>&1 >/dev/null); then
echo "granted $P"
else
echo "FAILED $P ($(echo "$ERR" | grep ERROR | head -1))"
fi
doneEach project prints one line:
granted: done.updated: the role already existed. It was brought up to date, then given.skippedorFAILED: something went wrong. The reason Google gave is in brackets. The usual cause is that you are Editor, not Owner, in that project. To check your role there, run the command below.
# Show your roles in one project. Replace <PROJECT_ID> and <YOUR_EMAIL>.
gcloud projects get-iam-policy <PROJECT_ID> --flatten="bindings[].members" --filter="bindings.members:<YOUR_EMAIL>" --format="value(bindings.role)"A skipped project is not scanned. Ask someone with Owner to run step 2 there, or leave that project out.
The loop hides gcloud's normal output on purpose. Without that, each project prints its whole role and its whole access policy, which buries the one line you need.
Creating the role may warn about permissions whose APIs are not on yet. The role is still created, and those permissions start working when you turn on the API in step 4.
You still add one source in Valkan, however many projects it covers. The service account reports back every project it can read.
How to undo this later
In each project, remove the service account's access and delete the role. Deleting the project that holds the service account does not remove access you gave in other projects.
3. Create a key
Goal: a JSON key file for the service account.
# Create a key for the service account and save it as valkan-key.json.
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. The key works like a password. Do not commit it to a repository or share it. Delete the file after you paste it into Valkan.
4. Turn on the APIs
Goal: turn on the APIs Valkan reads from, in each project in your list.
The first API is required. Connecting fails without it, because Valkan uses it to find out which projects the service account can read. The others cover areas of Google Cloud. If you leave one off, the scan still runs and reports that area as a coverage gap.
The commands turn on every API in every project in $PROJECTS, one API at a time. Turning on an API in one project does not turn it on in another. Giving access to a project does not turn on anything in it.
Turn them on one at a time, not in one call. If you give gcloud services enable several APIs and Google refuses one, none are turned on. Six of the APIs need a billing account on the project. In a project without one, a single call would fail and take cloudresourcemanager.googleapis.com with it. That is the API Connect cannot work without.
# The APIs Valkan reads from, and the ones that need a billing account.
APIS="cloudresourcemanager iam compute logging dns storage secretmanager run cloudfunctions aiplatform container sqladmin cloudkms modelarmor monitoring discoveryengine dialogflow bigquery pubsub"
BILLED="compute dns secretmanager run container discoveryengine"
# Turn each API on in each project, one at a time. Prints one line per project.
for P in $PROJECTS; do
NEEDS_BILLING=""
OTHER=""
for A in $APIS; do
if ! gcloud services enable "$A.googleapis.com" --project="$P" >/dev/null 2>&1; then
case " $BILLED " in
*" $A "*) NEEDS_BILLING="$NEEDS_BILLING $A" ;;
*) OTHER="$OTHER $A" ;;
esac
fi
done
if [ -z "$NEEDS_BILLING$OTHER" ]; then
echo "all on $P"
elif [ -z "$OTHER" ]; then
echo "no billing $P (left off:$NEEDS_BILLING)"
else
echo "partial $P (failed:$OTHER; needs billing:$NEEDS_BILLING)"
fi
doneIt takes a few seconds per API per project. Each project prints one line:
all on: every API is on.no billing: only APIs that need billing were refused. The project has no billing account, so it almost certainly has no resources of those kinds. You can leave it as it is, or add a billing account to that project if you do want them scanned.partial: an API that does not need billing was refused. Read the list in brackets. Valkan reports each missing area as a coverage gap. It does not fail the scan.
| API | Covers |
|---|---|
cloudresourcemanager.googleapis.com | Required. Which projects are readable, and project access policies |
iam.googleapis.com | Service accounts, their keys and access 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 database users |
cloudkms.googleapis.com | Encryption keys and who can use them |
modelarmor.googleapis.com | Whether Model Armor protects 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 |
The project that holds the service account is a special case. It needs cloudresourcemanager for the one call Valkan makes without naming a project, and iam if you want its own service accounts listed. A no billing line for that project is expected. It is not a problem.
5. Turn on DNS query logging
Goal: log DNS lookups on each VPC (virtual private cloud) network.
Google Cloud leaves DNS logging off. Valkan needs it for the DNS tab, and to see which machine calls an AI service. A VM that looks up api.openai.com is how Valkan finds an agent nobody registered. Without logging there is nothing to read. Valkan reports each network that has logging off as a finding.
The DNS policy itself is free. The logs go to Cloud Logging, which is free up to a monthly limit per project and charged by volume above it. Step 6 shows how to reduce that cost.
Logging belongs to a network, not a project, and a project can have several networks. You need Compute Network Admin or Owner in each project.
# In each project, turn on DNS logging for every network. Prints one line per network.
for P in $PROJECTS; do
# Without the Compute Engine API, a project has no networks. Skip it.
if ! gcloud services list --enabled --project="$P" \
--filter="config.name=compute.googleapis.com" --format="value(config.name)" 2>/dev/null | grep -q compute; then
echo "skipped $P (Compute Engine API is off, so there are no networks)"
continue
fi
for N in $(gcloud compute networks list --project="$P" --format="value(name)" 2>/dev/null); do
if gcloud dns policies create "valkan-dns-logging-$N" --project="$P" \
--networks="$N" --enable-logging \
--description="Valkan: log VPC DNS resolutions" >/dev/null 2>&1; then
echo "logging on $P / $N"
continue
fi
# A network can have only one DNS policy. If it already has one, turn logging on in that policy.
EXISTING=$(gcloud dns policies list --project="$P" \
--filter="networks.networkUrl~/networks/$N\$" --format="value(name)" 2>/dev/null | head -1)
if [ -n "$EXISTING" ] && gcloud dns policies update "$EXISTING" --project="$P" \
--enable-logging >/dev/null 2>&1; then
echo "logging on $P / $N (in your existing policy $EXISTING)"
else
echo "FAILED $P / $N"
fi
done
doneEach network prints one line. A network that already had a DNS policy gets logging turned on in that policy. The commands change nothing else about it. That policy may belong to someone else, so tell them. A FAILED line usually means you lack Compute Network Admin there.
With a shared VPC, the network lives in the host project. Turn logging on there. It covers the service projects that use the network.
6. (Optional) Shorten DNS log retention to reduce storage cost
Skip this unless log storage costs matter to you. Logging already works with the default 30-day retention.
Be careful. The _Default log bucket holds more than DNS logs. Shortening its retention shortens retention for every log type sent to it, such as data-access logs and application logs.
This is the one step with no loop that changes things. The check below only reads, so it covers your whole list. The change applies to one project at a time. A loop could cut retention across all your projects for logs other people rely on, and logs that have aged out do not come back.
# Only reads: each project's log retention, and how many DNS policies log.
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"
doneChange a project only if it shows retention=30 and logging-policies=0. That means no DNS policy is logging to _Default yet. Anything else means the bucket already holds logs something depends on.
# Shorten _Default log retention to 1 day in ONE project. Replace <PROJECT_ID>.
gcloud logging buckets update _Default --location=global --project=<PROJECT_ID> --retention-days=1See your projects first
Run this to list the projects your account can see:
# List the projects your account can see.
gcloud projects list --format="table(projectId,name,parent.type,parent.id)"These are the projects Valkan will scan. The service account does not live in any of them. You create a separate project for it in step 1.
By default, the steps below cover all of these projects. Steps 2, 4 and 5 show how to pick only some. Check the list before you go on, because turning on APIs and DNS logging in a project can add cost there.
Adding a project later, or projects Valkan cannot read
Access is given one project at a time. If you add a project later, run step 2 again for it.
Valkan also tells you about a project it cannot read when a project it can read points to it. One example is the host project of a shared VPC network. This finds only some unread projects, so do not wait for it. Run step 2 yourself whenever you add a project.
Projects that are waiting to be deleted do not appear in the list. In the console, the same list is under IAM & Admin → Manage resources (console.cloud.google.com/cloud-resource-manager).
To show organization and folder names above your projects, Valkan needs Organization Viewer and Folder Viewer at the organization. They are optional. Projects are scanned without them.
1. Create the project and service account
Goal: a new project that holds the service account.
Run these commands in a terminal where you have run gcloud auth login. You only type the project name. Every later command reads its values back from your session.
Use the commands on this tab, not the bash ones. In PowerShell, VAR=value, $(…), && and do/done are syntax errors. PowerShell reports them one line at a time, not as one clear failure. If a multi-line block misbehaves when you paste it, save it as setup.ps1 and run .\setup.ps1 instead.
# Create the project and make it the default for this 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
# Read the project ID back, so later commands never need it typed again.
$PROJECT = gcloud config list --format="value(core.project)"
"project=$PROJECT"
# Create the service account Valkan signs in as.
gcloud iam service-accounts create valkan --project=$PROJECT --display-name="Valkan Discovery"
# Keep its email for the steps that follow.
$SA = "valkan@$PROJECT.iam.gserviceaccount.com"You should see project= followed by the project you just created. If Google says the ID is already in use, choose another and change it in both lines. This project holds the service account and nothing else.
Some organizations require a new project to name its parent. If that applies to you, 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 give it to the service account
Goal: in each project, a role that can only read, given to the service account.
The role contains 47 read permissions. Each one only lists or gets something. None 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 works only in the project where you create it. So the commands below create the role and grant it once in each project.
Which projects?
By default, this covers every project your account can see. Run this to set the list and print it:
# Scan every project your account can see.
$PROJECTS = @(gcloud projects list --format="value(projectId)")
# Print the list to check it.
$PROJECTS | ForEach-Object { " will configure: $_" }The list includes the project you made in step 1. That is fine. It is empty, so a scan finds nothing in it.
Only some projects? Choose them
Replace the list with the projects you want. Steps 4, 5 and 6 use the same list.
# Scan only the projects you name here.
$PROJECTS = @("my-prod-project", "my-staging-project")
# Print the list to check it.
$PROJECTS | ForEach-Object { " will configure: $_" }Keep this session open. Steps 2, 4, 5 and 6 use $PROJECTS and $SA.
Create the role and grant it
# The first line of gcloud's error, so a failure says why.
function Why($text) { $text -split "`n" | Where-Object { $_ -match "ERROR" } | Select-Object -First 1 }
# In each project, create the role (or update it if it exists), then grant it
# to the service account. Prints one line per project.
foreach ($P in $PROJECTS) {
gcloud iam roles describe valkanDiscoveryReadOnly --project=$P 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
# The role exists from an earlier setup. Update it so it gets any permissions added
# since. --quiet stops gcloud asking before it overwrites.
$out = gcloud iam roles update valkanDiscoveryReadOnly --project=$P --quiet --file=gcp-iam-policy-project.json 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { "FAILED $P ($(Why $out))"; continue }
"updated $P"
} else {
$out = gcloud iam roles create valkanDiscoveryReadOnly --project=$P --file=gcp-iam-policy-project.json 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { "skipped $P ($(Why $out))"; continue }
}
$out = gcloud projects add-iam-policy-binding $P --member="serviceAccount:$SA" --role="projects/$P/roles/valkanDiscoveryReadOnly" 2>&1 | Out-String
if ($LASTEXITCODE -eq 0) { "granted $P" } else { "FAILED $P ($(Why $out))" }
}Each project prints one line:
granted: done.updated: the role already existed. It was brought up to date, then given.skippedorFAILED: something went wrong. The reason Google gave is in brackets. The usual cause is that you are Editor, not Owner, in that project. To check your role there, run the command below.
# Show your roles in one project. Replace <PROJECT_ID> and <YOUR_EMAIL>.
gcloud projects get-iam-policy <PROJECT_ID> --flatten="bindings[].members" --filter="bindings.members:<YOUR_EMAIL>" --format="value(bindings.role)"A skipped project is not scanned. Ask someone with Owner to run step 2 there, or leave that project out.
The loop hides gcloud's normal output on purpose. Without that, each project prints its whole role and its whole access policy, which buries the one line you need.
Creating the role may warn about permissions whose APIs are not on yet. The role is still created, and those permissions start working when you turn on the API in step 4.
You still add one source in Valkan, however many projects it covers. The service account reports back every project it can read.
How to undo this later
In each project, remove the service account's access and delete the role. Deleting the project that holds the service account does not remove access you gave in other projects.
3. Create a key
Goal: a JSON key file for the service account.
# Create a key for the service account and save it as valkan-key.json.
gcloud iam service-accounts keys create valkan-key.json --iam-account=$SAYou paste the contents of valkan-key.json into Valkan in step 7. The key works like a password. Do not commit it to a repository or share it. Delete the file after you paste it into Valkan.
4. Turn on the APIs
Goal: turn on the APIs Valkan reads from, in each project in your list.
The first API is required. Connecting fails without it, because Valkan uses it to find out which projects the service account can read. The others cover areas of Google Cloud. If you leave one off, the scan still runs and reports that area as a coverage gap.
The commands turn on every API in every project in $PROJECTS, one API at a time. Turning on an API in one project does not turn it on in another. Giving access to a project does not turn on anything in it.
Turn them on one at a time, not in one call. If you give gcloud services enable several APIs and Google refuses one, none are turned on. Six of the APIs need a billing account on the project. In a project without one, a single call would fail and take cloudresourcemanager.googleapis.com with it. That is the API Connect cannot work without.
# The APIs Valkan reads from, and the ones that need a billing account.
$APIS = "cloudresourcemanager", "iam", "compute", "logging", "dns", "storage", "secretmanager", "run", "cloudfunctions", "aiplatform", "container", "sqladmin", "cloudkms", "modelarmor", "monitoring", "discoveryengine", "dialogflow", "bigquery", "pubsub"
$BILLED = "compute", "dns", "secretmanager", "run", "container", "discoveryengine"
# Turn each API on in each project, one at a time. Prints one line per project.
foreach ($P in $PROJECTS) {
$needBilling = @()
$other = @()
foreach ($A in $APIS) {
gcloud services enable "$A.googleapis.com" --project=$P 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
if ($BILLED -contains $A) { $needBilling += $A } else { $other += $A }
}
}
if (-not $needBilling -and -not $other) {
"all on $P"
} elseif (-not $other) {
"no billing $P (left off: $($needBilling -join ' '))"
} else {
"partial $P (failed: $($other -join ' '); needs billing: $($needBilling -join ' '))"
}
}It takes a few seconds per API per project. Each project prints one line:
all on: every API is on.no billing: only APIs that need billing were refused. The project has no billing account, so it almost certainly has no resources of those kinds. You can leave it as it is, or add a billing account to that project if you do want them scanned.partial: an API that does not need billing was refused. Read the list in brackets. Valkan reports each missing area as a coverage gap. It does not fail the scan.
| API | Covers |
|---|---|
cloudresourcemanager.googleapis.com | Required. Which projects are readable, and project access policies |
iam.googleapis.com | Service accounts, their keys and access 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 database users |
cloudkms.googleapis.com | Encryption keys and who can use them |
modelarmor.googleapis.com | Whether Model Armor protects 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 |
The project that holds the service account is a special case. It needs cloudresourcemanager for the one call Valkan makes without naming a project, and iam if you want its own service accounts listed. A no billing line for that project is expected. It is not a problem.
5. Turn on DNS query logging
Goal: log DNS lookups on each VPC (virtual private cloud) network.
Google Cloud leaves DNS logging off. Valkan needs it for the DNS tab, and to see which machine calls an AI service. A VM that looks up api.openai.com is how Valkan finds an agent nobody registered. Without logging there is nothing to read. Valkan reports each network that has logging off as a finding.
The DNS policy itself is free. The logs go to Cloud Logging, which is free up to a monthly limit per project and charged by volume above it. Step 6 shows how to reduce that cost.
Logging belongs to a network, not a project, and a project can have several networks. You need Compute Network Admin or Owner in each project.
# In each project, turn on DNS logging for every network. Prints one line per network.
foreach ($P in $PROJECTS) {
# Without the Compute Engine API, a project has no networks. Skip it.
$compute = gcloud services list --enabled --project=$P --filter="config.name=compute.googleapis.com" --format="value(config.name)" 2>$null
if (-not $compute) { "skipped $P (Compute Engine API is off, so there are no networks)"; continue }
foreach ($N in gcloud compute networks list --project=$P --format="value(name)" 2>$null) {
gcloud dns policies create "valkan-dns-logging-$N" --project=$P --networks=$N --enable-logging --description="Valkan: log VPC DNS resolutions" 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { "logging on $P / $N"; continue }
# A network can have only one DNS policy. If it already has one, turn logging on in that policy.
$existing = gcloud dns policies list --project=$P --filter="networks.networkUrl~/networks/$N`$" --format="value(name)" 2>$null | Select-Object -First 1
if ($existing) {
gcloud dns policies update $existing --project=$P --enable-logging 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { "logging on $P / $N (in your existing policy $existing)"; continue }
}
"FAILED $P / $N"
}
}Each network prints one line. A network that already had a DNS policy gets logging turned on in that policy. The commands change nothing else about it. That policy may belong to someone else, so tell them. A FAILED line usually means you lack Compute Network Admin there.
With a shared VPC, the network lives in the host project. Turn logging on there. It covers the service projects that use the network.
6. (Optional) Shorten DNS log retention to reduce storage cost
Skip this unless log storage costs matter to you. Logging already works with the default 30-day retention.
Be careful. The _Default log bucket holds more than DNS logs. Shortening its retention shortens retention for every log type sent to it, such as data-access logs and application logs.
This is the one step with no loop that changes things. The check below only reads, so it covers your whole list. The change applies to one project at a time. A loop could cut retention across all your projects for logs other people rely on, and logs that have aged out do not come back.
# Only reads: each project's log retention, and how many DNS policies log.
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
}Change a project only if it shows retention=30 and logging-policies=0. That means no DNS policy is logging to _Default yet. Anything else means the bucket already holds logs something depends on.
# Shorten _Default log retention to 1 day in ONE project. Replace <PROJECT_ID>.
gcloud logging buckets update _Default --location=global --project=<PROJECT_ID> --retention-days=17. 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 you connect, click Test connection. It tells you now what a first scan would report later:
- Missing permissions: read-only permissions the service account lacks, per project when they differ.
- Disabled APIs: APIs that are off. Turn them on in the project.
- Projects it couldn't check: each with the reason Google gave.
- Could not confirm: APIs that gave no answer. Test again.
- Optional features that are off: organization-level and opt-in access, such as Cloud Asset Inventory and GKE workloads. You do not need them to connect.
It checks up to 5 projects. Valkan uses your key for this request only and does not store it until you click Connect. The test is advice. Connect works whatever the test says, and the first scan reports anything missing as a coverage gap.
Click Connect. The first scan starts on its own. It reports what it could not reach instead of leaving it out.
(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. For Vertex AI it does not do that by default. The records are Data Access audit logs, which are off until you turn them on. Until then, Valkan shows the counts and says that caller details are unavailable for that project.
In each project you want this for, open IAM & Admin → Audit Logs, find Vertex AI API, tick Data Read, and click Save. Google bills Data Access logs as log storage, so turn them on only where knowing the caller is worth it. The role from step 2 already includes logging.privateLogEntries.list, which 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, Valkan marks an agent as dormant by its age alone, and says so.
(Optional) Scan a whole organization with Cloud Asset Inventory
If you gave the role at the organization (or a folder), Valkan can ask Cloud Asset Inventory for every project under it. Two things follow. Valkan names every project the service account cannot read, not only the ones it finds referenced. And roles given on a folder count toward what each service account can reach in the projects beneath it.
This is off by default. To use it:
- Turn on the Cloud Asset API (
cloudasset.googleapis.com) in the project that holds the service account. - Ask your Valkan administrator to set the integration's asset inventory scope to
organizations/<ORG_ID>orfolders/<FOLDER_ID>.
The organization role in step 2 already has 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. It can also read the Deployments, StatefulSets, DaemonSets and CronJobs inside them, and which Google service account each one runs as through Workload Identity. To turn this on, ask your Valkan administrator to enable GKE workload reading for the integration. The role from step 2 already has 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, not left out silently. Valkan records environment variables by name only, and never reads Kubernetes Secrets.