はじめに
踏み台における公開鍵認証で十分なセキュリティが確保されていて、リモートがパスワード認証になる場合があります。今回は更に踏み台に下記のような制限が設けられているとします。
command="ssh ..." |
踏み台接続後に自動的にリモートに接続する |
---|---|
no-port-forwarding |
ポートフォワーディングを無効化する |
その状況下で今回実現したいのは下記のアクションです。
- パスワードの自動入力
- 透過的なコマンドの実行
【解決策1】 expectをフル活用する
expect
コマンドはユーザの入力を仮想的に再現することが出来るツールです。自分で「ssh パスワード 自動化」とかでググって最初に辿り着いたのがこの方法だったので、更に利便性を高めるために色々手を加えてみました…
ソースコード
標準の
ssh
コマンドをオーバーライドします。.bashrc
や.zshrc
に直接書けるように1つの関数で定義しています。但し、シェル非標準のプロセス置換機能を使っていることに注意してください。全てのシェルで動作が保証されるわけではありません。特殊な環境かどうかの判定は (
~/.ssh/config
で設定されている)特殊なホスト名がssh
の引数群に正規表現でマッチするかどうかで判定しています。今回は簡易的に対象を1つのみに絞り、ハードコーディングすることにしています。パスワードも同様に1つのみに絞り、ハードコーディングすることにしています。
# sshコマンドをオーバーライドする
ssh() {
# 設定
local password="PASSWORD"
local specialhost="FUMIDAI"
if [[ "$@" =~ $specialhost ]]; then # 処理を適用する場合
# sshの引数とサブコマンドの引数を分離する
local sshargs
sshargs=("ssh" "-t")
local reach=false
while (( $# > 0 )); do
if [ "--" = "$1" ]; then
shift
break
elif [[ "$1" =~ ^-[bcDEeFIiLlmOopQRSWw]+$ ]]; then
sshargs=("${sshargs[@]}" "$1")
shift
if (( $# > 0 )); then
sshargs=("${sshargs[@]}" "$1")
shift
fi
elif [[ "$1" =~ ^-.*$ ]]; then
sshargs=("${sshargs[@]}" "$1")
shift
elif ! $reach; then
sshargs=("${sshargs[@]}" "$1")
reach=true
shift
else
break
fi
done
if (( $# == 0 )); then # サブコマンドがない場合は通常通りシェルを起動
# Expectでパスワードを自動入力
expect <(echo '
set timeout -1
set password [lindex $argv 0]
set sshargs [lrange $argv 1 end]
proc are_you_sure {} {
send [gets stdin]
send "\n"
expect "password:" password_input "Please type" are_you_sure
}
proc password_input {} {
global password
send $password
send "\n"
expect "password:" password_input "Last login:" interact
}
eval spawn -noecho command $sshargs
expect "Are you sure" are_you_sure "password:" password_input "Last login:" interact
') $password "${sshargs[@]}"
else #サブコマンドがある場合は標準出力のゴミを取り除く
# BASE6464デコードにはオプション2通りあるのでタイプを確認
echo "" | base64 -D &>/dev/null
local base64=$?
[ "$base64" = "0" ] && base64="-D" || base64="-d"
# バウンダリ生成
local boundary=$(mktemp -u boundary---------XXXXXXXXXXXXXXXXXXXXXXXXXX)
# Expectでパスワードを自動入力
expect <(echo '
set timeout -1
set password [lindex $argv 0]
set boundary [lindex $argv 2]
set bashargs [lrange $argv 3 [expr 3 + [lindex $argv 1] - 1]]
set sshargs [lrange $argv [expr 3 + [lindex $argv 1]] end]
set boundary_count 0
proc are_you_sure {} {
send [gets stdin]
send "\n"
expect "password:" password_input "Please type" are_you_sure
}
proc password_input {} {
global password
send $password
send "\n"
expect "password:" password_input "Last login:" command_input
}
proc command_input {} {
global boundary
global bashargs
global eb
expect_background "$boundary---" observe_boundary
set eb "echo \"$boundary\"\"---\""
set en "echo \"\""
set args [join $bashargs]
send "$eb && ( $args | base64 ) 2>/dev/null && $eb && $en && $en\n"
interact
}
proc observe_boundary {} {
global boundary
global boundary_count
if { $boundary_count < 1 } {
incr boundary_count
expect_background "$boundary---" observe_boundary
} else {
exit 0
}
}
eval spawn -noecho command $sshargs
expect "Are you sure" are_you_sure "password:" password_input "Last login:" command_input
') $password $# $boundary "$@" "${sshargs[@]}" |
# バウンダリで囲まれた部分だけを抽出
awk '
BEGIN {
count=0
skip=0
}
$0 ~ "'$boundary'---" {
++count
skip=1
}
count==1 {
if (skip != 1) {
print $0
} else {
skip=0
}
}
' |
# BASE64デコードする
base64 $base64
fi
else; # 処理を適用しない場合
# 本来のシェルコマンドをそのまま実行
command ssh "$@"
fi
}
利用例
リモートでシェルを起動する
特に意識せず普段通り ssh
を実行するだけです。パスワードが自動入力されます。
local@localhost:~$ ssh FUMIDAI
リモートでコマンドを実行する
ローカルとのデータ通信を必要としない場合
これも特に気に留めることはありません。set timeout -1
を最初に実行してあるので nohup
もいらないと思います。
local@localhost:~$ ssh FUMIDAI ls -A
local@localhost:~$ ssh FUMIDAI brew upgrade --all
ローカルとのデータ通信を必要とする場合
要注意です。とんでもなく鈍速でしか通信できないので実用性はあまりありません…というかデータの欠落が激しいので…
local@localhost:~$ (sleep 3 && cat example.txt) | ssh FUMIDAI 'cat > /tmp/example.txt'
local@localhost:~$ (sleep 3 && cat example.txt) | pv -L 3500 | ssh FUMIDAI 'cat > /tmp/example.txt'
local@localhost:~$ (sleep 3 && tar -zcf - .) | base64 | pv -L 3500 | ssh FUMIDAI 'base64 -d | tar -xf - -C /tmp'
【解決策2】 ssh-agent の AgentForwarding を利用する
後から便利な方法を教えていただいたのでこちらもまとめておきます。パスワード認証を本質的にスキップすることができ、間にexpect
を挟む必要が無くなります!
1. ローカルのssh-agentに秘密鍵を登録する
踏み台用と同じ秘密鍵をssh-agentに登録します。ssh-agentはファイルではなくメモリ上のプロセスなので再起動ごとに行う必要がありますが、この処理は自動化出来ます。
if [ -z `pgrep ssh-agent` ]; then
eval `ssh-agent` 1>/dev/null &&
ssh-add ~/.ssh/id_rsa 2>/dev/null
fi
2. リモートのauthorized_keysに公開鍵を登録する
一度パスワード認証で入っておき、~/.ssh/authorized_keys
に上記の秘密鍵に対応する公開鍵を追記します。こちらはファイルなので1回きりで済みます。
-
~/.ssh
はパーミッション0700
が必要です。 -
~/.ssh/authorized_keys
はパーミッション0600
が必要です。 -
>
と>>
を間違えないように! (既に登録されているものが消えてしまう)
local@localhost:~$ cat ~/.ssh/id_rsa.pub
出てきたやつをコピー
local@localhost:~$ ssh FUMIDAI
パスワード入力してログイン
example@example.com:~$ chmod 0700 ~/.ssh
example@example.com:~$ chmod 0600 ~/.ssh/authorized_keys
example@example.com:~$ cat >> ~/.ssh/authorized_keys
ペースト ⇛ 改行(必須!) ⇛ Ctrl-D
example@example.com:~$ logout
3. ssh
利用時にオプションに -A
を付加する
local@localhost:~$ ssh -A FUMIDAI
パスワード無しに一発ログイン完了!
example@example.com:~$
毎回このオプションを付けるのが面倒、またssh
ではなくscp
で使いたい場合なども考慮して、~/.ssh/config
に ForwardAgent yes
を書いておいたほうがいいかもしれません。
Host FUMIDAI
HostName example.com
ForwardAgent yes
追記
やっぱりssh-agentダメだった
-
scp
だとフォワーディングしてくれない…何故だ… -
ssh
でログインした後に実行して欲しいサブコマンド渡しても実行が始まらない…しかも標準出力にログインメッセージとか出てきてるし…これもうわかんねぇな…
まだ最初のやつのほうがマシですね。