跳转至

安全

一句话

安全设计围绕三件事:身份(是谁)、权限(能做什么,越少越好)、证据(做了什么都有记录)。 本仓库的原则:程序不拿长期有效的密钥,用自己的身份(Instance Principal / Resource Principal);权限用 where 限定到具体的那一个资源;密码不经过 Terraform。

本页整理自 Phase 0、Phase 7、Phase 8、Phase 9、Phase 10 的实验。网络层的访问控制(Security List / NSG / Bastion)见 网络设计,VM 自身的设置(IMDS、传输中加密)见 计算和负载均衡。

1. 先认识这些组件

OCI AWS 一句话
Compartment (近似)资源组 / 账号划分 放资源的"文件夹",也是权限和费用的边界。本仓库的练习资源都放在 tf-lab
User / Group IAM User / Group 人的身份。权限给 Group,不直接给 User
Policy IAM Policy 用一句话写权限:Allow <谁> to <动词> <资源> in <范围> where <条件>
Dynamic Group (近似)IAM Role 的信任关系 按规则圈定一组资源(例:这台 VM、这个 compartment 里的 Function),让它们也能被授权
Instance Principal EC2 的 Instance Profile(IAM Role) VM 自己的身份。证书由 OCI 自动发放、轮换,VM 上不放密钥
Resource Principal Lambda 的执行 Role Function、DevOps Build Pipeline 等服务资源自己的身份
API Key Access Key 人从本机调用 API 用的密钥对(私钥在本机)
Auth Token (近似)CodeCommit 的 Git 凭证 git push、docker login 时当密码用
Vault KMS + Secrets Manager 保管加密密钥和机密
Master Encryption Key(主密钥) KMS Key 加密 Secret、bucket、磁盘等用的密钥。本身不交给任何人
Secret Secrets Manager 的 Secret 用主密钥加密保存的密码、Token 等
Audit CloudTrail 所有 API 调用的记录,默认就有
Logging(服务日志) CloudWatch Logs bucket 访问日志、VCN Flow Logs、Function 执行日志等
Connector Hub (近似)EventBridge Pipes / Firehose 把日志搬到别处(例:归档 bucket)
— IAM Identity Center AWS 侧:人用的单点登录,发临时凭证

2. 原理

2.1 Policy 的一句话语法

Allow dynamic-group tflab9-vm to read secret-bundles in compartment tf-lab where target.secret.id = '<Secret B>'
      └──── 谁 ────────────┘    └ 动词 ┘ └─ 资源类型 ─┘ └──── 范围 ─────┘ └──────────── 条件 ────────────────┘
  • 动词由弱到强:inspect(看列表)< read(读)< use(用,不能建删)< manage(全部)。选能完成工作的最小那个
  • 资源类型要选准:secret-bundles = Secret 的内容,secrets = Secret 的元数据;buckets = bucket 的设置,objects = bucket 里的文件
  • 范围:in compartment tf-lab 或 in tenancy
  • 条件(where):把对象限定到具体的那一个资源(target.bucket.name、target.secret.id、target.key.id)

2.2 程序怎么拿到身份

flowchart TB
  R["VM / Function / Build Pipeline"] -->|"OCI 自动发放证书<br/>定期轮换"| P["Instance / Resource Principal"]
  P -->|"符合 matching rule"| DG["Dynamic Group"]
  DG -->|"Policy 授权"| T["目标资源<br/>只限这一个 Secret / bucket"]

机器上没有任何长期有效的密钥:泄露风险、轮换的工作都没有了。这是"程序用自己的身份"的意思。

Dynamic Group 的成员规则越精确越好:

# 只有这一台 VM(09-secops/iam.tf,摘录)
matching_rule = "ALL {instance.id = '${oci_core_instance.vm.id}'}"

# tf-lab 里的所有 Function(08-guard/iam.tf,摘录)
matching_rule = "ALL {resource.type = 'fnfunc', resource.compartment.id = '${var.compartment_ocid}'}"

2.3 三条要记住的行为

  • 没权限和不存在都返回 404(NotAuthorizedOrNotFound):不泄露"这个资源存在不存在"。排查时两种可能都要想到
  • IAM 生效有延迟(秒〜分钟):同一次 apply 里"建 Policy → 马上用这个权限"可能失败。depends_on 只保证顺序,不保证已经生效
  • Dynamic Group 只能建在 tenancy(root)下,Policy 可以挂在 compartment 上

3. 最小权限:本仓库的实际写法

