security-best-practices
sidebar_position: 3 title: Security Best Practices description: Security hardening guide for terraform-aws modules covering authentication, sensitive data management, state file security, APRA CPS 234 compliance, and IAM least privilege tags: [security, APRA, CPS-234, IAM, state-locking, checkov, trivy]
Security Best Practices
This guide documents the security controls, patterns, and tooling enforced across the oceansoft/terraform-aws/aws registry modules. It is written for platform engineers deploying these modules into production AWS Organizations environments and covers authentication, sensitive data management, state protection, regulatory compliance, and continuous scanning.
All examples use Terraform >= 1.11.0 and AWS Provider >= 6.28, < 7.0 per ADR-003.
1. Authentication Strategy
Never Use Long-Lived Access Keys
Static IAM access keys are the single most common source of credential leakage in Terraform workflows. This project prohibits their use entirely.
For CI/CD (GitHub Actions): Use OIDC federation to assume an IAM role directly from the GitHub Actions workflow. No long-lived values are stored in GitHub.
# oidc-provider.tf — GitHub Actions OIDC provider (deploy once per account)
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
tags = {
CostCenter = "platform"
Project = "ci-cd"
Environment = "production"
ServiceName = "github-actions"
DataClassification = "internal"
}
}
resource "aws_iam_role" "github_actions_terraform" {
name = "github-actions-terraform"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github_actions.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# Restrict to specific repository and branch
"token.actions.githubusercontent.com:sub" = "repo:nnthanh101/terraform-aws:ref:refs/heads/main"
}
}
}]
})
tags = {
CostCenter = "platform"
Project = "ci-cd"
Environment = "production"
ServiceName = "github-actions"
DataClassification = "internal"
}
}
For local development: Use AWS IAM Identity Center (SSO) with short-lived session tokens. Authenticate via aws sso login, never export static keys.
# Authenticate via SSO — tokens auto-rotate, no keys on disk
aws sso login --profile=Cloudandplatformteam-Admin-123456789012
# Run Terraform with the SSO profile
export AWS_PROFILE="Cloudandplatformteam-Admin-123456789012"
terraform plan
2. Provider Authentication — Role Assumption Pattern
When operating across multiple AWS accounts (management, security, workload), use the assume_role block in provider configuration rather than switching profiles. This creates an auditable chain of role assumptions in CloudTrail.
# providers.tf — Cross-account role assumption pattern
provider "aws" {
region = "ap-southeast-2"
# Identity Center lives in us-east-1 but we deploy from ap-southeast-2
assume_role {
role_arn = "arn:aws:iam::${var.management_account_id}:role/TerraformExecution"
session_name = "terraform-identity-center"
# Optional: further restrict permissions for this session
# policy = data.aws_iam_policy_document.session_policy.json
}
default_tags {
tags = {
CostCenter = var.cost_center
Project = var.project_name
Environment = var.environment
ServiceName = "sso"
DataClassification = var.data_classification
}
}
}
# Separate provider for us-east-1 (Identity Center region)
provider "aws" {
alias = "identity_center"
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::${var.management_account_id}:role/TerraformExecution"
session_name = "terraform-identity-center-use1"
}
}
Session policies can further restrict an assumed role for specific operations. Use them when a single execution role serves multiple Terraform modules with different permission scopes.
3. Sensitive Data Management
Rule: Never Hardcode Sensitive Values in .tf Files
Terraform state files store resource attributes in plaintext. Any value that appears in a .tf file as a literal string will be visible in state. This project enforces the following hierarchy for storage of sensitive data:
| Data Type | Storage | Terraform Access Pattern |
|---|---|---|
| Database passwords | AWS Secrets Manager | data.aws_secretsmanager_secret_version |
| API keys | AWS Secrets Manager | data.aws_secretsmanager_secret_version |
| Configuration values | SSM Parameter Store | data.aws_ssm_parameter |
| Encryption keys | AWS KMS | data.aws_kms_key / aws_kms_alias |
| OIDC thumbprints | Computed at apply time | data.tls_certificate |
# sensitive-data.tf — Retrieve values at apply time, never store in code
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/rds/master-password"
}
resource "aws_db_instance" "main" {
# ...
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
# SSM Parameter Store for non-sensitive configuration
data "aws_ssm_parameter" "vpc_id" {
name = "/infrastructure/vpc/primary-id"
}
Pre-commit enforcement: The gitleaks scan runs on every commit via pre-commit hooks. Any detected sensitive value blocks the commit.
# .pre-commit-config.yaml (excerpt)
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Sensitive Variables
Mark variables that may contain protected values with sensitive = true to prevent their values from appearing in plan output and logs:
variable "database_password" {
description = "RDS master password — retrieved from Secrets Manager"
type = string
sensitive = true
}
4. State File Security
Terraform state files contain the complete resource graph including sensitive attributes. This project uses S3 native locking per ADR-006.
Backend Configuration (Production)
# versions.tf — S3 native locking, no DynamoDB (ADR-006)
terraform {
required_version = ">= 1.11.0"
backend "s3" {
bucket = "nnthanh101-terraform-state"
key = "terraform-aws/identity-center/terraform.tfstate"
region = "ap-southeast-2"
encrypt = true # SSE-S3 encryption at rest
use_lockfile = true # S3 native locking — NO DynamoDB needed
}
}
State Bucket Hardening
The S3 bucket that holds Terraform state must be locked down with the following controls:
| Control | Setting | Purpose |
|---|---|---|
| Encryption | SSE-S3 or SSE-KMS | Protect state data at rest |
| Versioning | Enabled | Recover from state corruption |
| Public access | Block all public access | Prevent accidental exposure |
| Bucket policy | Restrict to Terraform execution roles | Least-privilege access |
| Object Lock | Governance mode (optional) | Prevent accidental deletion |
| Logging | S3 access logging to separate bucket | Audit trail for state access |
Minimal IAM policy for state access (no DynamoDB permissions needed):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TerraformStateAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::nnthanh101-terraform-state",
"arn:aws:s3:::nnthanh101-terraform-state/*"
]
}
]
}
Why no DynamoDB? Terraform v1.11.0+ uses S3 Conditional Writes (If-None-Match header) for atomic lock file creation. This eliminates an entire service dependency, reduces the IAM surface, and removes DynamoDB throttling as a failure mode. See ADR-006 for the full decision record and migration path.
5. APRA CPS 234 Compliance
This project includes custom Checkov checks in .checkov/custom_checks/check_apra_cps234.py that enforce Australian Prudential Regulation Authority (APRA) CPS 234 requirements for information security. These checks run automatically via task build:lint.
Custom APRA Checks
| Check ID | CPS 234 Reference | What It Validates |
|---|---|---|
CKV_APRA_001 | Para 15 — Data Classification | All IAM resources must have a DataClassification tag with a valid value (public, internal, confidential, restricted) |
CKV_APRA_002 | Para 36 — Least Privilege | No AdministratorAccess AWS managed policy attached (unless break-glass per ADR-020) |
CKV_APRA_003 | Para 37 — Session Duration | SSO session duration must not exceed 8 hours |
CKV_APRA_004 | Para 36 — Separation of Duties | Administrative permission sets (name contains "admin") must have session duration of 1 hour or less |
CKV_APRA_005 | Para 37 — Permissions Boundary | High-privilege permission sets must have a permissions boundary attached |
FOCUS 1.2+ FinOps Tag Check
In addition to the APRA checks, check_focus_tags.py validates the 4 FOCUS cost allocation tags:
| Check ID | What It Validates |
|---|---|
CKV_CUSTOM_FOCUS_001 | All taggable resources include CostCenter, Project, Environment, ServiceName |
Mandatory Tags
Every taggable resource must include the 5 canonical tags. The module's default_tags variable ensures baseline coverage, and per-resource tags can override individual keys:
module "aws-iam-identity-center" {
source = "oceansoft/iam-identity-center/aws"
# 5 canonical tags: Enterprise + FOCUS 1.2+ + APRA CPS 234
default_tags = {
CostCenter = "platform"
Project = "landing-zone"
Environment = "production"
ServiceName = "sso"
DataClassification = "confidential" # CKV_APRA_001
}
permission_sets = {
LZReadOnlyAccess = {
description = "Read-only access for all accounts"
session_duration = "PT8H" # CKV_APRA_003: <= 8H
aws_managed_policies = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
tags = { Tier = "read-only" }
}
}
}
The DataClassification tag values map directly to the project's data governance tiers:
| Classification | Examples | Security Requirements |
|---|---|---|
public | Marketing materials, published documentation | No encryption required |
internal | Project plans, internal emails | Encryption at rest, SSO authentication |
confidential | Customer data, financial records | Encryption in transit and at rest, RBAC, audit logging |
restricted | PHI, PII, payment card data | End-to-end encryption, strict RBAC, immutable audit logs |
Break-Glass Pattern
The only sanctioned exception to CKV_APRA_002 (no AdministratorAccess) is the break-glass emergency access pattern documented in ADR-020. Use checkov:skip with the ADR reference:
permission_sets = {
# checkov:skip=CKV_APRA_002:ADR-020 break-glass pattern
# checkov:skip=CKV_APRA_005:ADR-020 break-glass; boundary deferred
LZAdministratorAccess = {
description = "Break-glass only (ADR-020)"
session_duration = "PT1H" # CKV_APRA_004: admin <= 1H
aws_managed_policies = ["arn:aws:iam::aws:policy/AdministratorAccess"]
}
}
6. Security Scanning
This project integrates multiple scanning tools at different stages of the development lifecycle. All scans run inside the nnthanh101/terraform:2.6.0 container to ensure reproducible results.
Scanning Matrix
| Tool | Scope | Taskfile Command | Runs On | Blocking |
|---|---|---|---|---|
| Checkov | SAST + custom APRA/FOCUS checks | task build:lint | Every commit (CI) | Critical/High |
| Trivy | Misconfiguration + CVE scan | task security:trivy | Pre-deploy | Critical |
| tflint | Terraform linting + provider rules | task build:lint | Every commit (CI) | All findings |
| gitleaks | Detection of sensitive values in source | Pre-commit hook | Every commit (local) | Any finding |
| terraform validate | HCL syntax + provider schema | task build:validate | Every commit (CI) | All errors |
Running Scans Locally
# Quick CI: format check + validate + lint (Checkov + tflint) + legal headers
task ci:quick
# Full security scan including Trivy misconfiguration analysis
task security:trivy
# Full CI pipeline: build + test + governance + security
task ci:full
Checkov Custom Checks
The custom checks in .checkov/custom_checks/ extend Checkov's built-in ruleset with project-specific requirements:
check_apra_cps234.py— 5 checks (CKV_APRA_001throughCKV_APRA_005) covering data classification tagging, least privilege, session duration limits, separation of duties, and permissions boundaries. See Section 5 above.check_focus_tags.py— 1 check (CKV_CUSTOM_FOCUS_001) validating that all taggable resources include the 4 FOCUS 1.2+ cost allocation tags:CostCenter,Project,Environment,ServiceName.
Plan-Time Validation (Terraform Check Blocks)
In addition to static analysis, the iam-identity-center module includes Terraform check blocks that validate permission set configurations at plan time. These are defined in locals.tf and surface issues in YAML-sourced configurations that bypass the variables.tf validation block:
yaml_session_duration_format— Validates ISO 8601 duration format (PT<n>HorPT<n>M) for all permission sets, including those loaded from YAML.yaml_required_tag_keys— ValidatesCostCenterandDataClassificationtag presence on all permission sets after the YAML merge.
7. IAM Least Privilege
Permission Set Hierarchy
The recommended pattern for multi-account Landing Zones is a tiered permission hierarchy with descending privilege and ascending session duration:
| Tier | Permission Set | Session Duration | Use Case |
|---|---|---|---|
| Admin | LZAdministratorAccess | PT1H (CKV_APRA_004) | Break-glass only (ADR-020) |
| Power User | LZPowerUserAccess | PT4H | Workload deployment, no IAM mutation |
| Read Only | LZReadOnlyAccess | PT8H (CKV_APRA_003 max) | Compliance review, monitoring |
| Security Audit | LZSecurityAuditAccess | PT8H | CloudTrail, GuardDuty, Config, SecurityHub |
The inverse relationship between privilege level and session duration is a core APRA CPS 234 control: higher-privilege sessions expire faster, reducing the blast radius of compromised tokens.
Permissions Boundaries
High-privilege permission sets should include a permissions boundary to cap the maximum effective permissions, even if the attached policies grant broader access. CKV_APRA_005 enforces this for any permission set whose name contains administratoraccess or poweruseraccess.
permission_sets = {
LZPowerUserAccess = {
description = "Power user access — deploy workloads, no IAM mutation"
session_duration = "PT4H"
aws_managed_policies = ["arn:aws:iam::aws:policy/PowerUserAccess"]
# Permissions boundary: caps effective permissions
permissions_boundary = {
managed_policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}
tags = { Tier = "power-user" }
}
}
Attribute-Based Access Control (ABAC)
The iam-identity-center module supports ABAC via sso_instance_access_control_attributes. This enables resource-level access control based on user attributes (e.g., Environment, CostCenter) without creating separate permission sets for each scope:
sso_instance_access_control_attributes = [
{
attribute_name = "Environment"
source = ["$${path:enterprise.Environment}"]
},
{
attribute_name = "CostCenter"
source = ["$${path:enterprise.CostCenter}"]
},
]
With ABAC configured, IAM policies can use aws:PrincipalTag/Environment conditions to restrict access to resources tagged with the same environment as the user's SSO session.
Security Checklist
Before deploying any module from this registry to production, verify the following:
- No long-lived access keys: CI/CD uses OIDC federation; developers use SSO (
aws sso login) - Provider uses
assume_role: Cross-account access via role assumption, not shared access keys - No hardcoded sensitive values: All protected data sourced from Secrets Manager or SSM Parameter Store
- State encrypted: S3 backend with
encrypt = trueanduse_lockfile = true(ADR-006) - State bucket hardened: Versioning enabled, public access blocked, bucket policy restricted
- 5 canonical tags present:
CostCenter,Project,Environment,ServiceName,DataClassification - APRA checks pass:
task build:lintreports 0 APRA/FOCUS check failures - Trivy scan clean:
task security:trivyreports 0 critical/high findings - Session durations compliant: Admin <= 1H, general <= 8H, power user <= 4H
- Break-glass documented:
checkov:skipannotations reference ADR-020 with justification - Sensitive value detection enabled:
gitleakspre-commit hook installed and active
Related Resources
- ADR-003: Provider Constraints — Terraform >= 1.11.0, AWS Provider >= 6.28
- ADR-006: S3 Native State Locking — No DynamoDB for state locking
- ADR-020: Break-Glass Emergency Access — Sanctioned exception to CKV_APRA_002
- Identity Center Guide — Full deployment guide for the IAM Identity Center module
- AWS IAM Identity Center Documentation
- APRA CPS 234 Information Security
- Checkov Custom Checks Documentation