AWS Security Hub provides a comprehensive view of your security state across AWS accounts. It aggregates findings from multiple AWS services and third-party tools, enabling centralized security management and compliance monitoring. Think of Security Hub as a security operations dashboard — it does not detect threats itself (that is GuardDuty’s job), but it collects findings from every security service, scores your compliance posture, and gives you a single pane of glass. Without it, you would have to check GuardDuty, Inspector, Macie, and Config separately across every account and region.What You’ll Learn:
# Example controls:# [ACM.1] ACM certificates should be renewed within 30 days# [CloudTrail.1] CloudTrail should be enabled# [EC2.1] EBS snapshots should not be publicly restorable# [IAM.1] IAM policies should not allow full "*" administrative privileges# [S3.1] S3 Block Public Access should be enabled# [Lambda.1] Lambda functions should prohibit public access# [RDS.1] RDS snapshots should be private
# Disable a control that doesn't apply to your environment.# Common mistake: Disabling controls to make your compliance score look better# without documenting WHY. Always provide a specific disabled-reason. Auditors# will review suppressed controls, and "not applicable" without justification# is a red flag. A senior engineer documents: "We don't use S3 Object Lock# because our data retention is handled via lifecycle policies per SEC-2024-03."aws securityhub update-standards-control \ --standards-control-arn "arn:aws:securityhub:us-east-1:123456789012:control/aws-foundational-security-best-practices/v/1.0.0/S3.11" \ --control-status DISABLED \ --disabled-reason "We don't use S3 Object Lock"
# remediation.pyimport boto3import jsons3 = boto3.client('s3')ec2 = boto3.client('ec2')securityhub = boto3.client('securityhub')def lambda_handler(event, context): for finding in event['detail']['findings']: control_id = finding.get('GeneratorId', '') if 'S3.1' in control_id: remediate_s3_public_access(finding) elif 'EC2.19' in control_id: remediate_security_group(finding) elif 'IAM.3' in control_id: remediate_iam_access_keys(finding) # Update finding status update_finding_resolved(finding) return {'statusCode': 200}def remediate_s3_public_access(finding): """S3.1 - Enable S3 Block Public Access. Common mistake with auto-remediation: Blindly remediating without checking if the resource is intentionally public (e.g., a static website bucket). Production auto-remediation should check for an "AllowPublicAccess" tag before acting, or limit scope to non-production accounts initially. """ s3control = boto3.client('s3control') account_id = finding['AwsAccountId'] s3control.put_public_access_block( AccountId=account_id, PublicAccessBlockConfiguration={ 'BlockPublicAcls': True, 'IgnorePublicAcls': True, 'BlockPublicPolicy': True, 'RestrictPublicBuckets': True } )def remediate_security_group(finding): """EC2.19 - Security groups should not allow unrestricted access to high risk ports""" for resource in finding['Resources']: if resource['Type'] == 'AwsEc2SecurityGroup': sg_id = resource['Details']['AwsEc2SecurityGroup']['GroupId'] # Remove unrestricted SSH access ec2.revoke_security_group_ingress( GroupId=sg_id, IpPermissions=[{ 'IpProtocol': 'tcp', 'FromPort': 22, 'ToPort': 22, 'IpRanges': [{'CidrIp': '0.0.0.0/0'}] }] )def remediate_iam_access_keys(finding): """IAM.3 - IAM users' access keys should be rotated every 90 days""" iam = boto3.client('iam') for resource in finding['Resources']: if resource['Type'] == 'AwsIamUser': username = resource['Id'].split('/')[-1] # List and deactivate old keys (> 90 days) response = iam.list_access_keys(UserName=username) for key in response['AccessKeyMetadata']: # Check key age and deactivate if > 90 days passdef update_finding_resolved(finding): """Update finding workflow status to RESOLVED""" securityhub.batch_update_findings( FindingIdentifiers=[{ 'Id': finding['Id'], 'ProductArn': finding['ProductArn'] }], Workflow={'Status': 'RESOLVED'}, Note={ 'Text': 'Auto-remediated by Lambda', 'UpdatedBy': 'automation' } )
Managed Insights: - "AWS resources with the most findings" - "S3 buckets with public write or read permissions" - "AMIs that are generating the most findings" - "IAM users with the most findings" - "EC2 instances involved in known TTPs" - "Resources with the most failed security checks"
# Designate delegated administrator (from management account).# Best practice: Use a dedicated "Security" account as delegated admin,# NOT the management account. The management account should have minimal# operational tooling -- blast-radius reduction applied to your org root.aws securityhub enable-organization-admin-account \ --admin-account-id 111122223333# From delegated admin - enable auto-enable for members.# This ensures any NEW account added to the org automatically gets# Security Hub enabled with your chosen standards -- no manual steps.aws securityhub update-organization-configuration \ --auto-enable# Enable specific standards across orgaws securityhub update-organization-configuration \ --auto-enable \ --organization-configuration '{ "ConfigurationType": "CENTRAL" }'
# splunk_exporter.pyimport boto3import jsonimport osimport requestsfrom datetime import datetimedef lambda_handler(event, context): """ Forward Security Hub findings to Splunk via HTTP Event Collector (HEC). Cost tip: This Lambda fires per finding event. In a large org, that can mean thousands of invocations/day. Consider Kinesis Firehose as the EventBridge target instead -- Firehose batches events and delivers to Splunk in bulk, which is cheaper and more reliable than per-event Lambda invocations. """ splunk_url = os.environ['SPLUNK_HEC_URL'] splunk_token = os.environ['SPLUNK_HEC_TOKEN'] for finding in event['detail']['findings']: splunk_event = { "time": datetime.now().timestamp(), "host": "aws-security-hub", "source": "security-hub", "sourcetype": "aws:securityhub:finding", "event": finding } response = requests.post( splunk_url, headers={ "Authorization": f"Splunk {splunk_token}", "Content-Type": "application/json" }, json=splunk_event ) if response.status_code != 200: print(f"Failed to send to Splunk: {response.text}") return {'statusCode': 200}
Collects findings from GuardDuty and other services
Runs security standard checks (CIS, PCI, etc.)
Provides centralized dashboard and compliance scores
They work together: GuardDuty detects threats, Security Hub aggregates and tracks compliance.
Q2: How do you handle thousands of Security Hub findings?
Prioritization:
Focus on CRITICAL/HIGH severity first — anything else is noise at scale
Filter by compliance requirements (PCI, HIPAA) relevant to YOUR workload
Group by resource type or AWS account to assign ownership
Automation:
Auto-remediate common, low-risk issues with Lambda (e.g., enable S3 encryption)
Suppress accepted risks with filters AND documented justification
Use custom actions for findings that require human judgment (e.g., public SG)
Process (a senior engineer would set these SLAs):
CRITICAL = 24h resolution, notify via PagerDuty
HIGH = 7 days, tracked in Jira sprint
MEDIUM = 30 days, in backlog
LOW = Quarterly review
Weekly review of suppressed findings to catch drift
Track security score trends — a declining score over 2 weeks triggers an investigation
Cost tip: Security Hub costs 0.0010perfindingingestedpermonthafterthefirst10,000.WithGuardDuty,Inspector,andConfigallfeedingfindings,alargeorganizationcangenerate100K+findings/month(100+). Disable standards you do not need (e.g., PCI-DSS if you do not handle card data).
Q3: How do you set up Security Hub for a multi-account organization?
Designate Admin: Use delegated administrator in security account
Enable Auto-Enable: New accounts automatically get Security Hub
Central Configuration: Push standards from admin account
Cross-Region: Set up finding aggregator to central region