error codes

ソフトウェアエンジニアリングなど、学んだこと、思ったことの記録です。

Kubernetes Mutating Webhook で、動的に秘匿情報を注入する

昨年、後半あたりから仕事でも Kubernetes に触れる機会が増えてきました。Kubernetes には、自らプログラミングすることによって、振る舞いを拡張する手段が用意されており、面白いなと感じています。

そんななかで、Kubernetes で動かすアプリケーションの秘匿情報の管理について、どのようにしようかと少し考えていました。秘匿情報というのは、データベースのパスワードであったり、外部サービスのAPIキーのようなものを想定しています。Kubernetes には、Secrets リソースがあり、シンプルに秘匿情報を扱えますが、いくつかの問題から、実際の運用においては、 Secrets リソースをそのまま使うのではなく、様々なアプローチが取られる場合もあるように思います。

今回は、Admission Webhook そのなかでも Mutating Webhook を用いて、秘匿情報を扱うためツールを書いてみました。 この記事は、二つのトピック、Kubernetes における秘匿情報管理、また、Mutating Webhook でどういったことができるか、を含みます。あまりまとまった内容になっていないと思いますが、参考になればと思います。

Mutating Webhook

Kubernetes には、Admission Webhook というものがあります。 Admission Webhook では、リソース操作のリクエストに対して、Mutation(変更) と Validation(検証) を行うための独自のWebhook を定義することができます。それぞれ、 Mutating Webhook 、 Validating Webhook と呼ばれます。

次の図を見てもらうのが、わかりやすいと思います。

f:id:dai_hi_saru:20200102221901p:plain
https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

この仕組みを用いて、クラスタ管理者は、セキュリティ・ガバナンス・構成管理などにおいて、独自の仕組みを提供することができます。

Istio など Service Mesh フレームワークでは、よく Sidecar Injection の機能があると思いますが、それは Mutating Webhook によって実現されています。 必ずしも自身で Webhook を実装するケースは多くはないと思いますが、このような仕組みがあることを知っておくのは良いと思います。

さて、つぎは、Kubernetes における秘匿情報の様々な管理方法について紹介します。そこでも Mutating Webhook を用いられるケースがあります。

Kubernetes における秘匿情報管理のアプローチ

普通、クラスタに適用するリソースのマニフェストは、 Git でバージョン管理していると思います。 Secrets リソースもマニフェストで定義できますが、その中身は平文であるため、そのままではコミットすることは望ましくありません。

また、GKE や EKS などのマネージドサービスで、クラスタを運用する場合も多いと思いますが、その場合はそれぞれのクラウドプロバイダが提供する暗号化サービスや秘匿情報データベースサービスとうまく連携できると良さそうに感じます。

たとえば、 AWS ECS では、 Parameter Store に保存された秘匿情報をコンテナ実行時に環境変数として引き渡すことができます。このような機能があると、コンテナ側からは、秘匿情報を環境変数としてシンプルに扱える上、管理の上でも、Parameter Store/KMSによる権限管理・監査ログなどの機能が使えます。

Kubernetes 上で秘匿情報を扱うためのツール・コントローラなどを調べてみると、次のような手法がありました。それぞれについて、簡単にどのように動作するかを記載しています。

1. kubesec

2 kustomize secretGeneratorPlugin

  • kustomize build 実行時に動的に外部から秘匿情報を取得して、マニフェストを生成する

3. godaddy/kubernetes-external-secrets

  • ExternalSecret というカスタムリソースを作成する
  • ExternalSecret リソースは、AWS Secrets Manager や Hashicorp Vault から秘匿情報を取得して、Secrets リソースを生成する
  • Pod からは、生成された Secrets を参照することで、秘匿情報にアクセスする

4. Mutating Webhook による Secrets の置き換え

5. hashicorp/vault-k8s

  • Pod に使用したい秘匿情報をアノテーションとして記載しておく
  • Mutating Webhook が Sidecar Container を挿入する
  • Sidecar Container は秘匿情報を Vault から取得して共有ボリュームに書き込む

6. banzaicloud/bank-vaults Mutating Webhook

  • Mutating Webhook は、InitContainer を追加する
  • InitContainer は、秘匿情報を取得するためのツールのバイナリを共有ボリュームにコピーする
  • アプリケーションをコンテナは、Mutating Webhookによって entrypoint を書き換えられており、コピーされたバイナリが実行される
  • コピーされたバイナリは、環境変数に特別なプレフィクス vault: がついたものがあった場合、 Vault から秘匿情報を取得して値を置き換えた上で、元のコマンドを実行する

1 の kubesec は、クラスタ側に何かを導入する必要はなく、秘匿情報をシンプルに扱えます。これで十分なケースも多いのではないかと思います。マニフェストは復号した上で適用する必要があるため、 Gitops のような Pull 型のデプロイ手法をとっている場合はすこし難しさがありそうです。

2 は kustomize を使うのが前提になってしまいますが、こちらもマニフェストの適用前に、外部の秘匿情報データベースなどを利用して秘匿情報を取得した上で適用します。

3 と 4 の手法は、仕組みも直感的で、スマートに外部の秘匿情報データベースと連携がおこなえます。ただし、Secrets に復号された秘匿情報が書き込まれるので、それも避けたいケースでは合いません。また、コントローラが全てのアプリの秘匿情報を復号するため、そこに権限が集中するのもやや気になりました。

