generated at
ssh秘密鍵のパスフレーズを自動入力する
sshssh-add秘密鍵を使うためにはパスフレーズを入力する必要がある。
これをプログラムから実行するにはどうすればよいか。
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. ターミナルがない状態で起動する
setsidでターミナルを引き継がないようにしている
> 呼び出したプロセスは、 新しいセッションのリーダーとなる
> 新しいセッションは制御端末を持たない
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()
上記のコードは以下を参考にした
python
env = {'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 )