Parameters: SNIRuleGroupName: Type: String Default: 'AllowListedSNIDomains' FingerprintRuleGroupName: Type: String Default: 'AutoUpdating-AllowListedTLSFingerprints' Resources: SNIRuleGroup: Type: 'AWS::NetworkFirewall::RuleGroup' Properties: RuleGroupName: !Sub "${AWS::StackName}-${SNIRuleGroupName}" Type: STATEFUL RuleGroup: RulesSource: RulesSourceList: GeneratedRulesType: ALLOWLIST Targets: - www.aws.com TargetTypes: - TLS_SNI Capacity: 1000 Description: !Sub 'This rule is used to manage which domains are limiting access. The CloudWatch Events Rule: TLFingerprintStatefulRulegroupHourlyTrigger, triggers a daily parse of the list of domains and adds their TLS fingerprint to the Rule Group: AllowListedTLSFingerprints' Tags: - Key: "ProjectName" Value: "AllowListedTLSFingerprints" FingerprintRuleGroup: Type: 'AWS::NetworkFirewall::RuleGroup' Properties: RuleGroupName: !Sub "${AWS::StackName}-${FingerprintRuleGroupName}" Type: STATEFUL RuleGroup: RulesSource: RulesString: !Sub '#This rule is automatically managed by the CloudWatch Events Rule: TLFingerprintStatefulRulegroupHourlyTrigger; it fetches the domain list from the SNI Domain Rule Group and updates the TLS fingerprints daily.' Capacity: 1000 Description: !Sub 'This rule is automatically managed by the CloudWatch Events Rule: TLFingerprintStatefulRulegroupHourlyTrigger; it fetches the domain list fromthe SNI Domain Rule Group and updates the TLS fingerprints daily.' Tags: - Key: "ProjectName" Value: "AllowListedTLSFingerprints" LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: LambdaLogs PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' - PolicyName: NetworkFirewall PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'network-firewall:*' Resource: - !GetAtt FingerprintRuleGroup.RuleGroupArn - Effect: Allow Action: - 'network-firewall:DescribeRuleGroup' Resource: - !GetAtt SNIRuleGroup.RuleGroupArn Tags: - Key: "ProjectName" Value: "AllowListedTLSFingerprints" ScheduledRule: Type: AWS::Events::Rule Properties: Description: "TLFingerprintStatefulRulegroupHourlyTrigger" ScheduleExpression: "cron(0 0 * * ? *)" State: "ENABLED" Targets: - Arn: Fn::GetAtt: - "LambdaFunction" - "Arn" Id: "TargetFunctionV1" LambdaLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" DeletionPolicy: Retain PermissionForEventsToInvokeLambda: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref "LambdaFunction" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: Fn::GetAtt: - "ScheduledRule" - "Arn" LambdaInvoke: Type: AWS::CloudFormation::CustomResource Version: "1.0" Properties: ServiceToken: !GetAtt LambdaFunction.Arn LambdaFunction: Type: AWS::Lambda::Function Properties: Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Handler: index.handler Timeout: 600 Description: Used to check TLS server fingerprint and update the associated RuleGroup Tags: - Key: "ProjectName" Value: "AllowListedTLSFingerprints" Code: ZipFile: !Sub | var AWS = require("aws-sdk"); var response = require('cfn-response'); const https = require("https"); const tls = require('tls'); const nf = new AWS.NetworkFirewall(); function genSid() { return Math.floor(Math.random() * 1000000000); } async function getDomains(arn){ let params = {Type: "STATEFUL", RuleGroupArn: arn}; let res = await nf.describeRuleGroup(params).promise(); if (res.RuleGroupResponse) { console.log("Found source rulegroup"); let domains = (res.RuleGroup.RulesSource.RulesSourceList.Targets); return domains; } else { console.log("ERROR: No matching Rule Group found"); } return; } function fetchCert(host) { let fCert = {subject: {CN: ""}, fingerprint: ""}; const options = { hostname: host, port: 443, path: "/", method: 'GET', checkServerIdentity: function(host, cert) { const err = tls.checkServerIdentity(host, cert); if (err) { return err; } fCert.subject.CN = cert.subject.CN; fCert.fingerprint = cert.fingerprint.toLowerCase(); } }; options.agent = new https.Agent(options); return new Promise((resolve, reject) => { let req = https.request(options, (res) => { res.on('data', d => {}); res.on('end', () => { console.log(' Fetching from:', host); console.log(' Subject Common Name:', fCert.subject.CN); console.log(' Certificate SHA-1 fingerprint:', fCert.fingerprint); resolve(fCert); }); res.on('error', (err) => { reject(err); }); }); req.end(); }); } async function updateRuleGroup(newRule){ let params = {Type: "STATEFUL", RuleGroupArn: '${FingerprintRuleGroup.RuleGroupArn}'}; let res = await nf.describeRuleGroup(params).promise(); if (res.RuleGroupResponse) { console.log("Found destination rulegroup"); res.RuleGroup.RulesSource.RulesString = newRule; delete res.Capacity; res.RuleGroupName = res.RuleGroupResponse.RuleGroupName; res.Description = res.RuleGroupResponse.Description; res.Type = res.RuleGroupResponse.Type; delete res.RuleGroupResponse; console.log("Updating rules"); let result = await nf.updateRuleGroup(res).promise(); if (result) { console.log("Updated '" + res.RuleGroupName); } else { console.log("Error updating '" + res.RuleGroupName + "'..."); } } else { console.log("No matching Rule Group found"); } return; } exports.handler = async (event, context) => { if (event.RequestType == "Delete") { await response.send(event, context, "SUCCESS"); return; } let sourceArn = '${SNIRuleGroup.RuleGroupArn}'; console.log("Fetch a list of domains from: ", sourceArn); let domains = await getDomains(sourceArn); if (domains) { console.log("Using a list of: " + domains.length + " domains"); let newRule = '# This rule is automatically managed by a Lambda\n# Last updated: ' + new Date().toUTCString() + "\n"; for (let index = 0; index < domains.length; index++) { let fCert = await fetchCert(domains[index]); if (fCert) { newRule += 'pass tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Allow https://' + domains[index] + '/"; tls.fingerprint:"' + fCert.fingerprint + '"; sid:' + genSid() + '; rev:1;)\n'; } else { newRule += '# ERROR: Unable to retrieve a fingerprint for: ' + domains[index] + '\n'; } } newRule += 'drop tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Drop all other TLS fingerprints"; tls.fingerprint:":"; sid:1; rev:1;)'; await updateRuleGroup(newRule); if (event.ResponseURL) await response.send(event, context, "SUCCESS"); } else { console.log("Error fetching a list of domains from: ", sourceArn); if (event.ResponseURL) await response.send(event, context, "FAILED"); } return; };