アクセス制御を厳格に行っている環境からのs3利用

こんにちは、IT基盤部第一グループの生井です。
DeNAが提供するヘルスケア系サービスのインフラを担当しています。

ヘルスケア領域ではセンシティブ情報を扱いますので、日々高レベルなセキュリティ設計・運用を行う必要があります。 今回はその一例として、アクセス制御を厳格に行っている環境からS3を利用する際に行った対応を紹介したいと思います。

はじめに

あるプロジェクトで、センシティブ情報を扱う環境から、S3の特定バケットにのみ、awscliでのデータdownload/uploadを許可したいという要件がありました。
  補足:特定バケットに限定するのは出口対策のためです。任意のS3バケットへのアクセスを許可してしまうと、内部犯行によるデータの持ち出しや、マルウェア感染によるデータ漏洩のリスクが高まります。

対応として、この環境で実績のある、FWでのFQDNベースでのアクセス制御を行うことにしました。 S3へのAPIアクセスは、以下2つの形式がありますが、

  • パス形式:s3-<region>.amazonaws.com/<bucket>
  • 仮想ホスト形式:<bucket>.s3-<region>.amazonaws.com

バケットの命名規則に従ってバケットを作成していれば、 awscliはデフォルトで仮想ホスト形式<bucket>.s3-<region>.amazonaws.comのアドレスでS3にアクセスします。
したがって、仮想ホスト形式のFQDNに一致する場合のみ、FWで許可するようにしました。

ところが、S3の場合には、FQDNは一致しているのにもかかわらず、通信が拒否されてしまうケースが発生し期待通りにアクセス制御ができないことがわかりました。
FWのFQDNベースでのアクセス制御は、FQDNに対するDNS問い合わせ結果をFWがキャッシュとして保持し、クライアントが名前解決を行い接続するIPと、FWのDNSキャッシュ情報にあるIPが一致する場合にアクセスを許可する、という動作でしたが、 S3のようにTTLが短い接続先の場合は、IPが一致しないケースが発生してしまうことが原因でした。
また、ベンダーに問い合わせた結果、この環境におけるFWの仕様として、FWのDNSキャッシュ期間をコントロールできないことがわかりました。

そこで、FWでの制御をあきらめ、代替策としてproxyサーバを用意して対応することにしました。
この環境の構成について次項に書きます。

構成

1g.aws.proxy.blog.png

上図は、システム構成の概要を示した図です。AWS環境にproxyサーバを建てて、Site-to-Site VPNで接続しています。通信品質の優れるDirect Connectを選択する案もありましたが、この環境の使用方法では妥協ができること、及び、コストが抑えられること、後からDirect Connectに変更することが容易であること、からSite-to-Site VPNを選択しています。
proxyインスタンスは異なるアベイラビリティゾーンに1台ずつ作成しLBを通して冗長化しています。インスタンスOSはCentOS7です。
セキュリティグループやproxy設定、IAMやバケットのポリシーでアクセス制御を行っています。許可する通信を最小限にするため、利用するAWSサービスの各種VPC endpointを作成しています。ログ保管や監視では、CloudWatchを利用しています。
以下で、もう少し具体的に構成について紹介していきます。

proxy

proxyは、ApacheやSquidがメジャーですが今回はホワイトリスト管理のしやすいSquidを採用しました。
squid.confでホワイトリストファイルを次のように指定します。

 acl whitelist-domain dstdomain "/path-to/whitelist-domain"

ホワイトリストファイル(上記例だとwhitelist-domain)には、アクセス許可したいバケットのFQDNを列挙します。

  <bucket1>.s3-<region>.amazonaws.com
  <bucket2>.s3-<region>.amazonaws.com
  ・・・
S3へのアクセス制御

VPC内からのS3バケットアクセスのみを許可するため、S3用のVPC endpoint を作成して、 IAMユーザに割り当てるポリシーのCondition要素で、接続元のVPC endpoint idが一致する場合のみアクセス許可するように設定しています。VPC endpointとS3は同一リージョンである必要があります。

       "Condition": {
         "StringEquals": {
           "aws:SourceVpce": "<vpce-id>"
         }
       }
