シェルスクリプトでパスワードを暗号化して保持する
背景
先日バックアップのスクリプトを書いていたときにバックアップ先サーバのログイン情報の置き方に困ったので、平文で置かなくても良いやりかたを探したところ、下記のページがヒットしました。 tkuchiki.hatenablog.com
(暗号化には全然詳しくないんですが)なんかこいつ弱そうなのでもっとちゃんとしたやり方をするべきなんですが、暗号化でバイナリじゃなくて文字列が出て来るところがシェルスクリプト的に大変扱いやすいので、平文で置いておくよりはマシだろうということで採用しました。
暗号化・復号化
リンク先にあった通りなんですが下記のようにして暗号化、復号化ができます。
# 暗号化 echo "$plain_password" | openssl enc -e -des -base64 -k "$pass_phrase" # 復号化 echo "$encrypted_password" | openssl enc -d -des -base64 -k "$pass_phrase"
パスフレーズをどうするかという話
実際にシェルスクリプトを作って運用するときにはパスフレーズを何にするかが重要です。ジョブ管理サーバからスクリプトを転送して実行するような場合を考えて、
- スクリプトが実行されるサーバに固有の情報で、
- 簡単に変わることがなく、
- 外部から読み取ることができない
という3つを条件として考えます。sshか何かでサーバに入られたらそもそも仕方がないので、とりあえずこれでいいんじゃないでしょうか。
最初に思いついたのがMACアドレスなんですが、同じセグメントに侵入できていれば見えちゃうのでボツでした。。
ホスト名は結構いい線いっていると思うんですが、簡単すぎる。いろいろ調べてたらdmidecodeというコマンドに行き着きました。
system-uuid
というのは使えそうな感じなので、これとdmidecodeから取れる他の値やホスト名とかを組み合わせたら、そこそこ強力なパスフレーズになるのではないでしょうか。
とりあえず現状は以下のようにしています。
#!/bin/bash encrypt() { echo "$1" | openssl enc -e -des -base64 -k "$(hostname)$(dmidecode -s system-uuid)" } decrypt() { echo "$1" | openssl enc -d -des -base64 -k "$(hostname)$(dmidecode -s system-uuid)" }
これで、(脆弱性を突かれない限り)スクリプトを実行したいサーバ以外の場所では見えない形でパスワードを書くことができるはずです。まあ今回の私の場合は公開鍵認証でsftpとかrsyncを使うべき案件っていう気がしてますが。
ただ、暗号の強度がどの程度のものかよく分からないのが不安です。詳しい方いたら教えてもらえると嬉しいです。
おまけ
そもそも、rsync+sshでパスワード入力が必要になるっていうところから調査を始めたのですが、対話の自動化にはsshpassを使いました。
sshpassを使ってパスワード指定のSSH接続を行ってみる(パスワード指定自動ログイン) | レンタルサーバー・自宅サーバー設定・構築のヒント
これがあればexpectを使ってごちゃごちゃやる必要はないかもしれません。
とりあえず以下の使い方ではちゃんと動きました。
sshpass -p $password ssh user@host 'echo "$(hostname) is up"' sshpass -p $password rsync /local/directory/ user@host:/remote/directory/ sshpass -p $password sftp user@host << EOF put /path/to/local/file EOF
シェルスクリプトの設定ファイルをyamlで書く
背景
今までシェルスクリプトの設定ファイルは変数の定義を別ファイルに外出しして、sourceで読んでいました。↓みたいな感じ。
SRC='/path/to/src/directory/' DEST='/path/to/dest/directory/'
#!/bin/bash source variables.conf rsync -av ${SRC} ${DEST}
多分かなり一般的な方法だと思うんだけど、これだと設定に自由度出そうと思うと設定ファイルの書き方が難しくなるし、設定ファイルから任意のコマンドが実行できちゃうしと色々気に入らない点がありました。
最近jq
というコマンドを覚えて、設定をjsonで渡してjqで処理したら設定の自由度も出るし内部でも扱いやすいなと思っていたんですが、jsonは設定ファイルに向いてないのでどうしたものかと。色々探していたらpythonのワンライナーでyaml -> jsonの変換ができることが判明したので試してみました。
jqに関しては以下のサイトがわかりやすいです。
yaml -> jsonの変換は以下のサイトにあったコードを丸パクリしました。
Converting YAML to JSON with Python: <block end> found - Stack Overflow
前提
以下のパッケージがインストールされている必要があります。好きにパッケージを入れられる環境ばかりだと良いんですが。。
yamlで書いた設定ファイルを読んで処理を流す
- name: backup1 src: /path/to/src/dir/ dest: user@host:/path/to/dest/dir/ - name: backup2 src: /another/src/dir/ dest: user@host:/another/dest/dir/
上記のようなyamlをもとにrsyncする処理は以下のように書けます。
#!/bin/bash # yamlで書かれた設定ファイルをもとにjsonファイルを作成 python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=2)' < config.yml > config.json # jsonを読み込んで変数に格納 config=$(jq -Mc '.' config.json) # for文で順次バックアップ処理を実行 num_backup=$(echo "${config}" | jq '. | length') for ((i=0; i < ${num_backup}; i++)); do # パラメータを取り出して変数に格納 name=$(echo "${config}" | jq -r ".[$i].name | select(. != null)") src=$(echo "${config}" | jq -r ".[$i].src | select(. != null)") dest=$(echo "${config}" | jq -r ".[$i].dest | select(. != null)") # パラメータのチェック(ここでは値が入っている確認のみ) if [ -z "${name}" ] || [ -z "${src}" ] || [ -z "${dest}" ]; then continue fi echo "starting ${name}" rsync rsync -av --copy-unsafe-links ${src} ${dest} done
とりあえず以上でyamlの設定ファイルを読んで処理が回せます。。
解説
[ { "dest": "user@host:/path/to/dest/dir/", "src": "/path/to/src/dir/", "name": "backup1" }, { "dest": "user@host:/another/dest/dir/", "src": "/another/src/dir/", "name": "backup2" } ]
連想配列の配列ですね。プログラム的に扱いやすいデータが人に優しく書けることが分かると思います。以下でjsonにしたあとの処理を解説します。
jqを雑に説明
jqコマンドでは、keyを指定して値を取り出します。keyは階層構造になっていて、.
がルートで以降.
で区切りながらkeyを記述していく感じになっています。また、配列に対しては[<index>]
でアクセスできます。
例として、以下のようなjsonを考えます。
{"manager" : {"name" : "太郎", "tel" : "####"}, "members" : [{"name" : "次郎"}, {"name" : "三郎"}]}
- managerのnameが欲しいときには識別子は
.manager.name
となります。 - 2人目のmemberのnameが欲しいときには
.members[1].name
です。
echo '{"manager" : {"name" : "太郎", "tel" : "####"}, "members" : [{"name" : "次郎"}, {"name" : "三郎"}]}' | jq '<識別子>'
とすると確認できます。
スクリプトの中のjqの使い方
以上を踏まえて、上のシェルスクリプトの例を見ていきます。
jq -Mc '.' config.json
とすると、jsonがコンパクトに一行になって標準出力に出てくるので、これを変数に格納します。複数行の文字列ってシェルだとなんか扱いにくいので。- jqのコマンドの中で、オブジェクトをパイプで繋いでstream的な処理が書けます。
. | length
では取り出した配列をlengthに渡して、配列の長さを取ってきています。 .[$i].name | select(. != null)
では、.
で出てくる配列から$i番目の連想配列を取り出し、さらにnameを取ったあとで、それをselectに渡してnullの場合をはじいています。nameが空だと、nullっていう文字列が返ってきちゃうので。
まとめ
シェルスクリプトを書くときに設定ファイルの作りや内部でのパラメータの管理がいまいちやりにくくてストレスなので、yamlでお手軽に設定ファイルを書きつつjqで設定を参照するようにすると捗るんじゃないかというお話でした。
今のところjqみたいに便利にyamlを読む方法がなくてjsonに変換しないといけないのが難点です。何かいいやり方知っている人がいたら教えてもらえるとうれしいです。