パスワード
シェルスクリプト
暗号化
埋め込み
安全

シェルスクリプトの中に安全にパスワードを埋め込む

シェルスクリプトの中でcurlやsshなどのコマンドにパスワードを渡したい場合に平文のテキストで書き込むよりは安全にパスワードを埋め込む方法を考えてみた。
コンセプトとしてはシェルスクリプト中にはシステム固有の情報で暗号化したパスワードを埋め込んで、シェルスクリプトが流出しても、システム固有の情報によってしか復号出来ない様にすることです。 (きっと、この方法でパスワードを扱えば、シェルスクリプトを間違えてgitなどのリポジトリに上げてしまっても安心?)

ちなみにパスワードの暗号化と復号にはopensslの暗号化(encオプション)を使い、パスフレーズにはシステム固有のUUID(macOSではHardware UUID、Linuxではsystem-uuid)を使っています。
※ Linuxでsystem-uuidを取得するのに利用しているdmidecodeはroot権限でしか実行できないので、/etc/sudersなどでスクリプトを実行するユーザーに/usr/sbin/dmidecodeの実行権限を与えておくひつようもあります。

/etc/sudoers
somebody    ALL=(ALL)   NOPASSWD:   /usr/sbin/dmidecode
encrypt_password.sh
#!/bin/sh
function encrypt_password() {
  plain_password=$1
  if [ "$(uname)" == 'Darwin' ]; then
    system_uuid=`system_profiler SPHardwareDataType |grep "Hardware UUID: [0-9A-Z]\{8\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{12\}" | sed -e 's/^.*Hardware UUID: \([0-9A-Z]\{8\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{12\}\).*$/\1/g'`
  elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then
    system_uuid=`sudo /usr/sbin/dmidecode -s system-uuid`
  else
    echo Unsupported system.
    exit
  fi
  encrypted_password=`echo "$plain_password" | openssl enc -e -des -base64 -k "$system_uuid"`
  echo $encrypted_password
}

# Usage
echo `encrypt_password $1`

上記のスクリプトを使って暗号化した文字列をシェルスクリプト内に埋め込んで使えば、少なくとも何らかの手違いでシェルスクリプトが外部に流出したとしても暗号化されたパスワードはそのシェルスクリプトが実行されるべきシステム以外では復号できないので安全なはず。 (まあ、システムをクラッキングされてシステムのUUIDをクラッカーに知られてしまえば終わりだけど…)

パスワードを暗号化する
$ encrypt_password.sh mypassword
U2FsdGVkX18m7HJ3Fbn0M3xc2+xG25DRM2b2zOhFUiA=
decrypt_password.sh
#!/bin/sh

function decrypt_password() {
  encrypted_password=$1
  if [ "$(uname)" == 'Darwin' ]; then
    system_uuid=`system_profiler SPHardwareDataType |grep "Hardware UUID: [0-9A-Z]\{8\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{12\}" | sed -e 's/^.*Hardware UUID: \([0-9A-Z]\{8\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{4\}-[0-9A-Z]\{12\}\).*$/\1/g'`
  elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then
    system_uuid=`sudo /usr/sbin/dmidecode -s system-uuid`
  else
    echo Unsupported system.
    exit
  fi
  plain_password=`echo "$encrypted_password" | openssl enc -d -des -base64 -k "$system_uuid"`
  echo $plain_password
}

# Useage:
# echo `decrypt_password $1`

decrypt_password.shの最後の行のコメントアウトを外せば引数に暗号化されたパスワードを渡して試せます。

パスワードを復号する
$ decrypt_password.sh U2FsdGVkX18m7HJ3Fbn0M3xc2+xG25DRM2b2zOhFUiA=
mypassword

例えば「bitbucketのリポジトリのバックアップを取得するスクリプトを書 いてみた」のスクリプトで使うならこんな感じでしょうか。

mirror_bitbucket_repos.sh
#!/bin/bash

SCRIPT_DIR=$(cd $(dirname $0); pwd)

. $SCRIPT_DIR/decrypt_password.sh

BITBUCKET_USER=bitbucket_user@email.address
BITBUCKET_PASSWORD=`decrypt_password U2FsdGVkX18m7HJ3Fbn0M3xc2+xG25DRM2b2zOhFUiA=`
BITBUCKET_OWNER=ownername

bitbucket_repositories=`curl --user $BITBUCKET_USER:$BITBUCKET_PASSWORD https://api.bitbucket.org/2.0/repositories/$BITBUCKET_OWNER | jq -r '.values[].links.clone[].href' | grep "^git"`

for repository_path in $bitbucket_repositories; do
  echo $repository_path
  dir=`echo "$(dirname $repository_path)" | sed -e 's/:/\//g' | sed -e 's/^git@//g'`
  if [ ! -d "$dir" ]; then
    mkdir -p "$dir"
  fi
  repo=$(basename $repository_path)
  if [ ! -d "$dir/$repo" ]; then
    (cd $dir; git clone --mirror $repository_path)
  else
    (cd "$dir/$repo"; git fetch --all)
  fi
done

まあ、いろいろと突っ込みどころもあるとは思いますが、パスワードを生のまま埋め込むよりは精神衛生上よろしいかと…

ただ、この方法の欠点としてはスクリプトは特定のシステムでしか使えないので、複数のシステムが連携して動作するシステムでは使えないのでCIシステムでのデプロイやChefやAnsibleでの設定の時も何らかの工夫が必要となるところでしょうか?

公開鍵暗号を使えばいいのかな?でも、その場合には秘密鍵はどうしよう…