Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

1. `secrets.enc` は平文の key material を決して含んではいけません。age で暗号化します。
2. `key.txt` は `0600`、store dir は `0700` でなければなりません。
3. すべての MCP tool result と resource はマスク済みでなければなりません。tool result に平文を出してはいけません。
4. `put` は `value_file` パスを受け取り、それを読み、読み取り後に削除しなければなりません。
3. すべての MCP tool result と resource はマスク済みでなければなりません。tool result に平文を出してはいけません。`mask.MaskKey` を使い、機密キー名 (DB_PASSWORD, GITHUB_TOKEN...) の場合は値がヒューリスティックに一致しなくてもマスクしてください。
4. `put` は **`<store>/inbox` 内** の `value_file` パスを受け取り、それを読み、読み取り後に削除しなければなりません。inbox の外のパス (外を指す symlink を含む) は拒否します。引数はエージェントが制御するためです。
5. マスキング規則は `internal/mask` にあります。entropy/prefix/URL segmentation のテストを green に保ってください。
6. **redemption は fail-closed です** (`internal/redeem`、docs/THREAT-MODEL.md)。未知のセッション、期限切れセッション、そのセッションで発行されていないキー名、値の欠落または空値、不正な予約プレフィックス、`argv` 内のトークン、audit log の書き込み失敗。いずれの場合もコマンドを起動しません。トークンをそのまま転送するフォールバックも、平文を転送するフォールバックも存在せず、エラーメッセージに値を含めてはいけません。
7. redemption は `argv` に値を置かず、値も引数リスト全体もログに残しません (`<store>/audit.log` はキー名のみを記録します)。
Expand Down
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ agents that develop it.
1. `secrets.enc` must never contain plaintext key material (age-encrypted).
2. `key.txt` must be `0600`, store dir `0700`.
3. Every MCP tool result and resource must be masked — no plaintext in
tool results, ever.
4. `put` must accept a `value_file` path, read it, and delete it after.
tool results, ever. Use `mask.MaskKey`, so a sensitive key name
(DB_PASSWORD, GITHUB_TOKEN, ...) masks the value even when the value itself
does not match a heuristic.
4. `put` must accept a `value_file` path **inside `<store>/inbox`**, read it,
and delete it after. Paths outside the inbox (including symlinks that
resolve outside) are refused: the argument is agent-controlled.
5. Masking rules live in `internal/mask` — keep the entropy/prefix/URL
segmentation tests green.
6. **Redemption fails closed** (`internal/redeem`, docs/THREAT-MODEL.md). On any
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ All notable changes to this project are documented here. The format follows
merging now have unit tests; CI runs `gofmt`, `go vet`, `go test`,
`e2e_test.py`, `verify_redeem.py` and the doc-parity check on every push.

### Fixed
- Masking now uses the key name: `DB_PASSWORD=correcthorsebatterystaple` is
masked by `get`, `list`, the MCP `get` tool and the MCP resource, instead of
being returned verbatim. `KeyNameIsSensitive` had no callers before.
- `put` only accepts a `value_file` inside `<store>/inbox`; an agent-supplied
path outside it was an arbitrary-file read and unlink. `keysmith inbox`
prints the directory, and symlink escapes are refused.
- Store writes take an exclusive lock (`<store>/store.lock`), so the MCP server
and the CLI can no longer lose each other's updates (20 concurrent `set`
calls used to store 12 keys).
- `--target` paths containing `.`/`..` segments, encoded slashes or
backslashes are refused, closing a `--allow-path` prefix escape such as
`/services/../../admin`.
- An HTTP listener on a non-loopback address requires `-http-token`; requests
without a matching `Authorization: Bearer` get 401.
- `keysmith scan` no longer prints the leaked value itself in its report.
- `randomSecret` uses rejection sampling instead of a biased modulo.

## 0.4.0 — 2026-09-14

### Added
Expand Down
19 changes: 13 additions & 6 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Keysmith は、**多層セキュリティモデル**でこれを解決します
|---|---|---|
| 暗号化された保存 | 保存時に age (X25519) で暗号化 | ディスク上の平文。コンテキストに漏れても blob は暗号文なので無害 |
| マスク済みビュー | resources/tools が `sk******ij` 形式で返す | 認証情報がエージェントのコンテキストに入ること |
| ハンドル渡し | `put` は transcript ではなく一時ファイル引数から値を読む | ツール呼び出し transcript 内の平文 |
| ハンドル渡し | `put` は transcript ではなくストアの inbox にあるファイルから値を読む | ツール呼び出し transcript 内の平文 |
| 自己修復ローテーション | `scan --rotate` が漏えいを検出し、漏れた値を無効化 | 古い漏えい済み認証情報が使われ続けること |
| 短 TTL (Vault) | 動的 DB 認証情報は 1 時間で期限切れ | 漏えいした認証情報の価値が残ること |
| Redemption | `keysmith run` がトークンをローカルで差し替え、子プロセスの環境変数へ渡す | 認証情報を「使う」必要があるエージェントが平文を出力してしまうこと。`argv`、transcript、ログ内の平文 |
Expand All @@ -47,17 +47,23 @@ KEYSMITH_STORE=/path/to/store keysmith
リモートエージェント向けに HTTP/SSE で提供します。

