HashiCorp Vaultを利用してPostgreSQLのデータを暗号化する

# 概要
HashiCorp Vaultの[Transit Secrets Engine](https://www.vaultproject.io/docs/secrets/transit)を利用して、
PostgreSQLに格納するデータを暗号化して保存するユーザ定義型(Cipher型)を学習のため実装してみました。

・pg_vault_encrypt
https://github.com/ssl-oyamata/pg_vault_encrypt
→ Cipher型を実装してみたGithubレポジトリです（途中です）

# HashiCorp Vault概要
Vaultは機密情報(シークレット)の管理を行うソフトウェアです。
データベースの接続パスワード、クラウドサービスのシークレットキー、証明書などの動的な払い出しとシークレットのライフサイクル管理を行います。
Vaulthへの認証にはトークンやユーザ・パスワードの他にLDAP,GitHubなどの多くのソフトウェアやサービスを利用可能です。
また、アクセスポリシーで要件に合わせた権限制御も可能です。

# Transit Secrets Engine概要
VaultのTransit Secrets Engineはクライアントから送付されたデータの暗号化・復号を行う機能です。
暗号化するデータをクライアントからVaultに送信するとVaultが暗号化し、暗号化データをクライアントに返却します。
暗号化されたデータをVaultに送付するとVaultが復号しクライアントに返却します。

暗号化キーはVaultでセキュアに管理されており、暗号化キーのローテーションやバージョン管理が容易に実現可能になっています。


# Transit Secrets Engineを使ってみる
手元のCentOS Linux release 7.8.2003 (Core)の環境で試してみます。

## Vaultインストール
~~~
# wget https://releases.hashicorp.com/vault/1.6.0/vault_1.6.0_linux_amd64.zip
# unzip vault_1.6.0_linux_amd64.zip -d /usr/local/bin/
# vault version
Vault v1.6.0 (7ce0bd9691998e0443bc77e98b1e2a4ab1e965d4)
~~~

## Vault起動
Vaultをdevモードで起動します。devモードではVaultのデータはメモリ上に保持され、
Vault停止時にデータは失われますので注意して下さい。

~~~
# export VAULT_ADDR="http://127.0.0.1:8200"
# vault server -dev
[省略]
Root Token: s.j0NkNK4zRglchBwvkTuTRVxt
~~~
Root TokenはVaultにログインする際に利用するので、メモしておきます。

## Vaultへのログイン
~~~
# export VAULT_ADDR="http://127.0.0.1:8200"
# vault login
Token (will be hidden): [Root Tokenを入力]
~~~

## Transit Secrets Engineの有効化
Transit Secrets Engineを有効化します。

~~~
# vault secrets enable transit
~~~

## 暗号化キーリングの作成
testという名前で暗号化キーリングを作成します。

~~~
# vault write -f transit/keys/test
~~~

## データ暗号化
Transit Secrets Engineで暗号化するためにはデータをbase64にエンコードする必要があります。

~~~
# vault write transit/encrypt/test \
    plaintext=$(base64 <<< "宇宙人になりたい")
Key            Value
---            -----
ciphertext     vault:v1:HYS1O0IEnf+IiI3WsSGULYXmDrGSxjmaMEKehLJvcSbejve9w58XhWJXHH7MXMAo559DtUc=
key_version    1
~~~

## データ復号化
Vaultから返却された暗号化データを復号します。
Base64で返却されるため、デコードします。

~~~
# vault write transit/decrypt/test \
    ciphertext="vault:v1:HYS1O0IEnf+IiI3WsSGULYXmDrGSxjmaMEKehLJvcSbejve9w58XhWJXHH7MXMAo559DtUc="
Key          Value
---          -----
plaintext    5a6H5a6Z5Lq644Gr44Gq44KK44Gf44GECg==
# base64 --decode <<< "5a6H5a6Z5Lq644Gr44Gq44KK44Gf44GECg=="
宇宙人になりたい
~~~

## キーのローテート
容易にキーのローテートも可能です。

~~~
# vault write -f transit/keys/test/rotate
~~~

キーのローテートを行うと暗号化データの接頭語がvault:v2になりました。

~~~
# vault write transit/encrypt/test \
     plaintext=$(base64 <<< "宇宙人になりたい2")
Key            Value
---            -----
ciphertext     vault:v2:n9krlq4iPxoAlKD894isR7qUbAdiQ/mynJFvEBVxQSUha4f8UHiDp321FuzgMN7AwpaUgTjV
key_version    2
~~~

デフォルトでは暗号化はaes256-gcm96に設定されていました。

~~~
# vault read transit/keys/test
[省略]
type                      aes256-gcm96
~~~

容易に高度なデータ暗号化とキーのローテートが実現できました。


# PostgreSQLのデータ暗号化・復号
VaultのTransit Secrets Engineを利用し、PostgreSQLに格納するテーブルのデータの一部を暗号化したいと考えました。
既存のアプリケーションを修正せずに、PostgreSQLのデータを暗号化したいと考え、ユーザ定義型(cipher型)を作成しました。
cipher型は下記のように動作します。

・データ保存時（入力） : Transit Secrets Engineでデータを暗号化し保存
・データ参照時（出力） : Transit Secrets Engineで暗号化データを復号し返却

## インストール
インストール方法は下記をご参照いただけると幸いです。
https://github.com/ssl-oyamata/pg_vault_encrypt

## Cipher型の動作
Cipher型を利用する場合、pg_vault_encrypt.vault_tokenパラメータなどを設定する必要があります。

~~~
$ psql -d cipherdb
=# SET pg_vault_encrypt.vault_token = 's.v8AYIpHOXGvNSKSpEfcsL1Dk';
SET
=# CREATE TABLE test(id int , data cipher);
=# INSERT INTO test values(1, 'secret data1'); # 暗号化したデータが保存されています。
=# INSERT INTO test values(2, 'secret data2');　# 暗号化したデータが保存されています。
=# SELECT * FROM test;
 id |     data
----+--------------
  1 | secret data1
  2 | secret data2
(2 行)


=# SELECT * FROM test where data = 'secret data2';
 id |     data
----+--------------
  2 | secret data2
(1 行)
~~~

cipher型ではpg_vault_encrypt.vault_tokenパラメータに指定したVaultのトークンが誤っていた場合にはエラーになり、データが参照できません。

~~~
=# SET pg_vault_encrypt.vault_token = 'dummy';
=# SELECT * FROM test where data = 'secret data2';
ERROR:  Cannot decrypt data (vault:v1:tA85X0C42FxEFVhX4VLXG49rfrTpHU4WoXl4fux9/5uJPET3iohAUA==): Error making API request.

URL: PUT https://192.168.0.30:8200/v1/encryption/decrypt/pgtest
Code: 403. Errors:

* permission denied
~~~

## 暗号化の確認
PostgreSQLのテーブルを構成するデータファイルが暗号化されていることを確認します。

### データベースのOIDを確認
pg_stat_databaseビューのdatid(データベースのOID)列で、確認します。

~~~
# psql -d cipherdb
=# SELECT datid, datname FROM pg_stat_database WHERE datname='cipherdb';

  datid  | datname
---------+----------
 1411807 | cipherdb
(1 行)
~~~
テスト用に用意したデータベース(cipherdb)は、$PGDATA/base/1411807ディレクトリに格納されています。

### テーブルのファイル名を確認
pg_classカタログのrelfilenode(リレーションのディスク上のファイルの名前)列で、確認します。

~~~
=# SELECT relname, relfilenode from pg_class WHERE relname='test';
 relname | relfilenode
---------+-------------
 test    |     1411946
(1 行)
~~~

テスト用に用意したデータベース(cipherdb)の、
testテーブルはファイル($PGDATA/base/1411807/1411946)に保存されていることを確認できました。

### 暗号化の確認
testテーブルが保存されているファイル($PGDATA/base/1411807/1411946)をstringsコマンドで参照してみます。

~~~
$ strings $PGDATA/base/1411807/1411946
vault:v1:41AB/8k2yiuth9rHdUAaiqOrqwpngg7CbuC3etKu195yCe3Bo/tOaA==
vault:v1:tA85X0C42FxEFVhX4VLXG49rfrTpHU4WoXl4fux9/5uJPET3iohAUA==
~~~

データが暗号化されていることを確認できました。

補足.
暗号化していないテーブルの場合、以下のように表示されます。

~~~
$ strings $PGDATA/base/1411807/1411949
secret data1
secret data2
~~~

以上です、間違いなどあればコメントください！

# 参考.
https://learn.hashicorp.com/tutorials/vault/eaas-transit