AWS Security: GDPR Compliance Checklist
A comprehensive guide to implementing GDPR-compliant AWS infrastructure for EU organizations
Introduction
The General Data Protection Regulation (GDPR) fundamentally changed how organizations handle personal data in the European Union. For companies using AWS, achieving and maintaining GDPR compliance requires a systematic approach combining AWS security services, proper architecture, and operational processes.
This guide provides a practical checklist for implementing GDPR compliance on AWS, with code examples and automation scripts tailored for the Dutch and EU market.
Understanding GDPR on AWS
The Shared Responsibility Model
AWS Responsibility (Security OF the Cloud):
- Physical infrastructure security
- Network infrastructure
- Hypervisor and hardware
- AWS services compliance certifications
Your Responsibility (Security IN the Cloud):
- Data encryption and protection
- Access management
- Network security configuration
- Compliance with GDPR requirements
- Data subject rights implementation
Key GDPR Principles for AWS
- Lawfulness, Fairness, and Transparency
- Purpose Limitation
- Data Minimization
- Accuracy
- Storage Limitation
- Integrity and Confidentiality
- Accountability
GDPR Compliance Checklist
1. Data Discovery and Classification
Identify Personal Data:
- Map all data stores containing personal data
- Classify data sensitivity levels
- Document data flows and processing activities
import boto3
import json
# Use AWS Macie for automated data discovery
macie = boto3.client('macie2', region_name='eu-west-1')
# Create classification job for S3 buckets
def create_classification_job():
response = macie.create_classification_job(
jobType='ONE_TIME',
name='GDPR-Data-Discovery',
s3JobDefinition={
'bucketDefinitions': [
{
'accountId': '123456789012',
'buckets': ['customer-data-bucket', 'analytics-bucket']
}
]
},
customDataIdentifierIds=[],
managedDataIdentifierSelector='ALL',
tags={
'Purpose': 'GDPR-Compliance',
'DataClassification': 'Personal-Data'
}
)
return response['jobId']
job_id = create_classification_job()
print(f"Classification job created: {job_id}")
Tag Resources with Data Classification:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
// Create bucket with GDPR classification tags
const customerDataBucket = new s3.Bucket(this, 'CustomerData', {
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
lifecycleRules: [
{
expiration: cdk.Duration.days(365),
noncurrentVersionExpiration: cdk.Duration.days(30),
},
],
});
cdk.Tags.of(customerDataBucket).add('DataClassification', 'Personal');
cdk.Tags.of(customerDataBucket).add('GDPR', 'True');
cdk.Tags.of(customerDataBucket).add('DataResidency', 'EU');
2. Data Residency and Localization
Enforce EU-Only Regions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonEURegions",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"eu-west-1",
"eu-west-2",
"eu-west-3",
"eu-central-1",
"eu-north-1"
]
}
}
}
]
}
Implement with AWS Organizations SCP:
# Create Service Control Policy for data residency
aws organizations create-policy \
--name "EU-Data-Residency-Policy" \
--description "Restrict resources to EU regions only" \
--type SERVICE_CONTROL_POLICY \
--content file://eu-region-policy.json
# Attach to organizational unit
aws organizations attach-policy \
--policy-id p-xxxxxxxx \
--target-id ou-xxxx-xxxxxxxx
Validate Region Compliance:
import boto3
def audit_non_eu_resources():
"""Audit resources outside EU regions"""
eu_regions = [
'eu-west-1', 'eu-west-2', 'eu-west-3',
'eu-central-1', 'eu-north-1'
]
ec2 = boto3.client('ec2')
all_regions = [region['RegionName'] for region in ec2.describe_regions()['Regions']]
violations = []
for region in all_regions:
if region not in eu_regions:
ec2_regional = boto3.client('ec2', region_name=region)
instances = ec2_regional.describe_instances()
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
violations.append({
'ResourceId': instance['InstanceId'],
'Region': region,
'Type': 'EC2 Instance'
})
return violations
# Run audit
violations = audit_non_eu_resources()
if violations:
print(f"WARNING: {len(violations)} resources found outside EU regions")
for violation in violations:
print(f" - {violation['Type']} {violation['ResourceId']} in {violation['Region']}")
3. Encryption at Rest
S3 Bucket Encryption:
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as kms from 'aws-cdk-lib/aws-kms';
// Create KMS key for data encryption
const dataEncryptionKey = new kms.Key(this, 'DataEncryptionKey', {
enableKeyRotation: true,
description: 'KMS key for GDPR-compliant data encryption',
alias: 'gdpr-data-encryption',
});
// Create encrypted bucket
const secureDataBucket = new s3.Bucket(this, 'SecureData', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: dataEncryptionKey,
bucketKeyEnabled: true,
versioned: true,
enforceSSL: true,
});
RDS Database Encryption:
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const database = new rds.DatabaseInstance(this, 'CustomerDatabase', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15_3,
}),
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T4G,
ec2.InstanceSize.MEDIUM
),
vpc,
storageEncrypted: true,
storageEncryptionKey: dataEncryptionKey,
backupRetention: cdk.Duration.days(35),
deletionProtection: true,
cloudwatchLogsExports: ['postgresql'],
});
Enforce Encryption with AWS Config:
Resources:
S3BucketEncryptionRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: s3-bucket-server-side-encryption-enabled
Source:
Owner: AWS
SourceIdentifier: S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED
Scope:
ComplianceResourceTypes:
- AWS::S3::Bucket
RDSEncryptionRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: rds-storage-encrypted
Source:
Owner: AWS
SourceIdentifier: RDS_STORAGE_ENCRYPTED
Scope:
ComplianceResourceTypes:
- AWS::RDS::DBInstance
4. Encryption in Transit
Enforce HTTPS/TLS:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::customer-data-bucket/*",
"arn:aws:s3:::customer-data-bucket"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
Application Load Balancer with TLS:
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
// Create ACM certificate
const certificate = new acm.Certificate(this, 'Certificate', {
domainName: 'app.example.nl',
validation: acm.CertificateValidation.fromDns(),
});
// Create ALB with HTTPS listener
const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
vpc,
internetFacing: true,
});
const httpsListener = alb.addListener('HttpsListener', {
port: 443,
certificates: [certificate],
sslPolicy: elbv2.SslPolicy.TLS13_RES,
});
// Redirect HTTP to HTTPS
const httpListener = alb.addListener('HttpListener', {
port: 80,
defaultAction: elbv2.ListenerAction.redirect({
protocol: 'HTTPS',
port: '443',
permanent: true,
}),
});
5. Access Control and Authentication
Implement Least Privilege IAM:
import * as iam from 'aws-cdk-lib/aws-iam';
// Create role with minimal permissions
const dataProcessorRole = new iam.Role(this, 'DataProcessorRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
description: 'Role for GDPR-compliant data processing',
});
// Add specific permissions only
dataProcessorRole.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:GetObject',
's3:PutObject',
],
resources: ['arn:aws:s3:::customer-data-bucket/processed/*'],
conditions: {
'StringEquals': {
's3:x-amz-server-side-encryption': 'aws:kms',
},
},
}));
Enable MFA for Sensitive Operations:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireMFAForDataDeletion",
"Effect": "Deny",
"Action": [
"s3:DeleteObject",
"s3:DeleteBucket",
"rds:DeleteDBInstance"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Implement SSO with SAML:
import * as iam from 'aws-cdk-lib/aws-iam';
// Create SAML provider for Azure AD integration
const samlProvider = new iam.SamlProvider(this, 'AzureADProvider', {
metadataDocument: iam.SamlMetadataDocument.fromFile('./azure-ad-metadata.xml'),
name: 'AzureAD',
});
// Create role for federated users
const federatedRole = new iam.Role(this, 'FederatedUserRole', {
assumedBy: new iam.SamlConsolePrincipal(samlProvider),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'),
],
});
6. Audit Logging and Monitoring
Enable Comprehensive Logging:
import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail';
import * as s3 from 'aws-cdk-lib/aws-s3';
// Create logging bucket
const logBucket = new s3.Bucket(this, 'AuditLogBucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
lifecycleRules: [
{
transitions: [
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
expiration: cdk.Duration.days(2555), // 7 years for compliance
},
],
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
// Enable CloudTrail
const trail = new cloudtrail.Trail(this, 'GDPRAuditTrail', {
bucket: logBucket,
enableFileValidation: true,
includeGlobalServiceEvents: true,
isMultiRegionTrail: true,
managementEvents: cloudtrail.ReadWriteType.ALL,
});
// Log data events for sensitive buckets
trail.addS3EventSelector([{
bucket: customerDataBucket,
objectPrefix: '',
}], {
readWriteType: cloudtrail.ReadWriteType.ALL,
includeManagementEvents: true,
});
Automated Compliance Monitoring:
import boto3
from datetime import datetime, timedelta
def monitor_data_access():
"""Monitor access to personal data"""
cloudtrail = boto3.client('cloudtrail', region_name='eu-west-1')
# Query CloudTrail for data access events
response = cloudtrail.lookup_events(
LookupAttributes=[
{
'AttributeKey': 'ResourceType',
'AttributeValue': 'AWS::S3::Object'
}
],
StartTime=datetime.now() - timedelta(hours=24),
MaxResults=50
)
suspicious_access = []
for event in response['Events']:
event_name = event['EventName']
username = event.get('Username', 'Unknown')
# Flag unusual access patterns
if event_name in ['DeleteObject', 'GetObject'] and 'customer-data' in str(event):
suspicious_access.append({
'Time': event['EventTime'],
'User': username,
'Action': event_name,
'Resource': event.get('Resources', [{}])[0].get('ResourceName')
})
return suspicious_access
# Run monitoring
alerts = monitor_data_access()
if alerts:
print(f"ALERT: {len(alerts)} suspicious data access events detected")
for alert in alerts:
print(f" {alert['Time']}: {alert['User']} performed {alert['Action']}")
7. Data Subject Rights Implementation
Right to Access (Article 15):
import boto3
import json
def export_user_data(user_email):
"""Export all data for a specific user"""
dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
s3 = boto3.client('s3', region_name='eu-west-1')
# Query user data from DynamoDB
table = dynamodb.Table('UserData')
response = table.query(
KeyConditionExpression='email = :email',
ExpressionAttributeValues={
':email': user_email
}
)
user_data = {
'personal_information': response['Items'],
'export_date': datetime.now().isoformat(),
'request_type': 'GDPR Article 15 - Right to Access'
}
# Upload to secure export bucket
export_key = f"gdpr-exports/{user_email}/{datetime.now().date()}.json"
s3.put_object(
Bucket='gdpr-export-bucket',
Key=export_key,
Body=json.dumps(user_data, indent=2),
ServerSideEncryption='aws:kms'
)
# Generate presigned URL valid for 7 days
url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'gdpr-export-bucket', 'Key': export_key},
ExpiresIn=604800
)
return url
Right to Erasure (Article 17):
def delete_user_data(user_email):
"""Delete all personal data for a user (right to be forgotten)"""
dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
s3 = boto3.client('s3', region_name='eu-west-1')
# Log deletion request for audit
log_deletion_request(user_email)
# Delete from DynamoDB
table = dynamodb.Table('UserData')
response = table.query(
KeyConditionExpression='email = :email',
ExpressionAttributeValues={':email': user_email}
)
with table.batch_writer() as batch:
for item in response['Items']:
batch.delete_item(Key={'email': user_email, 'timestamp': item['timestamp']})
# Delete S3 objects
prefix = f"users/{user_email}/"
objects = s3.list_objects_v2(Bucket='customer-data-bucket', Prefix=prefix)
if 'Contents' in objects:
delete_keys = [{'Key': obj['Key']} for obj in objects['Contents']]
s3.delete_objects(
Bucket='customer-data-bucket',
Delete={'Objects': delete_keys}
)
return {
'status': 'completed',
'user': user_email,
'deletion_date': datetime.now().isoformat()
}
def log_deletion_request(user_email):
"""Log deletion request for audit trail"""
dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
audit_table = dynamodb.Table('GDPRAuditLog')
audit_table.put_item(Item={
'request_id': str(uuid.uuid4()),
'user_email': user_email,
'request_type': 'RIGHT_TO_ERASURE',
'timestamp': datetime.now().isoformat(),
'status': 'PROCESSING'
})
Data Portability (Article 20):
def export_portable_data(user_email):
"""Export user data in machine-readable format"""
dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('UserData')
response = table.query(
KeyConditionExpression='email = :email',
ExpressionAttributeValues={':email': user_email}
)
# Convert to portable JSON format
portable_data = {
'data_subject': user_email,
'export_format': 'JSON',
'export_date': datetime.now().isoformat(),
'data': response['Items']
}
return json.dumps(portable_data, indent=2)
8. Data Retention and Lifecycle Management
Automated Data Retention:
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
// S3 lifecycle for automated deletion
const gdprBucket = new s3.Bucket(this, 'GDPRDataBucket', {
lifecycleRules: [
{
id: 'DeleteOldCustomerData',
enabled: true,
prefix: 'customer-data/',
expiration: cdk.Duration.days(365), // 1 year retention
noncurrentVersionExpiration: cdk.Duration.days(30),
},
{
id: 'ArchiveAnalytics',
enabled: true,
prefix: 'analytics/',
transitions: [
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
expiration: cdk.Duration.days(730), // 2 years
},
],
});
// DynamoDB with TTL for automatic expiration
const userData = new dynamodb.Table(this, 'UserData', {
partitionKey: { name: 'email', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'timestamp', type: dynamodb.AttributeType.NUMBER },
timeToLiveAttribute: 'ttl',
pointInTimeRecovery: true,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
});
Retention Policy Enforcement:
import boto3
from datetime import datetime, timedelta
def enforce_retention_policy():
"""Enforce data retention policies"""
dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('UserData')
# Calculate TTL (1 year from now)
ttl = int((datetime.now() + timedelta(days=365)).timestamp())
# Update items with TTL
response = table.scan()
with table.batch_writer() as batch:
for item in response['Items']:
if 'ttl' not in item:
item['ttl'] = ttl
batch.put_item(Item=item)
print(f"Updated {len(response['Items'])} items with retention policy")
9. Data Breach Detection and Response
Automated Breach Detection:
import * as guardduty from 'aws-cdk-lib/aws-guardduty';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
// Enable GuardDuty
const detector = new guardduty.CfnDetector(this, 'GDPRThreatDetector', {
enable: true,
dataSources: {
s3Logs: { enable: true },
kubernetes: { auditLogs: { enable: true } },
},
});
// Create SNS topic for breach alerts
const breachAlertTopic = new sns.Topic(this, 'BreachAlertTopic', {
displayName: 'GDPR Breach Alerts',
});
// EventBridge rule for high severity findings
const breachRule = new events.Rule(this, 'BreachDetectionRule', {
eventPattern: {
source: ['aws.guardduty'],
detailType: ['GuardDuty Finding'],
detail: {
severity: [7, 8, 9], // High and Critical only
},
},
});
breachRule.addTarget(new targets.SnsTopic(breachAlertTopic));
Breach Response Lambda:
import boto3
import os
from datetime import datetime
def lambda_handler(event, context):
"""Automated GDPR breach response"""
# Parse GuardDuty finding
finding = event['detail']
severity = finding['severity']
finding_type = finding['type']
# Log incident
log_security_incident(finding)
# Notify DPO if high severity
if severity >= 7:
notify_dpo(finding)
# Initiate 72-hour breach notification timer
start_breach_notification_process(finding)
# Take automated remediation action
if 'UnauthorizedAccess' in finding_type:
isolate_compromised_resource(finding)
return {
'statusCode': 200,
'incident_id': finding['id'],
'severity': severity
}
def notify_dpo(finding):
"""Notify Data Protection Officer"""
sns = boto3.client('sns', region_name='eu-west-1')
message = f"""
URGENT: Potential GDPR Data Breach Detected
Finding Type: {finding['type']}
Severity: {finding['severity']}
Resource: {finding['resource']['resourceType']}
Time: {finding['createdAt']}
Action Required: Assess if this constitutes a data breach requiring
notification to supervisory authority within 72 hours (GDPR Article 33).
"""
sns.publish(
TopicArn=os.environ['DPO_TOPIC_ARN'],
Subject='GDPR Breach Alert - Immediate Action Required',
Message=message
)
10. Privacy by Design and Default
Infrastructure as Code with Privacy Controls:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as kms from 'aws-cdk-lib/aws-kms';
export class GDPRCompliantStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Privacy by default: encryption key
const encryptionKey = new kms.Key(this, 'DefaultEncryptionKey', {
enableKeyRotation: true,
description: 'Default encryption for all data at rest',
});
// Privacy by default: secure bucket template
const createSecureBucket = (name: string) => {
return new s3.Bucket(this, name, {
encryption: s3.BucketEncryption.KMS,
encryptionKey,
bucketKeyEnabled: true,
versioned: true,
enforceSSL: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [
{
expiration: cdk.Duration.days(365),
},
],
});
};
// All buckets are secure by default
const dataBucket = createSecureBucket('CustomerDataBucket');
// Tag for GDPR compliance
cdk.Tags.of(this).add('GDPR-Compliant', 'true');
cdk.Tags.of(this).add('DataResidency', 'EU');
}
}
GDPR Compliance Automation
Automated Compliance Dashboard
import boto3
from datetime import datetime
def generate_gdpr_compliance_report():
"""Generate automated GDPR compliance report"""
report = {
'generated_at': datetime.now().isoformat(),
'checks': []
}
# Check 1: Data residency
report['checks'].append({
'control': 'Data Residency',
'status': check_data_residency(),
'requirement': 'All resources in EU regions'
})
# Check 2: Encryption at rest
report['checks'].append({
'control': 'Encryption at Rest',
'status': check_encryption_at_rest(),
'requirement': 'All S3 buckets and RDS databases encrypted'
})
# Check 3: Encryption in transit
report['checks'].append({
'control': 'Encryption in Transit',
'status': check_encryption_in_transit(),
'requirement': 'HTTPS/TLS enforced'
})
# Check 4: Access logging
report['checks'].append({
'control': 'Access Logging',
'status': check_cloudtrail_enabled(),
'requirement': 'CloudTrail enabled in all regions'
})
# Check 5: Data retention
report['checks'].append({
'control': 'Data Retention',
'status': check_lifecycle_policies(),
'requirement': 'Lifecycle policies configured'
})
# Calculate compliance score
passed = sum(1 for check in report['checks'] if check['status'] == 'COMPLIANT')
total = len(report['checks'])
report['compliance_score'] = f"{(passed/total)*100:.1f}%"
return report
def check_encryption_at_rest():
"""Check if all S3 buckets have encryption enabled"""
s3 = boto3.client('s3', region_name='eu-west-1')
buckets = s3.list_buckets()['Buckets']
non_compliant = []
for bucket in buckets:
try:
encryption = s3.get_bucket_encryption(Bucket=bucket['Name'])
except s3.exceptions.ClientError:
non_compliant.append(bucket['Name'])
return 'COMPLIANT' if not non_compliant else f'NON_COMPLIANT: {len(non_compliant)} buckets'
Conclusion
Achieving GDPR compliance on AWS requires a comprehensive approach combining technology, processes, and governance. By implementing these controls and automation scripts, EU organizations can:
- Ensure data protection by design and default
- Maintain continuous compliance monitoring
- Respond efficiently to data subject rights requests
- Detect and respond to data breaches within GDPR timelines
- Demonstrate accountability to supervisory authorities
Key Takeaways:
- Use AWS services designed for compliance (KMS, CloudTrail, Config, GuardDuty)
- Enforce EU data residency through SCPs and region controls
- Implement comprehensive encryption for data at rest and in transit
- Automate compliance monitoring and reporting
- Prepare for data subject rights requests with automated workflows
- Maintain detailed audit trails for 7+ years
Ready to ensure your AWS infrastructure is GDPR compliant? Contact Forrict for expert guidance tailored to Dutch and EU organizations.
Resources
- AWS GDPR Center
- AWS Data Privacy FAQ
- GDPR Official Text
- Dutch DPA (Autoriteit Persoonsgegevens)
- AWS Artifact for Compliance Reports
Fons Biemans
AWS expert and consultant at Forrict, specializing in cloud architecture and AWS best practices for Dutch businesses.