every Tech Blog

株式会社エブリーのTech Blogです。

クロスクラウド環境で AWS SSM を利用して SSH の開放範囲を絞る

エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。

エブリーでは全社的に SSH を使ったサーバーへのログインから、AWS Systems Manager Session Manager ( 以下 Session Manager ) を使った運用に切り替えました。

tech.every.tv

これは私達のチームで管理している他社クラウド (AWS 以外という意味で他社です) 上に存在するサーバについても対象ですが、Session Manager を直接利用することはできません。そこで Session Manager を使った SSH 踏み台サーバーを構築し、それ経由で SSH 接続することで、他社クラウド上のマシンの 22 番ポートを開放する範囲を限定することができました。

構成

簡単に前提とする構成を説明します。

AWS VPC と他社クラウドのネットワーク同士が Site-to-Site VPN で接続されている構成です。開発者や CI/CD は、AWS SSM を通して踏み台サーバーに SSH 接続し、更に踏み台サーバーから他社クラウド上の Target Server へ SSH でアクセスします。

この構成を取ることによって、目的のサーバーの SSH ポートの開放範囲を AWS VPC の CIDR ブロックのみに限定することができます。

Session Manager を通した SSH 接続

セッション開始時には aws ssm start-session というコマンドを使いますが、その際 AWS-StartSSHSession というドキュメントを指定することによって、SSH セッションが開始できます*1

なおその際の接続対象 (今回の場合、踏み台サーバー) では SSH 接続をサポートするように設定する必要があります*2。つまり SSH サーバーである必要があります。

踏み台サーバーの構築

sshd を動かすシンプルなサーバーを作ります。今回 ECS タスクとして動作させますが、環境に合わせて EC2 や EKS などでも良いかと思います。

Dockerfile は下記のようになりました。シンプルですね。

FROM debian:stable-20250520-slim

RUN apt-get update && \
    apt-get -y upgrade && \
    apt-get -y install openssh-server net-tools && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    mkdir /var/run/sshd

COPY --chmod=755 entrypoint.sh /root/entrypoint.sh
EXPOSE 22
ENTRYPOINT ["/root/entrypoint.sh"]

次に Dockerfile 内で ENTRYPOINT としている起動スクリプトです。

# entrypoint.sh
#!/bin/bash

if [ -n "$SSH_PUBLIC_HOST_KEY_BASE64ENCODED" ]; then
    echo "$SSH_PUBLIC_HOST_KEY_BASE64ENCODED" | base64 -d > /etc/ssh/ssh_host_ed25519_key.pub
    chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
fi
if [ -n "$SSH_PRIVATE_HOST_KEY_BASE64ENCODED" ]; then
    echo "$SSH_PRIVATE_HOST_KEY_BASE64ENCODED" | base64 -d > /etc/ssh/ssh_host_ed25519_key
    chmod 600 /etc/ssh/ssh_host_ed25519_key
fi
echo "$ROOT_PUBLIC_SSH_KEY_BASE64ENCODED" | base64 -d > /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys

exec /usr/sbin/sshd -D

ホストキーが起動毎に毎回生成されるのを避けるため、タスク起動時にあらかじめ生成した鍵でファイルを作成しています。なお各キーは AWS Secrets Manager に Base64 エンコードし保管してあるものを、そのまま ECS タスクの環境変数として注入している都合でデコードしています。

運用上の工夫

目的のサーバーへ踏み台サーバーを経由し SSH 接続をするためには、SSM セッションを確立後に SSH での多段接続という手順を踏む必要があり、毎回行うのは手間です。そこで簡単なスクリプトを使って、接続時の手間を軽減しています。

まず ECS タスクへと aws ssm start-session コマンドを実行する場合、--target オプションを <cluster_name>_<task_id>_<container_runtime_id> といった形式での指定が手間なので、下記 Make ターゲットで出力できるようにしています(中身はいささか力技ですが)。

CLUSTER_NAME := '<cluster_name>'
FAMILY_NAME := '<family_name>'

ACCOUNT_ID := `aws sts get-caller-identity --query "Account" --output text;`
TASK_ID := `aws ecs describe-tasks --cluster $(CLUSTER_NAME) --tasks \`aws ecs list-tasks --cluster $(CLUSTER_NAME) --family $(FAMILY_NAME) | jq -r '.taskArns[0]'\` | jq -r '.tasks[0].taskArn' | awk -F'/' '{print $$3}'`
CONTAINER_RUNTIME_ID := `aws ecs describe-tasks --cluster $(CLUSTER_NAME) --tasks \`aws ecs list-tasks --cluster $(CLUSTER_NAME) --family $(FAMILY_NAME) | jq -r '.taskArns[0]'\` | jq -r '.tasks[0].containers[0].runtimeId'`

.PHONY: echo-as-ssm-target
echo-as-ssm-target:
    @echo "ecs:$(CLUSTER_NAME)_$(TASK_ID)_$(CONTAINER_RUNTIME_ID)"

.ssh/config は下記のようにしています。少々トリッキーですが、make コマンドを .ssh/config 内で使用しています。ProxyJump オプションにより、bastion 経由で target_server への多段接続が自動的に行われます。結果的に ssh target_server を実行するだけで目的のサーバーへログインできます。

# .ssh/config

host bastion
  User root
  IdentityFile ~/.ssh/<bastion_key_name>
  ProxyCommand sh -c "aws ssm start-session --target `make -C <path_to_makefile_dir> echo-as-ssm-target` --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

Host target_server
  ProxyJump bastion
  IdentityFile ~/.ssh/<target_key_name>
  HostName ...

GitHub Actions などの CI/CD でも同様に、適切な IAM Role を渡せば SSM セッションを開始し、SSH 経由でデプロイやコマンド実行が可能です。

まとめ

今回踏み台サーバーを構築し、Session Manager を通した SSH 接続を行うことで、他社クラウドのマシンにおいても SSH ポートの開放範囲を絞ることができ、また運用上の手間も減らすことができました。似たような環境の方のお役に立てばと思います。