この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
先日 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 ユーザーはこちらを参照ください。