JS7® JobScheduler - HashiCorp Vaultを利用したSecret対応処理

# 概要

JobSchedulerのWorkflowでは、実行内容に応じて、ID/Password、API Key、秘密鍵 など、機密情報を取り扱うことも多くあるかと思います。
そこでsecret管理の一例として、Hashicorp Vault を使った対応手段をご紹介します。
（認証連携ではなく、機密情報管理にVaultを利用した事例です）

# 前提条件

- JS7 jobschedulerの基本操作を習熟していること。※Workflow設計や変数利用の知識が必要です。
まだの方はこちらから  [JS7® JobScheduler 関連記事の目次](https://qiita.com/saitamanokusa/items/645021798f5d17a79baf)
- 本記事はJobScheduler Ver. 2.5.1 で確認した内容です。
今後内容が変わる可能性があります。
- HashiCorp Vault が別途稼働しており、agentではvault、aws、jq コマンドが利用できるものとします。
※本記事ではVaultの細かな操作方法についての説明は行いません。

docker(alpine)環境を利用している場合は、下記操作で各コマンドを利用できます。

```shell
apk add --no-cache vault libcap aws-cli jq
setcap cap_ipc_lock=/usr/sbin/vault
```

# 処理の流れ

本記事では、下記のWorkflowを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529537/f89e2a38-ad43-8360-2846-2819753710dd.png)

- JobResource
  - JS7_VAULT_CONNECT
    vault のhttp(s)接続情報および認証情報を定義
- Workflow（のシェルJob）
  - vault_auth
    Vault認証を行い、認証Tokenを後続処理に渡す
  - kv_job
    認証Tokenを用いて、Vault KV Secret (ID/Passwordなど)を読み取り、処理を実行
  - vault_get_aws_secret
    認証Tokenを用いて、Vault AWS Secret (一時IAM AccessKey) を取得し、後続処理に渡す
  - aws_job
    一時IAM AccessKeyを用いて、aws関連処理を実行
  - vault_revoke_lease
    Vault AWS Secret(一時IAM AccessKey)の破棄
  - vault_revoke_token
    Vault 認証Tokenの破棄

:::note warn
※JobResourceの情報は、controller, agentのジャーナルに平文で保存されます。つまり定義した認証情報は、controller,agentにて閲覧できます。
[【参考】Workflow実行時のデータの流れ](https://qiita.com/saitamanokusa/items/ee7196cdcf080974bd83#workflow%E5%AE%9F%E8%A1%8C%E6%99%82%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%B5%81%E3%82%8C)

本記事では、userpass認証を例に取り上げますが、より安全な利用を目指す場合は、Vaultの認証情報が不要となる手段（AWS Auth など）の使用をお勧めします。
:::

また、本記事ではシンプルにagent1台構成とします。
（認証以降の後続処理を別agentで実行することも可能です）

# Vault 環境準備

vaultに、本記事で利用するkv, aws secret および userpass認証の設定を追加します。

```shell
# kv (version 1) の設定およびPolicy
vault secrets enable -path=kv/myapp -description="kv for myapp" kv

vault kv put kv/myapp/system -<<EOF
{
  "hostname": "system",
  "ipaddress": "192.168.0.1",
  "login_user": "user01",
  "login_password": "P@ssw0rd"
}
EOF

vault kv get kv/myapp/system

vault policy write kv_myapp_policy -<<EOF
# kv/myapp/ 以下のkey-value 管理
path "kv/myapp/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}
EOF

# vault aws secret の設定、Policy追加 ※別途AWS側でPolicy追加等必要
# 詳しくはこちら https://qiita.com/saitamanokusa/items/24888fdcabaa7b153be5#aws
# ※aws の利用の用途がない場合は飛ばしてください
access_key="xxxxxxxxxxx"
secret_key="xxxxxxxxxxx"
region="ap-northeast-1"
vault secrets enable -path=aws/myapp -description="aws for myapp" aws
vault write aws/myapp/config/root access_key="$access_key" secret_key="$secret_key" region=$region
vault write aws/myapp/config/lease lease=15m lease_max=1h
vault write aws/myapp/roles/s3_full credential_type=iam_user policy_arns=arn:aws:iam::aws:policy/AmazonS3FullAccess

vault policy write aws_myapp_policy -<<EOF
# aws/myapp/ 以下の管理
path "aws/myapp/*" {
  capabilities = ["create", "read", "update", "list" , "delete"]
}
path "sys/leases/lookup/aws/myapp/creds/*" {
  capabilities = ["create", "update"]
}
path "sys/leases/revoke/aws/myapp/creds/*" {
  capabilities = [ "read","update"]
}
EOF

# userpass 認証でのログイン設定追加、認証有効期限1h、上記kv, aws 利用Policyが付与されるように
vault auth enable userpass
vault write auth/userpass/users/js7user password=js7password
vault write auth/userpass/users/js7user token_ttl=1h token_max_ttl=1h token_explicit_max_ttl=4h
vault write auth/userpass/users/js7user policies=kv_myapp_policy,aws_myapp_policy
vault read auth/userpass/users/js7user
```

vaultの習得は、チュートリアルが充実していますので、そちらをご参照ください。
https://developer.hashicorp.com/vault/tutorials
主なコマンド操作はこちらにも記載があります。
https://qiita.com/saitamanokusa/items/24888fdcabaa7b153be5

# JobResource作成

Vault接続情報、および認証情報を環境変数を定義します。
Vault接続、認証関連の情報は、各Workflow、Jobで利用するため、JobResourceで定義することにします。

`/Practice` - `Workflows`より、JobResourceを新規作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529537/1bf9b635-ea83-4356-8c62-c5c7a53e8f18.png)

