ヘヴィメタル・エンジニアリング

AWS特化型エンジニアのほのぼのヘヴィメタルブログ

ヘヴィメタル・エンジニアリング

クラウド特化型ヘヴィメタルエンジニアのほのぼのブログ

AWSのセキュリティにはConfigとGuardDutyが外せない!

はじめに

よくAWSにふれるものです。

 以前、弊社のブログでそちらについて詳しく書きましたが、そちらで書ききれなかったことを加えて改めて執筆します。

 AWSはマネージドサービスも多く比較的容易に多くのシステムを構築することができますが、作成したリソースが多くなったり、開発者が多くなるほど、セキュリティチェックが曖昧になりがちなので、予期せぬセキュリティホールを作らないように運用する必要があります。

 そんな時にはAWS Config(以下Config)とGuardDutyが役に立ちます。

 今回はその特徴を捉え、実践的、応用的な使い方について紹介していきたいと思います。

ConfigとGuardDutyについて

仕様

 両者ともにS3, IAM, EC2を始めとしたリソースに対するセキュリティチェックを行いますが、詳しくは以下のような違いがあります。

Config
- 多くのAWSサービスのセキュリティチェックに対応している
- 事前にチェックする項目を選択できる
- セキュリティチェックをカスタマイズできる
- セキュリティチェックにかかったリソースを自動修復することができる

GuardDuty
- EC2, IAM, S3へのチェック
- CloudTrailログ、VPCフローログ、DNSログからチェック
- セキュリティチェックは事前に用意されているものが自動で働く
- セキュリティチェックのカスタマイズはできない

 基本的に Config = AWSリソースの設定ミス を検知してくれるもの、GuardDuty = 潜在的なセキュリティ脅威(バックドア、ポートスキャン、マルウェア) を検知するものという認識で良いと思います。
 ちなみに一部のセキュリティチェックは両者で被っているものもあるので、除外するといいでしょう。
 両者の詳細な機能に関してはドキュメントに書いてあるので、省きます。

料金について

 詳しくはドキュメントを見ることをおすすめします。

Configの料金

aws.amazon.com

GuardDutyの料金

aws.amazon.com

 ここでは両者の金額を抑えるポイントを書きたいと思います。
   まず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することで料金を最小限に抑えることができます。

・マネージドルール一覧

docs.aws.amazon.com

 一方、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としても提供されているので外部システムに組み込むのも容易です。

f:id:xkenshirou:20201212010953p:plain

応用

カスタムルールの利用

 Configルールにはマネージドルール、カスタムルールの2つがあります。前者はデフォルトでConfigに用意されているルールで、後者はLambdaで自作するルールです。
 カスタムルールを作成することで基本的にはどんなリソースでもチェックすることができます。
 Configはマネージドルールで指定したリソースタイプ (AWS::EC2::Instance、 AWS::S3::Bucketとか) のリソースすべてを評価します。ただ例えばS3BucketPublicReadProhibited (パブリックからの読み取りを有効にしているS3バケットをチェックする) のようなルールだと、サービスによってはそのルールに準拠しないS3バケットを作成、使用する用途もあるため、特定のリソースをルール評価から除外したいというケースは容易に想定できます。
 ただルールでは特定のリソースを除外することはできず、対象のリソースタイプのリソースをすべて評価します。そうなったときには自分でカスタムルールを作成するしかありません。

 このようにマネージドルールだけではできない処理をカスタムルールで作成する方法を書いていこうと思います。

 今回は上記の例をカスタムルールで作成します。
 マネージドルールに機能を付け足すような実装をするため、AWSが提供している以下のようなソースコードを利用します。

・Configマネージドルールのソース

github.com

 今回作成したいルールに対応するマネージドルールはS3BucketPublicReadProhibited なので、同じものを使いたいですがありません。そこまで頻繁にコミットのあるリポジトリではないので、動作が合致するソースが無いということもよくあります。なので、似ているソースを編集して意図したルールを作成しましょう。

 今回はこちらを参考にします。

github.com

 結構なソースのボリュームに見えますが、注目するアクションは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. Block Public Access
  2. ACL
  3. Bucket Policy

 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で許可される場合の例です

f:id:xkenshirou:20201212011117p:plain

 以下が3のバケットポリシーで許可される場合の例です。

f:id:xkenshirou:20201212011144p:plain

 評価処理はマネージドルールの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上で評価ができているかの確認をしましょう。

 以下のようなテストバケットを作成しておきます。

f:id:xkenshirou:20201212011233p:plain

 作成したルールで評価すると、作成したバケットには特にタグは付けていないので、評価されています。

f:id:xkenshirou:20201212011252p:plain

 そこで以下のようにバケットにタグを付けてみます。

f:id:xkenshirou:20201212011317p:plain

 一度、Config結果をコンソール上のアクションから結果の削除を選択して削除しましょう。その後アクションの再評価を押すと

f:id:xkenshirou:20201212011340p:plain

 先程評価されていたexclusion-samplae-bucketが消えています。完璧に処理が動いてることが分かりました。

GuardDuty

実行

 続いてGuardDutyについて説明していこうと思います。  GuadDutyの始め方はとても簡単!

f:id:xkenshirou:20201212011913p:plain

f:id:xkenshirou:20201212012029p:plain

 GuadDutyを有効にするをクリックするだけです。

 以下のように、EC2、IAM、S3に対して自動セキュリティチェックが行われ、アラートが出てきます。

f:id:xkenshirou:20201212012547p:plain

 各アラートの解決の仕方は公式のドキュメントがあるので、そちらを参照してください。

・セキュリティ脅威一覧
docs.aws.amazon.com

 GuardDutyも親子アカウントで結果を包括してみることができるので、親アカウントで集計の設定をすることをおすすめします。

f:id:xkenshirou:20201212013301p:plain

監視

 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で通知が来るようになりました。

f:id:xkenshirou:20201212014615p:plain

デフォルトでセキュリティリスクの危険度としては以下です。

青 -> 低
橙 -> 中
赤 -> 高

これでセキュリティ監視ができるようになりました。

まとめ

 ConfigもGuardDutyも導入コストが低く、効果が大きいのでおすすめなサービスです。

 セキュリティ管理や監視は範囲が広ければ広いほど人が実行するのが難しく、今回紹介したConfigとGuarddutyを導入することでそういった範囲を自動でカバーできるので、エンジニアの負担も減らしつつセキュリティリスクを少なくすることができるので、とても有益でした。