Cloud Security Architecture Questions
Designing and reasoning about the security posture of cloud and hybrid infrastructure: the shared responsibility model, network segmentation and boundary design, multi-account and multi-region security architecture, workload identity as an architectural choice, threat modeling a cloud architecture, cloud-specific attack vectors and mitigations, defense-in-depth control selection, secure cloud deployment patterns, and continuous cloud risk assessment and posture. IAM policy authoring, role/trust-policy mechanics, and secrets/credential lifecycle belong to identity-and-access-management; logging-pipeline design and SIEM/detection-rule engineering belong to security-monitoring-and-detection; encryption-key-management mechanics (KMS/CMK/BYOK) belong to data-protection-and-encryption; compliance-framework mapping (SOC2, PCI-DSS, HIPAA, GDPR) belongs to compliance-frameworks-and-certification-standards. This topic keeps identity, logging, or encryption content only when it is one ingredient inside a genuinely multi-control cloud-hardening question, not as a standalone ask.
Perform a threat model for a serverless web application that uses API Gateway (or equivalent), Lambda/Cloud Functions, DynamoDB/Cloud Datastore, and S3/Cloud Storage. Sketch the data flow, enumerate threats to authentication, authorization, data exfiltration, injection, and event source spoofing, and propose mitigations prioritized by risk and effort.
Sample Answer
Direct answer
A serverless web application built on an API Gateway, Lambda/Cloud Functions, a managed NoSQL store (DynamoDB/Cloud Datastore), and object storage has five threat categories worth enumerating explicitly (authentication, authorization, data exfiltration, injection, and event source spoofing), and the highest-leverage mitigations concentrate on the boundary where each event source hands control to a function, since that boundary is where this architecture's trust decisions actually get made.
Structured elaboration
flowchart LR
Client(["Client"]) --> APIGW["API Gateway"]
APIGW --> AuthFn["Auth-checking Lambda / authorizer"]
AuthFn --> BizFn["Business-logic Lambda"]
BizFn --> DB[("DynamoDB / Cloud Datastore")]
Client -->|"file upload"| S3[("S3 / Cloud Storage")]
S3 -->|"object-created event"| ProcFn["Processing Lambda"]
ProcFn --> DB
Data flow. A client calls the API Gateway, which routes to an authorizer function confirming the caller's identity and claims before forwarding to a business-logic function that reads and writes the managed data store; separately, a client uploads a file directly to object storage, which triggers a processing function through an object-created event, writing results back to the same data store.
Authentication threats. Token theft or replay (a stolen JSON Web Token (JWT) or API key reused by an attacker), and a misconfigured authorizer that accepts a token without fully validating its signature, issuer, and expiration; mitigation: short-lived tokens, full signature and claims validation on every request (not cached or skipped for performance), and token binding where the identity provider supports it.
Authorization threats. A business-logic function trusting a client-supplied identifier (a user ID passed in the request body, rather than derived from the validated token) to decide what data to return, letting an attacker request another user's data by simply changing the identifier; mitigation: authorization decisions must derive the acting identity from the validated token itself, never from a client-controlled field, and the data store's own access pattern should be scoped so a function can only query rows matching the authenticated caller's own identity.
Data exfiltration threats. An over-broad execution role on the business-logic or processing function allowing it to read more of the data store or object storage than its actual function requires, so a compromise of that one function (through any other vector) yields broader data access than necessary; mitigation: per-function least-privilege roles scoped to the specific table, partition key range, or bucket prefix each function actually needs.
Injection threats. A managed NoSQL data store is not immune to injection-style attacks: unsanitized user input used to construct a dynamic query expression, or, for a data store with any secondary compute (a stored procedure or attached scripting layer), unsanitized input reaching that layer; mitigation: parameterized query construction (never string-concatenating user input into a query expression) and strict input validation at the API Gateway or authorizer layer before the request ever reaches business logic.
Event source spoofing threats. The processing function's object-created trigger fires based on the object's presence in the bucket, not on any verification of who uploaded it; an attacker with any legitimate upload path (even a narrow, intended-for-a-different-purpose one) can trigger the processing function with an object it never expected, potentially exploiting how that function interprets the object's filename, metadata, or content. Mitigation: the processing function must treat every field of the triggering event, including the object key and any metadata, as untrusted input, and the upload path itself should be scoped (a pre-signed URL limited to a specific key pattern) so an attacker's upload options are as narrow as the legitimate use case actually requires.
Threats prioritized by risk and effort
| Threat | Risk | Mitigation effort | Priority |
|---|---|---|---|
| Authorization trusting a client-supplied identifier | High (direct cross-user data access) | Low (a code-level fix: derive identity from the validated token, not the request body) | Highest: high impact, low effort |
| Over-broad function execution roles | Medium-high (amplifies the impact of any other successful compromise) | Low-medium (IAM policy authoring, one-time per function) | High: compounds every other finding's severity |
| Event source spoofing via upload metadata | Medium (depends on what the processing function does with untrusted metadata) | Medium (requires validating every event field, a real but bounded code change) | Medium-high |
| Token replay/theft | Medium (requires the token to be stolen first, a separate precondition) | Medium (short-lived tokens, binding where supported) | Medium |
| Injection via unsanitized query construction | Medium (depends on the specific data store's query-construction pattern) | Low-medium (parameterized queries, mostly a code-pattern fix) | Medium |
Worked example
The processing function, triggered by an object-created event, is found to trust the uploaded object's filename directly, using it to construct a downstream storage key for the processed result without validation. An attacker who has any legitimate upload path (even one intended only for a narrow use case) uploads a file with a filename containing path-traversal characters, and the processing function's unsanitized use of that filename lets the resulting output land at an unintended location. This is both an event-source-spoofing finding (the function trusted an untrusted event field) and, once traced, reveals the function's own execution role is broader than necessary (it can write to more of the object storage bucket than its actual output path requires), the authorization/data-exfiltration finding compounding the first. The prioritized fix: validate and sanitize the filename before it is used to construct any downstream key (closing the injection-adjacent spoofing vector directly), and separately scope the function's role to only its actual intended output prefix (limiting what even a successful future exploit of this class could reach).
Trade-offs and pitfalls
- The authorization-trusting-a-client-supplied-identifier finding is prioritized highest specifically because it combines the two properties that matter most for triage: high impact (direct cross-user access) and low fix effort (a code-level change, not new infrastructure); a prioritization scheme based on severity alone, without also weighing effort, would not surface this as clearly as the most urgent, most tractable fix.
- Over-broad execution roles are rarely the direct entry point for a compromise, but they are the multiplier on every other finding's severity, which is why they are prioritized highly despite not being the initial vulnerability in the worked example; a report that lists them as a lower-priority, standalone finding misses how much they amplify everything else.
- Event source spoofing is the threat category most specific to this exact architecture (an event-driven serverless pipeline) and the one most likely to be missed by a security review written from a general web-application threat-modeling template, since a traditional template's authentication/authorization/injection categories do not naturally prompt a reviewer to ask "does this function trust something about how it was invoked that an attacker could control."
- The worked example's combined finding (spoofing plus over-broad role) illustrates why threat modeling should trace an actual attack path through the architecture, not just enumerate categories independently; the two findings compound specifically because of how they connect, a relationship an independent, category-by-category review can miss.
You are asked to design a simple VPC subnet layout for a development environment that isolates developer-facing services from production. Sketch (textually) subnets and their purposes, indicating where NAT gateways, public load balancers, and bastion hosts would be placed.
Sample Answer
Direct answer
A development-environment Virtual Private Cloud (VPC) that isolates developer-facing services from production needs the same tiering logic as a production three-tier design, but scaled down and, critically, kept in a genuinely separate VPC (and ideally a separate account) from production, not merely a different subnet range inside a shared network, since the whole point of the isolation is that a mistake or a compromise in the lower-trust development environment cannot reach production through the network at all.
Structured elaboration
Textual subnet layout.
VPC: 10.20.0.0/16 (development environment, separate from production's VPC entirely)
Public subnets (one per AZ):
10.20.0.0/24 (AZ-a) - public ALB, NAT gateway
10.20.1.0/24 (AZ-b) - public ALB, NAT gateway
Private developer-facing app subnets (one per AZ):
10.20.10.0/24 (AZ-a) - developer-facing services (feature-branch deployments, internal tools)
10.20.11.0/24 (AZ-b) - developer-facing services
Private shared-infrastructure subnet:
10.20.20.0/24 - CI/CD runners, internal artifact cache, shared dev tooling
Private database subnet (one per AZ, isolated, no default route):
10.20.30.0/24 (AZ-a) - development database instance
10.20.31.0/24 (AZ-b) - development database instance
Placement of NAT gateways. One NAT gateway per public subnet (per AZ), giving the private application and shared-infrastructure subnets outbound internet access for package downloads and external service calls, without any inbound reachability from the internet, following the same per-AZ pattern (rather than a single shared NAT gateway) used in a production design, since a development environment losing outbound connectivity due to a single NAT gateway failure is still a real productivity cost worth avoiding even if it is not a production incident.
Placement of public load balancers. A single internet-facing (or, more commonly for a development environment, an internally-facing-only) load balancer in the public subnets, fronting developer-facing services; for a genuinely internal-only development environment, this load balancer should be internal-scheme rather than internet-facing at all, reachable only from the corporate VPN or a specific known office/remote-access range, not the open internet, since a development environment is a lower-trust environment specifically because it runs less-reviewed code, which makes leaving it internet-reachable a materially worse decision than leaving production internet-reachable through its own, more carefully reviewed front door.
Placement of bastion hosts. Prefer a session-manager-based administrative access pattern over a traditional bastion host with an open inbound port, for the same reason it is preferable in production: it requires no inbound security-group rule and centralizes session logging; where a traditional bastion is used, restrict it to a narrow administrative CIDR, never the open internet, and treat it as a shared piece of infrastructure in the shared-infrastructure subnet rather than duplicating one per developer.
Isolation from production, structurally, not just by convention. The development VPC has no VPC peering connection, no shared transit gateway attachment, and no route of any kind to the production VPC; if a specific, narrow cross-environment need genuinely exists (a shared artifact registry, for instance), that access should route through a purpose-built, one-way path (a private endpoint to a shared-services account's registry, read-only) rather than a general peering relationship that would expose the whole production network to anything reachable from development.
Trade-offs and pitfalls
- Isolating development from production by subnet range alone, inside the same VPC or the same account, is not real isolation. Two subnets in the same VPC route to each other by default unless a security group or NACL is deliberately configured to prevent it, and that configuration can be loosened by a single, easy-to-make mistake; a genuinely separate VPC, and ideally a separate account, removes that risk at the routing layer itself rather than depending on an access-control rule staying correctly configured indefinitely.
- Guardrail enforcement (Service Control Policies, or an equivalent, restricting what a development account or VPC can be configured to do) matters as much as the initial layout, because a development environment tends to accumulate ad hoc changes over time as developers experiment. Without an enforced guardrail preventing, for instance, a developer from creating a new peering connection to production, the careful initial isolation can erode gradually and invisibly.
- The bastion-versus-session-manager choice matters here for the same reason it matters in production, and arguably more, since a development environment is a more attractive target precisely because it typically has weaker controls than production and can be a stepping stone toward it if the isolation above is ever imperfect. A session-manager-based approach's zero-open-inbound-port property is a meaningfully stronger default in exactly the environment most likely to have an accidental gap elsewhere.
- A shared-infrastructure subnet hosting CI/CD runners is a genuine, if narrow, risk concentration point, since a compromised runner potentially has credentials to deploy to multiple developer environments at once; scoping runner credentials narrowly (per-project or per-pipeline, not one broad shared credential) limits how far a single compromised runner's access actually reaches, even within the development environment's own boundary.
Operationalize security checks into your Terraform pipeline. Define where and how you'll run static analysis, policy-as-code (OPA/Sentinel), secrets scanning, and drift detection. Describe enforcement models (preventive gate vs post-apply remediation), how to surface failures to developers, and rollback or remediation strategies when insecure resources are introduced.
Sample Answer
Direct answer
Operationalizing security checks into a Terraform pipeline means running four distinct kinds of check (static analysis, policy-as-code, secrets scanning, drift detection) at the points in the pipeline where each is cheapest to act on, static analysis and secrets scanning as fast, blocking pre-merge gates; policy-as-code as a preventive gate at plan time for high-risk findings and a post-apply remediation path for lower-risk ones; and drift detection as a continuous, out-of-band check catching what never went through the pipeline at all.
Structured elaboration
Static analysis (Checkov, tfsec, or an equivalent). Runs on every pull request against the raw Terraform files, catching known-bad resource patterns (a public storage bucket, a wildcard identity and access management (IAM) policy) before a human reviewer even looks at the diff; this is the fastest and cheapest check to run, since it does not require a live plan against cloud credentials, and it should block the pull request from merging on a finding above an agreed severity.
Policy-as-code (Open Policy Agent (OPA)/Conftest, or HashiCorp Sentinel). Evaluates the actual terraform plan output, catching misconfigurations that only resolve once variables, modules, and data sources are fully computed, which static analysis alone can miss. This is where the preventive-gate-versus-post-apply-remediation distinction matters most: a policy violating a hard organizational rule (a public database, disabled encryption) blocks the apply outright as a preventive gate; a policy flagging a softer, more judgment-dependent finding (an unusually broad but not obviously wrong permission scope) can instead allow the apply to proceed while automatically opening a tracked remediation ticket, since blocking every borderline finding trains developers to treat the gate as an obstacle rather than a signal.
Secrets scanning (gitleaks, truffleHog, or an equivalent). Runs on every commit, not just Terraform files specifically, catching a credential accidentally committed into a .tf file, a terraform.tfvars, or anywhere else in the repository; like static analysis, this is a fast, pre-merge, blocking check, since a leaked secret is unambiguous and needs no judgment call about severity.
Drift detection. A scheduled, continuous check (comparing the actual deployed state against what Terraform's own state file or the last applied configuration describes) that catches changes made outside the pipeline entirely, a manual console edit during an incident, a change applied by a different tool; this is the layer that catches what the other three, all pipeline-triggered, structurally cannot see, since they only run when something goes through the pipeline.
Enforcement models: preventive gate versus post-apply remediation
A preventive gate blocks the specific change from being applied at all until the finding is resolved, appropriate for anything violating a hard, non-negotiable rule (public data exposure, disabled encryption on a resource type the organization has decided always requires it). Post-apply remediation allows the change to proceed but immediately opens a tracked, owned finding with a service-level agreement (SLA), appropriate for lower-confidence or more context-dependent findings where blocking would create more false-positive friction than the finding's own risk justifies. The choice between the two should be a deliberate, documented mapping from finding type to enforcement model, not a blanket "block everything" or "warn on everything" default; a blanket-block posture on every finding, however minor, is the single most common way a security gate loses developer trust and gets routed around.
Surfacing failures to developers
A blocking finding needs to appear directly in the pull request or plan output, with a specific, actionable message (which resource, which rule, what would satisfy it), not a generic "policy violation" that forces the developer to go find the policy definition themselves to understand what is wrong; a gate that blocks without explaining how to fix it trains developers to request an override rather than actually resolve the finding.
Rollback or remediation when an insecure resource is already introduced
For a preventive-gate violation, the fix is straightforward: the change never applied, so there is nothing to remediate, only the pull request to correct and resubmit. For a post-apply finding, or for a drift-detected out-of-band change, remediation means either an automated fix (for the narrow class of unambiguous findings safe to auto-remediate) or a tracked ticket with an SLA and a named owner; a rollback (reverting to the prior Terraform state and re-applying) is appropriate specifically when the insecure resource has not yet been in a compliant state at all and reverting is safer than forward-fixing, but a rollback that itself has not been tested against the current state can cause a worse outage than the finding it was meant to fix, so it should not be the default response without that verification.
Worked example
A developer's pull request adds a new S3 bucket with a wildcard IAM policy attached. Static analysis flags the wildcard policy immediately on pull-request creation, blocking merge with a message naming the specific resource and the specific rule violated, along with a link to the organization's least-privilege policy-writing guide. The developer fixes the policy and re-pushes; the updated pull request passes static analysis and merges. Separately, an unrelated change to a different resource's terraform plan shows a security group opening a database port to a specific administrative CIDR range, a legitimate but unusually broad grant that policy-as-code flags as a softer finding; rather than blocking, the pipeline allows the apply and automatically opens a ticket for the security team to review within an agreed SLA, since this finding requires judgment about whether the CIDR range is appropriate, not an unambiguous violation. Three weeks later, drift detection flags that same security group's rule has been further widened directly through the console during an unrelated incident, a change that never went through the pipeline at all and that neither static analysis nor policy-as-code could have caught, since both only evaluate changes that pass through Terraform.
Trade-offs and pitfalls
- A blanket preventive-gate posture on every finding, however minor, is the single most common way a Terraform security pipeline loses developer trust, since a team that finds every genuinely borderline finding treated with the same severity as a public bucket eventually starts requesting overrides reflexively rather than engaging with each finding on its actual merits; the deliberate preventive-versus-remediation mapping in this design exists specifically to avoid that outcome.
- Drift detection is the layer most often under-invested in, because it does not fit neatly into the pull-request workflow the other three checks live in, and a team that builds excellent pre-merge gates while neglecting drift detection has closed the pipeline-triggered attack surface while leaving the out-of-band one wide open, exactly the gap the worked example's third finding demonstrates.
- A rollback response to a post-apply finding needs to be verified against the current state before being treated as the default remediation, since a stale or untested rollback can itself cause an outage worse than the finding it addresses; forward-fixing (applying a new, corrected configuration) is often the safer default, with rollback reserved for cases where the insecure resource has genuinely never been in a compliant state to return to.
- The developer-facing message quality (naming the specific resource, rule, and fix) is easy to treat as a minor polish item relative to the underlying detection logic, and it is actually a significant driver of whether the gate gets engaged with or routed around; a technically correct check with an unhelpful failure message delivers less real security value than a slightly less sophisticated check with a genuinely actionable one.
You are reviewing an infrastructure-as-code repository (Terraform/CloudFormation) for a production cloud environment. List the most common high-risk misconfigurations you would look for across IAM, storage, networking, and compute. Also explain how you would automate detection of these misconfigurations in CI/CD before changes reach production.
Sample Answer
Direct answer
A production infrastructure-as-code (IaC) review should walk the same four surfaces every time: identity and access management (IAM), storage, networking, and compute, because that is the order in which a real breach typically chains (a broad credential finds an open door, and an open door leads to unencrypted data). Catching these at review time is necessary but not sufficient: the same checks need to run automatically on every pull request, not just when a human remembers to look.
Structured elaboration
| Category | High-risk misconfiguration to look for | Why it matters |
|---|---|---|
| IAM | Wildcard actions or resources ("Action": "*", "Resource": "*"); trust policies allowing AssumeRole from "Principal": "*"; missing multi-factor authentication (MFA) condition on privileged roles; static long-lived access keys checked into the repository | Turns any single compromised identity into an account-wide foothold |
| Storage | Bucket ACLs or policies allowing public read/write; Block Public Access disabled; missing default encryption; missing versioning on buckets holding data that must survive accidental deletion | Direct data exposure or irreversible data loss, often the visible symptom of a breach even when the entry point was elsewhere |
| Networking | Security groups with ingress from 0.0.0.0/0 on management ports (22, 3389); overly permissive network access control lists (NACLs); VPC (Virtual Private Cloud) flow logs disabled; resources placed in a public subnet with no clear reason | Widens the reachable surface for the earlier two categories to be exploited from the internet |
| Compute | Instances with unnecessary public IPs; hard-coded credentials in user-data or launch templates; instance metadata service left without http_tokens = "required" (IMDSv2 (Instance Metadata Service version 2) not enforced); containers configured to run as a privileged user | Gives an attacker who reaches a compute resource a path to the temporary IAM credentials or secrets on that host |
Worked example
The Terraform snippet below intentionally seeds one misconfiguration from each category; it is syntax-valid HashiCorp Configuration Language (HCL) against the AWS provider (validated with terraform validate), and each block is annotated with what a reviewer, or an automated check, should flag:
# IAM: wildcard action + wildcard resource
resource "aws_iam_policy" "too_wide" {
name = "app-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "*" # flag: no wildcard actions
Resource = "*" # flag: no wildcard resources
}]
})
}
# Storage: bucket has no Block Public Access resource attached
resource "aws_s3_bucket" "app_data" {
bucket = "example-app-data-bucket"
# flag: missing aws_s3_bucket_public_access_block, missing
# aws_s3_bucket_server_side_encryption_configuration
}
# Networking: SSH open to the entire internet
resource "aws_security_group" "app" {
name = "app-sg"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # flag: should be a bastion/VPN CIDR only
}
}
# Compute: IMDSv2 not enforced (http_tokens left at its default "optional")
resource "aws_instance" "app" {
ami = "ami-0123456789abcdef0"
instance_type = "t3.micro"
metadata_options {
http_endpoint = "enabled"
# flag: http_tokens = "required" is missing
}
}
Automating detection in CI/CD
- Static analysis on every pull request. A policy-as-code scanner (Checkov, tfsec, or Terrascan) runs against the raw HCL before a human ever reviews it, catching exactly the four patterns above by pattern-matching the resource configuration, not by executing anything.
- Plan-time policy gate. Beyond static patterns, evaluate the actual
terraform planJSON output with a policy engine (Open Policy Agent (OPA)/Conftest, or a managed equivalent such as HashiCorp Sentinel) so the check sees the fully resolved configuration, including values coming from variables or modules that a purely static scan might miss. - Secret scanning on the IaC repository itself. A tool such as gitleaks or truffleHog run in the same pipeline catches a credential accidentally committed into a
.tffile or aterraform.tfvars. - Post-deploy drift detection. A pipeline gate only catches what goes through the pipeline; a scheduled cloud-native check (AWS Config managed rules, or an equivalent cloud security posture management (CSPM) tool) catches the same four categories of misconfiguration when they are introduced through the console instead of through IaC.
Trade-offs and pitfalls
- A pipeline gate that only runs against production IaC misses the source of the problem. Misconfigurations are frequently written first in a development or staging module and then copied into production later; the same static and policy-as-code checks need to run against every environment's plan, not just the one that matters most.
- Over-strict gates get bypassed. If a policy-as-code check blocks a legitimate, reviewed exception (a genuinely public documentation bucket, for instance) with no override path, teams learn to route around the pipeline instead of fixing the finding; a documented, time-boxed exception mechanism keeps the gate credible.
- Static analysis alone cannot see values resolved at plan time from a module or a data source, which is why the plan-time policy gate is a separate, necessary layer rather than a duplicate of the static scan.
Walk through a threat model for a compromised dependency in the IaC toolchain that injects malicious resources during deployment. Identify the entry vectors and potential impact, then propose preventive and detective controls across the build, registry, and deployment stages.
Sample Answer
Direct answer
A compromised dependency in the infrastructure-as-code (IaC) toolchain that injects malicious resources during deployment is a supply-chain threat model with the same shape as a compromised application-code dependency, but with a categorically worse blast radius, since the "code" being tampered with does not just run in an application, it directly provisions and modifies cloud infrastructure, meaning a successful compromise here can create a persistent, infrastructure-level backdoor rather than a bounded application-level one. Controls need to exist at build, registry, and deployment, since a compromise at any single stage that goes unchecked at the next stage still reaches production.
Structured elaboration
Entry vectors. A malicious or hijacked third-party Terraform provider or module pulled from a public registry, its own maintainer account compromised, or a legitimate-looking but never-actually-reviewed new module; a compromised upstream dependency of a provider plugin itself (the provider binary's own build chain being tampered with, several supply-chain layers removed from the IaC author's own code); and a compromised internal module registry (if the organization runs its own private module registry, a compromise there could tamper with a module every internal team trusts by default, a single point of leverage across the whole organization).
Potential impact. A tampered module or provider can silently inject additional resources beyond what the visible Terraform configuration describes (a hidden IAM (identity and access management) role with broad permissions, a backdoor security-group rule, an additional compute resource under attacker control), modify the behavior of resources the configuration does appear to describe (weakening an encryption setting, widening a network rule), or exfiltrate the plan/apply's own state (which frequently contains sensitive values, credentials, connection strings) to an external destination during execution.
Preventive controls, by stage.
- Build stage: pin every provider and module to an exact, hash-verified version (never a floating version range), and generate and review a dependency inventory (a Software Bill of Materials (SBOM)-equivalent for the IaC toolchain itself) so a compromise of any specific dependency version is a known, trackable event rather than silently absorbed into "whatever the latest version happened to be" at build time.
- Registry stage: for any module sourced from a public registry, mirror it into an internally-controlled, reviewed registry rather than pulling directly from the public source on every build, so a compromise of the public registry after the organization's own review does not automatically propagate; for an internal module registry, apply the same access-control and change-review discipline used for any other production-critical system, since it is now exactly that.
- Deployment stage: run
terraform planoutput through a policy-as-code check before anyapply, specifically looking for resources or attributes not accounted for in the visible configuration diff, catching an injected resource even if the module or provider that introduced it was not independently caught at the build or registry stage.
Detective controls, by stage.
- Build stage: continuous re-scanning of pinned dependency versions against newly-disclosed vulnerability and compromise databases, since a dependency that was clean when pinned can be revealed as compromised later.
- Registry stage: monitoring for any change to a mirrored or internal module's content hash, flagging drift from the last-reviewed version even if the change technically came through an otherwise-legitimate update path.
- Deployment stage: post-apply drift detection (the same continuous, out-of-band comparison against the pipeline's own last-known-good state), catching a resource that was injected during an apply and does not match what the reviewed configuration describes, the layer that catches what a plan-time-only check could still miss if the tampering happened during the apply execution itself rather than being visible in the plan.
Worked example
An organization's Terraform configuration depends on a popular, publicly-sourced module for provisioning a standard networking pattern. The module's maintainer account is compromised, and a new version is published that looks functionally identical but silently adds an additional, broadly-permissioned IAM role to every deployment using it. Because the organization pins module versions to an exact, hash-verified reference rather than a floating range, this specific compromised version is never automatically pulled into an existing pipeline, closing the entry vector for currently-deployed infrastructure. A separate team, onboarding a new service and pulling the module fresh, would have picked up the compromised version had the build-stage dependency-scanning not flagged the version's hash mismatch against a subsequently-published community advisory, catching it before that team's own apply ever ran. For a hypothetical case where the compromised version had been pulled and applied before detection, the deployment-stage drift-detection control would separately have flagged the unexpected, undocumented IAM role appearing in the account, a second, independent layer that does not depend on the build-stage scan having caught it first.
Trade-offs and pitfalls
- Pinning to exact, hash-verified versions is the single highest-leverage control in this entire threat model, and it is also the control most often weakened for convenience, since floating version ranges reduce the maintenance burden of manually bumping pinned versions; the worked example's "closes the entry vector for currently-deployed infrastructure" outcome depends entirely on this discipline being followed consistently, not adopted once and eroded later.
- Mirroring public modules into an internally-controlled registry adds real process overhead (someone has to review and promote each update) and is easy to treat as a bottleneck worth removing under delivery pressure, but it is precisely the layer that stops a compromise of the public source from automatically propagating the moment it happens, rather than only after the organization's own next scheduled review.
- A plan-time policy-as-code check that only looks for known-bad resource patterns (a public bucket, a wildcard IAM policy) will miss a genuinely novel, unexpected resource injected by a compromised dependency, since the injected resource may not itself match any previously-known-bad pattern; the check needs to specifically flag resources or attributes NOT accounted for in the reviewed configuration's own diff, a different and complementary detection logic from pattern-matching known-bad configurations.
- Detective controls at the deployment stage (drift detection) are the safety net for everything the preventive controls at build and registry might miss, and treating them as optional once build and registry controls are in place underestimates how a sophisticated, targeted compromise could specifically be designed to evade exactly those two earlier layers; the three-stage design's real strength is that each stage's detection does not depend on the prior stage having caught the compromise first.
Unlock Full Question Bank
Get access to all Cloud Security Architecture interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.