Eight destinations outside the EEA, in one 88-line Terraform file that a platform engineer at an EU SaaS company had already merged and forgotten. The file works. It applies cleanly. terraform validate is happy with it, because terraform validate does not know what a country is.
That file is the sample shipped with EU Data Residency Lint, and it is not a strawman — it is what an assistant writes when you ask for "a Postgres instance with a read replica, backups and logging". Run the linter over it and you get 22 findings: 15 errors and 7 warnings, naming 9 resources.
Here is where the data goes. The Terraform state bucket sits in us-west-2, Oregon. The provider default is us-east-1, so the API log group and the Cognito user pool — names, emails, login events — land in Virginia. The database read replica is in us-east-2, Ohio: a complete second copy of the production rows, sitting in Ohio whether or not anyone ever reads it. The backup vault is ca-central-1, Canada, and the copy action inside the backup plan points at a destination ARN in us-east-1 that nothing in the file spells out as a region. An Azure storage account is in eastus. The invoice PDF function runs in ap-southeast-1, Singapore. The customer upload bucket has location = "US" — a multi-region location that cannot be written into a record at all, because nobody can say which country it means. And the Azure provider is in uksouth, which is lawful only for as long as the UK adequacy decision stands.
Eight destinations. Four of the region strings contain us, so a human reading the diff at 6pm skims straight past them.
The reason free scanners do not catch this is that they are not looking for it. checkov, tfsec and trivy grade encryption, IAM policy, public access and secret handling, and they grade all of it correctly — in us-east-1, with a green tick. None of them ships an EEA region allowlist, because residency is a policy your company holds, not a CVE. A chatbot does not catch it either: it sees the snippet you paste, not the provider alias three files away, not the region buried in a destination ARN, and not the state backend at the top of the repository.
EU Data Residency Lint has 16 rules, and each one carries the article it hangs on. Chapter V, Articles 44 to 46, is the one that makes a copy in Ohio a transfer. Article 30(1)(e) is the one that says your record of processing has to name the third countries. Article 45 is why London and Zurich get a warning instead of an error. Article 83(5) is the number that makes anyone read to the end: the upper fine band is EUR 20 million or 4% of worldwide annual turnover, whichever is higher.
The fix is smaller than the finding list suggests. _fixtures/clean.tf is the same stack rewritten around a single var.eu_region, with a data-classification tag on each store — 0 findings. One variable, one tag convention, and the drift stops.
Linting is free, in the editor or in the browser, and it is a finished job: every finding, its line, the country and the EEA region to use instead. The full version answers the question that comes next, the one an auditor asks in a flat voice — show me the record — by exporting the repository as an Article 30(1)(e) transfer register, one row per resource, region, country and legal basis, as Markdown or CSV. A consultant day spent building that same table by hand starts near $800, and it is stale the next time someone adds a replica.
Free in your browser (the same rules): https://getreadystack.com/tools/eu-data-residency-lint
Licence ($29, once, 7-day refund): https://buy.polar.sh/polar_cl_bZYdC7eAzpJHf1ZuInPSKGUNxok8PCMqFLY2K1orgTJ
Top comments (0)