Terraform security best practices: secrets management, OIDC authentication, least privilege IAM, state encryption, ephemeral values, and policy-as-code. Based on HashiCorp guidance and AWS/GCP prescriptive guidance.
From eccnpx claudepluginhub tatematsu-k/ai-development-skills --plugin eccThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Based on: HashiCorp Security Practices, AWS Prescriptive Guidance, and community best practices.
.tf に書かない# CRITICAL: 絶対にやってはいけない
provider "aws" {
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
variable "db_password" {
default = "supersecret123" # DANGER
}
# AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db.secret_string
}
# HashiCorp Vault
data "vault_generic_secret" "db" {
path = "secret/data/prod/db"
}
resource "aws_db_instance" "main" {
password = data.vault_generic_secret.db.data["password"]
}
variable "db_password" {
type = string
description = "Database master password"
sensitive = true # plan/apply 出力で非表示
}
output "db_connection_string" {
value = "postgres://admin:${var.db_password}@${aws_db_instance.main.endpoint}"
sensitive = true
}
注意: sensitive = true は表示を抑制するが、state ファイルには平文で保存される。
State に保存されないシークレット:
ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
password = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
}
リソース引数が state/plan に保存されない:
resource "aws_db_instance" "main" {
# write-only: state に保存されない
password_wo = var.db_password
password_wo_version = 1 # 変更時にインクリメント
}
長期間有効な credentials を排除。
# .github/workflows/terraform.yml
permissions:
id-token: write # OIDC に必要
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-deploy
aws-region: ap-northeast-1
- run: terraform init
- run: terraform plan
# AWS 側の OIDC Provider 設定
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
}
resource "aws_iam_role" "terraform_deploy" {
name = "terraform-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/infra:*"
}
}
}]
})
}
# Plan 用ロール (ReadOnly)
resource "aws_iam_role" "terraform_plan" {
name = "terraform-plan"
# ReadOnlyAccess + terraform state read
}
# Apply 用ロール (Write)
resource "aws_iam_role" "terraform_apply" {
name = "terraform-apply"
# 最小権限の write permissions
}
# State バケット
resource "aws_s3_bucket" "state" {
bucket = "my-terraform-state"
}
resource "aws_s3_bucket_versioning" "state" {
bucket = aws_s3_bucket.state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
bucket = aws_s3_bucket.state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "state" {
bucket = aws_s3_bucket.state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
*.tfstate
*.tfstate.*
.terraform/
crash.log
*.tfvars # シークレットを含む場合
絶対にコミットしてはいけない: terraform.tfstate, .tfstate.backup, credentials ファイル
# OpenTofu のみ
terraform {
encryption {
method "aes_gcm" "default" {
keys = key_provider.aws_kms.default
}
state {
method = method.aes_gcm.default
}
}
}
* ワイルドカードを actions と resources で避ける# BAD: 過剰な権限
resource "aws_iam_policy" "terraform" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = "*"
Resource = "*"
}]
})
}
# GOOD: 最小権限
resource "aws_iam_policy" "terraform" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = [
"ec2:DescribeInstances",
"ec2:RunInstances",
"ec2:TerminateInstances"
]
Resource = "arn:aws:ec2:ap-northeast-1:123456789012:instance/*"
Condition = {
StringEquals = {
"aws:RequestedRegion" = "ap-northeast-1"
}
}
}]
})
}
# sentinel/restrict-instance-type.sentinel
import "tfplan/v2" as tfplan
main = rule {
all tfplan.resource_changes as _, rc {
rc.type is "aws_instance" implies
rc.change.after.instance_type in ["t3.micro", "t3.small", "t3.medium"]
}
}
# policy/tags.rego
package terraform
deny[msg] {
resource := input.resource_changes[_]
not resource.change.after.tags["Environment"]
msg := sprintf("%s must have Environment tag", [resource.address])
}
# WARNING: local-exec はシークレット漏洩のリスク
resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo ${var.secret}" # DANGER: ログに記録される
}
}
Rule: 本番環境では local-exec provisioner を制限する。Policy as Code でブロック推奨。
.tf / .tfvars にシークレットがハードコードされていないsensitive = true がシークレット変数に設定されているterraform.tfstate が .gitignore に含まれているlocal-exec provisioner が制限されているSources: