続:「Bastion ~ AWS Fargateで実現するサーバーレスな踏み台設計」

はじめに

先日、Infra Study Meetup#6にお邪魔させていただき、「Bastion ~ AWS Fargateで実現するサーバーレスな踏み台設計」というタイトルでLTしてきました。

speakerdeck.com

運営の皆様、改めて素晴らしいイベントの企画ありがとうございました。 登壇後、Twitterタイムラインやはてなブックマーク上で思ったより大きな反響を頂いたので、鮮度が高いうちに続編として少し踏み込んだ内容をご紹介できればと思い、ブログに書き起こしてみました。 もし、これをきっかけにサーバーレスBastionホストにチャレンジしてみようという方の一助になれれば嬉しいです。

登壇内容の振り返り

BastionホストをFargateでサーバーレス化する背景は登壇スライドに譲るとして、達成したい構成は以下でした。

f:id:iselegant:20200928005034p:plain

ただ、具体的に実現しようとすると、いくつか考慮すべき点があります。 LTでは時間があまりにも足りなかったのでさっくりした流れのご紹介のみでしたが、今回は少し具体的な設計ポイントをご紹介できればと思います。

構築・運用を行う上でのポイント

実際に今回ご紹介した構成でBastionホストを実現しようとすると、割と考えるべきことがあります。 例えば、以下のような例です。

順を追って検討していきましょう。

アクティベーションコードの運用

登壇スライドでご紹介したのは、Systems ManagerのアクティベーションコードとIDを事前に手動作業で発行し、パラメータストアに保存することでECSタスク定義から参照できるという流れでした。 これにより、ECSタスクからSSMエージェントをアクティベーション化できるので、Systems Managerで単にBastionホストを稼働させるのであれば、この流れで問題ありません。

しかし、マネジメントコンソール等で実際に行うと気づくかと思うのですが、アクティベーションコードには有効期限の設定が必須です。 この有効期限はデフォルトで24時間、最大で30日となっています。

参考:Step 4: Create a managed-instance activation for a hybrid environment - AWS Systems Manager

ここで問題となるのが、必要な時だけBastionホストを起動運用する場合です。 Bastionホスト起動時にアクティベーションコードが有効期限切れになっていると、再度コード発行からパラメータストアへ保存しなければなりません。 運用していく上でこの作業は地味に手間ですし、可能であれば避けたいと考える方も多いかもしれません。 Bastionホスト(ECSタスク)を常に起動していれば問題ないのですが、Fargate障害やAWSメンテナンス等によるタスク停止、なにかの拍子にBastionホストが停止してしまうと結局は同様の対応が必要です。 加えて、Well-Architectedフレームワークのコスト最適化の観点から、ECSタスク起動しっぱなしは適切な対応とは言えません。

参考:https://d1.awsstatic.com/whitepapers/ja_JP/architecture/AWS-Cost-Optimization-Pillar.pdf

そのため、アクティベーションコードをいずれかの方法で定期的に更新する方が望ましいのでは考えています。 例えば、以下のような方法が挙げられます。

  1. ECSタスク内で起動時に都度アクティベーションコードを発行&削除
  2. Lambda + CloudWatch Eventによるアクティベーションコードの定期更新

前者は、作成したアクティベーションコードがタスク内に直接返却されるため、Parameter Storeからの取り出しが不要になりシンプルな構成になります。後者はLambdaの追加開発が必要になりますが、タスクに対してアクティベーションコード発行に必要なIAMポリシー権限を付与しなくてよいため、Bastionホストがよりセキュアな設計となるでしょう。

f:id:iselegant:20200928005406p:plain

それぞれ一長一短ですが、取得したアクティベーションコードはBastionホスト起動時の一時的な使い捨てと考えると、「ECSタスク起動時にタスク内で都度アクティベーションコードを発行&削除」でも十分運用に耐えられるのでは、と考えています。 後続のトピックはこちらを前提にご紹介します(LT登壇時はParameter Storeありきで話していましたが、今回はこちらの方式でお話します。齟齬がありごめんなさい・・・)。

各種IAMロールの設定