ログの保管
infra.awslog.blog.png

ログ保管のため、Squidログやsecureログなどのセキュリティ系ログをインスタンスから CloudWatchへ送っています。ログを送るためインスタンスでawslogsを稼働させています。
CloudWatchに恒久的にログを保管すると、費用が高くつきますので、古いログはさらにS3に退避するようにしています。このためログ保管専用のバケットを用意しています。
S3へのログ退避は、ログをS3にexportする処理を行うLambda関数を作成して、 CloudWatch Eventsをトリガーとして、dailyで定期実行するようにしています。
またS3のアクセスログ記録も有効にして、log保管用の専用バケットにログを保管するようにしています。

改竄防止

セキュリティ系ログを改竄されないように、バケットポリシーで権限を最小限に絞り、ログの削除・変更をできないようにしています。 以下はバケットポリシーに追加する設定の例です。ログを保管するバケットではバージョニングを有効にしています。

 {
     "Version": "2012-10-17",
     "Statement": [
         {
             "Sid": "<description>",
             "Effect": "Deny",
             "Principal": "*",
             "NotAction": [
                 "s3:List*",
                 "s3:Get*",
                 "s3:AbortMultipartUpload",
                 "s3:PutObject",
                 "s3:PutObjectTagging",
                 "s3:PutBucketTagging",
                 "s3:PutMetricsConfiguration",
                 "s3:DeleteObjectTagging",
                 "s3:DeleteObjectVersionTagging",
                 "s3:ListBucket"
             ],
             "Resource": [
                 "arn:aws:s3:::<bucket>",
                 "arn:aws:s3:::<bucket>/*"
             ]
         }
     ]
 }

rootアカウントでは、バケットポリシーの変更が可能ですが、弊社にはAWSアカウントの自動監査 の仕組みがあり、この仕組みによりrootアカウントログインがあった場合は通知により気づくことができます。

インスタンスへのログイン

