23卒内定者の友利 拓誠(@tksx1227)です。

2022年の11, 12月にAmeba事業本部のピグ事業部で内定者アルバイトをしていました。

アルバイト期間中、KubernetesのSecretをGitHub上で管理できるようにするために、プロジェクトにSealed Secretsを導入するという経験をしたので、その手順や工夫したポイントを紹介していきます。

Secret管理方法の一例として参考になれば幸いです。

目次

課題

KubernetesのSecretは、マニフェスト上に平文、あるいは平文をBase64変換でエンコードした値が記述されているため、簡単に解読できてしまうと言う特徴があります。

そのため、SecretのマニフェストファイルをGitHubなどのオープンな場に晒すことは、セキュリティの観点から推奨されるものではありません。

ピグでは基本的にHelmを使ってKubernetesリソースを管理している一方で、上記の理由により、Secretリソースのみ必要に応じてCLI操作で直接追加していると言う状態でした。

この方法では、主に次の2つの問題があります。

  • Secretの管理が属人化する
  • 何らかの理由でクラスタが吹き飛んだ際の復旧コストが高い

ここで、「Secretも他のリソースと同様に、HelmとGitHubで管理できるようにしたい」と言う課題が出てきます。

Sealed Secretsはこの課題を解決します。

Sealed Secretsとは

Sealed Secretsは、KubernetesのSecretリソースを安全に管理するために開発されたOSSであり、以下の2つの要素で構成されています。

  • クライアント側のユーティリティ kubeseal
  • クラスタ側のコントローラ / オペレータ

ユーティリティである kubeseal は、非対称暗号(公開鍵暗号)を用いて、コントローラだけが復号化できるように秘匿情報を暗号化します。

アーキテクチャは下図のようになります。

Sealed Secretsのアーキテクチャ図

kubeseal はコントローラが生成したシーリングキー(公開鍵と秘密鍵の両方を保持するSecretリソース)から公開鍵のみを取得し、SecretマニフェストをもとにSealedSecretマニフェストを生成します。

SealedSecretマニフェストには暗号化された秘匿情報が記載されているため、第三者に見られても問題ありません。

そのため、SealedSecretマニフェストはGitHub上で安全に管理することができます。

(例)Secretマニフェストとそれをもとに生成されたSealedSecretマニフェスト

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
  namespace: default
type: Opaque
stringData:
  secret: "secret dayo"
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: mysecret
  namespace: default
spec:
  encryptedData:
    secret:  AgBPNEC9qTnTzl5LomlaL7ncdxPONnWa1ooZB5tUXrhOESClY4k3...
  template:
    metadata:
      creationTimestamp: null
      name: mysecret
      namespace: default
    type: Opaque

SealedSecretマニフェストをクラスタにapplyすると、SealedSecretリソースのデプロイを検知したコントローラは、シーリングキーから秘密鍵を参照し、SealedSecretが持つ暗号文を復号化します。

その後、復号化された値を持つSecretリソースを同じ名前空間内に生成します。

ここで生成されたSecretは、通常のSecretと同様に扱うことができます。

以上のフローにより、Sealed Secretsを用いることで安全にSecretの管理ができるという仕組みになっています。

導入手順

ここでは、Helmチャートのディレクトリ構成を確認したのち、各種ツールのインストール、およびシークレットの追加手順を紹介します。

ディレクトリ構成

ピグでは以下のようなディレクトリ構成でHelmチャートを管理しています。

project-helm/
    ├── mychart/   # プロジェクトのカスタムチャート
    │      ├── Chart.lock
    │      ├── Chart.yaml
    │      ├── charts/
    │      ├── templates/
    │      │       ├── _helpers.tpl
    │      │       ├── namespace.yaml   # コントローラ専用の名前空間を追加する
    │      │       └── sealed-secrets.yaml   # SealedSecretのテンプレート
    │      └── values.yaml   # 全ての環境で共通の値を記述する
    └── values/   # 環境ごとの値を記述する
           ├── test.yaml
           ├── dev.yaml
           ├── stg.yaml
           └── prd.yaml

kubesealのインストール

kubeseal はHomebrewを使ってインストールすることができます。

$ brew install kubeseal

Homebrew以外のツールでのインストール方法はこちらを参照してください。

コントローラのインストール

Helmでコントローラをインストールするために、 Chart.yamldependencies にSealed Secretsを追加します。

dependencies:
  - name: sealed-secrets
    version: 2.7.1
    repository: <https://bitnami-labs.github.io/sealed-secrets>
    condition: sealed-secrets.enabled

次に values.yaml に値の設定を追加します。

ここには全ての環境で共通となるコントローラに関する設定を記述します。

各パラメータの説明はこちらを参照してください。

sealed-secrets:
  enabled: true
  namespace: sealed-secrets
  fullnameOverride: sealed-secrets-controller
  keyrenewperiod: "0"

keyrenewperiod: "0" に関してのみ補足します。

コントローラは、デフォルトで30日ごとに新しいシーリングキーを1つ追加するような設定になっています。

これにより秘密鍵が漏洩した際の影響範囲を30日ごとに区切れるというメリットがありますが、以下の理由により、ここでは自動更新をオフに設定しています。

  • 頻度に合わせてバックアップを取る必要がある(30日おきにシークレットを追加する場合)
  • 無駄なシークレットが何個も生成される(バックアップファイルの肥大化)

 

後はクラスタにチャートをインストールするだけで、クラスタにコントローラがインストールされ、Sealed Secretsを利用できる環境が整います。

シークレットの追加

SealedSecretマニフェストを生成するために、まずは実際に追加したいSecretのマニフェストを作成します。

その後、下記のコマンドを実行することでSealedSecretマニフェストを生成することができます。

$ kubeseal \\
  --controller-namespace sealed-secrets \\
  --controller-name sealed-secrets-controller \\
  --secret-file secrets.yaml \\
  --format yaml > sealed-secrets.yaml

実際に生成されたSealedSecretマニフェストには、上の例で紹介したようにSecretの名前や名前空間名、暗号化された秘匿情報などの情報が記載されています。

これらの可変部を環境ごとに管理するために、テンプレートと値のそれぞれへ分離します。

(例)SealedSecretマニフェストをテンプレートと値に分離する

{{- with index .Values "sealed-secrets" "default" "mysecret" }}
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: {{ .name }}
  namespace: {{ .namespace }}
spec:
  encryptedData: {{ toYaml .data | nindent 4 }}
  template:
    metadata:
      name: {{ .name }}
      namespace: {{ .namespace }}
      labels: {{ toYaml .labels | nindent 8 }}
      annotations: {{ toYaml .annotations | nindent 8 }}
    type: {{ .type }}
{{- end }}
sealed-secrets:
  default:
    mysecret:
      name: mysecret
      namespace: default
      type: Opaque
      labels: {}
      annotations: {}
      data:
        key-dayo: AgCvbVEJ9xfwSRmMA6L/6W...

SealedSecretの各値は sealed-secrets.{SECRET_NAMESPACE}.{SECRET_NAME} 配下に記述することで、名前空間とシークレット名が決まるとシークレットが一意に定まるようにしています。

これにより、次章で紹介する自動化処理を行う際にシークレットの重複チェック等ができるようになっています。

続けて他の環境にシークレットを追加する時も同様にしてSealedSecretマニフェストを作成し、値の部分のみ {ENV_NAME}.yaml に追加します。

テンプレート1つに対して、値のファイルが環境の数だけある状態です。

テンプレートと値ファイルの対応関係を示した図

これで各環境に1つのシークレットを追加することができました。

補足情報

シークレットの追加に伴い、一点だけ気をつけることがあります。

各クラスタに既にコントローラをインストールしている場合、クラスタが持つシーリングキーはそれぞれ異なるものになるため、それぞれの環境にシークレットを追加するたびにコンテキストを切り替える必要があります。

例として、コンテキストを切り替えない場合、testクラスタにある公開鍵でdev用のシークレットを暗号化してしまい、devクラスタ内で対応する秘密鍵が見つからずSealedSecretリソースが復号化できないといったことが起きます。

この手間を省く案の1つとして、シーリングキーをローカルで作成し、全ての環境で同一のキーを利用する、というものがあります。

同一のキーを利用することで、任意のクラスタを参照して作成したSealedSecretマニフェストが全ての環境で正常に復号化されるようになります。

セキュリティの堅牢性を重視する場合はおすすめできませんが、実際に扱う秘匿情報に応じて検討してもいいかもしれません。