JobResource名 : JS7_VAULT_CONNECT
Arguments : 使用しないため、空のままで
Environment : Variables に下記を設定します。

|Name|Value|
|-|-|
|VAULT_ADDR|https://vault.saitamanokusa.net:8200<br />（vaultのURL指定）|
|VAULT_SKIP_VERIFY|true<br />（vaultでhttps利用時の証明書チェックをスキップ）|
|VAULT_FORMAT|json<br />（vaultコマンドの出力をjson形式に）|
|VAULT_USER_ID|js7user<br />（vault userpass認証を使用する場合）|
|VAULT_PASSWORD|js7password<br />（vault userpass認証を使用する場合）|

:::note warn
※JobResourceの情報は、controller, agentのジャーナルに平文で保存されます。つまりcontroller,agentにて、Vaultの認証情報を閲覧できます。
[【参考】Workflow実行時のデータの流れ](https://qiita.com/saitamanokusa/items/ee7196cdcf080974bd83#workflow%E5%AE%9F%E8%A1%8C%E6%99%82%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%B5%81%E3%82%8C)

より安全な利用を目指す場合は、Vaultの認証情報が不要となる手段（AWS Auth など）の使用をお勧めします。
:::

# workflow 作成

`/Practice` - `Workflows`より、Workflow名 `job_using_vault`を新規作成します。
あとは下記のように設定を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529537/96d02a16-951a-7e69-1ee0-04f4dfa9eef9.png)

## Workflow

JobResource`JS7_VAULT_CONNECT`は各Jobで利用するため、Workflowにて指定します。

|Properties|設定値|
|-|-|
|Name|job_using_vault|
|Title|Vault利用Job|
|JS7_VAULT_CONNECT|`/Practice/JS7_VAULT_CONNECT`|

## Job: vault_auth

|Job Properties|設定値|
|-|-|
|Name|vault_auth|
|Label|vault_auth|
|Agent|primaryAgent|
|Job Class|Shell|
|Script|(下記参照)|

Scriptは下記のように設定します
※Vault認証を行い、認証Tokenを後続処理に渡します。

```shell
#!/usr/bin/env bash
set -euo pipefail

# vault auth token 取得
res=$(vault login -method=userpass -path=userpass username="$VAULT_USER_ID" password="$VAULT_PASSWORD")
[ $? -ne 0 ] && { echo "vaultの応答がありません" >&2 && exit 1; }

vault_token=$(echo "$res" | jq -r '.auth.client_token')
[ ! "$vault_token" ] || [ "$vault_token" = "null" ] && { echo -e "vaultの応答に問題があります\n${res}" >&2 && exit 1; }

# 標準出力でvaultのレスポンスを返す
echo "$res" | jq .

# js7 後続処理向けにorder変数を定義
[ "${JS7_RETURN_VALUES:=}" ] && echo "vault_token=$vault_token" >>"$JS7_RETURN_VALUES"

```

