ラベル sshpass の投稿を表示しています。 すべての投稿を表示
ラベル sshpass の投稿を表示しています。 すべての投稿を表示

2014年8月31日日曜日

subprocess.Popen() で子プロセスにファイルディスクリプタを渡す方法について

「 Python から sshpass を実行する時に安全にパスワードを渡す方法について 」では Python で sshpass にファイルディスクリプタ経由でパスワードを渡す方法も紹介しました。

その記事にも書いてますが、Python3 でちゃんとパスワードを渡せるようになるまでにかなり苦労し、少し詳しくなれたので忘れないように記事にしときます。

まず、 http://pydoc.net/Python/ansible/0.9/ansible.runner.connection_plugins.ssh/ に Python2 系と思われるコードが掲載されています。これを参考に以下のサンプルコードを書いてみました。

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

oSub = subprocess.Popen(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'localhost', 'hostname'],
    stdout=subprocess.PIPE)

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())

このサンプルコードは Python2.7 では期待通り動きます。

$ python2.7 SampleSshpass.py
Password:
TestServer

しかし、Python3.2 ではうまくいきません。

$ python3.2 SampleSshpass.py
Password: Permission denied, please try again.

os.pipe() で作成したパイプを使って sshpass にパスワードを渡そうとしてますが、うまくいってないようです。

Python2.7 と Python3.2 の subprocess.Popen() のドキュメントを見比べてみると引数の close_fds のデフォルト値が Python2.7 では False ですが、Python3.2 では True になっていました。

ドキュメントの close_fds の説明を見ると Linux では以下のようです。 (Windows はこれとは違うようです)

If close_fds is true, all file descriptors
except 0, 1 and 2 will be closed before the child
process is executed. (POSIX only).

close_fds が True の場合は、子プロセス (今回の場合は sshpass) が実行される前に 0 (標準入力), 1 (標準出力), 2 (標準エラー出力) 以外のファイルディスクリプタは閉じられてしまうとのことです。

iPipe_r, iPipe_w = os.pipe() で作成したパイプを子プロセスの標準入力として渡していないので閉じられてしまっているようです。

なので、標準入力としてパイプのファイルディスクリプタを渡す以下のコードは Python3.2 でもうまく動きます。

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

oSub = subprocess.Popen(
    ['sshpass', 'ssh', 'localhost', 'hostname'],
    stdout=subprocess.PIPE, stdin=iPipe_r)

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())

sshpass のオプションに -f, -d, -p, -e のどれも指定しないようにしたので、標準入力からパスワードが読み込まれます。

stdin=iPipe_r で標準入力としてパイプのファイルディスクリプタを渡しています。

また、標準入力として渡さなくても subprocess.Popen() の引数で close_fds=False としてやれば Python3.2 でもうまく動きます。

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

oSub = subprocess.Popen(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'localhost', 'hostname'],
    stdout=subprocess.PIPE, close_fds=False)

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())
しかし、このコードは Python3.4 では動きません。。。
$ python3.4 SampleSshpass2.py
Password: Permission denied, please try again.

先程と同様のエラー内容からして、パイプのファイルディスクリプタが閉じられているように思われます。

Python3.4 のドキュメントでそれらしいところが無いか探してみると、os.pipe() の説明が以下のようになっていました。

os.pipe()
    Create a pipe. Return a pair of file descriptors (r, w)
    usable for reading and writing, respectively.
    The new file descriptor is non-inheritable.

    Availability: Unix, Windows.

    Changed in version 3.4: The new file descriptors
    are now non-inheritable.

non-inheritable の説明は ここ にあります。

ファイルディスクリプタには inheritable というフラグがあり、このフラグが False の場合は子プロセス実行時に OS レベルでこのファイルディスクリプタが閉じられるようです。

Python3.4 より前のバージョンでは os.pipe() で作成されるパイプの inheritable フラグは True でしたが、Python3.4 では False になったとのことです。

