sops-mcp について
sops-mcp は age を使って SOPS 暗号化シークレットファイルを作成・管理する MCP サーバーです。暗号化したシークレットをソース管理に置く開発者向けで、平文は MCP 経由でクライアントに返されず、クライアントのファイルシステムにもアクセスしません。
主な機能
- 01暗号化シークレットの作成と一覧表示
- 02シークレットの生成、ローテーション、更新
- 03生成、外部、派生シークレットへの対応
- 04名前付きキー ドメインによる受信者管理
- 05ファイルの名前変更、削除、再キー設定
活用シーン
- 01暗号化シークレットファイルをソースコードと共にコミット
- 02デプロイ時に CI/CD でシークレットを復号
- 03シークレットをローテーションして派生値を再計算
MCP 対応
MCP クライアントに SOPS 暗号化ファイルを作成・管理するツールを提供します。ツールはテキストパラメーターで内容を受け渡し、個別のシークレットを復号するツールはなく、クライアントのファイルシステムにもアクセスしません。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
uvx sops-mcpSOPS_MCP_AGE_PUBLIC_KEYシークレットAge recipient public key. Required at startup.SOPS_AGE_KEYシークレットAge private key. Required at call time for any mutation tool (decrypt + re-encrypt). Read-only tools do not need it.ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
sops-mcp
age 暗号化を使用して、SOPS で暗号化されたシークレットファイルを作成・管理する MCP サーバーです。
Claude Code(または任意の MCP クライアント)が、モデルに平文の値を一度も見せることなく、暗号化された secrets.enc.yaml ファイルを生成できるように設計されています。ファイルの内容はすべてテキストパラメーターとして渡され、テキストとして返されます。サーバーはクライアントのファイルシステムにアクセスできません。
理由
設計の目的は次の 2 つです。
1. シークレットを漏洩させずにソースツリー内に保管する。 小規模なプロジェクトでは、少数の認証情報のために本格的なシークレットマネージャー(Vault、AWS Secrets Manager など)を運用するのは過剰です。git でシークレットを保存時に暗号化し、デプロイ時に CI/CD パイプラインで復号するほうが、はるかに低コストです。
- このサーバーを使って
secrets.enc.yamlを作成します。age で公開鍵に対して暗号化されるため、安全にコミットできます。 - コードと一緒にコミットします。
- CI/CD パイプラインに age 秘密鍵を保管し、デプロイ時に復号して、オーケストレーターのコンテナーに環境変数として平文を注入します。
age 秘密鍵が置かれるのは、CI/CD のシークレットストアのただ 1 か所です。それ以外のすべての場所、つまりノートパソコン、git リモート、コンテナーイメージにあるのは、暗号文だけです。詳しくは、以下の使用例をご覧ください。
2. AI コーディングエージェントが、自分では決して読み取れないシークレットを生成できるようにする。 Claude(または任意の MCP クライアント)は、パスワードの作成やローテーション、ハッシュの導出、名前の変更、削除を行えますが、平文の値が MCP の境界を越えてモデルに返されることはありません。 サーバーが暗号化キーを保持し、クライアントはリクエストを送信してメタデータを受け取るだけです。 「このシークレットを 1 つ復号する」ツールは意図的に用意していません。 これにより、プロンプトインジェクションや、正常に動作しないエージェントによるシークレットの外部流出を防ぎます。
最もシンプルな構成では、単一の age recipient(CI の秘密鍵)を使用します。複数の鍵が必要な場合(CI の鍵と運用担当者の鍵、または関係者ごとに分けた鍵セットなど)は、キーのドメインをご覧ください。
シークレットのソース
各シークレットは次の 3 種類のソースのいずれかであり、_meta_unencrypted に記録されます。
generated— 暗号学的にランダムな値(Pythonsecrets/OS の CSPRNG)です。長さと文字セットを指定し、サーバーは両方を保存するため、ローテーション時に再生成できます。external— ユーザーが指定した値をそのまま暗号化したもの(SMTP 認証情報、サードパーティー API キーなど)です。ローテーション後も保持されます。sops_update_externalを使って更新します。derived— 同じファイル内の別のキーから、名前付きの変換を使って計算された値です。ソースがローテーションされた場合(または外部ソースが更新された場合)、導出された値はトポロジカル順に自動で再計算されます。OIDC クライアントシークレットから Authelia の PBKDF2 ハッシュを生成する場合などに便利です。
設計
ツールの構成は、次の 3 つの考え方に基づいています。
- 平文は MCP の境界を越えません。 生成されたシークレット値がクライアントに返されることはありません。「このキーを 1 つ復号する」ツールは意図的に用意していません。平文が必要な場合は、age 秘密鍵を使って自分で
sops decryptを実行してください。 - メタデータは平文で保持します。 暗号化された値と並んで
_meta_unencryptedブロックを置き(SOPS のunencrypted_suffix機能を使用)、各シークレットのソース、生成方法、最終ローテーション日時、所属するキーのドメインを記録します。これにより、サーバーは復号せずにシークレットの一覧表示やローテーションを行えます。デフォルトでは SOPS の MAC がこれらの値を保護するため、ブロックが改ざんされていると復号に失敗します。mac_only_encryptedで MAC の保護を無効にしたファイルは拒否されます。鍵なしでブロックを読み取るツールでは MAC を検証できないため、recipient は別途検証します。 generatedまたはderivedシークレットの値を直接更新することはできません。 これらの値はローテーションによってのみ変更され、その際、サーバーも呼び出し元も平文にアクセスできません。Externalシークレット(ユーザーが管理する上流 API キーなど)はsops_update_externalで更新できます。
Transforms(derived シークレット用)
| Transform | 目的 | 決定性 |
|---|---|---|
pbkdf2_sha512_authelia | Authelia の configuration.yml 形式の PBKDF2-SHA512 ハッシュ($pbkdf2-sha512$310000$...) | いいえ — 呼び出しごとにランダムなソルトを使用 |
sha256_hex | 16進数でエンコードされた SHA-256 ダイジェスト | はい |
ツール
作成と一覧表示
| ツール | 説明 |
|---|---|
sops_create_secrets | 1つ以上のシークレット(任意のソースの組み合わせ)を含む、新しい暗号化ファイルを作成します。 |
sops_list_secrets | 復号せずに、ファイル内のキー、ソース、説明を一覧表示します。 |
sops_create_oidc_secret | 便利な機能です。Authelia の OIDC クライアントシークレットを、generated + derived(pbkdf2_sha512_authelia)のペアとして1回の呼び出しで作成します。configuration.yml に貼り付けるためのハッシュが応答で返されます。 |
sops_create_k8s_secret | stringData の値を含む Kubernetes Secret マニフェストを作成し、stringData のみが暗号文になるように暗号化します。作成専用です。すべての変更ツールがこの結果を拒否します。Kubernetes Secret マニフェストを参照してください。 |
sops_list_domains | 設定済みのキー ドメイン、その age 受信者、それぞれが復号できるか、暗号化のみ可能かを一覧表示します。秘密鍵の情報は返しません。 |
変更(対象ドメインの秘密鍵が必要です)
| ツール | 説明 |
|---|---|
sops_rotate_generated | すべての generated シークレットを再生成します。ソースがローテーションされた派生シークレットは再計算され、それ以外は保持されます。外部シークレットは保持されます。 |
sops_add_secrets | 既存のファイルに新しいシークレットを追加します。3種類すべてのソースに対応しています。既存のキーとの衝突は拒否します。 |
sops_update_external | external シークレットの値を置き換えます。そのシークレットを参照する派生シークレットにも反映されます。generated または derived の更新は拒否します。 |
sops_rename_secret | キー名を変更し、値とメタデータを保持します。派生シークレット内の from: 参照も更新します。 |
sops_delete_secrets | 1つ以上のキーを削除します。別の派生シークレットが引き続き参照しているシークレットの削除は拒否します(同じ呼び出しで依存するシークレットも削除する場合を除きます)。 |
sops_add_metadata | _meta_unencrypted がないレガシー SOPS ファイルに、後から _meta_unencrypted を追加します。generated、external、derived のエントリーに対応しています。 |
sops_rekey | ファイルをドメインの現在の受信者リストで再暗号化します。ドメインの受信者が変更された後や、「受信者が一致しません」という拒否を解消する場合に実行してください。明示的な domain が必要です。メタデータブロックのないレガシーファイルは、その点について変更しないため、引き続き sops_add_metadata を使用できます。 |
domain 引数は sops_list_domains を除くすべてのツールで指定できます。sops_rekey では、ファイルを読めるユーザーが変わるため必須です。それ以外では省略できます。キー ドメインを参照してください。
セットアップ
前提条件
- Python 3.11+
- sops CLI バイナリ
- age キーペア(下記参照)
age キーペアの生成
まだキーペアがない場合は、age をインストールして、次を実行します。
age-keygen -o age-key.txt
ファイルは次のようになります。
# created: 2026-04-22T12:34:56Z
# public key: age1abc...xyz
AGE-SECRET-KEY-1HH...
- 公開鍵(
age1...)— このサーバーにSOPS_MCP_AGE_PUBLIC_KEYとして渡します。どこで共有しても安全です。 - 秘密鍵(
AGE-SECRET-KEY-...)— CI/CD シークレットとして保存します(一般的な名前はSOPS_AGE_KEYです)。ソース管理にコミットしないでください。この鍵を持つ人は、対応する公開鍵で暗号化されたすべてのsecrets.enc.yamlを復号できます。
秘密鍵は安全な場所(パスワードマネージャー、ハードウェアトークンなど)にバックアップしてください。秘密鍵を失うと、暗号化したすべてのシークレットにアクセスできなくなります。
インストール
git clone <repo-url>
cd sops-mcp
python3 -m venv .venv
.venv/bin/pip install -e .
Claude Code の設定
プロジェクトの .mcp.json に追加します。
{
"mcpServers": {
"sops-mcp": {
"command": "/path/to/sops-mcp/.venv/bin/python",
"args": ["-m", "sops_mcp"],
"env": {
"SOPS_MCP_SOPS_BINARY": "/path/to/sops",
"SOPS_MCP_AGE_PUBLIC_KEY": "<your-age-public-key>"
}
}
}
}
環境変数
| 変数 | 必須 | 用途 |
|---|---|---|
SOPS_MCP_AGE_PUBLIC_KEY | はい* | 暗号化用の Age 公開鍵 |
SOPS_AGE_RECIPIENTS | はい* | SOPS_MCP_AGE_PUBLIC_KEY の代替 |
SOPS_MCP_SOPS_BINARY | いいえ | sops バイナリへのパス(デフォルト:sops) |
SOPS_MCP_LOG_LEVEL | いいえ | ログレベル(デフォルト:WARNING) |
SOPS_AGE_KEY | 場合による | default ドメイン用の Age 秘密鍵 — そのドメインに属するファイルを変更する場合に必要です。名前付きドメインでは、ドメインファイルから鍵を取得します。 |
SOPS_MCP_DOMAINS_FILE | いいえ | 名前付き鍵ドメインを定義する YAML ファイルへのパス。1 つのサーバーで複数の宛先セットが必要な場合に使用します。 |
SOPS_MCP_DOMAINS | いいえ | 同じドメイン定義をインラインで指定します。YAML またはコンパクトな JSON を使用します。公開鍵の宛先セットのみ指定できます。ここで定義するドメインには、keys や key_file を含められません。SOPS_MCP_DOMAINS_FILE とマージされます。 |
SOPS_MCP_REQUIRE_DOMAIN | いいえ | 1 に設定すると、すべてのツール呼び出しでドメインの指定が必須になります。ファイルに記録されたドメインと default フォールバックはどちらも無効になります。 |
SOPS_MCP_TRANSPORT | いいえ | stdio(デフォルト)または sse |
SOPS_MCP_HOST / SOPS_MCP_PORT | いいえ | SSE トランスポートのバインド先ホスト/ポート(デフォルト:127.0.0.1:55090)。0.0.0.0 へのバインドには SOPS_MCP_API_TOKEN が必要です。設定されていない場合、サーバーは起動を拒否します。 |
SOPS_MCP_ALLOWED_HOSTS | いいえ | SSE の Host ヘッダーに対する、カンマ区切りの許可リスト(DNS リバインディング対策)。デフォルト:127.0.0.1,127.0.0.1:*,localhost,localhost:*。ループバック以外のアドレスにバインドする場合は、明示的に設定します(例:mcp.example.com,mcp.example.com:*)。 |
SOPS_MCP_API_TOKEN | 場合による | SSE トランスポートを 0.0.0.0 にバインドする場合は必須です。それ以外の場合は任意です。設定すると、SSE では Authorization: Bearer <token> が必要になります。 |
* SOPS_MCP_DOMAINS_FILE または SOPS_MCP_DOMAINS に少なくとも 1 つのドメインが含まれていない限り、SOPS_MCP_AGE_PUBLIC_KEY または SOPS_AGE_RECIPIENTS のいずれかを設定する必要があります。ドメインがまったくない場合、サーバーは起動を拒否します。
どちらの宛先変数もカンマ区切りのリストを受け付け、SOPS_AGE_KEY は改行区切りで複数の鍵を受け付けます。これらを合わせて default ドメインを構成します。
鍵ドメイン
ドメインとは、名前付きの Age 宛先のセットと、任意でサーバーが保持するそれらの秘密鍵をまとめたものです。サーバーが暗号化と復号に使用する単位です。
ドメインを設定する必要はありません。上記の環境変数が default というドメインになり、宛先が 1 つだけの構成では、以降ドメインについて指定する必要はありません。
ドメインを使う理由
すべての変更ツールは、復号、変更、再暗号化を行います。再暗号化にはサーバーに設定された宛先を使用します。そのため、ファイルが CI 鍵 と オペレーター鍵で暗号化されている一方で、サーバーが CI 鍵しか認識していない場合、以前はオペレーターがファイルから黙って削除されていました。ドメインを使うと、宛先セットに名前を付けて照合できるため、これを防げます。
- 変更処理では、ファイルの実際の宛先とドメインの宛先を比較します。異なる場合は、ファイルを読める人を黙って変更するのではなく、処理を拒否します。
- 1 つのサーバープロセスで、複数の独立した鍵セットを保持できます。
sops_rekeyは意図的な宛先変更を実行します。これはsops updatekeysのワークフローです。
複数の宛先を 1 つのドメインにまとめる
一般的なケースでは、ドメインファイルは必要ありません。通常の変数にすべての宛先を列挙します。
SOPS_MCP_AGE_PUBLIC_KEY="age1ci...,age1operator..."
SOPS_AGE_KEY="AGE-SECRET-KEY-1CI..."
これでファイルは両方の宛先に対して暗号化され、変更後も両方が維持されます。サーバーに必要なのは、実際の復号に使用する秘密鍵だけです。
複数のドメイン
SOPS_MCP_DOMAINS_FILE に YAML ファイルを指定します。
version: 1
domains:
homelab:
recipients:
- age1ci... # CI runner
- age1operator... # operator laptop
keys:
- AGE-SECRET-KEY-1CI...
client-acme:
recipients:
- age1acme...
key_file: /run/secrets/acme.agekey # an age keys file
archive:
recipients:
- age1archive...
# no keys: this domain can encrypt but never decrypt
version: の意味
version: は、この設定ドキュメントのスキーマバージョン、つまりドメインが持つことのできるフィールドと、その配置方法を示します。キー、受信者、ローテーションするものなどのバージョンではありません。ドメインの受信者をローテーションしても変わりません。ドキュメント形式自体がリリースで変更されるまでは、1 のままです。
省略可能で、省略した場合は 1 と見なされます。指定する場合は完全に一致していなければならないため、version: 2 のドキュメントは、1 しか理解できないサーバーによって一部だけ読み込まれるのではなく、拒否されます。このフィールドの目的はそこにあります。認識できないフィールドを持つドメインは拒否されるため、バージョンを確認しないと、実際の問題はドキュメント全体がサーバーより新しいことなのに、古いサーバーはフィールド名のせいだと誤って判断してしまいます。
シークレットファイルの _meta_unencrypted ブロック内にある version は、別の番号で、そのファイルのメタデータスキーマをバージョン管理するものです。この2つに関係はなく、どちらもたまたま 1 です。
ドメインファイルは、グループまたはその他のユーザーから書き込み可能であってはならず、サーバーの実行ユーザーまたは root が所有している必要があります。このチェックは、ファイルに鍵の素材が含まれているかどうかにかかわらず適用されます。ファイルによって、すべてのデータの暗号化先となる受信者が決まるためです。
秘密鍵を含むファイル、つまりインラインの keys: を持つドメインファイルや、任意の key_file は、さらにその他のユーザーから読み取り可能であってはなりません。警告付きであればグループからの読み取りは許可されるため、root 所有でランタイムグループから読み取り可能なコンテナシークレットのマウントは機能します。公開イメージではサーバーは uid 65532 で実行されるため、Docker または compose のシークレットには、そのユーザーが読み取れるように uid:/gid:/mode: を設定する必要があります。通常は、グループを一致させた mode: 0640 が適切です。
起動時に、すべての秘密鍵を解析できることを確認し、公開鍵部分がそのドメインの受信者リストにない場合は警告します。これはエラーではなく警告です。受信者のローテーション中は通常の状態だからです。古い鍵は変更前に暗号化されたファイルを引き続き開くことができ、それらを移行するには sops_rekey を使います。
ファイルを使わずにドメインを定義する
秘密鍵を置けるのはドメインファイルだけです。アクセス許可を確認できるのがそのファイルだけだからです。秘密鍵をまったく持たないドメイン、つまり誰かほかの人が鍵を保持する、暗号化先としての受信者セットには適していません。そのため、ファイルの書き込みが難しいデプロイメント(シェルのない distroless イメージや、インラインファイルのプリミティブがないオーケストレーター)では、公開鍵の素材を2行渡すためにボリュームをマウントする必要がありました。
SOPS_MCP_DOMAINS には、同じドキュメントをインラインで指定できます。
SOPS_MCP_DOMAINS='{"version":1,"domains":{"archive":{"recipients":["age1archive..."]}}}'
YAML も使えます。複数行の値が扱いにくい環境でも使える形式が JSON であり、YAML は JSON のスーパーセットなので解析もできます。
ここでの version は、キーのバージョンではなく、ドキュメントのスキーマバージョンです。ファイル形式と同じフィールドです。
ここで定義するドメインでは、keys や key_file を設定できません。いずれかを設定すると、サーバーは起動を拒否します。環境変数は docker inspect や /proc/<pid>/environ から見えるうえ、ファイルに適用される所有者とモードのチェックも行われないため、秘密鍵を置く場所として適切ではありません。
2つのソースはマージされるため、鍵を保持するドメインはファイルに、公開ドメインはインラインに配置できます。複数のソースで定義されたドメイン名は、暗黙に解決されるのではなく、致命的なエラーになります。これは、ファイルで default と v1 環境変数を併せて定義した場合にすでに適用されているルールと同じです。
ツールが使用するドメインはどれですか?
優先順位は、domain 引数、次にファイルの _meta_unencrypted ブロックに記録されたドメイン、最後に default です。SOPS_MCP_REQUIRE_DOMAIN=1 を設定すると、最後の手順を無効にして、すべての呼び出しで明示的な指定を必須にできます。
記録された名前はヒントです。確認に使われるのはファイル自身の SOPS エンベロープにある受信者リストです。そのため、ラベルが誤っているファイルは、再キー化されるのではなく拒否されます。
受信者のローテーション
受信者を追加または削除するには、次の2段階の操作が必要です。
- ドメインの受信者リストを編集して、サーバーを再起動します。
- 影響を受ける各ファイルに対して、ドメイン名を指定して
sops_rekeyを実行します。
ファイルの再キー化が完了するまで、そのファイルに対する変更は拒否され、ここを案内するメッセージが表示されます。sops_list_secrets も不一致を報告します。この確認に秘密鍵は必要ありません。
sops_rekey でファイルを別のドメインに移すことはできません。ドメインが記録されたファイルは、同じドメインの現在の受信者リストに対してのみ再キー化できます。別のドメインを指定すると拒否されます。これは、2つのドメインが秘密鍵を共有している場合に重要です。そうでなければ復号に成功し、もう一方のドメインにはない受信者がひそかに削除される可能性があります。ドメイン間でシークレットを移す場合は、意図的に手動で行います。sops CLI で復号してから、新しいドメインに対して sops_create_secrets を実行してください。
このサーバーが処理しないファイル
SOPS では、このサーバーでは再現できない方法でファイルを保護できます。このサーバーは常に、フラットな age 受信者リストに再暗号化するためです。
- age と併用する別のマスターキー(
pgp、kms、gcp_kms、azure_kv、hc_vault)。再暗号化すると、そのキーを持つユーザーが削除されます。 - Shamir キーグループ(
shamir_thresholdを含むkey_groups)。再暗号化すると、n-of-m のしきい値が、1人のキー所有者だけで開けるリストに変わります。 mac_only_encrypted。これを使うと、平文のメタデータブロックが認証されません。そのため、ファイルを編集できる人なら誰でも、記録されたドメインや各シークレットのsourceを書き換えられます。- 別の暗号化モード:
encrypted_regex、unencrypted_regex、encrypted_suffix、コメント用の正規表現、または_unencrypted以外のunencrypted_suffix。このサーバーは--unencrypted-suffix _unencryptedを使って再暗号化するため、どの値が暗号化されずに残るかが変わります。これにはsops_create_k8s_secretが書き込むマニフェストも含まれ、専用のメッセージとともに拒否されます。
これらはどれも、気づかれないまま保護レベルが下がるか、構造が変わるため、すべての変更操作で該当ファイルを拒否し、sops_list_secrets でも警告します。これらのファイルは sops CLI で管理してください。
バージョンが混在している場合
このバージョンが書き込んだファイルは、sops-mcp 0.10.1 以前でも読み書きできます。旧バージョンは domain フィールドを認識しないため、次にファイルを変更したときにこのフィールドを削除し、ドメインが記録されていない状態にします。この場合、このサーバーでは default として扱われます。どちらの場合も受信者チェックによって保護されます。後で domain を明示して呼び出すか、sops_rekey を使ってドメインを再記録してください。
ドメインでは分離できないもの
ドメインで分離されるのはキーのセットであり、呼び出し元ではありません。SSE トランスポートでは、1つの API トークンでサーバーが保持するすべてのドメインにアクセスできます。互いのシークレットを読み取れないようにする必要がある2者がいる場合は、それぞれにサーバープロセスを実行してください。
セキュリティ
- ファイルシステムにアクセスしません — サーバーがクライアントのファイルシステムを読み書きすることはありません。すべてのコンテンツはテキストパラメーターとして渡され、テキストで返されます。
- 通常の使用に秘密鍵は不要です — 暗号化には公開鍵のみを使用します。秘密鍵が必要なのは変更ツールを使う場合だけです。
- 応答に平文は含まれません — 生成されたシークレット値が返されることはありません。派生値は設定ファイルに貼り付けることを想定しているため返されます(例:Authelia の PBKDF2 ハッシュ)。公開するつもりのない値は派生させないでください。
- 安全な一時ファイル — sops CLI の呼び出しにのみ使用します。0600 のアクセス許可で作成され、削除前にゼロで上書きされ、
finallyブロックで確実にクリーンアップされます。 - OS レベルのエントロピー — シークレットの生成には Python の
secretsモジュールを使用します(/dev/urandomを使用)。 - デフォルトの stdio トランスポート — ネットワークに公開されず、クライアントの子プロセスとして実行されます。
- 入力の検証 — キー名は
^[A-Z][A-Z0-9_]*$に一致する必要があります。
暗号化ファイル形式
DB_PASSWORD: ENC[AES256_GCM,data:...,tag:...,type:str]
SMTP_USER: ENC[AES256_GCM,data:...,tag:...,type:str]
DB_PASSWORD_HASH: ENC[AES256_GCM,data:...,tag:...,type:str]
_meta_unencrypted:
version: 1
domain: default
secrets:
DB_PASSWORD:
source: generated
description: Database password
generation:
length: 32
charset: alphanumeric
last_rotated: "2026-04-21T15:30:00Z"
SMTP_USER:
source: external
description: SMTP username
DB_PASSWORD_HASH:
source: derived
derivation:
transform: sha256_hex
from: DB_PASSWORD
last_rotated: "2026-04-21T15:30:00Z"
sops:
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
unencrypted_suffix: _unencrypted
シークレット値は AES-256-GCM で暗号化されます。_meta_unencrypted ブロックは平文で保存されるため(SOPS の unencrypted_suffix 機能を使用)、復号しなくてもメタデータを読み取れます。
このサーバーが実行するすべての sops 呼び出しでは、空の --config が指定されます。そのため、サーバーの起動元ディレクトリにある .sops.yaml によって、ファイルの暗号化方法が変わることはありません。受信者はドメインから取得され、それ以外の情報は使用されません。
上記の sops: ブロックは一部を省略しています。実際のファイルには、lastmodified、mac、version と、このサーバーが使用しないマスターキー種別(pgp、kms、gcp_kms、azure_kv、hc_vault)の空のリストも含まれます。MAC は暗号化されていない値も対象とするため、_meta_unencrypted ブロックを編集すると復号に失敗します。これらの他のキーリストのいずれかが空でない場合、このサーバーはファイルの変更を拒否します。暗号化には age のみを使用するため、そうしなければそのキーホルダーが黙って削除されてしまいます。
出力の形を変えないでください
このサーバーが返す暗号化済みコンテンツは、返されたとおりにコミットする必要があります。SOPS は各値の AES-GCM タグをドキュメント内のその値の パス に結び付けます。そのため、値を別のキーに移動すると、たとえばルートレベルの DB_PASSWORD を stringData: の下にネストして Kubernetes Secret マニフェストを作成すると、ENC[...] 文字列自体を変更していなくてもタグが無効になります。
$ sops decrypt secrets.enc.yaml
Error decrypting tree: Error walking tree: Could not decrypt value: Could not decrypt with AES_GCM: cipher: message authentication failed
この時点で値は復元できなくなります。平文部分を手作業で編集した場合も、別の理由で同じように失敗します。MAC は暗号化されていない値も対象とするため、_meta_unencrypted ブロックを編集すると MAC mismatch が発生します。SOPS はドキュメントを解析するため、インデントの変更、キーの並べ替え、YAML の整形し直しは安全です。重要なのはキーのパスと値だけです。
Kubernetes Secret が必要な場合は、sops_create_k8s_secret(以下を参照)を使って、その形で作成してください。それ以外のドキュメント形式が必要な場合は、暗号化ファイルの形を変えるのではなく、デプロイ時にフラットファイルから作成してください(Kustomize の secretGenerator、helm-secrets、sops-secrets-operator)。
Kubernetes Secret マニフェスト
sops_create_k8s_secret は、各値がすでに stringData の下のパスに配置された Secret マニフェスト全体を書き出すため、後から移動する必要はありません。
apiVersion: v1
kind: Secret
metadata:
name: web-app
namespace: prod
labels:
app.kubernetes.io/name: web
annotations:
sops-mcp.privacyplaybook.com/meta: '{"version":1,"domain":"default","secrets":{"DB_PASSWORD":{"source":"generated","generation":{"length":32,"charset":"alphanumeric"},"last_rotated":"..."}}}'
type: Opaque
stringData:
DB_PASSWORD: ENC[AES256_GCM,data:...,tag:...,type:str]
sops:
...
encrypted_regex: ^(data|stringData)$
- 暗号化されるのは
stringDataのみです(--encrypted-regex '^(data|stringData)$')。apiVersion、kind、metadata、typeは読み取り可能なまま保持され、sops decrypt、ksops、Flux はファイルをkubectl applyがそのまま受け入れられるマニフェストに変換します。 - 平文の項目はすべて引数として指定します。
name、namespace、labels、annotations、typeが該当します。MAC は平文も対象にするため、これらは後から編集できません。ラベルキーとアノテーションキーのdataおよびstringDataは、sops によって暗号化されてしまうため拒否されます。 - キー名には Kubernetes の規則(
[-._a-zA-Z0-9]+)を適用します。環境変数の規則ではないため、型付き Secret ではtls.crtや.dockerconfigjsonなどのキーを使用できます。値は文字列としてstringDataに格納されます。このツールが base64 のdata:を書き出すことはありません。 - メタデータは
sops-mcp.privacyplaybook.com/metaアノテーションに格納します。_meta_unencryptedと同じフィールドを持つ JSON で、ドメインと、Secret ごとのソース、説明、生成方法、派生情報が含まれます。値が格納されることはありません。kubectl の厳密な検証では不明なルートフィールドが拒否されるため、ルートには置けません。クラスターオブジェクトには含まれるため、アノテーションに記載したくない内容を説明に書かないでください。 - このファイルは作成専用です。 すべての変更ツール(
sops_rotate_generated、sops_add_secrets、sops_update_external、sops_rename_secret、sops_delete_secrets、sops_add_metadata、sops_rekey)は、復号せずにsopsエンベロープからこの形式を認識し、変更を拒否します。変更またはローテーションするには、外部値を再度指定して、sops_create_k8s_secretを完全な値セットとともにもう一度呼び出してください。sops_list_secretsはこのファイルを読み取り、stringDataの下のキー名、アノテーションに記載されたソース、ドメインを取得します。 - 必要な最小バージョン。 この拒否機能があるのは、このツールを追加したリリース以降のみです。sops-mcp 0.11.0 以前にはこの保護機能がありません。そのため、
sops_add_secrets、sops_rekey、またはその他の変更ツールを実行すると、マニフェストはフラットファイルとして書き換えられ、kind:とmetadata:が暗号化され、ルートレベルのキーが追加されます。値は引き続き復号できるため失われることはありませんが、マニフェストは適用できなくなります。これらのファイルを古いサーバーに指定しないでください。
他のツールではなく、これらのツールを採用した理由
キー単位の読み取り(decrypt-one-secret): 意図的に提供していません。MCP の境界を越えて平文を返すと、ツールの呼び出し中にモデルが Secret の内容にアクセスできてしまい、意図しない情報漏えいの経路となるためです。平文の値が必要な場合は、age 秘密鍵を使って自分で sops decrypt を実行してください。
生成/派生 Secret のキー単位のインプレース更新: 意図的に提供していません。これらの値を変更する方法は sops_rotate_generated のみです。これにより、ローテーションの監査証跡(last_rotated タイムスタンプ)が残り、派生 Secret にも適切に変更が反映されます。
.sops.yaml の作成ルール: 対象外です。このサーバーはファイルシステムを参照できないため、パスベースの作成ルールは適用できません。代わりに キー ドメイン から受信者を指定し、sops_rekey で updatekeys のワークフローに対応します。
SSE トランスポートでのテナント分離: まだ対応していません。ドメインが分離するのはキーの セット であり、呼び出し元 ではありません。1 つの SOPS_MCP_API_TOKEN でサーバーが保持するすべてのドメインにアクセスできるため、相互に信頼していない 2 者を 1 つの SSE サーバーの背後に置かないでください。それぞれ別のプロセスを実行してください。
その他のソースタイプ(インポート、テンプレート): 対象外です。これらはオーケストレーションの問題です。Vault から値を取得するか、デプロイ用のテンプレートレイヤーで URL を組み立て、その結果を external Secret としてここに渡してください。
サプライチェーンの完全性
Docker のビルドは 3 層の検証で強化されており、CI ゲートによって適用されます。
ベースイメージのダイジェスト固定
Dockerfile は cgr.dev/chainguard/python(Wolfi)をベースにビルドします。SHA-256 ダイジェスト(@sha256:...)で固定しているため、Docker はタグが現在指しているイメージではなく、監査済みの正確なイメージを常にプルします。2 つのステージは個別に固定されています。ビルドステージの :latest-dev には apk、シェル、ビルドツールチェーンが含まれ、ランタイムステージの :latest は distroless で、シェルもパッケージマネージャーもありません。両方のダイジェストと cosign の署名ステータスは base-images.lock.json で追跡されています。
ベースイメージを更新する場合(上流でセキュリティパッチが公開されたとき):
pip install requests # one-time; cosign on PATH is needed for the signature checks
python3 lib/pin_base_images.py
Wolfi の署名済みバイナリ
sops と age のバイナリは、Chainguard が署名したインデックスを使用する Wolfi のパッケージリポジトリから、apk でインストールされます。これらはベースイメージのダイジェストを介して間接的に固定されているため、ベースイメージを再固定すると、これらのバイナリも再固定されます。
Python 依存関係のハッシュ固定
ランタイム依存関係は requirements.lock.txt から pip install --require-hashes でインストールされます。このオプションは、記録された SHA-256 ハッシュと内容が一致しないパッケージを拒否します。これにより、依存関係の乗っ取りやタイポスクワッティングを防止します。
requirements.in を編集した後に依存関係を更新するには:
pip install pip-tools # one-time
lib/compile_requirements.sh
CI による検証
supply-chain.yml ワークフローは、lib/verify_requirements.py、lib/verify_base_images.py、lib/verify_version.py を実行した後、ロックファイルに既知のアドバイザリがないか監査します。main へのすべてのプルリクエスト、および Dockerfile、ロックファイル、pyproject.toml、server.json、lib/ のいずれかに変更がある main へのプッシュで実行されます。ロックファイルの形式が正しいこと、Dockerfile のすべての FROM 行でダイジェストが固定されていること、server.json のバージョンが pyproject.toml のバージョンと一致すること、固定された依存関係に公開済みの脆弱性がないことを確認します。
この監査は公開ワークフローと同じゲートであるため、リリースをブロックするロックファイルは、今後はプルリクエストもブロックします。最新のアドバイザリデータを照会するため、固定された依存関係に新たなアドバイザリが登録されると、無関係なプルリクエストでも失敗することがあります。これは意図したトレードオフです。ロックファイルを更新して対処してください。
pip-compile は指示がない限り既存の固定バージョンを維持するため、アドバイザリへの対応後に再生成する際は、アップグレードフラグが必要です。
lib/compile_requirements.sh --upgrade
公開リリースの検証
GHCR に公開されるすべてのコンテナイメージには、このリポジトリの公開ワークフローによる keyless 署名が付与されます。署名には sigstore 経由で cosign を使用し、SLSA v1.0 ビルド来歴と SPDX SBOM が OCI アーティファクトとして添付されます。main のすべてのコミットと、すべてのリリースタグには SSH 署名が付与されます。長期間有効な鍵情報を保有しなくても、これらすべてを検証できます。署名は sigstore の公開透明性ログに記録されています。
cosign のインストール先:https://docs.sigstore.dev/system_config/installation
イメージ署名を検証します。 検証が成功すれば、イメージのダイジェストが誰かから送られた参照と一致するだけでなく、このリポジトリの publish.yml ワークフローによってイメージがビルドされたことが証明されます。
cosign verify \
--certificate-identity-regexp 'https://github.com/privacyplaybook/sops-mcp/\.github/workflows/publish\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/privacyplaybook/sops-mcp:<tag>
アテステーションを確認します。
# List all artifacts attached to the image
cosign tree ghcr.io/privacyplaybook/sops-mcp:<tag>
# Download the SBOM (SPDX JSON)
cosign download sbom ghcr.io/privacyplaybook/sops-mcp:<tag>
# Verify the SLSA build provenance
cosign verify-attestation --type slsaprovenance1 \
--certificate-identity-regexp 'https://github.com/privacyplaybook/sops-mcp/\.github/workflows/publish\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/privacyplaybook/sops-mcp:<tag>
Git タグとコミットを検証します。 最も簡単な確認方法は、github.com の コミット ページと タグ ページに表示される緑色の Verified バッジです。
開発
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
pytest tests/ -v # unit tests plus end-to-end sops round-trips
ruff check src/ tests/
例:CI/CD デプロイパイプラインとの統合
一般的なパターン:このサーバーを使って、インフラリポジトリにコミットする secrets.enc.yaml ファイルを生成し、CI で復号して、プレーンテキストの値を環境変数としてコンテナオーケストレーター(Portainer、Kubernetes、Nomad)に渡します。
- age キーペアを生成します。秘密鍵は CI のシークレット(例:
SOPS_AGE_KEY)として、公開鍵は Claude Code のSOPS_MCP_AGE_PUBLIC_KEYとして設定します。 sops_create_secretsを使ってsecrets.enc.yamlを生成するよう、モデルに依頼します。- 暗号化されたファイルをコミットします。
- デプロイワークフローで
sops decrypt secrets.enc.yaml > .env(または同等の処理)を実行し、結果をオーケストレーターに渡します。 - シークレットをローテーションする必要がある場合は、
sops_rotate_generatedまたはsops_update_externalを実行するようモデルに依頼し、コミットして再デプロイします。
_meta_unencrypted ブロックを使うと、オーケストレーターへの値の送信時に、ツールでメタデータ(_ で始まるキー)を除外できます。そのため、メタデータが環境変数に漏れることはありません。
ライセンス
Apache-2.0。詳細は LICENSE を参照してください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。