## Job: kv_job

|Job Properties|設定値|
|-|-|
|Name|kv_job|
|(省略)||

Job環境変数で、vault認証Tokenを定義します ※以降の各Jobでも同様

|Environment Variables||
|-|-|
|VAULT_TOKEN|$vault_token|

Scriptは下記のように設定します
※認証Tokenを用いて、Vault KV Secret (ID/Passwordなど)をjson形式で取得し、必要な処理を実行します。

```shell
#!/usr/bin/env bash
set -euo pipefail

res=$(vault kv get kv/myapp/system)
[ $? -ne 0 ] && { echo "vaultの応答がありません" >&2 && exit 1; }

myapp_hostname=$(echo "$res" | jq -r '.data.hostname')
myapp_ipaddress=$(echo "$res" | jq -r '.data.ipaddress')
myapp_user=$(echo "$res" | jq -r '.data.login_user')
myapp_password=$(echo "$res" | jq -r '.data.login_password')

# secret情報を使った処理を実行（処理は任意）
echo "hostname: $myapp_hostname"
echo "ipaddress: $myapp_ipaddress"
echo "user: $myapp_user"
echo "password: $myapp_password"

```

## Job: vault_get_aws_secret

|Job Properties|設定値|
|-|-|
|Name|vault_get_aws_secret|
|(省略)||

|Environment Variables||
|-|-|
|VAULT_TOKEN|$vault_token|

Scriptは下記のように設定します
※認証Tokenを用いて、Vault AWS Secret (s3_full権限をもった一時AccessKey)を取得し、後続処理に渡します。
またAccessKey利用終了後の処理のために、lease_id も後続処理に渡します。

```shell
#!/usr/bin/env bash
set -euo pipefail

vault_path="aws/myapp"
vault_role="s3_full"

# vault secret 取得
res=$(vault read "${vault_path}/creds/${vault_role}")
[ $? -ne 0 ] && { echo "vaultの応答がありません" >&2 && exit 1; }

# 動作確認および js7後続処理用に値のチェック
vault_lease_id=$(echo "$res" | jq -r '.lease_id')
access_key=$(echo "$res" | jq -r '.data.access_key')
secret_key=$(echo "$res" | jq -r '.data.secret_key')
security_token=$(echo "$res" | jq -r '.data.security_token')
[ "$security_token" = "null" ] && security_token=""
# 応答内容に問題がある場合はエラーを返す
[ ! "$vault_lease_id" ] || [ "$vault_lease_id" = "null" ] && { echo -e "vaultの応答に問題があります\n${res}" >&2 && exit 1; }

# 即応答を返すと、取得したAccessKeyで認証エラーが出るため、
# aws認証が通るようになるまで待機
export AWS_ACCESS_KEY_ID="$access_key"
export AWS_SECRET_ACCESS_KEY="$secret_key"
export AWS_SESSION_TOKEN="$security_token"

declare count=0
while ! aws sts get-caller-identity >/dev/null 2>&1; do
    sleep 1
    count=$((count + 1))
    [[ $count -gt 60 ]] && { echo "aws 認証タイムアウト" >&2 && exit 1; }
done

# 認証が不安定であるため追加で5秒待機
sleep 5

# 標準出力でvault のレスポンスを返す
echo "$res" | jq .

# JS7 workflowでの実行の場合は、$JS7_RETURN_VALUES にorder変数をセット
[ "${JS7_RETURN_VALUES:=}" ] && {
    echo "vault_lease_id=$vault_lease_id"
    echo "aws_access_key_id=$access_key"
    echo "aws_secret_access_key=$secret_key"
    echo "aws_session_token=$security_token"
} >>"$JS7_RETURN_VALUES"

```

## Job: aws_job

|Job Properties|設定値|
|-|-|
|Name|aws_job|
|(省略)||

