LabHub
ブログ

ブログ

クラウドの bastion とは何か、AWS ではどう使うのがよいか — ProxyJump 実測、SSM Session Manager、EC2 Instance Connect Endpoint

한국어English日本語中文

扉が一つしかない建物

サーバーをプライベートサブネットに置けば、インターネットからは届かない。それが目的だ。しかし運用者は入らなければならない。この矛盾を解く古典的な答えが bastion host だ。パブリックサブネットに小さなホストを一台置き、インターネットに開いている扉はそのホストの SSH ポート一つだけ、残りのサーバーは bastion からの接続しか受けない。

建物にたとえれば、出入口を一つに減らしてその扉にだけ警備を置くことだ。警備が一人なら出入りの記録も一か所に残る。代わりにその扉が破られれば建物全体が破られる。だから bastion は最も堅く作り、最も頻繁にパッチを当て、最も丁寧に記録すべきホストになる。

古典的な bastion は SSH 一行で作られる

OpenSSH にはこのパターンが組み込まれている。ssh_config のマニュアルは ProxyJump をこう説明する。

Setting this option will cause ssh(1) to connect to the target host by
first making an ssh(1) connection to the specified ProxyJump host and
then establishing a TCP forwarding to the ultimate target from there.

まずジャンプホストに SSH で繋ぎ、そこから最終的な対象まで TCP フォワーディングを開く。コマンドラインでは -J だ。ホームラボで実際にやってみた。私の Mac(192.168.219.101)から cubi01(192.168.219.120)を経由して nuc1(192.168.219.116)に入る。

ssh -J cubi01 nuc1 'hostname; echo $SSH_CONNECTION'

最初の試みは失敗した。

channel 0: open failed: connect failed: Temporary failure in name resolution
stdio forwarding failed

-v を付けると ssh が何をしているかが見える。

debug1: Setting implicit ProxyCommand from ProxyJump: ssh -v -W '[%h]:%p' cubi01
debug1: Executing proxy command: exec ssh -v -W '[nuc1]:22' cubi01
Authenticated to cubi01 ([192.168.219.120]:22) using "publickey".
debug1: channel_connect_stdio_fwd: nuc1:22

nuc1 という名前は私の Mac の /etc/hosts にはあるが、cubi01 にはない(そこで getent hosts nuc1 は空を返す)。対象の名前を解決するのは自分のノートパソコンではなくジャンプホストだ。 bastion を使う人が最初にぶつかる事実である。IP に切り替え、known_hosts の名前の項目をそのまま使うように HostKeyAlias を与えれば通る。

ssh -o HostKeyAlias=nuc1 -J cubi01 192.168.219.116 'hostname; echo $SSH_CONNECTION'
nuc1
SSH_CONNECTION=192.168.219.120 53994 192.168.219.116 22

ジャンプなしで直接繋いだときと比べると違いが見える。

SSH_CONNECTION=192.168.219.101 55330 192.168.219.116 22

対象サーバーは接続が bastion(…120)から来たと見ている。 私の Mac のアドレス(…101)はどこにもない。対象サーバーのログだけ見ても「cubi01 が入ってきた」ことしか分からない。誰が入ったかは bastion のログにしかない。bastion の監査ログを必ず残して外に出さなければならない理由がこの一行にある。

このホームラボは五台のノードが 192.168.219.0/24 の中にあり、インターネットから開いているのはゲートウェイの 80(ACME HTTP-01 用)と 443 だけだ。運用コマンドは LAN の中から ssh cubi01 で入って実行する。つまり cubi01 は LAN 内の管理ノードであって、インターネットから見える bastion ではない。インターネットに 22 番を開けなければならない瞬間からが本物の bastion で、その瞬間から上の心配がすべて現実になる。

bastion を自分で運用すると背負うもの

AWS はこの一覧をほぼそのまま文書に書き、二つの代替案を出した。

AWS の答え 1:SSM Session Manager

Systems Manager の文書は Session Manager を「インバウンドポートを開いたり、bastion ホストを維持したり、SSH 鍵を管理したりする必要なく」ノードを管理する機能として紹介する。インスタンス内の SSM Agent が 外向きに Systems Manager のエンドポイントへ接続し、運用者はその接続に乗って入る。インバウンドルールは一つも要らない。

aws ssm start-session --target i-0123456789abcdef0

権限はすべて IAM だ。どのインスタンスに誰が入れるかをタグやリソース ARN で決め、セッションの内容は S3・CloudWatch Logs に、API 呼び出しは CloudTrail に残す。鍵ファイルが無いので退職者の鍵を消す作業も無い。

ポートフォワーディングもできる。インスタンスの 80 番を自分のノートパソコンの 56789 に引いてきたり、

aws ssm start-session --target i-0123456789abcdef0 \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{"portNumber":["80"], "localPortNumber":["56789"]}'