作成したパイプが inheritable かどうかは以下のように調べます

$ python3.4
>>> import os
>>> iPipe_r, iPipe_w = os.pipe()
>>> os.get_inheritable(iPipe_r)
False

パイプを os.set_inheritable() で inheritable に変更すればいいので、一応以下のコードで sshpass できます。

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()
os.set_inheritable(iPipe_r, True)

oSub = subprocess.Popen(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'localhost', 'hostname'],
    stdout=subprocess.PIPE, close_fds=False)

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())

しかし、os.set_inheritable() は Python3.4 で新たに追加された関数なので Python3.2 とかでは動かず、あまり好ましくありません。

もう少しドキュメントを読んでみると subprocess.Popen() の pass_fds という引数がありました。この引数で指定されたファイルディスクリプタは子プロセス起動時に閉じられないとのことです。

ということで、最終的にコードは以下のようになりました。

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

oSub = subprocess.Popen(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'localhost', 'hostname'],
    stdout=subprocess.PIPE, pass_fds=(iPipe_r,))

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())

Python3.2 でも、3.4 でも動いてます。

おまけですが、subprocess.check_output() は ドキュメントでは引数は

subprocess.check_output(args, *, input=None, stdin=None, stderr=None,
        shell=False, universal_newlines=False, timeout=None)

となっていて、pass_fds がありませんが、もう少しドキュメントを読み進めると subprocess.Popen() の stdout 引数以外は使えるようです。なので、subprocess.check_output() の場合は以下のコードとなります。

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

bOutput = subprocess.check_output(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'localhost', 'hostname'],
    pass_fds=(iPipe_r,))

print(bOutput.decode())

2014年8月29日金曜日

Python から sshpass を実行する時に安全にパスワードを渡す方法について

リモートのサーバーで ssh 経由でコマンドを実行することを Python からやろうとしてまして、sshpass を使うことにしました。

sshpass は ssh 実行時にプロンプトでのパスワード入力を省略したい時等に使えます。例えば

$ sshpass -p hogehoge ssh worker@RemoteServer ls -la

とすると、プロンプトでのパスワード入力を求められることなく RemoteServer に worker ユーザーでログイン (パスワードは hogehoge) して、ls -la を実行した結果を得ることができます。

パスワード入力を省略したい場合は ssh で公開鍵認証方式を使うこともよくありますが、サーバーに公開鍵をインストールしていない場合でも ssh できるように sshpass を使うことにします。

なお、サーバーによっては ID/Password での ssh 認証方式を設定によって禁止しているものもありますが、その場合は sshpass は使えません。

また、パスワードを取り扱うので、他のユーザーにある程度パスワードが漏れないように気を付ける必要があります。

sshpass にパスワードを渡す方法はいくつかあるので、それぞれについてどうやって他のユーザーに漏れてしまうのかを調べてみました。

まず、sshpass にパスワードを渡す方法はヘルプで確認することができます。
$ sshpass -h
Usage: sshpass [-f|-d|-p|-e] [-hV] command parameters
   -f filename   Take password to use from file
   -d number     Use number as file descriptor for getting password
   -p password   Provide password as argument (security unwise)
   -e            Password is passed as env-var "SSHPASS"
   With no parameters - password will be taken from stdin
... snip ...

こんな感じです。

  • -f オプション: ファイルにパスワードを書いて、ファイル名を sshpass に知らせる
  • -d オプション: ファイルディスクリプタ経由
  • -p オプション: パスワードをベタ書き
  • -e オプション: SSHPASS 環境変数を使う
  • オプション無し: 標準入力からパスワードを流し込む

それぞれについて調べてみました。

-p オプションの場合

一般にコマンドラインにパスワードをベタ書きするのが最も漏れやすいと言われている (ような気がします。)

まずは sshpass を実行します。

$ sshpass -p hogehoge ssh worker@RemoteServer sleep 300

ローカルサーバー側 (sshpass を実行した側) で ps コマンドを打つと

