安全¶
一句话
安全设计围绕三件事:身份(是谁)、权限(能做什么,越少越好)、证据(做了什么都有记录)。
本仓库的原则:程序不拿长期有效的密钥,用自己的身份(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):
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
}