当我第一次阅读GDPR第32条时,犯了一个错误——我以为它是一份法律文件。
但实际上并非如此,它只是一份关于技术规范的文档而已。
该法规规定,必须采取“适当的技术措施”来保护个人数据。但这个表述非常模糊:“适当”到底意味着什么?什么是“技术措施”?又由谁来判定这些措施是否足够有效呢?
合规咨询机构会提供一份长达50页的政策文件,但审计人员根本不会理会这份文件,他们只会要求你提供数据库架构信息。
本指南旨在提供一个折中的解决方案。我已经在12家SaaS企业中实施了第32条规定的相关措施,每次都会用到相同的9项控制措施,同时也会遇到同样的3个审计问题。
这份指南全面介绍了你必须实施的9项技术措施、每项措施的具体实现代码与命令,以及GDPR审计人员会提出的常见问题。
目录
你将学到什么
-
GDPR第32(1)(a)至(d)条所要求的9项技术措施
- 用于实现匿名化和字段级加密的PostgreSQL具体命令
- 如何设置自动登出功能及实现唯一用户身份识别
- 比CloudTrail更强大的应用层审计日志记录功能
- 验证数据未被篡改的数据完整性控制措施
- mTLS与TLS 1.3在数据传输安全方面的作用
- 你必须用证据回答的5个审计问题
让我们开始吧。
先决条件
在开始学习之前,你需要具备以下条件:
知识要求:
-
熟悉PostgreSQL及基本SQL语法
-
对AWS服务(如KMS、RDS、CloudTrail)有基本了解
-
能够熟练阅读Python以及JavaScript/Node.js代码
-
清楚GDPR的基本概念;如果完全不了解,建议先阅读ICO发布的GDPR概述文档
所需工具与条件:
-
PostgreSQL 14或更高版本
-
拥有IAM管理员权限的AWS账户
-
Python 3.8或更高版本,并安装
cryptography库(通过pip install cryptography命令安装) -
Node.js 16或更高版本
预计实施时间:根据您的现有基础设施情况,完成本指南中规定的各项措施通常需要2至4周的时间。其中某些具体操作的耗时范围为:30分钟(用于配置KMS密钥),到5天(用于全面部署应用层加密功能)。
第1部分:了解第32条——技术要求
1.1. 第32条实际要求什么
GDPR中的第32条规定,控制者与处理者必须采取“适当的技术及组织措施”,以确保安全防护水平与面临的风险相匹配。
大多数团队都会忽略一个关键点:第32条并非政策清单。政策只是说明“我们要对个人数据进行加密”,而证据才才是真正能证明这一点的东西——比如“这就是具有自动轮换功能的KMS密钥,这是应用层加密代码,而这些CloudTrail日志记录了所有的解密尝试”。审计人员需要的是证据,而非书面文件。
四大主要要求:
| 条款 | 要求 | 对工程师而言意味着什么 |
|---|---|---|
| 32(1)(a) | 伪名化处理与数据加密 | 个人数据必须以这样的方式存储:在没有额外信息的情况下,无法将其关联到特定个体身上 |
| 32(1)(b) | 数据的保密性、完整性、可用性及灾后恢复能力 | 系统必须能够防止数据被未经授权的人员访问、篡改或丢失,并且能够在发生故障时及时恢复功能 |
| 32(1)(c) | 恢复数据的可用性与访问权限 | 在发生物理性或技术性故障后,必须能够恢复数据并重新获得对系统的访问权限 |
| 32(1)(d) | 定期进行安全测试与风险评估 | 必须建立定期检测和评估安全措施的有效机制 |
1.2>适用范围:哪些数据受此条款约束
在开始实施任何安全措施之前,首先需要明确哪些数据属于第32条的覆盖范围。该法规适用于个人数据——即任何能够直接或间接识别特定个人的资料。
数据类型及其保护等级:
| 类别 | 示例 | 保护等级 |
|---|---|---|
| 个人数据 | 姓名、电子邮件地址、电话号码、IP地址 | 标准级保护 |
| 敏感个人数据 | 健康信息、生物识别数据、政治观点、宗教信仰 | 加强级保护 |
| 伪名化数据 | 直接标识信息已被替换为代码的数据 | 标准级保护 |
| 匿名化数据 | 在任何合理情况下都无法被重新识别出的数据 | 不在保护范围之内 |
审计人员会问到的关于数据映射的问题:
“您能否提供一份数据流图,说明个人数据是如何进入您的系统、在何处存储、如何被处理以及最终如何被删除的?”
在审计人员提问之前,请运行以下命令,以便了解AWS环境中所有存储个人数据的数据库情况:
# 列出所有RDS实例及其加密状态
# 如果有任何实例的StorageEncrypted值为false,那就表示存在问题
aws rds describe-db-instances \
--query 'DBInstances[*].{
ID:DBInstanceIdentifier,
Engine:Engine,
StorageEncrypted:StorageEncrypted,
Region:AvailabilityZone
}' \
--output table
如果发现有任何实例的StorageEncrypted: false,那么在第32条规定的审计之前,必须尽快解决这个问题。
第二部分:第32(1)(a)条——匿名化与加密
2.1. 如何在数据库层实现匿名化
匿名化的做法是用假名或代码来替代直接标识符,例如姓名、电子邮件地址或护照号码。这样做的目的是:除非能够访问单独存储且受到保护的查询表,否则主要的工作数据集将无法识别出数据的实际所属者。
以下是错误的实现方式——在主工作表中直接使用原始的标识符:
-- 错误示例:在主工作表中存储直接标识符
CREATE TABLE users (
id SERIAL PRIMARY KEY,
full_name VARCHAR(255), -- 这是直接标识符,不应该出现在这里
email VARCHAR(255), -- 同样是直接标识符,不应在此处
passport_number VARCHAR(50) -- 也是直接标识符,不可放置于此表中
);
采用这种方式的话,任何拥有对users表SELECT权限的工程师、分析师或攻击者都可以立即读取并识别出这些个人的身份。工作数据与用于识别的数据没有被分开存储。
下面是正确的实现方法——使用单独的标识符表:
-- 正确示例:使用匿名化处理后的数据,并配备独立的、受限制的查询表
-- 第一步:主工作表仅使用假名
CREATE TABLE users (
id SERIAL PRIMARY KEY,
pseudonym UUID DEFAULT gen_random_uuid(), -- 这是一个无法被猜测到的假名
created_at TIMESTAMP DEFAULT NOW(),
account_status VARCHAR(50)
-- 此表中不包含任何直接标识符
);
-- 第二步:创建独立的标识符查询表,并限制访问权限
CREATE TABLE user_identifiers (
pseudonym UUID PRIMARY KEY,
full_name VARCHAR(255),
email VARCHAR(255),
passport_number VARCHAR(50),
FOREIGN KEY (pseudonym) REFERENCES users(pseudonym)
);
-- 第三步:根据角色分配最低必要的访问权限
GRANT SELECT ON users TO app_role; -- 应用程序仅使用假名
GRANT SELECT, INSERT, UPDATE ON user_identifiers TO identity_service_role; -- 只有身份验证服务才能查看这些原始的标识符信息
各部分的功能:
-
gen_random_uuid()会为每位用户生成一个版本号为4的UUID伪标识符——这种标识符是不可预测的,且在没有查询表的情况下也无法被还原。 -
主要的
users表格可用于数据分析、报告编制以及各种常规应用场景,而不会泄露任何用户的身份识别信息。 -
只有
identity_service_role角色才能连接这两个表格;这一角色仅被分配给负责处理身份认证相关操作的特定服务。
审计人员会提出的问题:
“你们是如何确保这些经过伪化处理的数据不会被未经授权的第三方重新识别出来的?”
你们的证明依据:
-- 证明只有identity_service_role角色才能访问标识符表格
SELECT grantee, privilege_type, table_name
FROM information_schema.role_table_grants
WHERE table_name = 'user_identifiers';
-- 预期结果:仅显示identity_service_role角色
2.2. 如何使用客户管理的密钥来实现数据静态加密
存储层加密可以在有人物理上窃取硬盘时保护数据安全。但它无法防止拥有高级权限的AWS员工、被入侵的云管理账户,或是那些可以直接访问数据库的授权用户窃取数据。进行第32条规定的审计的人员非常清楚这一点——他们肯定会就此提出疑问。
以下是错误的实现方式:使用AWS管理的密钥:
# 错误做法:使用AWS管理的KMS密钥
# 你无法控制AWS内部哪些人员能够访问这些密钥
aws kms create-key \
--origin AWS_KMS \
--description "用于生产环境的AWS管理密钥"
问题在于:当审计人员询问“你们能否证明AWS的员工无法解密客户的数据?”时,答案是否定的。因为AWS管理的密钥实际上是由AWS自己来控制的。
正确的实现方式是:使用客户管理的密钥,并配置自动轮换机制:
# 第一步:创建一个客户管理的KMS密钥
KEY_ID=$(aws kms create-key \
--origin AWS_KMS \
--description "用于存储用户个人信息的客户管理密钥——符合第32条规定" \
--tags TagKey=Purpose,TagValue=GDPR; TagKey=Environment,TagValue=production \
--query 'KeyMetadata.KeyId' \
--output text)
echo "创建的KMS密钥编号:$KEY_ID"
# 第二步:启用自动90天轮换机制
aws kms enable-key-rotation --key-id $KEY_ID
# 第三步:将此密钥应用到生产环境的RDS实例中
aws rds modify-db-instance \
--db-instance-identifier production-db \
--kms-key-id $KEY_ID \
--apply-immediately
审计人员会提出的问题:
“请证明你们的加密密钥是自动进行轮换的,并且你们能够明确哪些人曾经访问过这些密钥。”
你们的证明依据:
# 检查旋转加密功能是否已启用——预期输出结果为“true”
aws kms get-key-rotation-status --key-id $KEY_ID \
--query 'KeyRotationEnabled'
# 查看与每个密钥使用事件相关的CloudTrail审计记录
aws logs filter-log-events \
--log-group-name cloudtrail-logs \
--filter-pattern '{ $.eventSource = "kms.amazonaws.com" }' \
--query 'events[*].{Time:timestamp, Event:message}' \
--output table
2.3 如何为敏感字段实施应用层加密
存储加密是最低要求,而应用层加密则是第32条规定的审计人员越来越期望看到的功能,尤其是对于健康数据、财务记录以及其他敏感的个人信息而言。
两者的区别在于:仅使用存储加密的情况下,当数据库管理员执行SELECT email FROM users这条查询时,他们看到的是未加密的电子邮件地址;而应用层加密则会使他们看到gAAAAABm...这样的加密字节串——只有拥有Vault密钥的应用程序才能解密这些数据。
# application_encryption.py
from cryptography.fernet import Fernet
class FieldEncryption:
"""
在敏感个人数据字段被存储到数据库之前对其进行加密。
加密密钥保存在HashiCorp Vault或AWS Secrets Manager中,绝不会硬编码在代码中。
直接通过SQL访问数据库的管理员只能看到加密后的字节数据。
"""
def __init__(self, key: str):
# 密钥必须是一个32字节的Base64编码字符串——从Vault中获取该密钥
self.cipher = Fernet(key.encode())
def encrypt_field(self, plaintext: str) -> str:
"""在将敏感数据写入数据库之前对其进行加密。"""
if not plaintext:
return None
encrypted_bytes = self.cipher.encrypt(plaintext.encode())
return encrypted_bytes.decode()
def decrypt_field(self, ciphertext: str) -> str:
"""
当应用程序确实需要解密这些数据时,使用此方法进行解密。
此方法需要Vault密钥——数据库管理员无法直接调用这个方法。
"""
if not ciphertext:
return None
decrypted_bytes = self.cipher.decrypt(ciphertext.encode())
return decrypted_bytes.decode()
# 在你的应用程序中使用这个类:
from vault_client import get_secret # 你的Vault或Secrets Manager客户端库
# 在应用程序启动时获取加密密钥——切勿将其硬编码在代码中
encryption_key = get_secret("gdpr/field-encryption-key")
encryptor = FieldEncryption(encryption_key)
# 在将用户的健康数据存储到数据库之前
user.health_data_encrypted = encryptor.encrypt_field(user.health_data_plaintext)
# 在出于合法目的读取数据之前(例如处理用户访问请求等)
health_data = encryptor.decrypt_field(user.health_data_encrypted)
审计人员会问:
“如果数据库管理员直接查询users表,他们能看到客户的健康数据吗?”
你的回应:让审计人员直接执行数据库查询,让他们看到加密后的结果;同时证明数据库管理员无法获取解密密钥——只有通过Vault,应用程序才能获取该密钥。
第3部分:第32条(1)(b)款——保密性与完整性
3.1. 如何实现自动登出功能
根据第32(1)(b)条的规定,必须采取措施防止“未经授权的访问个人数据”。如果会话永不停止有效,或者仅在24小时后失效,那就存在安全漏洞。当用户在共享设备上登录后离开时,实际上就留下了一个“敞开的门”,任何人都可以利用这个机会访问个人数据。
以下是一种错误的实现方式——使用有效期为24小时的JWT会话令牌:
// 错误做法:使用有效期为24小时、不支持失效检测的访问令牌
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' } // 有效期过长,违反法规要求
);
问题在于:如果用户在共享电脑上登录后直接关闭电脑而没有登出,那么该会话令牌仍然有效,最长可保持24小时。任何打开这台电脑的人都可以访问用户的个人数据。
正确的实现方式是使用有效期为15分钟、支持自动更新的访问令牌:
// 正确做法:使用有效期为15分钟的访问令牌,并通过HTTP-only cookie实现自动更新
// 访问令牌——在用户活动15分钟内有效
const accessToken = jwt.sign(
{ userId: user.id, role: user.role, type: 'access' },
process.envJWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
// 更新令牌——在整个会话期间有效,有效期为8小时
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ ExpiresIn: '8h' }
);
// 将更新令牌设置为HTTP-only cookie,这样JavaScript就无法访问它
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // 防止XSS攻击
secure: true, // 仅允许HTTPS连接
sameSite: 'strict', // 防止CSRF攻击
maxAge: 8 * 60 * 60 * 1000 // 8小时,以毫秒为单位
});
// 中间件,用于强制会话在指定时间后失效
const MAX_TOTAL_SESSION_MS = 8 * 60 * 60 * 1000; // 8小时
app.use((req, res, next) => {
if (!req.session?.createdAt) return next();
const sessionAge = Date.now() - req.session.createdAt;
if (sessionAge > MAX_TOTAL_SESSION_MS) {
req.session.destroy();
return res.status(401).json({
error: '会话已过期,请重新登录。'
});
}
next();
});
审计人员会问的问题:
“请证明你们的应用程序会在合理的时间内终止无效会话。”
你们应提供的证据:浏览器开发者工具中显示的cookie失效时间,以及测试记录,这些记录能够证明用户在15分钟未进行任何操作后会被提示重新登录。
3.2. 如何使用IRSA实现唯一用户识别功能
根据第32(1)(b)条的规定,必须能够确定是谁访问了个人数据。如果使用共享的服务账户,就无法实现这一目标——审计日志中虽然显示的是data-export-service,但却无法判断是哪位工程师触发了数据导出操作。
以下是错误的做法——使用共享的服务账户:
# 错误示例:多个工程师和管道系统共用同一个Kubernetes服务账户
apiVersion: v1
kind: ServiceAccount
metadata:
name: data-export # 三个工程师和两个管道系统共用这个账户
namespace: production
当审计日志显示“data-export在03:17 UTC时间执行了批量用户数据导出操作”时,你将无法回答审计人员的疑问:“是谁授权进行了这一操作?”
正确的做法是——为服务账户分配IAM角色(IRSA):
# 第一步:为每个服务账户创建一个独立的IAM角色
# 该命令用于创建一个仅能由“production”命名空间中的“payment-service”Kubernetes服务账户使用的角色
aws iam create-role \
--role-name eks-payment-service-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/YOUR OIDC_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/YOUR_OIDC_ID:sub": "system:serviceaccount:production:payment-service"
}
}
}
]
}'
# 第二步:为Kubernetes服务账户添加与其对应的IAM角色信息
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-service # 一个服务账户对应一个服务,因此也需要一个唯一的IAM角色
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-payment-service-role
现在,从“payment-service”发出的所有AWS API请求在CloudTrail系统中都会被记录为“eks-payment-service-role”——这是一个唯一且可追踪的身份标识。不再存在共享账户,也不会出现模糊不清的审计日志。
审计人员的疑问:
“你们是如何确保每一项针对个人数据的操作都能被明确地归因于特定的个人或服务吗?”
你们的证明:
# 确认不存在共享的服务账户——每个账户都应该拥有唯一的IAM角色注释
kubectl get serviceaccounts --all-namespaces \
-o jsonpath '{range .items[*]}{.metadata.namespace}/{.metadata.name}: {.metadata.annotations.eks\.amazonaws\.com/role-arn}{"\n"}{end}'
第四部分:第32(1)(c)条——可用性与弹性
4.1. 如何实现多区域部署与备份要求
第32(1)(c)条规定:“在发生物理故障或技术问题时,必须能够及时恢复个人数据的可用性及访问权限。”这并非建议,而是法律强制要求。如果你的数据库仅部署在一个区域内,而该区域发生了网络故障,那么你就违反了这一规定。
以下是错误的实施方式——单区域RDS且不启用自动备份:
# 错误方案:单区域RDS,一旦发生网络故障,个人数据就会无法访问
resource "aws_db_instance" "production" {
identifier = "production-database"
multi_az = false # 不支持自动故障转移
backup_retention_period = 0 # 不进行自动备份——违反第32条规定
}
如果该区域的网络出现故障,数据库将无法被访问;如果实例发生损坏,也没有备份数据可供恢复。这两种情况都违反了第32条第1款(c)项的规定。
以下是正确的实施方式——多区域RDS并启用经过测试的自动备份功能:
# 正确方案:多区域RDS,支持30天的数据保留周期
resource "aws_db_instance" "production" {
identifier = "production-database"
# 多区域配置会在另一个区域创建同步备用副本
# 自动故障转移可在60到120秒内完成,且不会导致数据丢失
multi_az = true
# 数据保留周期为30天,这样可以灵活选择恢复点
backup_retention_period = 30
backup_window = "03:00-04:00" # 选择低流量时段进行备份
# 将所有标签复制到快照中,以便后续进行合规性检查
copy_tags_to_snapshot = true
# 启用Performance Insights功能,以监控数据库的性能状况
performance_insights_enabled = true
performance_insights_retention_period = 7
tags = {
Environment = "production"
DataClassification = "personal-data"
GDPRScope = "article32"
}
}
如何每月测试RTO和RPO值:
# 第一步:查找最新的自动备份快照
SNAPSHOT_ID=$(aws rds describe-db-snapshots \
--db-instance-identifier production-database \
--snapshot-type automated \
--query 'sort_by(DBSnapshots, &SnapshotCreateTime)[-1].DBSnapshotIdentifier' \
--output text)
echo "正在测试使用快照进行数据恢复:$SNAPSHOT_ID"
# 第二步:开始恢复操作并记录所需时间
START_TIME=$(date +%s)
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier gdpr-restore-test \
--db-snapshot-identifier $SNAPSHOT_ID \
--db-instance-class db.t3.medium \
--no-publicly-accessible \
--tags Key=Purpose,Value=gdpr-rto-test Key=DeleteAfter,Value=$(date -d '+1 day' +%Y-%m-%d)
# 第三步:等待恢复操作完成
aws rds wait db-instance-available \
--db-instance-identifier gdpr-restore-test
END_TIME=$(date +%s)
RTO_seconds=$((END_TIME - START_time))
echo "恢复操作在$((RTOSeconds / 60))分钟内完成"
# 第四步:通过随机检查来验证数据完整性
# 连接到已恢复的数据库实例,确认记录数量与生产环境中的数据一致
# psql -h RESTORED_ENDPOINT -U admin -d production \
# -c "SELECT COUNT(*) FROM users; SELECT MAX creado_at) FROM orders;"
# 第五步:删除测试用数据库实例
aws rds delete-db-instance \
--db-instance-identifier gdpr-restore-test \
--skip-final-snapshot
审计员提出的问题:
“你们针对个人数据设定的恢复时间目标及恢复点目标是什么?上次进行测试是在什么时候?”
你们的证据: 一份记录有每月一次的灾难恢复测试情况的日志,其中包含所使用的快照、恢复开始时间、恢复完成时间、数据验证结果,以及负责执行测试的工程师信息。
第5部分:第32(1)(d)条——定期检测
5.1 如何实施自动漏洞扫描
第32(1)(d)条规定,必须“建立一套定期测试、评估技术性及组织性措施有效性的流程”。这其中包括在所有容器镜像投入生产之前对其进行自动漏洞扫描。
以下是错误的做法——在部署流程中不进行任何漏洞扫描:
# 错误做法:不进行漏洞扫描,导致基础镜像中的严重安全漏洞被直接部署
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: docker build -t myapp .
- run: docker push myapp # 不进行任何安全检查即可完成部署
如果基础镜像中存在严重的安全漏洞(例如OpenSSL中的远程代码执行漏洞),那么该镜像就会直接被部署到生产环境中。根据第32(1)(d)条的规定,这种情况属于违规行为。
以下是正确的做法——使用Trivy进行漏洞扫描,并在部署流程中强制执行这一步骤:
# 正确做法:Trivy会扫描所有镜像,存在严重或高风险的漏洞将阻止部署
name: 安全扫描与部署
on: [push, pull_request]
jobs:
trivy-scan:
name: 容器漏洞扫描
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 构建容器镜像
run: docker build -t myapp:${{ github.sha }} .
- name: 用Trivy扫描漏洞
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # 如果发现严重或高风险漏洞,流程将失败,镜像无法被部署
- name: 将扫描结果上传到GitHub的安全页面
uses: github/codeql-action/upload-sarif@v2
if: always() # 即使扫描失败也要上传结果,以便进行审查
with:
sarif_file: 'trivy-results.sarif'
Trivy会扫描以下内容:
-
基础镜像中操作系统包存在的漏洞(例如Ubuntu基础镜像中的OpenSSL漏洞)
-
应用程序依赖项的脆弱版本(例如应用程序所使用的npm或pip包中存在的已知漏洞)
-
Dockerfile中的配置错误(例如以root用户身份运行容器,或者使用
latest标签而非固定的SHA值来指定镜像版本)
扫描结果会显示在 GitHub 的“安全”选项卡中,这样就能生成一份带有时间戳且可搜索的扫描记录。这份记录就是你履行第 32(1)(d) 条规定的有力证据。
如何为正在运行的工作负载每周执行一次 AWS Inspector 安全评估:
# 列出你的 AWS 账户中所有未解决的严重安全问题
aws inspector2 list-findings \
--filter-criteria '{
"severity": [{"comparison": "EQUALS", "value": "CRITICAL"}],
"findingStatus": [{"comparison": "EQUALS", "value": "ACTIVE"}]
}' \
--query 'findings[*].{
Title: title,
Resource: resources[0].id,
Severity: severity,
CVE: packageVulnerabilityDetails.vulnerabilityId
}' \
--output table
审计人员会问什么:
“请展示你们的漏洞管理方案,包括你们是如何确定这些安全问题的优先级并采取补救措施的。”
你该提供的证据:一份每周自动生成的漏洞报告——这份报告会显示未解决的漏洞信息、其严重程度、为每个漏洞创建的 GitHub 问题记录,以及问题得到解决后的关闭日期。
第 6 部分:第 32(1)(d) 条规定——渗透测试
6.1. 为什么自动化扫描还不够
第 32(1)(d) 条规定要求企业必须评估自身安全措施的有效性。自动化漏洞扫描工具能够发现库文件和操作系统包中存在的已知安全漏洞,但它们无法发现以下类型的问题:
-
业务逻辑层面的漏洞(例如:当接收到特定参数时,某个 API 端点会返回其他用户的数据)
-
身份验证绕过漏洞(例如:某些 JWT 实现方式会接受未签名的令牌)
-
权限提升路径(例如:攻击者可以通过一系列合法的 API 调用,从低权限角色升变为管理员权限)
-
不安全的直接对象引用漏洞(例如:访问
/api/users/124会获取另一位用户的数据,而本应访问/api/users/123)
英国信息专员办公室及法国数据保护机构都明确要求,那些处理大量个人数据的组织必须每年进行一次人工渗透测试。
合格的渗透测试范围应该包括哪些内容:
# 年度渗透测试范围——符合第 32 条规定
## 测试时间
开始日期:2025-04-01
结束日期:2025-04-14
测试机构:[具有认证资格的机构——CREST 或 CHECK 认证]
如何追踪并验证整改措施的执行情况:
# 在收到渗透测试报告后,为每一项发现问题在GitHub上创建一个问题
# 这样就可以为每一个发现问题及其相应的整改措施留下可追溯的记录
for finding_id in $(cat pentest-report-findings.txt); do
gh issue create \
--title "渗透测试发现问题:$finding_id" \
--body "详见pentest-report-2025-04.pdf文件中$finding_id对应的部分。严重程度:高。处理时限:7天。" \
--label "security,pentest" \
--assignee "@security-lead"
done
审计人员会问的问题:
“你们上次进行渗透测试是在什么时候?请出示测试报告以及整改措施的执行证据。”
你们应该提供的证据:
-
由CREST或CHECK认证机构出具的、在最近12个月内完成的渗透测试报告。
-
用于记录所有“严重”或“高风险”发现问题及其处理进展的跟踪系统(如GitHub问题或Jira任务),其中应明确标注每个问题的关闭日期。
-
能够证明所有“严重”发现问题都在24小时内得到处理的证据(例如Git提交记录或部署日志)。
遵守GDPR第32条的最佳实践
以下是本指南中的关键建议:
✅ 必须做到: 对敏感数据字段实施应用层加密。仅使用存储加密是不够的——拥有直接数据库访问权限的管理员仍然可以读取明文数据。
✅ 必须做到: 使用由客户管理的KMS密钥,并确保这些密钥会自动定期更新。您需要证明自己能够控制这些密钥。
✅ 必须做到: 将经过匿名处理的数据与标识信息分开存储,同时对用于查询这些数据的系统设置基于角色的访问限制。
✅ 必须做到: 设置为在用户15分钟未活动后自动登出系统,并且每个会话的有效时间不得超过8小时。
✅ 必须做到: 使用IRSA服务生成的唯一服务账户来处理个人数据相关操作。每一项与个人数据相关的操作都必须能够追溯到具体的操作者。
✅ 必须做到: 每月对备份数据进行测试,并用实际的恢复测试结果来记录数据恢复的时间和可用性。
✅ 必须做到: 在持续集成流程中运行Trivy工具,以便在代码部署之前及时发现并处理“严重”或“高风险”的安全漏洞。
✅ 必须做到: 每年聘请由CREST或CHECK认证机构出具的团队进行一次手动渗透测试。
❌ 绝对不要: 使用持续24小时的JWT会话机制,也不允许会话在没有用户活动的情况下仍然保持开启状态。
❌ 绝对不要: 将敏感信息存储在环境变量、.env文件中,或者直接硬编码到源代码中。
❌ 绝对不要: 忽略每年的渗透测试。ICO或CNIL等监管机构不会接受“我们进行了自动扫描”这种解释作为替代方案。
❌ 绝对不要: 如果您需要向审计人员证明自己能够控制密钥,就不要使用AWS管理的KMS密钥。
资源
Ayobami Adejumo是一位资深平台工程师,同时也是一名专注于合规性基础设施建设的专家。他撰写的内容涉及GDPR相关的技术控制措施、SOC2标准的实施方法,以及云成本优化方面的策略。