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段階です。
- EC2の起動時に、IAMロールが紐付く
EC2にはIAMロールを直接付けられません。「インスタンスプロファイル」という入れ物を通して紐付けます。 - SSM AgentがIMDSから一時認証情報を受け取る
Amazon Linux 2023には、SSM Agentが最初から入っています。Agentは、EC2の中から見えるIMDS(169.254.169.254)に問い合わせて、①のロールの一時認証情報を受け取ります。 - SSM Agentが外向きにSystems Managerへ接続し、待機する
Agentは受け取った認証情報で、Systems ManagerのエンドポイントへHTTPS(443番)で接続します。そのまま指示を待ちます。privateサブネットなので、この通信はNAT GatewayとInternet Gatewayを通って出ていきます。 - 手元でセッションを開始し、IAMで認可される
手元のPCでaws ssm start-sessionを実行します。Systems Managerは、実行した人がこのインスタンスに接続してよいかを、IAMで確認します。 - ③の接続を通ってシェルがつながる
許可されると、③で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にロールを付ける流れは、次のようになります。
- IAMのコンソールでロールを作る。ユースケースに「EC2」を選び、
AmazonSSMManagedInstanceCoreを付ける - 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" # ARNTerraform (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.com | Systems Manager本体(インスタンス情報の登録など) |
| ssmmessages.ap-northeast-1.amazonaws.com | Session 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 Manager | OSのパッチ適用を管理する |
| 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にも同じ仕組みのサービスはあるのか
| 目的 | AWS | Azure |
|---|---|---|
| VMのエージェントが外向きにつないで、シェルで入る | Session Manager | なし |
| VNet/VPCの中の入口から、VMへSSHで入る | EC2 Instance Connect Endpoint | Azure Bastion |
| VMに持たせるID | IAMロール | マネージドID(仕組みとして近いのはユーザー割り当て) |
| IDをVMに渡すための入れ物 | インスタンスプロファイル | なし(VMに直接割り当てる) |
| IDを使ってよい相手の指定 | 信頼ポリシー | なし(割り当てたVMだけが使える) |
| 権限の付け方 | ロールにポリシーを付ける | 対象リソースの側で、IDにロールを割り当てる(Azure RBAC) |
| 認証情報の取得口 | IMDS(v2はトークン必須) | IMDS(Metadata: true ヘッダー必須) |
| コマンドの一斉実行 | Run Command | 実行コマンド(Run Command) |
| 設定値の保存 | Parameter Store | App 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をいじってみてもらえればと思います。