```sh
keysmith -store ~/.keysmith -http :8080
keysmith -store ~/.keysmith -http 127.0.0.1:8080
# endpoint: http://localhost:8080/sse
```

MCP 2025 Streamable HTTP で提供します。単一 POST エンドポイントのステートレス方式です。

```sh
keysmith -store ~/.keysmith -http :8080 -streamable
keysmith -store ~/.keysmith -http 127.0.0.1:8080 -streamable
# endpoint: http://localhost:8080/mcp
```

HTTP リスナー自体には認証がないため、keysmith は `-http-token` なしで非ループバックアドレスにバインドしません。トークンを指定した場合、すべてのリクエストに `Authorization: Bearer <token>` が必要です。

```sh
keysmith -store ~/.keysmith -http 192.168.1.10:8080 -http-token "$(openssl rand -hex 16)" -streamable
```

HashiCorp Vault をバックエンドとして使用します。短 TTL の動的シークレットに対応します。

```sh
Expand Down Expand Up @@ -95,6 +101,7 @@ keysmith set API_KEY < value.txt # stdin から値を読み、shell history
keysmith rotate API_KEY 32 # 強力な新しいシークレットを生成して保存
keysmith delete API_KEY # キーを削除
keysmith scan [--rotate] [repo-dir] # git 履歴から漏えいしたシークレットをスキャン
keysmith inbox # MCP の put が値ファイルを受け取るディレクトリ
keysmith token API_KEY # セッションに紐づくプレースホルダートークンを発行
keysmith token API_KEY --allow-host api.example.test --allow-path /v1/
# ...宛先に結合する(下記参照)
Expand Down Expand Up @@ -134,7 +141,7 @@ keysmith -vault http://127.0.0.1:8200 vault-db-creds app-role # 短 TTL
|---|---|---|
| `list` | すべてのキーとマスク済み値 | 値は平文にならない |
| `get` | 1 つのキーのマスク済み値 | 値は平文にならない |
| `put` | シークレットを保存 | 値は `value_file` 引数から読み、一時ファイルは自動削除 |
| `put` | シークレットを保存 | `value_file` はストアの inbox 内のみ受け付け、読み取り後に削除 |
| `rotate` | 強力なランダムシークレットを生成して保存 | マスク済み値のみを返す |
| `delete` | キーを削除 | - |

Expand Down Expand Up @@ -192,9 +199,9 @@ flowchart TB

- **保存時**: 常に age (X25519) で暗号化され、armor 形式です。平文ファイルはディスク上に存在しません。atomic write (temp + rename) と `0600` 権限を使います。
- **コンテキスト内**: マスク済み値のみです。マスキングは先頭/末尾 2 文字を残し (`sk******ij`)、認証情報を識別できるが露出しない形にします。
- **Transcript 内**: `put` は平文を引数として受け取りません。一時ファイルパスを読み、読み取り後にファイルを削除します。
- **Transcript 内**: `put` は平文を引数として受け取りません。ファイルパスを読み、ストアの inbox (`keysmith inbox`) 内のパスのみ受け付け、読み取り後にファイルを削除します。inbox の外は拒否されるため、任意ファイルの読み取りや削除には使えません。
- **使用時**: `keysmith run` はセッションに紐づくトークンをローカルで解決し、値を子プロセスの環境変数へ渡します。値は `argv`、audit log、モデルへのリクエストに到達しません。失敗時はコマンド起動前に中止し、平文フォールバックもトークンのそのまま転送も行いません。結合済みトークンは `--target` も満たす必要があり、宣言された宛先は audit 行に記録されます。
- **マスキング規則**: キー名マーカー (SECRET/TOKEN/PASSWORD/API_KEY/DSN...)、既知の値プレフィックス (sk-, ghp_, glpat-, xoxb-, JWT...)、高エントロピーな英数字列 (英字と数字が混在する 20 文字以上、Shannon entropy 3.5 以上) を使います。URL 形状の値はセグメントごとにマスクされます。userinfo のパスワードは常にマスクされ、高エントロピーな path/query はマスクされ、host/port は残ります。timeout や retry などの純数字値はマスクされません。
- **マスキング規則**: キー名マーカー (SECRET/TOKEN/PASSWORD/API_KEY/DSN...)、既知の値プレフィックス (sk-, ghp_, glpat-, xoxb-, JWT...)、高エントロピーな英数字列 (英字と数字が混在する 20 文字以上、Shannon entropy 3.5 以上) を使います。キー名が認証情報を示す場合は、値がヒューリスティックに一致しなくても必ずマスクします (数字を含まないパスフレーズもシークレットです)。機密でないキー (DEBUG, PORT) の設定値はそのまま読めます。URL 形状の値はセグメントごとにマスクされます。userinfo のパスワードは常にマスクされ、高エントロピーな path/query はマスクされ、host/port は残ります。timeout や retry などの純数字値はマスクされません。

## 開発

Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Keysmith fixes this with a **layered security model**:
|---|---|---|
| Encrypted residency | age (X25519) encrypted store at rest | Plaintext on disk; even a leaked context is harmless (blobs are ciphertext) |
| Masked views | resources/tools return `sk******ij` form | Credentials entering agent context |
| Handle-passing | `put` reads value from a temp file arg, not the transcript | Plaintext in tool-call transcripts |
| Handle-passing | `put` reads the value from a file in the store inbox, not from the transcript | Plaintext in tool-call transcripts |
| Self-healing rotation | `scan --rotate` detects leaks, kills the leaked value | Stale leaked credentials keep working |
| Short-TTL (Vault) | dynamic DB creds expire in 1h | Leaked credentials become worthless |
| Redemption | `keysmith run` substitutes a token locally, into the child's environment | An agent that must *use* a credential being tempted to print it; plaintext in `argv`, transcripts, or logs |
Expand All @@ -58,17 +58,25 @@ KEYSMITH_STORE=/path/to/store keysmith
Serve over HTTP/SSE for remote agents:

```sh
keysmith -store ~/.keysmith -http :8080
keysmith -store ~/.keysmith -http 127.0.0.1:8080
# endpoint: http://localhost:8080/sse
```

Serve MCP 2025 Streamable HTTP (single POST endpoint, stateless):

```sh
keysmith -store ~/.keysmith -http :8080 -streamable
keysmith -store ~/.keysmith -http 127.0.0.1:8080 -streamable
# endpoint: http://localhost:8080/mcp
```

An HTTP listener has no authentication of its own, so keysmith refuses to bind
a non-loopback address unless `-http-token` is set. With a token, every request
must carry `Authorization: Bearer <token>`:

```sh
keysmith -store ~/.keysmith -http 192.168.1.10:8080 -http-token "$(openssl rand -hex 16)" -streamable
```

Use HashiCorp Vault as the backend (dynamic short-TTL secrets):

```sh
Expand Down Expand Up @@ -106,6 +114,7 @@ keysmith set API_KEY < value.txt # value from stdin, no shell-history leak
keysmith rotate API_KEY 32 # generate + store new strong secret
keysmith delete API_KEY # remove a key
keysmith scan [--rotate] [repo-dir] # scan git history for leaked secrets
keysmith inbox # directory the MCP put tool accepts files from
keysmith token API_KEY # issue a session-bound placeholder token
keysmith token API_KEY --allow-host api.example.test --allow-path /v1/
# ...bound to a destination (see below)
Expand Down Expand Up @@ -157,7 +166,7 @@ keysmith -vault http://127.0.0.1:8200 vault-db-creds app-role # dynamic sh
|---|---|---|
| `list` | All keys with masked values | Values never plaintext |
| `get` | Single key, masked value | Values never plaintext |
| `put` | Store a secret | Value read from `value_file` arg, temp file auto-removed |
| `put` | Store a secret | Reads `value_file` from the store inbox only, then deletes it |
| `rotate` | Generate + store a new strong random secret | Returns masked value only |
| `delete` | Remove a key | — |

Expand Down Expand Up @@ -220,10 +229,12 @@ itself, transports decide how agents connect.

- **At rest**: always age-encrypted (X25519), armor format. A plaintext file
never exists on disk. Atomic writes (temp + rename), `0600` perms.
- **In transcripts**: `put` never takes the plaintext as an argument — it
reads a file path, and only inside the store inbox (`keysmith inbox`); the
file is deleted after it is read. A path outside the inbox is refused, so the
tool cannot be used as an arbitrary-file read or unlink.
- **In context**: masked values only. Masking keeps first/last 2 chars
(`sk******ij`) so credentials are distinguishable without being revealed.
- **In transcripts**: `put` never takes the plaintext as an argument — it
reads a temp file path and deletes the file after.
- **At use time**: `keysmith run` redeems a session-bound token locally and
places the value in the child's environment. Values never reach `argv`, the
audit log, or a model request; every failure aborts before the command starts
Expand All @@ -233,6 +244,9 @@ itself, transports decide how agents connect.
- **Masking rules**: key-name markers (SECRET/TOKEN/PASSWORD/API_KEY/DSN...),
known value prefixes (sk-, ghp_, glpat-, xoxb-, JWT...), and high-entropy
alphanumeric runs (≥20 chars mixing letters+digits, Shannon entropy ≥3.5).
A sensitive key name forces masking even when the value does not match a
value heuristic — a passphrase without digits is still a secret. Plain
configuration under a non-sensitive key (DEBUG, PORT) stays readable.
URL-shaped values are masked per-segment: userinfo password always masked,
high-entropy path/query runs masked, host/port stay clear. Pure-numeric
values (timeouts, retries) are never masked.
Expand Down
Loading
Loading