谁 能做什么 限定 出处
网站的 Build Pipeline 写网站 bucket 的对象;读代码仓库;读 AWS Key 的 Secret target.bucket.name、target.secret.id Phase 7 / 10
自动停网站的 Function 改网站 bucket 的设置(use buckets);读 Budget target.bucket.name Phase 8
私有 VM 读一个 Secret 的内容 target.secret.id Phase 9
Object Storage 服务 用主密钥加解密;执行生命周期规则 target.key.id、target.bucket.name Phase 9
Connector Hub 往归档 bucket 写对象 request.principal.type、target.bucket.name Phase 9
# Function 只能改 tflab-site 这一个 bucket 的设置(08-guard/iam.tf,摘录)
#   use buckets:能改设置(公开 / 私有),删不了 bucket,也读写不了里面的文件
statements = [
  "Allow dynamic-group ${oci_identity_dynamic_group.guard_fn.name} to use buckets in compartment ${data.oci_identity_compartment.lab.name} where target.bucket.name = '${var.site_bucket_name}'",
]

几个细节:

  • 名字不写死,引用资源的属性(oci_identity_dynamic_group.guard_fn.name):Terraform 才知道"先建 Dynamic Group 再建 Policy"
  • 服务本身也要权限:Object Storage 要用 CMK 加密,得给 service objectstorage-<region> 授 use keys;Functions 服务(faas)要拉镜像、接 subnet
  • 不是 Dynamic Group 成员的服务(Connector Hub):any-user + request.principal.type = 'serviceconnector' 限定

4. 机密:密码不经过 Terraform

4.1 Vault 的结构

Vault(保险库:容器,也是权限的边界。DEFAULT 类型免费)
 └─ 主密钥(SOFTWARE 保护:免费)
      ├─ 加密 Secret(密码、Token 等)
      └─ 加密 bucket / Block Volume 等(CMK)

信封加密(Envelope Encryption):数据用临时的数据密钥加密,数据密钥再用主密钥加密。所以主密钥一停用,数据就读不出来,这就是"自己管密钥"的意义。

4.2 谁来生成密码:两种 Secret 的对比

Phase 9 故意建了两个 Secret 做对比:

Secret A(反面教材) Secret B(推荐)
谁生成密码 Terraform(random_password) Vault 自己(enable_auto_generation)
plan 的显示 (sensitive value) (known after apply)
state 里 有明文 没有
# Secret B:Vault 自己生成,Terraform 只建"框"(09-secops/vault.tf,摘录)
resource "oci_vault_secret" "db_b" {
  vault_id               = oci_kms_vault.this.id
  key_id                 = oci_kms_key.master.id
  secret_name            = "${local.name_prefix}-db-password-gen"
  enable_auto_generation = true

  secret_generation_context {
    generation_type     = "PASSPHRASE"
    generation_template = "SECRETS_DEFAULT_PASSWORD"
    passphrase_length   = 24
  }
}

sensitive 只是「不显示」,不是「不保存」

(sensitive value) 只防被看到。state 文件、plan 文件(terraform show -json)里都有真值。 → 密码让 Vault(或 DB 服务)自己生成,Terraform 只建框;程序里只写 Secret 的 OCID,运行时用自己的身份去取。

4.3 外部的机密:Terraform 只建框,真值由脚本写入

网站 build 要往 AWS 上传,需要 AWS 的 Access Key(见第 7 节)。这个 Key 也不经过 Terraform:

# 07-site/aws.tf(摘录)
resource "oci_vault_secret" "aws_deployer" {
  secret_content {
    content_type = "BASE64"
    content      = base64encode(jsonencode({ placeholder = true }))   # 占位符
  }
  lifecycle {
    ignore_changes = [secret_content]   # 不然下次 apply 会把真值改回占位符
  }
}

真值由 tools/rotate_aws_deploy_key.py 写入:建新 Key → 写进 Secret → 成功后才删旧 Key(写入失败时删掉新 Key)。Key 不显示、不落盘。build 时上传脚本用 Resource Principal 读 Secret,Key 只在进程内存里,不进环境变量、不进 build 日志。

5. 加密

层 默认 本仓库的做法
静态数据(磁盘、bucket) 默认就加密(云厂商管理的密钥) 日志归档 bucket 用 CMK(kms_key_id = 主密钥)
VM ↔ boot volume 的传输 不加密 打开传输中加密(见 计算和负载均衡,要用 API 确认实际生效)
网站访问 — OCI 版是 oraclecloud.com 的 HTTPS;AWS 版是 ACM 证书 + 只允许 HTTPS

