Security AWS

Roll Out Wiz CSPM Across a Multi-Account AWS Organization with the AWS Connector

A fintech with 140 AWS accounts under a single AWS Organization just failed a customer security questionnaire on one line: “Do you have continuous, agentless posture management across all cloud accounts?” The honest answer was no — three accounts had Wiz, the rest were a spreadsheet and good intentions. The mandate from the CISO is blunt: every account onboarded to Wiz CSPM by quarter-end, new accounts auto-enrolled the day they are created, no per-account click-ops, and the noise tuned so the on-call engineer trusts the queue instead of muting it. This guide is the runbook to do exactly that — onboard the whole Organization through the Wiz AWS connector with a single organization-wide CloudFormation StackSet, then tune risk rules and security-graph policies so the findings are signal, not a wall of red.

The goal is one connector that scans the entire Organization agentlessly (no sensors to install on workloads for posture), discovers every resource into the Wiz Security Graph, and surfaces toxic combinations — the public-exposure-plus-IAM-path-plus-sensitive-data chains that are the real risk — rather than ten thousand isolated config flags. We will keep humans out of the IAM-key business entirely.

Prerequisites

Target topology

Roll Out Wiz CSPM Across a Multi-Account AWS Organization with the AWS Connector — topology

The end state is one Wiz AWS connector registered against the Organization. The connector assumes a single IAM role — WizAccess-Role — that exists in every member account, provisioned by a CloudFormation StackSet with auto-deployment so new accounts get the role automatically. Wiz uses that role to run agentless workload scanning (it snapshots EBS volumes in your account and analyzes them out-of-band) and to read the AWS control plane via describe/list calls. Everything it discovers — accounts, VPCs, security groups, IAM roles, S3 buckets, EKS clusters, secrets, exposed data — lands as nodes and edges in the Wiz Security Graph, where risk rules and graph policies evaluate paths, not points.

Around that core: Okta/Entra ID federates engineers into the Wiz console via SAML so access is your existing SSO and conditional-access posture (no local Wiz passwords). HashiCorp Vault holds the Wiz service-account clientId/clientSecret used by automation, leased short-lived rather than baked into CI. GitHub Actions / Jenkins apply the Terraform and run Wiz Code (IaC scanning) on pull requests so a misconfiguration is caught before it becomes a runtime finding. ServiceNow receives high-severity issues as change/incident records. Optionally, CrowdStrike Falcon provides runtime threat detection on the same workloads and shares signals with Wiz so a posture risk and an active runtime detection correlate; Dynatrace or Datadog ingests the Wiz issue stream for security-ops dashboards alongside performance telemetry.

1. Bootstrap: enable StackSet trusted access in the management account

A single connector for the whole Organization depends on a role landing in every account. CloudFormation StackSets with service-managed permissions is the clean way to do that — it deploys into Organizational Units and auto-deploys to accounts created later. Enable it once from the management account.

# Run as the AWS Organization management account.
aws organizations enable-all-features 2>/dev/null || true   # idempotent if already on

# Let CloudFormation StackSets deploy across the Organization
aws organizations enable-aws-service-access \
  --service-principal member.org.stacksets.cloudformation.amazonaws.com

# Verify trusted access is registered
aws organizations list-aws-service-access-for-organization \
  --query "EnabledServicePrincipals[?ServicePrincipal=='member.org.stacksets.cloudformation.amazonaws.com']"

Grab the Organization root ID and the OUs you intend to cover — you will target the root to mean “all accounts”:

aws organizations list-roots --query "Roots[0].Id" --output text          # e.g. r-ab12
aws organizations list-organizational-units-for-parent --parent-id r-ab12 \
  --query "OrganizationalUnits[].[Id,Name]" --output table

2. Create the Wiz AWS connector and capture its external ID

In Wiz, you create the connector first so it hands you the two values the IAM trust policy must pin: the Wiz AWS principal account that will assume your role, and a per-tenant external ID that defeats the confused-deputy problem. Do it in the console (Settings → Cloud Accounts → Add Account → AWS → AWS Organization) or via the Wiz API. The API path is what you automate:

# Authenticate to the Wiz API with a service account (clientId/clientSecret from Vault).
export WIZ_CLIENT_ID="$(vault kv get -field=clientId secret/wiz/connector-sa)"
export WIZ_CLIENT_SECRET="$(vault kv get -field=clientSecret secret/wiz/connector-sa)"

