Skip to main content10ETLabsRequest demo

Setup guide

Cloud connector setup guide

How to connect AWS, Azure, Google Cloud, Kubernetes and other VPS providers to 10ETLabs: what you create in your account, which permissions it grants and why, what we collect, what we never touch, and how to fix every setup error the console can show.

Every connection is read-only. 10ETLabs never creates, changes or deletes anything in your accounts through a connection, and never reads data (objects, rows, secrets, prompts, model weights).

Read-only by default. Fixes are code you review, or — only if you deploy a separate remediator role — one-click changes you approve. See remediation.md.

How a connection works

  1. Set up access. You create a read-only identity in your account (an IAM role, an app registration, a service account, a Kubernetes service account or a vendor API token). The console's wizard offers one or more setup methods per provider, with the recommended one first.
  2. Connect. You paste the values into the wizard. The format of each value is checked as you type. Test connection signs in and runs one read per service the scan needs, and shows a checklist:
    • ✓ passed
    • ⚠ warning: the scan can still run, but that service is skipped and shown as a coverage gap until you fix it
    • ✗ failed: a scan cannot run until this is fixed
    • – skipped: not applicable (for example a service with no endpoint in your home region)
  3. Verify and scan. Saving stores the credentials encrypted (AES-256-GCM, never returned to the browser, never logged). Verify access runs the same checklist against the saved credentials, then Start first scan collects inventory and findings.

Coverage gaps. When one read fails during a scan (a permission removed, throttling, a disabled API), the scan records a *collection finding* and a *coverage gap* for that scope instead of failing. Assets and findings inside a gap are kept exactly as they were, so a missing permission never looks like everything was deleted or fixed.

Progress is saved. If you leave the wizard, it reopens at the same step. Secrets are never saved in the browser, so paste them again after a resume.

AWS

What you create

One IAM role, 10ETLabsReader, in each account you want scanned. It trusts only the 10ETLabs platform account (arn:aws:iam::<platform account ID>:root) and only when the caller presents your connection's External ID (sts:ExternalId condition), which protects against the confused-deputy problem. The wizard shows both values.

Setup methods (all create the same role and the same permissions):

MethodTimeWhat you do
One-click CloudFormation (recommended)~3 minOpen the pre-filled quick-create link, tick the IAM acknowledgement, create the stack, copy the RoleArn output.
Terraform~5 minDownload 10etlabs-aws-reader-role.tf, run terraform apply -var external_id=… -var platform_account_id=…, copy terraform output -raw role_arn.
AWS CLI~4 minDownload the CloudFormation template and run aws cloudformation deploy … --capabilities CAPABILITY_NAMED_IAM --parameter-overrides ExternalId=… PlatformAccountId=… (works in AWS CloudShell).

When the quick-create link is not offered, the deployment does not publish its templates in S3; download the template and create the stack from the CloudFormation console instead (Create stack → Upload a template file, stack name 10ETLabsReader, then paste the two parameters).

Permissions and why

  • SecurityAudit (AWS managed, read-only). Used by the optional Prowler checks.
  • TenetLabsCollectorRead (inline): every API call the 10ETLabs collectors make, listed explicitly so a scan never depends on what AWS adds to or removes from SecurityAudit. A test in the codebase (packages/cloud-aws/src/iam-actions.spec.ts) fails if a collector calls an API the templates do not grant.
AreaActionsUsed for
Regionsec2:DescribeRegionsWhich regions to scan (opt-in regions you have not enabled are skipped). Required: a scan stops without it.
Compute and networkec2:DescribeInstances, ec2:DescribeSecurityGroups, ec2:DescribeVolumesInstances and public IPs, world-open security groups, unencrypted EBS volumes
Identityiam:GetAccountSummary, iam:ListUsers, iam:ListMFADevices, iam:ListAttachedUserPolicies, iam:ListRoles, iam:ListAttachedRolePolicies, iam:ListRolePolicies, iam:GetRolePolicyRoot MFA and access keys, users without MFA, admin users, roles used by AI workloads and public functions
Storages3:ListAllMyBuckets, s3:GetAccountPublicAccessBlock, s3:GetBucketPublicAccessBlock, s3:GetBucketPolicyStatus, s3:GetBucketAcl, s3:GetEncryptionConfiguration, s3:GetBucketLocationBucket publicity and encryption. No object is listed or read.
Databases, containers, serverlessrds:DescribeDBInstances, eks:ListClusters, eks:DescribeCluster, ecr:DescribeRepositories, lambda:ListFunctions, lambda:GetFunctionUrlConfig, apigateway:GET (on /restapis only, never /apikeys)Public databases, EKS endpoint exposure, ECR scan-on-push, public Lambda URLs, public REST APIs
Loggingcloudtrail:DescribeTrails, cloudtrail:GetTrailStatus, cloudtrail:GetEventSelectorsWhether a multi-region trail logs management events
AIsagemaker:List/Describe… (notebooks, endpoints, endpoint configs, models), bedrock:ListCustomModels, GetCustomModel, ListGuardrails, ListProvisionedModelThroughputs, GetModelInvocationLoggingConfiguration, ListAgents, GetAgent, ListAgentActionGroups, GetAgentActionGroup, ListAgentKnowledgeBases, ListKnowledgeBases, GetKnowledgeBase, ListDataSources, GetDataSourceAI inventory and AI posture (public endpoints, admin roles, training data or knowledge-base sources in public buckets, invocation logging, guardrails). Agents for Bedrock authorizes under the bedrock: prefix.
Organizationsorganizations:ListAccountsMember accounts, when you connect the management account
Prowler extrasaccess-analyzer:List*Optional Prowler checks

Never accessed: S3 objects, database rows, EBS snapshots, secret values (secretsmanager:GetSecretValue), KMS decrypt, API key values, model invocation, prompts or outputs.

What is collected

Account and enabled regions; IAM users, roles and their attached/inline policy risk; S3 buckets (publicity, encryption); EC2 instances, security groups, EBS volumes; RDS instances; EKS clusters; ECR repositories; Lambda functions and function URLs; API Gateway REST APIs; CloudTrail status; SageMaker notebooks and endpoints (with their models); Bedrock custom models, provisioned throughput, guardrails, invocation logging, agents (action groups, knowledge bases) and knowledge bases (data sources).

Scans run 4 regions and 2 member accounts at a time (AWS_REGION_CONCURRENCY, AWS_ACCOUNT_CONCURRENCY), every listing is paginated, SDK calls retry throttling adaptively (8 attempts) with 30-second request timeouts, and assumed-role sessions are refreshed before their one-hour expiry.

AWS Organizations

Connect the management account (or a delegated administrator) and 10ETLabs lists every active member account (organizations:ListAccounts; suspended or closing accounts are skipped). For each member it assumes arn:aws:iam::<member>:role/<same role name> with the same External ID, directly from the platform account. So:

  1. Deploy the CloudFormation template to every member account as a StackSet (CloudFormation → StackSets → Create StackSet, service-managed permissions, deploy to your organization or OUs) with the same ExternalId and PlatformAccountId parameters and the default role name.
  2. Deploy the stack in the management account too (StackSets do not deploy to the management account), and connect it.

A member account without the role appears as a "Member account was not assumed" collection finding and a coverage gap; the rest of the organization is still scanned.

Known limitations

  • AWS GovCloud (US), China and ISO partitions are not supported; the wizard refuses their ARNs.
  • CloudTrail is read from us-east-1 (home trails plus shadow copies of multi-region trails). A single-region trail whose home region is elsewhere is not listed, so an account with only such trails is reported as having no multi-region trail (which is still true).
  • Member accounts must use the same role name and External ID as the connected account.

Troubleshooting

What you seeCauseFix
"The role's trust policy doesn't allow us…" / AccessDenied … sts:AssumeRoleThe stack was deployed with a different External ID or platform account ID, the stack is not finished, or you pasted another role's ARNRe-deploy (or update) the stack with the External ID and platform account ID shown in the wizard; wait for CREATE_COMPLETE; paste the stack's RoleArn output
"Role ARN is not valid" / "That is an IAM user ARN" / "That is an account ID"The value is not arn:aws:iam::<12 digits>:role/<name>Copy RoleArn from the stack Outputs tab
"Unsupported AWS partition"A GovCloud/China ARNConnect a commercial AWS account
"Could not list AWS regions" / UnauthorizedOperation (check List regions failed)The role lacks ec2:DescribeRegions (older template or edited policy)Update the stack with the latest template (Update → Replace template)
Warning on IAM, S3, CloudTrail, SageMaker, Bedrock or Bedrock agentsThe role lacks that permission (the check names the action)Update the stack with the latest template. Until then that service is a coverage gap
"Member account was not assumed" findingThe role is missing in that member account, or has another name or External IDDeploy the StackSet to that account with the same parameters
"This console's own AWS credentials are missing or expired"The platform's own AWS credentials (operator side)Ask your operator; nothing to change in your account
No "Open CloudFormation quick-create" linkTemplates are not published in S3 on this deploymentUse the download + Create stack path, or ask the operator to set PUBLIC_TEMPLATE_BASE_URL to an S3 URL

Azure

What you create

An app registration (service principal) named 10ETLabs Reader with a client secret, assigned the built-in Reader role on each subscription you want scanned, and (for Entra ID) four read-only Microsoft Graph application permissions with admin consent. You paste the Directory (tenant) ID, Application (client) ID and the secret Value.

