Cloud

SSH鍵なしでEC2に入る:Session Managerの仕組み

海洋

この記事では、EC2にAWS Systems ManagerのSession Managerで接続する仕組みを整理します。AWSマネジメントコンソールで一度作った構成をTerraformで書き直したときに見えてきた部品や、作業中に気をつけた点もあわせてまとめます。

この記事でわかること
  • インバウンドを1つも開けずにEC2へ接続できる仕組み
    接続は、EC2側から外へ向かう通信で始まる
  • EC2に権限を渡すための部品
    IAMロール・信頼ポリシー・インスタンスプロファイルの関係と、コンソールでは見えない理由
  • 「SSM」という言葉が指すもの
    よく使う管理ポリシーには、接続以外の権限も含まれている
  • Azureサービスとの違い
    Session Managerと同じ仕組みのサービスは無い

構成と接続の流れ

構成と接続の流れは、次の図のとおりです。

環境:Terraform v1.16 / AWS provider v6 / 東京リージョン / Amazon Linux 2023
図では、Webアクセス用のALBなど、SSMの接続に関係しない部分を省いています。

EC2はprivateサブネットに置いています。パブリックIPはなく、キーペアも作っていません。セキュリティグループのインバウンドは、ALBからのHTTP(80番)だけでSSH(22番)は開けていません。インターネットへ出られるのは、NAT Gateway経由の外向き通信だけです。

この状態でも、手元のPCからEC2のシェルに入れます。流れは次の5段階です。

  1. EC2の起動時に、IAMロールが紐付く
    EC2にはIAMロールを直接付けられません。「インスタンスプロファイル」という入れ物を通して紐付けます。
  2. SSM AgentがIMDSから一時認証情報を受け取る
    Amazon Linux 2023には、SSM Agentが最初から入っています。Agentは、EC2の中から見えるIMDS(169.254.169.254)に問い合わせて、①のロールの一時認証情報を受け取ります。
  3. SSM Agentが外向きにSystems Managerへ接続し、待機する
    Agentは受け取った認証情報で、Systems ManagerのエンドポイントへHTTPS(443番)で接続します。そのまま指示を待ちます。privateサブネットなので、この通信はNAT GatewayとInternet Gatewayを通って出ていきます。
  4. 手元でセッションを開始し、IAMで認可される
    手元のPCで aws ssm start-session を実行します。Systems Managerは、実行した人がこのインスタンスに接続してよいかを、IAMで確認します。
  5. ③の接続を通ってシェルがつながる
    許可されると、③でEC2が張っておいた接続を通してセッションがつながります。EC2に向かう新しい接続は発生しません。

図の矢印をもう一度見てください。どの矢印も、「EC2から外へ向かう」か「手元のPCからSystems Managerへ向かう」かのどちらかです。EC2に向かって入ってくる矢印は1本もありません。EC2側から張ったトンネルに、あとからセッションが相乗りする形です。

なお、④の start-session の実行には、AWS CLIとは別にSession Manager pluginのインストールが必要です。CLIだけでは動きません。また、--target に渡すインスタンスIDを毎回コンソールなどで調べずに済むように、Terraformの output で出力するようにすると便利です。

output "app_instance_id" {
  description = "aws ssm start-session --targetに渡すインスタンスID"
  value       = aws_instance.app.id
}
Terraform (HCL)

次の章から、この①〜⑤の中身を順に見ていきます。

EC2に権限を渡す部品

図の①、「EC2にIAMロールが紐付く」部分です。

コンソールでは、プルダウンで1つ選ぶだけ

コンソールでEC2にロールを付ける流れは、次のようになります。

  1. IAMのコンソールでロールを作る。ユースケースに「EC2」を選び、AmazonSSMManagedInstanceCore を付ける
  2. EC2の起動画面(または作成後の「IAMロールを変更」)で、プルダウンからそのロールを選ぶ

画面の上では、「EC2にロールを付けた」だけに見えます。

Terraformでは4つのブロックになる

同じものをTerraformで書くと、次の4つに分かれます。

ブロック役割対応するAPI
data “aws_iam_policy_document”信頼ポリシーのJSONを組み立てるなし(Terraformの中で生成)
aws_iam_roleロール本体(信頼ポリシーを含む)CreateRole
aws_iam_role_policy_attachmentロールに許可ポリシーを付けるAttachRolePolicy
aws_iam_instance_profile入れ物を作り、ロールを入れるCreateInstanceProfile / AddRoleToInstanceProfile

これに加えて、aws_instance の iam_instance_profile で入れ物をEC2に紐付けます。

data "aws_iam_policy_document" "ec2_assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "app" {
  name               = "tf-role-app"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}