WIZ_TOKEN=$(curl -s -X POST https://auth.app.wiz.io/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&audience=wiz-api" \
  -d "client_id=${WIZ_CLIENT_ID}&client_secret=${WIZ_CLIENT_SECRET}" \
  | jq -r '.access_token')

# Read back the values Wiz expects in the role trust policy for your tenant.
# (These are stable per-tenant; the console shows them on the AWS connector screen.)
curl -s -X POST "https://api.${WIZ_REGION}.app.wiz.io/graphql" \
  -H "Authorization: Bearer ${WIZ_TOKEN}" -H "Content-Type: application/json" \
  -d '{"query":"query { cloudOrganizationProviders(first:1) { nodes { externalId } } }"}' \
  | jq .

Record two outputs for the next step:

If you prefer fully declarative setup, the official wizio/wiz Terraform module wraps this connector creation — but reading the external ID once and pinning it is the part you must not skip.

3. Deploy the WizAccess role to every account via StackSet (Terraform)

This is the heart of the rollout. One IAM role, one trust policy pinned to Wiz’s principal + your external ID, deployed org-wide with auto-deployment so day-N accounts self-enroll. The role is read-mostly for posture plus the specific permissions agentless scanning needs (creating/sharing EBS snapshots in-account, KMS describe for encrypted volumes).

# providers: aws (management account), plus a CloudFormation StackSet
variable "wiz_principal_account" { type = string }   # from step 2
variable "wiz_external_id"       { type = string }   # from step 2 (store via Vault, not in VCS)
variable "org_root_id"           { type = string }   # e.g. r-ab12

resource "aws_cloudformation_stack_set" "wiz_access" {
  name             = "wiz-access-role"
  permission_model = "SERVICE_MANAGED"
  capabilities     = ["CAPABILITY_NAMED_IAM"]

  auto_deployment {
    enabled                          = true   # new accounts get the role automatically
    retain_stacks_on_account_removal = false
  }

  parameters = {
    WizPrincipalAccount = var.wiz_principal_account
    WizExternalId       = var.wiz_external_id
  }

  template_body = file("${path.module}/wiz-access-role.yaml")
}

resource "aws_cloudformation_stack_set_instance" "wiz_access_org" {
  stack_set_name = aws_cloudformation_stack_set.wiz_access.name

  deployment_targets {
    organizational_unit_ids = [var.org_root_id]   # target the root = all member accounts
  }

  region = "us-east-1"   # IAM is global; pick one home region for the stack instance

  operation_preferences {
    failure_tolerance_percentage = 10
    max_concurrent_percentage    = 25   # stagger across 140 accounts
    region_concurrency_type      = "PARALLEL"
  }
}

The CloudFormation template the StackSet deploys (wiz-access-role.yaml) — note the trust policy pins both the Wiz principal and the external ID, and grants a least-privilege scanning policy. The WizSecurityAudit managed-style permissions read the control plane; the inline WizDiskScanning statement is what agentless volume scanning needs:

AWSTemplateFormatVersion: "2010-09-09"
Description: Wiz CSPM cross-account access role (org-wide StackSet)
Parameters:
  WizPrincipalAccount: { Type: String }
  WizExternalId:       { Type: String, NoEcho: true }
Resources:
  WizAccessRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: WizAccess-Role          # identical name in every account
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { AWS: !Sub "arn:aws:iam::${WizPrincipalAccount}:root" }
            Action: "sts:AssumeRole"
            Condition:
              StringEquals: { "sts:ExternalId": !Ref WizExternalId }
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/SecurityAudit
      Policies:
        - PolicyName: WizDiskScanning
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateSnapshot
                  - ec2:CreateSnapshots
                  - ec2:CreateTags
                  - ec2:DescribeSnapshots
                  - ec2:ModifySnapshotAttribute   # share snapshot to Wiz scan account
                  - ec2:DeleteSnapshot
                  - kms:DescribeKey
                  - kms:ReEncryptFrom
                Resource: "*"

Apply through CI, never from a laptop:

terraform init && terraform plan -out=wiz.plan && terraform apply wiz.plan
# Watch the StackSet roll out across accounts
aws cloudformation list-stack-instances \
  --stack-set-name wiz-access-role \
  --query "Summaries[].[Account,Region,Status]" --output table

4. Register the Organization in Wiz against the deployed role

Now point the connector at the role you just spread across the Organization. Wiz needs the role ARN pattern (it is the same WizAccess-Role in every account) and the management account ID so it can enumerate members via the Organizations API. In the console this is one screen; via API it is a mutation:

curl -s -X POST "https://api.${WIZ_REGION}.app.wiz.io/graphql" \
  -H "Authorization: Bearer ${WIZ_TOKEN}" -H "Content-Type: application/json" \
  -d @- <<'JSON'
{ "query": "mutation CreateAwsOrg($input: CreateCloudOrganizationProviderInput!) { createCloudOrganizationProvider(input:$input){ cloudOrganizationProvider{ id } } }",
  "variables": { "input": {
      "cloudProvider": "AWS",
      "externalId": "REPLACE_WIZ_EXTERNAL_ID",
      "organizationId": "REPLACE_AWS_ORG_MGMT_ACCOUNT_ID",
      "roleArnTemplate": "arn:aws:iam::{accountId}:role/WizAccess-Role"
  } } }
JSON

Give Wiz the management account’s read access to AWS Organizations as well (the same WizAccess-Role in the management account inherits SecurityAudit, which covers organizations:List*/Describe*). Within minutes Wiz begins enumerating accounts and assuming the role in each. Confirm coverage in the console (Settings → Cloud Accounts) — you want the account count to match aws organizations list-accounts | jq '.Accounts | length'.

5. Wire SSO (Okta/Entra) and pull automation secrets from Vault

Before engineers touch the findings, federate access. In Wiz, Settings → Identity Providers → SAML, upload the IdP metadata from Okta (or Entra ID), and map IdP groups to Wiz roles so your sec-eng group lands as Wiz Admin and dev-* groups get scoped read on their own projects. This means access to the posture data follows your existing joiner/mover/leaver and conditional-access controls — there is no separate Wiz password to deprovision.

The Wiz service account used by CI (steps 2–4) should never have its secret in the repo. Store it in HashiCorp Vault and have the pipeline fetch it at run time:

# In GitHub Actions / Jenkins, authenticated to Vault via OIDC/JWT:
export WIZ_CLIENT_ID=$(vault kv get -field=clientId  secret/wiz/connector-sa)
export WIZ_CLIENT_SECRET=$(vault kv get -field=clientSecret secret/wiz/connector-sa)
# secrets exist only in the job's memory; nothing is written to disk or VCS

6. Tune risk rules and Security Graph policies

A fresh connector will light up thousands of findings; untuned, the queue gets muted within a week. The job now is to make the queue trustworthy. Three levers, in order.

(a) Set the project structure so risk has business context. Map AWS accounts/OUs to Wiz Projects (e.g. payments-prod, data-platform, sandbox) so severity is weighted by environment — a public S3 bucket in payments-prod outranks the same in sandbox. Projects also scope who sees what via the SSO group mapping from step 5.

(b) Tune the built-in risk rules / Cloud Configuration Rules. Wiz ships hundreds of config rules mapped to CIS AWS / NIST. Do not disable noisy ones blindly — re-scope them. For known-accepted designs, attach a resource exception with an expiry and a justification rather than turning the rule off globally, so the exception is auditable and time-boxed.

© Write Security Graph policies for the toxic combinations you actually fear. This is where Wiz earns its keep. A graph query expresses an attack path — not “this SG is open” but “an internet-exposed EC2 instance with a role that can read a bucket holding PII.” Author these as custom Wiz queries so they become high-severity Issues:

# Wiz Security Graph: internet-exposed compute that can reach sensitive data
MATCH (vm:VirtualMachine)-[:HAS]->(role:AccessRole)-[:CAN_ACCESS]->(b:Bucket)
WHERE vm.exposure = "PUBLIC"
  AND b.hasSensitiveData = true
RETURN vm, role, b

Promote that query to a policy so any matching path opens an Issue at CRITICAL. Build the handful that matter for your estate: public-exposure → admin-equivalent IAM; unencrypted volume holding secrets; a *:* role assumable cross-account; an EKS pod with a privileged service account reachable from the internet. Each policy should map to an owner project so the Issue routes to the right team.

(d) Shift left with Wiz Code in CI. Add Wiz Code IaC scanning to the same pipeline that applies the Terraform, so the misconfiguration is blocked on the pull request — the cheapest place to fix it:

# .github/workflows/wiz-code.yml — runs on every PR touching infra/
- name: Wiz Code IaC scan
  run: |
    wizcli auth --id "$WIZ_CLIENT_ID" --secret "$WIZ_CLIENT_SECRET"
    wizcli iac scan --path ./infra --policy "Default IaC policy" \
      --tag "repo=$GITHUB_REPOSITORY" --tag "pr=$PR_NUMBER"

7. Route findings to ServiceNow and the SOC tooling

Tuned Issues are only useful if they reach an owner. In Wiz, Settings → Integrations, add the ServiceNow integration and create an Automation Action that opens a ServiceNow incident (or change record) for any Issue at HIGH/CRITICAL, stamped with the project, the graph path, and the remediation steps. Set the rule to re-resolve the ticket when Wiz marks the Issue resolved so the two systems stay in sync.

For security operations, stream the Wiz Issue webhook into Datadog or Dynatrace so posture risk sits on the same pane as runtime telemetry, and correlate with CrowdStrike Falcon runtime detections — a Wiz “public host with admin role” plus a Falcon “process injection on that host” is a page-now event, where either alone might wait for business hours.

Validation

Confirm the rollout end to end, not just that Terraform applied.

# 1) Role exists and is assumable in a sample member account (use account creds or assume-role chain)
aws iam get-role --role-name WizAccess-Role --query "Role.Arn"

# 2) Account coverage in Wiz matches the Organization
EXPECTED=$(aws organizations list-accounts --query "length(Accounts[?Status=='ACTIVE'])")
echo "Active AWS accounts: $EXPECTED — confirm this matches the Wiz connector account count."

# 3) StackSet has no failed instances
aws cloudformation list-stack-instances --stack-set-name wiz-access-role \
  --query "Summaries[?Status!='CURRENT'].[Account,Status,StatusReason]" --output table

In the Wiz console, verify: the connector status is Connected (not “partial access”), the inventory shows resources from multiple accounts (filter the Security Graph by Cloud Account), your custom graph policies have run at least once, and a deliberately-broken test resource (e.g. a public S3 bucket in a sandbox account) shows up as an Issue and lands in ServiceNow. That last loop — break something on purpose, watch it flow to a ticket — is the real proof the pipeline works.

Rollback / teardown

Because the whole rollout is one StackSet plus one connector, removal is clean and reversible.

# 1) Remove stack instances from all accounts (Terraform destroys the instance + set)
terraform destroy -target=aws_cloudformation_stack_set_instance.wiz_access_org
terraform destroy -target=aws_cloudformation_stack_set.wiz_access

# 2) If StackSet instances linger, delete them explicitly (service-managed)
aws cloudformation delete-stack-instances \
  --stack-set-name wiz-access-role \
  --deployment-targets OrganizationalUnitIds=r-ab12 \
  --regions us-east-1 --no-retain-stacks

# 3) Delete the connector in Wiz (console: Settings -> Cloud Accounts -> Remove),
#    or via API deleteCloudOrganizationProvider(id: ...)

Deleting the connector stops scanning immediately; deleting the StackSet removes WizAccess-Role from every account so no residual trust to Wiz remains. Leave auto_deployment.retain_stacks_on_account_removal = false so an account leaving the Org also sheds the role.

Common pitfalls

Security notes

The design keeps humans out of long-lived AWS keys entirely: Wiz assumes a role with an external-ID guard (no access keys to leak), and the CI service account’s Wiz secret is leased from Vault, not committed. Console access is Okta/Entra SAML only, so deprovisioning a leaving engineer in the IdP removes their Wiz access too. The WizAccess-Role is read-mostly — the only mutating permissions are the snapshot create/share/delete that agentless scanning requires, scoped by the inline policy; review it against your own threat model and tighten Resource from * to volume ARNs if your compliance bar demands it. Pair Wiz’s posture view with CrowdStrike Falcon for runtime so a misconfiguration and an active exploit correlate rather than living in two silos.

Cost notes

Wiz CSPM is licensed per billable cloud resource / workload, so the dominant cost lever is what you scan, not how often. Onboarding 140 accounts will surface idle and forgotten resources — decommissioning those trims both the bill and the attack surface. Agentless scanning incurs minor AWS charges for the short-lived EBS snapshots Wiz creates and deletes; keep snapshot retention at the Wiz default (cleaned up after analysis) so they do not accumulate. Putting Wiz Code in CI is the cheapest spend of all: a misconfiguration fixed on a pull request never becomes a runtime Issue, a ServiceNow ticket, and an on-call interruption downstream. Right-size by mapping sandbox/ephemeral accounts to a lower-frequency scan schedule and reserving continuous scanning for production projects.

WizCSPMAWS OrganizationsCloud SecurityTerraformSecurity Graph
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading