kustomize で環境変数から厳密に Secret を生成するのが意外と面倒な話

# 注意
kustomize 2.0.0からsecretGeneratorのcommandsはセキュリティ上の理由から廃止されました。そのため以下で紹介している方法は現在は使用できません。

https://github.com/kubernetes-sigs/kustomize/blob/master/docs/version2.0.0.md#secretgeneratorcommands

## やりたいこと

* [kustomize](https://github.com/kubernetes-sigs/kustomize) の `secretGenerator` を使って、環境変数から厳密に Secret を生成したい
  * スペースや改行も含めてバイトレベルで同一の値を持たせて生成したい
  * 環境変数が存在しないときは `kustomize build` をエラーにさせたい

例えば CircleCI では環境変数として秘密情報を持たせることができます。
この秘密情報を露出しないように Secret を生成する方法として、`secretGenerator` を使おうというものです。

なお、kustomize はこの記事を書いている時点で最新の 1.0.8 を使用します。

## 結論

これでうまくできます。
多分。
問題になるケースがあれば教えてください。

```yaml
secretGenerator:
- name: foo-secret
  commands:
    SECRET_JSON: bash -euc 'echo -n "$JSON"'
```

## 検証手順

いくつかの `kustomization.yaml` を用意して、それぞれ別々の `secretGenerator` を定義します。
これを検証用のスクリプトから実行し、結果を入力の環境変数と比較して値の同一性を比較します。

検証用のコードは GitHub に置いています。
https://github.com/yuya-takeyama/kustomize-secret-generator-env-problem
検証スクリプトの本体はこちら
https://github.com/yuya-takeyama/kustomize-secret-generator-env-problem/blob/master/bin/test

引数として `kustomization.yaml` が存在する overlay ディレクトリのパスとして渡して実行することで、検証を行えます。

git clone してそのまま実行することもできますし、Dockerfile も同梱しているので Linux (Alpine Linux) 環境での検証も行えます。

```
$ docker run -it yuyat/kustomize-secret-generator-env-problem
```

この記事中では macOS Sierra での実行結果を中心に記載しつつ、必要に応じて Linux での実行結果についても言及します。

### 1. echo

まずは素朴に `echo` でやってみましょう。

実行すると以下が出力されます。

* `kustomization.yaml` の中身
* 入力として渡した環境変数 (Expected)
  * 様々な問題をあぶり出すために、文字列中に改行コードを含み、改行と空白でインデントされた JSON を渡しています
  * 改行コード等も含めて違いがわかりやすいよう、Ruby で inspect した結果を出力しています
* 生成された Secret から取り出した値 (Actual)
  * Secret には Base64 エンコードした値が保存されるので、それを取り出してデコードした値です
  * こちらも inspect した結果です
* Expected と Actual を比較した結果 (OK or NG)

```
$ ./bin/test 01-echo
kustomization.yaml:
secretGenerator:
- name: foo-secret
  commands:
    SECRET_JSON: echo $JSON

Expected:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}"

Actual:
"{ \"FOO\": \"FOO BAR\nBAZ\" }\n"

NG
```

初歩的な問題として、`$JSON` をダブルクォートで囲んでいないので、半角スペースが一部消えてしまっています。
またよく見ると、JSON 中のエスケープされた改行コードであった `\n` が本物の改行コードに変わってしまっていますし、末尾にも改行が追加されてしまっていることがわかります。

### 2. ダブルクォートを使って echo

変数をきちんとダブルクォートで囲んだ上で `echo` してみます。

```
$ ./bin/test 02-echo-with-quotes
kustomization.yaml:
secretGenerator:
- name: foo-secret
  commands:
    SECRET_JSON: echo "$JSON"

Expected:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}"

Actual:
"{\n  \"FOO\": \"FOO    BAR\nBAZ\"\n}\n"

NG
```

空白が一部消えてしまう問題は解消されました。
しかし改行コード関連の問題は相変わらずです。

### 3. printenv

`echo` ではなく `printenv` を使ってみます。

```
$ ./bin/test 03-printenv
kustomization.yaml:
secretGenerator:
- name: foo-secret
  commands:
    SECRET_JSON: printenv JSON

Expected:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}"

Actual:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}\n"

NG
```

かなり正解に近づきましたが、末尾に改行が追加されてしまっています。

### 4. gprintenv -0

macOS の `printenv` は BSD のものですが、[GNU Coreutils](http://www.gnu.org/software/coreutils/coreutils.html) に含まれる GNU printenv (`gprintenv`) も試してみましょう。
こちらには BSD 版にはない `-0` というオプションが含まれています。
(Linux では `printenv` として呼び出す必要がありますが、まぁそこは alias か何かで解決することにしましょう)

```
$ ./bin/test 04-gprintenv-0
kustomization.yaml:
secretGenerator:
- name: foo-secret
  commands:
    SECRET_JSON: gprintenv -0 JSON

Expected:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}"

Actual:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}\x00"

NG
```

今度は末尾にヌルバイトが含まれてしまいました。
`man gprintenv` には以下のように記載されています。

```
       -0, --null
              end each output line with NUL, not newline
```

JSON としては同一ですが、バイトレベルでの同一性を保証したいので、やはりこれも NG です。
これではパスワード等の文字列を渡したい時には問題になるかもしれません。

### bash から echo -n を呼ぶ

`printenv` はうまく行かなそうなので、echo をもっと工夫して呼んでみることにします。

```
$ ./bin/test 05-bash-echo
kustomization.yaml:
secretGenerator:
- name: foo-secret
  commands:
    SECRET_JSON: bash -euc 'echo -n "$JSON"'

Expected:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}"

Actual:
"{\n  \"FOO\": \"FOO    BAR\\nBAZ\"\n}"

OK
```

うまくいきました！
これは Alpine Linux 上で実行してみても同じ結果が得られます。

ここからは各オプションの詳細をみていきます。

#### echo -n

これは末尾に改行が追加されるのを防ぐためのものです。
`man echo` によると以下のようにあります。

```
     -n    Do not print the trailing newline character.
```

注意しないといけないのは、`echo` には `/bin/echo` の他に、シェルによってはビルトインコマンドもあり、挙動が異なることです。
手元の macOS 上の Bash 3.2.57 の `echo` は Bash ビルトインのコマンドですが、sh の echo は `/bin/echo` で、こちらには `-n` オプションが存在しません。

ちなみにエスケープされた改行コードが改行に変換されてしまうのは `sh` のせいで、Bash ビルトインの `echo` ではこの問題もありません。
(Alpine Linux ではデフォルトのシェルが ash で、ash の echo も同様に問題ないようです)

#### bash -u

これは環境変数が存在しない時にエラーとするためのものです。
試しに検証スクリプトを通さずに `kustomize build` すると以下のようにエラーになることがわかります。

```
$ kustomize build 05-bash-echo
Error: NewResMapFromSecretArgs: makeSecret: commands map[SECRET_JSON:bash -euc 'echo -n "$JSON"']: exit status 1
```

#### bash -e

これは Bash スクリプト中でコマンドがエラーになったらその時点でエラーとしてスクリプトの実行を中止するためのオプションです。
今回の場合はあってもなくても特に変わりませんが、まぁあった方が厳密な感じはするので...