什么时候用 CMK(客户自管密钥):有"密钥由我方管理"的要求时。好处是能自己停用 / 轮换 / 删除密钥,而且每次使用都留下审计记录。学习环境用 SOFTWARE 保护的密钥(免费)就够;正式环境 / 合规要求再考虑 HSM。

AWS 侧的 state bucket 用的是 SSE-S3(默认加密),没用 KMS CMK:每把 1 USD / 月,这里不需要自己管密钥。

6. 日志和审计

flowchart TB
  A["Audit<br/>所有 API 调用"] --> C["Connector Hub"]
  S["网站 bucket<br/>访问日志"] --> C
  C -->|"最多 7 分钟写一个文件"| B["归档 bucket<br/>CMK 加密、30 天后删除"]
  F["VCN Flow Logs<br/>谁连了谁"] --> L["Logging<br/>保留 30 天,不归档"]
日志 内容 本仓库看到的
Audit(默认就有) 谁、什么时候、从哪里、对什么、结果 VM 读 Secret B → 200、读 A → 404;调用者是 VM(authType = instance)、IP 是 VM 的私有 IP
bucket 访问日志 每个请求的 IP、User-Agent、状态码 curl 3 次 → GET index.html 200 ×3,几分钟内出现
Flow Logs 网卡级的通信记录 Bastion → VM:22 ACCEPT;VM ↔ Oracle 服务 :443 ACCEPT
  • 被拒绝的操作也会被记录:这正是审计要的,"谁试过不该做的事"
  • Audit 的量比想象的多:terraform plan 一次就有几十条 GetXxx
  • 长期保存时算一下请求数:Connector 每分钟写一个文件,一个月就 4 万多次 PUT,接近 Object Storage 每月 5 万次的免费额度 → 用上限 7 分钟

Audit 的查询(Logging Search):

search "<tf-lab 的 OCID>/_Audit" | where data.eventName = 'GetSecretBundle'

7. AWS 侧:人和外部系统的身份

7.1 人:不用 root,不存长期 Key

  • root:开 MFA、确认没有 Access Key,以后不用
  • 日常用 IAM Identity Center 的用户:从 access portal 登录,本机 CLI 用 aws configure sso 拿临时凭证(session 4 小时),磁盘上没有长期有效的 Key
  • AdministratorAccess ≒ root,但不完全一样:改 root 邮箱、关闭账号等只有 root 能做

7.2 外部系统:只能用 Access Key 时的补偿措施

OCI 的 build runner 不是 AWS 的资源,用不了 IAM Role,只能发 IAM 用户的 Access Key(长期有效)。于是:

措施 内容
最小权限 只能列 / 写 / 删网站 bucket 的对象、清这个 distribution 的缓存。连读都不给
Key 不经过 Terraform aws_iam_access_key 的 secret 会明文留在 state 里 → 用脚本建,直接写进 OCI Vault
OCI 侧只能读这一个 Secret read secret-bundles ... where target.secret.id = '<这个 Secret>'
定期轮换 约 90 天一次,脚本轮换(新 Key 写入成功后才删旧 Key)
# 10-aws-site/site/deploy.tf(摘录)
statement {
  sid       = "WriteSiteObjects"
  actions   = ["s3:PutObject", "s3:DeleteObject"]
  resources = ["${aws_s3_bucket.site.arn}/*"]
}
statement {
  sid       = "InvalidateSiteCache"
  actions   = ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"]
  resources = [aws_cloudfront_distribution.site.arn]
}

8. 本机和仓库

  • API Key 的私钥放 ~/.oci/,绝对不进项目目录和 Git
  • .gitignore 从一开始就排除:*.tfstate(可能有密码)、*.tfvars(OCID、IP 等环境信息,只提交 *.example)、*.pem / *.key、backend.hcl
  • plan 文件里也有真值 → 用完就删
  • 网站发布前用 tools/prepublish_check.py 扫描真实 IP、OCID、namespace、邮箱等,网站上只用占位符

9. 设计决策一览

决策 选了 没选 理由
程序访问 OCI API Instance / Resource Principal 在机器上放 API Key 没有长期密钥,不用轮换,泄露不了
权限的范围 where 限定到具体资源 + 最小动词 整个 compartment 的 manage 被盗用时影响只限于那一个资源
Dynamic Group 的成员 按 OCID / 资源类型 + compartment 精确圈定 compartment 里的所有资源 别的 VM 不会顺带拿到权限
密码的生成 Vault 自动生成 Terraform 的 random_password state 里不留明文
外部的密钥(AWS Key) 脚本写入 Vault,Terraform 只建框 + ignore_changes aws_iam_access_key 写进 state 同上;还能脚本化轮换
主密钥的保护方式 SOFTWARE(免费) HSM / 独占 Vault 学习环境够用;正式环境按要求选
日志的长期保存 Connector Hub → CMK 加密 bucket + 30 天自动删除 只用 Logging 的默认保留 能自己管密钥和保存期;用生命周期控制费用
AWS 的日常登录 Identity Center + 临时凭证 root / IAM 用户的长期 Key 本机磁盘上没有长期 Key

