Do you often wonder about how to maintain consistent security best practice in the cloud? With DevOps and agile being all the rage, how do you keep your infrastructure secure in these fast paced environments? If you're running on AWS, Security Hub is one tool that can help.
What is Security Hub?
AWS Security Hub is a managed security aggregation and compliance service provided by AWS. Security Hub will inspect changes to your infrastructure and compare them against a set of compliance packs, such as the Center for Internet Security (CIS) foundations benchmark. It raises findings for non-compliant resources, which can prompt further action and remediation. It also has multi-account support, so you can collect findings from all accounts into a central account.
It's like a linter, but for your infrastructure. This is important as we can't depend on linting CloudFormation (or CDK, or Terraform) templates, since not all projects use CloudFormation, and there isn't a security-focused linter available as far as I can tell.
For engineers and administrators responsible for managing infrastructure, this is a welcome tool for helping to maintain consistent security best practice and for keeping infrastructure secure.
Whilst it's becoming easier to manage multi-account environments, there are some challenges in the rollout of Security Hub that you'll want to keep in mind. In this article, we'll cover our experience, and tips for ensuring a smooth rollout and keeping it relevant for your organisation.
Multi-account rollout of Security Hub
Before we start, a warning; enabling Security Hub and compliance packs will cause a large number of AWS Config rules to execute for the first time across your accounts. If you have subscribed to AWS Config notifications (e.g. default AWS Control Tower setup), then you will receive a large influx of notifications during the rollout. We recommend you disable these subscriptions temporarily.
Also, as we recommend for all cloud services, please review the pricing pages for AWS Security Hub and AWS Config to assess their suitability for your budget. Enabling AWS Config does cause charges for configuration items recorded on every resource change, so keep that in mind.
Now, the first step to using Security Hub is to elect the admin account and then enable all other accounts as members. It consolidates findings into the admin account for each region to give a centralised view of your security state. If you have a security or audit focused account, then that's a good candidate.
The method we used to rollout Security Hub at the time was the Security Hub Enabler tool provided by AWS. As of November 2020, Security Hub now supports AWS Organizations, which is a much simpler method for enabling Security Hub.
To elect the Security Hub admin account, run this via the AWS CLI for your Organization root account (where 123456789012
is the account ID you want to elect) in each of the regions you want to use Security Hub:
aws securityhub enable-organization-admin-account --admin-account-id 123456789012
According to the documentation, this will also add all AWS Organizations accounts as Security Hub members.
Then, from your Security Hub main account, run the following to automatically add new accounts as Security Hub members in the future:
aws securityhub update-organization-configuration --auto-enable
That's it! You should now have the CIS AWS Foundations benchmark and AWS Foundational Security Best Practices standard packs enabled in all accounts by default.
What else to consider after deployment?
Compliance pack review
Once you have enabled Security Hub, we recommend reviewing the individual security controls for each of the compliance packs:
Not all controls will be relevant to your organisation, also there is some overlap between the two default packs. Also, we should enable rules that apply to global services in one region (e.g. IAM rules).
An example of a rule that we disabled is Foundations S3.5; "S3 buckets should require requests to use Secure Socket Layer". We felt that it's a low risk issue, is not applicable in all cases, can be auto remediated, and is laborious to add to every single S3 bucket.
An example of duplication between CIS and Foundations is, CIS rule 1.1 and IAM.4. Both check that the root user is not used, so you may like to disable one or the other.
Disabling irrelevant rules
Once you have an idea of the rules you want to enable, you need to roll it out. You can use Stack Sets or Control Tower Customisations to roll that out to all accounts and regions.
One drawback of Security Hub (as of December 2020) is the lack of CloudFormation support for managing individual security controls. You can rely on Custom resources backed by lambda functions to polyfill this in the meantime. We created the following inline function to do that:
SecurityHubRuleTogglerFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.7
Handler: index.handler
Role: !GetAtt SecurityHubRuleTogglerExecutionRole.Arn
Timeout: 300
MemorySize: 256
Environment:
Variables:
FND_RULES: !Join
- ","
- !Ref FoundationControlIds
CIS_RULES: !Join
- ","
- !Ref CISRuleIds
Code:
ZipFile: |
import time
import os
import json
import boto3
import cfnresponse
client = boto3.client('securityhub')
def handler(event, context):
try:
stds = client.get_enabled_standards()
cis_rules = list(
map(lambda id: f"CIS.{id}", os.getenv('CIS_RULES').split(',')))
fnd_rules = os.getenv('FND_RULES').split(',')
for std in stds['StandardsSubscriptions']:
is_foundation = 'aws-foundational-security-best-practices' in std['StandardsSubscriptionArn']
is_cis = 'cis-aws-foundations-benchmark' in std['StandardsSubscriptionArn']
if is_foundation or is_cis:
rules = cis_rules if is_cis else fnd_rules
controls = get_controls(std['StandardsSubscriptionArn'])
for control in controls:
expected_enabled = control['ControlId'] in rules
actual_enabled = control['ControlStatus'] == 'ENABLED'
if expected_enabled != actual_enabled:
print(
f"- {control['ControlId']} updating to: {'ENABLED' if expected_enabled else 'DISABLED'}.")
for i in range(3):
try:
client.update_standards_control(
StandardsControlArn=control['StandardsControlArn'],
ControlStatus='ENABLED',
) if expected_enabled else client.update_standards_control(
StandardsControlArn=control['StandardsControlArn'],
ControlStatus='DISABLED',
DisabledReason='Disabled by security control orchestration tool based on review of Security Hub rules',
)
except Exception as e:
if 'TooManyRequestsException' in f"{e}":
time.sleep(2 ** i)
print('TooManyRequestsException, retrying...')
continue
else:
raise e
break # BREAK OUT OF RETRY LOOP
else:
print(
f"- {control['ControlId']} is OK")
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
except Exception as e:
print(e)
cfnresponse.send(event, context, cfnresponse.FAILED, {})
def get_controls(sub_arn, NextToken=None):
controls = client.describe_standards_controls(
NextToken=NextToken,
StandardsSubscriptionArn=sub_arn) if NextToken else client.describe_standards_controls(
StandardsSubscriptionArn=sub_arn)
if ('NextToken' in controls):
return controls['Controls'] + get_controls(sub_arn, NextToken=controls['NextToken'])
else:
return controls['Controls']
Here's an example of parameters to supply to the template. Those in the lists will be ENABLED
, those omitted will be DISABLED
:
[
{
"ParameterKey": "FoundationControlIds",
"ParameterValue": [
"ACM.1",
"AutoScaling.1",
"CodeBuild.1"
// ... more here
]
},
{
"ParameterKey": "CISRuleIds",
"ParameterValue": [
"1.4",
"1.12",
"2.2"
// ... more here
]
}
]
Notifications and alerts
Now that you have Security Hub enabled and configured, you may want to subscribe to events. For example, you may want Slack or email notifications when Security Hub reports a failing security control.
We created an Event Bridge rule to achieve this. For example, for AWS Foundation events see the below CloudFormation YAML snippet:
SecurityHubFoundationEvents:
Type: AWS::Events::Rule
Properties:
Description: Whenever Security Hub findings occur for AWS Foundation standards
EventPattern:
source:
- "aws.securityhub"
detail-type:
- "Security Hub Findings - Imported"
detail:
findings:
Compliance:
Status:
- "FAILED"
Workflow:
Status:
- "NEW"
ProductFields:
ControlId: !Ref FoundationControlIds
State: ENABLED
Targets:
- Arn: !Ref MyTopic
Id: SecurityTopicFoundation
Where FoundationControlIds
is a list of AWS Foundation control IDs (e.g. AutoScaling.1
), and MyTopic
is a reference to a AWS::SNS::Topic
resource.
For CIS, copy-paste the above and replace the ProductFields
block with the following:
ProductFields:
RuleId: !Ref CISRuleIds
Where CISRuleIds
is a list of CIS rule IDs (e.g. 1.1
).
If you then subscribe to the topic, you can receive Security Hub notifications via Email, SMS or ChatOps. In our case, we created our own custom Slack application to manage notifications. We won't cover that in this article, but perhaps a separate one in the future!
Avoiding re-notification
Findings identified by Security Hub will perpetually remain in a NEW
workflow state until you resolve the finding, or manually adjust the state to NOTIFIED
or SUPRESSED
. It will re-send notifications for the same finding if updated at a later date, creating excessive noise for the team to sift through.
To deal with this, we created an inline lambda function (this time with NodeJS for variety) in CloudFormation to automatically progress NEW
notifications to NOTIFIED
:
SecurityHubFindingNotifiedFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs12.x
Handler: index.handler
Role: !GetAtt SecurityHubFindingNotifiedExecutionRole.Arn
MemorySize: 256
Timeout: 60
Code:
ZipFile: |
const AWS = require('aws-sdk')
exports.handler = async (event) => {
if (event.detail.findings.length !== 1)
throw Error(
`Expected one finding, found ${event.detail.findings.length}`,
)
const sh = new AWS.SecurityHub()
console.log(`Marking finding ID ${event.detail.findings[0].Id} as NOTIFIED`)
const result = await sh
.batchUpdateFindings({
FindingIdentifiers: [
{
Id: event.detail.findings[0].Id,
ProductArn: event.detail.findings[0].ProductArn,
},
],
Workflow: {
Status: 'NOTIFIED',
},
})
.promise()
if (
result.ProcessedFindings.length !== 1 ||
result.UnprocessedFindings.length !== 0
) {
throw Error(
'Finding was not processed (incorrect count returned from SDK)',
)
}
}
Then, add a new target to the event rules created in the previous section:
- Arn: !GetAtt SecurityHubFindingNotifiedFunction.Arn
Id: SecurityHubFindingNotified
Example findings
Now that you've enabled Security Hub, adjusted the enabled rules and added event rules, you may wonder what the actual findings look like...
My favourite test case for generating findings is to create an S3 bucket without encryption enabled, which should trigger S3.4. Let's deploy the following CloudFormation snippet (or create a bucket via the console if you prefer):
MyDodgeyBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
BucketName: my-dodgey-bucket
# Oops, no encryption...
#
# BucketEncryption:
# ServerSideEncryptionConfiguration:
# - ServerSideEncryptionByDefault:
# SSEAlgorithm: AES256
After about five minutes, our security-orientated Slack app reported the following:
I then inspected findings in the Security Hub console and found this:
Summary
We've learnt how to setup Security Hub and customise it to our organisation's needs and we've seen an example of the contents of a finding.
The feedback that these findings provide is useful for engineers and administrators, who are often busy solving problems and can overlook security best practice.
With Security Hub enabled, Auto Remediation is now a possibility. We'll cover that in a future article, as well as details on our custom Slack App.
Need help managing the security of your infrastructure? Please get in touch!