ssh秘密鍵のパスフレーズを自動入力する
これをプログラムから実行するにはどうすればよいか。
ssh-add
コマンドは、パスフレーズの入力を
標準入力からではなく、コンソールデバイスから直接受け取っている。このため、プログラムから
ssh-add
コマンドを実行して、
標準入力にパスフレーズを渡してもうまくいかない。
ssh-addのドキュメントを読むと、以下の環境変数を設定することで、パスフレーズの入力を任意のプログラムに任せられることが分かる。
> DISPLAY and SSH_ASKPASS
> If ssh-add needs a passphrase, it will read the passphrase from the current terminal if it was run from a terminal. If ssh-add does not have a terminal associated with it but DISPLAY and SSH_ASKPASS are set, it will execute the program specified by SSH_ASKPASS (by default “ssh-askpass”) and open an X11 window to read the passphrase. This is particularly useful when calling ssh-add from a .xsession or related script. (Note that on some machines it may be necessary to redirect the input from /dev/null to make this work.)
ssh-addは、ターミナルから起動されていた場合、現在のターミナルからパスフレーズを読み込む。もし、ターミナルがなく、環境変数DISPLAYとSSH_ASKPASSが設定されていたら、 SSH_ASKPASS
に指定されたプログラムを実行してX11ウインドウからパスフーレズを読み込む。これは、一般的に .xsession
や関連スクリプトからssh-addを呼ぶ際に使われる(コンピューターによってはこの仕組みを機能させるために、入力を/dev/nullからリダイレクトさせる必要がある)。
必要条件
1. ターミナルがない状態で起動する
2. 環境変数 DISPLAY
を設定する
3. 環境変数 SSH_ASKPASS
を設定する
4. 入力を /dev/null
からリダイレクトさせる(必要なら)
Webで検索すると、この条件を満たすシンプルなスクリプトを作ったサンプルがいくつかありました。
sshauto.sh#!/bin/bash
# 接続先情報
SSH_USER=test
SSH_PASS=mypass
SSH_HOST=your_ssh_server
REMOTE_CMD="ls -al"
# 後述のSSH_ASKPASSで設定したプログラム(本ファイル自身)が返す内容
if [ -n "$PASSWORD" ]; then
cat <<< "$PASSWORD"
exit 0
fi
# SSH_ASKPASSで呼ばれるシェルにパスワードを渡すために変数を設定
export PASSWORD=$SSH_PASS
# SSH_ASKPASSに本ファイルを設定
export SSH_ASKPASS=$0
# ダミーを設定
export DISPLAY=dummy:0
# SSH接続 & リモートコマンド実行
exec setsid ssh $SSH_USER@$SSH_HOST $REMOTE_CMD
このシェルスクリプトコードは、sshコマンドの呼び出し元と、SSH_ASKPASSのパスワード入力元を1つで兼ねているため、ちょっと分かりづらいですが、シンプルにまとまっています。
前述の3つの条件を満たすコードになっています
1. ターミナルがない状態で起動する
> 呼び出したプロセスは、 新しいセッションのリーダーとなる
2. 環境変数 DISPLAY
を設定する
3. 環境変数 SSH_ASKPASS
を設定する
Python版を実装した
do-ssh.py#!/usr/bin/env python
# 1行目のshebangはSSH_ASKPASSとして実行されるために必要
import os
import sys
import subprocess
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 接続先情報
SSH_USER = 'test'
SSH_HOST = 'your_ssh_server'
SSH_KEY = 'path/to/ssh/keyfile'
SSH_KEYPASS = 'mypass'
REMOTE_CMD = "ls -al"
def do_ssh():
env = os.environ.copy() # 現在のPython環境(venv等)を利用するため、環境変数をコピー
env['SSH_ASKPASS'] = os.path.abspath(sys.argv[0]) # 自分自身をパスワード入力用に指定
env['DISPLAY'] = ':999'
env['PASSPHRASE'] = SSH_KEYPASS
p = subprocess.Popen(
['ssh', f'{SSH_USER}@{SSH_HOST}', '-i', os.path.abspath(SSH_KEY), REMOTE_CMD],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
encoding='utf-8', # REMOTE_CMD実行結果に日本語が含まれる場合にutf-8としてdecodeする
preexec_fn=os.setsid # 子プロセス起動時にsetsidを呼んで現在のターミナルから切り離す
)
try:
out, err = p.communicate(timeout=1)
logger.debug('ssh return code: %s', p.returncode)
print(out, err)
except subprocess.TimeoutExpired:
# パスフレーズを間違えていた場合に待ち続けるのを避ける
logger.error('timeout')
p.kill()
logger.error(p.communicate())
def main():
if os.environ.get('PASSPHRASE'):
# ssh-addから呼ばれる、ssh-askpassとしての動作
print(os.environ['PASSPHRASE'])
sys.exit(0)
else:
do_ssh()
if __name__ == '__main__':
main()
上記のコードは以下を参考にした
pythonenv = {'SSH_ASKPASS':'/path/to/myprog', 'DISPLAY':':9999'}
p = subprocess.Popen(['ssh', '-T', '-v', 'user@myhost.com'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
preexec_fn=os.setsid
)