10. 验证

权限设计一定要做反向测试:确认不该能做的事确实做不了。

要证明的事 怎么做 结果
VM 能读被授权的 Secret VM 上 --auth instance_principal 读 Secret B 成功(只看长度,不 echo 密码)
VM 读不了没授权的 Secret(反向) 读 Secret A NotAuthorizedOrNotFound(404)
审计里留下了记录 Logging Search 查 GetSecretBundle B → 200、A → 404,调用者是 VM
state 里有没有明文 terraform state pull,只看有没有 A 和 random_password 有,B 没有
AWS deployer 能部署 列 / PUT / DELETE / 清缓存 成功
AWS deployer 做不了别的(反向) 读对象、碰 state bucket、删 bucket policy、列 bucket、看 IAM 全部 AccessDenied
state bucket 拒绝不安全的访问(反向) 匿名访问 / 用 HTTP 访问 403 / 被 bucket policy 明确拒绝
只建框的 Secret 不被覆盖 写入真值后再 plan No changes(ignore_changes 生效)

11. 坑

现象 原因 对策
读 Secret → 404 没权限和不存在都返回 404 查 Policy(read secret-bundles)和 Dynamic Group 的规则
新建 CMK 加密的 bucket → 404-NotAuthorizedOrFoundKmsKey 同一次 apply 刚建的 Policy 还没生效 过一会儿再 apply
Build 读不到代码仓库(RelatedResourceNotAuthorizedOrNotFound) repos 是 OCIR(镜像仓库)的资源类型 代码仓库要 read devops-repository
(sensitive value) 的密码在 state 里是明文 sensitive 只是不显示 让 Vault 生成
destroy 后同名 Secret 建不了 Vault / 密钥 / Secret 进入"计划删除"(7〜30 天) 换名字,或等删除完成
destroy 时归档 bucket 删不掉 bucket 不空,Connector 还在往里写 先停 Connector → 清空 bucket → destroy 其余
OCI config → did not find a proper configuration for private key config 不支持行尾注释,# TODO 被当成路径 删掉 # TODO
Terraform 报 failed to refresh cached SSO token,CLI 却能用 AWS 的 SSO token 过期 aws sso login --profile <profile>

12. 自测问题

1. 一个 Function 要把某个 bucket 改成私有,Policy 怎么写最合适?

Allow dynamic-group <Function 的组> to use buckets in compartment <名字> where target.bucket.name = '<那个 bucket>'。use buckets 能改设置,但删不了 bucket,也读写不了里面的文件。

2. VM 要读 Vault 里的密码,需要主密钥的权限吗?

不需要。解密在 Vault 内部完成,VM 只要 read secret-bundles(Secret 的内容)的权限,最好再用 where target.secret.id 限定到那一个 Secret。

3. Terraform 里用 random_password 生成 DB 密码,输出设成 sensitive,安全吗?

不够。sensitive 只是不显示,state 文件和 plan 文件里都有明文。应该让 Vault 或 DB 服务自己生成,Terraform 只建框。

4. 读 Secret 返回 404,可能是什么原因?

Secret 不存在,或者没有权限。OCI 对两种情况都返回 404。要检查 Policy、Dynamic Group 的成员规则,以及 IAM 是否已经生效。

5. 为什么 VM / Function 不放 API Key,而用 Instance / Resource Principal?

API Key 是长期有效的,放在机器上就有泄露风险,还要自己轮换。Principal 的证书由 OCI 自动发放和轮换,机器上没有长期密钥。

6. OCI 的 build 往 AWS 上传,为什么只能用 Access Key?怎么降低风险?

OCI 的 build runner 不是 AWS 的资源,不能扮演 IAM Role。降低风险:权限最小化、Key 不经过 Terraform(放 OCI Vault)、OCI 侧只能读这一个 Secret、定期轮换。

7. 怎么证明「VM 只能读 Secret B」?

正向:VM 读 B 成功。反向:VM 读 A 返回 404。再在 Audit 里确认两次调用都被记录(200 / 404)。

8. CMK 和默认加密有什么区别?什么时候需要 CMK?

