CoursesAWS security for DevOps engineersSecurity Hub and Config rules

Security Hub and Config rules

Findings in one place, drift as a signal.

Intermediate30 min · lesson 10 of 13

A hotel with forty smoke detectors and no panel at the front desk is forty separate beeps in forty separate corridors. Somebody has to walk the building to work out which one is going off, and by the time they reach the third floor the guest in 312 has already opened a window to clear the smoke. The panel downstairs puts out no fires. What it does is tell one person, in one place, which room is smoking and how badly, so the walk is short.

AWS Security Hub CSPM (Cloud Security Posture Management, a suffix Amazon added when it launched a second product under the old Security Hub name) is that panel. It collects security findings from other AWS services and from partner tools, puts them on one severity scale, and gives you a single queue to work. The detector wired into it that does the most work is AWS Config, a service that watches your account, writes down what every resource looks like each time it changes, and tests those written-down snapshots against rules. Config is the clerk with the clipboard walking the corridors. Security Hub is the panel the clerk reports to. GuardDuty, which you met in the last lesson, is the guard who notices behaviour rather than shape, and it reports to the same panel. Your command line still spells the service aws securityhub, so nothing you type changes.

The reason any of this counts as a security control and not a dashboard is timing. An attacker who gets in with a leaked key almost never stops at the first thing they touch. They widen a security group so they can reach a database from home. They attach an admin policy to a role they already control. They turn off block public access on a bucket so the data can be pulled straight out over HTTPS. Every one of those moves is a configuration change, and every configuration change is something Config sees. The distance between the change happening and a human being told about it is the whole point of this lesson.

What Config Actually Writes Down

Turning The Clerk On

Config's unit of work is the configuration item, shortened to CI in every AWS document you will read: a dated photograph of one resource holding its full settings, its tags, and its relationships to other resources. Change a security group rule and Config writes a new photograph. The old ones stay. That history is what lets you answer "when did this bucket become readable, and what did it look like the day before" without guessing.

One field will disappoint you if you go looking for it. A configuration item carries a relatedEvents list that was meant to point at the CloudTrail entries around the same moment, CloudTrail being the audit log of every API call made in your account. Since configuration item version 1.3 that list ships empty. Config tells you what changed and when. To find out who did it, you take the timestamp off the configuration item and run aws cloudtrail lookup-events yourself. Plenty of runbooks still tell people to read relatedEvents, and those runbooks were written before the field went quiet.

Switching Config on takes three calls, and only the last one starts anything. The recorder decides what gets photographed. The delivery channel decides where the photographs are shipped and who gets told.

terminal
aws configservice put-configuration-recorder \
--configuration-recorder name=default,roleARN=arn:aws:iam::444455556666:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig \
--recording-group allSupported=true,includeGlobalResourceTypes=true
aws configservice put-delivery-channel \
--delivery-channel name=default,s3BucketName=org-config-archive,s3KeyPrefix=444455556666,snsTopicARN=arn:aws:sns:eu-west-1:444455556666:config-changes
aws configservice start-configuration-recorder --configuration-recorder-name default
# none of the three print anything on success, so verify separately:
aws configservice describe-configuration-recorder-status
output
{
"ConfigurationRecordersStatus": [
{
"name": "default",
"lastStartTime": "2026-07-27T08:31:02.417000+00:00",
"recording": true,
"lastStatus": "SUCCESS",
"lastStatusChangeTime": "2026-07-27T08:31:47.902000+00:00"
}
]
}

Two details in those commands decide whether you end up with a working control or a bill. The S3 bucket named in the delivery channel should live in a separate log archive account, with a bucket policy that lets the Config service principal write and lets no human delete, the same shape as the CloudTrail archive from the previous lesson. S3 is Simple Storage Service, Amazon's object store. And includeGlobalResourceTypes=true belongs in exactly one region. IAM (Identity and Access Management, the service that holds your users, roles and policies) objects are global, so recording them in every region files the same photograph a dozen times, and Config bills per configuration item recorded. Leave the recording group out of the call altogether and Config defaults to every supported resource type except the four global IAM types, which is a defensible starting point and not at all what most people think they asked for.

