Snyk IaC で CloudFormation テンプレートをセキュリティ解析してみた
先日 Snyk のウェビナーに参加していました。そこで CloudFormation のコード(yaml 形式のファイル)に対して CLI で手軽にセキュリティチェックしていたのが印象的でした。そのときは RDS を作成するテンプレートから、IAM のデータベース認証が無効化されていますと指摘されていました。
近しいものでは以下の記事で取り上げられています。
ちょうど CloudFormation でテンプレートを書いているところだったので手元のコードをローカル環境でスキャンをかけてみました。Snyk CLI のインストールから Snyk IaC のセキュリテイチェックの結果を紹介します。
実行結果
S3 バケットのアクセスログ設定、WAF の設定はどう?という結果が返ってきました。詳細は本文で。
Snyk CLI の準備
CI/CD に組み込まなくても Snyk CLI があればローカル環境で手軽に Snyk IaC の恩恵を授かることができることを知ったので公式ドキュメントを参考に実行環境を作ります。5分で終わります。
実行環境
項目 | 値 |
---|---|
macOS | 12.4 |
CPU | Apple M1 |
インストール
Apple M1 machine について注釈がありましたがbrew install
で問題ありませんでした。
Install or update the Snyk CLI - Snyk User Docs
$ brew tap snyk/tap $ brew install snyk $ snyk --version 1.976.0 (standalone)
認証
APIキーや、クレデンシャルをローカルに保存しておくこともなく、コマンド入力から Web ブラウザが開きAuthenticatをクリックするだけでした。
$ snyk auth
Snyk CLI の準備が整いました。ドキュメントに目を通していた時間の方が長いくらいです。
Snyk IaC で Cloudformation と向き合う
CloudFront + S3 の静的サイトを Cloudformation で デプロイして、コンテンツは GitHub Actions から S3 バケットにデプロイする構成です。
AWS のリソース構築用に Cloudformation のテンプレートを書いていました。Cloudformation のテンプレートは GitHub Actions で CI/CD する予定まではなかったですが、ローカルでセキュリティチェックしてくれるならありがたいのでは?ということで Snyk IaC をカジュアルに使い始めてみます。
Cloudformation テンプレートは長いので折りたたみます。
AWSTemplateFormatVersion: "2010-09-09" Description: Cloudfront and S3 with CloudFront Functions Parameters: ProjectName: Description: Project Name Type: String Default: unnamed Environment: Description: Environment Type: String Default: dev AllowedValues: - prod - dev S3BucketName: Description: Bucket name for static site Type: String AliasName: Description: Alias for CloudFront Type: String HostedZoneId: Description: Route53 Host Zone ID Type: String CertificateId: Description: ACM Certificate ID must be us-east-1 region Type: String Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Common Settings Parameters: - ProjectName - Environment - Label: default: SSL Settings Parameters: - AliasName - HostedZoneId - CertificateId Resources: # ------------------------------------------------------------------------------------ # # S3 # ------------------------------------------------------------------------------------ # # Static site bucket S3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId} OwnershipControls: Rules: - ObjectOwnership: "BucketOwnerEnforced" PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: "AES256" BucketKeyEnabled: false LifecycleConfiguration: Rules: - Id: AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload: DaysAfterInitiation: 7 Status: "Enabled" - Id: NoncurrentVersionExpiration NoncurrentVersionExpiration: NewerNoncurrentVersions: 3 NoncurrentDays: 1 Status: Enabled # Bucket Policy for CloudFront BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref S3Bucket PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Resource: !Sub arn:aws:s3:::${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}/* Principal: AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity} # ------------------------------------------------------------------------------------ # # CloudFront # ------------------------------------------------------------------------------------ # CloudFrontDistribution: Type: "AWS::CloudFront::Distribution" Properties: DistributionConfig: PriceClass: PriceClass_All Origins: - DomainName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}.s3.${AWS::Region}.amazonaws.com Id: !Sub "S3origin-${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}" S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" DefaultRootObject: index.html DefaultCacheBehavior: TargetOriginId: !Sub "S3origin-${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}" Compress: true ViewerProtocolPolicy: redirect-to-https # CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled AllowedMethods: - GET - HEAD CachedMethods: - GET - HEAD ForwardedValues: Cookies: Forward: none QueryString: false # CloudFront Function Association FunctionAssociations: - EventType: viewer-request FunctionARN: !GetAtt CloudFrontFunction.FunctionMetadata.FunctionARN Logging: Bucket: !GetAtt S3BucketLogs.DomainName IncludeCookies: false Aliases: - !Ref AliasName ViewerCertificate: SslSupportMethod: sni-only MinimumProtocolVersion: TLSv1.2_2021 AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}" HttpVersion: http2 IPV6Enabled: true Enabled: true # OAI CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: Unique Domain Hosting Environment # CloudFront log bucket S3BucketLogs: Type: AWS::S3::Bucket DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: BucketName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-cloudfrontlogs-${AWS::AccountId} OwnershipControls: Rules: - ObjectOwnership: "ObjectWriter" PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: "AES256" BucketKeyEnabled: false LifecycleConfiguration: Rules: - Id: AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload: DaysAfterInitiation: 7 Status: "Enabled" - Id: CurrentVersionExpiration ExpirationInDays: 180 Status: "Enabled" - Id: NoncurrentVersionExpiration NoncurrentVersionExpiration: NoncurrentDays: 30 Status: "Enabled" # Alias Record Route53RecordSet: Type: AWS::Route53::RecordSet Properties: Name: !Sub ${AliasName} HostedZoneId: !Sub ${HostedZoneId} Type: A AliasTarget: DNSName: !GetAtt CloudFrontDistribution.DomainName HostedZoneId: Z2FDTNDATAQYW2 # fixed # ------------------------------------------------------------------------------------ # # CloudFront Functions # ------------------------------------------------------------------------------------ # CloudFrontFunction: Type: "AWS::CloudFront::Function" Properties: Name: "add-index-function" FunctionCode: | function handler(event) { var request = event.request; var uri = request.uri; // Check whether the URI is missing a file name. if (uri.endsWith('/')) { request.uri += 'index.html'; } // Check whether the URI is missing a file extension. else if (!uri.includes('.')) { request.uri += '/index.html'; } return request; } FunctionConfig: Comment: "Add index.html to the path" Runtime: "cloudfront-js-1.0" AutoPublish: true
セキュリティチェックしてみる
snyk iac test
とコマンドを打つだけでセキュリティチェックの結果が返ってきました。
$ snyk iac test Snyk Infrastructure as Code ✔ Test completed. Issues Low Severity Issues: 3 [Low] S3 server access logging is disabled Info: The s3 access logs will not be collected. There will be no audit trail of access to s3 objects Rule: https://snyk.io/security-rules/SNYK-CC-TF-45 Path: [DocId: 0] > Resources > S3BucketLogs > Properties > LoggingConfiguration File: cloudfront-s3.yaml Resolve: Set `Properties.LoggingConfiguration` attribute [Low] S3 server access logging is disabled Info: The s3 access logs will not be collected. There will be no audit trail of access to s3 objects Rule: https://snyk.io/security-rules/SNYK-CC-TF-45 Path: [DocId: 0] > Resources > S3Bucket > Properties > LoggingConfiguration File: cloudfront-s3.yaml Resolve: Set `Properties.LoggingConfiguration` attribute [Low] Cloudfront distribution without WAF Info: The AWS WAF is not in front of cloudfront distribution. The WAF service will not protect the application from common web based attacks such as SQL injections Rule: https://snyk.io/security-rules/SNYK-CC-TF-75 Path: [DocId: 0] > Resources[CloudFrontDistribution] > Properties > DistributionConfig > WebACLId File: cloudfront-s3.yaml Resolve: Set `Properties.DistributionConfig.WebACLId` attribute to existing AWS WAF acl ARN ------------------------------------------------------- Test Summary Organization: ohmura.yasutaka Project name: cloudformation ✔ Files without issues: 2 ✗ Files with issues: 1 Ignored issues: 0 Total issues: 3 [ 0 critical, 0 high, 0 medium, 3 low ] ------------------------------------------------------- Tip New: Share your test results in the Snyk Web UI with the option --report
構成図で説明するとこうなります。今回は意図的に設定していない箇所でした。
- 静的サイトの S3 バケットのアクセスログも保存した方が好ましいのは事実です
- CloudFront のアクセスログ保存用の S3 バケットにアクセスログ設定は必要ない
- WAF は利用しない
「S3 バケットのアクセスログ機能の存在」や、「CloudFront に WAF を設定できること」を知らないといったように誰しもセキュアな設定・機能を把握できているとは限らないため、テンプレートを書いている時点で設計を見直せるのはよいことです。デプロイしてから設計・設定を変更出戻りの工数の方が大きですからね。
Web UI のレポート
実行結果を共有するなら Web UI が便利そうなので試してみます。(実行結果に書いてあったので知りました。)
Tip New: Share your test results in the Snyk Web UI with the option --report
実行結果は同じ情報が返ってきたので省略します。違いは文末に URL のリンクが案内されました。
$ snyk iac test --report ... snip ... ------------------------------------------------------ Report Complete Your test results are available at: https://snyk.io/org/ohmura.yasutaka/projects under the name: cloudformation
テキストで表示されていたメッセージが Web UI で確認できました。
Cloudformation のなんのプロパティを追加すればよいのか教えてくれるのは丁寧ですね。今回は意図的な設定しないため無視してみます。
永続的に表示されなくてよい(Ignore permanently
)設定で指摘を無視します。
何かしら理由を書いてすべて無視しました。無視した理由と入力者の名前が記録されるのは後で追えるからよいですね。
snyk iac test
を再実行すると無視した項目が再検出されました。
$ snyk iac test ... snip ... ------------------------------------------------------- Test Summary Organization: ohmura.yasutaka Project name: bigmuramura/hugo-cloudfront-s3 ✔ Files without issues: 2 ✗ Files with issues: 1 Ignored issues: 0 Total issues: 3 [ 0 critical, 0 high, 0 medium, 3 low ] ------------------------------------------------------- Tip New: Share your test results in the Snyk Web UI with the option --report
どうやら.snyk
ポリシーファイルに記述しないといけないようです。
IaC update-exclude-policy - Snyk User Docs
便利に扱うためには多少の学習コストが必要でした。今回はカジュアルにチェックしたかっただけなので今後学習が必要な箇所を把握できたので十分です。
おわりに
Free plan でもカジュアルにチェックできたので CloudFormation のテンプレート書いているとき思い出したら試してみてください。自分の知らないセキュアな設定を教えてくれるかもしれませんし、凡ミスでガバガバセキュリティだったことに気付けるかもしれませんよ。
Terraform ユーザーはこちらを参照ください。