Learning objectives
- Read trust versus permission policies as separate contracts.
- Encrypt metadata without committing or outputting a secret value.
- Place security groups at workload boundaries and understand when NACLs fit.
Two IAM questions
| Policy | Answers | Example |
|---|---|---|
| Trust policy | Who may receive this role? | EC2 service or a GitLab OIDC identity. |
| Permission policy | What may the assumed role do? | Describe one secret and decrypt with one KMS key. |
A trust policy alone grants no AWS API access. The foundation keeps the workload role permission to metadata discovery and KMS use scoped to its own key.
Security ownership boundary
The network module owns VPC routing and the interface-endpoint security group. security.tf owns the workload group, KMS key and alias, secret metadata, roles, and budget alert. The secret resource has a name and KMS key only: inserting a secret value through Terraform would place it in state. Supply values through an approved runtime path instead.
Security groups are stateful, workload-level firewalls. NACLs are stateless subnet rules and must allow both directions including ephemeral return ports. Keep the default NACL until a subnet-wide rule has a reviewed traffic matrix; do not duplicate workload rules in both places.
Lab: review the security plan only
Use a copied working directory, named profile, expected account ID, and primary region. This lab does not create AWS resources.
export COURSE_AWS_PROFILE="replace-with-approved-aws-profile"
export COURSE_AWS_ACCOUNT_ID="123456789012"
export COURSE_AWS_REGION="ap-south-1"
: "${COURSE_BUDGET_ALERT_EMAIL:?export a real budget alert email first}"
export TF_VAR_budget_alert_email="$COURSE_BUDGET_ALERT_EMAIL"
# Confirm the named profile, account, and region before either init.
ACTUAL_AWS_ACCOUNT_ID="$(aws --profile "$COURSE_AWS_PROFILE" --region "$COURSE_AWS_REGION" sts get-caller-identity --query Account --output text)"
test "$ACTUAL_AWS_ACCOUNT_ID" = "$COURSE_AWS_ACCOUNT_ID"
export COURSE_ROOT="$PWD/learn-terraform/capstone"
export TERRAFORM_DIR="$(mktemp -d)"
export TOFU_DIR="$(mktemp -d)"
cp -R "$COURSE_ROOT/infra" "$TERRAFORM_DIR/infra"
cp -R "$COURSE_ROOT/infra" "$TOFU_DIR/infra"
# Terraform choice.
cd "$TERRAFORM_DIR/infra/stacks/foundation"
cp ../../environments/learning/foundation.backend.hcl.example foundation.backend.hcl
# Edit the bucket and kms_key_id placeholders in this untracked copy.
terraform init -reconfigure -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
terraform validate
terraform plan -input=false -var-file=../../environments/learning/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE" -var="expected_aws_account_id=$COURSE_AWS_ACCOUNT_ID" -out=tfplan
# OpenTofu choice in its own fresh copy, never in the Terraform directory.
cd "$TOFU_DIR/infra/stacks/foundation"
cp ../../environments/learning/foundation.backend.hcl.example foundation.backend.hcl
# Edit the bucket and kms_key_id placeholders in this untracked copy.
tofu init -reconfigure -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
tofu validate
tofu plan -input=false -var-file=../../environments/learning/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE" -var="expected_aws_account_id=$COURSE_AWS_ACCOUNT_ID" -out=tfplanReview the plan
- The caller account must equal the expected account before the plan proceeds.
- Expect KMS rotation, a stable alias, and no output containing a secret value.
- Expect the workload group to have no ingress and only TCP 443 egress.
- Read the role trust principal separately from its tightly scoped permission policy.
- Budget notification is a cost cue, not an enforcement control.
Failure drill
Verify without applying
- Confirm KMS policy keeps account IAM policy administration enabled.
- Check the KMS condition limits decrypt use to Secrets Manager in
ap-south-1. - Confirm the security group does not inherit a broad ingress rule.
- Read the JSON policy from the plan before accepting a trust or permission change.
Cleanup
Remove only the temporary copied working directory. The KMS key has prevent_destroy and a deletion window, while secret metadata has a recovery window. Do not remove either control merely to make a disposable cleanup command easier; first remove all consumers and retain the recovery evidence.
Quick check
Does a trust policy authorize kms:Decrypt?
No. It only controls who can assume the role; a permission policy and key policy govern KMS use.
Why not put a password in a Terraform variable marked sensitive?
Sensitive hides display output but does not remove the value from state.
Recap and next
Security is a set of explicit ownership contracts: pinned identity, separate trust and permission, encryption scoped to a key, and network rules at the right layer.
Next: compare learning and production profiles through the plan before accepting their cost and deletion differences.