secret. 参照を解決します。これはエンジンの境界モデル(シークレットは値ではなく 境界 である)をチーム環境に拡張します。単一の browserflow run で成り立つのと同じ不変条件が、すべてのスケジュール実行、すべてのフロント、すべての環境にわたって成り立ちます。
ブローカーは コントロールプレーン の一部であり、採用してもFlowの実行方法は決して変わりません(原則 T6)。追加されるのは調整であって、capabilityではありません。
境界の不変条件をワーカー群全体へ
browser-flowにおけるシークレットは、値が最後の可能な瞬間にだけ越える境界です。secret: true とマークされた入力スロットは次の性質を持ちます。
- FlowSpec、Journal、ログ行、公開済みPackのいずれにも現れない。
secretscapabilityを持つToolが、注入されたSecretsproviderを通じて解決する瞬間まで、参照としてだけ運ばれる。- そうでなければ漏えいし得るすべての場所でマスクされる。
概念モデル、つまりシークレットが参照である理由、静的に強制されるtaint rule、そして漏えいさせずにeffectを記録する方法は、シークレットと境界 にあります。このページは運用上の見方、つまりチーム全体のバックエンドがそれらの参照をどのように解決するかを説明します。
参照が値になるまで
このドキュメント全体で使う実行例はdaily-price-watch です。ブラウザーTool(search-price@1.2.0)が価格を調べ、transform 手順が結果を整形し、条件付きの http.post が、URLをシークレットとして持つwebhookへ通知します。以下は、そのシークレットを消費するnotify手順です。
secret.WEBHOOK_URL だけです。http.post は secrets capabilityを宣言する唯一の手順なので、値が到達できる手順もこれだけです。その参照の道のりは、ローカルでもプレーン管理ワーカー上でも同じです。変わるのは resolve(ref) に応答するバックエンドだけです。
作成者がスロットを参照する
Flowのテキストには
${{ secret.WEBHOOK_URL }} が含まれます。コンパイラのシングルパスtaint checkは、その参照が secrets 対応Toolに丸ごと、かつ変換なしで束縛されていることを確認し、そうでなければ secret_leak 診断を出します。コンパイル済みFlowSpecが運ぶのは 参照 であり、値ではありません。Runがワーカーへ送出される
コントロールプレーンはRunをスケジュールし、ワーカーへ送出します。実行はいつもと同じ
Rider で、secrets providerがプレーンのブローカーを指す RunDeps で構成されます。ワーカーが受け取るFlowSpecには、まだ参照だけが含まれています。providerがワーカー上で実行時に解決する
notify 手順が実行されると、Riderは注入された Secrets providerの resolve(ref) を WEBHOOK_URL に対して呼び出します。ブローカーはバックエンドからlive値を取得し、その手順に、そしてその手順だけに渡します。解決は ワーカー上で実行時に 行われ、送出前にプレーン上で行われることはありません。Secrets providerは、store、clock、random sourceと並ぶ、エンジンの注入依存関係の1スロットです。
差し替え可能なバックエンド
ブローカーはバックエンドインターフェイスであり、単一のstoreではありません。仕様どおり、バックエンドは差し替え可能です。| Backend | 内容 |
|---|---|
| プレーン暗号化store | コントロールプレーン自身のエンベロープ暗号化されたシークレットstore。デフォルトであり、ホスト型サービスが現在実行しているものです(下記参照)。 |
| HashiCorp Vault | プレーンは、値を自身で保持するのではなく、Vault経由で読み取りを仲介します。 |
| クラウドシークレットマネージャー | クラウドプロバイダーのマネージドシークレットサービス。同じ方法で仲介されます。 |
resolve(ref) に応答する場合でも、ワーカーから見える契約は同一です。参照が入り、live値が正確に1つの手順へ戻ります。シークレットは、RegistryやRunと同じくworkspaceごとに名前空間化されます(tenancy)。
ホスト型サービス(
@jinbatrail/service)は現在、下記のエンベロープ暗号化方式により、プレーン暗号化store の行を実装しています。Vaultまたはcloud-secret-managerバックエンドは同じ Secrets インターフェイスの有効な実装ですが、本稿執筆時点ではホスト型サービスには接続されていません。保存時のエンベロープ暗号化
ホスト型サービスは、プロビジョニングされたシークレット(例: オーサリング中に取得されたセッショントークン)を、自身のsecrets テーブルに (org_id, slot) をキーとして永続化します。バイト列が 平文で保存されることはありません。古典的なエンベロープ暗号化により、データstoreが保持するのは暗号文だけになり、長期鍵素材は小さく差し替え可能な Kms 境界の背後に置かれます。
シークレットごとに新しい32-byte data key
encryptSecret(plaintext, kms) の各呼び出しはランダムなAES-256 data keyを生成します。このキーはシークレット間で決して再利用されません。AES-256-GCMがplaintextをsealする
plaintextは、そのdata keyのもとで、ランダムな12-byte IVと16-byte auth tagを使ってsealされます。これは認証付き暗号化なので、改ざんは復号時に検出され、黙って受け入れられることはありません。
Kms 実装だけです。ディスク上のenvelope形式はどちらでも同一です。
| 環境 | Kms 実装 | Master keyの所在 |
|---|---|---|
| Dev / self-host | createMasterKeyKms(JINBA_MASTER_KEY)。環境から一度だけ読み取られる32-byte hex keyのもとでAES-256-GCMを使います。存在しない場合は、大きな警告とともにランダムな一時キーが生成されます(そのプロセスの寿命の間だけencrypted-at-rest)。 | サービスプロセスのメモリ |
| Tests | createFakeKms()。テストスイートがbyte-stableになるよう、固定された公開既知のキーを使います。 | N/A。テスト外では決して使用しない |
| Production (AWS) | createKmsEnvelopeKms({ keyArn, encryptedMasterKey })。起動時に、AWS KMS CMKに対する認可済みの Decrypt 呼び出しを1回行い、32-byte master keyをプロセスメモリへ復元します。その後のシークレットごとのwrap/unwrapはすべてローカルのAES-256-GCMです(シークレットごとのKMS往復はありません)。 | 起動時の1回の Decrypt まではAWS KMS、その後はプロセスメモリ |
kms_key_id(dev master keyのhash、またはprodのCMK ARN)は、rotationや監査の追跡可能性のためだけに、各シークレットのciphertextと一緒に保存されます。それ自体がシークレットになることはありません。test/live境界
APIキーのmode(live または test)は、RBACとスコープの上に置かれる、2つ目の独立したゲートです。これはREST実行ゲートウェイ(POST /v1/run/:flow)とMCP(POST /mcp)で同じように強制されます。
test キーは、シークレット型入力を まったく 宣言しない任意のオートメーションを実行できます。これは意図された「実験しても安全」な領域です。ただし、そのオートメーションの inputs に secret: true とマークされたスロットが含まれた瞬間、即座に拒否されます。これにより、漏えいしたtestキー(スクリプト、デモ、第三者へ渡されたキー)が、liveキーと同じ本番認証情報への到達範囲を持ってしまう失敗モードを閉じます。部分的なアクセスはありません。オートメーションのどこかでシークレットを使っているなら、シークレットを持つ手順だけでなく、オートメーション全体がtestキーには立ち入り禁止です。
ローテーションは透過的
シークレットはFlowSpecにもJournalにも埋め込まれないため、値が変わっても再構築するものはありません。設計にあるとおり、シークレットはすべてのRunで再解決され、決して埋め込まれません。そのためローテーションは透過的です。 これは、エンジンが resume と replay の間にシークレットを扱う方法とも一貫しています。シークレットは、Journalから読み戻されるのではなく liveで再解決される 唯一のものです。再開されたRunは再解決します。hermeticなreplay は、記録済みeffectがシークレットを持たないため、シークレットをまったく必要としません。writeを再発火する --rerun だけが、ブローカーに新しい値を要求します。
RBACとrun inspector
シークレットは アクセス制御 と1つの決定的な形で交差します。roleに関係なく、シークレット値がrun inspectorによって公開されることは決してありません。 どのroleでも、adminであっても、そのRun自身の作成者であっても、inspectorを通じて解決済みの値を読むことはできません。手順のinputはシークレットがマスクされた状態で表示されます。http.post は、その url fieldを、Journalが記録した {secret:<slot>} プレースホルダーとして表示します。これは browserflow inspect が出力するものと同じデータです。
RBACが 実際に 制御するのは周辺の操作です。ホスト型サービスでは、owner だけがオーサリングを進め(これにより secret.provisioned 経由でシークレットがprovisionされます)、シークレットを持つオートメーションをliveで実行できるAPIキーを発行または取り消し、スケジュールを管理できます。member はRunとオートメーションを読めますが、それらには一切触れられません。secret.provisioned 監査エントリは、実行者、時刻、slot nameを記録します。値は決して記録しません。線引きは明確です。roleが制限するのは 操作へのアクセス であり、値の可視性 ではありません。owner/memberの完全な表と監査アクション一覧については、アクセス制御 を参照してください。
プレーンが値を見ることは、たとえ一瞬でもありますか?
プレーンが値を見ることは、たとえ一瞬でもありますか?
解決は、注入された
Secrets providerを通じて、ワーカー上で実行時に行われます。送出前にプレーン上で行われるわけではありません。プレーンはデプロイレコード、スケジュール、Journalを保存しますが、境界の不変条件により、そのどれにもシークレット値を保存しません。バックエンドがプレーン自身の暗号化storeである場合、値はそこで暗号化された状態で存在し、解決する手順へ仲介されます。バックエンドがVaultまたはクラウドシークレットマネージャーである場合、プレーンは値自体を保持せずに読み取りを仲介します。Flowがシークレットをlogまたはoutputしようとしたらどうなりますか?
Flowがシークレットをlogまたはoutputしようとしたらどうなりますか?
コンパイルできません。taint ruleは、変換された
secret. 参照、secrets ではないToolへ渡された参照、または output で使われた参照を拒否します。各違反は secret_leak 診断であり、コンパイル時に証明されます。そのため、漏えいするFlowがそもそもワーカーへ到達することはありません。taint rule を参照してください。シークレットはnested flowへどのように運ばれますか?
シークレットはnested flowへどのように運ばれますか?
外側の
composite Tool上の secret: true inputは、内側のFlowの対応するsecret inputへ 参照 として渡され、最も内側の secrets 対応Toolでのみ解決されます。ローカルとまったく同じように、プレーン管理ワーカー上でも、境界はどのnesting depthでも保たれます。関連
シークレットと境界
概念モデル: 参照、静的なtaint rule、effectのリダクション。
アクセス制御
owner/member RBAC、キーごとのスコープ、監査ログ、orgごとのtenancy。
実行と再現性
Riderがresumeとreplayでシークレットを新しく解決する方法。
コントロールプレーン
デプロイ、レプリケーション、スケジューリング、RBAC、APIキー、MCP。