Now the field that matters more than recording: true. Ask the recorder what it is actually recording.

terminal
aws configservice describe-configuration-recorders \
--query 'ConfigurationRecorders[0].recordingGroup'
output
{
"allSupported": false,
"includeGlobalResourceTypes": false,
"resourceTypes": [
"AWS::EC2::Instance"
],
"recordingStrategy": {
"useOnly": "INCLUSION_BY_RESOURCE_TYPES"
}
}
recording: true does not mean recording anything useful
Teams write a service control policy that denies config:StopConfigurationRecorder and config:DeleteConfigurationRecorder, then sleep well. The quieter move is config:PutConfigurationRecorder. The API reference describes it plainly as create or update, and says outright that you use it to change the roleARN and the recordingGroup of an existing recorder. An attacker who calls it with allSupported=false and one harmless resource type leaves the recorder running, the status green, the delivery channel intact, and every S3, IAM and security group change unrecorded. Nothing alarms, because nothing stopped. Deny PutConfigurationRecorder and PutDeliveryChannel alongside the obvious two, and alarm on all four in CloudTrail. The output above is what a neutered account looks like from the outside.

Security Hub Is Only As Honest As Config

Most Security Hub controls are not independent checks. They are AWS Config rules wearing a friendly name and a severity, created for you as service-linked rules whose names begin securityhub-. So if Config is not recording S3 buckets, the S3 controls do not fail. They return no data, and a control with no data is dropped from your security score before the arithmetic starts. The score is the share of controls that passed, measured against the controls that came back Passed, Failed or Unknown, with each control counted once even when three standards include it. Blind controls sit outside that fraction completely. That is how a half-configured account displays 96% and means very little.

Turn Hub on deliberately rather than accepting the defaults, then look at what standards exist before you subscribe to any of them.

terminal
# enable-security-hub returns an empty body, so it prints nothing at all
aws securityhub enable-security-hub \
--no-enable-default-standards \
--control-finding-generator SECURITY_CONTROL
aws securityhub describe-standards \
--query 'Standards[].{Name:Name,Arn:StandardsArn,OnByDefault:EnabledByDefault}'
output
[
{
"Name": "AWS Foundational Security Best Practices v1.0.0",
"Arn": "arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices/v/1.0.0",
"OnByDefault": true
},
{
"Name": "CIS AWS Foundations Benchmark v1.2.0",
"Arn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0",
"OnByDefault": true
},
{
"Name": "CIS AWS Foundations Benchmark v3.0.0",
"Arn": "arn:aws:securityhub:eu-west-1::standards/cis-aws-foundations-benchmark/v/3.0.0",
"OnByDefault": false
},
{
"Name": "CIS AWS Foundations Benchmark v5.0.0",
"Arn": "arn:aws:securityhub:eu-west-1::standards/cis-aws-foundations-benchmark/v/5.0.0",
"OnByDefault": false
},
{
"Name": "NIST Special Publication 800-53 Revision 5",
"Arn": "arn:aws:securityhub:eu-west-1::standards/nist-800-53/v/5.0.0",
"OnByDefault": false
}
]

Copy those ARNs (Amazon Resource Names, the long unique addresses AWS gives everything), do not retype them. The oldest CIS benchmark uses :::ruleset/ with no region and no account, while every version since uses <region>::standards/, and a hand-typed ARN fails with an error that will not tell you which character you got wrong. CIS here is the Center for Internet Security, a non-profit that publishes hardening checklists. Note that v1.2.0 is still one of the two standards AWS switches on by default, even though AWS now recommends v5.0.0. Subscribe to one standard first and see what it does to your queue before adding a second.

terminal
aws securityhub batch-enable-standards \
--standards-subscription-requests '[{"StandardsArn":"arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices/v/1.0.0"}]'
output
{
"StandardsSubscriptions": [
{
"StandardsSubscriptionArn": "arn:aws:securityhub:eu-west-1:111122223333:subscription/aws-foundational-security-best-practices/v/1.0.0",
"StandardsArn": "arn:aws:securityhub:eu-west-1::standards/aws-foundational-security-best-practices/v/1.0.0",
"StandardsInput": {},
"StandardsStatus": "PENDING"
}
]
}

