Architecture of a Self-Hosted CSPM
How a self-hosted CSPM is built: collectors and credentials, the graph store, the policy engine, and multi-tenant isolation—not a recap of which scanner to buy.
A SaaS CSPM is a product. A self-hosted CSPM is a system you operate: processes that call cloud APIs, a store that can answer reachability, a policy engine that emits findings, and isolation so customer A cannot read customer B’s IAM. This is that architecture. It is not a shopping list of scanners—that survey is open source CSPM and CNAPP tools in 2026. The schema we care about is the graph. Code: OpenSourceOM core.
cloud APIs
→ collectors (read-only creds, per tenant)
→ ingest queue
→ graph store (nodes: resource, identity, net)
→ policy engine (checks + path queries)
→ findings / tickets
If policy runs on flattened JSON dumps and never walks edges, you rebuilt Prowler with extra YAML, not a CSPM that can rank.
Collectors and credentials
Collectors are workers that list and describe. They are not remediation bots.
Credential shape
| Cloud | Collector identity | Bound to |
|---|---|---|
| AWS | IAM role assumed via Org account or StackSet | SecurityAudit + extra read (ec2:Describe*, iam:Get*, s3:GetBucketPolicy) — no *Write* |
| Azure | Management group-scoped user-assigned MI or app | Reader + Security Reader + Resource Graph; never Owner |
| GCP | WIF from your collector project | roles/viewer + roles/iam.securityReviewer on the org; no setIamPolicy |
Store the secret as a K8s SA + IRSA / Workload Identity / federated credential, not an access key in a ConfigMap. Rotate by replacing the role trust, not by emailing a new JSON key.
What to collect (minimum viable graph)
- Inventory + public exposure flags (IPs, bucket policies, firewall rules)
- IAM bindings and resource policies (the edges)
- Kubernetes: API resources you can get with a tightly scoped SA (not kube-system dump of secrets)
What not to collect until you have a legal and threat model: disk snapshots, object contents, packet captures. CSPM is configuration. DSPM is a different collector.
Failure mode: one AWS role in the management account that can sts:AssumeRole into every member with AdministratorAccess “to make onboarding easy.” That role is a shadow org-admin. Use a dedicated security account, StackSets that create a read-only member role, and a trust policy that names the collector role only.
Failure mode: collectors in the same cluster namespace as the UI, with the same SA. A SSRF in the dashboard then has cloud-read on the estate. Separate namespaces, NetworkPolicies, no cloud creds on the API pods.
Quota: paginate, backoff, cache etags. A naive Describe* loop across 200 accounts will get you throttled and a partial graph that looks like “those accounts are clean.”
Graph store
Inventory without edges is a CMDB. The store must hold:
- Nodes: account/subscription/project, resource, principal, policy document, finding
- Edges:
CAN_ASSUME,CAN_REACH(SG/NSG),HAS_POLICY,EXPOSES,RUNS_IN
Writes are idempotent upserts keyed by cloud ARN/RID. Deletes are tombstones until the next full reconcile, or you will “lose” a public bucket that the API skipped.
Query examples you should be able to run without exporting CSV:
- Internet-facing compute that
CAN_ASSUMEa role withs3:GetObjecton a labeled prod bucket - Principals with
iam:PassRoleto an admin-equivalent role
That is attack path analysis as a workload on this store, not a separate product pitch.
Indexes: (tenant_id, resource_arn), (tenant_id, edge_type, from, to). Missing tenant_id on every key is how multi-tenant isolation fails in the next section.
Backup the graph like a production database. A CSPM that cannot restore last night’s inventory is not an audit trail.
Policy engine
Policy is a function: (graph slice) → findings.
Keep two families:
- Asset checks — “this bucket policy allows
Principal: *” (CSPM classic) - Path checks — “this check is true AND an ingress edge exists” (what SaaS CNAPP sells)
Engines you might plug in (names only; see the 2026 survey for the landscape): Rego against a JSON export, CEL, or native queries in the graph database. The architecture requirement is the engine reads the graph, not a second copy of the API that drifted.
Run policy incrementally: IAM change on one role invalidates path queries that touch that node, not the entire org. Full-estate recompute nightly is fine as a consistency check; five-minute full recompute is a bill and a lag.
Findings need: control id, resource id, tenant, first_seen, evidence (the edge list). Evidence is how you avoid black-box scores.
Failure mode: policy in the collector (“if public, emit”). Then you cannot ask a new path question without shipping a new collector. Collect raw; decide later.
Multi-tenant isolation
Whether tenants are “customers” or “business units,” treat them as hostile to each other.
| Layer | Rule |
|---|---|
| Credentials | Collector role per tenant (or per cloud org), not one super-role with a string filter |
| Ingest | Queue partition / topic per tenant; no shared “latest resources” table without tenant_id |
| Store | Every query includes tenant predicate; row-level security or separate keyspaces |
| Policy | Packs may be global; results are tenant-scoped |
| UI/API | Authz on tenant id from the session, not from a query parameter the client chose |
| Ops | Break-glass to read another tenant is logged and time-boxed |
Shared-kernel, separate-data is normal. Shared-data with a WHERE tenant= you remember to add is a leak waiting for one JOIN.
If you run OpenSourceOM core, assume the graph schema is tenant-aware from day one; bolting it on after the first external user is a migration you will not finish.
Kubernetes: collectors for tenant A must not mount tenant B’s cloud secret. That is namespace + ExternalSecrets + admission, not a comment in Helm.
Checklist
- Collector identities are read-only, federated, per tenant/org
- Graph upserts include
tenant_idon every node and edge - Policy reads the graph; collectors do not decide severity
- Path queries exist as first-class jobs, not only CIS asset checks
- Incremental invalidation plus a nightly reconcile
- UI cannot select another tenant’s id; collector SAs are not on API pods
Related: Open source CSPM and CNAPP tools · The graph · CSPM vs CNAPP · OpenSourceOM core