バックアップと復旧手順

SealedSecretマニフェストは、暗号化する際に使用した公開鍵に対応する秘密鍵がなければ二度と復号化することができません。

そのため、バックアップとしてクラスタ内にあるシーリングキーのみ別途保管する必要があります。

シーリングキーはただのSecretリソースであるため、以下のコマンドでバックアップファイルを作成することができます。

$ kubectl get secret -n sealed-secrets -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealing-key-backup.yaml

シーリングキーを復旧したい場合、このバックアップファイルをクラスタにapplyすることで復旧できます。

※ コントローラを先にインストールしている場合、apply後にコントローラを再起動する必要があります。

$ kubectl apply -f sealing-key-backup.yaml

# 必要に応じて以下も実行する
$ kubectl -n sealed-secrets delete pod -l app.kubernetes.io/name=sealed-secrets

自動化する

前章まででSealed Secretsの概要と各手順を確認しました。

ここまでやってみると、シークレット追加時に以下のような点が気になってきました。

  • シークレットを追加する人にSealed Secretsの前提知識が求められる
  • Secretマニフェストを誤ってコミットに含めてしまう可能性がある
  • テンプレートと値の分離に手間がかかる

ここで、Makefileとシェルスクリプトを用いてシークレット追加のフローを整備することで、ツールの利用者が直感的にシークレットを追加できるようにしました。

実際にシークレットを追加する様子がこちらになります(GIF画像)。

インタラクティブにシークレットを追加する様子(GIF画像)

make実行後、表示されるテキストに従い以下の順番で各値を設定していきます。

  1. 環境の選択
  2. シークレットの名前
  3. シークレットを追加する名前空間名
  4. シークレットのタイプ
  5. ラベル
  6. アノテーション
  7. シークレットのキー
  8. シークレットのバリュー(標準入力 or ファイルから読み込む)

入力が完了すると、 mychart/template/sealed-secrets.yaml にテンプレートが追加され、 values/{ENV_NAME}.yaml に各値がそれぞれ追加されるようになっています。

また、Secretマニフェストを作成する工程を挟まないため、誤ってSecretマニフェストをコミットしてしまうこともありません。

以上により、インタラクティブにシークレットの追加ができ、かつHelmとGitHubで安全にSecretの管理ができるようになっています。

スクリプト内部のお話

スクリプト内部ではYAMLの操作に yq を使用しており、以下のようにして値の追加処理などを行なっています。

{ENV_NAME}.yamlに値を追加する

SealedSecretの値を追加する実装コード

 

また、値とテンプレートのそれぞれにおいて重複を防ぐようにもしています。

環境と名前空間が同じ場合、同じ名前のシークレットは追加できない

SealedSecretの値の重複をチェックする実装コード

 

名前空間とシークレット名が同じで環境だけが違う場合、テンプレートは重複して追加されない

SealedSecretのテンプレートの重複をチェックする実装コード

 

その他、Makefileでは「シークレットの追加」以外に「カスタムシーリングキーの生成」や「カスタムシーリングキーをクラスタに登録」などの操作を行うターゲットも用意しており、極力手間を削減するように工夫しました。

Makefileの画像

あとがき

本記事では、Sealed Secretsを導入してSecretをHelm + GitHubで管理するための手順を紹介しました。

実際にSealed Secretsを導入してみて、Sealed Secretsは外部のサービスに依存せずにシークレットを管理することができるため、比較的導入コストは低いように感じました。

一方で、シークレットを追加する際の手間やHelmでの管理のやりづらさ等が目立つようにも感じました。

今回、シークレット追加とHelm管理の一部を自動化することである程度の手間は削減できたのですが、シーリングキーのバックアップ・管理やシークレット追加時のコンテキストの切り替えなどの手間は残ったままとなっています。

実際に調査・検証と導入に取り組んでいた時期が1、2ヶ月ほど前であるため、実装当時は上記の手間まで考慮できていなかったのですが、この辺りもスマートに解決できればなお良かったなぁと思っています。

 

色々書きましたが、Sealed Secrets自体は導入コストが低く、かつ簡単に使うことができるツールだと思っているので、Secretの管理に課題を感じている方は是非導入を検討してみてください!

また、その際に少しでもこの記事が参考になれば嬉しいです!