PENDING means Hub is still creating the Config rules behind the control set. Re-run get-enabled-standards a few minutes later and you want READY. Give it longer if the standard shares Config rules with something already enabled, where AWS quotes up to eighteen hours before findings appear. The --control-finding-generator SECURITY_CONTROL flag you passed at enable time is the single most effective noise setting in the service. It produces one finding per control per resource however many standards happen to include that control. The older STANDARD_CONTROL behaviour gives you one finding per standard, and any account enabled before 23 February 2023 is still running it unless somebody changed it. Turn on FSBP (AWS Foundational Security Best Practices, Amazon's own house checklist), CIS v5.0.0 and NIST 800-53 under that older mode and one public bucket becomes three findings, three tickets, and three people asking the same team the same question.

Reading One Finding Properly

Every product reporting into Hub fills in the same form. It is called ASFF, the AWS Security Finding Format: a fixed JSON shape so a GuardDuty alert and a failed Config rule can sit in one queue and sort by the same severity field. Pull the worst of them.

terminal
aws securityhub get-findings \
--filters '{"SeverityLabel":[{"Value":"CRITICAL","Comparison":"EQUALS"}],
"ComplianceStatus":[{"Value":"FAILED","Comparison":"EQUALS"}],
"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}],
"WorkflowStatus":[{"Value":"NEW","Comparison":"EQUALS"}]}' \
--max-results 3 \
--query 'Findings[].{Control:Compliance.SecurityControlId,Sev:Severity.Label,Acct:AwsAccountId,Resource:Resources[0].Id}'
output
[
{
"Control": "S3.2",
"Sev": "CRITICAL",
"Acct": "444455556666",
"Resource": "arn:aws:s3:::prod-invoice-exports"
},
{
"Control": "EC2.19",
"Sev": "CRITICAL",
"Acct": "444455556666",
"Resource": "arn:aws:ec2:eu-west-1:444455556666:security-group/sg-0a1b2c3d4e5f60718"
},
{
"Control": "IAM.6",
"Sev": "CRITICAL",
"Acct": "111122223333",
"Resource": "AWS::::Account:111122223333"
}
]

Four fields decide what happens to a finding, and the one you drive daily is the last of them. Severity.Label runs INFORMATIONAL through CRITICAL and arrives from whichever product raised the finding, though you can override it with batch-update-findings when your own risk view disagrees. Compliance.Status is PASSED, FAILED, WARNING or NOT_AVAILABLE and comes from the check itself. RecordState is ACTIVE or ARCHIVED, and this is where most people guess wrong: Hub does not archive a finding because somebody fixed the problem. It archives when the resource is deleted, when you disable the control, when Config returns NOT_APPLICABLE, or when nothing has touched the finding for three to five days. Workflow.Status is yours to write, and it holds NEW, NOTIFIED, RESOLVED or SUPPRESSED. Teach on-call those four names once and your tickets stop needing a security engineer to translate them.

terminal
aws securityhub get-findings \
--filters '{"ComplianceSecurityControlId":[{"Value":"S3.2","Comparison":"EQUALS"}]}' \
--max-results 1 \
--query 'Findings[0].{Title:Title,Compliance:Compliance.Status,Also:Compliance.RelatedRequirements,Fix:Remediation.Recommendation.Url}'
output
{
"Title": "S3 general purpose buckets should block public read access",
"Compliance": "FAILED",
"Also": [
"NIST.800-53.r5 AC-21",
"NIST.800-53.r5 AC-3",
"PCI DSS v3.2.1/1.3.1"
],
"Fix": "https://docs.aws.amazon.com/console/securityhub/S3.2/remediation"
}

RelatedRequirements is why one queue beats three. The same failing bucket answers an auditor's NIST question (NIST being the US National Institute of Standards and Technology, whose 800-53 catalogue most public-sector contracts quote) and a card-data question from PCI DSS (Payment Card Industry Data Security Standard) without anyone maintaining a mapping spreadsheet. Remediation.Recommendation.Url points at a per-control page carrying the exact fix, and pasting it into the ticket body is the cheapest quality improvement available to your alerting.