5 と 6 の手法は、ややトリッキーにも思えましたが、Pod実行時に直接復号するため、その点ではセキュアな方法のように思えます。

今回は、自身の練習を兼ねて、6 の banzaicloud/bank-vaults を真似たツールを作成しました。banzaicloud/bank-vaults は対象が Hashicorp Vault のため、普段私がよく使う AWS Parameter Store あるいは Secrets Manager を扱えるようなものとしました。

作ったもの

作ったものは、こちらにおいてあります。

cloud-secrets

(とりあえず、動くところまで作ってみたという段階なので、実用には耐えるものではないと思います。)

Go で書かれていますが、2つのコマンド cloud-secrets controller cloud-secrets exec からなります。

前者を実行すると、Mutating Webhook のサーバが起動します。後者は、秘匿情報を取得した上で、与えられた任意のコマンドを実行するシンプルなコマンドです。

まずは、 cloud-secrets exec について、説明します。

実は、 cloud-secrets exec はそれ単体でも使うことができます。 似たようなものに、 chamber というツールの exec コマンドや、envconsul などがあります。これらは、指定した秘匿情報を取得し、環境変数にセットした上で与えられた任意のコマンドを実行します。

cloud-secrets exec は、次のように使うことができます。

$ export FOO=cloud-secrets://aws-parameter-store/my-secrets/foo
$ cloud-secrets exec sh -c 'echo $FOO'
FOO_SECRET_VALUE

cloud-secrets:// というプレフィクスがついた環境変数を見つけると、指定されたパスの秘匿情報を取得して、値を置き換えてコマンドを実行します。

一方、 cloud-secrets controller は、Mutating Webhook のHTTPサーバです。これをクラスタにデプロイすると、Pod リソースを書き換えを行う Mutating Webhook として機能するようになります。

どのような書き換えを行うかというと、各コンテナの entrypoint を書き換えて、さきほどの cloud-secrets exec を通して元の コマンドを実行するようにします。ただし、cloud-secrets のバイナリ は各コンテナにはインストールされていないはずなので、もう一つの工夫として、InitContainer の挿入を行います。この InitContainer は、コンテナ内の cloud-secrets のバイナリ を emptyDir ボリュームにコピーするだけです。そのコピーされたバイナリを各コンテナは entrypoint として使用します。

ユーザとしては、次のような Pod のマニフェストをデプロイすると、秘匿情報をアプリケーションに引き渡すことが可能になります。

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  annotations:
    cloud-secrets.daisaru11.dev/enabled: "true"
spec:
  containers:
    - name: myapp
      image: myapp
      env:
        - name: FOO
          value: "cloud-secrets://aws-parameter-store/myapp-secrets/foo"

普通のPodマニフェストと異なるのは、この2点です。

以上が、今回作成してみたツールの大まかな仕組みになります。

この仕組みは、ほぼ banzaicloud/bank-vaults を参考にしており、そちらの紹介記事も合わせて参考にするとわかりやすいかもしれません。

Mutating Webhook を実装する際に参考にしたもの

今回初めて Mutating Webhook を実装してみましたが、比較的簡単に実装を行えました。Mutating Webhook 自体は単なる HTTP サーバであり、期待した形式でレスポンスを返してあげればよいです。

とはいえ、最初はどこから手をつけて良いかわからなかったので、他のOSSの実装、特に、vault-k8sbanzaicloud/bank-vaults を参考にしながら実装しました。

banzaicloud/bank-vaults は、 kubewebhook という Admission Webhook を書くためのフレームワークを使っているようでした。これを用いると、Mutation の部分の処理のみ実装すれば良く、少し楽ができるようです。とはいえ、使わなくてもそれほど大変というわけでもなく、私は今回使わずに実装を行いました。

そのほか、少しわかりづらかったのはTLS証明書の生成についてです。Webhook と apiserver との通信は HTTPS で行われるため、証明書が必要です。実際の運用では、cert-manager を使って管理することになりそうですが、とりあえず動かすことが目的であれば cfssl というツールを使って発行するのが楽なようです。作り方はこちらを参考にさせてもらいました

終わりに

今回は、秘匿情報の管理について、どのようなアプローチがあるか調べてみました。また、 Mutating Webhook を実装することで、秘匿情報の展開を行う独自ツールの作成を行いました。

作ったツールは、実際に仕事で使うかどうかは微妙なところかなと思っています。 Admission Webhook は導入すると運用が発生しますし、あまり独自の仕組みを作るとシステム全体の見通しが悪くなります。kubesec など、シンプルなツールで十分なケースはそちらを使おうと思っています。

ただやはり、 Admission Webhook は強力な仕組みであり、Kubernetes を基盤として提供する上では、有効に使っていきたいと考えています。最近、 EKS Fargate を触っているのですが、 Fargate では DaemonSet が使えないため、ロギングやメトリクス収集といった横断的な機能は Sidecar で行う必要があります。そういったものを統一的に提供するには、 Mutating Webhook を用いて Sidecar Injection が有効なのではないかと考えおり、挑戦してみたいと思っています。