Linux(RHEL、CentOS、Fedora)におけるクライアント鍵を用いたAWS EC2 EBSディスクのLUKS(cryptsetup)クライアントサイド暗号化・復号を行うシェルスクリプト(バッチ処理プログラム) – AWS KMSとLUKSを併用する二重暗号化の実現方法 ~金融系、流通系などにおける独自暗号化によるセキュリティ対策・個人情報保護~

5月 10, 2017AWS,AWS CLI,EC2,KMS,Programming,Shell Script

最近ではパブリッククラウドはシステムソリューションの選択肢の一つとして一般的になり、多方面で当たり前のように使われるようになってきました。

AWS、GCP、Azureの三大クラウドにおいては厳格な監査の上で信頼性の高いセキュリティコンプライアンス規制や監査規格の世界的な認証・フレームワークを取得しているため、今回紹介するクライアントサイドのディスク暗号化を用いなくても非常に高いセキュリティレベルで運用されています。

特にAWSにおいてはAWS KMSという鍵管理サービスを用いることでクライアントサイドのキーマテリアルから生成したカスタマーマスターキー(CMK)を使用した暗号化ソリューションをEBSを始めとする各サービスで利用することもできます。

しかしながら、金融系、流通系など重要情報や個人情報を扱う業界では一部の企業がパブリッククラウドを活用し始めているものの、情報システムのデータをパブリッククラウドに保存することは未だに心理的ハードルが高いようです。

今回はそのようなクライアント側からの堅牢な暗号化を必要とする情報システムでAWSをさらにセキュアに使用する一つの案として、LinuxサーバにおけるLUKS暗号化の方法を記載しておきます。

※LUKSはAWS KMSの暗号化とは違うレイヤーでの暗号化を行うため、AWS KMSとLUKSを併用することで二重暗号化が実現できます。

Linux(RHEL、CentOS、Fedora)におけるクライアント鍵を用いたAWS EC2 EBSディスクのLUKS(cryptsetup)クライアントサイド暗号化・復号を行うシェルスクリプト(バッチ処理プログラム) – AWS KMSとLUKSを併用する二重暗号化の実現方法 ~金融系、流通系などにおける独自暗号化によるセキュリティ対策・個人情報保護~

基本方針としては下記の3つに分けてシェルスクリプトを用意して、クライアントサイドの秘密鍵によるディスクパーティションの暗号化・復号の運用ができるようにしています。

  • 秘密鍵の生成とLUKSによる最初の暗号化を行うシェルスクリプト
  • LUKSによる最初の復号、パーティションのフォーマット、マウントを行うシェルスクリプト
  • LUKSによる暗号化ディスクをインスタンス起動時にリモートサーバからSSH経由で復号するシェルスクリプト

特に「LUKSによる暗号化ディスクをインスタンス起動時にリモートサーバからSSH経由で復号するシェルスクリプト」についてはオンプレミス環境のバッチサーバからSSH経由でLUKS暗号化ディスクを使用する対象サーバに秘密鍵を転送し、暗号化ディスクを復号・マウント後、秘密鍵を対象サーバから削除する運用を想定しています。

この運用により、オンプレミス環境からのバッチ処理が行われないとLUKS暗号化ディスクは復号できないため、EBSディスクのセキュリティレベルをより一層高くすることができます。

さらに冒頭でも触れましたがAWS KMSで暗号化したEBSに対してもLUKS暗号化は適用できるため、AWS KMSとLUKSの二重暗号化が可能です。

余談ですが、AWS KMSもクライアント側で用意したキーマテリアルを使用してカスタマーマスターキー(CMK)を生成できます(ただし、カスタマーマスターキーはオンプレミス環境で保存するわけではなくAWS KMSに保存される)。

そのため、「カスタマーマスターキーを用いたAWS KMSによるEBS暗号化」と「オンプレミス環境に秘密鍵を保存するLUKSディスク暗号化」を併用する方法が低コストで実現できるEBS暗号化のセキュリティベストプラクティスではないかと個人的に考えています。

秘密鍵の生成とLUKSによる最初の暗号化を行うシェルスクリプト

まず、LUKSによる暗号化・復号を操作するコマンドcryptsetupをインストールする必要があります。
CentOS7では下記のコマンドでインストールできます。

[magtranetwork@localhost ~]# yum install -y cryptsetup cryptsetup-libs

次に一番最初に暗号化用の秘密鍵を生成し、アタッチしたEBSディスクのパーティションを暗号化フォーマットするシェルスクリプトを作成します。

下記のシェルスクリプトにあるVOL_DEV、KEY_DIR、KEY_NAMEなどの値は自分の環境に併せて変更して下さい。

[magtranetwork@localhost ~]# vim luks_format_by_org_key.sh
#!/bin/bash