resource "aws_iam_role_policy_attachment" "app_ssm" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "app" {
  name = "tf-profile-app"
  role = aws_iam_role.app.name
}

resource "aws_instance" "app" {
  # ...
  iam_instance_profile = aws_iam_instance_profile.app.name
}
Terraform (HCL)

コンソールでは意識しづらい部品は2つあります。

1つ目は信頼ポリシーです。 IAMロールは、EC2だけのものではありません。Lambdaも、ECSのタスクも、別のアカウントの人も、ロールを引き受けることができます。そこで、「このロールを誰が引き受けてよいか」をロールごとに書く必要があります。それが信頼ポリシーで、今回は ec2.amazonaws.com(EC2というサービス)だけを許可しています。コンソールでユースケースに「EC2」を選んだとき、この中身は自動で埋められていました。

なお、信頼ポリシーを data "aws_iam_policy_document" で書いているのは、JSONを手書きしないためです。このdataブロックはAWSに問い合わせをせず、Terraformの中でJSONを組み立てるだけです。HCLとして書けるので、カンマの抜けのようなJSONの書き間違いが起きません。

2つ目がインスタンスプロファイルです。 EC2にはロールを直接付けられず、インスタンスプロファイルという入れ物にロールを入れて、その入れ物をEC2に付けます。1つの入れ物に入れられるロールは1つだけです。

コンソールでEC2用のロールを作ると、ロールと同じ名前のインスタンスプロファイルが自動で作られます。さらに、EC2の画面で選んでいたプルダウンの中身は、実はロールの一覧ではなくインスタンスプロファイルの一覧です。名前が同じなので、区別する必要がありませんでした。

Terraformでは、この入れ物を自分で書かない限り存在しません。コンソールで一度作ってからコードで書き直したことで、初めて見えた部品でした。

ロール名とインスタンスプロファイル名を分ける

コードでは、ロールを tf-role-app、インスタンスプロファイルを tf-profile-app と命名しています。

aws_instance の iam_instance_profile に渡すのは、ロールの名前ではなくインスタンスプロファイルの名前です。コンソールと同じように両方を同じ名前にしていると、ここでロール名を渡していても動いてしまい、取り違えに気づけません。名前を分けておけば、取り違えたときは apply の段階でエラーになります。

「どちらを指しているのか」をコードの上で区別できるようにするための名前付けです。

名前で渡すか、ARNで渡すか

もう1つ、コードを書いていて引っかかったのが、同じIAMまわりでも名前を渡す場所とARNを渡す場所があることです。

role       = aws_iam_role.app.name                                    # 名前
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"   # ARN
Terraform (HCL)

ARNは「AWS全体で一意な住所」です。Í

arn:aws:iam::123456789012:role/tf-role-app
|   |   |  |  |           |
|   |   |  |  |           +-- リソースの種類と名前
|   |   |  |  +-- アカウントID
|   |   |  +-- リージョン(:: の間。IAMはグローバルなので空)
|   |   +-- サービス名
|   +-- パーティション(通常は aws)
+-- 固定文字列
Plaintext

名前は、アカウントの中でのみ一意となるものです。role が名前でよいのは、ロールの付け先が同じアカウントの中に限られるからであり、 policy_arn がARNでなければならないのは、AWS管理ポリシーが自分のアカウントではなく「AWS自身」の持ち物だからです。ARNのアカウントIDの位置が aws になっているのがその印です。

インスタンスプロファイルなしでも管理できる仕組み

ここまで「EC2にはインスタンスプロファイルが必要」と書いてきましたが、SSMで管理するだけなら、それがなくても済む仕組みがあります。Default Host Management Configuration(DHMC)です。

DHMCをオンにすると、そのアカウント・リージョンのEC2すべてに、SSM用のロールが自動で渡されます。条件は次の2つです。

  • IMDSv2を使っていること(IMDSv1には対応していない)
  • SSM Agentが3.2.582.0以降であること

インスタンスプロファイルとDHMCの両方がある場合は、インスタンスプロファイルが優先されます。また、DHMCで渡される認証情報はSSM Agentだけが使うもので、EC2の上で動くアプリケーションは使えません。

今回はDHMCを使わず、インスタンスプロファイルを書きました。理由は2つです。

  • DHMCは、アカウントとリージョン単位の設定です。インスタンスプロファイルなら、「このEC2に何の権限が必要か」がこの構成のコードの中で完結します。
  • アプリケーションがAWSのAPIを呼ぶようになれば、その権限はどのみちインスタンスプロファイルで渡すことになります。

EC2が大量にあり、「全台を一律にSSMの管理下に置きたい」という場面では、DHMCのほうが向いていると思います。

IMDS:認証情報を受け取る窓口

図の②です。インスタンスプロファイルで紐付けたロールの一時認証情報は、EC2の中からIMDS(インスタンスメタデータサービス)に問い合わせて受け取ります。

IMDSには2つの世代があります。IMDSv1は、GETリクエストを1回送るだけで認証情報が返ってきます。そのため、EC2上のWebアプリケーションが外部からの指示で任意のURLにアクセスさせられる脆弱性(SSRF)があると、認証情報を盗み出される恐れがありました。

IMDSv2では、まずPUTリクエストでセッショントークンを取得し、以降のリクエストにそのトークンを付ける必要があります。コードでは、次のように書いています。

resource "aws_instance" "app" {
  # ...
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"   # IMDSv2 を必須にする
    http_put_response_hop_limit = 1            # トークンの応答を 1 ホップまでに制限
  }
}
Terraform (HCL)

http_put_response_hop_limit は、トークンを返すPUT応答のIPパケットが何ホップまで届くかを決めます。1にすると、応答はそのEC2自身の外には届きません。たとえばEC2の上でコンテナを動かしている場合、ネットワークの構成によってはコンテナからトークンを取得できなくなります。

実は、Amazon Linux 2023のAmazon Machine Image(AMI)は、何も指定しなくてもIMDSv2必須で起動します。そして、ホップ数の既定は2となっており、これはコンテナからもIMDSを使えるようにするためです。

今回はコンテナを動かしておらず、IMDSを使うのはEC2の上で直接動くSSM Agentだけなので、1で足ります。そして、既定で同じ値になる項目も含めて、コードに明示しました。AMIは「常に最新」をSSMのパブリックパラメータから取得しているので、AMI側の既定が将来変わると、EC2の挙動も知らないうちに変わる可能性があります。明示しておけば、AMIが替わってもこの部分は変わりません。

コードに書いたことで、Amazon Linux 2023の既定(ホップ数2)を知るきっかけにもなりました。コンソール操作だけでは、そもそもホップ数という設定があることすら意識しなかったと思います。

なお、先ほどのDHMCはIMDSv2が前提です。IMDSv2を必須にしておくことは、今後DHMCのような仕組みを使うときの前提条件にもなります。

インバウンドを1つも開けずに入れる理由

接続はEC2の側から始まる

図の③と⑤が、この仕組みの中心です。

SSM Agentは、起動するとSystems Managerのエンドポイントへ自分から接続し、その接続を張ったまま待機します。手元のPCで start-session を実行すると、Systems Managerはこの待機中の接続を使ってセッションを流します。

セキュリティグループはステートフルなので、EC2から始めた通信の戻りは、インバウンドのルールがなくても通ります。NAT Gatewayも、内側から始まった通信の戻りだけを通します。つまり、EC2から見るとすべての通信が「自分から出ていった通信とその戻り」なので、インバウンドに許可を書く必要がありません。

Agentが外向きに通信する先は、主に次の2つです。

エンドポイント用途
ssm.ap-northeast-1.amazonaws.comSystems Manager本体(インスタンス情報の登録など)
ssmmessages.ap-northeast-1.amazonaws.comSession Managerの通信路

古い資料では、これに ec2messages を加えた3つが挙げられていることが多いです。SSM Agent 3.3.40.0以降は、ssmmessages が使えるときはそちらを使うように変わりました。2024年以降に開設されたリージョンでは、ec2messages はそもそも提供されていません。

誰が入れるかはIAMで決まる

SSHなら、「誰が入れるか」はEC2の中に置いた公開鍵で決まります。人が増えれば鍵を配り、辞めれば鍵を消す必要があります。

Session Managerでは、それがIAMの許可に置き換わります。④で start-session を実行した人に、そのインスタンスへの ssm:StartSession の許可があるかどうかで決まります。私の環境ではIAM Identity Centerでログインしているので、接続の可否もそこで一元的に管理できます。

EC2の中には、鍵という管理対象が1つもないので運用が楽になります。

似た方式との違い:EC2 Instance Connect Endpoint

パブリックIPなしでEC2に入る方法には、EC2 Instance Connect Endpointもあります。VPCの中にエンドポイントを置き、そこを経由してSSHで接続する方式です。

こちらはSSHを使うので、EC2のセキュリティグループに「エンドポイントのセキュリティグループから22番」の許可が必要です。インターネットには開けないものの、VPCの中では22番を開けることになります。