インスタンスを足場にしてその後ろの RDS まで引いてくることもできる。リモートホストは Systems Manager に登録されている必要がない。

aws ssm start-session --target i-0123456789abcdef0 \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["mydb.example.us-east-2.rds.amazonaws.com"],"portNumber":["3306"], "localPortNumber":["3306"]}'

これまでの sshscp をそのまま使いたければ ~/.ssh/config に ProxyCommand を置けばよい。ノードの 22 番は閉じたままだ(文書の表現では "You can close inbound ports on the node")。

# SSH over Session Manager
Host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
    User ec2-user

ここに一つ、文書に明記された罠がある。SSH やポートフォワーディングで張ったセッションは内容が記録されない。 SSH が TLS トンネルの中でさらに暗号化するため、Session Manager はトンネルの役割しかしない。「誰がいつどのインスタンスに繋いだか」は CloudTrail に残るが、「どんなコマンドを打ったか」は通常のセッション(start-session だけを使った場合)でしか残らない。監査が目的なら SSH 経由を IAM で拒否でき、文書がそのポリシーを示している。

{
  "Effect": "Deny",
  "Action": "ssm:StartSession",
  "Resource": "arn:aws:ssm:*:*:document/AWS-StartSSHSession"
}

AWS の答え 2:EC2 Instance Connect Endpoint

Session Manager にはインスタンス内にエージェントが要る。エージェントを入れられないインスタンスや、単に普通の SSH・RDP を使いたいときのために、AWS は EC2 Instance Connect Endpoint(EICE) を用意した。文書はこれを "identity-aware TCP proxy" と呼ぶ。VPC のサブネット一つにエンドポイントを作ると、IAM 資格情報で認証されたトンネルが自分のコンピュータからそのエンドポイントまで開き、そこから VPC 内のインスタンスへ TCP が出て行く。

文書で確認した性質はこうだ。

使い方は SSH の ProxyCommand を変えるだけだ。

Host i-*
    ProxyCommand aws ec2-instance-connect open-tunnel --instance-id %h

あるいは CLI が代わりにやってくれる。

aws ec2-instance-connect ssh --instance-id i-0123456789abcdef0 --connection-type eice

Windows インスタンスは --remote-port 3389 でトンネルを開き、ローカルポートに RDP クライアントを繋ぐ。

三つを並べる

EC2 bastion(自前運用)SSM Session ManagerEC2 Instance Connect Endpoint
インターネットに開くポート22なし(エージェントが外向きに接続)なし(エンドポイントが VPC 内)
インスタンスに必要なものパブリック IP 付き bastion + 対象の SG ルールSSM Agent + IAM ロールなし(キーペアのみ)
認証SSH 鍵IAMIAM + SSH 鍵
接続の記録bastion の sshd ログ(自前管理)CloudTrail + セッション内容(S3/CloudWatch)CloudTrail(試みすべて)
セッション内容の記録別のツールが必要通常セッションのみ、SSH/フォワーディング経由は不可不可(TCP プロキシ)
上限なしサブネットごと 1、同時 20、1 時間
費用EC2 インスタンス料金なしなし(AZ 間転送を除く)
合う場所エージェントもエンドポイントも使えない場合監査・コマンド記録が必要な運用普通の SSH/RDP、エージェントの無い AMI

AWS ではこう使うのがよい

最初の選択は Session Manager だ。 インバウンドポートが無く、鍵が無く、コマンドまで記録される。bastion が背負っていた五つの負担がすべて消える。インスタンスに SSM Agent と AmazonSSMManagedInstanceCore 権限を持つロールを付けるだけでよい。プライベートサブネットにインターネット経路が無ければ、Systems Manager 用の VPC エンドポイント(PrivateLink)を置けばエージェントは VPC 内で接続する。

SSH そのものが必要なら EICE だ。 rsyncscp、IDE のリモート開発、RDP のように本物の TCP 接続が要る作業は EICE でトンネルを開き、いつもの道具を使う。サブネットごとに一つ、同時 20、1 時間という上限を覚えておき、maxTunnelDuration でより短く縛る。

bastion EC2 は最後の選択だ。 それでも置くなら、実測が教えた二つを守る。対象サーバーは bastion のアドレスしか見ないので bastion の sshd ログを外(CloudWatch Logs など)に出し、対象のセキュリティグループは bastion のセキュリティグループから来る 22 番だけを許可する。そしてその bastion にも SSM Agent を入れておけば、ある日 22 番を閉じても入る道が残る。

一行で

bastion は「扉を一つに減らす」という考えであり、AWS でその考えを最もよく実現するのは、もはや bastion ホストではなく インバウンドポート無しに IAM で入る Session Manager だ。本物の SSH が必要なら EICE を使い、自前運用の bastion は、対象が bastion のアドレスしか見ないという事実を忘れないまま最後に選ぶ。

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます