GitOpsのSecret管理をSealedSecretからOpenBao+ESOへ移行する――混在期間を安全に進める
KubernetesのmanifestをGitで管理し始めると、最後まで例外として残りやすいのがSecretです。
パスワードやAPIキーをそのままGitへ入れることはできません。しかし、Gitの外だけで管理すると、「リポジトリを見ればクラスタの構成が分かる」というGitOpsの利点が崩れてしまいます。
私は個人のk3sクラスタをGitとKustomizeで運用する中で、次の3方式が混在する状態になりました。
- Gitに入れないraw Secret
- SealedSecret
- External Secrets Operator+OpenBao
この記事では、理想的なSecret管理方式を一つ選ぶ話ではなく、複数の方式が混在する移行期間を、どう安全に進めるかを整理します。
現在の方針は次のとおりです。
- 新しく導入するアプリはExternalSecretに統一する
- 既存のSealedSecretは、設定を変更する機会に順次移行する
- OpenBao自身を起動するためのブートストラップ層は、OpenBaoの外に残す
なお、この記事ではSecretの値、鍵、IPアドレス、実際のドメイン、node名、namespace名、role名、Secretパスなどは公開していません。コード例に含まれる識別子は、説明用の名前へ置き換えています。
記事確認日: 2026年7月15日
先に結論
- 現在のリポジトリには、raw Secret、SealedSecret、ExternalSecretが混在している
- 混在は設計の失敗ではなく、移行中には自然に発生する
- SealedSecretは軽量で使いやすいが、値の更新や復旧時の運用負荷が積み上がる
- 移行先はOpenBao+External Secrets Operator
- 一斉移行はせず、新規アプリからExternalSecretに統一する
- OpenBao自身の起動に必要な秘密は、OpenBao外のブートストラップ層で管理する
- LLMのAPIキーなどを、ローカルファイルに平文で保存せずクラスタへ渡せるようになった
何に困っていたか――3方式が混在するリポジトリ
現在のprod環境には、Secretを扱う方法が3種類あります。
| 方式 | 現在の状態 | 導入時期 |
|---|---|---|
| raw Secret | 一部の古いアプリに参照が残る | 運用初期 |
| SealedSecret | 数十ファイルが稼働中 | 運用中期 |
| ExternalSecret | 数十ファイルへ拡大中 | 現在の標準 |
これは最初から設計した結果ではなく、運用を続ける中で方式が変化した結果です。
最初はraw Secretをgitignoreしていた
運用初期は、secret-*.yamlを.gitignoreに追加し、手元だけに置いてkubectl applyしていました。
仕組みは単純ですが、リポジトリだけではクラスタを再現できません。
実際に、kustomization.yamlからSecretファイルを参照しているにもかかわらず、実ファイルがローカルにしか存在しないため、別環境ではkubectl kustomizeが失敗する状態が発生しました。
次にSealed Secretsを導入した
平文のSecretを公開鍵で暗号化し、SealedSecretとしてGitへcommitする方式です。
復号できるのはクラスタ内のcontrollerだけなので、平文をGitへ置かずにGitOpsへ組み込めます。
導入が軽く、長い間この方式がSecret管理の中心でした。
現在はOpenBao+ESOへ移行している
その後、OpenBaoとExternal Secrets Operatorを導入しました。
現在は、新しく追加するアプリでは最初からExternalSecretを利用しています。既存のSealedSecretはそのまま稼働させ、設定変更やローテーションの機会に少しずつ移しています。
移行を始めたからといって、既存のSealedSecretをすぐに消す必要はありません。
Sealed SecretsとExternal Secrets Operatorは同じクラスタ内で共存できます。
3つの方式と、実際の運用コスト
個人運用で特に差を感じた点を比較します。
| 観点 | raw Secret | SealedSecret | ESO+OpenBao |
|---|---|---|---|
| Gitに入るもの | 参照のみ | 暗号文 | Secretの参照情報 |
| 値の正本 | ローカルファイル | Git上の暗号文+復号鍵 | OpenBao |
| 値の更新 | ファイル編集→apply | 平文作成→seal→commit | OpenBaoの値を更新 |
| GitOpsとの相性 | 悪い | 良い | 良い |
| クラスタ全損時 | 手元のファイルが必要 | 復号鍵が必要 | OpenBaoのデータとブートストラップ情報が必要 |
| 平文ファイル | 常に存在しやすい | seal時に作成しやすい | 作成せずに登録可能 |
| 追加コンポーネント | なし | controller | OpenBao+ESO |
raw Secretの問題は分かりやすく、リポジトリだけではクラスタを再現できません。
SealedSecretはGitOpsとの相性が良く、導入コストも低い方式です。一方で、アプリ数とSecret数が増えるほど、更新や確認にかかる手間が積み上がりました。
SealedSecretを長期間使って分かった限界
Sealed Secrets自体は、よく設計された便利なツールです。
それでも移行を始めた理由は、主に次の4点です。
1. 平文をローカルに作る工程が残る
一般的な手順では、最初に平文のSecret manifestを作り、それをkubesealで暗号化します。
平文Secretを作る
↓
kubesealで暗号化
↓
SealedSecretをGitへcommit
.gitignoreは誤commitを防ぐ助けにはなりますが、平文ファイルそのものがローカルへ残らないことまでは保証しません。
作業用ファイル、バックアップ、エディタの履歴などに、意図せず値が残る可能性があります。
2. 値の更新に複数の工程が必要
APIキーやパスワードを更新する場合、一般的には次の作業が必要です。
- 平文のSecret manifestを作り直す
kubesealで再暗号化する- Gitへcommitする
- GitOpsツールで同期する
一つひとつは難しくありません。
しかし、更新作業が面倒だと、必要なローテーションを後回しにしやすくなります。
3. 復号鍵がクラスタ復旧の前提になる
Git上のSealedSecretを復号できるのは、controllerが持つ秘密鍵です。
クラスタを作り直した際に、その復号鍵を戻せなければ、Gitに保存された暗号文からSecretを復元できません。
つまり、Gitとは別に、絶対に失ってはいけない鍵のバックアップが必要です。
これはSealed Secretsの欠点というより、公開鍵暗号を使う以上必要な性質です。ただし、クラスタ全損時の復旧手順を考えると、重要な依存関係になります。
4. 暗号文だけでは中身を確認できない
SealedSecretの差分をGitで確認することはできますが、暗号文を見ても中身は分かりません。
時間がたつと、次のようなことが分からなくなります。
- どのキーが入っているのか
- どこで発行した値なのか
- 更新時に何をすればよいのか
- そのSecretが現在も必要なのか
クラスタから生成済みのSecretを確認すれば分かりますが、リポジトリだけでは判断できません。
どれも致命的な問題ではありません。だからこそ長期間使えました。
しかし、アプリとSecretが増えるほど、更新と確認の摩擦が大きくなりました。
移行先――OpenBao+External Secrets Operator
移行先は、次の2つを組み合わせた構成です。
OpenBao
OpenBaoは、Vaultから派生したオープンソースのSecret管理基盤です。
Secretの保存、認証、アクセス制御、監査などを担当します。CLIとWeb UIの両方から操作できます。
私の環境では、OpenBaoをKubernetes上で稼働させ、外部ストレージへデータを保存しています。
External Secrets Operator
External Secrets Operatorは、外部のSecret管理基盤から値を取得し、Kubernetes Secretを生成するoperatorです。
ExternalSecretリソースに、
OpenBaoのこのパスにある値を、このnamespaceの、この名前のSecretへ同期する
という宣言を書きます。
全体の流れは次のようになります。
[管理者]
│
│ 値を登録・更新
▼
[OpenBao]
│
│ Kubernetes認証
▼
[External Secrets Operator]
│
│ Kubernetes Secretを生成・同期
▼
[Kubernetes Secret]
│
│ envFrom / secretKeyRef
▼
[アプリケーション]
Gitへcommitするのは値そのものではなく、値の取得方法を記述したExternalSecretです。
# 例示用の設定です。実環境の名前やパスは置き換えています。
#
# 発行元:
# 利用するサービスの管理画面
#
# 登録・更新:
# OpenBaoの対象パスへ値を登録する
#
# 反映:
# ESOの同期後、必要に応じてPodを再起動する
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: example-app-secret
namespace: example-app
spec:
refreshInterval: 1m
secretStoreRef:
name: example-secret-store
kind: ClusterSecretStore
target:
name: example-app-secret
creationPolicy: Owner
data:
- secretKey: API_KEY
remoteRef:
key: example-app
property: API_KEY
値そのものはGitに書けません。
しかし、次の情報はGitへ残せます。
- どのキーが必要か
- どこで発行するか
- OpenBaoのどこへ登録するか
- 更新後に何をするか
私はこれを、値は書かなくても、値の育て方はGitに書けると考えています。
OpenBaoへの認証にはKubernetes認証を使う
ESOがOpenBaoへアクセスする認証には、Kubernetes認証を利用しています。
ESOのServiceAccountトークンをOpenBao側で検証し、許可されたパスだけを読み取れるようにします。
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: example-secret-store
spec:
provider:
vault:
server: "https://openbao.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "example-eso-role"
serviceAccountRef:
name: external-secrets
namespace: external-secrets
audiences:
- vault
ここで重要なのは、ESO用の固定トークンをKubernetes Secretへ保存しないことです。
固定トークンをSecretへ置くと、そのトークンを誰が管理するのかという問題が再び発生します。
Kubernetes認証を使えば、ServiceAccountの情報を基に認証できるため、長期間有効な静的トークンを別途配布する必要がありません。
移行期間を安全に進める3つのルール
ここがこの記事の本題です。
既存のSealedSecretを一度にすべて移行すると、失敗時の影響範囲が大きくなります。また、現在正常に動いているアプリを、移行のためだけに触る必要もありません。
そこで、次の3つのルールを決めました。
ルール1:新規はExternalSecret、既存は触る機会に移す
現在は、次の方針に統一しています。
- 新しく追加するアプリはExternalSecretを使う
- 既存アプリは、Secretを更新する機会に移行する
- 正常に動いているSealedSecretは、無理に移行しない
Sealed SecretsのcontrollerとESOは共存できるため、技術的には混在しても問題ありません。
問題になるのは、人間が「どのアプリがどの方式なのか」を把握しにくくなることです。
そのため、ファイル名やディレクトリ構成から方式が分かるようにしています。
例えば、
sealed-secret.yamlが残っているアプリは未移行external-secret.yamlがあるアプリは移行済み
といった形です。
移行の進捗を一覧表で手作業管理するのではなく、リポジトリを検索すれば分かる状態にしています。
アプリ単位で移行する
移行は、Secret単位ではなくアプリ単位で行います。
基本的な手順は次のとおりです。
- 現在の値を確認する、または発行元で新しい値を再発行する
- OpenBaoへ値を登録する
ExternalSecretを追加する- 生成先を既存のKubernetes Secretと合わせる
- SealedSecretの参照を削除する
- ESOが新しいSecretを生成したことを確認する
- 必要に応じてPodを再起動する
Secretの所有権に注意する
Sealed Secretsのcontrollerは、生成したKubernetes Secretに所有者情報を付けることがあります。
その状態で同じ名前のSecretをESOにも管理させると、所有権が衝突する可能性があります。
特にcreationPolicy: Ownerを利用している場合、ESOは別のリソースが所有するSecretをそのまま引き継げないことがあります。
単純に移行すると、次のどちらかが発生します。
- ExternalSecretを先に作ると、既存Secretとの所有権競合が起きる
- SealedSecretを先に消すと、ESOが生成するまで一時的にSecretが消える
個人環境で短い停止を許容できる場合は、次の順で対応できます。
- OpenBaoへ値を登録する
- SealedSecretを削除する
- ExternalSecretを反映する
- Secret生成を確認する
- Podを再起動する
数十秒から1分程度、Secretが存在しない時間が発生する可能性があります。
停止を許容できない環境では、一時的に別名のSecretを作る方が安全です。
- ESOで新しい名前のSecretを生成する
- Deploymentの参照先を新しいSecretへ変更する
- Podが正常に起動することを確認する
- 古いSealedSecretとSecretを削除する
- 必要であれば最終的な名前へ整理する
ルール2:ブートストラップ層はOpenBaoの外へ残す
OpenBao自身を起動するためにも、いくつかの秘密が必要です。
例えば、次のような情報です。
- ストレージへの接続情報
- TLSに関する情報
- OpenBaoを利用可能な状態にするためのブートストラップ情報
これらをOpenBao自身へ保存することはできません。
OpenBaoが起動していなければ取得できない値を、OpenBaoの起動に使うことはできないからです。
この「鶏と卵」の問題は、どこかでOpenBao以外の管理方法へ着地させる必要があります。
現在は、OpenBao自身の起動に必要な一部の情報だけを、OpenBao外のブートストラップ層として分離しています。
第0層:
OpenBaoを起動・復旧するための情報
↓
第1層:
OpenBao
↓
第2層:
ESOが各アプリへSecretを配布
ブートストラップ層では、SealedSecretを含むOpenBao外の仕組みを用途に応じて利用しています。
ただし、すべてのブートストラップ情報を同じ場所へ集約しているわけではありません。具体的な保管場所、復旧手順、鍵の分割方法などは、セキュリティ上公開しません。
重要なのは、最終形を「SealedSecretを完全にゼロにすること」と定義しないことです。
目標は、
各アプリのSecretを一元管理し、ブートストラップに必要な最小限の秘密だけをOpenBao外へ残す
ことです。
ルール3:raw Secretはなくし、サンプルだけを残す
gitignoreされたraw Secretへの参照は、順次リポジトリから削除します。
代わりに、値をプレースホルダにしたサンプルを残します。
apiVersion: v1
kind: Secret
metadata:
name: example-app-secret
namespace: example-app
type: Opaque
stringData:
API_KEY: "REPLACE_ME"
DATABASE_PASSWORD: "REPLACE_ME"
このファイルは、実際のデプロイからは参照しません。
目的は、「このアプリにはどのキーが必要か」をリポジトリから確認できるようにすることです。
実際の値はOpenBaoへ保存し、Kubernetes SecretはESOに生成させます。
これにより、gitignoreされたファイルが存在しないとKustomizeのbuildが失敗する問題を避けられます。
副産物:APIキーをローカルファイルへ保存せずに済む
この移行で特に便利になったのが、LLMのAPIキーの管理です。
セルフホストするアプリの中には、外部のLLM APIキーを要求するものがあります。
例えば、次のような用途です。
- ブックマークの自動タグ付け
- 文書の要約
- エージェント機能
- 自作アプリからのLLM利用
LLMのAPIキーは、漏洩すると不正利用による課金へ直接つながる可能性があります。
raw Secretでは、YAMLや.envへ平文で書きます。
SealedSecretでも、一般的な手順では暗号化前の平文manifestを一度作成します。
OpenBaoへ移行すると、キーをローカルファイルへ保存せず、直接登録できます。
ただし、次のようにコマンドライン引数へ直接キーを書くと、シェル履歴へ残る可能性があります。
# 推奨しない例
bao kv put secret/example-app API_KEY="<実際のAPIキー>"
標準入力から登録する方が安全です。
read -rsp "API key: " API_KEY
printf '\n'
printf '%s' "$API_KEY" |
bao kv put -mount=secret example-app API_KEY=-
unset API_KEY
この方法では、APIキーをコマンドライン引数やローカルファイルへ書かずに登録できます。
OpenBaoのWeb UIから入力する方法でも構いません。
ここでの目標は「ローカル上に一切存在しない」ことではなく、平文をファイルやシェル履歴へ残さないことです。
ローテーションも同じ操作で行える
APIキーを再発行した場合も、同じパスへ新しい値を登録します。
ESOが設定された間隔で同期し、Kubernetes Secretを更新します。
ただし、環境変数としてSecretを読み込んでいるPodは、自動的に新しい値へ切り替わりません。
その場合は、同期を確認した後にPodを再起動します。
kubectl rollout restart deployment/example-app \
-n example-app
更新方法が単純になると、APIキーのローテーションを後回しにしにくくなります。
OpenBaoを導入して新しく増えた運用
OpenBao+ESOへ移行すると、Secret管理は便利になります。
一方で、OpenBao自身の運用が新しく必要になります。
OpenBaoが止まっても、既存アプリはすぐには停止しない
OpenBaoが停止すると、ESOは値を取得できなくなります。
ただし、すでに生成されているKubernetes Secretは、通常そのまま残ります。
動作中のPodが環境変数として値を読み込み済みであれば、OpenBao停止直後にアプリまで停止するわけではありません。
影響を受けるのは主に次の操作です。
- Secretの新規生成
- Secretの更新
- 新しいnamespaceやアプリへの配布
- Secretを失った後の再生成
OpenBaoが単一障害点であることに変わりはありませんが、「OpenBaoが止まると全アプリが直ちに停止する」という構成ではありません。
再起動後に利用可能な状態へ戻す手順が必要
OpenBaoの構成によっては、再起動後にsealed状態となり、利用可能な状態へ戻す操作が必要です。
自動化するか、人間が操作するかは、環境の規模やリスクの考え方によって変わります。
個人環境では手動操作も現実的ですが、次の点を明確にしておく必要があります。
- 誰が操作するか
- 必要な情報をどこから取得するか
- どの順番で復旧するか
- 管理者が不在でも復旧できるか
- 復旧手順を実際に試しているか
具体的な保管場所や操作方法は公開しませんが、手順そのものはオフラインでも確認できる形で管理しています。
バックアップ対象が明確になる
OpenBaoを利用する場合、少なくとも次の種類の情報を復旧できる必要があります。
- OpenBaoの保存データ
- OpenBaoを起動・復旧するためのブートストラップ情報
- Kubernetesクラスタを再構築するためのGitリポジトリ
- ブートストラップ層で利用する鍵や設定
これらを同じ場所だけに保存すると、その保存先を失ったときに同時に復旧不能になります。
そのため、バックアップは役割ごとに分離し、実際に空の環境へ戻せるかを定期的に確認する必要があります。
「バックアップファイルが存在すること」と、「復旧できること」は別です。
移行後の復旧順序
具体的な秘密の保管場所は環境ごとに異なりますが、論理的な復旧順序は次のようになります。
Kubernetesクラスタ
↓
ブートストラップ層
↓
OpenBao
↓
External Secrets Operator
↓
各アプリのKubernetes Secret
↓
各アプリケーション
この順番を意識しておくと、クラスタ全損時に「どのSecretが、どのSecretに依存しているのか」が分かりやすくなります。
この構成が向いている人
OpenBao+ESOが向いているのは、次のような環境です。
- 管理するアプリが増えてきた
- Secretの更新やローテーションが定期的に発生する
- 複数のnamespaceへSecretを配布したい
- 複数のクラスタでSecretを共有したい
- クラスタ全損からの復旧を考えている
- 監査やアクセス制御を細かく管理したい
一方で、アプリが数個だけでSecretをほとんど更新しない場合は、SealedSecretのままでも十分です。
OpenBaoは常駐サービスであり、バックアップ、可用性、認証、復旧手順の管理が必要になります。
Secretの数や更新頻度が少ない環境では、導入によって増える運用負荷の方が大きい可能性があります。
raw Secret+gitignoreを長期運用にしない
raw Secretをgitignoreして管理する方法は、最初の動作確認では便利です。
しかし、長期運用では次の問題があります。
- 手元のファイルが失われると復旧できない
- GitOpsの検証が成立しない
- 別PCや別担当者へ引き継ぎにくい
- バックアップの対象が分散する
- どのファイルが正本か分からなくなる
- 誤ってcommitする可能性が残る
最初からOpenBaoまで導入する必要はありません。
小規模な環境では、まずSealedSecretへ移るだけでも大きな改善になります。
まとめ
Secret管理の移行は、「最も優れた方式を一つ選ぶ」という話に見えます。
しかし、実際に重要だったのは、方式が混在する期間をどう管理するかでした。
現在は次の方針で運用しています。
- 新しく追加するアプリはExternalSecretに統一する
- 既存のSealedSecretは、設定を変更する機会に移す
- raw Secretへの参照は順次なくす
- 必要なキーの一覧や更新手順はGitへ残す
- OpenBao自身のブートストラップ情報はOpenBaoの外へ分離する
- 具体的な鍵の保管場所や復旧情報は公開しない
最終形は、SealedSecretを完全にゼロにすることではありません。
各アプリのSecretをOpenBaoへ集約し、OpenBaoを起動するための最小限の情報だけを外側へ残すことが目標です。
そして副産物として、LLMのAPIキーのような「漏れると直接課金につながる値」を、ローカルファイルやGitへ平文で残さずにクラスタへ届けられるようになりました。
OpenBaoという新しい運用対象は増えましたが、Secretの正本、更新方法、復旧順序が明確になったことは、それ以上に大きな改善でした。
次に読む記事
-
クラスタ内Gitの落とし穴|k3s GitOpsを全損から復旧する設計 SealedSecretの鍵とクラスタが運命共同体になる問題を、クラスタ全損からの復旧という視点で整理しています。
-
実際に試したセルフホストソフトウェア一覧【2026年版】 OpenBaoやExternal Secrets Operatorを含め、実際に導入したセルフホストソフトウェアをまとめています。
-
バックアップして終わりにしない――セルフホスト環境の復元テスト入門 バックアップの存在確認だけでなく、実際に復元できることを確かめるための考え方を整理しています。