インバウンドを1つも開けずに済むのは、エージェントが外向きにつなぐSession Managerならではの特徴です。その代わり、Session ManagerにはEC2の中のエージェントと、そこから外へ出る経路が必要になりますが、外向きに開ける方がセキュリティ的には良いでしょう。

「SSM」は1つの機能の名前ではない

ここまで「SSM」と書いてきましたが、SSMは1つの機能の名前ではありません。AWS Systems Managerというサービスの中に、次のような機能がまとめられています。APIの名前空間が ssm なので、全体がSSMと呼ばれています。

機能できること
Session Manager今回使ったシェル接続
Run Command複数のEC2に一斉にコマンドを実行する
Patch ManagerOSのパッチ適用を管理する
Inventoryインストール済みソフトウェアなどの情報を集める
Parameter Store設定値やシークレットを保存する
Fleet Manager管理下のサーバーを一覧で確認・操作する

機能の入れ替わりもあります。たとえばIncident ManagerとChange Managerは2025年11月から、Application Managerは2026年7月から、新規の利用を受け付けていません。

標準ポリシーの中身

IAMポリシーは、「どの操作を」「どのリソースに対して」許可するかを書いたJSONです。操作は サービス名:API名 の形で書きます。たとえば ssm:GetParameter は、「SSMの GetParameter(パラメータの値を取得する)というAPIを呼んでよい」という意味です。

EC2に付けた AmazonSSMManagedInstanceCore は、SSMの管理下に置くためにAWSが用意している標準のポリシーです。中には、こうした操作が25個並んでいます。何のための操作かで分けると、次のようになります。

何のための操作か含まれる操作(抜粋)
インスタンスをSSMに登録し、状態を報告するssm:UpdateInstanceInformation
Session Managerの通信路を作る・開くssmmessages:CreateControlChannel、ssmmessages:OpenDataChannel など4個
Run Commandなどの指示を受け取る(旧方式)ec2messages:GetMessages、ec2messages:SendReply など6個
Run CommandやState Managerの手順書(ドキュメント)を取得するssm:GetDocument、ssm:ListAssociations など
Inventoryに情報を送るssm:PutInventory
Patch Managerに必要なパッチ情報を取得するssm:GetDeployablePatchSnapshotForInstance
Parameter Storeの値を読むssm:GetParameter、ssm:GetParameters

Session Managerで接続するだけなら、必要なのは上の2行、合わせて5個の操作だけです。それ以外は、Run CommandやPatch Managerなど、SSMの別の機能のための権限です。

気をつけたいのは最後の行です。ポリシーの該当部分を抜き出すと、次のようになっています。

{
  "Effect": "Allow",
  "Action": [
    "ssm:GetParameter",
    "ssm:GetParameters"
  ],
  "Resource": "*"
}
JSON

実際のポリシーでは、この2つはほかの ssm: の操作と同じ Action の中に並んでいます。ここでは該当部分だけを抜き出しています。

Resource は「どのリソースに対して許可するか」の指定で、"*" は「すべて」を意味します。つまりこの部分は、「どのパラメータでも、値を読んでよい」という許可です。読めるパラメータを名前やパスで限定していません。

Parameter Storeには、データベースの接続先やパスワードのような設定値を置くことがよくあります。接続のために付けたつもりのポリシーで、このEC2はアカウント内のパラメータを読める状態になっている、ということです。なお、暗号化したパラメータ(SecureString)を復号できるかどうかは、暗号化に使った鍵(KMS)の設定にもよります。

ちなみに、先ほどのDHMCで使われる管理ポリシー(AmazonSSMManagedEC2InstanceDefaultPolicy)は、中身がほぼ同じなのに、この ssm:GetParameter と ssm:GetParameters だけが入っていません。AWS自身も、「全台に一律に付ける権限」からはパラメータの読み取りを外しているということです。

今回は、Parameter Storeにパラメータを1つも置いていない検証環境なので、標準のポリシーをそのまま使っています。パラメータを置くようになったら、接続に必要な5個の操作だけの独自ポリシーに差し替えるか、読めるパラメータをパスで絞った権限を別に付ける、という見直しが必要になります。

「SSM」という名前のポリシーを付けたとき、それがSSMのどの機能のための権限なのかは、中身を見ないと分からないので注意が必要です。

Azureにも同じ仕組みのサービスはあるのか