默认加密的密钥由云厂商管理;CMK 由自己管理,可以自己停用 / 轮换 / 删除,每次使用都有审计记录。有"密钥由我方管理"的要求时才需要。

完整代码

09-secops/iam.tf(Dynamic Group + Policy)
# ------------------------------------------------------------
# 9-2:VM 用 Instance Principal 读 Secret
#   Instance Principal = VM 本身的身份(证书由 OCI 自动发放、轮换)→ VM 上不放 API Key
#   (Phase 7 的 Build Pipeline / Phase 8 的 Function 用的 Resource Principal 是同一个思路)
# ------------------------------------------------------------

# tf-lab 的父 = tenancy(Dynamic Group 只能建在 tenancy 下)
data "oci_identity_compartment" "lab" {
  id = var.compartment_ocid
}

# 成员只有这一台 VM(按 OCID)。VM 重建后 OCID 会变 → 规则跟着更新
resource "oci_identity_dynamic_group" "vm" {
  compartment_id = data.oci_identity_compartment.lab.compartment_id
  name           = "${local.name_prefix}-vm"
  description    = "The 09-secops VM (${oci_core_instance.vm.display_name})"
  matching_rule  = "ALL {instance.id = '${oci_core_instance.vm.id}'}"

  freeform_tags = local.common_tags
}

# 只允许读 Secret B 的内容(secret-bundles = Secret 的内容;secrets = 元数据)
#   Secret A 故意不授权 → VM 上读 A 被拒绝 = 最小权限的确认
#   解密由 Vault 服务内部完成 → VM 不需要主密钥的权限
resource "oci_identity_policy" "vm" {
  compartment_id = var.compartment_ocid
  name           = "${local.name_prefix}-vm"
  description    = "09-secops VM reads one secret"

  statements = [
    "Allow dynamic-group ${oci_identity_dynamic_group.vm.name} to read secret-bundles in compartment ${data.oci_identity_compartment.lab.name} where target.secret.id = '${oci_vault_secret.db_b.id}'",
  ]

  freeform_tags = local.common_tags
}

# ------------------------------------------------------------
# 9-3:日志归档用的权限(都限定在这一把密钥 / 这一个 bucket)
#   Object Storage 服务(objectstorage-<region>):
#     - use keys:用主密钥加密 / 解密 bucket 里的数据(CMK)
#     - manage object-family:执行生命周期规则(30 天后删除)
#   Connector Hub:服务本身不是 dynamic group 的成员 → any-user + request.principal.type 限定
# ------------------------------------------------------------
locals {
  archive_bucket_name = "${local.name_prefix}-log-archive"
}

resource "oci_identity_policy" "archive" {
  compartment_id = var.compartment_ocid
  name           = "${local.name_prefix}-archive"
  description    = "09-secops log archive: CMK, lifecycle, connector writes"

  statements = [
    "Allow service objectstorage-${var.region} to use keys in compartment ${data.oci_identity_compartment.lab.name} where target.key.id = '${oci_kms_key.master.id}'",
    "Allow service objectstorage-${var.region} to manage object-family in compartment ${data.oci_identity_compartment.lab.name} where target.bucket.name = '${local.archive_bucket_name}'",
    "Allow any-user to manage objects in compartment ${data.oci_identity_compartment.lab.name} where all {request.principal.type = 'serviceconnector', target.bucket.name = '${local.archive_bucket_name}', request.principal.compartment.id = '${var.compartment_ocid}'}",
  ]

  freeform_tags = local.common_tags
}
09-secops/vault.tf(Vault / 主密钥 / 两种 Secret)
# ------------------------------------------------------------
# 9-2:Vault + 主密钥 + Secret
#
#   Vault(DEFAULT:和别人共用 HSM 的分区,免费)
#    └─ 主密钥(SOFTWARE 保护:免费;HSM 保护只有 20 个版本免费)
#         ├─ Secret A:Terraform 的 random_password 生成 → 放进 Vault(反面教材:state 里有明文)
#         ├─ Secret B:Vault 自己生成(auto-generation)→ Terraform 只管"框",state 里没有密码
#         └─ 9-3:日志归档 bucket 的 CMK
#
# ⚠ 删除:destroy 后不会马上消失,进入"计划删除"(7〜30 天)
#   这期间同名的 Secret 不能再建(名字被占着)。软件密钥不收费
# ------------------------------------------------------------

resource "oci_kms_vault" "this" {
  compartment_id = var.compartment_ocid
  display_name   = "${local.name_prefix}-vault"
  vault_type     = "DEFAULT" # VIRTUAL_PRIVATE(独占 HSM 分区)不在免费范围

  freeform_tags = local.common_tags
}

