はじめに
よくAWSにふれるものです。
以前、弊社のブログでそちらについて詳しく書きましたが、そちらで書ききれなかったことを加えて改めて執筆します。
AWSはマネージドサービスも多く比較的容易に多くのシステムを構築することができますが、作成したリソースが多くなったり、開発者が多くなるほど、セキュリティチェックが曖昧になりがちなので、予期せぬセキュリティホールを作らないように運用する必要があります。
そんな時にはAWS Config(以下Config)とGuardDutyが役に立ちます。
今回はその特徴を捉え、実践的、応用的な使い方について紹介していきたいと思います。
ConfigとGuardDutyについて
仕様
両者ともにS3, IAM, EC2を始めとしたリソースに対するセキュリティチェックを行いますが、詳しくは以下のような違いがあります。
Config
- 多くのAWSサービスのセキュリティチェックに対応している
- 事前にチェックする項目を選択できる
- セキュリティチェックをカスタマイズできる
- セキュリティチェックにかかったリソースを自動修復することができる
GuardDuty
- EC2, IAM, S3へのチェック
- CloudTrailログ、VPCフローログ、DNSログからチェック
- セキュリティチェックは事前に用意されているものが自動で働く
- セキュリティチェックのカスタマイズはできない
基本的に Config = AWSリソースの設定ミス を検知してくれるもの、GuardDuty = 潜在的なセキュリティ脅威(バックドア、ポートスキャン、マルウェア) を検知するものという認識で良いと思います。
ちなみに一部のセキュリティチェックは両者で被っているものもあるので、除外するといいでしょう。
両者の詳細な機能に関してはドキュメントに書いてあるので、省きます。
料金について
詳しくはドキュメントを見ることをおすすめします。
Configの料金
GuardDutyの料金
ここでは両者の金額を抑えるポイントを書きたいと思います。
まずConfigに関してですが、主な料金は設定項目(リソースの設定や依存関係に変更のあった際に記録されるもの)、とルール評価です。
なので無駄に設定項目の範囲を広げないこと、ルールを作りすぎないことがポイントになります。
私はCloudFormationでConfigを作成しますが、以下のようにAllSupported = falseとしResourceTypeを指定することで、対象のリソースを絞り、設定項目の数を減らすことができます。(対象のリソースを絞らない場合はAllSupported = true)
ConfigRecorder: Type: AWS::Config::ConfigurationRecorder DependsOn: - ConfigRecorderRole - ConfigBucketPolicy Properties: Name: configuration-recorder RecordingGroup: AllSupported: false IncludeGlobalResourceTypes: false ResourceTypes: - AWS::EC2::Host - AWS::EC2::Instance - AWS::EC2::NetworkInterface - AWS::EC2::SecurityGroup - AWS::S3::Bucket - AWS::S3::AccountPublicAccessBlock - AWS::RDS::DBInstance - AWS::RDS::DBSecurityGroup - AWS::RDS::DBSnapshot - AWS::RDS::DBCluster - AWS::RDS::DBClusterSnapshot - AWS::IAM::User - AWS::IAM::Group - AWS::IAM::Role - AWS::IAM::Policy - AWS::ACM::Certificate - AWS::CloudTrail::Trail - AWS::Lambda::Function - AWS::Config::ResourceCompliance RoleARN: !GetAtt ConfigRecorderRole.Arn
ルールに関しては以下ドキュメントにあるようにかなりの種類があるので、必要なものだけpick upすることで料金を最小限に抑えることができます。
・マネージドルール一覧
一方、GuardDutyに関してはVPCフローログの有無、CloudTrailのデータ収集範囲が大きく関わります。特に後者に関しては注意が必要で、CloudTrailで証跡を作成した際にデータイベントを有効にしている場合、そのS3バケットへのGetObject, PutObject, DeleteObjectアクションが多い場合、相当な数のイベントが発生します。そのイベント単位でGuardDutyに料金が加算されていくので、導入する際はまずCloudTrailの証跡でデータイベントが有効になっているものがあるかのチェックをしましょう。
Config
細かい使い方についてはドキュメントに書いてあるので省きます。
ここでは大きく、実践的、応用的な使い方について書いていこうと思います。
実践
リソース作成
CloudFormationで作成します。
必要なリソースは以下です。
AWS::Config::ConfigurationRecorder
AWS::Config::AggregationAuthorization
AWS::Config::DeliveryChannel
AWS::Config::ConfigRule
AWS::S3::Bucket
AWS::S3::BucketPolicy
AWS::IAM::Role
ここでポイントとなることが1点。後述することに繋がりますが、AWS::Config::AggregationAuthorizationを作成することで親アカウントへConfig結果を共有することができます。そうすることで、親アカウントで子アカウントの結果を包括的に管理、監視することができます。
リソース例は以下(IAM、S3は省きます)
ConfigRecorder: Type: AWS::Config::ConfigurationRecorder DependsOn: - ConfigRecorderRole - ConfigBucketPolicy Properties: Name: configuration-recorder RecordingGroup: AllSupported: false IncludeGlobalResourceTypes: false ResourceTypes: - AWS::EC2::Host - AWS::EC2::Instance - AWS::EC2::NetworkInterface - AWS::EC2::SecurityGroup - AWS::S3::Bucket - AWS::S3::AccountPublicAccessBlock - AWS::RDS::DBInstance - AWS::RDS::DBSecurityGroup - AWS::RDS::DBSnapshot - AWS::RDS::DBCluster - AWS::RDS::DBClusterSnapshot - AWS::IAM::User - AWS::IAM::Group - AWS::IAM::Role - AWS::IAM::Policy - AWS::ACM::Certificate - AWS::CloudTrail::Trail - AWS::Lambda::Function - AWS::Config::ResourceCompliance RoleARN: !GetAtt ConfigRecorderRole.Arn AggregationAuthorization: Type: AWS::Config::AggregationAuthorization Properties: AuthorizedAccountId: !Ref AuthorizedAccountId AuthorizedAwsRegion: !Ref AuthorizedAwsRegion DeliveryChannel: Type: AWS::Config::DeliveryChannel DependsOn: - ConfigBucketPolicy Properties: ConfigSnapshotDeliveryProperties: DeliveryFrequency: "Six_Hours" S3BucketName: !Ref ConfigBucket RootAccountMFAEnabled: Type: AWS::Config::ConfigRule DependsOn: - ConfigRecorder Properties: ConfigRuleName: RootAccountMFAEnabled Description: Checks whether the root user of your AWS account requires multi-factor authentication for console sign-in. Source: Owner: AWS SourceIdentifier: ROOT_ACCOUNT_MFA_ENABLED MaximumExecutionFrequency: TwentyFour_Hours IAMRootAccessKeyCheck: Type: AWS::Config::ConfigRule DependsOn: - ConfigRecorder Properties: ConfigRuleName: IAMRootAccessKeyCheck Description: Checks whether the root user access key is available. The rule is compliant if the user access key does not exist. Source: Owner: AWS SourceIdentifier: IAM_ROOT_ACCESS_KEY_CHECK MaximumExecutionFrequency: TwentyFour_Hours
ルールは適宜追加しましょう。
aggregator
個人利用であればAWSアカウントを複数管理することは少ないでしょうが、企業利用だとそうはいきません。その際に有効な手段がaggregatorです。それにより全管理アカウントのConfigルール評価結果を包括的に見ることができます。
作成はコンソールで行い、親子アカウントの連携は先程のCloudFormationテンプレート作成の際に説明したので省略します。
実際に優位な点は集約ビューにて全アカウントの結果を包括的に見ることができるだけではありません。高度なクエリ機能を使うとConfig結果に対してSQLを用いることができ、より詳細な結果の検索も可能になります。
以下は高度なクエリページでSQLを用いた例です。アカウントIDと、評価結果が非準拠(セキュリティに問題がある評価)である条件を指定してリソースが検索できました。
この機能はAPIとしても提供されているので外部システムに組み込むのも容易です。
応用
カスタムルールの利用
Configルールにはマネージドルール、カスタムルールの2つがあります。前者はデフォルトでConfigに用意されているルールで、後者はLambdaで自作するルールです。
カスタムルールを作成することで基本的にはどんなリソースでもチェックすることができます。
Configはマネージドルールで指定したリソースタイプ (AWS::EC2::Instance、 AWS::S3::Bucketとか) のリソースすべてを評価します。ただ例えばS3BucketPublicReadProhibited (パブリックからの読み取りを有効にしているS3バケットをチェックする) のようなルールだと、サービスによってはそのルールに準拠しないS3バケットを作成、使用する用途もあるため、特定のリソースをルール評価から除外したいというケースは容易に想定できます。
ただルールでは特定のリソースを除外することはできず、対象のリソースタイプのリソースをすべて評価します。そうなったときには自分でカスタムルールを作成するしかありません。
このようにマネージドルールだけではできない処理をカスタムルールで作成する方法を書いていこうと思います。
今回は上記の例をカスタムルールで作成します。
マネージドルールに機能を付け足すような実装をするため、AWSが提供している以下のようなソースコードを利用します。
・Configマネージドルールのソース
今回作成したいルールに対応するマネージドルールはS3BucketPublicReadProhibited なので、同じものを使いたいですがありません。そこまで頻繁にコミットのあるリポジトリではないので、動作が合致するソースが無いということもよくあります。なので、似ているソースを編集して意図したルールを作成しましょう。
今回はこちらを参考にします。
結構なソースのボリュームに見えますが、注目するアクションは2つです。
- evaluate_compliance
- lambda_handler
上記アクションに追記していきます。
evaluate_complianceは以下のように修正しました。
def evaluate_compliance(event, configuration_item, valid_rule_parameters): client = get_client('s3', event) bucketName = json.loads(event['invokingEvent'])['configurationItem']['resourceId'] evaluations = [] ## Block public access try: publicAccessBlock = client.get_public_access_block( Bucket=bucketName ) if (publicAccessBlock['PublicAccessBlockConfiguration']['BlockPublicAcls'] == True) \ and (publicAccessBlock['PublicAccessBlockConfiguration']['IgnorePublicAcls'] == True) \ and (publicAccessBlock['PublicAccessBlockConfiguration']['BlockPublicPolicy'] == True) \ and (publicAccessBlock['PublicAccessBlockConfiguration']['RestrictPublicBuckets'] == True): evaluations.append(build_evaluation(bucketName, 'COMPLIANT', event)) return evaluations except: print('The public access block configuration was not found') ## Block public access for Acl publicAccessBlockAcl = client.get_bucket_acl( Bucket=bucketName ) for grant in publicAccessBlockAcl['Grants']: if 'URI' in grant['Grantee']: if grant['Grantee']['URI'] == 'http://acs.amazonaws.com/groups/global/AllUsers' \ and grant['Permission'] == 'FULL_CONTROL'\ or grant['Grantee']['URI'] == 'http://acs.amazonaws.com/groups/global/AllUsers'\ and grant['Permission'] == 'READ': evaluations.append(build_evaluation(bucketName, 'NON_COMPLIANT', event)) return evaluations ## Block public access for bucket policy try: bucketPolicyList = client.get_bucket_policy( Bucket=bucketName ) for bucketPolicy in json.loads(bucketPolicyList['Policy'])['Statement']: if bucketPolicy['Principal'] == '*': if 'Condition' in bucketPolicy: if '0.0.0.0/0' in bucketPolicy['Condition']['IpAddress']['aws:SourceIp']: evaluations.append(build_evaluation(bucketName, 'NON_COMPLIANT', event)) return evaluations else: evaluations.append(build_evaluation(bucketName, 'NON_COMPLIANT', event)) return evaluations except: print('The bucket policy does not exist') evaluations.append(build_evaluation(bucketName, 'COMPLIANT', event)) return evaluations
こちらはConfigがリソースを評価するアクションですが、S3に対して下記の3点をチェックするような処理を追加しています。
1でパブリックアクセスをブロックしていればConfigルール評価では準拠になるようにしています。
## Block public access try: publicAccessBlock = client.get_public_access_block( Bucket=bucketName ) if (publicAccessBlock['PublicAccessBlockConfiguration']['BlockPublicAcls'] == True) \ and (publicAccessBlock['PublicAccessBlockConfiguration']['IgnorePublicAcls'] == True) \ and (publicAccessBlock['PublicAccessBlockConfiguration']['BlockPublicPolicy'] == True) \ and (publicAccessBlock['PublicAccessBlockConfiguration']['RestrictPublicBuckets'] == True): evaluations.append(build_evaluation(bucketName, 'COMPLIANT', event)) return evaluations except: print('The public access block configuration was not found')
1で公開になっていたとしても、パブリックアクセスはできないので、2、3のチェックをします。2、3のどちらかで許可しているとパブリックアクセスが可能になります。
以下が2のACLで許可される場合の例です
以下が3のバケットポリシーで許可される場合の例です。
評価処理はマネージドルールのS3BucketPublicReadProhibitedと同じものが再現できました。今回の肝は特定のリソースを除外する処理を追加することなので、そちらをlambda_handlerアクションに追加していきます。
既存のlambda_handlerの最後に以下のような処理を追加しました。
## Exclude AWS resource have an exclusion tag. AWS_S3_CLIENT = get_client('s3', event) try: evaluationResourceTags = AWS_S3_CLIENT.get_bucket_tagging( Bucket=evaluation_copy[0]['ComplianceResourceId'] ) for tag in evaluationResourceTags['TagSet']: if tag['Key'] == 'excludeConfigRule' and tag['Value'] == 'S3BucketPublicReadProhibited': return evaluations except: print("This bucket has no tags.") while evaluation_copy: AWS_CONFIG_CLIENT.put_evaluations(Evaluations=evaluation_copy[:100], ResultToken=result_token, TestMode=test_mode) del evaluation_copy[:100] # Used solely for RDK test to be able to test Lambda function return evaluations
S3バケットにタグを複数つけることができますが、そのうちexcludeConfigRuleというkeyを持ち、指定している特定のルール名がvalueでついていたら、後半のwhileループ内で行われているConfigに評価結果を送る処理へ通さないようにします。
大まかには以上の二点を追記してやることで今回の目的のカスタムルールの処理が完成します。あとはこちらの処理を実行するLambdaを作成して、そのLambdaを使うカスタムルールをCloudFormationで作成します。
## custom rule S3BucketPublicReadProhibited: Type: AWS::Config::ConfigRule DependsOn: - ConfigPermissionToCallLambda Properties: ConfigRuleName: S3BucketPublicReadProhibited Description: Checks that your Amazon S3 buckets do not allow public read access. The rule checks the Block Public Access settings, the bucket policy, and the bucket access control list (ACL). Scope: ComplianceResourceTypes: - "AWS::S3::Bucket" Source: Owner: "CUSTOM_LAMBDA" SourceDetails: - EventSource: "aws.config" MessageType: "ScheduledNotification" MaximumExecutionFrequency: TwentyFour_Hours - EventSource: "aws.config" MessageType: "ConfigurationItemChangeNotification" ## lambda functionのArn SourceIdentifier: !GetAtt ConfigRuleFunctionForS3BucketPublicReadProhibited.Arn
以上で目的のカスタムルールの完成です。実際にConfig上で評価ができているかの確認をしましょう。
以下のようなテストバケットを作成しておきます。
作成したルールで評価すると、作成したバケットには特にタグは付けていないので、評価されています。
そこで以下のようにバケットにタグを付けてみます。
一度、Config結果をコンソール上のアクションから結果の削除を選択して削除しましょう。その後アクションの再評価を押すと
先程評価されていたexclusion-samplae-bucketが消えています。完璧に処理が動いてることが分かりました。
GuardDuty
実行
続いてGuardDutyについて説明していこうと思います。 GuadDutyの始め方はとても簡単!
GuadDutyを有効にするをクリックするだけです。
以下のように、EC2、IAM、S3に対して自動セキュリティチェックが行われ、アラートが出てきます。
各アラートの解決の仕方は公式のドキュメントがあるので、そちらを参照してください。
・セキュリティ脅威一覧
docs.aws.amazon.com
GuardDutyも親子アカウントで結果を包括してみることができるので、親アカウントで集計の設定をすることをおすすめします。
監視
GuardDutyの結果をいちいちコンソールに入って見に行くのは面倒です。なので、こちらにはSNS経由でChatBotを通してslack通知しましょう。
ChatBotに関しては本ブログでも紹介していますので、こちらを御覧ください。 xkenshirou.hatenablog.com
以下のように、GuardDutyでセキュリティ脅威が取得されたら検知するCloudwatch Eventを作成しましょう。
Parameters: SnsTopicArn: Type: String Description: arn:aws:sns:ap-northeast-1:xxx:chatbot-topic Resources: GuarddutyEventRule: Type: AWS::Events::Rule Properties: Name: GuarddutyNotificationEvent Description: GuarddutyNotificationEvent EventPattern: source: - aws.guardduty detail-type: - GuardDuty Finding State: ENABLED Targets: - Arn: !Ref SnsTopicArn Id: GuarddutynotificationTopic
上記のEventを受け取ってSlack通知するSNSとChatbotを作成します。
Parameters: TargetWorkspaceId: Type: String TargetChannelId: Type: String Resources: ChatbotIamRole: Type: AWS::IAM::Role Properties: RoleName: chatbot-iam-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: chatbot.amazonaws.com Action: sts:AssumeRole - Effect: Allow Principal: Service: management.chatbot.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: chatbot-iam-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cloudwatch:Describe* - cloudwatch:Get* - cloudwatch:List* - sns:Get* - sns:List* - sns:Unsubscribe - sns:Subscribe - logs:Get* - logs:List* - logs:Describe* - logs:TestMetricFilter - logs:FilterLogEvents Resource: - "*" ChatbotTopic: Type: AWS::SNS::Topic Properties: TopicName: chatbot-topic ChatbotSlack: Type: AWS::Chatbot::SlackChannelConfiguration Properties: ConfigurationName: slack-channel-configuration IamRoleArn: !GetAtt ChatbotIamRole.Arn LoggingLevel: INFO SlackChannelId: !Ref TargetChannelId SlackWorkspaceId: !Ref TargetWorkspaceId SnsTopicArns: - !Ref ChatbotTopic
これで以下のようにSlackで通知が来るようになりました。
デフォルトでセキュリティリスクの危険度としては以下です。
青 -> 低 橙 -> 中 赤 -> 高
これでセキュリティ監視ができるようになりました。
まとめ
ConfigもGuardDutyも導入コストが低く、効果が大きいのでおすすめなサービスです。
セキュリティ管理や監視は範囲が広ければ広いほど人が実行するのが難しく、今回紹介したConfigとGuarddutyを導入することでそういった範囲を自動でカバーできるので、エンジニアの負担も減らしつつセキュリティリスクを少なくすることができるので、とても有益でした。