Last reviewed: May 2026
Build the AWS services on the SCS-C03 exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, the four security primitives every AWS account should have on day one — a customer-managed KMS key with automatic rotation, an IAM Access Analyzer scanning for unintended cross-account or public access, GuardDuty turned on with threat-intel feeds, and a centralized audit-log bucket that other workloads write to. This is the SCS-C03 secure-by-default baseline.
Every resource is plain Terraform — the same code works without modification on OpenTofu. No variables, no modules. Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.us-east-1.Three of the four services here are free for new accounts; one has a real bill:
If you keep this baseline running on a real account, expect to pay roughly $5–15/month total. Cheap for the visibility it gives you. Destroy after the lab if you'd rather not.
Standard opener. SCS-C03 is provider- and region-agnostic in scope, but a secure baseline needs to be applied per region — GuardDuty and Access Analyzer are regional services. Most teams pick a primary region (e.g. us-east-1) for the baseline and replicate it via Terraform per region as the account expands.
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "certlabpro-scs-c03"
ManagedBy = "terraform"
}
}
}
data "aws_caller_identity" "current" {}SCS-C03 tests the difference between AWS-managed KMS keys (free, automatic rotation, no key policy you control) and customer-managed keys ($1/month, you own the policy, you control rotation, you get audit visibility into every Encrypt/Decrypt call via CloudTrail). Production should always reach for customer-managed.
We enable annual rotation (enable_key_rotation = true) and write a key policy that gives the account root the ability to administer the key (the AWS default for any customer-managed key) and explicitly grants the IAM Access Analyzer service permission to evaluate the key's policy. The alias makes the key referable by a human-readable name in downstream code instead of by UUID — SCS-C03 tests this naming convention as a CloudTrail-audit prerequisite.
resource "aws_kms_key" "app_data" {
description = "Customer-managed key for application data encryption."
enable_key_rotation = true
deletion_window_in_days = 30
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EnableRootAccountAdmin"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "kms:*"
Resource = "*"
},
{
Sid = "AllowAccessAnalyzerToDescribe"
Effect = "Allow"
Principal = { Service = "access-analyzer.amazonaws.com" }
Action = ["kms:DescribeKey", "kms:GetKeyPolicy"]
Resource = "*"
},
]
})
}
resource "aws_kms_alias" "app_data" {
name = "alias/certlabpro-scs-c03-app-data"
target_key_id = aws_kms_key.app_data.id
}Access Analyzer continuously evaluates resource-based policies (S3 bucket policies, KMS key policies, IAM role trust policies, Lambda permissions, SQS queue policies, Secrets Manager rotation policies, and more) and surfaces a finding anytime it detects a resource accessible from outside your account or trust boundary. SCS-C03's Threat Detection and Incident Response domain tests this exact pattern — Access Analyzer findings are the canonical signal for "oh, this bucket is now public".
type = "ACCOUNT" scopes the analyzer to your account; type = "ORGANIZATION" requires AWS Organizations and scopes across the whole org (the SCS-C03 multi-account answer). Either way, it's always-free and zero-config once enabled.
resource "aws_accessanalyzer_analyzer" "main" {
analyzer_name = "certlabpro-scs-c03"
type = "ACCOUNT"
}GuardDuty is AWS's managed threat-detection service. It continuously analyzes CloudTrail, VPC Flow Logs, and DNS logs against AWS's threat-intel feeds — published lists of known-bad IPs, crypto-mining domains, command-and-control infrastructure, and suspicious account behavior patterns. Findings come out tagged with severity (Low / Medium / High) and a machine-parseable type like Recon:EC2/PortProbeUnprotectedPort.
We turn on the detector with 15-minute publishing frequency (faster than the 6-hour default; SCS-C03 questions about MTTR almost always assume frequent publishing). The IAM-based protection plug-ins (MALWARE_PROTECTION_FOR_EC2, S3_DATA_EVENTS, etc.) cost extra and are out of scope for this baseline; turn them on per workload in production.
Findings publish to EventBridge automatically — that's how downstream automation (Slack notifications, Security Hub correlation, auto-remediation Lambdas) hooks in. SCS-C03 tests this findings → EventBridge → response chain repeatedly. We don't wire the downstream here, but the moment Step 4 finishes, GuardDuty starts publishing.
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "FIFTEEN_MINUTES"
}Every SCS-C03 Security Logging and Monitoring question expects you to have a centralized, encrypted, write-once-style bucket that other security tools — CloudTrail, VPC Flow Logs, ELB access logs, GuardDuty exports, Config recorders — write into. The bucket is encrypted with the customer-managed KMS key from Step 2, public access is fully blocked, and a bucket policy enforces that any object written must come through the key from Step 2 (no plaintext writes accepted).
We use the bucket-owner-enforced ownership setting (the ACLs-disabled mode, AWS-recommended since 2023) — this is one of the most-tested S3 hardening attributes on SCS-C03 because it eliminates an entire class of cross-account ACL misconfigurations.
With the bucket in place, the baseline is complete: KMS for encryption keys, Access Analyzer for unintended exposure, GuardDuty for threat detection, and an audit bucket for everything that needs to be retained for compliance review. Every SCS-C03 multi-service scenario expects you to have these four; the more advanced questions add Security Hub, Config, Inspector, or Macie on top of this base.
resource "aws_s3_bucket" "audit_logs" {
bucket_prefix = "certlabpro-scs-c03-audit-"
}
resource "aws_s3_bucket_public_access_block" "audit_logs" {
bucket = aws_s3_bucket.audit_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_ownership_controls" "audit_logs" {
bucket = aws_s3_bucket.audit_logs.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "audit_logs" {
bucket = aws_s3_bucket.audit_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.app_data.arn
}
bucket_key_enabled = true # cuts KMS request costs ~99% on bulk writes
}
}
resource "aws_s3_bucket_versioning" "audit_logs" {
bucket = aws_s3_bucket.audit_logs.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_policy" "audit_logs_require_kms" {
bucket = aws_s3_bucket.audit_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "DenyUnencryptedWrites"
Effect = "Deny"
Principal = "*"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.audit_logs.arn}/*"
Condition = {
StringNotEquals = {
"s3:x-amz-server-side-encryption" = "aws:kms"
}
}
}]
})
}terraform destroy tears down everything in this lab. Two notes:
deletion_window_in_days = 30, meaning destroy schedules it for deletion in 30 days — it's not immediately removed (this is the SCS-C03-prescribed safety pattern; KMS deletion is irreversible). During the 30 days you can cancel via the console. To actually delete, you also need to wait out the window. AWS continues billing the $1/month during this period.force_destroy = false — if you've collected logs in it, empty before destroying (aws s3 rm s3://<bucket> --recursive).SCS-C03 covers more ground than any single lab can fit — AWS Config (continuous compliance evaluation), Security Hub (centralized findings aggregation), Macie (S3 PII discovery), Inspector (EC2/Lambda vulnerability scanning), Detective (threat investigation), WAF + Shield (edge protection), Network Firewall, Network Access Analyzer, Cognito (user identity), Secrets Manager + Systems Manager Parameter Store, ACM (TLS), Firewall Manager (multi-account policy enforcement), and the entire CloudHSM hardware-key space.
We stick to the four primitives above because they're the day-one baseline that everything else builds on. Config rules write findings into Security Hub, which correlates them with GuardDuty findings, which point at resources protected by KMS keys, with policy issues caught by Access Analyzer — and all of it lands in the audit bucket for compliance review. Build the foundation first; layer on the rest as the workload's risk profile demands.
For service-by-service coverage, see the Browse, Playbook, and Editorial sections of this cert page. A follow-up lab adding Security Hub + Config + the GuardDuty-to-EventBridge auto-remediation chain would be a natural next step.