先程ご紹介のとおり、今回はECSタスク内でアクティベーションコードを発行します。 つまり、ECSタスクからAWS CLI経由でSystems Managerを操作するための権限を付与する必要があります。

また、アクティベーションコードを発行するためにはaws ssm create-activationを実行するのですが、このコマンドの引数としてSystems Manager用のサービスロールを指定する必要があり、事前に作成が求められます。

参考:create-activation — AWS CLI 1.18.147 Command Reference

そこで今回はIAMロール名をSSMServiceRoleなどとして信頼関係とユーザー管理ポリシーを以下のように作成します。

  • 信頼関係
{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Principal": {"Service": "ssm.amazonaws.com"},
        "Action": "sts:AssumeRole"
    }
}
  • アタッチするIAMポリシー(AWS管理ポリシーを利用)
aws iam attach-role-policy \
    --role-name SSMServiceRole \
    --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore  

Step 2: Create an IAM service role for a hybrid environment - AWS Systems Manager

一方、これでIAMに関する作業は終わりではありません。 繰り返しになりますが、今回はECSタスク内でSSMエージェントを起動する前にSSMアクティベーションコードを発行しますが、その引数としてIAMロールを指定する必要があります。 言い換えると、ECSタスクからSystems MsnagerにIAMロールを渡す権限を考慮しなければなりません。 これを実現するために、ECSタスクロールとしてiam:PassRoleが付与します。 IAMロールを作成して、以下のようなポリシーを作成しておいてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*",
            "Condition": {
                "StringEquals": {"iam:PassedToService": "ssm.amazonaws.com"}
            }
        }
    ]
}

若干煩雑ですが、図で整理すると以下のような感じになります。 f:id:iselegant:20200928005810p:plain

ベースイメージの選択

Bastionホストとして利用するコンテナベースイメージですが、軽量かつパッケージマネージャーによる柔軟な構成が可能なイメージとしてAlpine Linuxがよく用いられますよね。 ただし、Systems ManagerはAlpine Linuxをオフィシャルにサポートしていません。

参考:Supported operating systems - AWS Systems Manager

また、アクティベーションコードを有効化する際にAWS CLIを利用しますが、Alpine LinuxAWS CLI v2に対応しておらず、現時点では以下のIssueが報告されています(正確に述べれば、glibcAWS CLI v2をダウンロードし、不要なものをいくつか削除すれば利用可能とのコメントがあります)。

aws-cli-v2 issue with alpine using Docker · Issue #4971 · aws/aws-cli · GitHub

[v2] Distribute binaries for alpine / musl libc · Issue #4685 · aws/aws-cli · GitHub

イメージサイズが若干大きくなってしまうものの、上記制約とAWS上での相性からAmazon Linuxを利用するほうが無難かもしれません。 今回もAmazon Linux前提でBastionホストを作成しています(私も最初はAlpine Linuxで作成していましたが、Amazon Linuxに切り替えました)。

アドバンスドインスタンスティアへの変更

Systems Managerで扱うインスタンス層の設定として、以下の2つの設定があります。

各ティアに関する内容は以下ドキュメントをご参照ください。

docs.aws.amazon.com

Session Managerで自前のインスタンスに接続するためには、アドバンスドインスタンスティアに変更する必要があります。 以下はCLIでの設定例ですが、事前に変更しておくとよいでしょう(変更無しにSession Managerを利用しようとすると、変更が必要な旨が表示されます)。

aws ssm update-service-setting --setting-id arn:aws:ssm:region:account_id:servicesetting/ssm/managed-instance/activation-tier \
--setting-value advanced

アドバンスドインスタンスティアに変更する注意点として、Session ManagerでBastionホストに接続する際、従量制で課金が発生します。 本ブログ執筆時点では、時間あたり 0.00695 USDなので、月当たり約500〜600 円になります。 おそらくBastionホストは利用時のみ起動するケースがほとんどなので、さほど気にならないとは思いますが、無料枠などはないため、一応把握しておくと良いです。

参考:料金 - AWS Systems Manager | AWS

アクセス制御と監査ログ

