ing. The architecture follows a four-stage flow: snapshot collection, constraint modeling, solver execution, and witness parsing.
Infrastructure state must be normalized into a structured, versioned format. Each fact represents a discrete security property: IAM policy statements, SCP boundaries, bucket ACLs, identity federation trusts, and account-level access controls.
interface SecurityFact {
id: string;
resourceType: 'IAM_POLICY' | 'BUCKET_POLICY' | 'SCP' | 'FEDERATION_TRUST';
resourceArn: string;
properties: Record<string, unknown>;
provenance: {
accountId: string;
region: string;
timestamp: string;
source: 'API_SNAPSHOT' | 'CONFIG_STREAM';
};
}
Facts are validated against a strict schema before entering the verification pipeline. This prevents malformed inputs from corrupting the constraint set and ensures provenance tracking for audit trails.
Step 2: Constraint Modeling (SMT-LIB v2 Generation)
The normalized facts are translated into SMT-LIB v2 constraints. Each security property becomes a logical predicate. Trust relationships, policy conditions, and resource boundaries are encoded as quantified formulas.
class ConstraintBuilder {
private constraints: string[] = [];
encodeAccessControl(fact: SecurityFact): void {
const resourceId = `res_${fact.resourceArn.split('/').pop()}`;
const principalVar = `p_${fact.provenance.accountId}`;
this.constraints.push(
`(define-fun ${resourceId} () Resource (Resource "${fact.resourceArn}"))`,
`(define-fun ${principalVar} () Principal (Principal "${fact.provenance.accountId}"))`,
`(assert (implies`,
` (and (has-policy ${resourceId})`,
` (allows-action ${resourceId} "s3:GetObject"))`,
` (not (is-public ${resourceId}))))`
);
}
generateSmtLib(): string {
return [
'(set-logic ALL)',
'(declare-datatypes () ((Resource (Resource (name String))))',
'(declare-datatypes () ((Principal (Principal (id String))))',
...this.constraints,
'(check-sat)'
].join('\n');
}
}
The builder separates declaration from assertion. This keeps the constraint set modular and allows incremental updates when infrastructure changes. SMT-LIB v2 is chosen for solver interoperability: Z3, cvc5, and Yices all natively parse the format without translation layers.
Step 3: Solver Execution & Timeout Management
The generated constraint set is piped to the solver. Production environments must enforce strict timeouts and memory limits to prevent unbounded solving on complex trust chains.
class SolverEngine {
async verify(constraints: string): Promise<VerificationResult> {
const child = spawn('z3', ['-smt2', '-in', '-T:10']);
return new Promise((resolve, reject) => {
let output = '';
child.stdout.on('data', (chunk) => output += chunk);
child.stderr.on('data', (chunk) => reject(new Error(chunk.toString())));
child.on('close', (code) => {
if (code !== 0) return reject(new Error('Solver execution failed'));
resolve(this.parseOutput(output));
});
child.stdin.write(constraints);
child.stdin.end();
});
}
private parseOutput(raw: string): VerificationResult {
if (raw.includes('unsat')) {
return { status: 'SAFE', witness: null, proof: raw };
}
if (raw.includes('sat')) {
return { status: 'VIOLATION', witness: this.extractWitness(raw), proof: raw };
}
throw new Error('Unknown solver state');
}
}
The -T:10 flag enforces a 10-second timeout. Complex IAM chains with deep role assumptions can trigger exponential state exploration. Timeouts prevent pipeline stalls and force architectural review of over-permissive trust boundaries.
When the solver returns sat, the output contains a concrete assignment that satisfies the violation constraints. This witness is parsed into actionable remediation steps.
interface ViolationWitness {
violatingPrincipal: string;
affectedResource: string;
exploitedAction: string;
trustPath: string[];
}
function extractWitness(solverOutput: string): ViolationWitness {
const principalMatch = solverOutput.match(/\(Principal "([^"]+)"\)/);
const resourceMatch = solverOutput.match(/\(Resource "([^"]+)"\)/);
const actionMatch = solverOutput.match(/\(allows-action \([^)]+ "([^"]+)"\)/);
return {
violatingPrincipal: principalMatch?.[1] ?? 'unknown',
affectedResource: resourceMatch?.[1] ?? 'unknown',
exploitedAction: actionMatch?.[1] ?? 'unknown',
trustPath: solverOutput.split('\n').filter(l => l.includes('assume-role'))
};
}
The witness provides exact attribution. Instead of "this configuration resembles a risky pattern," the output states: Principal A assumed Role B via Federation C to access Resource D with Action E. This eliminates triage ambiguity and accelerates remediation.
Architecture Decisions & Rationale
- Decoupled Fact/Constraint Layer: Separating snapshot collection from verification allows independent scaling. Fact extraction can run asynchronously; verification executes synchronously on demand.
- Standard SMT-LIB v2: Avoids vendor lock-in. Solvers can be swapped or run in parallel for cross-verification without rewriting constraint logic.
- Offline-First Execution: The pipeline requires no outbound connectivity. This satisfies air-gapped deployment requirements and eliminates API dependency risks.
- Deterministic Artifact Generation: The fact base and solver output are versioned together. Auditors can replay the exact input against any compliant solver to independently verify the verdict.
Pitfall Guide
1. Incomplete Trust Chain Modeling
Explanation: Modeling only direct IAM policies while ignoring role assumption paths, identity federation, or cross-account trust relationships leaves verification gaps. The solver will return unsat because the violation path was never encoded.
Fix: Explicitly model sts:AssumeRole, sts:AssumeRoleWithSAML, and sts:AssumeRoleWithWebIdentity transitions. Encode trust boundaries as transitive relations in the constraint set.
2. Ignoring Service Control Policies (SCPs)
Explanation: SCPs act as guardrails that override or restrict IAM permissions. Omitting them from the model produces false negatives, as the solver assumes IAM policies are the sole enforcement mechanism.
Fix: Include SCP deny/allow lists as hard constraints. Encode SCP evaluation order and inheritance hierarchy before IAM policy resolution.
3. Treating SAT as a Failure State
Explanation: Teams often misinterpret sat as a solver error or pipeline failure. In formal verification, sat means the property is falsifiable. It is the expected output when a violation exists.
Fix: Treat sat as a successful detection. Parse the witness, map it to remediation playbooks, and track violation frequency as a security metric rather than a pipeline error.
4. Solver Timeout Mismanagement
Explanation: Complex trust chains with circular role assumptions can trigger exponential state exploration. Without timeouts, the solver hangs, blocking CI/CD pipelines and posture dashboards.
Fix: Enforce strict timeouts (-T:10 to -T:30). Implement fallback logic: if timeout occurs, flag the resource for manual review and simplify the constraint set by pruning low-risk trust paths.
5. Fact Provenance Drift
Explanation: Verification results lose audit value if the fact base cannot be traced to a specific infrastructure snapshot. Drift occurs when facts are updated without versioning or timestamp correlation.
Fix: Attach cryptographic hashes to fact exports. Store fact bases in immutable storage with version tags. Require solver runs to reference a specific fact hash for reproducibility.
6. Over-Modeling Behavioral Data
Explanation: Attempting to encode CloudTrail access patterns, user intent, or anomaly baselines into SMT constraints forces deterministic solvers to handle probabilistic data. This degrades performance and produces meaningless results.
Fix: Reserve formal verification for structural, policy-based checks. Route behavioral analysis, natural-language policy interpretation, and intent inference to AI/ML pipelines. Maintain a strict boundary between deterministic and probabilistic workloads.
7. Assuming Cross-Account Isolation by Default
Explanation: Many models assume resources are isolated to their home account. In practice, cross-account roles, shared VPCs, and organization-level SCPs create implicit trust boundaries that violate this assumption.
Fix: Explicitly model cross-account trust relationships. Include organization hierarchy, shared resource identifiers, and federation endpoints in the constraint set. Verify isolation as a property, not an assumption.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Binary policy checks (MFA, Principal:*, encryption) | Formal Verification | Exact predicates require deterministic answers | Near-zero marginal cost |
| Cross-account trust chain analysis | Formal Verification | Transitive relations are mathematically modelable | Flat CPU cost |
| Behavioral access pattern anomaly detection | AI/ML Scanning | Baselines are statistical, not logical | Linear per-query cost |
| Natural-language compliance attestation parsing | AI/ML Scanning | Ambiguous text requires intent inference | Linear per-query cost |
| Air-gapped or classified environments | Formal Verification | No outbound API dependency required | One-time infrastructure cost |
| Continuous posture verification (hundreds of changes/day) | Formal Verification | Deterministic checks run in milliseconds | Flat, predictable cost |
Configuration Template
; SMT-LIB v2 Constraint Template for IAM Policy Verification
; Generated from fact base: obs.v0.1-hash-a1b2c3d4
(set-logic ALL)
(declare-datatypes ()
((Resource (Resource (name String)))
(Principal (Principal (id String)))
(Action (Action (verb String)))))
; Resource declarations
(define-fun bucket-prod-data () Resource (Resource "arn:aws:s3:::prod-customer-data"))
(define-fun role-data-analyst () Principal (Principal "arn:aws:iam::123456789012:role/DataAnalyst"))
; Policy constraints
(assert (has-policy bucket-prod-data))
(assert (allows-action bucket-prod-data (Action "s3:GetObject")))
(assert (allows-action bucket-prod-data (Action "s3:PutObject")))
; Trust boundary constraints
(assert (implies
(and (is-public bucket-prod-data)
(not (has-mfa role-data-analyst)))
(violation-detected)))
; Verification query
(check-sat)
(get-model)
Quick Start Guide
- Initialize Fact Export: Run your cloud provider's configuration API to collect IAM policies, SCPs, and bucket ACLs. Normalize outputs into a schema-validated JSON structure with provenance metadata.
- Generate Constraints: Feed the fact base into a constraint builder that translates each policy statement into SMT-LIB v2 predicates. Append
(check-sat) and (get-model) to the output.
- Execute Solver: Pipe the constraint file to Z3 or cvc5 with timeout flags (
z3 -smt2 -T:15 constraints.smt2). Capture stdout for verdict parsing.
- Parse & Archive: If
unsat, archive the fact hash and solver output as a compliance artifact. If sat, extract the witness, map it to remediation steps, and log the violation for tracking.
- Integrate into CI/CD: Run the pipeline on every infrastructure commit. Block merges that produce
sat outputs for critical security properties. Route non-critical violations to a triage queue with automated remediation playbooks.