# 主密钥:AES-256(length 的单位是字节 → 32)
#   management_endpoint:密钥的管理 API 是每个 Vault 自己的 endpoint
resource "oci_kms_key" "master" {
  compartment_id      = var.compartment_ocid
  display_name        = "${local.name_prefix}-master-key"
  management_endpoint = oci_kms_vault.this.management_endpoint
  protection_mode     = "SOFTWARE"

  key_shape {
    algorithm = "AES"
    length    = 32
  }

  freeform_tags = local.common_tags
}

# ---- Secret A:Terraform 生成(反面教材)----
#   random_password 的值是 sensitive → plan / apply 的输出里不显示
#   但 state 里是明文(terraform state show 也只是遮住,state 文件本身能看到)
resource "random_password" "db_a" {
  length  = 24
  special = true
}

resource "oci_vault_secret" "db_a" {
  compartment_id = var.compartment_ocid
  vault_id       = oci_kms_vault.this.id
  key_id         = oci_kms_key.master.id
  secret_name    = "${local.name_prefix}-db-password-tf"
  description    = "DB password generated by Terraform (plaintext in state)"

  secret_content {
    content_type = "BASE64"
    content      = base64encode(random_password.db_a.result)
  }

  freeform_tags = local.common_tags
}

# ---- Secret B:Vault 自己生成(推荐)----
#   generation_type / generation_template 的值:SDK 的 PassphraseGenerationContext 查到
#   密码只在 Vault 里 → Terraform 的 state、plan、本机都碰不到
resource "oci_vault_secret" "db_b" {
  compartment_id         = var.compartment_ocid
  vault_id               = oci_kms_vault.this.id
  key_id                 = oci_kms_key.master.id
  secret_name            = "${local.name_prefix}-db-password-gen"
  description            = "DB password generated by Vault (not in Terraform state)"
  enable_auto_generation = true

  secret_generation_context {
    generation_type     = "PASSPHRASE"
    generation_template = "SECRETS_DEFAULT_PASSWORD"
    passphrase_length   = 24
  }

  freeform_tags = local.common_tags
}
09-secops/logging.tf(访问日志 / Flow Logs / 归档)
# ------------------------------------------------------------
# 9-3:日志
#   ① 网站 bucket(07-site 的 tflab-site)的访问日志(Object Storage read)
#   ② VCN Flow Logs(09 的 subnet)
#   ③ Audit 日志:默认就有(365 天),不用建 → Logging Search 里查
#   ④ Connector Hub:① + ③ → 归档 bucket(9-2 的主密钥加密 = CMK、30 天后自动删除)
#
# 类别名(read / write、all):LoggingManagementClient.list_services() 查到
# 日志量很小 → Logging 每月 10GB 免费额度内;Connector 免费 2 个,这里用 1 个
# ------------------------------------------------------------

data "oci_objectstorage_namespace" "ns" {
  compartment_id = var.compartment_ocid
}

resource "oci_logging_log_group" "this" {
  compartment_id = var.compartment_ocid
  display_name   = "${local.name_prefix}-logs"

  freeform_tags = local.common_tags
}

# ① 网站的访问日志:谁(IP、User-Agent)在什么时候 GET 了哪个对象
#   bucket 是 07-site 管理的 → 这里只按名字引用(日志设定集中在 09)
resource "oci_logging_log" "site_read" {
  log_group_id       = oci_logging_log_group.this.id
  display_name       = "${local.name_prefix}-site-read"
  log_type           = "SERVICE"
  is_enabled         = true
  retention_duration = 30 # 天(最短 30)

  configuration {
    source {
      category    = "read"
      resource    = var.site_bucket_name
      service     = "objectstorage"
      source_type = "OCISERVICE"
    }
  }

  freeform_tags = local.common_tags
}

# ② VCN Flow Logs:subnet 里每个 VNIC 的通信(来源 / 目的 IP、端口、ACCEPT / REJECT)
resource "oci_logging_log" "flow" {
  log_group_id       = oci_logging_log_group.this.id
  display_name       = "${local.name_prefix}-flow"
  log_type           = "SERVICE"
  is_enabled         = true
  retention_duration = 30

  configuration {
    source {
      category    = "all"
      resource    = oci_core_subnet.private.id
      service     = "flowlogs"
      source_type = "OCISERVICE"
    }
  }

  freeform_tags = local.common_tags
}

# ---- ④ 归档 ----

# 归档 bucket:9-2 的主密钥加密(CMK)
#   Object Storage 服务要先有 use keys 的权限(iam.tf 的 archive policy)→ depends_on
# ⚠ destroy:bucket 里有对象时删不掉 → 先清空(Console 或 SDK)
resource "oci_objectstorage_bucket" "archive" {
  #checkov:skip=CKV_OCI_8:Log archive objects are write-once; versioning is not needed
  #checkov:skip=CKV_OCI_7:No event consumers for the archive bucket
  compartment_id = var.compartment_ocid
  namespace      = data.oci_objectstorage_namespace.ns.namespace
  name           = local.archive_bucket_name # iam.tf 的 policy 里也用这个名字
  access_type    = "NoPublicAccess"
  storage_tier   = "Standard"
  versioning     = "Disabled"
  kms_key_id     = oci_kms_key.master.id

  freeform_tags = local.common_tags

  depends_on = [oci_identity_policy.archive]
}

# 30 天后自动删除(存储费用 / 10GB 免费额度)
#   生命周期由 Object Storage 服务执行 → 它也要权限(iam.tf)
resource "oci_objectstorage_object_lifecycle_policy" "archive" {
  namespace = data.oci_objectstorage_namespace.ns.namespace
  bucket    = oci_objectstorage_bucket.archive.name

  rules {
    name        = "delete-after-30-days"
    action      = "DELETE"
    is_enabled  = true
    target      = "objects"
    time_amount = 30
    time_unit   = "DAYS"
  }

  depends_on = [oci_identity_policy.archive]
}

# Connector Hub:日志 → 归档 bucket(gzip 的 JSON 文件)
#   Audit 的 log group 是固定名 "_Audit"(compartment 单位)
#   batch_rollover_time:最多 7 分钟写一个文件(每分钟的话一个月最多 4 万多次请求,接近 5 万次的免费额度)
#     范围 60000〜420000 ms(10 分钟 → 400 InvalidParameter)→ 取上限 7 分钟:一个月最多约 6,200 个文件
resource "oci_sch_service_connector" "archive" {
  compartment_id = var.compartment_ocid
  display_name   = "${local.name_prefix}-log-archive"
  description    = "site access logs + audit -> ${oci_objectstorage_bucket.archive.name}"

  source {
    kind = "logging"

    log_sources {
      compartment_id = var.compartment_ocid
      log_group_id   = oci_logging_log_group.this.id
      log_id         = oci_logging_log.site_read.id
    }

    log_sources {
      compartment_id = var.compartment_ocid
      log_group_id   = "_Audit"
    }
  }

  target {
    kind                       = "objectStorage"
    namespace                  = data.oci_objectstorage_namespace.ns.namespace
    bucket                     = oci_objectstorage_bucket.archive.name
    object_name_prefix         = "logs"
    batch_rollover_time_in_ms  = 420000 # 7 分钟(上限)
    batch_rollover_size_in_mbs = 100
  }

  freeform_tags = local.common_tags

  depends_on = [oci_identity_policy.archive]
}
08-guard/iam.tf(Function 的权限)
# ------------------------------------------------------------
# Phase 8:权限
#   Function 用 Resource Principal(和 Phase 7 的 Build Pipeline 一样,不发 API Key)
# ------------------------------------------------------------

# tf-lab 的父 = tenancy(Dynamic Group 只能建在 tenancy 下)
data "oci_identity_compartment" "lab" {
  id = var.compartment_ocid
}

# tf-lab 里的 Function 都算这个 Dynamic Group 的成员
resource "oci_identity_dynamic_group" "guard_fn" {
  compartment_id = data.oci_identity_compartment.lab.compartment_id
  name           = "${var.project_name}-guard-fn"
  description    = "Functions in ${data.oci_identity_compartment.lab.name}"
  matching_rule  = "ALL {resource.type = 'fnfunc', resource.compartment.id = '${var.compartment_ocid}'}"

  freeform_tags = local.common_tags
}