VOL_DEV=/dev/xvdf
KEY_DIR=~/.luks
KEY_NAME=luks.pem
LUKS_KEY_PATH=${KEY_DIR}/${KEY_NAME}

#パーティションをランダムなデータで埋めて難読化する
#t2.smallでEBS100GBが約40分程度の時間がかかる
shred -v --iterations=1 ${VOL_DEV}

#作成した鍵を保存するディレクトリを作成
mkdir -p ${KEY_DIR}

#openssl genrsaコマンドでLUKSの暗号化・復号に使用する秘密鍵を生成
openssl genrsa 2024 > ${LUKS_KEY_PATH}

#cryptsetupで生成した秘密鍵を用いてluksによる暗号化フォーマットを行う。
cryptsetup luksFormat --key-file=${LUKS_KEY_PATH} ${VOL_DEV}

暗号化対象のEBSをアタッチした状態で作成したluks_format_by_org_key.shを実行すると下記のように出力されます。
途中で本当に暗号化するかの確認を要求されますので大文字で「YES」を入力して暗号化フォーマットを完了させます。

[magtranetwork@localhost ~]# chmod 755 luks_format_by_org_key.sh
[magtranetwork@localhost ~]# ./luks_format_by_org_key.sh
shred: /dev/xvdf: 経過 1/1 (random)...
shred: /dev/xvdf: 経過 1/1 (random)...197MiB/10GiB 1%
~省略~
shred: /dev/xvdf: 経過 1/1 (random)...10GiB/10GiB 100%
Generating RSA private key, 2024 bit long modulus
........+++
.................................................................+++
e is 65537 (0x10001)

WARNING!
========
This will overwrite data on /dev/xvdf irrevocably.

Are you sure? (Type uppercase yes): YES ←大文字でYESを入力し、luksFormatを完了させる。

LUKSによる最初の復号、パーティションのフォーマット、マウントを行うシェルスクリプト

luks_format_by_org_key.shによる最初の暗号化が完了したら、次に最初の復号とパーティションのディスクフォーマット、ディレクトリへのマウントを下記のシェルスクリプトで行います。

下記のシェルスクリプトにあるVOL_DEV、KEY_DIR、KEY_NAME、MAPPER_NAME、MOUNT_PATHなどの値は自分の環境に併せて変更して下さい。

[magtranetwork@localhost ~]# vim luks_open_first_time_by_org_key.sh
#!/bin/bash

VOL_DEV=/dev/xvdf
KEY_DIR=~/.luks
KEY_NAME=luks.pem
LUKS_KEY_PATH=${KEY_DIR}/${KEY_NAME}
MAPPER_NAME=luks
MOUNT_PATH=/mnt/luks_disk

#cryptsetupコマンドで秘密鍵を用いてパーティションを復号オープンする。
cryptsetup luksOpen ${VOL_DEV} ${MAPPER_NAME} --key-file=${LUKS_KEY_PATH}

#復号したパーティションのディスクフォーマットを行う。
mkfs.xfs /dev/mapper/${MAPPER_NAME}

#マウントするディレクトリの作成
mkdir -p ${MOUNT_PATH}

#フォーマットした復号パーティションをディレクトリにマウントする。
mount /dev/mapper/${MAPPER_NAME} ${MOUNT_PATH}

実際にluks_open_first_time_by_org_key.shを実行するとLUKSの復号、ディスクフォーマット、ディレクトリへのマウントが行われます。

[magtranetwork@localhost ~]# chmod 755 luks_open_first_time_by_org_key.sh
[magtranetwork@localhost ~]# ./luks_open_first_time_by_org_key.sh

LUKSによる暗号化ディスクをインスタンス起動時にリモートサーバからSSH経由で復号するシェルスクリプト

このシェルスクリプトはLUKSによる暗号化ディスクを持つEC2インスタンスにSSH経由でログインできるオンプレミス環境のバッチサーバで実行することを想定しています。

ただ、SSHログインができればEC2の他のインスタンスやLUKSによる暗号化ディスクを持つEC2インスタンスそのものからも実行することは可能です。

このシェルスクリプトではリモートサーバからSSH経由でsudoコマンドを実行するため、/etc/sudoersを下記の例のように変更する必要があります。

#■sudoersの例:sshでログインするユーザがbatch-userの場合

#requirettyの無効化
#Defaults    requiretty

#batch-userのrootコマンド実行許可
batch-user    ALL=(ALL)       ALL

#batch-userのパスワード無しでの実行許可
%batch-user        ALL=(ALL)       NOPASSWD: ALL

check_ins_and_open_luks_via_ssh.shの実装の内容はまず、AWS CLIでインスタンスの状態、システムステータス、インスタンスステータスを取得し、全てが正常であればSSHを施行し、SSHログインが可能であることを確認します。
そして、秘密鍵をオンプレミス環境からEC2インスタンスへ転送してLUKSの復号、ディレクトリへのマウントを行い、秘密鍵を削除します。

