第4章:S3存储桶SSE-KMS配置

好,这一章我们聊聊S3和KMS的三件套。说白了就是:你上传文件时,怎么保证数据一落地就被KMS加密?有3种玩法,我按自己的习惯分成三层。

  • 第一层:存储桶默认加密——自动化兜底
  • 第二层:PutObject时显式指定KMS——控制力最强
  • 第三层:Bucket Policy强制加密——策略铁门

这三层可以叠加使用,互不冲突。我个人建议:至少开前两层,第三层看安全等级。先看一张关系图,把这三步串起来。

S3 SSE-KMS 三层配置流程 默认加密 控制台 / PutBucketEncryption 自动加密所有对象 PutObject指定KMS x-amz-server-side-encryption 每个请求独立控制 Bucket Policy强制 Deny未加密PutObject 条件键 s3:x-amz-server-side-encryption 建议策略 默认加密兜底 + PutObject灵活控制 + Policy安全加固 注意冲突点 ▪ 默认加密 + Policy强制SSE-KMS → 上传时必须指定KMS密钥 ▪ 若Policy要求SSE-KMS但请求用AES256 → 被拒绝 ▪ 默认加密不影响Policy判断,但Policy会覆盖默认行为

4.1 默认加密设置

先讲最简单的:存储桶默认加密。你在S3控制台打开存储桶属性,找到"Default encryption",选SSE-KMS,指定一个KMS密钥ID(或别名)。

这层的作用是:任何没有显式指定加密头部的PutObject请求,都会被自动套用KMS加密。说白了就是兜底,不怕漏掉。

我的一个小习惯:新建存储桶时,第一件事就是开默认加密。别指望每个工程师都记得加头部。我见过生产环境里,就因为开发忘了指定,对象以明文形式存了一整天。

用AWS CLI配置默认加密的命令长这样:

aws s3api put-bucket-encryption \
  --bucket my-secure-bucket \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "aws:kms",
          "KMSMasterKeyID": "1234abcd-12ab-34cd-56ef-1234567890ab"
        }
      }
    ]
  }'

注意:KMSMasterKeyID 可以填密钥ID、别名(alias/xxx)、或ARN。我一般用别名,方便轮换。

关键点:默认加密只影响未来上传的对象。已存在的对象不会自动重新加密。需要手动拷贝或使用S3 Batch Operations。

4.2 PutObject时指定KMS

第二层比较灵活。你可以在每个PutObject请求的头部里明确指定加密方式。举个例子,如果你要针对某个重要文件单独用密钥A,而其他文件用密钥B,就可以靠这个实现。

对应的请求头部是:

  • x-amz-server-side-encryption: aws:kms
  • x-amz-server-side-encryption-aws-kms-key-id: 密钥ID/别名/ARN(可选,不指定则用默认KMS key)

CLI的话这样写:

aws s3api put-object \
  --bucket my-secure-bucket \
  --key confidential.pdf \
  --body /tmp/confidential.pdf \
  --server-side-encryption aws:kms \
  --ssekms-key-id alias/my-app-key

嗯,这里要注意:如果你指定了 ssekms-key-id,但IAM角色没有 kms:GenerateDataKey 权限,请求会报403。我踩过这个坑——当时以为只要密钥一致就行,结果忘了加KMS权限。

重要:SDK自动加签时,如果同时使用了默认加密和显式指定,显式指定的优先级更高。但如果显式指定的加密类型与默认加密不一样(比如默认是KMS,你写了AES256),那么默认加密会被跳过,使用请求指定的方式。这往往是安全审计发现漏洞的地方。

4.3 Bucket Policy强制加密

最后这层,才是真正的铁门。Bucket Policy里写一条Deny规则,条件键是 s3:x-amz-server-side-encryption,要求必须等于 aws:kms 或指定的某个KMS密钥。

为什么需要这个?你想想,用户就算上传时忘了加头部,默认加密会兜底。但如果用户恶意绕过了默认加密呢?比如他上传时指定了 aws:kms 但使用了不被允许的外部密钥?

一条典型的强制策略:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyIncorrectEncryption",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-secure-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "DenyUnencryptedObjectUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-secure-bucket/*",
      "Condition": {
        "Null": {
          "s3:x-amz-server-side-encryption": "true"
        }
      }
    }
  ]
}

解释一下两个Statement:

  1. 如果 x-amz-server-side-encryption 的值不是 aws:kms,Deny。
  2. 如果请求头部完全没带这个字段(Null条件为true),也Deny。

这样一来,所有PutObject请求必须显式携带 aws:kms 头部。默认加密只是辅助,最终强制权在Policy手里。

我曾经遇到过的事:某次审计发现一个存储桶有几千个对象使用了SSE-S3而不是SSE-KMS。原因就是没有Policy强制,开发团队直接传了对象,默认加密虽然开着,但被请求头覆盖了。后来加了上面的Deny策略,再也没出过问题。

4.4 三者组合的最佳实践

这三种方式不是互斥的。我通常建议这样组合:

配置 推荐做法 说明
默认加密 开启SSE-KMS,使用自定义CMK 避免明文漏洞,兜底策略
PutObject指定 在脚本/SDK中显式添加头部 细粒度控制,密钥可区分
Bucket Policy强制 Deny未指定SSE-KMS的请求 安全底线,不可绕过

个人建议:如果你需要严格的合规要求(如PCI DSS),三种全开。如果只是常规数据保护,默认加密 + 政策强制就够了。

最后提一嘴:Policy中还可以更进一步,要求特定的KMS密钥ID,而不仅仅是 aws:kms。比如:

"Condition": {
  "StringNotEquals": {
    "s3:x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-..."
  }
}

这样别人即使用了KMS,但不是你的CMK,也会被拒绝。我个人在金融场景下常用这种写法。

好,这一章就到这里。这三层配置吃透了,S3的KMS加密基本就稳了。