sinter secrets
目的: シークレットファイル(SSH 鍵、.env、トークン、パスワードハッシュ、
任意のバイナリ)を、標準の age 形式で暗号化
したままリポジトリに置く。ファイルは通常の .age ファイルで、Sinter なしでも
age 互換ツールで復元できます。
sinter secrets encrypt [--passphrase | -r, --recipient RECIPIENT ...] [-o, --output OUT] [-f, --force] <FILE | ->sinter secrets decrypt [-i, --identity IDENTITY] <FILE>sinter secrets list [--format text|json] [--recipe FILE ...] [PATH ...]これらがフラグのすべてで、ここに示した短い形と長い形以外の別名はありません。
--passphrase は -r/--recipient(複数指定可)と併用できません。decrypt の
--identity は SSH 鍵ではなく age の identity で、パスフレーズで暗号化された
ファイルに対してはエラーになります。--recipe は複数指定でき、list 専用です。
- encrypt は
FILE.age(または-o OUT)を書きます。-は stdin を読み、 その場合は-oが必要です。入力は不透明なバイト列で(UTF-8 を仮定せず、 改行も変更しない)、最大 16 MiB です。元のファイルは変更も削除もされません。 SSD やコピーオンライトのファイルシステムでは安全な削除を保証できないため、 元のファイルは自分で削除してください。 - decrypt は平文を標準出力にのみ書き、端末への出力は拒否します
(リダイレクトまたはパイプしてください)。ファイルは作りません:
sinter secrets decrypt x.age > x。 - list は指定パス(既定
.)以下の*.ageファイルをヘッダーから一覧します (.gitは除外、シンボリックリンクは辿らない): 状態、方式 (passphraseまたはrecipients)、受信者数。復号はしません。明示した ファイルは名前に関係なく内容で判定します。受信者数には age 形式が意図的に 加えるダミー stanza を含めません。age はどの受信者かを示さないため、 Sinter が「復旧用受信者」と主張することはありません。--recipe FILE(複数指定可。レシピまたはバンドル)を付けると、指定した レシピがファイルについて示す内容が加わります。何も自動検出しません。読むのは 指定したレシピだけです。各レシピはvalidateと同じローダーで読み込まれ、 構造・フィールド・include・シークレット参照の文字列は同じ規則で検査されます。 参照先ファイルの状態だけは、必須とせず観測します。各シークレットに 参照元(type:idと、書いたとおりのレシピ引数。リソースの値は出しません) を追加し、次の結論を示します:missing状態: 有効な参照だが、ファイルが存在しない。コマンドは失敗せず (終了コード 0)、同じレシピはvalidateでは引き続き拒否されます。 シンボリックリンクを通る参照、ディレクトリなど通常ファイル以外、 検査できない参照(権限不足など)は注記付きのunreadable、age でない ファイルはnot-age/invalidで、いずれもmissingとは呼びません。- 指定した N 個のレシピから参照されていない: 一覧対象のパスにある age
ファイルで、指定したどのレシピも参照しないもの。これは指定したレシピに
ついてだけの記述で、ファイルが未使用である、削除してよい、という意味
ではありません(別のレシピ、別のリポジトリ、人が使っているかもしれません)。
照合はファイルの実体(デバイスと inode)で行うため、別の表記、ハードリンク、
大文字小文字を区別しないファイルシステムでの綴り違いも参照済みと扱います。
照合を確立できない場合は結論を出しません。
when付きやstate: absentの リソースも参照として数えます。 - Sinter の ID 探索が選び得るファイル(
identity.age)には、「参照されて いない」ではなく注記を付けます。参照なしとして提示することはありません。 認識するのはidentity.ageという名前のファイル(またはそのディレクトリ内の 同一ファイルの別名)だけです。別の場所や別名に置いた ID(--identityやSINTER_IDENTITYで指定したものなど)は認識しないため、この一覧を削除候補 リストとして扱わないでください。参照を検査できない場合(権限不足など)は、 「参照されていない」という結論を一切出しません。 いずれかのレシピが読み込めない場合(YAML 不正、未知のフィールド、不正な 参照・include、読めないファイル)は、終了コード 2 で、失敗したレシピをすべて 標準エラーに示し、一覧も結論も一切出力しません。--recipeは一覧対象の パスを変えず、復号・パスフレーズ入力・ID ファイルの探索も行いません。--recipeなしの出力は従来どおりです。
list の出力
Section titled “list の出力”list はファイルから導けること以外は何も出力しません。復号も、パスフレーズ入力も、
identity の探索もしません。出力は、シークレットの中身や、ほかの場所での使われ方に
ついての証拠にはなりません。
テキスト。 見出し行 STATUS METHOD RECIPIENTS PATH、続いてファイルごとに 1 行
(該当しない列は -)で、注記と、--recipe 指定時は [referenced by …] や
「参照されていない」の表示が付きます。ファイルが 1 つもなければ
no age files found を出力します。上限に達した場合は、最後の行に一覧が切り詰め
られたことを示します。--recipe 指定時は、最終行で、分析が指定したレシピだけを
対象にしたことを示します。
JSON(--format json):
{ "command": "secrets list", "secrets": [ { "path": "secrets/app.age", "status": "ok", "method": "recipients", "recipients": 2, "armored": false, "note": null, "referenced_by": [{ "recipe": "site.yaml", "resource": "file:app_key" }], "not_referenced": false, "possible_identity_file": false } ], "truncated": false, "recipes": ["site.yaml"]}method と recipients は、状態が ok でなければ null です。referenced_by、
not_referenced、possible_identity_file、最上位の recipes は --recipe 指定時
のみ現れます。not_referenced は true、false、null のいずれかで、null は
結論を出す根拠がないこと(解析済みの age ファイルではない、identity ファイルの
可能性がある、ファイルの実体を特定できない、または検査できない参照がある)を
意味します。
| 状態 | 意味(すべてファイルのヘッダーとパスから判断し、復号はしない) |
|---|---|
ok |
正しい age ヘッダーを持つ通常ファイル。方式、受信者数、armor の有無を報告します。 |
not-age |
age 形式ではない通常ファイル、またはヘッダーが不正か Sinter の上限を超えているもの。 |
unsupported |
Sinter が読めない形式・バージョンの age 風ファイル。 |
too-large |
16 MiB のシークレットに対して Sinter が受け付ける最大の age ファイルより大きい。 |
invalid |
age ファイルのように始まるが、age ファイルとして読めない(例: armor の破損)。 |
unreadable |
通常ファイルではない、シンボリックリンク(辿らない)、または読めない・検査できない。読み取り予算に達してスキップしたファイルも含みます。注記がどれかを示します。 |
missing |
--recipe のみ: 有効な参照だが、ファイルが存在しない。 |
状態は 1 つのファイルのヘッダーについての記述です。ok は、そのファイルが手元の
どの鍵でも復号できる、鍵が存在する、受信者の一覧が意図どおりである、という意味では
ありません。list はどの受信者かを示せず、受信者が 1 つのファイルとパスフレーズの
ファイルには、その鍵やパスフレーズを失うとシークレットを失うことを示す注記が付きます。
上限。 走査は深さ最大 32 ディレクトリ、最大 10,000 ファイル、最大 100,000 の
ディレクトリエントリで、読み取りは合計で最大 512 MiB です。いずれかの上限で止まると
truncated が true になります(テキストでは「listing truncated」)。.git は除外
し、シンボリックリンクは辿りません。
終了コード。 一覧を出力したら 0(missing、unreadable、not-age の行を含む
場合も)。指定したパスが存在しない場合、または --recipe のいずれかが読み込めない
場合は 2(その場合は何も一覧しません)。参照されていないシークレットがあっても
list は失敗しません。
not referenced の正しい読み方。 意味は次のとおりで、これ以上ではありません:
一覧対象のパスにある age ファイルで、指定した N 個のレシピのどれも参照しないもの。
そのファイルがどこでも未使用である、削除してよい、という意味ではありません。
ほかのレシピ、リポジトリ、ブランチ、スクリプト、人が使っているかもしれず、条件付き
(when)や state: absent のリソースも参照として数えます。possible_identity_file
は Sinter の identity 探索が選び得るファイル(identity.age という名前、またはその
ディレクトリ内の同一ファイルの別名)を示し、そのようなファイルは「参照されていない」
とは決して報告されません。別の場所にある identity は認識しないため、一覧を削除
候補リストとして扱わないでください。
| 方式 | 指定 | 備考 |
|---|---|---|
| 受信者 | -r age1…(複数可)、または最も近い recipients.txt |
いずれか 1 つの対応する identity(秘密鍵)で復号できます。複数の受信者で冗長性を持てます。 |
| パスフレーズ | --passphrase |
端末で入力(エコーなし)、2 回確認、12 文字以上。強度は完全にパスフレーズ次第です。 |
受信者(-r、および recipients.txt の各行)はネイティブな age 受信者 age1…
(X25519)でなければなりません。SSH 公開鍵(ssh-ed25519、ssh-rsa)、プラグイン
受信者(age1plugin1…)などは拒否され、1 ファイルあたり最大 256 です。identity も
同様に、ネイティブな AGE-SECRET-KEY-1… の鍵か、パスフレーズで暗号化された age
ファイルです。Sinter が書くファイルは標準の age ファイルなので、どの age ツールでも
読めます。一方、Sinter 自身が読めるのは、scrypt の作業係数が 2^20 以下のパスフレーズ
ファイルと、ネイティブ identity 向けのファイルだけで、ほかのツールが SSH やプラグイン
受信者向けに作ったファイルは開けません。
パスフレーズと受信者を 1 つのファイルに混在させることはできません(age 形式が
禁止しています)。どちらも指定しない場合、出力先ディレクトリ、またはリポジトリ
ルート(.git を含むディレクトリ)までの親にある最も近い recipients.txt
を使います(リポジトリがなければ出力先ディレクトリのみ検索)。Sinter は使用した
recipients.txt と各受信者の短いフィンガープリントを必ず表示し、端末では確認を
求めます。複製したリポジトリに付属する recipients.txt は、暗号化したものを
誰が読めるかを決めるので、内容を確認してください(または -r を指定)。なければ、
端末が使えるときは対話で選択(パスフレーズ、または新しい鍵ペア)し、端末が
なければ 2 つのフラグ形式を示して失敗します。
パスフレーズは端末(/dev/tty)からのみ受け付けます。引数、環境変数、
stdin、ファイルからは受け付けません。したがって自動化や CI は、パスフレーズ
ではなく受信者と identity を使います。
identity(秘密鍵)とその置き場所
Section titled “identity(秘密鍵)とその置き場所”受信者で暗号化されたシークレットの decrypt は、次の順で identity を探し、
最初に見つかった 1 つだけを使います(候補間の試行錯誤はしません):
--identity PATHSINTER_IDENTITY— 鍵そのものではなくパス- リポジトリ外の既定 identity:
$XDG_CONFIG_HOME/sinter/identity(絶対パスのXDG_CONFIG_HOMEのみ)、なければ~/.config/sinter/identity - シークレットのディレクトリまたはリポジトリルートまでの親にある、
パスフレーズ保護された
identity.age
identity ファイルは、平文の age identity(AGE-SECRET-KEY-1…)か、
パスフレーズで暗号化された age ファイル(端末で入力)のいずれかです。平文の
identity は自分だけが読めて(chmod 600)自分の所有である必要があり、そうで
なければ拒否されます。リポジトリ内で見つかった平文の identity.age は自動では
決して使われません。リポジトリ内の identity.age がシンボリックリンクの場合も
拒否されます。
encrypt の対話メニュー(2 番目の選択肢)で鍵ペアを作るとき、秘密鍵の置き場所を
尋ねます。秘密鍵は常にパスフレーズで保護され、置き場所は:
- リポジトリ外(
~/.config/sinter/identity、推奨の既定)、または - このリポジトリ内(
recipients.txtと同じ場所のidentity.age)。 閉域網や簡易構成向けの、明示的な選択です。
2 つの運用モデル
Section titled “2 つの運用モデル”| A. 本番 / 分離 | B. 閉域網 / 簡易 | |
|---|---|---|
| 暗号文 | リポジトリ内 | リポジトリ内 |
| 秘密鍵 | リポジトリ外(自動化向けは無保護、またはパスフレーズ保護) | リポジトリ内のパスフレーズ保護された identity.age |
| 盗まれたリポジトリの複製を復号できる人 | 鍵ファイルなしでは誰も不可 | パスフレーズを推測できる人 |
| 自動化 / CI | リポジトリ外に置いた無保護 identity | パスフレーズ(端末のみ)なしでは不可 |
モデル B は鍵の分離と同等ではありません。 リポジトリがコピーされると、
パスフレーズが唯一の防御となり、1 つのパスフレーズがすべてのシークレットを
守ることになります。長いパスフレーズ(または生成したもの)を使い、このトレード
オフが許容できない場合はモデル A を使ってください。自動化用の無保護 identity は
標準の age-keygen で作成します。Sinter は無保護の identity を生成しません。
レシピでシークレットを使う
Section titled “レシピでシークレットを使う”file リソースは、リテラルやコントローラ上のファイルの代わりに、暗号化された
シークレットから内容を取れます。
- id: app_key type: file with: path: /home/app/.ssh/id_ed25519 content: { secret: secrets/app-id-ed25519.age } owner: app mode: "0600"- 参照は、それを書いたレシピファイル(include されたレシピなら、そのレシピ自身の
ディレクトリ)からの静的な相対パスで、1024 バイト以下です。絶対パス、バックスラッシュ、
制御文字、
{{/}}(あらゆる補間)、空・.・..の要素を含むもの、またはそのディレクトリ 以下のいずれかの要素がシンボリックリンクであるものは拒否されます。対象は、正しい age ヘッダーを持つ通常ファイルでなければなりません。値は厳密に{ secret: <path> }で なければならず、contentとsourceは引き続き排他です。シークレット参照を受け付けるのはfile.contentとuser.password_hashだけです。 - 方式はレシピに書きません。 パスフレーズで開くか identity で開くかは age の ヘッダが示し、Sinter はそれに従います。
- シークレットを持つリソースは常に sensitive です。差分は伏せられ、新規ファイルの
既定モードは
0600、診断メッセージも、リソースの宣言に関わらず伏せられます。 validateは、参照が許される形であること、ファイルが存在し正しい age ファイルで あることだけを確認します。復号はせず、鍵も要りません。plan/apply/auditは復号します。 目的のファイルの SHA-256 に平文が 必要だからです(推測しやすいシークレットのハッシュを保存すると推測の手掛かりに なります)。平文は変更のないファイルパイプラインを通ってターゲットへ渡り、 コントローラ上に一時ファイルは作りません。内容は正確なバイト列として比較・公開 され、改行や行末は変わりません。鍵がない場合、planとapplyはそのリソースを 失敗させ、auditはターゲットにファイルが存在するときERRORを報告します。COMPLIANTとは決して報告せず、apply が部分的な内容のファイルを書くこともありません。 レポートの文面は伏せられます(リソースは sensitive)。原因(例: identity が見つからない、 パスフレーズ保護のシークレットには端末が必要)は標準エラーにsecret unavailable: <reference>: <cause>として 1 度だけ出力されます。原因の 文言は診断用で、安定したインターフェースではありません。 ターゲットにファイルがなければ、auditはシークレットを開かずにDRIFTとします。state: absentは鍵を必要としません。applyは各リソースに到達した時点で シークレットを開くため、先にplanを実行してください。何かを変更する前に、鍵が 使えないことを報告します。- identity の探索は上の順序から
--identityを除いたものです。plan/apply/auditの--identityはすでに SSH 秘密鍵を意味し、シークレットには 使われません。SINTER_IDENTITY(パス)、既定の identity ファイル、またはリポジトリ内の パスフレーズ保護されたidentity.ageを使います。保護された identity は 1 回の 実行で 1 度だけ、端末でのみ解除されます(解除に失敗しても再度は尋ねません)。 パスフレーズ方式のシークレットは、ファイルごと・実行ごとに 1 度、端末でパスフレーズを 尋ねます。無人実行には受信者方式のシークレットを使ってください。 user.password_hash: { secret: <path> }も同じ参照ルールと identity 探索を使い、 シークレットはパスワードハッシュ 1 行です。user リソースを 参照してください。- MCP のマニフェスト系ツールは
content/password_hashのシークレット参照を拒否 します。クライアントが送ったマニフェストでゲートウェイにファイルを復号させることは できません。
平文は、使い終えたら Sinter がゼロ化するバッファに保持され、既存の SSH チャネル (またはローカルのパイプ)の標準入力でターゲットに送られます。コマンドラインや 環境変数には載りません。SSH ライブラリや OS 内部のコピーは Sinter の管理外です。 コントローラやターゲット上の root に対する保護はありません。
出力の安全性
Section titled “出力の安全性”- 暗号文は出力先ディレクトリの一時ファイル(モード 0600、排他的作成)に書き、 同期してから原子的に公開し、ディレクトリエントリも同期します。平文の一時 ファイルは作りません。
- 既存の出力を黙って置き換えることはありません。
--forceは、既存ファイルが age ファイルである場合に限り置き換えます。出力先がシンボリックリンクなら常に 拒否します。入力は通常ファイルである必要があります(シンボリックリンクは 拒否)。 - ハードリンクのないファイルシステム(一部のネットワークや exFAT ボリューム) では、上書きしない公開は排他的作成にフォールバックします。既存ファイルを置換 することはありませんが、原子的ではありません。
- 暗号文を標準出力や端末に書くことはありません。
- stdin と stdout はバッファなしで使い、パスフレーズ入力中のシグナルでは端末の エコー設定を復元します。
- プロセスはシークレットを扱う前にコアダンプ(Linux では ptrace のアタッチも) を無効化し、復号したバッファはベストエフォートでゼロ化します。同一マシンの root に対する防御にはなりません。
0 成功、2 使い方・ポリシー・入力の問題(フラグ不足、存在しない・拒否された
ファイル、identity が見つからない、危険なパーミッション、パスの代わりに鍵が
入った identity 設定、受信者の未確認)、5 処理の失敗(鍵やパスフレーズでは
開けない、データ破損、I/O)。メッセージは固定文言とユーザーが指定したパス(名前の
制御文字・双方向制御文字は置換)のみで、平文・パスフレーズ・鍵・受信者の全文は
含みません。
復旧とローテーション
Section titled “復旧とローテーション”- 使える identity とパスフレーズがすべて失われると、シークレットは復元 できません。Sinter に復旧の仕組みはなく、鍵のエスクローもしません。
- 必要になる前に、別の受信者(別の場所に保管した予備・復旧用の鍵)を追加
してください:
sinter secrets decrypt a.age | sinter secrets encrypt -r NEW1 -r NEW2 --force -o a.age -。 ファイルから、ある受信者が「復旧用」かどうかを Sinter が判断することはできません。 - 再暗号化は、すでにコピーされた暗号文を無効化しません: 古いコピーと古い鍵を 持つ人は、そのコピーを読めます。鍵やパスフレーズが漏えいした可能性がある場合は、 元のシークレットを発行元でローテーションしてください(新しい SSH 鍵、 新しいパスワード、新しいトークン)。
listは、受信者が 1 つのファイルとパスフレーズのファイル(忘れたパス フレーズは復旧できません)について警告します。
1 つのシークレットは 16 MiB まで、1 ファイルの受信者は 256 まで、パスフレーズは 1024 バイトまで。age ヘッダーは上限があり、過大または敵対的なヘッダーは鍵の処理の前に 拒否されます。
転送の期限。 シークレットを使うファイルは、内容を標準入力で受け取る 1 つのコマンドで ターゲットに書き込まれ、ターゲット上のコマンドにはすべて 300 秒の期限があります。したがって 16 MiB のシークレットには、持続的に約 56 KiB/s 以上の速度が必要です(実測: 実ターゲットの ループバック SSH で 4.2 秒と 4.4 秒。これは最良のケースで、ネットワーク経由の測定では ありません)。書き込み中に期限を過ぎると、そのリソースは不確定(終了コード 6)と報告され、 実行は止まります。内容はプライベートなステージングファイルに書かれ、書き込み完了後にだけ 所定の場所へ移されるため、書き込み先が中途半端な状態で残ることはありません。ただし結果が 不明なため、書き込み先の隣にあるプライベートなステージングディレクトリが残ることがあります。 これより遅い転送はサポートされません。ターゲットへのより速い経路を使ってください。ほかの ファイルの書き込みにも同じ期限があります。