目的AWSAzure
VMのエージェントが外向きにつないで、シェルで入るSession Managerなし
VNet/VPCの中の入口から、VMへSSHで入るEC2 Instance Connect EndpointAzure Bastion
VMに持たせるIDIAMロールマネージドID(仕組みとして近いのはユーザー割り当て)
IDをVMに渡すための入れ物インスタンスプロファイルなし(VMに直接割り当てる)
IDを使ってよい相手の指定信頼ポリシーなし(割り当てたVMだけが使える)
権限の付け方ロールにポリシーを付ける対象リソースの側で、IDにロールを割り当てる(Azure RBAC)
認証情報の取得口IMDS(v2はトークン必須)IMDS(Metadata: true ヘッダー必須)
コマンドの一斉実行Run Command実行コマンド(Run Command)
設定値の保存Parameter StoreApp Configuration / Key Vault

Session Managerに当たるものはない

Azure VMにシェルで入るためのマネージドなサービスは、Azure Bastionです。BastionはVNetの中に置く踏み台で、そこからVMのプライベートIPへ22番(RDPなら3389番)でつなぎます。VMにパブリックIPは要りませんが、BastionからVMへの内向きの通信は発生します。NSGで通信を絞っている場合は、VM側でBastionからの22番を許可する必要があります。

この形は、AWSでいえばSession ManagerではなくEC2 Instance Connect Endpointに当たります。どちらも「VNet/VPCの中に置いたマネージドな入口から、VMのプライベートIPへSSHでつなぐ」方式で、VM側では入口からの22番を開けます。違いは主に置き場所と料金です。

  • Azure Bastion(Basic以上)は専用サブネット(AzureBastionSubnet)が必要で、時間単位の料金がかかります。検証向けのDeveloper SKUは無料で専用サブネットも要りませんが、一度に接続できるVMは1台だけです。
  • EC2 Instance Connect Endpointは既存のサブネットに置けて、追加料金はかかりません(AZをまたいで接続する場合のデータ転送料金を除く)。誰がトンネルを開けるかは、IAMで制御します。

一方、Session Managerのように「VMの中のエージェントが外向きにつないで待ち、そこへシェルのセッションを流す」サービスは、Azure VM向けにはありません。パブリックIPも追加のポートも開けずにSSHできる仕組みは、Azure Arc対応サーバー(Azureの外にあるサーバーをAzureから管理する仕組み)向けに用意されていますが、Azure VMは対象外です。コマンドを実行するだけなら、VMエージェント経由の実行コマンド(Run Command)がインバウンドなしで使えます。

マネージドIDとIAMロール

Azureでは、VMにマネージドIDを割り当てて権限を渡します。マネージドIDには2種類あります。

  • システム割り当て:VMの設定として有効にする。VMと一緒に作られ、VMを削除すると一緒に消える。そのVMだけが使う
  • ユーザー割り当て:独立したリソースとして作り、複数のVMに割り当てられる。VMを削除しても残る

IAMロールに仕組みとして近いのは、ユーザー割り当てのほうです。IAMロールも独立したリソースで、同じロールを複数のEC2で使え、EC2を削除しても残ります。「VMごとに1つ用意する」という使い方ならシステム割り当ても近く見えますが、IDの寿命がVMに縛られる点が違います。

どちらの種類でも、インスタンスプロファイルに当たる入れ物はありません。マネージドIDはVMに直接割り当てます。また、マネージドIDは割り当てたVMからしか使えないので、信頼ポリシーのように「誰がこのIDを使ってよいか」を別に書く必要もありません。

AWSのIAMロールは、EC2だけでなく、Lambdaや別のアカウントからも引き受けられる汎用的なIDです。だからこそ、引き受けてよい相手を書く信頼ポリシーと、EC2に渡すための入れ物(インスタンスプロファイル)が必要になります。

権限の付け方も逆向きです。AWSでは、ロールに「何をしてよいか」を書いたポリシーを付けます。権限はID側に書きます。Azureでは、ストレージアカウントやリソースグループといった対象の側で、マネージドIDにロールを割り当てます(Azure RBAC)。Azureの「ロール」は権限のセットのことで、AWSの「IAMロール」(ID)とは、同じ言葉でも指すものが違います。

おわりに

Terraformを書くと、信頼ポリシーやインスタンスプロファイル、IMDSの設定など、コンソールでは気付けない細かいところまで見えてくるのが面白いところ。

普段コンソールしか使わない、という人もぜひterraformでAWSをいじってみてもらえればと思います。

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

ABOUT ME
海洋
海洋
NWエンジニア
ネットワーク・セキュリティを中心に、業務や検証環境で実際に触れた内容を記録しています。
現職ではネットワークの運用・構築を担当。

保有資格
ネットワーク:ネットワークスペシャリスト / CCNA
セキュリティ:情報セキュリティマネジメント / ISC2 CC
クラウド  :AZ-104 / AZ-305
その他   :応用情報技術者 / LinuC レベル1

記事URLをコピーしました