Bastionホストはプライベートなネットワーク領域にアクセスするための唯一の踏み台として役割を果たします。 そのため、アクセス制御や監査ログ取得などのセキュリティ要件が求められることがあると思います。

まずアクセス制御ですが、これはIAMポリシーにて制限をかけることが可能です。 以下のActionをDenyで指定することで、SessionManagerへのアクセス制限を掛けることができ、結果としてIAMポリシーが付与されたIAMユーザーのみがBastionホストにログインすることができます。

  • ssm:DescribeSessions
  • ssm:ResumeSession
  • ssm:StartSession
  • ssm:TerminateSession

次に監査ログですが、これはCloudTrail上で記録されます。例えば、セッションを開始した(ログインした)際にはeventNameに"StartSession"とエントリされた下記のようなCloudTrailログが記録されます。 ※実際に出力されたログを一部加工した内容です。

    {
      "eventVersion": "1.05",
      "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROA53L5...:m-arai",
        "arn": "arn:aws:sts::123456789012:assumed-role/m-arai-admin/m-arai",
        "accountId": "123456789012",
        "accessKeyId": "xxxxxxxxxxxxxx",
        "sessionContext": {
          "sessionIssuer": {
            "type": "Role",
            "principalId": "AROA53L5...",
            "arn": "arn:aws:iam::123456789012:role/m-arai-admin",
            "accountId": "123456789012",
            "userName": "m-arai-admin"
          },
          "webIdFederationData": {},
          "attributes": {
            "mfaAuthenticated": "false",
            "creationDate": "2020-09-27T13:48:12Z"
          }
        }
      },
      "eventTime": "2020-09-27T14:26:39Z",
      "eventSource": "ssm.amazonaws.com",
      "eventName": "StartSession",
      "awsRegion": "ap-northeast-1",
      "sourceIPAddress": "xxx.xxx.xxx.xxx",
      "userAgent": "aws-internal/3 aws-sdk-java/1.11.861 Linux/4.9.217-0.1.ac.205.84.332.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.262-b10 java/1.8.0_262 vendor/Oracle_Corporation",
      "requestParameters": {
        "target": "mi-0389c6c..."
      },
      "responseElements": {
        "sessionId": "m-arai-0ebd90...",
        "tokenValue": "Value hidden due to security reasons.",
        "streamUrl": "wss://ssmmessages.ap-northeast-1.amazonaws.com/v1/data-channel/m-arai-0ebd9...?role=publish_subscribe"
      },
      "requestID": "439c0cca-...",
      "eventID": "260e0e58-...",
      "readOnly": false,
      "eventType": "AwsApiCall",
      "recipientAccountId": "123456789012"
    }

以上の通り、IAMとCloudTrailでセキュリティ要件をカバーできるため、AWSを利用する上では他のサービス同様の開発体験が提供されているわけですね。

参考:AWS Systems Manager Session Manager - AWS Systems Manager

Session Managerのセッションタイムアウト

Session Managerでは、一定時間操作していないと「our session timed out due to inactivity and has been terminated.」というメッセージと共にセッションが切断されます。 このセッションタイムアウトですが、20分で固定されており、残念ながら利用者側でチューニングすることができません

参考:Terminate a session - AWS Systems Manager

回避策として、例えば、やwhile true; do date; sleep 1; doneなどを実行することで、フォアグラウンドの状態をアクティブにしておけばセッション切断が回避できます。 ただ、セッションが切れたら単純に再度接続すればよく、それほど手間でもないので「別にコマンドを打つまでも無いかな」、というのが個人的な考えです(若干のストレスを感じるかも知れませんが・・・)。 逆な意味で捉えると、セッション繋ぎっぱなしよりはセキュリティ的に望ましいと言えるかと思います。

また、セッションが切れてもBastionホスト上で実行するプログラムを停止したくない場合はバックグラウンドでプロセスを実行(&)したりnohupのアプローチを採用すればよさそうですね。

コンテナセキュリティ

スライドで掲載の通り、AWS Fargateを利用することで、Amazon EC2を利用していた際に考慮が必要であったOSレイヤーのセキュリティ対策はAWS側に責務を委譲することができます。

f:id:iselegant:20200928010334p:plain

一方、Bastionホストのコンテナ定義(Dockerfile等)に関する責務は利用者側です。 つまり、作成されるコンテナイメージに関するセキュリティ対策は利用者側で行わなければなりません。 ECRの脆弱性スキャンやTrivyなどを活用することで、この点はケアするほうが当然望ましいですよね。

もしご興味がある方は前回のブログでコンテナセキュリティに関して記事を投稿しているのでご覧ください。

iselegant.hatenablog.com

ここまでで、色々な考慮点についてお話してきました。 以降は少しコラム的な内容になります。

Bastion on Fargateをどんなシーンでも使えるか?

LT時にチェシャ猫さんが以下のようにコメントされていました。

現状IaaS(EC2)中心のサービスを展開している場合、Golden AMIのようなセキュリティ対策が施されたマシンイメージを保持していることが多いのではないでしょうか?

OS運用にもこなれているケースでは、逆にECS / AWS Fargateの学習コストの方が高くつく可能性があります。 CloudFormationやTerraform等でGolden AMIから一時的にBastionホストを作成・破棄するのが最適かと思います。

一方、ECS/Fargateでアーキテクチャをデザインしている場合、EC2運用は責任共有モデルの歪みからOS管理がトイルになりがちなので、その場合は有効なアーキテクチャの一つかな、と考えています。 また、昨今はAWSでサービス開発をする場合、利用するミドルウェアの制約等がなければ、あえてEC2を採用するモチベーションが少ないのでは?というのが個人的な考えです。 ECS/Fargateを利用が増えてきている中で、今回ご紹介のBastion構成は知っておいて損はない構成かな、とも思いますが、あくまで選択肢の一つとして考えておくと良いでしょう。

AWSでマネージドなBastionサービスがあってもいいのに・・・

本当にそう思います。現状は無いんですよね。 似たようなサービスとしてCloud9がありますが、セキュリティの文脈で利用されるBastionホストと違って、Cloud9は統合開発環境の用途なので思想が異なります。 また、Cloud9の実態はEC2なので、もともとやりたかったサーバーレス化は結局解消しません。

ちなみに、Microsoft AzureではAzure Bastionというマネージド・サービスがあるみたいです。 筆者は使ったことはないですが、料金計算してみたところ、約¥16,000弱/月で少々お高めでした(Bastionホストは一時的に立ち上げるケースがほとんどだと思うので、さほど気にならない金額に収まるとは思いますが)。

azure.microsoft.com

AWSでもマネージドBstionサービスが出ると嬉しいですよね。みなさんも是非AWSサポートケースに上げましょう。

おまけ

筆者が作ったBastion on FargateのDockerイメージをGitHub上に公開しておきました。 良ければ適当に活用してください。

github.com

なお、ネットワークスキャンツールであるnmapの導入も良いと思いますが、一点注意点があります。 AWSでネットワークスキャンを行う場合、以下の規定に従って行わなければなりません。

aws.amazon.com

BastionホストをFargateで実現したいケースでは、おそらくFargateタスクなどに対してスキャンしたい背景があるかと思います。 しかし、Fargate許可されたサービスに含まれておらず、事前の申請が必要になります。 もし、Bastionホスト経由でFargateコンテナにスキャンする場合はご注意ください。

まとめ

今回はFargate上でBastionホストを実現する際の設計・運用上の注意点やアクセス制御・監査観点からの確認などを中心にご紹介しました。 今回の構成ではBastionホストをPrivate Subnetに配置することができます。 すなわち、実ワークロード上で稼働するECSタスクと同様のSubnetにBastionホストを配置できるので、より柔軟な作業等が行えるかと思います。 是非、今回の記事をご参考にチャレンジしてみてください。

ちなみに、よくよく調べてみると、1年前ぐらいにtoriさんが同じようなことをコメントされていました。さすが!

※ここでの内容はすべて筆者の私見であり、所属する組織の意見でも代表するものでもありません。また、あくまで特定の秘匿情報ではなく公開可能な情報から述べているため、その点はご留意ください。

ではまた〜。