From a config change to a ticket
1Someone changes a resource
console, CLI, Terraform apply, attacker
2Config records a CI
dated snapshot, usually within minutes
3Config rule evaluates
securityhub-* service-linked rule
4Control finding in Hub
ASFF, severity, Workflow NEW
5Automation rules run
admin account, aggregation region
6EventBridge match
CRITICAL or HIGH, NEW, ACTIVE
7Ticket or page
then set Workflow to NOTIFIED

Drift Is the Signal

Drift is the gap between the shape you declared in code and the shape the account is in right now. Config measures that gap continuously, and how fast you hear about it depends entirely on the rule's trigger type. A change-triggered rule fires when a matching configuration item lands, so a widened security group gets flagged within a few minutes. A periodic rule runs on a fixed clock, and your choices are one, three, six, twelve or twenty-four hours. Set a control to twenty-four hours and you have quietly accepted that a bad change can sit unremarked for most of a day. Some controls run both ways, which is why a bucket that goes public gets caught quickly and a bucket that was already public still gets picked up on the next sweep.

Config also answers questions across a whole account without walking every resource, which is how you test a hypothesis fast in the middle of an incident.

terminal
aws configservice select-resource-config \
--expression "SELECT resourceId, resourceName, awsRegion WHERE resourceType = 'AWS::EC2::SecurityGroup' AND configuration.ipPermissions.ipRanges = '0.0.0.0/0'"
output
{
"Results": [
"{\"resourceId\":\"sg-0a1b2c3d4e5f60718\",\"resourceName\":\"eks-node-shared\",\"awsRegion\":\"eu-west-1\"}"
],
"QueryInfo": {
"SelectFields": [
{ "Name": "resourceId" },
{ "Name": "resourceName" },
{ "Name": "awsRegion" }
]
}
}

Each result arrives as a JSON string inside an array, so pipe it through jq -r '.Results[] | fromjson' if you want to read it without squinting. Then confirm the matching rule is unhappy about the same thing, which tells you the control is genuinely working rather than sitting blind. EC2.19 is backed by the vpc-sg-restricted-common-ports Config rule, so the service-linked copy in your account carries that name with a random suffix.

terminal
aws configservice describe-compliance-by-config-rule \
--config-rule-names securityhub-vpc-sg-restricted-common-ports-3f8c1a2b \
--query 'ComplianceByConfigRules[].{Rule:ConfigRuleName,Status:Compliance.ComplianceType,Failing:Compliance.ComplianceContributorCount.CappedCount}'
output
[
{
"Rule": "securityhub-vpc-sg-restricted-common-ports-3f8c1a2b",
"Status": "NON_COMPLIANT",
"Failing": 1
}
]

Routing, Or It Is A Wall Poster

A panel nobody looks at is decoration. Hub pushes every finding it takes in, new or updated, onto EventBridge (the message bus that lets one AWS service hand a JSON document to another) as a Security Hub Findings - Imported event. A rule then decides which of those deserve a human.

sechub-pattern.json
{
"source": ["aws.securityhub"],
"detail-type": ["Security Hub Findings - Imported"],
"detail": {
"findings": {
"Severity": { "Label": ["CRITICAL", "HIGH"] },
"Workflow": { "Status": ["NEW"] },
"RecordState": ["ACTIVE"],
"Compliance": { "Status": ["FAILED"] }
}
}
}
terminal
aws events put-rule --name sechub-critical-to-queue \
--event-pattern file://sechub-pattern.json \
--state ENABLED
output
{
"RuleArn": "arn:aws:events:eu-west-1:111122223333:rule/sechub-critical-to-queue"
}
The same finding is imported again and again
A control that keeps failing gets re-evaluated on its schedule, and every re-evaluation lands as another Findings - Imported event. Point that rule straight at a ticket creator and one unfixed public bucket opens a fresh ticket twice a day forever, which is how alert fatigue really starts. Two fixes, and you want both. Since July 2025 Hub updates the existing control finding in place instead of minting a new one, so the finding Id is stable across re-checks. Key your ticket on that Id and a repeat updates the ticket you already have. Second, have the ticket creator call batch-update-findings to move Workflow.Status from NEW to NOTIFIED. The pattern above only matches NEW, and Hub resets NOTIFIED back to NEW in exactly two situations: the finding goes from ARCHIVED to ACTIVE, or Compliance.Status flips from PASSED to something worse. A check that has been failing all week does neither, so the queue stays quiet while the finding stays live and visible.