$ ps auxwww
... snip ...
worker   26378  0.0  0.0  12764   748 pts/7    S+   13:19   0:00 sshpass -p zzzzzz ssh worker@RemoteServer sleep 300
... snip ...

パスワードを覗き見できてしまいま。。。せん。。。

最近は zzzzzz というようにパスワードをマスクして見えなくしてくれるようです。

しかし、マスクしてくれないケースもあるかと思われますし、ps コマンドで見れてしまうということは良く言われているので -p オプションは避けるべきです。

-f オプションの場合

ファイルにパスワードを書いて -f オプションでファイル名を指定します。

ファイルへのアクセス権を制限して意図しないユーザーがファイルに書いたパスワードを読めないようにする必要があります。

ファイルにパスワードを書くのがなんとなく好きでないので私は -f も使いません。

-e オプションの場合

SSHPASS 環境変数にパスワードを設定します。

$ export SSHPASS='hogehoge'
$ sshpass -e ssh worker@RemoteServer sleep 300

export はシェルの組み込みコマンドなので ps の出力結果には export SSHPASS=’hogehoge’ は出てこないようです。

しかし、ps コマンドで実行中のプロセスの環境変数も表示させることができます。

$ ps auxwwwe
... snip ...
worker    1529  0.0  0.0  12764   744 pts/7    S+   20:03   0:00 sshpass -e ssh worker@RemoteServer sleep 300 LESS_TERMCAP_mb=?[01;31m X
..............................................................................
........................ SSHPASS=hogehoge ....................................
..............................................................................
... snip ...

実際にやってみると少しわかりにくいですが、たくさん環境変数が表示されてその中に SSHPASS もあります。

オプション無しの場合

標準入力からパスワードを流し込むとのことで、パスワードが他者に漏れそうな要因が思い浮かびません。 (コアダンプとられたら漏れるのかな?)

以下、Python のサンプルコードです。 (RemoteServer で hostname コマンドを実行します)

subprocess.check_output() の例

#!/usr/bin/env python3

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

bOutput = subprocess.check_output(
    ['sshpass', 'ssh', 'worker@RemoteServer', 'hostname'],
    stdin=iPipe_r)

print(bOutput.decode())

subprocess.Popen() の例

#!/usr/bin/env python3

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

oSub = subprocess.Popen(
    ['sshpass', 'ssh', 'worker@RemoteServer', 'hostname'],
    stdout=subprocess.PIPE, stdin=iPipe_r)

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())

-d オプションの場合

オプション無しの場合と同様に、パスワードが他者に漏れそうな要因が思い浮かびません。

以下、Python のサンプルコードです。 (RemoteServer で hostname コマンドを実行します)

subprocess.check_output() の例

#!/usr/bin/env python3

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

bOutput = subprocess.check_output(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'worker@RemoteServer', 'hostname'],
    pass_fds=(iPipe_r,))

print(bOutput.decode())

subprocess.Popen() の例

#!/usr/bin/env python3

import getpass
import os
import subprocess

iPipe_r, iPipe_w = os.pipe()

oSub = subprocess.Popen(
    ['sshpass', '-d', str(iPipe_r), 'ssh', 'worker@RemoteServer', 'hostname'],
    stdout=subprocess.PIPE, pass_fds=(iPipe_r,))

sPasswd = getpass.getpass()
os.write(iPipe_w, '{}\n'.format(sPasswd).encode())

print(oSub.stdout.read().decode())

-d オプションでパスワードを渡す方法がわからず、かなりはまりました。

結論としては subprocess.chck_output(), subprocess.Popen() の引数に pass_fds=(iPipe_r,) を追加すれば良いのですが、ここまでたどり着くのにいろいろ調べました。

おかげでファイルディスクリプタを子プロセスに引き継ぐ場合の挙動に少し詳しくなることができました。

忘れないように次回の記事に詳しく書いとく予定です。

参考: