Data Sovereignty and Compliance Testing Across Cloud Providers
Data sovereignty requirements are among the most strictly enforced regulations in software: GDPR mandates EU data stays in the EU, certain healthcare data must remain within national borders, and financial data has jurisdiction-specific retention requirements. In a multi-cloud architecture, violating these rules isn't a technical bug — it's a legal liability.
Testing for data sovereignty isn't optional. It's the same category as testing for SQL injection: a failure mode with consequences far beyond "the tests didn't catch it."
What Data Sovereignty Testing Must Verify
Data sovereignty compliance testing must prove four things:
- Data is stored where it's supposed to be stored — EU customer data in EU regions only
- Data doesn't cross borders it shouldn't — no unexpected replication or caching
- Data retention policies are enforced — data deleted when required, retained when required
- Audit trails are complete — every data access and movement is logged
Each of these requires different testing techniques.
Data Residency Validation
The foundational test: verify that data lands in the correct region and never leaves it.
def test_eu_customer_data_stored_in_eu():
"""EU customer data must be stored exclusively in EU regions."""
EU_REGIONS = {
"aws": ["eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1", "eu-north-1"],
"gcp": ["europe-west1", "europe-west2", "europe-west3", "europe-west4", "europe-north1"],
"azure": ["westeurope", "northeurope", "germanywestcentral", "francecent"]
}
# Create a customer record flagged as EU
customer_id = create_eu_customer(
name="Test User",
country="DE",
data_residency_requirement="EU"
)
# Query where each piece of PII is stored
storage_locations = get_data_storage_locations(customer_id)
for data_type, location in storage_locations.items():
provider = location["provider"]
region = location["region"]
assert region in EU_REGIONS.get(provider, []), (
f"EU customer data ({data_type}) stored in non-EU region: "
f"{provider}/{region}. Allowed regions: {EU_REGIONS[provider]}"
)
def test_us_customer_data_does_not_replicate_to_eu():
"""US customer data must not be replicated to EU regions."""
customer_id = create_us_customer(
name="Test User",
country="US",
data_residency_requirement="US"
)
# Wait for any background replication jobs
time.sleep(30)
# Check all storage locations for this customer's data
storage_locations = get_data_storage_locations(customer_id)
EU_REGION_PATTERNS = ["eu-", "europe-", "westeurope", "northeurope", "germany"]
for data_type, location in storage_locations.items():
region = location["region"].lower()
for eu_pattern in EU_REGION_PATTERNS:
assert eu_pattern not in region, (
f"US customer data ({data_type}) found in EU region: {location['region']}. "
"Possible GDPR violation — data crossed borders without authorization."
)Cross-Border Transfer Detection
Data can cross borders in unexpected ways: CDN caching, backup replication, analytics pipelines, logging systems. Test all of them.
class CrossBorderTransferDetector:
"""Monitor data flows for unauthorized cross-border transfers."""
def __init__(self, customer_id: str, allowed_regions: list[str]):
self.customer_id = customer_id
self.allowed_regions = allowed_regions
self.detected_regions = set()
def check_all_data_stores(self):
"""Scan every data store for this customer's data."""
findings = {}
# Check primary database
db_locations = self._check_database()
findings["database"] = db_locations
# Check object storage
storage_locations = self._check_object_storage()
findings["object_storage"] = storage_locations
# Check CDN caches
cdn_locations = self._check_cdn_cache()
findings["cdn"] = cdn_locations
# Check backup storage
backup_locations = self._check_backups()
findings["backups"] = backup_locations
# Check analytics/data warehouse
analytics_locations = self._check_analytics()
findings["analytics"] = analytics_locations
# Check logging systems
log_locations = self._check_logs()
findings["logs"] = log_locations
# Find violations
violations = []
for store_type, locations in findings.items():
for location in locations:
if location["region"] not in self.allowed_regions:
violations.append({
"store_type": store_type,
"region": location["region"],
"data_type": location.get("data_type"),
"violation": f"Customer data found outside allowed regions"
})
return violations
def test_no_unauthorized_cross_border_transfers():
"""EU customer PII must not appear in non-EU data stores."""
customer = create_eu_customer_with_pii()
# Trigger data flows that might inadvertently replicate data
perform_typical_operations(customer)
time.sleep(60) # Allow background jobs to run
detector = CrossBorderTransferDetector(
customer_id=customer.id,
allowed_regions=EU_ALLOWED_REGIONS
)
violations = detector.check_all_data_stores()
assert len(violations) == 0, (
f"Unauthorized cross-border data transfers detected:\n"
+ "\n".join(f" - {v['store_type']}/{v['region']}: {v['data_type']}"
for v in violations)
)Data Retention Policy Testing
Regulations specify both minimum retention (you must keep this for 7 years) and maximum retention (you must delete this within 30 days). Test both.
def test_gdpr_right_to_erasure():
"""Customer data must be deleted from ALL stores within 30 days of deletion request."""
customer = create_eu_customer_with_pii()
customer_id = customer.id
# Create various data artifacts
upload_customer_document(customer_id, b"sensitive document")
record_customer_transaction(customer_id, amount=99.99)
log_customer_activity(customer_id, action="login")
# Submit GDPR deletion request
deletion_request = submit_gdpr_deletion_request(customer_id)
# The request must be acknowledged
assert deletion_request.acknowledged_at is not None
assert deletion_request.expected_completion_date <= datetime.now() + timedelta(days=30)
# After processing, verify deletion
# In tests, we can trigger immediate processing
process_deletion_request(deletion_request.id)
# Check every data store
remaining_data = find_customer_data_in_all_stores(customer_id)
# Some data may be retained in audit logs (lawful retention) — document this
allowed_retained = {
"audit_logs": "Required for 7 years under financial regulations",
"deletion_log": "Required to prove deletion was completed"
}
for store, data in remaining_data.items():
assert store in allowed_retained, (
f"Customer data still present in {store} after GDPR deletion: {data}"
)
def test_financial_data_minimum_retention():
"""Financial transaction records must be retained for at least 7 years."""
# Create a financial record
record_id = create_financial_record(customer_id="test-cust", amount=1000.00)
record_date = datetime.now()
# Attempt to delete before retention period expires
result = attempt_data_deletion(record_id, reason="user_request")
assert result.status == "retention_hold", (
f"Financial record should be under retention hold for 7 years, "
f"got: {result.status}"
)
assert result.hold_until >= record_date + timedelta(days=7 * 365), \
f"Retention hold expires too early: {result.hold_until}"
# Verify the data actually still exists
record = get_financial_record(record_id)
assert record is not None, "Financial record deleted despite retention hold"Audit Trail Verification
Every data access in a regulated system must be logged. Test that the audit trail is complete and tamper-evident.
def test_audit_trail_completeness():
"""Every PII access must produce an audit log entry."""
customer_id = create_eu_customer_with_pii().id
# Perform operations that must be audited
operations = [
("read_profile", lambda: get_customer_profile(customer_id)),
("update_email", lambda: update_customer_email(customer_id, "new@example.com")),
("export_data", lambda: export_customer_data(customer_id)),
("admin_access", lambda: admin_view_customer(customer_id, admin_id="admin-1")),
]
for op_name, operation in operations:
# Record time before operation
before = datetime.utcnow()
operation()
after = datetime.utcnow()
# Find audit log entry
audit_entries = get_audit_log(
customer_id=customer_id,
after=before,
before=after + timedelta(seconds=5)
)
matching = [e for e in audit_entries if e["operation_type"] == op_name]
assert len(matching) >= 1, \
f"No audit log entry found for {op_name} on customer {customer_id}"
entry = matching[0]
assert "actor_id" in entry, f"Audit entry missing actor_id for {op_name}"
assert "timestamp" in entry, f"Audit entry missing timestamp for {op_name}"
assert "ip_address" in entry, f"Audit entry missing ip_address for {op_name}"
def test_audit_log_tamper_evidence():
"""Audit logs must be tamper-evident — modifications must be detectable."""
# Write an audit entry
entry_id = write_audit_log({
"operation": "read_profile",
"customer_id": "cust-123",
"actor_id": "admin-1",
"timestamp": datetime.utcnow().isoformat()
})
# Get the log's integrity hash
original_hash = get_audit_entry_hash(entry_id)
# Attempt to modify the audit log (simulating tampering)
try:
tamper_with_audit_log(entry_id, {"actor_id": "different-admin"})
except PermissionError:
# Best case: direct modification is blocked
return
# If modification was allowed, the hash must have changed
current_hash = get_audit_entry_hash(entry_id)
assert current_hash != original_hash, \
"Audit log was modified without changing its integrity hash — tamper detection broken"
def test_cross_cloud_audit_trail_aggregation():
"""Audit logs from all cloud providers must be aggregated into one queryable store."""
customer_id = create_eu_customer_with_pii().id
# Trigger operations on different cloud providers
access_via_aws_region(customer_id)
access_via_azure_region(customer_id)
# Query centralized audit store
all_entries = query_centralized_audit_log(customer_id=customer_id)
sources = {entry["source_cloud"] for entry in all_entries}
assert "aws" in sources, "AWS operations not in centralized audit log"
assert "azure" in sources, "Azure operations not in centralized audit log"Compliance Testing in CI
Add compliance tests to your CI pipeline — not as a separate quarterly audit, but as code that runs on every deployment.
# .github/workflows/compliance.yml
name: Data Sovereignty Compliance Tests
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * *' # Daily
jobs:
compliance:
runs-on: ubuntu-latest
steps:
- name: Run data residency tests
run: pytest tests/compliance/test_data_residency.py -v
- name: Run cross-border transfer tests
run: pytest tests/compliance/test_cross_border.py -v
- name: Run retention policy tests
run: pytest tests/compliance/test_retention.py -v
- name: Run audit trail tests
run: pytest tests/compliance/test_audit.py -v
- name: Generate compliance report
run: python scripts/generate-compliance-report.py
- name: Store compliance evidence
uses: actions/upload-artifact@v4
with:
name: compliance-evidence-${{ github.sha }}
path: compliance-reports/
retention-days: 365 # Keep for 1 year as audit evidenceHelpMeTest can continuously verify that your compliance-critical endpoints — GDPR deletion APIs, data export APIs, audit log queries — remain functional. When a compliance endpoint breaks, you need to know immediately, not when a regulator asks for a report.
Summary
Data sovereignty compliance testing requires:
- Data residency validation — verify data lands in the correct regions
- Cross-border transfer detection — scan all data stores including CDN and logs
- Retention policy tests — both minimum (must keep) and maximum (must delete)
- Audit trail completeness and tamper evidence verification
- Compliance tests in CI with stored evidence for audit purposes
Compliance failures discovered by regulators are infinitely more expensive than compliance failures discovered by tests. Test your compliance requirements like you test your business logic.