Business Challenge
A platform team has an account baseline. It is written down, it is applied by a pipeline, and it is good. During a rush eight months ago an account was created by hand for a proof of concept, the proof of concept became a product, and the account never went through the pipeline. Somebody notices during an audit.
Most of the list applies cleanly. Block Public Access goes on at the account level in one call. Encryption by default is a switch. Region restriction is an SCP the account inherits the moment it is moved into the right OU. A handful of items need remediation work β the unencrypted volumes created over eight months have to be snapshotted and rebuilt β and that is annoying, schedulable, and finite.
Then the team reaches three items and stops. The organisation trail, the Config recorder and GuardDuty can all be switched on this afternoon, and every one of them will start working immediately. None of them will tell you anything about the eight months that already happened. The auditor's question is not "is this account monitored now". It is "show me who changed the security group on this database, and when".
That answer does not exist. It was never written down, and there is no procedure, budget or escalation that creates it now.
Every item on a good baseline is important. What separates them is whether the control is a setting β a state the account is in, which can be entered at any time β or a record, whose value is the history it accumulated. Settings applied late are late. Records started late have a hole in them, permanently, and the hole is exactly the period nobody was watching.
Architecture
Sort the baseline into two columns before designing anything. The sort is mechanical: ask what the control produces, and whether producing it late gives you the same artefact.
The reversible column, and what lateness actually costs there
These are settings. Switching them on in month eight produces the same state as switching them on in week one, and the bill for the delay comes in two parts: the exposure during the gap, and remediating whatever was created while the setting was off.
The second part is not always small. You can't directly encrypt existing unencrypted volumes or snapshots. The documented route is to create a snapshot of the volume and then use the snapshot to create a new encrypted volume β per volume, with the downtime that implies, and you cannot change the KMS key that is associated with an existing snapshot or volume either. Eight months of unencrypted volumes is a project. It is, however, a project with an end date.
S3 Block Public Access is the cheapest of them and has a property worth knowing. Applied at the account level it applies to all buckets and access points that are owned by that account, and the settings apply to all AWS Regions globally β one call, no Region loop. And because a bucket policy can be edited by anyone with the permission, AWS makes a specific recommendation about where to apply it: if BlockPublicPolicy is enabled for the entire account rather than for a specific bucket, Amazon S3 blocks public policies even if a user alters the bucket policy to disable this setting.
Block public access settings don't alter existing policies or ACLs. Therefore, removing a block public access setting causes a bucket or object with a public policy or ACL to again be publicly accessible. Turning it on does not fix the public bucket policy someone wrote β it masks it. The account is safe while the setting is on and reverts the instant it is off, which makes the setting itself a thing to protect with an SCP rather than a remediation you can tick off.
The path-dependent column, and why it is not about cost
Nothing in this column is hard or expensive to enable. That is what makes it easy to defer, and deferring it is the only mistake in this post that cannot be corrected.
AWS Config is the clearest case. The configuration recorder stores the configuration changes to the resource types in scope as configuration items, and AWS Config creates a configuration item whenever it detects a change to a resource type that it is recording. A configuration history is then a collection of the configuration items for a given resource over any time period β which is to say, it is made of the items that were recorded. Turn the recorder on in month eight and the configuration history for months one to seven is not thin or degraded. It is empty, and it always will be.
GuardDuty is the same shape with a friendlier surface. When you enable it, it automatically starts ingesting the foundational data sources associated with that account β CloudTrail management events, VPC flow logs and DNS logs β and you don't need to enable anything else. Nothing to configure, nothing to wire up, and on first enablement it turns on every protection plan except Runtime Monitoring inside a 30-day free trial. There is no argument against having had it on. There is also no finding it can produce about last March.
CloudTrail: ninety days of grace, and why that is worse than nothing
CloudTrail deserves its own treatment because it is the one that hides the problem. CloudTrail is enabled by default for your AWS account and you automatically have access to the CloudTrail event history, which is a viewable, searchable, downloadable, and immutable record of the past 90 days of management events in an AWS Region, and there are no CloudTrail charges for viewing the event history.
So an account with no trail at all is not obviously broken. Open the console and there is activity, searchable, going back months. Everything looks fine. The documentation is direct about what you actually have: the event history is limited to the past 90 days of events. For an ongoing record of events in your AWS account, create an event data store or a trail.
Three further limits compound it, and each one bites precisely when you are under pressure:
- The event history doesn't provide organization level event aggregation. There is no way to ask a question across the estate.
- An event history search is limited to a single AWS account, only returns events from a single AWS Region, and cannot query multiple attributes. One filter, one Region, one account β and an incident is rarely confined to any of those.
- The Event history page only shows management events. It does not show data events, Insights events, or network activity events. So "who read that object" was never in scope at all.
A rolling ninety-day window that looks like an audit log is the most dangerous shape available. An account with nothing would be noticed. An account with ninety days is noticed on the day someone asks about month four.
Why This Architecture Holds Up
It gives the baseline an argument a checklist cannot make
"Apply the baseline before the account is used" is a policy everyone agrees with and nobody can defend under schedule pressure, because every individual item sounds deferrable. The two-column split changes the conversation: three of these items are not deferrable in the ordinary sense, because deferring them is a decision to never have the data. That is a different sentence from "we will get to it", and it survives contact with a deadline.
It tells you what to automate first
Automation effort is finite and baselines are long. Sorting by column rather than by perceived importance produces an unambiguous order: everything path-dependent goes into account creation itself, because a gap there is permanent, and the reversible items can be enforced by detection and remediation afterwards without loss. That is why these controls belong to account creation and not to a post-creation pipeline that runs "soon".
It explains why organisation-level enablement is the right shape
Every control in the path-dependent column can be applied to the organisation rather than to an account β but not all in the same way, and the difference matters. An organisation trail and GuardDuty with auto-enable genuinely cover accounts that do not exist yet. AWS Config does not work like that: you can have only one customer managed configuration recorder for each AWS account for each AWS Region, so a recorder has to be deployed into every account and Region, and AWS documents Systems Manager Quick Setup for exactly that β to create a customer managed configuration recorder across multiple organizational units (OUs) and AWS Regions using AWS best practices.
An aggregator is not that mechanism and cannot substitute for it. Aggregators provide a read-only view into the source accounts and regions that the aggregator is authorized to view, and do not provide mutating access into a source account or region. It is the console you look at, not the thing that records. Deploy the recorders, then aggregate them β an aggregator over accounts with no recorder is an empty dashboard that looks like a healthy one, which is this post's own failure mode wearing a different hat.
The reversible column stays honest about its own cost
Calling something reversible is not calling it free. The EBS remediation is real work with real downtime, and the S3 masking behaviour means a late Block Public Access does not leave you in the same place as an early one β it leaves you with public policies still written, suppressed by a setting. The model does not claim lateness is cheap. It claims lateness is payable, which the other column is not.
Key Architecture Decisions
An account template applies when the template is used. The failure mode in this post is an account created without it, which is a category of event no template can cover. An organisation trail and GuardDuty with auto-enable for new accounts both apply to accounts that do not exist yet and to accounts created by people who did not read the runbook.
Config is the exception, and the one people get wrongThere is no organisation-level recorder. You can have only one customer managed configuration recorder for each AWS account for each AWS Region, so recording has to be deployed — by Systems Manager Quick Setup across OUs and Regions, by Control Tower, or by a StackSet — and then aggregated for the central view. An aggregator is a read-only view and enables recording nowhere; reaching for it as the org-level form of Config produces a dashboard over accounts that are not recording, which fails exactly as silently as everything else in this post.
Then stop it being switched offAdd an SCP denying the actions that disable these locally — cloudtrail:StopLogging, cloudtrail:DeleteTrail, config:StopConfigurationRecorder, config:DeleteConfigurationRecorder, guardduty:DeleteDetector. Recording that a member account can switch off is recording with an opt-out.
Because event history is free and on by default, an account missing its trail produces no error, no finding and no empty console. The only reliable signal is a check that asks the question directly: does every account in the organisation appear in the organisation trail's coverage, and does every Region have a recorder.
The number that mattersNinety days is how long you have to notice. A quarterly reconciliation is therefore exactly the wrong cadence — it is the interval at which a missed account is discovered at roughly the moment its evidence expires. Run the check monthly, and treat a newly discovered account as an incident with a known data loss rather than a configuration task.
Because block public access settings don't alter existing policies or ACLs, an account that has had BPA switched on is one API call away from being exactly as public as it was before. The public bucket policies are still there, unmodified, waiting.
Two things, not oneEnforce the setting centrally, and run IAM Access Analyzer over the account to find the policies underneath it so they can actually be removed. Doing only the first leaves a loaded control; doing only the second leaves nothing stopping the next one.
Use the Organizations policy, not an SCP denyThe obvious move — denying s3:PutAccountPublicAccessBlock in an SCP — is a trap, because that is the same API for setting the block as for removing it. A blanket deny stops your own landing zone applying the baseline in the first place. S3 has a purpose-built answer: block public access can be managed at the organization level, where organization-level policies automatically apply to member accounts and S3 applies the most restrictive combination of organisation, account and bucket settings. That enforces the block without also blocking the people trying to enforce it.
By default, a customer managed configuration recorder records all supported resources in the AWS Region where AWS Config is running. That default is the right starting point, because the resource type you excluded is the one the question will be about, and excluding it is a decision made before you know the question.
Except it does not quite record everythingRead the default precisely, because the gap is in the worst possible place: the default for a customer managed configuration recorder is to record all supported resource types, excluding the global IAM resource types AWS::IAM::Group, AWS::IAM::Policy, AWS::IAM::Role and AWS::IAM::User. A recorder left on its defaults is not recording changes to your roles and policies — which, for anyone reading this series, is close to the only configuration history they would ever want. Include them explicitly, in one Region, since they are global.
Narrow by resource type with a written reason, never by Region. A Region with no recorder is not cheaper coverage, it is a blind spot — and blind spots are where things get put deliberately. If cost is genuinely the constraint, change the recording frequency before changing the scope; a daily configuration item is a weaker record than a change-triggered one, but it is still a record.
Sorting a baseline: the two questions
| Control | Produces | Add late? | Cost of lateness |
|---|---|---|---|
| Organisation CloudTrail trail | A record | No | Everything older than 90 days is gone |
| Config recorder | A record | No | No configuration history at all for the gap |
| GuardDuty | A record | No | No findings for anything that happened first |
| S3 Block Public Access | A setting | Yes | Exposure window; underlying policies remain and are only masked |
| EBS encryption by default | A setting | Yes | Snapshot-and-rebuild per existing volume, with downtime |
| Region restriction (SCP) | A setting | Yes | Migrating whatever was built in a Region you now deny |
| Root MFA, alternate contacts | A setting | Yes | Small, but per-account and easy to lose track of at scale |
Reference: the SCP that keeps the recording on
The controls in the first three rows are worth nothing if a member account can switch them off, and the point of an organisation-level control is that the account cannot opt out. This is the deny that makes that true. It sits in the deny sweep, so no identity policy in any member account overrides it.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RecordingIsNotOptional",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail",
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder",
"config:DeleteDeliveryChannel",
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn":
"arn:aws:iam::111122223333:role/OrgSecurityBreakGlass"
}
}
}
]
}
Note what this does not do: it grants nobody the ability to manage any of these services. It is a ceiling, so the security tooling account still needs identity policies allowing the actions it legitimately performs β and because SCPs do not apply to the management account, none of this constrains an administrator there, which is one more reason to keep that account empty.
Pin the break-glass principal to a real account ID. Writing arn:aws:iam::*:role/OrgSecurityBreakGlass looks tidier and destroys the policy: any member-account administrator who can create an IAM role can create one with that name and exempt themselves from every deny in it. The exception must name the account the break-glass role actually lives in.
And note what is not in the list. s3:PutAccountPublicAccessBlock does not belong here, because it is the same API for setting Block Public Access as for removing it — denying it stops your own baseline being applied. Enforce that one through the Organizations-level S3 block public access policy instead, as in decision 3.
Closing Thought
Most security work is reversible in the sense that matters: you can arrive late, do the work, and end up where you would have been. That is why "we will harden it after launch" is so often a reasonable trade rather than negligence β for most controls, it genuinely is.
A small number of controls are not like that, and they are unusually easy to defer precisely because they are cheap, quiet and produce nothing visible on the day you turn them on. Their entire output is a record, and the record is generated in real time or not at all. There is no supplier, no budget line and no escalation that produces last March.
The test to apply to every item on a baseline: if I switch this on a year late, do I end up with the same thing I would have had? Where the answer is yes, you have a scheduling problem, and scheduling problems have solutions. Where the answer is no, it is not a baseline item at all β it is a property of how accounts are created, and it belongs there rather than on a list someone works through afterwards.
Security & Identity — the log archive account: why the destination for all this recording belongs somewhere nobody can write to, what object lock actually guarantees, and the difference between a backup of your logs and a log you cannot alter.
Official AWS Reference
- AWS Documentation β Working with CloudTrail event history
- AWS Documentation β AWS Config terminology and concepts
- AWS Documentation β Working with the configuration recorder
- AWS Documentation β What is Amazon GuardDuty?
- AWS Documentation β Amazon EBS encryption
- AWS Documentation β Blocking public access to your Amazon S3 storage
- AWS Documentation β Service control policies (SCPs)
Comments