Infrastructure-as-code was supposed to make cloud configuration reviewable, version-controlled, and predictable. In practice, the resources running in AWS, Azure, or GCP at 3 a.m. are frequently not the resources described in the last terraform apply or CloudFormation stack update. HashiCorp ships a dedicated command for this exact problem — terraform plan -refresh-only -detailed-exitcode, which returns exit code 0 for no drift, 1 for an error, and 2 when the live state has diverged from the declared configuration — and yet open issues against the Terraform CLI as recently as 2026 (hashicorp/terraform #36403 and #35117) document cases where that exit code behaves inconsistently in -refresh-only mode. AWS built an equivalent capability directly into CloudFormation: its native Drift Detection feature walks every resource in a stack and reports a per-resource status of IN_SYNC, MODIFIED, or DELETED. Both tools exist because drift is common, not exceptional — a security group rule opened during an incident and never backported to code, an IAM policy widened by a script, an S3 bucket's public-access block flipped off in the console and forgotten. None of that shows up in a pull request. This post covers why drift happens, how to detect it in both major IaC ecosystems, and what a remediation workflow looks like when the fix has to happen faster than the next scheduled apply.
What actually causes infrastructure drift?
Drift happens whenever a resource is changed by any path other than the IaC pipeline that declared it. The most common sources are manual console or CLI edits made under time pressure — a responder disabling a security group ingress rule directly in the AWS console during an active incident, intending to fix it in Terraform "after the fire is out," and never doing so. Auto-scaling and managed services introduce a second category: an Auto Scaling Group replacing instances with a slightly different launch template, or AWS Backup silently modifying retention settings on a resource Terraform also manages. A third source is other automation tooling — a separate CI job, a security team's remediation script, or a third-party SaaS integration — writing to the same resources IaC owns, without updating the state file. Each of these is individually reasonable and often necessary in an emergency. The security problem is that none of them are visible in code review, none require a second approver, and none get flagged unless something is actively comparing declared state to live state on a recurring basis.
Why is drift a security problem and not just an operational one?
Drift is a security problem because the properties security teams rely on for posture — least-privilege IAM, closed ports, private storage — are exactly the properties most likely to be changed manually under pressure and least likely to be reverted. A widened IAM policy, a reopened port, or a bucket with public access re-enabled looks identical to a compliant resource in the CI/CD pipeline's eyes, because the pipeline only checks the code, not the cloud. The Capital One breach in 2019 is a useful illustration of the underlying risk pattern even though it was not a drift incident specifically: an over-privileged IAM role (ISRM-WAF-Role) combined with a misconfigured web application firewall let an attacker use server-side request forgery to pull credentials from the EC2 instance metadata service and exfiltrate more than 100 million customer records from S3. Analyses of the breach, including case studies published by ACM and MIT researchers, describe it as the product of roughly five compounding control failures. Whether an overprivileged role originates from a bad initial design or from drift introduced after the fact, the live permission boundary — not the Terraform file describing it — is what an attacker actually encounters.
How does Terraform's native drift detection work, and where does it fall short?
Terraform's -refresh-only plan mode queries each provider's API for the current state of every managed resource, compares it to the last recorded state file, and reports the difference without proposing to apply anything — it's read-only by design, which makes it safe to run on a schedule against production. Piped through -detailed-exitcode, it returns a clean 0/1/2 signal that CI pipelines can gate on. Terraform Cloud and Terraform Enterprise build on this with a scheduled drift-detection workflow that runs -refresh-only plans automatically and posts results without a human needing to remember to invoke it. The gap teams hit in practice is twofold: refresh-only mode only detects drift in resources Terraform already manages — anything created outside IaC entirely (a security group spun up by hand and never imported) is invisible to it — and the open GitHub issues noted above document real cases where the exit code doesn't reliably distinguish "drift found" from "no drift" in certain provider and module configurations. Treat the exit code as a strong signal worth alerting on, not an infallible gate.
How does CloudFormation's drift detection differ from Terraform's approach?
CloudFormation's Drift Detection is a first-party AWS API rather than a CLI flag layered on top of a state file, and it can be invoked on a whole stack or a single resource through the console, the CLI (aws cloudformation detect-stack-drift), or the API. It returns a per-resource StackResourceDriftStatus of IN_SYNC, MODIFIED, DELETED, or NOT_CHECKED, along with the specific property-level differences for anything marked MODIFIED — useful when you need to know not just that a security group changed but which rule changed. Because it's native to the AWS control plane, it doesn't depend on a local state file being current or a Terraform provider version supporting a given resource type, which is an advantage for teams running mixed IaC or migrating between tools. The tradeoff is scope: drift detection is scoped to CloudFormation-managed stacks, so resources created by Terraform, the console, or another AWS account entirely still need a separate detection path — no single native tool covers a mixed-IaC estate end to end.
What does a practical drift remediation workflow look like?
A workable remediation workflow treats drift detection as a recurring, scheduled control rather than a manual check run only during audits. Run -refresh-only plans or CloudFormation drift detection on a fixed cadence — daily for production accounts is a reasonable baseline — and route any exit-code-2 or MODIFIED result to the same alerting channel used for security findings, not a separate infrastructure-only queue. When drift is found, the response depends on intent: if the manual change was a legitimate, necessary fix (the incident-response security-group edit), the correct remediation is importing that change back into the IaC source and re-applying, so the next plan reflects reality instead of reverting a fix nobody meant to lose. If the drift was unintended or unauthorized, the correct remediation is reverting the live resource to match declared state via a normal apply. Both paths require someone to look at the diff and make that call — automatically reconciling in either direction without review risks silently re-opening a security gap or silently destroying an emergency fix. The Codecov Bash Uploader compromise in 2021, in which attackers modified a CI script between January 31 and April 1 to exfiltrate environment variables — potentially including AWS credentials — from roughly 23,000 customers' pipelines, is a reminder that the CI/CD system running your apply and plan commands is itself part of the security boundary and needs credentials scoped and rotated accordingly.
What should teams prioritize if they can only do one thing?
If a team can only add one control, it should be scheduled, alerting-integrated drift detection on the resources that carry the most blast radius — IAM roles and policies, security groups, S3 bucket policies and public-access settings, and KMS key policies — rather than attempting full-estate coverage on day one. These are the resource types most likely to be touched manually during an incident and most likely to matter if they silently diverge afterward. Pairing -refresh-only scheduled runs (or Terraform Cloud's built-in drift workflow) with CloudFormation's native drift detection for any CloudFormation-managed stacks covers both major IaC ecosystems without requiring a third-party tool. The open exit-code inconsistencies in newer Terraform versions are a reason to treat any nonzero result as worth a human look rather than trusting the binary signal blindly, but they are not a reason to skip the control — undetected drift in a high-privilege IAM policy is a far larger risk than an occasional false positive from a CLI flag.