Skip to content

Engineering / 25 May 2026

Writing Rego for GDPR transfers: a policy your DPO can read

A worked OPA v1 policy for Chapter V routing decisions: adequacy as data, deny reasons a lawyer can audit, and tests that pin the legal intent.

Post record

author
Meera Krishnan
role
Head of Policy Engineering
published
category
Engineering
length
4 min / ~950 words
Key takeaways
  • Keep legal facts such as adequacy lists in data documents, and keep the decision logic in Rego, so a Commission decision means a data update, not a code review.
  • Model the US as a conditional destination: adequacy under the Data Privacy Framework covers only certified organisations.
  • Every deny should return a sentence a DPO can read in an audit log, not a boolean.
  • Write tests that read like the legal position, and run them in CI on every policy change.

Why policy as code, and why Rego

Before I wrote policy for a living I wrote transfer memos. They were careful documents that described which destinations were acceptable for which data, and they had one shared weakness: nothing in production read them. The engineering team implemented their interpretation in an if statement somewhere, and the memo and the code drifted apart the first time anyone touched either.

Open Policy Agent fixes the drift by making the policy the thing that executes. Rego is declarative, which suits legal rules: you state the conditions under which something is allowed or denied, and the engine works out the answer for each input. It is also small enough that a privacy lawyer can learn to read it in a week. I know, because our legal reviewers do. What follows is a trimmed version of how we structure transfer rules, written in OPA v1 syntax, where if, contains and in are keywords with no import needed.

Separate the law from the logic

The first design decision is the one teams most often get wrong. Adequacy decisions change. The Commission adopts new ones, and courts have struck down old ones twice. If the list of adequate countries is hard-coded in a rule, every change becomes a code change with a deploy behind it. Put the facts in a data document instead, versioned and reviewed by legal, and let the rules reference it.

The same goes for subprocessor locations and provider attributes such as zero retention or DPF certification. These come from contracts and due diligence, not from engineers, so they belong in data that the people who own those facts can change. The Rego below reads data.adequacy.countries and expects the router to put the endpoint's country, operator attributes and subprocessor chain into the input.

The input itself is assembled by the router for each candidate endpoint, before any bytes go upstream. It carries the tenant's domicile, the classification of the payload produced by the PII guardrail, and the registry record of the endpoint under consideration. The router asks the policy about each candidate in preference order and takes the first one that is allowed. If none is, the request fails closed with the deny reasons attached, which is a far easier incident to explain than a request that quietly found a way out.

The policy

This is a tenant-level policy for a controller that allows EEA destinations, accepts adequacy destinations, treats the United States as acceptable only where the operator is DPF-certified, and keeps special category data inside the EEA without exception. Every deny carries a reason, and the router writes those reasons into the signed receipt.

rego
package acme.transfers

# EEA: the 27 Member States plus Iceland, Liechtenstein and Norway
eea := {
	"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
	"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
	"PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO",
}

default allow := false

allow if {
	destination_permitted
	count(deny) == 0
}

destination_permitted if input.endpoint.country in eea

# data.adequacy.countries excludes US: its adequacy is partial
destination_permitted if input.endpoint.country in data.adequacy.countries

destination_permitted if {
	input.endpoint.country == "US"
	input.endpoint.operator.dpf_certified == true
}

deny contains "Article 9 data must stay in the EEA under this policy" if {
	input.payload.special_category
	not input.endpoint.country in eea
}

deny contains "endpoint does not guarantee zero prompt retention" if {
	not input.endpoint.zdr
}

deny contains msg if {
	some hop in input.endpoint.subprocessors
	not hop.country in eea
	not hop.country in data.adequacy.countries
	msg := sprintf("subprocessor %s in %s has no transfer basis on file", [hop.name, hop.country])
}

Reading it as a lawyer would

Walk the rules top to bottom and they map onto Chapter V almost sentence by sentence. Transfers inside the EEA are not Chapter V transfers at all. Transfers to a country with an Article 45 adequacy decision need no further authorisation. For the United States, the 10 July 2023 decision covers only organisations certified under the Data Privacy Framework, so certification is a condition, not an assumption. Everything else falls to the default, which is deny.

Two choices here are policy, not law, and the code makes them visible. Keeping Article 9 data inside the EEA is stricter than Chapter V requires; it reflects this controller's risk appetite for health and biometric data in prompts. Requiring zero retention is an operational choice. Writing them as named deny reasons means a reviewer can see exactly where the organisation chose to go beyond the minimum, which is the kind of thing an Article 5(2) accountability file should show.

Notice also what the policy leaves out. There is no rule permitting transfers on the basis of Article 46 standard contractual clauses. That is deliberate for this controller: SCCs for inference would need a transfer impact assessment showing effective supplementary measures for processing in the clear, and their DPO concluded none existed for their providers. A different controller might add a rule that permits SCC destinations listed in a reviewed data document. The point is that the choice is written down where it executes.

  • Default deny: anything the rules do not affirmatively permit is refused.
  • Subprocessors are checked hop by hop, because a compliant first hop can sit on top of a non-compliant second one.
  • Deny reasons are plain sentences, stored verbatim in the receipt.

Tests that pin the legal intent

A policy without tests is a memo with extra steps. Each test below states a legal position in its name and checks it with a minimal input. When someone edits the policy six months from now, the test suite tells them which position they are about to change. Run opa test in CI on every pull request and require a reviewer from legal for any change to the data documents.

rego
package acme.transfers_test

import data.acme.transfers

adequacy := {"CH", "GB", "JP", "KR", "NZ"}

test_us_without_dpf_is_refused if {
	req := {"endpoint": {"country": "US", "zdr": true, "operator": {"dpf_certified": false}, "subprocessors": []}, "payload": {}}
	not transfers.allow with input as req with data.adequacy.countries as adequacy
}

test_health_data_to_switzerland_is_refused if {
	req := {"endpoint": {"country": "CH", "zdr": true, "subprocessors": []}, "payload": {"special_category": true}}
	not transfers.allow with input as req with data.adequacy.countries as adequacy
}

test_frankfurt_zdr_is_allowed if {
	req := {"endpoint": {"country": "DE", "zdr": true, "subprocessors": []}, "payload": {}}
	transfers.allow with input as req with data.adequacy.countries as adequacy
}

What Rego will not do for you

A policy engine enforces the decision you encoded. It does not decide whether the encoded position is lawful for your processing. Whether a given prompt contains special category data is an input the policy trusts, so the quality of your classification upstream sets the ceiling on the quality of the decision. Whether an adequacy country is right for a particular dataset may depend on onward transfer rules in that country, which the list does not capture.

So treat the policy as the executable half of your transfer position, with the DPIA and transfer impact assessment as the reasoning half. When they disagree, one of them is wrong, and you want to find out in code review rather than in a supervisory authority's questionnaire. Our pre-built eu-gdpr-strict pack follows the structure above; most customers start from it and then write their own deny rules for the choices only they can make.

About the author

MK

Meera Krishnan

Head of Policy Engineering, YTT Secunet

Former privacy counsel in Wipro's global DPO office. BA LLB, NLSIU Bengaluru; CIPP/E. Translates Chapter V, the AI Act and sector rules into Rego policy packs.

Meet the team

Sovereign route ready

Stop risking 4% global turnover GDPR fines on AI inference.

Deploy sovereign, cryptographically proven AI routing in 15 minutes.