Skip to main content

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 plain sts:AssumeRole calls; 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_id 1–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 source is recorded as cloud_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 CloudAccount row — 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:GetSecretValue is inert without kms: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 gcp as 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 into Assets → Checks → Findings exactly 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.