MethodTimeWhat you do
Cloud Shell script (recommended)~3 minIn Azure Cloud Shell (Bash) run 10etlabs-azure-setup.sh (optionally with subscription IDs). It creates the app and a one-year secret, assigns Reader on every enabled subscription you can see (or the ones you pass), requests the Microsoft Graph read permissions and tries to grant admin consent, and prints the three values. GRAPH_PERMISSIONS=0 skips the Graph part.
Azure portal~8 minApp registrations → New registration → Certificates & secrets → New client secret (copy the Value) → each Subscription → Access control (IAM) → Add role assignment → Reader → select the app → back in the app: API permissions → Add a permission → Microsoft Graph → Application permissions → the four below → Grant admin consent.
Terraform~5 min10etlabs-azure-reader.tf creates the app, secret, Reader assignments (subscription_ids variable) and the Graph app role assignments (admin consent; grant_graph_read = false skips them). The secret is in your Terraform state.

An ARM template (10etlabs-azure-reader-assignment.json) is also available to assign Reader on one subscription for an existing service principal: az deployment sub create --location <region> --template-file 10etlabs-azure-reader-assignment.json --parameters principalId=<object ID>. The principalId is the Enterprise application object ID, not the app registration's.

Whoever runs the setup needs permission to register applications and Owner or User Access Administrator on the subscriptions. Granting admin consent for the Microsoft Graph permissions needs a Global Administrator or Privileged Role Administrator; anyone else can request them and ask an admin to click Grant admin consent later (App registrations → 10ETLabs Reader → API permissions). The ARM template cannot grant Graph permissions; use the portal step for them.

Permissions and why

  • Reader (built-in role acdd72a7-3385-48ef-bd42-f606fba81ae7) on each subscription: lists resource configuration through Azure Resource Graph. Reader cannot read storage data, Key Vault secrets, keys or certificates, or change anything.
  • The AI supply-chain reads are ARM GETs under the same Reader role: model deployments and content filters (RAI policies), Foundry projects and connections, Azure ML datastores, online endpoints and deployments, model, job, data and environment versions, compute instances, diagnostic settings, blob container access levels, and the role assignments and role definitions of workload managed identities. AI Search index, indexer and data source *definitions* are read on the search data plane with an Entra ID token for the same app; Reader covers them when the service has role-based access enabled. No extra role is needed, and none that can invoke a model (such as Cognitive Services OpenAI User or Azure AI User) is ever requested.
  • Never called: listKeys, connection listsecrets, datastore listSecrets, the Azure OpenAI data plane (files, fine-tuning jobs, completions), the Foundry agent (Assistants) APIs, and AI Search document reads.
  • Reader also covers Microsoft.Authorization/*/read: role assignments, role definitions and deny assignments, used for effective permissions. Nothing extra is needed on Azure resources.

Entra ID (Microsoft Graph)

Application permissions on Microsoft Graph, all read-only, granted with admin consent:

PermissionWhyWithout it
Directory.Read.AllUsers (member or guest, enabled), groups and group membership, service principals and managed identities, app registrations with their owners, credential types and expiry dates, and federated credentials. Secret and certificate values are never read (Graph does not return them; 10ETLabs also drops the secret hint).Entra ID objects are not collected; Azure role assignments still show, by object ID.
RoleManagement.Read.DirectoryDirectory role assignments (Global Administrator and the other admin roles), and with Entra ID P2 the PIM eligible versus active (time-bound) assignments.Admin checks are skipped.
Policy.Read.All (optional)Conditional Access policies and security defaults, to tell whether administrators must use MFA.The admin MFA check is skipped.
AuditLog.Read.All (optional)Last sign-in dates (needs Entra ID P1), to find privileged accounts unused for 90 days.The stale admin check is skipped.

Test connection reads the permissions actually granted (the roles claim of a Graph token) and lists each one: missing ones are warnings with the exact fix, and the scan then records those areas as coverage gaps. Features your license does not include are not errors: without Entra ID P2 there is no PIM, so every active admin assignment is permanent; without P1 there is no Conditional Access or sign-in activity. Graph paging (@odata.nextLink, only followed on graph.microsoft.com) and throttling (429 with Retry-After) are handled. Large directories are read up to 20,000 users, groups and service principals and 5,000 app registrations; beyond that the rest is a coverage gap.

Effective permissions

For every subscription, 10ETLabs reads role assignments at, above and below it (management groups, the subscription, resource groups and resources), role definitions (built-in and custom) and deny assignments. Each assignment is resolved into what it allows: actions minus notActions and dataActions minus notDataActions, tested against concrete operations (manage resources, Microsoft.Authorization/roleAssignments/write, list storage keys, write or read blob data, read Key Vault secrets, VM run command). Group assignments are expanded through transitive membership (up to 300 groups and 5,000 members each), and capabilities a deny assignment covers are removed. Assignments with an ABAC condition are shown but never treated as conclusive. The Identity tab shows, for each principal, the chain behind its access: *member of group → role → scope → what it allows*.

Managed identities are linked to the VM, Azure OpenAI or AI services account they belong to (the resource's identity block), so attack paths can follow *internet-exposed VM → managed identity → role assignment → subscription Owner or storage data*. A path is built only when every hop was read: the NSG / NIC / public IP evaluation that shows the exposure, the identity link, and the assignment and definition that grant the access.

Identity checks

RuleSeverityRaised when
azure.entra.privileged_role_guestHighA guest account holds a privileged directory role (active or PIM eligible).
azure.entra.privileged_role_service_principalHighA service principal holds Global Administrator, Privileged Role Administrator or Privileged Authentication Administrator.
azure.entra.privileged_role_no_mfaHighAn enabled admin is covered by no enabled Conditional Access policy requiring MFA and security defaults are off. Policies scoped to some apps, locations or unread groups count as "may apply" and do not raise it. Per-user (legacy) MFA is not visible to this check.
azure.entra.too_many_global_adminsMediumMore than five principals hold Global Administrator permanently (not time-bound through PIM).
azure.entra.stale_privileged_userMediumAn enabled admin (directory role, or Owner/Contributor/User Access Administrator at subscription scope or above) has not signed in for 90 days. Only with sign-in data.
azure.entra.app_credential_expiringLowAn app registration credential (or a service principal secret) expired or expires within 30 days.
azure.entra.app_secret_long_livedMediumA client secret is valid for more than one year.
azure.entra.app_guest_ownerMediumA guest owns an app registration.
azure.rbac.sp_privileged_roleHighA service principal or managed identity can manage resources or assign roles at subscription, management-group or root scope (no condition, no deny assignment).
azure.rbac.guest_privileged_roleHighA guest can manage resources or assign roles at subscription scope or above.
azure.rbac.custom_role_wildcardHighA custom role allows a cross-provider wildcard such as * or */write.

