Cloud (CSPM) — Architecture
CSPM scans a customer's AWS account agentlessly and read-only. There are no
stored long-lived credentials: at scan time the platform assumes a read-only
role the customer created, via a two-hop STS chain that quarantines the
blast radius to a single isolated account. The scan engine runs Prowler
(de-branded) and maps its OCSF output into the platform's normal
Assets → Checks → Findings → Tasks.
The two-hop assume chain
- Hop 1 — into the isolated assumer account. The
cloud-worker(in the prod account) assumes the assumer role in Account B (scanners-cloud-access,399827112847). Account B is a dedicated, single-purpose account holding only that role — no data, no other workloads — and is locked down by an SCP (assume-only, no compute, no data, region-locked; the assumer's trust policy is tamper-protected). (Hop 1 is skipped in local dev, where the worker uses ambient credentials directly.) - Hop 2 — into the customer. From the assumer role, the worker assumes the customer's read-only role, gated by that customer's External ID. This is the confused-deputy guard: the customer's trust policy requires the exact External ID, so no other principal can assume the role even if the ARN leaks.
- Credentials are short-lived STS sessions (1 hour, role-session name
binarynetworks-scanner), auto-refreshed by the SDK for the life of a long scan. Between scans, standing access is zero — only the role definitions and the External ID exist; there are no live credentials anywhere.
Code:
app/scanners/aws/auth.py(assume_customer_role,validate_account). The two hops are plainsts:AssumeRolecalls; the External ID is passed on hop 2.
The External ID
A per-customer, 256-bit URL-safe random value (secrets.token_urlsafe(32)),
minted at connect time. It is sensitive config, not a crypto secret — its job
is confused-deputy prevention, not confidentiality. It is stored on the tenant's
CloudAccount record (never exposed in scan output). The real isolation controls
are the per-customer trust, the read-only policy, the SCP-locked Account B, the
ephemeral STS sessions, and the customer's ability to revoke at any time.
The scan engine (Prowler, de-branded)
The cloud scanner is a lean worker image (Python + Prowler; no Go toolchain),
running on its own ECS service consuming the scanners-aws SQS queue, scaled to
zero when idle. For each validated connected account it assumes the role,
injects the temporary credentials, and runs:
prowler aws --output-formats json-ocsf -o <tmpdir> --no-banner
(optionally narrowed with --service / --region). It then parses Prowler's
OCSF Detection Finding records (class_uid 2004) and maps them in:
- OCSF severity (
severity_id1–6: Info / Low / Medium / High / Critical / Fatal) maps to the platform's severity; the finalization pass then re-rates and de-noises it like any other finding. - dedup key =
check_id | resource-arn(the ARN carries account + region), so re-scans update in place instead of duplicating. - De-branding (enforced): the underlying tool's name is stripped from all free
text (→ "the S.E.T system") and its own links/scoring framework are dropped; real
compliance mappings and AWS documentation links are kept. The evidence
sourceis recorded ascloud_cspm. The SDK user-agent is also rebranded, so the customer's CloudTrail shows the product, not the tool. - Fault isolation: if one connected account can't be assumed, it is skipped
(logged
action=cloud.assume_failed) and the scan continues with the rest.
Code:
app/scanners/aws/prowler.py(AwsProwlerScanner,parse_ocsf,_run_prowler). Raw OCSF is large and is not stored in the DB.
Azure & GCP — the credential broker
The two-hop chain above is AWS-specific — it works because AWS lets us assume a role, so no secret is ever stored. Azure and GCP have no cross-cloud assume-role, so the customer's read-only identity comes with a durable credential (an Azure service-principal client secret, or a GCP service-account key). The platform keeps the same "workers never hold a durable key" property using a credential broker:
- Write-only onboarding. The API stores the credential in Secrets Manager,
encrypted with a dedicated customer-creds KMS key, and keeps only a reference
on the
CloudAccountrow — the secret value never touches the database and is never returned by any API. The internet-facing API can write but cannot read a stored credential. - Only the broker can decrypt. The KMS key policy allows exactly two principals:
the API (encrypt) and the credential broker Lambda (decrypt). A stolen
secretsmanager:GetSecretValueis inert withoutkms:Decrypt. - Workers get tokens, never keys. At scan time the azure/gcp worker calls the broker, which decrypts the credential and mints a short-lived (~1h) read-only token (an Entra access token for Azure, an OAuth access token for GCP). The durable key never enters a worker; a compromised worker yields at most a 1-hour read-only token for the account it is scanning.
- Read-only by construction. The credential's scope — Azure Reader, GCP Viewer + Security Reviewer — means even a leaked key can only read the customer's cloud, never modify it.
- Provider mechanics. GCP runs
prowler gcpas a subprocess with the token (CLOUDSDK_AUTH_ACCESS_TOKEN). Azure has no CLI token flag, so Prowler's Azure provider is run in-process with a token-credential object. Both parse the same OCSF output intoAssets → Checks → Findingsexactly like AWS.
Code:
app/scanners/{azure,gcp}/,app/scanners/cloud/broker.py,infra/lambda/cred_broker/handler.py,infra/terraform/{kms,lambda}.tf. The full threat model is the platform's Cloud Credential Security design doc.
Trust-chain monitoring
Both sides of the chain are covered by the organization CloudTrail. A metric-filter
alarm fires on an AssumeRole into a customer role without the ExternalId
(a confused-deputy attempt or a regression), routed to #alerts-security. See
Platform Architecture and
Monitoring & Alerting.
Customer-side setup
The customer creates the read-only role once (CloudFormation for a single account, or a StackSet for a whole AWS Organization), trusting their assumer identity + External ID. Step-by-step: Connecting AWS.