·5 min read·#aws#security#terraform#opentofu

Security Foundations - CloudTrail, Config, and Password Policies

Security Foundations - CloudTrail, Config, and Password Policies architecture diagram
Click to expand
620 × 487px

Before deploying any workloads, you need a security baseline. This means audit logging, compliance monitoring, and sensible defaults. In AWS, that translates to CloudTrail, AWS Config, and IAM password policies.

These aren't glamorous services. You set them up once and mostly forget they exist - until you need to investigate an incident or prove compliance. Then they're invaluable.

CloudTrail: The Audit Log

CloudTrail records API activity across your AWS accounts. Every CreateBucket, every AssumeRole, every RunInstances - it's all logged.

aws-account-structure-part-3-security-foundations/cloudtrail-flow diagram

Multi-Region Trail

We configure a multi-region trail that captures events from all AWS regions:

hcl
resource "aws_cloudtrail" "main" { name = "organization-trail" s3_bucket_name = aws_s3_bucket.cloudtrail.id include_global_service_events = true is_multi_region_trail = true is_organization_trail = true enable_log_file_validation = true event_selector { read_write_type = "All" include_management_events = true } }

Key settings:

is_organization_trail = true: Captures events from all accounts in the organization, not just the management account. Without this, you'd need a separate trail in every member account. See organisation trails for more detail.

is_multi_region_trail = true: Logs events from every region. Without this, someone could spin up resources in a region you're not monitoring.

enable_log_file_validation = true: Creates digest files that let you verify logs haven't been tampered with.

include_global_service_events = true: Captures IAM, CloudFront, and other global services that aren't region-specific.

The S3 Bucket

CloudTrail logs go to an S3 bucket with appropriate security:

hcl
resource "aws_s3_bucket" "cloudtrail" { bucket = "${var.organization_name}-cloudtrail-${data.aws_caller_identity.current.account_id}" lifecycle { prevent_destroy = true } } resource "aws_s3_bucket_versioning" "cloudtrail" { bucket = aws_s3_bucket.cloudtrail.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail" { bucket = aws_s3_bucket.cloudtrail.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_public_access_block" "cloudtrail" { bucket = aws_s3_bucket.cloudtrail.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true }

Versioning: Prevents accidental deletion of log files.

Encryption: AES256 server-side encryption. You could use KMS for more control, but it adds cost and complexity.

Public access block: Belt and braces. CloudTrail logs should never be public.

Cost Considerations

CloudTrail has two cost components:

  1. Management events: The first trail in each region is free. Additional trails cost $2 per 100,000 events.
  2. S3 storage: You pay for the log files stored in S3.

For a small organisation, costs are minimal - a few dollars per month. We use S3 lifecycle policies to move old logs to cheaper storage tiers:

hcl
resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" { bucket = aws_s3_bucket.cloudtrail.id rule { id = "archive-old-logs" status = "Enabled" transition { days = 30 storage_class = "STANDARD_IA" } transition { days = 90 storage_class = "GLACIER" } expiration { days = 2555 # 7 years for compliance } } }

In practice, centralising CloudTrail logs in one management-account bucket made retention policy changes much easier. We change one lifecycle policy and every member account trail benefits.

AWS Config: Compliance Monitoring

While CloudTrail records what happened, AWS Config records resource state over time. It answers questions like "what did this security group look like last Tuesday?"

hcl
resource "aws_config_configuration_recorder" "main" { name = "default" role_arn = aws_iam_role.config.arn # Service role with AWSConfigRole managed policy recording_group { all_supported = true include_global_resource_types = true } } resource "aws_config_delivery_channel" "main" { name = "default" s3_bucket_name = aws_s3_bucket.config.id snapshot_delivery_properties { delivery_frequency = "Six_Hours" } depends_on = [aws_config_configuration_recorder.main] } resource "aws_config_configuration_recorder_status" "main" { name = aws_config_configuration_recorder.main.name is_enabled = true depends_on = [aws_config_delivery_channel.main] }

Config Rules (Optional)

AWS Config can evaluate resources against rules. We haven't implemented many rules yet, but common ones include:

  • s3-bucket-public-read-prohibited - Alerts on public S3 buckets
  • iam-root-access-key-check - Ensures root account has no access keys
  • encrypted-volumes - Checks EBS volumes are encrypted

Each rule has a cost ($0.001 per evaluation), so start with the most critical ones.

IAM Password Policy

For any IAM users you do create (hopefully few), enforce strong passwords:

hcl
resource "aws_iam_account_password_policy" "strict" { minimum_password_length = 14 require_lowercase_characters = true require_numbers = true require_uppercase_characters = true require_symbols = true allow_users_to_change_password = true max_password_age = 90 password_reuse_prevention = 12 }

minimum_password_length = 14: NIST SP 800-63B recommends at least 8. We went with 14 for stronger entropy.

max_password_age = 90: Forces rotation every 90 days. NIST actually recommends against mandatory rotation (it encourages weaker passwords), but many compliance frameworks still require it. We kept it as a pragmatic default - remove it if your compliance posture allows.

password_reuse_prevention = 12: Prevents cycling through old passwords.

Why This Still Matters

"But you're using IAM Identity Center - why care about IAM passwords?"

Because IAM users still exist for some use cases:

  • Service accounts that can't use OIDC
  • Break-glass emergency access
  • Legacy integrations

The password policy applies to any IAM users created in the account, even if your primary access is through Identity Center.

Cross-Account CloudTrail

For member accounts, CloudTrail needs to write to the centralised S3 bucket. The bucket policy grants this access:

hcl
resource "aws_s3_bucket_policy" "cloudtrail" { bucket = aws_s3_bucket.cloudtrail.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AWSCloudTrailAclCheck" Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } Action = "s3:GetBucketAcl" Resource = aws_s3_bucket.cloudtrail.arn }, { Sid = "AWSCloudTrailWrite" Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } Action = "s3:PutObject" Resource = "${aws_s3_bucket.cloudtrail.arn}/*" Condition = { StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" } } } ] }) }

This allows CloudTrail from any account in the organization to write logs to the central bucket.

What We Didn't Enable (Yet)

Several security services are on the "eventually" list:

GuardDuty: Threat detection using machine learning. Pricing is based on data volume - expect $3-10/month for a small setup with light CloudTrail and VPC flow log analysis. Worth enabling once you have production traffic.

Security Hub: Aggregates findings from GuardDuty, Inspector, Config rules, and other sources into a single dashboard. Costs $0.0010 per check per region. Useful once you have multiple security services feeding it.

Macie: Scans S3 for sensitive data (PII, credentials). $1/bucket/month for automated discovery. Targeted use only - enable it for buckets that might contain customer data.

CloudTrail metric filters/alarms: We intentionally deferred cross-account alerting. It adds log shipping and ownership decisions we weren't ready for. The first alarms to add are StopLogging, trail deletion, and Config recorder changes - once on-call workflows are in place.

We stopped here. The current baseline - CloudTrail, Config, and password policy - is enough to start. The services above can be added incrementally as the setup matures.

Verifying It Works

After deployment, verify CloudTrail is capturing events:

bash
# List recent events aws cloudtrail lookup-events --max-results 10 # Check trail status aws cloudtrail get-trail-status --name organization-trail

For AWS Config:

bash
# Check recorder status aws configservice describe-configuration-recorder-status # List discovered resources aws configservice list-discovered-resources --resource-type AWS::S3::Bucket

The Security Baseline

With these services enabled, you have:

  • Audit trail: Every API call logged and stored
  • State history: Resource configurations tracked over time
  • Password hygiene: Strong defaults for any IAM users
  • Centralised logging: All accounts reporting to one bucket

This isn't comprehensive security, but it's the baseline you need before deploying anything else.

What's Next

With audit logging and compliance monitoring in place, the next problem is Terraform state. Where does the state file live? How do you share it across repositories? Part 4 covers the bootstrap pattern for S3 backends and DynamoDB locking.


← Back to all posts