許可する通信を最小限にしたかったので、sshのinbound許可の必要がないSSM セッションマネージャを利用しました。
注意点としまして、WebSocket に対応していないproxyを経由してAWS マネジメントコンソールにアクセスしている場合は、SSM セッションマネージャでのコンソール操作ができません(私の使用しているクライアントPCでは動作確認用にinstallしていたツールがWebSocket に対応しておらず、この問題に躓きサポートに頼りました)。
SSM セッションマネージャの利用のためscreenがinstallされていない場合はinstallしておく必要があります。また、amazon-ssm-agentをインスタンスで稼働させる必要があります。このために必要な作業は一時的にsshアクセスを許可して行いました。
SSM セッションマネージャの設定は、AWS マネジメントコンソールでは[セッションマネージャ]の[設定タブ」から行います。
証跡ログを残すため、セッションログをS3に暗号化して保管するようにしています。このログを保管するS3バケット側でも暗号化が有効になっている必要があります。
また、アクティブなセッションデータをKMSで暗号化するようにしています。
amazon-ssm-agentに必要な権限はインスタンスのIAMロールで与えています。デフォルトで用意されているポリシーAmazonEC2RoleForSSMは権限が強い(リソース制限なしのs3:GetObject, s3:PutObjectがある)ので、 必要な権限だけ与えるようにカスタマイズしたポリシーを作成して権限を付けました。 最低限必要な権限は、公式ドキュメントの最小限の Session Manager アクセス権限 と、SSM エージェント の最小 S3 バケットアクセス許可を参照しています。
ログを保管するバケットに対しては、s3:PutObjectの権限が必要で、上記のようにセッションログを暗号化して保管する場合には、ログを保管するバケットに対して、s3:GetEncryptionConfigurationの権限も必要となります。

通信制御

セキュリティグループで許可する通信を最小限にするため、以下のVPC endpointを作成しています(既出のS3 endpointも以下に含めています)。
S3のみgateway type、他はinterface typeのendpointです。

  • com.amazonaws.<region>.s3
  • com.amazonaws.<region>.ssm
  • com.amazonaws.<region>.ec2messages
  • com.amazonaws.<region>.ec2
  • com.amazonaws.<region>.ssmmessages
  • com.amazonaws.<region>.kms
  • com.amazonaws.<region>.monitoring

ssm,ec2messages,ec2,ssmmessagesのendpointはSSM セッションマネージャの利用に必要な通信のため作成しています(詳しくは公式ドキュメントを参照下さい)。 SSM セッションマネージャでセッションデータの暗号化をする場合には、KMS endpoint作成も必要です。monitoring endpointはCloudWatch用です。
仕上げとして必要な通信だけ許可するようにセキュリティグループで設定します。 VPC内のipと、vpn経由の通信以外は、inboundに必要な通信はありませんのでdenyします。
outboundについては、VPC内のipと、vpn経由からの通信以外に、S3 endpointのプレフィックスリストIDに対して、80port,443portを許可します。
80portはSSM セッションマネージャでセッションログをS3に保管する場合に許可が必要です、公式ドキュメントの「Systems Manager の前提条件」に記述がありませんが、ソース の以下箇所で使用しています。S3バケットに対してHEADメソッドを使ってリージョン情報を取得する処理です。

 func getRegionFromS3URLWithExponentialBackoff(url string, httpProvider HttpProvider) (region string, err error) {
    // Sleep with exponential backoff strategy if response had unexpected error, 502, 503 or 504 http code
    // For any other failed cases, we try it without exponential back off.
    for retryCount := 1; retryCount <= 5; retryCount++ {
        resp, err := httpProvider.Head(url)
監視

基本的なリソース監視としてcpu,メモリ,ディスクの使用率をCloudWatchで監視するようにしています。一定の閾値を超えた場合CloudWatchのアラーム設定により通知が飛びます。 メモリ,ディスクの使用率のモニタリングにはインスタンスにモニタリングスクリプトを設置して定期実行する必要があります。
死活監視としてインスタンスのステータスチェックのアラームも作成しています。
また、想定しないアクセスがあった場合に通知するように、CloudWatchに送っているSquidログに対して、メトリクスフィルターを作成し、ログにSquidホワイトリストにない宛先がある場合には、アラーム設定で通知が飛ぶようにしています。

切り替え

新しく環境を用意する場合は気にする必要はありませんが、元々internet経由から接続元IPを制限してS3を利用している場合には、 接続元が特定IPの場合と、特定VPCの場合を、or条件でアクセス許可して、VPCからのアクセスができることを確認してから、特定VPCの場合のみ許可するように絞るのが進め方として安心です。
ポリシー設定で、Conditionの列挙はandで評価されるため、両方の場合を許可するのには、以下のように、特定VPCでない場合、かつ特定接続元IPでない場合をdenyするポリシーを設定します。

   "Statement": [{
     "Effect": "Deny",
     "Action": "s3:<action>",
     "Resource": [
       "arn:aws:s3:::<bucket>",
       "arn:aws:s3:::<bucket>/*"
     ],
     "Condition": {
       "StringNotLike": {
         "aws:sourceVpce": [
           "<vpce-id>"
         ]
       },
       "NotIpAddress": {
         "aws:SourceIp": [
           "<ip-range>"
         ]
       }
     }
   }]

この状態で動作確認行い、S3バケットのアクセスログから、接続元IPがプライベートIPとなっていることを確認できればokです。 最後にVPC経由のアクセスのみ許可するようにポリシーを設定してinternet経由ではアクセスできなくなったことを確認します。

おわりに

以上、proxy環境が必要になった経緯と用意したproxy環境についての紹介をさせて頂きました。
TTLが短い場合にFWでのFQDNベースでの制御が効かないケースは他にも見かけます。類似の問題に取り組んでいる方がいるかと思います、そのような方のお役に立てば幸いです。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る