What is collected

Every subscription the app can read, and one Azure Resource Graph query per subscription (Resources, 1,000 rows per page, all pages, including each resource's managed identity): virtual machines and their NICs, public IPs, subnets and network security groups; storage accounts (public blob access, HTTPS-only, TLS version, shared-key access, public network access); Key Vaults (purge protection, public network access); SQL servers (public network access, minimum TLS); AKS clusters (API server exposure, RBAC, local accounts); Azure OpenAI and Cognitive Services accounts (public network access). Requests time out after 30 seconds and throttling (429) is retried with backoff that honours Retry-After.

AI supply chain (only for subscriptions with AI resources): each model deployment with its model, version, SKU and content filter; each Azure ML online endpoint with its network access, auth mode and managed identity, and per deployment the registered model version (artifact storage account and container, resolved through datastores), the job that produced it (the storage accounts and containers its inputs read, including data assets) and the serving image registry; compute instances (public IP, public SSH, idle shutdown, identity); Foundry and ML connections (category, target, auth type; never credentials); AI Search indexes with each indexer's data source type and container; diagnostic settings; the public access level of blob containers the AI estate reads (only on accounts that allow public blobs); and the role assignments of each workload managed identity (Owner, Contributor or role-assignment rights at subscription or resource-group scope). Every failed read is a coverage gap for service ai. Requests time out after 30 seconds and throttling (429) is retried with backoff that honours Retry-After.

Disabled or deleted subscriptions are skipped and recorded as a coverage gap (their inventory is kept). Azure role assignments, role definitions and deny assignments are read per subscription, and Entra ID through Microsoft Graph (see above); a read that fails there is a coverage gap for that area, never a failed scan or a false finding.

Known limitations

  • Azure public cloud only (login.microsoftonline.com, management.azure.com). Azure Government and Azure China are not supported.
  • Client secrets only (no certificate or federated credential sign-in). Secrets expire (the script creates a one-year secret); create a new one before expiry and update the connection.
  • SQL firewall rules are not collected.
  • Azure OpenAI fine-tuning jobs and uploaded training files live on the OpenAI data plane, which needs a role that can also invoke models. They are not read, so a fine-tuned deployment is marked as fine-tuned but not linked to its training data. Foundry agents (Assistants API) are not collected for the same reason.
  • AI Search data sources never return their connection string to a Reader, so an index is linked to its source container name and type, not to a storage account. AI Search services that accept API keys only are a coverage gap (keys are never used).
  • Effective permissions cover Azure RBAC and Entra ID directory roles. Microsoft Graph API permissions granted to apps, administrative-unit scoped roles, Azure AD B2C tenants and per-user (legacy) MFA are not evaluated.
  • Management-group hierarchy is taken from the assignments each subscription returns (Reader on subscriptions does not list management groups).

Troubleshooting

What you seeCauseFix
"The client secret has expired" (AADSTS7000222)Secret past its end dateCreate a new secret (run the script again, or Certificates & secrets) and update credentials
"The client secret is wrong" (AADSTS7000215)Secret ID pasted instead of the Value, or a typoPaste the Value column (shown once when created)
"That is the Secret ID, not the secret"A GUID was pasted in the secret fieldSame as above
"The application (client) ID isn't in this tenant" (AADSTS700016)Client ID and tenant ID are from different directoriesCopy both from the same app registration's Overview
"The directory (tenant) ID was not found" (AADSTS90002)Typo in the tenant IDMicrosoft Entra ID → Overview → Tenant ID
"A Conditional Access policy blocks the app" (AADSTS53003)Workload-identity Conditional AccessExclude the service principal from the policy or allow this console's IP
"The app can't see any subscriptions" / AuthorizationFailed / Resource Graph 403No Reader assignment yet, or it has not propagatedAssign Reader on each subscription, wait a minute, retry
"Every subscription the app can see is disabled"All assigned subscriptions are disabledRe-enable one, or assign Reader on an active subscription
Warning on Microsoft Graph Directory.Read.All (or another Graph permission)The permission is not added, or admin consent was not grantedApp registrations → 10ETLabs Reader → API permissions → add it (Microsoft Graph → Application permissions) → Grant admin consent
"Entra ID was not read" collection findingNo Graph permission, or a Graph token could not be issuedSame as above; Azure resources are still scanned
"Partial Entra ID coverage" on the Identity tabSome Graph areas were not read (permission, license or throttling)The notice names each area and its fix; the checks that need it are skipped until then

Google Cloud

What you create

A service account 10etlabs-reader in the project, with four predefined read-only roles, and a JSON key for it. The Cloud Asset and Resource Manager APIs are enabled; the Vertex AI and Notebooks APIs are left as they are (if they are off, Vertex AI is not in use and there is nothing to scan).

MethodTimeWhat you do
Cloud Shell script (recommended)~3 minIn Cloud Shell run bash 10etlabs-gcp-setup.sh <PROJECT_ID>. It enables the APIs, creates the service account, grants the roles and writes 10etlabs-gcp-key.json (cloudshell download 10etlabs-gcp-key.json, then delete it).
Google Cloud console~8 minEnable the Cloud Asset API and Cloud Resource Manager API → IAM & Admin → Service accounts → Create (grant Cloud Asset Viewer, Browser, Vertex AI Viewer and Notebooks Viewer) → Keys → Add key → JSON.
Terraform~5 min10etlabs-gcp-reader.tf enables the APIs and creates the service account and bindings; create the key with gcloud iam service-accounts keys create.

In the wizard, drop or paste the key file; the wizard shows the detected service account and project. Leave Project ID blank to scan the key's own project, or enter another project where the service account has the roles.

Permissions and why

RoleWhy
Cloud Asset Viewer (roles/cloudasset.viewer)Every inventory and posture read goes through Cloud Asset Inventory: resources (contentType=RESOURCE) and IAM policies (contentType=IAM_POLICY)
Browser (roles/browser)Read the project itself (resourcemanager.projects.get)
Vertex AI Viewer (roles/aiplatform.viewer)Vertex AI configuration in the locations Cloud Asset Inventory shows in use: endpoints (network, deployed models, service accounts, request logging), models (artifact bucket, serving image, producing job), datasets and the training pipelines, pipeline jobs and tuning jobs that produced a model (input bucket or BigQuery table), Vector Search indexes and endpoints, feature stores, RAG corpora (file locations), Agent Engine (package bucket, service account). It does not include predict or generateContent.
Notebooks Viewer (roles/notebooks.viewer)Vertex AI Workbench instances: public IP, root access, idle shutdown, service account. It does not include opening the notebook (proxy access).

APIs: Cloud Asset API (cloudasset.googleapis.com) and Cloud Resource Manager API (cloudresourcemanager.googleapis.com). If the service account lives in a different project from the one scanned, the Cloud Asset API must also be enabled in the service account's project (quota is charged there).

No basic role (Viewer, Editor, Owner) is needed. The OAuth scope is cloud-platform.read-only. A test in the codebase (packages/cloud-gcp/src/permissions.spec.ts) records every Google API URL the collector and verify request and fails if one needs a permission the templates' roles do not grant.

Never accessed: objects in buckets, BigQuery or database data, Secret Manager values, Vertex AI dataset items, RAG file contents, model prompts, predictions, outputs or weights. No model is ever invoked.

What is collected

Project IAM policy (service accounts with Owner/Editor); Compute Engine instances (external IPs) and VPC firewall rules; Cloud Storage buckets (public access prevention, uniform access, bucket IAM for allUsers / allAuthenticatedUsers); GKE clusters; Cloud SQL instances; IAM service accounts and user-managed keys. Listings are paginated (500 per page), requests time out after 60 seconds and 429/5xx are retried.

Vertex AI supply chain. One Cloud Asset listing (aiplatform.googleapis.com.*, notebooks.googleapis.com.*) finds the locations in use; only those are read through the Vertex AI API ({location}-aiplatform.googleapis.com, 100 per page) and the Notebooks API v2. Each model is linked to its artifact bucket, serving image repository and the job that produced it; each training pipeline, pipeline job or tuning job to its input buckets and BigQuery datasets; each endpoint to its deployed models and service accounts; each Vector Search index and RAG corpus to its source bucket; each Agent Engine deployment to its package bucket. Endpoint, Artifact Registry repository and BigQuery dataset IAM policies come from Cloud Asset Inventory (allUsers / allAuthenticatedUsers). Custom jobs are not listed on their own: only jobs a model names are read. A location or collection that cannot be read (denied, API disabled, not offered in that region) is a coverage gap for service vertex; nothing there is deleted or resolved.

Known limitations

  • One project per connection. Folder and organization scope are not supported yet; connect each project.
  • Service account JSON keys only. Workload identity federation configurations ("type": "external_account") are not supported yet.
  • Organizations created since 2024 enforce iam.disableServiceAccountKeyCreation by default. Key creation then fails; an organization policy administrator must allow keys for this project.

Troubleshooting

What you seeCauseFix
"The service account JSON isn't valid" / no client_email / no private_keyPartial paste, or the wrong filePaste or drop the whole downloaded key file
"Workload identity federation is not supported yet"An external_account config was pastedCreate a service account JSON key
"That is a personal gcloud credential"authorized_user credentials were pastedUse the service account key
"Google rejected the service account key" (invalid_grant, Invalid JWT)Key deleted, service account disabled, or clock skewCreate a new key
"A required Google API is turned off" (SERVICE_DISABLED, "has not been used in project")Cloud Asset or Resource Manager API disabled (in the scanned project or the service account's project)Enable both APIs, wait a few minutes, retry
"The service account is missing read roles" (PERMISSION_DENIED)Roles not granted on this projectGrant Cloud Asset Viewer and Browser (or run the script)
Warning on Read IAM policiesCloud Asset Viewer missing IAM-policy listingGrant Cloud Asset Viewer; bucket publicity and Owner/Editor checks stay unknown until then
Warning on Read Vertex AI configuration or Read Vertex AI Workbench instancesVertex AI Viewer or Notebooks Viewer not grantedGrant the role (or run the script again). Skipped when the API is off in the project. Until then Vertex AI supply-chain checks are a coverage gap
"That isn't a project ID"A project name or number was enteredUse the project ID (e.g. my-project-123)

Kubernetes

What you create

Apply one manifest (kubectl apply -f 10etlabs-k8s-reader-clusterrole.yaml). It creates:

  • ClusterRole 10etlabs-reader with only list on nodes, pods, services (core) and deployments (apps)
  • ServiceAccount kube-system/10etlabs-reader and a ClusterRoleBinding to the role
  • Secret kube-system/10etlabs-reader-token of type kubernetes.io/service-account-token: a long-lived token (tokens from kubectl create token expire after an hour and scans would then fail)

Then paste into the wizard:

ValueCommand
Tokenkubectl -n kube-system get secret 10etlabs-reader-token -o jsonpath='{.data.token}' | base64 -d
Cluster CA certificatekubectl -n kube-system get secret 10etlabs-reader-token -o jsonpath='{.data.ca\.crt}' | base64 -d (or kubeconfig certificate-authority-data)
API server URLkubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'

The CA certificate is needed for EKS, GKE, AKS and most self-managed clusters, whose API server certificate is signed by a private cluster CA. TLS is verified against it. "Skip TLS verify" exists for lab clusters and is refused in production.

What is collected

Nodes (external IPs, provider ID), services (public LoadBalancers and external IPs, label selector, internal load-balancer annotation), deployments and pods with a security summary of their pod spec: namespace, node, owning workload, labels, service account and whether its token is automounted, host namespaces, hostPath paths, runAsNonRoot/runAsUser, and per container the image reference, privileged, whether CPU/memory limits are set and how many GPUs it requests. The summary feeds the admission-policy simulate view and runtime attack paths (runtime.md). Lists are paginated (limit=500 with continue); each request times out after 30 seconds (KUBE_REQUEST_TIMEOUT_MS). A kind that cannot be listed becomes a coverage gap; if none can be listed the scan fails.

Never accessed: Secrets, ConfigMaps, pod logs, exec, port-forward, etcd. Note that listing pods returns full pod specs, including literal environment variable values; 10ETLabs keeps only the fields above (never env, args, commands or annotations).

Optional: runtime sensor and admission policy

Everything above is agentless. For runtime evidence (shells in model servers, instance-metadata access, unexpected egress, miners, risky pods actually running) and deploy-time blocking, you can add an optional Helm chart from Findings → Cloud → Runtime (or the link on the Kubernetes connection). It uses its own per-cluster key (hashed at rest, rotate/revoke in the console) and signed HTTPS requests, sends metadata only, and the admission webhook fails open by default. See runtime.md for what it collects, what it never collects, resource use and troubleshooting.

Network and SSRF protection

The API server URL must be https://. Every request resolves the host, rejects private, loopback, link-local and metadata addresses (unless the deployment is on-prem or hybrid), pins the connection to the vetted IP and never follows redirects, so the token cannot be sent elsewhere. A cluster whose API endpoint is private-only cannot be scanned from the SaaS console.

Troubleshooting

What you seeCauseFix
"The cluster's TLS certificate isn't trusted" (self-signed certificate in certificate chain, unable to verify)No or wrong CA certificatePaste the cluster CA certificate
"The cluster CA certificate isn't valid"Not PEM or base64 PEMPaste certificate-authority-data or the PEM
"The cluster rejected the token" (HTTP 401)Token expired (e.g. from kubectl create token), deleted, or from another clusterCopy the token from the 10etlabs-reader-token Secret
"The token can't list cluster resources" (HTTP 403, "Could not list …")ClusterRole or binding missingApply the manifest again
"That API server address isn't allowed"Private/loopback address on the SaaS consoleUse the cluster's public endpoint
"The API server URL must use https://"http:// URLUse the server: value from kubeconfig
"We can't find that address" / timeoutsDNS or firewall (authorized networks / IP allowlists on the API server)Allow the console's egress IPs on the API server

Other VPS providers

What you create

A read-only API token (or a host list). Every vendor listing is paginated to the end with the vendor's maximum page size; requests time out after 30 seconds and rate limits (429, and 5xx) are retried with backoff.

ProviderTokenEndpoint
DigitalOceanAPI → Personal access tokens → Custom scopes → droplet:readGET api.digitalocean.com/v2/droplets (200 per page)
Hetzner CloudProject → Security → API tokens → Read (one token per project; connect each project)GET api.hetzner.cloud/v1/servers (50 per page)
Linode (Akamai)Profile → API Tokens → Linodes Read Only, everything else No AccessGET api.linode.com/v4/linode/instances (500 per page)
VultrAccount → API → Enable API; allow this console's IPv4 in Access Control (or switch on Any IPv4)GET api.vultr.com/v2/instances (500 per page)
Custom host listPaste {"hosts":[{"id":"web-1","ip":"203.0.113.10","name":"web-1","region":"ams3"}]}Nothing is contacted

Vultr API keys have no read-only scope; the key is stored encrypted and only used to list instances.

Pagination links returned by a vendor are only followed on the vendor's own API origin, so the token is never sent to another host.

What is collected

Server name, region and public IP. A host with a public IP gets a finding to restrict inbound access. CVE, secret and malware findings need the optional host sensor installed on each host.

Troubleshooting

What you seeCauseFix
"The API token's IP allowlist blocks this console" (Vultr "Unauthorized IP address")Token ACLAdd the IP named in the error, or turn on Any IPv4
"The vendor refused the API token" (401/403)Wrong, expired or under-scoped tokenCreate a new read-only token
"The host list isn't in the right format" (hostsJson …)Invalid JSON, a row that is not an object, an invalid IP or a duplicate idFix the named row
"No hosts were found"Token for another team/project, or an empty listUse the token of the team/project that owns the servers
"The vendor's server list did not finish paging"The vendor kept returning more pages (over 200)Retry later; nothing was changed

DeepScan (opt-in disk scanning)

Every connection above is read-only and agentless. DeepScan is a separate, opt-in addition for AWS, Azure and Google Cloud: 10ETLabs snapshots VM disks, a short-lived scanner VM in your account reads the snapshot, and only findings metadata (package + CVE, secret type + path + fingerprint, malware signature, AI model file path/size/hash) comes back. Snapshots, scanner VMs and result files are deleted after every scan. It needs its own permission set, deployed from a separate template (templates/aws/deepscan.cfn.yaml / deepscan.tf, templates/azure/deepscan-setup.sh / deepscan.tf, templates/gcp/deepscan-setup.sh / deepscan.tf); the reader role is never changed. Turn it on under Cloud accounts → ⋯ → DeepScan or at the end of the connect wizard. Architecture, exact permissions, cost and troubleshooting are in docs/deepscan.md.

Host sensor

The host sensor is optional and runs on your VPS hosts to report package vulnerabilities, exposed secrets and malware. Save a VPS connection first, then copy the install script from the connection (the connection API key is shown once; rotate it to get a new one). Sensors report to the console's public URL.

Optional remediator (one-click fixes)

Nothing in this section is needed for scanning. A remediator is a second, separate identity you deploy on purpose so an Owner or Admin can approve one-click fixes. It never replaces the read-only identity, and one-click fixes are unavailable on a connection until its remediator is connected and verified. Set it up in Settings → Remediation (one card per AWS, Azure or Google Cloud connection).

CloudWhat you deployExactly what it can do
AWStemplates/aws/remediator.cfn.yaml (or remediator-role.tf): role 10ETLabsRemediator, trusted by the platform account with its own ExternalId (shown on the card, different from the reader's)s3:GetBucketPublicAccessBlock, s3:PutBucketPublicAccessBlock, s3:GetEncryptionConfiguration, s3:PutEncryptionConfiguration, ec2:DescribeSecurityGroups, ec2:RevokeSecurityGroupIngress, ec2:AuthorizeSecurityGroupIngress (rollback only), sagemaker:DescribeNotebookInstance, sagemaker:UpdateNotebookInstance, bedrock:GetModelInvocationLoggingConfiguration, bedrock:PutModelInvocationLoggingConfiguration, bedrock:DeleteModelInvocationLoggingConfiguration (rollback only), lambda:GetFunctionUrlConfig, lambda:UpdateFunctionUrlConfig
Azuretemplates/azure/remediator-setup.sh (Cloud Shell): custom role 10ETLabs Remediator (remediator-role.json), a separate app registration and one assignment on the subscriptionMicrosoft.Storage/storageAccounts/read, …/write, Microsoft.CognitiveServices/accounts/read, …/write, Microsoft.Network/networkSecurityGroups/read, …/securityRules/delete, …/securityRules/write (rollback only)
Google Cloudtemplates/gcp/remediator-setup.sh (or remediator.tf): custom role tenetlabsRemediator and a separate service account 10etlabs-remediatorstorage.buckets.get, storage.buckets.update, compute.firewalls.get, compute.firewalls.update, compute.networks.updatePolicy

No wildcard actions, no IAM or role-assignment rights, no object/blob/data access, and no resource deletion. Tests keep the templates identical to what the fix executors call (packages/cloud-aws/src/remediation/iam-actions.spec.ts, packages/cloud-azure/src/remediation.spec.ts, packages/cloud-gcp/src/remediation.spec.ts).

Verify (no write is attempted):

  • AWS: the ARN is not the reader role, is in the connection's account, can be assumed with the remediator ExternalId, and STS reports that role.
  • Azure: the client ID is not the reader app, the app signs in, and its effective permissions on the subscription include every action above (a warning if it holds more).
  • Google Cloud: the key is not the reader service account, and testIamPermissions on the project returns every permission above.

On each card you choose which fixes the remediator may apply and whether two-person approval is required (the approver must not be the requester). To turn one-click fixes off, press Disconnect (credentials are wiped; history stays) and delete the stack / app / service account in your cloud.

MessageCauseFix
"This is the read-only scanner role/app/service account"The reader identity was pastedDeploy the remediator template and paste its output
"not authorized to perform sts:AssumeRole"Wrong ExternalId (the reader's was used) or the stack is in another accountRedeploy with the remediator External ID shown on the card
"Missing Microsoft.… / Missing compute.…"The custom role is not assigned, or not yet propagatedRerun the setup script; assignments can take a few minutes

For operators

SettingPurpose
PLATFORM_AWS_ACCOUNT_ID12-digit account the scanner runs in; customers' roles trust it. AWS onboarding is disabled without it.
PUBLIC_TEMPLATE_BASE_URLHTTPS base where templates/ is mirrored. Must be an S3 URL for the CloudFormation quick-create link (CloudFormation only loads templates from S3); also enables one-line curl commands in the Azure and GCP scripts.
TEMPLATES_DIRWhere the API finds templates/ (defaults to searching upwards).
DEPLOYMENT_MODE=onprem or hybridAllows private IP Kubernetes API servers.
AWS_REGION_CONCURRENCY, AWS_ACCOUNT_CONCURRENCY, KUBE_REQUEST_TIMEOUT_MSCollector tuning.