Job環境変数に一時AccessKeyを定義することで、aws-cliで利用できるようにします。
※AWS_SESSION_TOKENは、assume_role認証利用時に使用します。

|Environment Variables||
|-|-|
|AWS_ACCESS_KEY_ID|$aws_access_key_id|
|AWS_SECRET_ACCESS_KEY|$aws_secret_access_key|
|AWS_SESSION_TOKEN|$aws_session_token|

Scriptは下記のように設定します
※（一時AccessKeyを使って）awsコマンドで必要な処理を行います。

```shell
#!/usr/bin/env bash
set -euo pipefail

# aws 関連の処理を実行（処理は任意）
aws sts get-caller-identity
sleep 1
aws s3 ls --output text
sleep 1

```

## Job: vault_revoke_lease

|Job Properties|設定値|
|-|-|
|Name|vault_revoke_lease|
|(省略)||

Job環境変数に`$vault_lease_id`を定義することで、vault lease revoke に使用します。

|Environment Variables||
|-|-|
|VAULT_TOKEN|$vault_token|
|VAULT_LEASE_ID|$vault_lease_id|

Scriptは下記のように設定します
※VAULT_LEASE_IDを指定して、AWS Secret(一時IAM AccessKey)を破棄します。

```shell
#!/usr/bin/env bash
set -euo pipefail

# vault lease revoke処理
if ! vault lease revoke "$VAULT_LEASE_ID"; then
  echo "vaultの応答がありません" >&2
  exit 1
fi

```

## Job: vault_revoke_token

|Job Properties|設定値|
|-|-|
|Name|vault_revoke_token|
|(省略)||

|Environment Variables||
|-|-|
|VAULT_TOKEN|$vault_token|

Scriptは下記のように設定します
※指定した認証Tokenを破棄します。

```shell
#!/usr/bin/env bash
set -euo pipefail

# vault 認証Token破棄
if ! vault token revoke -self; then
  echo "vaultの応答がありません" >&2
  exit 1
fi

```

# 動作チェック

JobResource、WorkflowのDeployをしましたら、実際に実行してみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529537/aadf5523-4db9-0164-a921-151486680f0f.png)

もしエージェントを複数台利用している場合は、2つ目以降の処理を別エージェントにしても、上手く処理が流れることを確認してみてください。
必要な変数値が後続処理に渡されるため、エージェント間をまたいだ処理も可能です。

:::note
認証は常に特定のエージェントで行い、後続の実務処理は用途に応じた環境で行うことも可能。
:::

# 【参考】VaultコマンドのWeb API対応について

vaultはWebAPIにも対応していますので、例えば下記のようにも書き換えられます。
（vaultコマンドを追加しなくとも対応可能）
なおvault認証の処理は、vaultコマンドを使ったほうが楽です。
※認証方式によっては、ややこしくなる

```shell
# vault kv secret
res=$(curl -s -k \
    -H "X-Vault-Request: true" -H "X-Vault-Token: $VAULT_TOKEN" \
    "${VAULT_ADDR}/v1/kv/myapp/system")

# vault aws secret
res=$(curl -s -k \
    -H "X-Vault-Request: true" -H "X-Vault-Token: $VAULT_TOKEN" \
    "${VAULT_ADDR}/v1/${vault_path}/creds/${vault_role}")

# vault lease revoke
curl -s -k -X PUT \
    -H "X-Vault-Request: true" -H "X-Vault-Token: $VAULT_TOKEN" \
    -d '{"sync":false}' \
    "${VAULT_ADDR}/v1/sys/leases/revoke/${VAULT_LEASE_ID}"

# vault token revoke
curl -s -k -X PUT \
    -H "X-Vault-Request: true" -H "X-Vault-Token: $VAULT_TOKEN" \
    "${VAULT_ADDR}/v1/auth/token/revoke-self"

```

# 最後に

今回ご紹介した方法を用いることで、JobScheduler上にも、実行スクリプト上にも、機密情報を極力保持しない運用が可能です。
また機密情報が一か所管理されることで、機密情報の定期更新などの対応も容易となります。
より安全な環境を検討する際の参考にしていただければ幸いです。