Send CRITICAL and HIGH to the ticket queue through a target that resolves the account ID into a team name from your account tags, because a ticket that says 444455556666 and nothing else costs an on-call engineer ten minutes of asking around at three in the morning. Send MEDIUM and LOW to a weekly digest instead. Track mean time to acknowledge on the CRITICAL queue, since that number tells you whether routing works, while the raw finding count only tells you how big your estate is.

In an organisation, do this once rather than once per account. The management account nominates a delegated administrator with aws securityhub enable-organization-admin-account --admin-account-id 111122223333, and that account then picks one aggregation region. The call has to be made from the region you want to aggregate into.

terminal
aws securityhub create-finding-aggregator \
--region-linking-mode ALL_REGIONS \
--region eu-west-1
output
{
"FindingAggregatorArn": "arn:aws:securityhub:eu-west-1:111122223333:finding-aggregator/5c9c3f0a-9a11-4f2e-bb0e-0e9a1c2d3e4f",
"FindingAggregationRegion": "eu-west-1",
"RegionLinkingMode": "ALL_REGIONS"
}

Write that region down somewhere people actually read, because automation rules and cross-region updates only work from there, and the failure mode is a rule that silently does nothing in the region you created it in. Add partner products one at a time as well. Every new source multiplies volume, and a queue that doubles overnight gets ignored wholesale, including the parts that were working fine.

Suppression With An Expiry Date

Some findings are genuinely fine. A sandbox account with no multi-factor authentication on a throwaway user does not deserve a page at 2am. Silence it in a way that leaves a trace, using an automation rule, which runs in the administrator account and in the aggregation region as findings arrive. Bear in mind the rule only touches findings ingested after you create it, so existing noise needs a one-off batch-update-findings to clear.

terminal
aws securityhub create-automation-rule \
--rule-name suppress-sandbox-iam5 \
--rule-order 10 \
--rule-status ENABLED \
--description "Sandbox 999988887777 does not page for IAM.5" \
--criteria '{"AwsAccountId":[{"Value":"999988887777","Comparison":"EQUALS"}],
"ComplianceSecurityControlId":[{"Value":"IAM.5","Comparison":"EQUALS"}]}' \
--actions '[{"Type":"FINDING_FIELDS_UPDATE",
"FindingFieldsUpdate":{
"Workflow":{"Status":"SUPPRESSED"},
"Note":{"Text":"Exception SEC-4412, expires 2026-10-01, owner platform-security",
"UpdatedBy":"automation-rule"}}}]'
output
{
"RuleArn": "arn:aws:securityhub:eu-west-1:111122223333:automation-rule/8d0a4f3c-2b61-4c5e-9f77-1a2b3c4d5e6f"
}

SUPPRESSED is the one workflow status Hub will not reset for you. Findings keep being re-evaluated and keep that status even when they go from archived back to active, so the noise stays gone. That is the point and also the hazard, because nothing in AWS ever expires a suppression on your behalf. The date in that note is a promise your calendar has to keep. Once a month, list what you have muted and read the notes.

terminal
aws securityhub get-findings \
--filters '{"WorkflowStatus":[{"Value":"SUPPRESSED","Comparison":"EQUALS"}],
"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}]}' \
--max-results 100 \
--query 'Findings[].{Control:Compliance.SecurityControlId,Acct:AwsAccountId,Why:Note.Text}' \
--output text
output
999988887777 IAM.5 Exception SEC-4412, expires 2026-10-01, owner platform-security
444455556666 S3.2 Exception SEC-3990, expires 2025-11-15, owner data-platform
444455556666 EC2.19 None

