π In This Post
Why β The Problem This Solves
Security Hub's FSBP standard checks roughly 300 controls, and by Week 11 it already covered nearly every security-relevant misconfiguration in this account β open ports, public buckets, disabled encryption defaults. But it has no concept of governance drift. A bucket created without a cost-center tag, or one that was never versioned, isn't a security finding β Security Hub simply has no control for either. Nothing watches for it.
That kind of drift doesn't announce itself. It accumulates quietly until a monthly cost-allocation report has a chunk of unattributed spend, or until someone deletes an object from an unversioned bucket and there's no previous version to recover. The usual response β a quarterly manual tagging audit β runs long after the drift has already cost something.
This week adds exactly the checks Security Hub can't: mandatory tagging, S3 versioning, S3 default encryption. Two of the three fix themselves; the third can only ever notify, because a governance value like cost-center isn't something automation is allowed to guess.
What You Need to Know β Skills & Tools
The one decision that shaped everything: one conformance pack, not standalone rules
The original plan was three standalone aws_config_config_rule resources, wrapped in a conformance pack for the dashboard view. That isn't how conformance packs actually work. A pack's template defines its own AWS::Config::ConfigRule and AWS::Config::RemediationConfiguration resources β it doesn't reference rules created elsewhere. Building both would have meant two copies of the same rule. So this stack has no standalone Config-rule resources at all: the pack, rendered from a CloudFormation-style YAML template via Terraform's templatefile(), is the only place these three rules exist. That also means the pack's own console compliance view is the real compliance dashboard, not a second copy of something that already existed.
Why these 3 rules, and not more
The account already has roughly 300 Config rules β all prefixed securityhub-*, auto-created by Security Hub's FSBP standard in Week 11. Adding more security-flavored rules would be redundant, not additive. required-tags, s3-bucket-versioning-enabled, and s3-bucket-server-side-encryption-enabled were chosen because they're confirmed absent from that set β FSBP's S3 checks are public-access and SSL-in-transit only, and it has no tagging-governance concept whatsoever.
Why SSM Automation, not a custom Lambda β the opposite of Week 11
Week 11's remediators were bespoke Lambdas, because revoking the specific ingress rule a finding pointed at needed real parsing logic no canned document could do. These two fixes are the opposite shape: enabling versioning or default encryption is one documented API call, and AWS already ships tested SSM Automation documents for exactly that β AWS-ConfigureS3BucketVersioning and AWS-EnableS3BucketEncryption, both confirmed live against this account before use. Writing a Lambda to do the same thing would be reinventing something AWS already maintains. Reach for a custom Lambda only when the fix isn't a single documented action β that's genuinely Week 11's situation, not this one.
Without a recorder actively recording, these rules (and Security Hub's FSBP controls) report NO_DATA and evaluate nothing. This stack provisions its own recorder β see Architecture below for why that became necessary partway through the build.
Architecture β How It Fits Together
A Config recorder feeds configuration items to a single conformance pack containing three rules. Two are tag-scoped and auto-remediate through SSM Automation; the third is broad-scope and notify-only. A scheduled Lambda reads the pack's own compliance status daily and reports it to a subscribed inbox β scoped to just these three rules, never the account's other ~300.
Record
An AWS Config recorder captures configuration changes for every supported resource type, continuously, account-wide. This stack owns its recorder outright β more on why in Challenges.
Evaluate
The conformance pack's three rules evaluate against the recorded configuration items. required-tags checks everything broadly; the two S3 rules only evaluate buckets carrying auto-remediate=true, via Config's own Scope.TagKey/TagValue filter β no custom code decides what's in scope.
Remediate β only what's tagged
A non-compliant, tagged bucket triggers Config's remediation configuration, which invokes an AWS-managed SSM Automation document. One API call each: enable versioning, or enable default encryption.
Digest, daily
A Lambda on a daily EventBridge schedule asks Config directly which rules exist in the pack and what their compliance status is, then publishes a plain-text summary to SNS β scoped to just these 3 rules, never the account's other ~300.
How We Built It β Step by Step
The stack is 23 Terraform resources across three modules (config_recorder, config_compliance, reporter), deployed through HCP Terraform.
Step 1 β Deploy via HCP Terraform
Same VCS-connected workflow as every week since Week 5: push to GitHub, HCP plans and applies remotely. This run's history is honest about the fact that it took more than one try β two early plans errored before the stack came up clean. That's the subject of the next section.
Step 2 β Confirm the recorder is live
Everything downstream depends on this. The recorder records all 621 supported resource types, continuously, account-wide.
Step 3 β The conformance pack, deployed as one unit
All three rules and both remediation configs land as a single CREATE_COMPLETE conformance pack. This view β not a separate dashboard resource β is the compliance dashboard this week set out to build.
Step 4 β required-tags flags real drift, immediately
No synthetic test needed here: the account already had 22 real resources β EC2 instances, EBS volumes, S3 buckets from other projects β missing the mandatory tags. This rule found actual, pre-existing governance debt on its first evaluation.
Step 5 β Exercise the auto-remediation end to end
A small script (generate_misconfig.sh) creates a bucket tagged auto-remediate=true with no versioning or encryption configured, plus an untagged bucket to exercise the notify-only path. Config picked up the tagged bucket, flagged it non-compliant, and Systems Manager Automation ran the fix.
Step 6 β The daily digest
compliance_reporter runs once a day, asks Config directly for the pack's own compliance status, and emails a plain-text summary.
Challenges β What Actually Went Wrong
This week had an unusually dense run of real bugs β six worth writing down, none of them caught by terraform validate or reading documentation. Only an actual apply, an actual Lambda invoke, and an actual remediation attempt surfaced any of them.
The first plan on the brand-new week-12-dev workspace errored immediately: "No valid credential sources found". Obvious in hindsight β HCP's remote execution environment isn't running inside this AWS account, so nothing hands it credentials automatically.
Two workspace environment variables enable HCP's dynamic-credentials (OIDC) flow: TFC_AWS_PROVIDER_AUTH=true and TFC_AWS_RUN_ROLE_ARN=<role that trusts HCP's OIDC provider>. Every prior week's workspace already had these β this was simply the first time a brand-new workspace was created without copying them over. Now on the checklist for every future week.
The second plan's apply failed creating the conformance pack: "Scope cannot be applied to both resource and tag". The two S3 rules' Scope block specified both ComplianceResourceTypes: [AWS::S3::Bucket] and TagKey/TagValue β a combination that looks reasonable and is explicitly documented as invalid.
Drop the resource-type filter and scope by tag alone. The underlying managed rule (S3_BUCKET_VERSIONING_ENABLED, for instance) only ever evaluates S3 buckets regardless of Scope's type filter, so nothing is lost. AWS's own Scope API docs state it plainly: one or more resource types, or a tag key/value pair, or one resource type plus one resource ID β never type-plus-tag together.
Once the pack deployed, its rules showed up as week12-required-tags-conformance-pack-qvc7a6yj1 β not the plain week12-required-tags declared in the template. AWS Config appends its own generated suffix to every rule inside a pack. The reporter Lambda's hardcoded rule-name list would have silently matched nothing, forever, without ever raising an error.
Don't hardcode rule names for anything inside a conformance pack. compliance_reporter now calls DescribeConformancePackCompliance to discover the real names at runtime β which also means it never needs updating if AWS ever changes the suffix scheme.
The first real Lambda invoke crashed instantly: UnknownServiceError: Unknown service: 'configservice'. The AWS CLI's command namespace for this service really is aws configservice ..., which made the wrong guess feel confident. boto3's client name is simply "config".
One-line fix, but only a real invoke caught it β py_compile and terraform validate have no way to know a boto3 service name is wrong.
Partway through testing, every rule fell back to INSUFFICIENT_DATA and a brand-new test bucket simply never got discovered. describe-configuration-recorders came back empty β the recorder this stack had been reusing (borrowed from an unrelated project in the same account) was gone. CloudTrail confirmed it: StopConfigurationRecorder and DeleteConfigurationRecorder, timestamped minutes earlier, as part of unrelated cleanup on that other project. It also silently broke Week 11's already-published Security Hub FSBP controls, which depend on the same recorder existing.
This Lab now provisions and owns its own recorder (S3 delivery bucket, IAM role using the AWS_ConfigRole managed policy plus a bucket-write inline policy, the recorder, delivery channel, and status β nine resources total). One AWS account allows exactly one recorder per region, so this also fixed Week 11's dependency with zero changes to Week 11's own Terraform. The lesson generalizes: "reuse the account's centrally-managed recorder" is only safe if you actually own or control the thing you're calling centrally-managed β otherwise it's a dependency on someone else's cleanup schedule.
With the recorder back, the versioning rule stayed NON_COMPLIANT for twelve straight minutes with no remediation firing. describe-remediation-execution-status showed a real, specific denial: "User: ...ssm-automation-role/Automation-... is not authorized to perform: s3:PutBucketVersioning ... because no identity-based policy allows the s3:PutBucketVersioning action" β despite the role's policy explicitly listing that exact action. The IAM statement also carried an aws:ResourceTag/auto-remediate condition, copied from Week 11's EC2 pattern. Amazon S3 only supports tag-based IAM conditions for object-level actions; a condition on a bucket-level admin action like PutBucketVersioning simply never evaluates true, so the whole statement silently granted nothing.
Drop the tag condition from the IAM policy entirely. The guardrail doesn't disappear β it's still fully enforced by Config's own Scope.TagKey/TagValue (challenge #2's fix), which means an untagged bucket is never flagged non-compliant in the first place, so remediation never runs against it regardless of IAM. The general lesson: a tag-scoped-IAM trick that works for one service (EC2 security groups, confirmed in Week 11) doesn't automatically transfer to another (S3 bucket admin calls) β verify per action, not per service.
Security β Guardrails on the Automation
Tag-scoped opt-in, enforced by Config alone
The two S3 rules only ever evaluate buckets tagged auto-remediate=true, via Config's native Scope. An untagged bucket is never flagged non-compliant, so remediation never fires for it β no custom code decides this.
Native remediation substrate, not custom code
Both fixes are AWS-managed SSM Automation documents, verified live against this account before use. A single documented API call needs nothing more elaborate than that.
Notify, never invent
required-tags never auto-tags a resource. A human has to supply the actual cost-center value β automation only ever reports what's missing.
No silent failures
Failed reporter invocations land in a 14-day SQS dead-letter queue that alarms to SNS, same pattern as every remediation-adjacent Lambda since Week 11.
Cost
Single account, us-east-1. Prices as of July 2026 β verify at aws.amazon.com/config/pricing.
| Item | Estimate |
|---|---|
| Config rule + conformance pack evaluations ($0.001/eval, no free tier) | pennies / month at this account's resource count |
| Config recorder β configuration items ($0.003/item, continuous, all resource types, account-wide) | this Lab now owns the account's only recorder β bills for the whole account's inventory, not just this week's resources; roughly $1β5/month depending on total resource count and change frequency |
| Lambda + EventBridge + SNS + SQS | pennies (well within free tier) |
| S3 delivery bucket (90-day lifecycle expiry) | pennies |
| Destroyed | $0 for this week's own resources β but destroying also removes the account's only recorder; check nothing else (Week 11's Security Hub FSBP controls) still needs it first |
Cleanup
Queue a destroy plan on week-12-dev in the HCP UI. This removes the conformance pack, the reporter Lambda/schedule/SNS/DLQ, the SSM automation role β and the recorder itself, since this stack now owns the account's only one. Confirm nothing else depends on it first. Leftover test buckets clean up with ./scripts/cleanup.sh.
References
Key Takeaways
- A conformance pack is self-contained. It defines its own rules rather than wrapping externally-created ones β build standalone Config rules or a conformance pack, never both for the same rule.
- Config's Scope is either-or. Resource type filtering and tag filtering can't combine in one rule β use tag-only scoping when you need both a type and a tag constraint.
- Never hardcode a name AWS generates for you. Conformance-pack rule names get a real, generated suffix β discover them at runtime instead.
- A tag-scoped IAM guardrail doesn't automatically transfer between services. It works for EC2 security groups; it silently does nothing for S3 bucket-level admin actions. Verify per action.
- "Reuse the centrally-managed recorder" is only safe if you actually control it. Otherwise it's a dependency on someone else's cleanup schedule β own the resources you depend on, or expect them to disappear without warning.
Comments