IBM BobでSSH先サーバーの aws login --remote を通す方法
背景
IBM BobからSSHでリモートサーバーへ接続し、そのサーバー上でAWS CLIを使いたい場面がありました。
ローカル端末ではなくSSH先のサーバーからAWSへアクセスしたいので、AWS CLIの認証もSSH先で完了させる必要があります。
AWS CLIには、このようなリモート環境向けに aws login --remote があります。
aws login --remote
このコマンドはブラウザを自動で開かず、代わりに認証用URLを表示します。人間がそのURLを手元のブラウザで開き、ログイン後に表示された認証コードをCLIへ貼り付ける、という流れです。
なぜSSH先でAWS CLI認証したかったのか
今回やりたかったのは、QuarkusのネイティブアプリをAWS Lambda向けにビルドしてデプロイすることです。
手元の環境はApple SiliconのmacOSでした。この環境でQuarkusのローカルnative buildを実行すると、基本的にはmacOS用arm64バイナリが生成されます。
一方、AWS Lambdaで必要なのはLinux用のarm64またはx86_64バイナリです。そのため、macOS上で直接作ったネイティブバイナリは、そのままLambdaでは動きません。
file target/*-runner
macOSで直接ビルドした場合は、たとえば次のように表示されます。
Mach-O 64-bit executable arm64
Lambda向けには、次のようなLinux用ELFバイナリが必要です。
ELF 64-bit LSB executable, ARM aarch64
この差を避けるには、Quarkusのコンテナビルドを使うのが基本です。
./mvnw package -Dnative -Dquarkus.native.container-build=true
ただし、今回はリモートのLinuxサーバー上でビルドやデプロイ作業を行いたかったため、SSH先サーバーからAWSへアクセスできるようにする必要がありました。
そもそも、なぜQuarkus Nativeを使いたかったのか
今回QuarkusのネイティブアプリをAWS Lambdaに載せたかった理由は、主に起動速度です。
データ連携のバッチ処理を使う程度なら、小さいVMを借りるよりもコストは安くなるのではないか?と思い検証しようと考えました。
Javaアプリを通常のJVMモードでLambdaに載せると、コールドスタート時にJVMの起動、クラスロード、フレームワーク初期化が発生します。QuarkusはJVMモードでも起動が速いフレームワークですが、Native Imageにするとさらに起動時間を短くできます。
一般的には、JVMモードではコールドスタートが数百ミリ秒から数秒程度になることがあります。一方、Native Imageでは数十ミリ秒から数百ミリ秒程度に収まるケースがあります。
もちろん実際の差は、以下の条件で変わります。
- Lambdaのメモリサイズ
- 初期化するライブラリの量
- VPC接続の有無
- 使用するAWS SDKやDBクライアント
- arm64かx86_64か
- SnapStartやProvisioned Concurrencyの有無
そのため、「必ず何倍速い」とは言えませんが、コールドスタートを小さくしたい用途ではQuarkus Nativeは有力な選択肢です。
今回も、Lambdaでの起動速度を重視してQuarkus Nativeを使うことにしました。
そこで、BobからSSH先で aws login --remote を実行する手順を整えました。
やりたいこと
Bobに以下の流れを実行させます。
- BobがSSHでリモートサーバーへ接続する
- SSH先で
aws login --remoteを実行する - 表示された認証URLを人間に見せる
- 人間がブラウザでURLを開いて認証する
- 表示された認証コードをBob経由でSSH先のCLIへ渡す
- SSH先サーバーからAWS CLIが使えるようになる
最初に試した方法
まずは素直にSSH越しに実行します。
ssh user@host 'aws login --remote'
しかし、Bob経由では対話的なコマンドがうまく扱えないことがあります。
そこで疑似TTYを強制します。
ssh -tt user@host 'AWS_PAGER= AWS_CLI_AUTO_PROMPT=off aws login --remote'
-tt はSSHで疑似TTYを強制的に割り当てるオプションです。
通常のターミナルなら、これで次のようにURLが表示されます。
Browser will not be automatically opened.
Please visit the following URL:
https://...
Please enter the authorization code displayed in the browser:
Bobで起きた問題
BobのRun表示では、次の行で止まったように見えることがありました。
Browser will not be automatically opened.
本来はこの次にURLが表示されるはずですが、Bobの画面ではURLが見えません。
つまり、aws login --remote は進んでいるように見えるものの、人間が開くべきURLが取得できない状態です。
tmux を使う案
このような対話コマンドは、リモート側で tmux に逃がしておくと扱いやすいです。
ssh user@host "tmux new-session -d -s aws-login 'AWS_PAGER= AWS_CLI_AUTO_PROMPT=off aws login --remote'"
出力はあとから取得できます。
ssh user@host 'tmux capture-pane -pt aws-login -S -200'
ただし、tmux はリモートサーバーにインストールされていないことがあります。
今回の目的では、追加インストールなしで実現したかったため、別の方法を使いました。
追加インストールなしの回避策
aws login --remote の出力を一時ファイルへ保存し、認証コード入力にはFIFOを使います。
まず、SSH先で aws login --remote をバックグラウンド起動します。
ssh user@host 'set -eu; d=/tmp/aws-login-bob; rm -rf "$d"; mkdir -m 700 "$d"; mkfifo "$d/code"; nohup sh -c '"'"'exec 3<> "$1/code"; AWS_PAGER= AWS_CLI_AUTO_PROMPT=off aws login --remote <&3 > "$1/output" 2>&1; echo $? > "$1/exit"'"'"' sh "$d" >/dev/null 2>&1 & echo $! > "$d/pid"; echo "$d"'
出力を確認します。
ssh user@host 'sed -n "1,200p" /tmp/aws-login-bob/output'
ここで認証URLが見えるようになります。
Browser will not be automatically opened.
Please visit the following URL:
https://...
Please enter the authorization code displayed in the browser:
人間がURLをブラウザで開き、認証コードを取得します。
取得したコードをSSH先の待機中プロセスへ渡します。
ssh user@host "printf '%s\n' 'AUTHORIZATION_CODE' > /tmp/aws-login-bob/code"
認証結果を確認します。
ssh user@host 'sed -n "1,240p" /tmp/aws-login-bob/output'
AWS CLIとして認証できているか確認します。
ssh user@host 'aws sts get-caller-identity'
最後に一時ファイルを削除します。
ssh user@host 'rm -rf /tmp/aws-login-bob'
仕組み
この方法では、SSH先に一時ディレクトリを作っています。
/tmp/aws-login-bob/
├── code # 認証コード入力用のFIFO
├── output # aws login --remote の出力
├── pid # バックグラウンドプロセスID
└── exit # 終了コード
aws login --remote の標準出力と標準エラーを output に保存します。
一方で、標準入力にはFIFOである code をつなぎます。
そのため、Bobは別コマンドで output を読んでURLを人間に提示できます。また、人間が取得した認証コードを code に書き込むことで、待機中の aws login --remote に入力できます。
Bob Skillにする
この手順はBobのSkillにしておくと(自分にとって)便利なのではと思いました。
例えば、以下の場所にSkillを作成します。
~/.bob/skills/aws-remote-login/SKILL.md
Skillの流れは以下のようにします。
- まず
ssh -ttでaws login --remoteを試す - URLが見えない場合は
/tmp/aws-login-bob方式に切り替える -
/tmp/aws-login-bob/outputからURLを取得して人間に提示する - 人間が取得した認証コードを
/tmp/aws-login-bob/codeに流し込む -
aws sts get-caller-identityで確認する -
/tmp/aws-login-bobを削除する
これでBobに次のように頼めます。
SSH先でAWS remote loginして
注意点
SSH認証方式の考え方
SSH接続時の認証情報は、できるだけBobの画面、コマンド履歴、プロセス一覧に出さないようにします。
安全性の優先順位としては、以下の順で考えます。
- SSH公開鍵認証を使う
- 人間がSSHのパスワードプロンプトへ直接入力する
-
.env+sshpass -eを使う -
sshpass -p 'password'のような直書きは避ける
本来の推奨はSSH公開鍵認証です。
ただし今回は、検証環境で一時的にパスワード認証を使う前提とし、コマンドラインへパスワードを直接書かないために .env + sshpass -e を使います。
.env を使う前提
この方法は、以下の前提で使います。
- 検証環境または一時的な作業である
-
.envはGit管理しない -
.envの内容を記事、ログ、スクリーンショットに載せない - Bobに
.envの中身を表示させない - 作業後に
.envまたは認証情報を削除する - 可能になった時点でSSH公開鍵認証へ切り替える
.env はコマンドライン直書きよりはましですが、平文ファイルに秘密情報を置く点は変わりません。公開鍵認証の代替ではなく、あくまで一時的な緩和策として扱います。
.env の例
SSH_USER=user
SSH_HOST=example.com
SSHPASS=your_password
.env は必ず .gitignore に追加します。
.env
必要に応じて権限も絞ります。
chmod 600 .env
.env を使ったSSH実行例
sshpass -e は、環境変数 SSHPASS からパスワードを読みます。
set -a
. ./.env
set +a
sshpass -e ssh "${SSH_USER}@${SSH_HOST}" 'aws sts get-caller-identity'
unset SSHPASS
aws login --remote を実行する場合も、パスワードをコマンドに直接書かず、.env から読み込ませます。
set -a
. ./.env
set +a
sshpass -e ssh "${SSH_USER}@${SSH_HOST}" 'AWS_PAGER= AWS_CLI_AUTO_PROMPT=off aws login --remote'
unset SSHPASS
避けたい例
以下のようにパスワードをコマンドへ直接書くのは避けます。
sshpass -p 'password' ssh user@example.com
この形式だと、Bobの表示、シェル履歴、プロセス一覧などにパスワードが残る可能性があります。
投稿時の注意
Qiitaなどに投稿する場合は、スクリーンショットやログに含まれる以下の情報を必ずマスクします。
- IPアドレス
- ユーザー名
- パスワード
- 認証URL
- 認証コード
- AWSアカウントID
- ARN
まとめ
IBM BobからSSH先サーバーで aws login --remote を実行すると、BobのRun表示では認証URLが見えないことがあります。
まずは ssh -tt を試します。
ssh -tt user@host 'AWS_PAGER= AWS_CLI_AUTO_PROMPT=off aws login --remote'
それでもURLが見えない場合は、追加インストール不要の一時ファイル/FIFO方式が使えます。
この手順をBob Skill化しておくと、SSH先サーバーからAWSへアクセスしたいときに再利用しやすくなります。
ということでgithubに置きました。