セキュリティ¶
ひとことで
セキュリティ設計は 3 つを軸にします:ID(誰か)、権限(何ができるか。少ないほどよい)、証跡(何をしたかがすべて記録される)。
このリポジトリの原則:プログラムには長期間有効なキーを持たせず、自分の ID(インスタンス・プリンシパル / リソース・プリンシパル)を使う。権限は where で特定の 1 つのリソースに絞る。パスワードは Terraform を経由させない。
このページは Phase 0、Phase 7、Phase 8、Phase 9、Phase 10 の実験をまとめたものです。ネットワーク層のアクセス制御(セキュリティ・リスト / NSG / 要塞)は ネットワーク設計、VM 自体の設定(IMDS、転送中の暗号化)は コンピュートとロード・バランサ を参照してください。
1. まず構成要素を知る¶
| OCI | AWS | ひとことで |
|---|---|---|
| コンパートメント(Compartment) | (近い)リソース・グループ / アカウントの分割 | リソースを入れる「フォルダ」で、権限と費用の境界でもある。このリポジトリの演習リソースはすべて tf-lab に置く |
| ユーザー / グループ(User / Group) | IAM User / Group | 人の ID。権限はグループに与え、ユーザーに直接は与えない |
| ポリシー(Policy) | IAM Policy | 権限を1 文で書く:Allow <誰> to <動詞> <リソース> in <範囲> where <条件> |
| 動的グループ(Dynamic Group) | (近い)IAM Role の信頼関係 | ルールでリソースのまとまりを指定し(例:この VM、このコンパートメント内の Function)、それらにも権限を与えられるようにする |
| インスタンス・プリンシパル(Instance Principal) | EC2 のインスタンス・プロファイル(IAM Role) | VM 自身の ID。証明書は OCI が自動で発行・ローテーションし、VM にキーを置かない |
| リソース・プリンシパル(Resource Principal) | Lambda の実行ロール | Function、DevOps のビルド・パイプラインなどのサービス・リソース自身の ID |
| API キー(API Key) | アクセス・キー | 人がローカル PC から API を呼ぶためのキー・ペア(秘密鍵はローカル PC) |
| 認証トークン(Auth Token) | (近い)CodeCommit の Git 認証情報 | git push、docker login のときにパスワードとして使う |
| ボールト(Vault) | KMS + Secrets Manager | 暗号鍵と機密情報を保管する |
| マスター暗号化キー(Master Encryption Key) | KMS キー | シークレット、バケット、ディスクなどの暗号化に使うキー。キー自体は誰にも渡さない |
| シークレット(Secret) | Secrets Manager のシークレット | マスター暗号化キーで暗号化して保存するパスワード、トークンなど |
| 監査(Audit) | CloudTrail | すべての API 呼び出しの記録。デフォルトで有効 |
| ロギング(Logging、サービス・ログ) | CloudWatch Logs | バケットのアクセス・ログ、VCN フロー・ログ、Function の実行ログなど |
| コネクタ・ハブ(Connector Hub) | (近い)EventBridge Pipes / Firehose | ログを別の場所へ運ぶ(例:アーカイブ用バケット) |
| — | IAM Identity Center | AWS 側:人が使うシングル・サインオン。一時的な認証情報を発行する |
2. しくみ¶
2.1 ポリシーは 1 文で書く¶
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= シークレットの内容、secrets= シークレットのメタデータ。buckets= バケットの設定、objects= バケット内のファイル - 範囲:
in compartment tf-labまたはin tenancy - 条件(
where):対象を特定の 1 つのリソースに絞る(target.bucket.name、target.secret.id、target.key.id)
2.2 プログラムはどうやって ID を得るか¶
flowchart TB
R["VM / Function<br/>ビルド・パイプライン"] -->|"OCI が証明書を自動発行<br/>定期的にローテーション"| P["インスタンス /<br/>リソース・プリンシパル"]
P -->|"マッチング・ルールに合致"| DG["動的グループ"]
DG -->|"ポリシーで許可"| T["対象リソース<br/>この 1 つの<br/>シークレット / バケット<br/>だけ"]
マシンには長期間有効なキーが一切ありません。漏えいのリスクも、ローテーションの手間もなくなります。これが「プログラムは自分の ID を使う」という意味です。
動的グループのメンバー・ルールはできるだけ厳密にします:
# この 1 台の 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 覚えておくべき 3 つの挙動¶
- 権限がない場合も、存在しない場合も 404 を返す(
NotAuthorizedOrNotFound):「このリソースが存在するかどうか」を漏らさないため。調査のときは両方の可能性を考える - IAM の反映には遅延がある(数秒〜数分):同じ apply の中で「ポリシーを作成 → すぐにその権限を使う」と失敗することがある。
depends_onが保証するのは順序だけで、反映済みであることは保証しない - 動的グループはテナンシ(ルート)直下にしか作れない。ポリシーはコンパートメントにアタッチできる
3. 最小権限:このリポジトリの実際の書き方¶
| 誰 | できること | 絞り込み | 出典 |
|---|---|---|---|
| サイトのビルド・パイプライン | サイトのバケットへのオブジェクトの書き込み、コード・リポジトリの読み取り、AWS キーのシークレットの読み取り | target.bucket.name、target.secret.id |
Phase 7 / 10 |
| サイトを自動停止する Function | サイトのバケットの設定の変更(use buckets)、予算の読み取り |
target.bucket.name |
Phase 8 |
| プライベート VM | 1 つのシークレットの内容の読み取り | target.secret.id |
Phase 9 |
| オブジェクト・ストレージ・サービス | マスター暗号化キーでの暗号化・復号、ライフサイクル・ルールの実行 | target.key.id、target.bucket.name |
Phase 9 |
| コネクタ・ハブ | アーカイブ用バケットへのオブジェクトの書き込み | request.principal.type、target.bucket.name |
Phase 9 |
# Function が変更できるのは tflab-site バケットの設定だけ(08-guard/iam.tf、抜粋)
# use buckets:設定(公開 / 非公開)は変更できるが、バケットの削除や中のファイルの読み書きはできない
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 が「動的グループを作ってからポリシーを作る」と判断できる - サービス自体にも権限が要る:オブジェクト・ストレージで CMK 暗号化を使うには、
service objectstorage-<region>にuse keysを許可する。ファンクション・サービス(faas)にはイメージの取得とサブネットへの接続の権限が要る - 動的グループのメンバーにならないサービス(コネクタ・ハブ):
any-user+request.principal.type = 'serviceconnector'で絞り込む
4. 機密情報:パスワードは Terraform を経由させない¶
4.1 ボールトの構造¶
ボールト(金庫:入れ物であり、権限の境界でもある。DEFAULT タイプは無料)
└─ マスター暗号化キー(SOFTWARE 保護:無料)
├─ シークレット(パスワード、トークンなど)を暗号化
└─ バケット / ブロック・ボリュームなどを暗号化(CMK)
エンベロープ暗号化(Envelope Encryption):データは一時的なデータ暗号化キーで暗号化し、そのデータ暗号化キーをマスター暗号化キーで暗号化します。そのためマスター暗号化キーを無効にすると、データは読めなくなります。これが「キーを自分で管理する」ことの意味です。
4.2 パスワードを誰が生成するか:2 種類のシークレットの比較¶
Phase 9 ではあえて 2 つのシークレットを作って比較しました:
| シークレット A(悪い例) | シークレット B(推奨) | |
|---|---|---|
| パスワードを生成するのは | Terraform(random_password) |
ボールト自身(enable_auto_generation) |
| plan の表示 | (sensitive value) |
(known after apply) |
| state の中 | 平文がある | ない |
# シークレット B:ボールト自身が生成し、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)にも本当の値が入っています。
→ パスワードはボールト(または DB サービス)に生成させ、Terraform は枠だけを作ります。プログラムにはシークレットの OCID だけを書き、実行時に自分の ID で取得します。
4.3 外部の機密情報:Terraform は枠だけを作り、本当の値はスクリプトで書き込む¶
サイトのビルドは AWS にもアップロードするため、AWS のアクセス・キーが必要です(7 章を参照)。このキーも 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 で書き込みます:新しいキーを作成 → シークレットに書き込む → 成功してから古いキーを削除(書き込みに失敗したら新しいキーを削除)。キーは画面に表示せず、ディスクにも書きません。ビルド時はアップロード用のスクリプトがリソース・プリンシパルでシークレットを読み、キーはプロセスのメモリにだけ置きます。環境変数やビルド・ログには出しません。
5. 暗号化¶
| 層 | デフォルト | このリポジトリのやり方 |
|---|---|---|
| 保存データ(ディスク、バケット) | デフォルトで暗号化(クラウド事業者が管理するキー) | ログのアーカイブ用バケットは CMK(kms_key_id = マスター暗号化キー) |
| VM ↔ ブート・ボリュームの転送 | 暗号化なし | 転送中の暗号化を有効化(コンピュートとロード・バランサ を参照。API で実際に有効になったかを確認する必要がある) |
| サイトへのアクセス | — | OCI 版は oraclecloud.com の HTTPS。AWS 版は ACM 証明書 + HTTPS のみ許可 |
CMK(顧客管理キー)を使うのはいつか:「キーは自分たちで管理する」という要件があるときです。キーを自分で無効化 / ローテーション / 削除でき、使用のたびに監査記録が残るのが利点です。学習環境では SOFTWARE 保護のキー(無料)で十分で、本番環境やコンプライアンス要件があれば HSM を検討します。
AWS 側の state バケットは SSE-S3(デフォルトの暗号化)で、KMS の CMK は使っていません。1 本あたり 1 USD / 月かかり、ここではキーを自分で管理する必要がないためです。
6. ログと監査¶
flowchart TB
A["監査<br/>すべての API 呼び出し"] --> C["コネクタ・ハブ"]
S["サイトのバケット<br/>アクセス・ログ"] --> C
C -->|"最大 7 分ごとに 1 ファイル"| B["アーカイブ用バケット<br/>CMK で暗号化<br/>30 日後に削除"]
F["VCN フロー・ログ<br/>誰が誰に接続したか"] --> L["ロギング<br/>30 日保持<br/>アーカイブしない"]
| ログ | 内容 | このリポジトリで見えたもの |
|---|---|---|
| 監査(デフォルトで有効) | 誰が、いつ、どこから、何に対して、結果はどうだったか | VM がシークレット B を読む → 200、A を読む → 404。呼び出し元は VM(authType = instance)、IP は VM のプライベート IP |
| バケットのアクセス・ログ | リクエストごとの IP、User-Agent、ステータス・コード | curl 3 回 → GET index.html 200 ×3。数分以内に表示 |
| フロー・ログ | NIC レベルの通信記録 | 要塞 → VM:22 が ACCEPT。VM ↔ Oracle サービス :443 が ACCEPT |
- 拒否された操作も記録される:これこそ監査に必要なもの。「誰がやってはいけないことを試したか」
- 監査ログの量は想像より多い:
terraform plan1 回で数十件のGetXxx - 長期保存するときはリクエスト数を計算する:コネクタが毎分 1 ファイル書くと月に 4 万回以上の PUT になり、オブジェクト・ストレージの月 5 万回の無料枠に近づく → 上限の 7 分にする
監査ログのクエリ(ログ検索):
7. AWS 側:人と外部システムの ID¶
7.1 人:root を使わず、長期間有効なキーを保存しない¶
- root:MFA を有効にし、アクセス・キーがないことを確認して、以後は使わない
- 日常は IAM Identity Center のユーザーを使う:アクセス・ポータルからサインインし、ローカル PC の CLI は
aws configure ssoで一時的な認証情報(セッション 4 時間)を取得する。ディスクに長期間有効なキーは置かない AdministratorAccess≒ root だが同じではない:root のメールアドレスの変更やアカウントの解約などは root にしかできない
7.2 外部システム:アクセス・キーしか使えない場合の補完策¶
OCI のビルド・ランナーは AWS のリソースではないため IAM Role が使えず、IAM ユーザーのアクセス・キー(長期間有効)を発行するしかありません。そこで:
| 対策 | 内容 |
|---|---|
| 最小権限 | できるのはサイトのバケットのオブジェクトの一覧 / 書き込み / 削除と、この distribution のキャッシュ削除だけ。読み取りすら与えない |
| キーは Terraform を経由させない | aws_iam_access_key の secret は state に平文で残る → スクリプトで作成し、直接 OCI のボールトに書き込む |
| OCI 側で読めるのはこの 1 つのシークレットだけ | read secret-bundles ... where target.secret.id = '<このシークレット>' |
| 定期的なローテーション | 約 90 日ごとに、スクリプトでローテーション(新しいキーの書き込みに成功してから古いキーを削除) |
# 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. ローカル PC とリポジトリ¶
- API キーの秘密鍵は
~/.oci/に置き、プロジェクトのディレクトリにも Git にも絶対に入れない .gitignoreで最初から除外する:*.tfstate(パスワードが入ることがある)、*.tfvars(OCID や IP などの環境情報。コミットするのは*.exampleだけ)、*.pem/*.key、backend.hcl- plan ファイルにも本当の値が入る → 使い終わったら削除
- サイトの公開前に
tools/prepublish_check.pyで実際の IP、OCID、ネームスペース、メールアドレスなどをスキャンし、サイトにはプレースホルダーだけを載せる
9. 設計判断の一覧¶
| 判断 | 採用 | 不採用 | 理由 |
|---|---|---|---|
| プログラムから OCI API へのアクセス | インスタンス / リソース・プリンシパル | マシンに API キーを置く | 長期間有効なキーがなく、ローテーション不要で、漏えいしようがない |
| 権限の範囲 | where で特定のリソースに限定 + 最小の動詞 |
コンパートメント全体への manage |
悪用されても影響はその 1 つのリソースだけ |
| 動的グループのメンバー | OCID / リソース・タイプ + コンパートメントで厳密に指定 | コンパートメント内のすべてのリソース | ほかの VM がついでに権限を得ることがない |
| パスワードの生成 | ボールトの自動生成 | Terraform の random_password |
state に平文が残らない |
| 外部のキー(AWS キー) | スクリプトでボールトに書き込み、Terraform は枠だけ + ignore_changes |
aws_iam_access_key を state に入れる |
同上。さらにローテーションをスクリプト化できる |
| マスター暗号化キーの保護方式 | SOFTWARE(無料) | HSM / 専有ボールト | 学習環境には十分。本番は要件に応じて選ぶ |
| ログの長期保存 | コネクタ・ハブ → CMK で暗号化したバケット + 30 日で自動削除 | ロギングのデフォルト保持期間だけ | キーと保存期間を自分で管理でき、ライフサイクルで費用を抑えられる |
| AWS の日常のサインイン | Identity Center + 一時的な認証情報 | root / IAM ユーザーの長期キー | ローカル PC のディスクに長期キーがない |
10. 検証¶
権限の設計では必ず逆方向のテストをします:やってはいけないことが本当にできないことを確認します。
| 証明したいこと | 方法 | 結果 |
|---|---|---|
| VM は許可されたシークレットを読める | VM で --auth instance_principal を使ってシークレット B を読む |
成功(長さだけを確認し、パスワードは表示しない) |
| VM は許可されていないシークレットを読めない(逆方向) | シークレット A を読む | NotAuthorizedOrNotFound(404) |
| 監査に記録が残る | ログ検索で GetSecretBundle を検索 |
B → 200、A → 404。呼び出し元は VM |
| state に平文があるか | terraform state pull で有無だけを確認 |
A と random_password にはあり、B にはない |
| AWS の deployer でデプロイできる | 一覧 / PUT / DELETE / キャッシュ削除 | 成功 |
| AWS の deployer はほかのことができない(逆方向) | オブジェクトの読み取り、state バケットへのアクセス、バケット・ポリシーの削除、バケットの一覧、IAM の参照 | すべて AccessDenied |
| state バケットは安全でないアクセスを拒否する(逆方向) | 匿名アクセス / HTTP でのアクセス | 403 / バケット・ポリシーで明示的に拒否 |
| 枠だけ作ったシークレットが上書きされない | 本当の値を書き込んだあとで再度 plan | No changes(ignore_changes が効いている) |
11. ハマりどころ¶
| 現象 | 原因 | 対策 |
|---|---|---|
| シークレットの読み取り → 404 | 権限がない場合も存在しない場合も 404 | ポリシー(read secret-bundles)と動的グループのルールを確認 |
CMK で暗号化するバケットを新規作成 → 404-NotAuthorizedOrFoundKmsKey |
同じ apply で作ったばかりのポリシーがまだ反映されていない | 少し待ってからもう一度 apply |
ビルドがコード・リポジトリを読めない(RelatedResourceNotAuthorizedOrNotFound) |
repos は OCIR(コンテナ・レジストリ)のリソース・タイプ |
コード・リポジトリには read devops-repository |
(sensitive value) のパスワードが state では平文 |
sensitive は表示しないだけ | ボールトに生成させる |
| destroy のあと、同じ名前のシークレットが作れない | ボールト / キー / シークレットは「削除予定」(7〜30 日)になる | 名前を変えるか、削除の完了を待つ |
| destroy でアーカイブ用バケットが削除できない | バケットが空でない。コネクタが書き込みを続けている | まずコネクタを止める → バケットを空にする → 残りを 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 トークンの期限切れ | aws sso login --profile <profile> |
12. 確認問題¶
1. ある Function でバケットを非公開にしたい。ポリシーはどう書くのが適切ですか?
Allow dynamic-group <Function のグループ> to use buckets in compartment <名前> where target.bucket.name = '<そのバケット>'。use buckets は設定を変更できますが、バケットの削除や中のファイルの読み書きはできません。
2. VM がボールトのパスワードを読むのに、マスター暗号化キーの権限は必要ですか?
不要です。復号はボールトの内部で行われるため、VM には read secret-bundles(シークレットの内容)の権限だけがあればよく、さらに where target.secret.id でその 1 つのシークレットに絞るのが望ましいです。
3. Terraform の random_password で DB のパスワードを生成し、出力を sensitive にすれば安全ですか?
不十分です。sensitive は表示しないだけで、state ファイルにも plan ファイルにも平文が入っています。ボールトや DB サービス自身に生成させ、Terraform は枠だけを作るべきです。
4. シークレットを読むと 404 が返ります。考えられる原因は?
シークレットが存在しないか、権限がないかです。OCI はどちらの場合も 404 を返します。ポリシー、動的グループのメンバー・ルール、IAM がすでに反映されているかを確認します。
5. なぜ VM / Function には API キーを置かず、インスタンス / リソース・プリンシパルを使うのですか?
API キーは長期間有効なので、マシンに置けば漏えいのリスクがあり、ローテーションも自分で行う必要があります。プリンシパルの証明書は OCI が自動で発行・ローテーションし、マシンには長期キーが残りません。
6. OCI のビルドから AWS にアップロードするとき、なぜアクセス・キーしか使えないのですか?リスクはどう下げますか?
OCI のビルド・ランナーは AWS のリソースではないため、IAM Role を引き受けられません。リスクを下げるには:権限を最小にする、キーを Terraform に通さない(OCI のボールトに置く)、OCI 側で読めるのをその 1 つのシークレットだけにする、定期的にローテーションする。
7. 「VM はシークレット B しか読めない」ことはどう証明しますか?
正方向:VM で B の読み取りに成功する。逆方向:VM で A を読むと 404 が返る。さらに監査で 2 回の呼び出しがどちらも記録されている(200 / 404)ことを確認します。
8. CMK とデフォルトの暗号化は何が違いますか?CMK が必要なのはいつですか?
デフォルトの暗号化はクラウド事業者がキーを管理します。CMK は自分で管理し、自分で無効化 / ローテーション / 削除でき、使用のたびに監査記録が残ります。「キーは自分たちで管理する」という要件があるときにだけ必要です。
コード全文¶
09-secops/iam.tf(動的グループ + ポリシー)
# ------------------------------------------------------------
# 9-2:VM がインスタンス・プリンシパルでシークレットを読む
# インスタンス・プリンシパル = VM 自身のアイデンティティ(証明書は OCI が自動で発行・ローテーション)→ VM に API キーを置かない
# (Phase 7 のビルド・パイプライン / Phase 8 の Function が使うリソース・プリンシパルと同じ考え方)
# ------------------------------------------------------------
# tf-lab の親 = テナンシ(動的グループはテナンシ直下にしか作れない)
data "oci_identity_compartment" "lab" {
id = var.compartment_ocid
}
# メンバーはこの VM 1 台だけ(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
}
# シークレット B の内容の読み取りだけを許可(secret-bundles = シークレットの内容、secrets = メタデータ)
# シークレット A はわざと許可しない → VM から A を読むと拒否される = 最小権限の確認
# 復号はボールト・サービスの内部で行われる → 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:ログ・アーカイブ用の権限(どれもこのキー 1 つ / このバケット 1 つに限定)
# Object Storage サービス(objectstorage-<region>):
# - use keys:マスター暗号化キーでバケット内のデータを暗号化 / 復号(CMK)
# - manage object-family:ライフサイクル・ルールを実行(30 日後に削除)
# コネクタ・ハブ:サービス自体は動的グループのメンバーではない → 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(ボールト / マスター暗号化キー / 2 種類のシークレット)
# ------------------------------------------------------------
# 9-2:ボールト + マスター暗号化キー + シークレット
#
# ボールト(DEFAULT:ほかのユーザーと共用の HSM パーティション、無料)
# └─ マスター暗号化キー(SOFTWARE 保護:無料。HSM 保護は 20 キー・バージョンまでしか無料にならない)
# ├─ シークレット A:Terraform の random_password で生成 → ボールトに格納(反面教師:state に平文が残る)
# ├─ シークレット B:ボールト自身が生成(auto-generation)→ Terraform は「枠」だけ管理、state にパスワードはない
# └─ 9-3:ログ・アーカイブ用バケットの CMK
#
# ⚠ 削除:destroy してもすぐには消えず、「削除保留中」になる(7〜30 日)
# この間は同名のシークレットを再作成できない(名前が使われたまま)。ソフトウェア・キーは課金されない
# ------------------------------------------------------------
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 はボールトごとに専用のエンドポイント
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
}
# ---- シークレット 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
}
# ---- シークレット B:ボールト自身が生成(推奨)----
# generation_type / generation_template の値:SDK の PassphraseGenerationContext で調べた
# パスワードはボールトの中だけ → Terraform の state、plan、ローカル PC のどこにも出てこない
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(アクセス・ログ / フロー・ログ / アーカイブ)
# ------------------------------------------------------------
# 9-3:ログ
# ① サイトのバケット(07-site の tflab-site)のアクセス・ログ(Object Storage の read)
# ② VCN フロー・ログ(09 のサブネット)
# ③ 監査(Audit)ログ:最初から有効(365 日)、作成不要 → ログ検索(Logging Search)で検索
# ④ コネクタ・ハブ:① + ③ → アーカイブ用バケット(9-2 のマスター暗号化キーで暗号化 = CMK、30 日後に自動削除)
#
# カテゴリ名(read / write、all):LoggingManagementClient.list_services() で調べた
# ログ量はごく少ない → Logging の毎月 10GB の無料枠内。コネクタは 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 したか
# バケットは 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 フロー・ログ:サブネット内の各 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
}
# ---- ④ アーカイブ ----
# アーカイブ用バケット:9-2 のマスター暗号化キーで暗号化(CMK)
# Object Storage サービスに先に use keys の権限が必要(iam.tf の archive ポリシー)→ depends_on
# ⚠ destroy:バケットにオブジェクトがあると削除できない → 先に空にする(コンソールまたは 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 のポリシーでもこの名前を使う
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]
}
# コネクタ・ハブ:ログ → アーカイブ用バケット(gzip された JSON ファイル)
# 監査(Audit)のロググループは固定名 "_Audit"(コンパートメント単位)
# batch_rollover_time:最長 7 分ごとに 1 ファイル書き込む(1 分ごとだと月に最大 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 はリソース・プリンシパル(Phase 7 のビルド・パイプラインと同じく、API キーは発行しない)
# ------------------------------------------------------------
# tf-lab の親 = テナンシ(動的グループはテナンシ直下にしか作れない)
data "oci_identity_compartment" "lab" {
id = var.compartment_ocid
}
# tf-lab 内の Function はすべてこの動的グループのメンバー
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 バケット名 → tflab-site の設定の読み取り / 変更だけ(バケットの削除、オブジェクトやほかのバケットには触れない)
# ファンクション・サービス自体(faas):Function をサブネットに接続、OCIR からイメージを取得
# イベント・サービス(cloudEvents):予算アラートのイベントをトピックに送る
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}",
# イベント(予算アラートのルール)から tf-lab のアラート用トピックへメッセージを送る
"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 キーのシークレット:枠だけを作る)
# ------------------------------------------------------------
# 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 で「プログラムが自分の ID を使う」やり方)は使えない
# → 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
}