下記のシェルスクリプトにあるINS_IP、SSH_USER、SSH_KEY_PATH、VOL_DEV、KEY_DIR、KEY_NAME、MAPPER_NAME、MOUNT_PATHなどの値は自分の環境に併せて変更して下さい。

[magtranetwork@localhost ~]# vim check_ins_and_open_luks_via_ssh.sh
#!/bin/bash

INS_IP=10.0.0.5
SSH_USER=batch-user
SSH_KEY_PATH=~/.ssh/id_rsa
VOL_DEV=/dev/xvdf
KEY_DIR=/home/${SSH_USER}/.luks
KEY_NAME=luks.pem
LUKS_KEY_PATH=${KEY_DIR}/${KEY_NAME}
MAPPER_NAME=luks
MOUNT_PATH=/mnt/luks_disk

#AWS CLIでインスタンスIDを取得する。
INS_ID=`aws --output text ec2 describe-instances --filter "Name=private-ip-address,Values=${INS_IP}" --query 'Reservations[].Instances[].InstanceId'`

#AWS CLIでインスタンスの状態を取得する。
STATE=`aws --output text ec2 describe-instances --instance-ids ${INS_ID} --query 'Reservations[].Instances[].State[].Name'`

#AWS CLIでシステムステータスを取得する。
SYSTEM_STATUS=`aws --output text ec2 describe-instance-status --instance-ids ${INS_ID} --query 'InstanceStatuses[].SystemStatus[].Details[].Status'`

#AWS CLIでインスタンスステータスを取得する。
INSTANCE_STATUS=`aws --output text ec2 describe-instance-status --instance-ids ${INS_ID} --query 'InstanceStatuses[].InstanceStatus[].Details[].Status'`

#インスタンスの状態がrunning、システムステータスがpassed、インスタンスステータスがpassedとなった場合にsshの状態を確認し、接続>を試みる。
if [ "${STATE}" = "running" -a "${SYSTEM_STATUS}" = "passed" -a "${INSTANCE_STATUS}" = "passed" ]; then

  CAN_SSH=1
  #sshコマンドが実行できるかを試してコマンドステータスを確認する。
  ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "uname -a"
  RES=$?
  if [ "${RES}" != "0" ]; then
    CAN_SSH=0
  fi

  #ssh接続が正常にできる場合に秘密鍵を転送し、LUKSパーティションの復号とマウントを行う。
  if [ "${CAN_SSH}" = "1" ]; then
    #LUKSの復号がすでに行われているかどうかを確認。
    IS_LUKS=`ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "ls -1 /dev/mapper/ | grep ^${MAPPER_NAME}\$ | wc -l"`

    #LUKSの復号が行われていなければ、秘密鍵を転送して復号する。
    if [ "${IS_LUKS}" = "0" ]; then
      ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "mkdir -p ${KEY_DIR}"
      scp -i ${LUKS_KEY_PATH} ${LUKS_KEY_PATH} ${SSH_USER}@${INS_IP}:${LUKS_KEY_PATH}
      ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "sudo cryptsetup luksOpen ${VOL_DEV} ${MAPPER_NAME} --key-file=${LUKS_KEY_PATH}"
    fi

    #複合したLUKSのパーティションがマウントされているかを確認。
    IS_MOUNT=`ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "df | grep ^/dev/mapper/${MAPPER_NAME} | grep ${MOUNT_PATH}\$ | wc -l"`

    #複合したLUKSのパーティションがマウントされていなければ、ディレクトリにマウントする。
    if [ "${IS_MOUNT}" = "0" ]; then
      ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "sudo mkdir -p ${MOUNT_PATH}"
      ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "sudo mount /dev/mapper/${MAPPER_NAME} ${MOUNT_PATH}"
    fi

    #復号、マウントの作業に関わらず、リモートに転送された秘密鍵を削除する。
    ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${INS_IP} "rm -f ${LUKS_KEY_PATH}"
  else
    #SSHサービスが立ち上がっていなければ標準出力。
    echo "SSH Service is NOT Available yet."
  fi
fi

実際にcheck_ins_and_open_luks_via_ssh.shを実行するとEC2インスタンスの3つのチェック項目の確認とSSH接続によるLUKS復号処理、マウントが行われます。

実際の運用を考えると、バッチサーバでこのシェルスクリプトをバッチ処理プログラムとして数分毎にスケジュール実行し、EC2インスタンスの再起動に備えるのが良いでしょう。

[magtranetwork@localhost ~]# chmod 755 check_ins_and_open_luks_via_ssh.sh
[magtranetwork@localhost ~]# ./check_ins_and_open_luks_via_ssh.sh
Reference: Tech Blog citing related sources