セキュリティの仕組み
このガイドでは、シークレットがどのように保護されるか、異なるアクセス方法がどのように機能するか、多層防御モデルについて説明します。
アーキテクチャ概要
┌─────────────────────────────────────────────────────────────────────┐
│ secretctl アーキテクチャ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 人間オペレーター AI エージェント │
│ ═══════════════ ═════════════ │
│ CLI / デスクトップアプリ MCP サーバー │
│ │ │ │
│ │ フルアクセス │ 制限付き(AI安全設計) │
│ │ │ │
│ └──────────┬────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Vault │ │
│ │ (SQLite) │ │
│ │ │ │
│ │ AES-256-GCM │ │
│ │ 暗号化 │ │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 監査ログ │ │
│ │ HMAC チェーン│ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
鍵階層
secretctl は防御を深めるために3層の鍵階層を使用します:
┌─────────────────────────────────────────────────────────┐
│ ユーザー入力 │
│ マスターパスワード │
└─────────────────────┬───────────────────────────────────┘
│
▼ Argon2id (memory: 64MB, time: 3, threads: 4)
┌─────────────────────────────────────────────────────────┐
│ マスターキー (256ビット) │
│ ※ メモリのみ、保存されない │
└─────────────────────┬───────────────────────────────────┘
│
▼ AES-256-GCM 暗号化
┌─────────────────────────────────────────────────────────┐
│ データ暗号化キー (DEK) (256ビット) │
│ ※ 暗号化して保存 │
└─────────────────────┬───────────────────────────────────┘
│
▼ AES-256-GCM 暗号化
┌─────────────────────────────────────────────────────────┐
│ 暗号化されたシークレット │
│ ※ SQLite に保存 │
└─────────────────────────────────────────────────────────┘
なぜこの設計か?
| レイヤー | 目的 |
|---|---|
| マスターパスワード | ユーザー認証、保存されない |
| マスターキー | アンロックごとに新たに導出、DEK を保護 |
| DEK | 実際の暗号化キー、パスワードローテーションを可能に |
| 暗号化されたシークレット | 保存時に保護されたデータ |
Vault 構造
Vault ディレクトリには以下が含まれます:
~/.secretctl/
├── vault.db # SQLite DB(値はアプリで暗号化)
├── vault.salt # Argon2 salt (128ビット)
└── vault.meta # メタデータ(バージョン、作成日時)
ファイル権限
すべての機密ファイルは厳格な権限を使用:
| ファイル | 権限 | 理由 |
|---|---|---|
| vault.db | 0600 | 暗号化されたシークレットを含む |
| vault.salt | 0600 | 鍵導出に必要 |
| vault.meta | 0600 | メタデータ保護 |
| ~/.secretctl/ | 0700 | ディレクトリアクセス制御 |
アクセス制御モデル
CLI vs MCP: 信頼レベル
secretctl はアクセス方法に基づいて機能を制限します:
┌─────────────────────────────────────────────────────────────────────┐
│ secretctl アクセスモデル │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CLI (人間オペレーター) MCP サーバー (AI エージェント) │
│ ════════════════════ ═════════════════════ │
│ 信頼レベル: 完全 信頼レベル: 制限付き │
│ │
│ ✅ secretctl get KEY ❌ secret_get (未実装) │
│ → 平文 OK → AI に平文なし │
│ │
│ ✅ secretctl set KEY ❌ secret_set (未実装) │
│ → stdin で保存 → AI は値を設定できない │
│ │
│ ✅ secretctl list ✅ secret_list │
│ → 完全なキーリスト → キー名のみ │
│ │
│ ✅ secretctl run -- cmd ✅ secret_run │
│ → 環境変数注入 → マスクされた出力のみ │
│ │
│ ✅ secretctl delete KEY ❌ (未実装) │
│ → 削除 OK → AI は削除できない │
│ │
│ ✅ secretctl export ❌ (未実装) │
│ → エクスポート OK → AI はエクスポートできない │
│ │
└─────────────────────────────────────────────────────────────────────┘
設計の理由
| アクセス | オペレーター | 信頼 | 制限 |
|---|---|---|---|
| CLI | 人間直接 | 完全 | なし(全機能) |
| デスクトップ | 人間直接 | 完全 | なし(全機能) |
| MCP | AI エージェント | 制限付き | 平文の get/set/delete なし |
なぜ違いがあるのか?
- CLI ユーザーはシークレットを所有する人間であり、見る権利がある
- AI エージェントは実際の値を知る必要のないプロキシ
- AI はシークレット自体ではなく、シークレットを使用した結果だけが必要
AI安全設計: 露出なしのアクセス
AI 連携のコアセキュリティモデル:
┌─────────────────────────────────────────────────────────────────────┐
│ AI安全設計: 露出なしのアクセス │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 従来の MCP 実装: │
│ AI → MCP サーバー → 平文シークレット → AI → 使用 │
│ 問題: AI がシークレットを「知っている」 │
│ │
│ ✅ secretctl AI安全設計: │
│ AI → 「このシークレットでこのコマンドを実行」 │
│ → シークレットは直接注入(AI は見ない) │
│ → 結果のみが AI に返る │
│ │
└─────────────────────────────────────────────────────────────────────┘
MCP ツールの能力
| 能力 | 説明 | AI レスポンス |
|---|---|---|
reference_only | キー存在確認 | {"exists": true, "created_at": "..."} |
env_inject | 環境変数注入 | {"exit_code": 0, "stdout": "..."} |
masked_return | マスク値表示 | {"masked_value": "sk-****7890"} |
注意: full(平文返却)は廃止されました。MCP ツールは平文値を返しません。
secret_run のセキュリティレイヤー
secret_run コマンドは多層防御を使用します:
AI → secret_run(keys, command)
│
▼
┌─────────────────────────────────────────────────┐
│ レイヤー 1: コマンド検証 │
│ - ブロックされたコマンドリスト (env, printenv, etc.) │
│ - 追加の AI 制限 │
├─────────────────────────────────────────────────┤
│ レイヤー 2: 実行環境 │
│ - 非 TTY モード強制 │
│ - 一時的な作業ディレクトリ │
│ - タイムアウト: 300秒 │
├─────────────────────────────────────────────────┤
│ レイヤー 3: 環境変数注入 │
│ - シークレットはプロセス環境にのみ存在 │
│ - 値は AI に渡されない │
├─────────────────────────────────────────────────┤
│ レイヤー 4: 出力サニタイズ │
│ - stdout/stderr でシークレットをスキャン │
│ - 検出された値をマスク │
│ - Base64 エンコード検出 │
└─────────────────────────────────────────────────┘
│
▼
AI ← {"exit_code": 0, "stdout": "...", "stderr": "..."}
※ シークレット値は含まれない
ブロックされたコマンド
環境変数を露出する可能性のあるコマンド:
var blockedCommands = []string{
"env",
"printenv",
"set",
"export",
"declare",
"cat /proc/*/environ",
}
出力サニタイズ
// OutputSanitizer は stdout/stderr のシークレットをマスク
type OutputSanitizer struct {
secrets map[string]string // key -> value
}
func (s *OutputSanitizer) Sanitize(output string) string {
result := output
for key, value := range s.secrets {
// 平文検出
if strings.Contains(result, value) {
result = strings.ReplaceAll(result, value, "[REDACTED:"+key+"]")
}
// Base64 エンコード検出
encoded := base64.StdEncoding.EncodeToString([]byte(value))
if strings.Contains(result, encoded) {
result = strings.ReplaceAll(result, encoded, "[REDACTED:"+key+":base64]")
}
}
return result
}
マスキング形式
値はセキュリティのため固定長プレフィックスでマスク:
func maskValue(value string) string {
if len(value) <= 8 {
return "********"
}
// 末尾4文字のみ表示
return "********" + value[len(value)-4:]
}
// 例: "sk-proj-abc123xyz789" → "********z789"
固定長マスキングは値の長さの推測を防ぎます。
監査ログ
HMAC チェーン整合性
┌─────────────────────────────────────────────────────────────────────┐
│ 監査ログ HMAC チェーン │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ レコード 1 レコード 2 レコード 3 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ data │ │ data │ │ data │ │
│ │ prev: Ø │──┐ │ prev: H1│──┐ │ prev: H2│ │
│ │ hash: H1│ │ │ hash: H2│ │ │ hash: H3│ │
│ └─────────┘ │ └─────────┘ │ └─────────┘ │
│ │ ▲ │ ▲ │
│ └─────────┘ └─────────┘ │
│ │
│ H = HMAC-SHA256(id || action || key_hash || ... || prev, key) │
│ key = HKDF-SHA256(master_key, "audit-log-v1") │
│ │
│ 改ざん検出: prev_hash 不一致 → チェーン破損 │
│ │
└─────────────────────────────────────────────────────────────────────┘
記録される内容
記録:
- 操作タイプ (get, set, delete, list, run)
- キーハッシュ(機密操作ではキー名自体ではなく)
- ソース (cli, mcp, ui)
- タイムスタンプ(ナノ秒精度)
- 結果 (success, denied, error)
記録されない(セキュリティのため):
- シークレット値(平文または暗号化)
- マスターパスワード
- 生のユーザー入力
検証
# 監査ログ整合性を検証
$ secretctl audit verify
✓ 15,234 レコードを検証
✓ チェーン整合性: OK
✓ ギャップは検出されず
# 改ざんが検出された場合
$ secretctl audit verify
✗ レコード id=01HQ5E7N8K... でチェーン破損
期待される prev: a3f2b1...
実際の prev: 7c8d4e...
アラート: 改ざんの可能性が検出されました
MCP ポリシーエンジン
ポリシー設定
# ~/.secretctl/mcp-policy.yaml
policies:
- name: "claude-desktop"
agent_id: "claude-desktop-*"
allowed_keys:
- "api/*" # api/ プレフィックスを許可
- "config/dev/*"
denied_keys:
- "*/prod/*" # prod シークレットを拒否
capabilities:
- env_inject # 環境変数注入
- reference_only # 存在確認
- masked_return # マスク表示
- name: "untrusted-agent"
agent_id: "*"
allowed_keys: [] # すべて拒否
capabilities:
- reference_only # 存在確認のみ
ポリシー評価順序
- 最初に
denied_keysをチェック(拒否が優先) - 許可のため
allowed_keysをチェック - 必要な
capabilitiesを検証 - アクセス試行を監査にログ
セキュリティの利点
-
AI に平文を露出しない
- 最も重要な保護
- 値は AI モデルログやトレーニングデータに含まれない
-
プロンプトインジェクション耐性
- 悪意のあるプロンプトで抽出するシークレット値がない
- AI が侵害されてもシークレットは保護されたまま
-
多層防御
- コマンド検証 + 環境分離 + 出力フィルタリング
- 防御を深めるアプローチ
-
完全な監査可能性
- すべてのコマンドが改ざん検出でログ
- サニタイズがトリガーされるとアラート
-
最小権限
- AI は必要最小限の情報のみ取得
- キー名とマスク値のみ
次のステップ
- 暗号化詳細 - 暗号仕様
- MCP ツールリファレンス - 完全な MCP ツールドキュメント
- AI エージェント連携 - 実践的な MCP セットアップ