Three rows, three different problems. The first is a live exception with an owner and a date. The second expired eight months ago and has quietly become a permanent hole nobody agreed to. The third has no note at all, which means somebody clicked Suppress in the console and moved on, and neither you nor an auditor can now say why a critical control is muted. Findings with a null note are the ones to unsuppress first. While you are in there, give every enabled control an owning team, in a tag or a plain spreadsheet if that is what you have. Controls without an owner never get fixed. They get suppressed.

Keeping The Detectors On

Detection you can switch off with one API call is a suggestion. Put the off switches out of reach with a service control policy, which is an account-wide ceiling set in AWS Organizations that no IAM policy inside the account can rise above. It grants nothing by itself. It only caps what anything else can grant.

scp-protect-detection.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "KeepPostureTelemetryOn",
"Effect": "Deny",
"Action": [
"securityhub:DisableSecurityHub",
"securityhub:BatchDisableStandards",
"securityhub:DisassociateFromAdministratorAccount",
"securityhub:DeleteMembers",
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder",
"config:PutConfigurationRecorder",
"config:DeleteDeliveryChannel",
"config:PutDeliveryChannel",
"config:DeleteConfigRule"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/BreakGlassSecurityAdmin"
}
}
}
]
}

Two limits on that policy are worth knowing before you trust it. A service control policy never applies to the organisation management account, so an attacker who lands there stands outside this fence entirely, which is the whole argument for keeping no workloads and almost no humans in that account. And service control policies do not apply to service-linked roles, so denying Config actions here does not break the securityhub- rules that Hub creates on your behalf. Ordinary principals cannot edit those rules anyway. Config now also runs a separate service-linked configuration recorder for some features, and StopConfigurationRecorder has no effect on it by design, which is worth knowing so you do not mistake a green service-linked recorder for proof that your own recorder is healthy.

One more permission deserves your attention. securityhub:BatchUpdateFindings lets a caller flip findings to SUPPRESSED or RESOLVED and empty your queue without touching a single resource. It cannot rewrite a finding's title, its resource, or its compliance status, so the evidence survives and an auditor can still see the bucket was public. The tickets simply stop appearing, and nobody notices until the quarterly review. Keep that permission inside the security account, and alarm on the CloudTrail event whenever it comes from anywhere else.

Auto-Remediation And The Role That Does The Fixing

Config can fix things itself by running an SSM Automation document whenever a rule goes non-compliant. SSM is AWS Systems Manager, and an Automation document is a prewritten runbook: a named sequence of API calls with parameters, the sort of thing you would otherwise paste into a wiki page and hope somebody follows correctly at midnight.

remediate-s3-public.json
[
{
"ConfigRuleName": "s3-bucket-level-public-access-prohibited",
"TargetType": "SSM_DOCUMENT",
"TargetId": "AWSConfigRemediation-ConfigureS3BucketPublicAccessBlock",
"TargetVersion": "1",
"Automatic": true,
"MaximumAutomaticAttempts": 3,
"RetryAttemptSeconds": 60,
"Parameters": {
"AutomationAssumeRole": {
"StaticValue": {
"Values": ["arn:aws:iam::444455556666:role/ConfigRemediationS3PublicAccessOnly"]
}
},
"BucketName": { "ResourceValue": { "Value": "RESOURCE_ID" } }
}
}
]
terminal
aws configservice put-remediation-configurations \
--remediation-configurations file://remediate-s3-public.json \
--query 'FailedBatches'
output
[]

An empty FailedBatches list means it took. Note that the rule named there is your own copy of s3-bucket-level-public-access-prohibited, not the service-linked securityhub- rule, since those are managed by Hub and not yours to hang remediation off. The interesting part of the file is AutomationAssumeRole, because that is a role Systems Manager can assume and it holds write permissions on production resources, which makes it a target in its own right. Name it after the one thing it does and scope its policy to that one action on that one resource type. A convenience role carrying s3:* and iam:* is a robot with a master key, and an attacker who can trigger remediations can aim it.