# 挂在 tf-lab 上
#   Function:use buckets + where bucket 名 → 只能读 / 改 tflab-site 的设置(删不了 bucket、碰不到对象和其他 bucket)
#   Functions 服务本身(faas):把 Function 接到 subnet 上、从 OCIR 拉镜像
#   Events 服务(cloudEvents):Budget 告警的事件发到 Topic
resource "oci_identity_policy" "guard" {
  compartment_id = var.compartment_ocid
  name           = "${var.project_name}-guard"
  description    = "site-guard function and the Functions service"

  statements = [
    "Allow dynamic-group ${oci_identity_dynamic_group.guard_fn.name} to use buckets in compartment ${data.oci_identity_compartment.lab.name} where target.bucket.name = '${var.site_bucket_name}'",
    "Allow service faas to use virtual-network-family in compartment ${data.oci_identity_compartment.lab.name}",
    "Allow service faas to read repos in compartment ${data.oci_identity_compartment.lab.name}",
    # Events(Budget 告警的规则)往 tf-lab 的告警 Topic 发消息
    "Allow service cloudEvents to use ons-topics in compartment ${data.oci_identity_compartment.lab.name}",
  ]

  freeform_tags = local.common_tags
}
07-site/aws.tf(AWS Key 的 Secret:只建框)
# ------------------------------------------------------------
# Phase 10-4:build 后也上传到 AWS(10-aws-site/site 的 S3 + CloudFront)
#   AWS 的 Access Key(IAM 用户 tflab-site-deployer)放在这里的 Vault Secret 里
#   build runner 用 Resource Principal 读这个 Secret → 代码、build_spec、build 日志里都没有 Key
#
#   Vault(DEFAULT,免费)
#    └─ 主密钥(SOFTWARE,免费)
#         └─ Secret:{"access_key_id": ..., "secret_access_key": ...}(JSON)
#
#   Secret 的内容不经过 Terraform:
#     Terraform 只建 Secret(内容是占位符)+ ignore_changes
#     真值由 tools/rotate_aws_deploy_key.py 写成新版本(不然下次 apply 会把真值改回占位符)
#
# ⚠ 删除:destroy 后进入"计划删除"(7〜30 天),这期间同名的 Secret 不能再建
# ------------------------------------------------------------

resource "oci_kms_vault" "site" {
  compartment_id = var.compartment_ocid
  display_name   = "${var.project_name}-site-vault"
  vault_type     = "DEFAULT"

  freeform_tags = local.common_tags
}

resource "oci_kms_key" "site" {
  compartment_id      = var.compartment_ocid
  display_name        = "${var.project_name}-site-key"
  management_endpoint = oci_kms_vault.site.management_endpoint
  protection_mode     = "SOFTWARE"

  key_shape {
    algorithm = "AES"
    length    = 32
  }

  freeform_tags = local.common_tags
}

resource "oci_vault_secret" "aws_deployer" {
  compartment_id = var.compartment_ocid
  vault_id       = oci_kms_vault.site.id
  key_id         = oci_kms_key.site.id
  secret_name    = "${var.project_name}-aws-site-deployer"
  description    = "AWS access key of IAM user tflab-site-deployer (written by tools/rotate_aws_deploy_key.py)"

  secret_content {
    content_type = "BASE64"
    content      = base64encode(jsonencode({ placeholder = true }))
  }

  lifecycle {
    ignore_changes = [secret_content]
  }

  freeform_tags = local.common_tags
}
10-aws-site/site/deploy.tf(AWS 部署用 IAM 用户)
# ------------------------------------------------------------
# Phase 10-4:OCI DevOps 的 build 往这个网站上传用的 IAM 用户
#   OCI 的 build runner 不是 AWS 的资源 → 用不了 IAM Role(AWS 里"程序用自己的身份"的做法)
#   → 只能发 Access Key(长期有效)。所以把能做的事限制到最小:
#     只能列 / 写 / 删网站 bucket 的对象、只能清这个 distribution 的缓存(bucket 本身、其他资源都碰不到)
#   Access Key 不在 Terraform 里建:aws_iam_access_key 的 secret 会以明文留在 state 里
#     → tools/rotate_aws_deploy_key.py 建 Key 后直接写进 OCI Vault 的 Secret(本机不落盘、不显示)
# ------------------------------------------------------------

resource "aws_iam_user" "deployer" {
  name = "tflab-site-deployer"
  path = "/tflab/"
}

data "aws_iam_policy_document" "deployer" {
  # 同步时要比较 bucket 里现有对象的 ETag(list_objects_v2)
  statement {
    sid       = "ListSiteBucket"
    actions   = ["s3:ListBucket"]
    resources = [aws_s3_bucket.site.arn]
  }

  statement {
    sid       = "WriteSiteObjects"
    actions   = ["s3:PutObject", "s3:DeleteObject"]
    resources = ["${aws_s3_bucket.site.arn}/*"]
  }

  statement {
    sid       = "InvalidateSiteCache"
    actions   = ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"]
    resources = [aws_cloudfront_distribution.site.arn]
  }
}

# 用户内联 policy(只给这个用户用,不需要单独的托管 policy)
resource "aws_iam_user_policy" "deployer" {
  name   = "site-deploy"
  user   = aws_iam_user.deployer.name
  policy = data.aws_iam_policy_document.deployer.json
}