Pick what you automate by asking how badly a wrong fix hurts. Re-enabling block public access on a bucket is safe, because the change is reversible in one call and the worst case is a legitimately public site going dark for ten minutes while somebody swears. Detaching an IAM policy automatically is a different animal, because the wrong detach takes down a deployment pipeline at the exact moment your responders need it working. Automate the reversible fixes with a small blast radius. Route identity changes to a human, with the remediation URL already sitting in the ticket.

Try This

Do this in a lab account you can throw away. Create a bucket, deliberately break the block public access setting on it, and watch the whole chain run end to end. S3.8 is the control that checks bucket-level block public access, and it is change-triggered, so you will not be waiting long.

terminal
aws s3api put-public-access-block --bucket my-lab-bucket-2026 \
--public-access-block-configuration \
BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
# wait for the Config rule to evaluate, then look for the control finding
aws securityhub get-findings \
--filters '{"ComplianceSecurityControlId":[{"Value":"S3.8","Comparison":"EQUALS"}]}' \
--query 'Findings[].{Res:Resources[0].Id,Compliance:Compliance.Status,Workflow:Workflow.Status,Record:RecordState}'
output
[
{
"Res": "arn:aws:s3:::my-lab-bucket-2026",
"Compliance": "FAILED",
"Workflow": "NEW",
"Record": "ACTIVE"
}
]

Now set the four settings back to true, wait for the next evaluation, and run the same query. Compliance flips to PASSED and Workflow flips to RESOLVED on its own, because Hub resolves control findings automatically once the check passes. RecordState stays ACTIVE, and that surprises people who expect a fixed problem to disappear from the record. It will not archive until the bucket is deleted or you disable the control. Nobody closed a ticket by hand in that round trip, and it is your proof the control genuinely works. Repeat it on every control you rely on, because the alternative is discovering mid-incident that a rule has been sitting blind for a year. Watch your retention too: Hub keeps archived control findings for thirty days from their last update and active findings for ninety, measured off the UpdatedAt field. Treat Hub as a working queue and your log archive as the permanent record.

Next up: EC2 instance roles and SSM Session Manager, and how to kill standing SSH access to bastions so there is no key left for anyone to steal.

Quick check
01Your Security Hub dashboard shows a security score of 97% with FSBP enabled, yet a colleague finds a publicly readable production bucket that no finding ever mentioned. What is the most likely explanation?
Correct — Most controls are Config rules underneath, and a control with no data neither fails nor counts toward the score's denominator.
Incorrect — The score covers every enabled control that returned Passed, Failed or Unknown, at all severities, and S3.2 is rated CRITICAL.
Incorrect — FSBP covers public read and public write on buckets directly, as S3.2 and S3.3, both rated CRITICAL.
Incorrect — get-findings applies no severity filter unless you pass one, so nothing would be hidden that way.
02An engineer holds config:PutConfigurationRecorder but your service control policy denies both StopConfigurationRecorder and DeleteConfigurationRecorder. What can they still do to your detection?
Incorrect — PutConfigurationRecorder is documented as create or update, and updating the recording group is exactly what it is for.
Incorrect — That needs S3 permissions on the archive bucket, which is a separate control in a separate account if you built it properly.
Correct — Put replaces the recorder definition in place, and the status stays green because nothing was ever stopped.
Incorrect — Severity lives in Security Hub findings and is unreachable through any AWS Config API.
03Your EventBridge rule matches CRITICAL or HIGH with Workflow.Status NEW and opens a Jira ticket. A public bucket has been failing for a week and the team now has fourteen open tickets for that one bucket. What is the right fix?
Incorrect — That flag belongs to GuardDuty. Security Hub has no equivalent, and control findings would keep being re-imported on the control's own schedule.
Incorrect — Hub archives when the resource is deleted, the control is disabled or the finding goes stale, not when a problem is fixed, so that pattern would match almost nothing you want to see.
Incorrect — Suppression mutes a live critical finding, which swaps a ticketing problem for a security problem.
Correct — The pattern only matches NEW, and Hub resets NOTIFIED to NEW only when a finding returns from ARCHIVED or flips from PASSED back to FAILED, neither of which a continuously failing check does.

Takeaway

The trap worth remembering here: recording: true does not mean recording anything useful. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related