Repository navigation
feat(secret): ${file:/absolute/path} references (Spec 111 FR-030) - #1717
Conversation
9298fbb to
a58e22d
Compare
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Good call treating it as the same trust boundary as ${env:} for now, and thanks for documenting it. Please keep the path allow-list out of this PR. I would rather do it as a follow-up that covers both providers at once, something like a secret_file_dirs setting defaulting to /run/secrets, /run/credentials and a secrets folder under the data dir, plus the equivalent guard for ${env:} naming MCPPROXY_API_KEY. If you want to file that issue with your proposed defaults I will take it from there. I will review this one once #1716 is in and you have rebased. |
7f3a45a to
09a7161
Compare
|
Rebased onto the updated #1716 (28730ef, both-set is an error; an env key still wins over a |
Add the file provider FR-030 asks for, next to env and keyring, so a secret can come from a Docker/Compose secret, a Kubernetes secret volume or a systemd credential wherever the existing reference syntax is accepted: an upstream server's env, args and headers, data_dir and the proxy's own api_key. The read rules are secret.ReadFile's (absolute path, regular file after symlinks, 64 KiB cap, one trailing LF/CRLF removed, no NUL, errors never include content). An empty file is an error, as an empty env var is. api_key: a reference is resolved at load as a process-only override, so every save (including PATCH /api/v1/config) writes the reference back and never the secret. One that does not resolve fails the load; no key is generated over it. A key from the environment overrides the file's and the reference is then not read. Spec 111 FR-030 amended; docs and CHANGELOG updated. Refs smart-mcp-proxy#1706
Formatting a Windows path (C:\Users\...) into a JSON string produced invalid escapes on the Windows runner. The api_key reference tests now write mcp_config.json through json.Marshal, compare the stored reference by decoding the file, and a new test pins the backslash round trip. The symlink read test skips on Windows when the account cannot create one. Docs say what an absolute path is on Windows.
09a7161 to
6061e50
Compare
…oads
A load that never becomes the live config (a server restart re-reads the whole
file) after the key file was rotated re-registered the process override with
the new value. The running key then no longer equalled the override, so the next
save, e.g. PATCH /api/v1/config, treated it as an operator edit and wrote it in
clear over the ${file:} reference.
Once a reference has been resolved, later loads in the same process carry the
key the process runs with (api_key is restart-gated anyway); the file is still
read so an unusable reference fails the load. A rotated key takes effect on the
next start; the docs said a config reload, which a reload pins away.
Dumbris
left a comment
There was a problem hiding this comment.
Thank you, James. This is careful work: the read rules are shared with #1716, the api_key override reuses the existing persisted-vs-effective machinery instead of adding a new path, and the docs state what an absolute path is on Windows. Approving, with one fix I pushed to your branch.
What I found and fixed (a97dd95)
After the key file is rotated, any load that does not become the live config re-registered the api_key override with the new value. The server restart does this: refreshServerFromDiskForRestart re-reads the whole file. The running key then no longer equalled the override, so the next save treated it as an operator edit and wrote it in clear over the reference.
Reproduced with a built binary:
- start with
api_key: "${file:/…/key}"(key A) - overwrite the file with key B
POST /api/v1/servers/s1/restartPATCH /api/v1/config {"tools_limit":22}mcp_config.jsonnow contains"api_key": "KEY-A-…"
The fix: once a reference has been resolved, later loads in the same process carry the key the process runs with. That matches the running config, because api_key is restart-gated and a reload pins the live value anyway. The file is still read, so an unusable reference still fails the load. A rotated key takes effect on the next start. I corrected the doc line that promised "restart or config reload". A regression test (TestPersistableConfig_APIKeyReferenceSurvivesARotationAndAnotherLoad) fails without the fix. The same steps now leave the reference on disk.
What I verified (on the branch, 6061e50 plus the fix)
go test -race ./internal/secret/... ./internal/config/...passes../scripts/test-api-e2e.shpasses (70/70, with the server-edition binary built).- Both golangci-lint v2 runs (bare and
--build-tags server) report nothing in your files. Threereflect.Ptrgovet notes insecret/resolver.goand one inconfig/zero_value_preservation_test.goare on lines this PR does not touch and are also on main. - A real
mcpproxy servewithapi_key: "${file:…}"and a CRLF key file:- the right key gets 200 and a wrong key gets 401
PATCH {"tools_limit":21}is saved andapi_keystays the reference- the secret appears neither in
mcp_config.jsonnor in the log (only the masked prefix)
- Server
envandargswith${file:}reach the child astok-XYZ(trailing LF removed). A relative path and a missing file are refused, with an error that names the reference and no content. - No Gemini or other bot review comments exist on this PR (only the Codecov note).
- One cross-model round with opencode
github-copilot/gpt-6.1-sol, three findings, each checked by hand:- the P1 above (confirmed by the repro)
- the docs claim about reload (confirmed, fixed)
- a note (below)
- It merges cleanly with current main.
Not blocking, for @Dumbris or a follow-up
api_keyis only resolved inValidate(), not inValidateDetailed().PATCH {"api_key":"${file:/nonexistent}"}is accepted and saved, and the next start then refuses to run. That is the intended hard-fail, but a write-time check would surface the typo earlier. It fits the #1721 follow-up better than this PR.- This is unchanged behaviour: a server whose
${file:}(or${env:}) cannot be resolved is still started with the literal placeholder as the value, and the error is only logged. It matches US7 scenario 2 as written.
Merge is Algis's call; I have not merged.
|
Merged. Thank you, @james-crowley! Contributions like this are what keep MCPProxy moving, and they matter a lot to the project. One small ask, only if you feel like it: a short public note about your change (a tweet, a LinkedIn post, a line in a community you are part of) helps more people discover MCPProxy. No pressure at all, and I would be glad to repost it. |
Description
Second of the two PRs for #1706 (Spec 111 FR-030), on top of #1716 (merged). Closes #1706. The path allow-list is the separate follow-up #1721.
Adds the
fileprovider next toenvandkeyring. It is registered insecret.NewResolver, so${file:/absolute/path}works everywhere the existing references do: an upstream server'senv,argsandheaders(resolved on a copy, so the stored config keeps the reference),data_dir, and the proxy's ownapi_key.secret.ReadFile's from feat(config): read the API key from MCPPROXY_API_KEY_FILE #1716: absolute path, symlinks followed but the target must be a regular file, 64 KiB cap, one trailing LF or CRLF removed (an LF file and a CRLF file resolve to the identical value), NUL rejected, errors name the reference and reason and never the content. An empty file is an error, as an empty env var is for${env:}.api_key: a reference is resolved at load as a process-only override, so the process runs with the secret while every save, including the one aPATCH /api/v1/configtriggers, writes the reference back and never the secret. One that does not resolve fails the load and never falls back to a generated key. A key from the environment overrides the file's, and the reference is then not read. Editingapi_keyitself is still persisted as the operator's edit.keyring-integration.md,upstream-servers.md,environment-variables.md) and CHANGELOG updated.@Dumbris, one security question.
${file:}reads any file the proxy process can read, so a server added through theupstream_serverstool withenv: {X: "${file:/data/mcp_config.json}"}could hand that file to the server's process.${env:}already has the same exposure (it can nameMCPPROXY_API_KEY), servers added that way are quarantined, andallow_server_addgates the tool, so I treated it as the same trust boundary and documented it. Would you like a path allow-list (for example asecret_file_dirssetting that defaults to/run/secrets,/run/credentialsand the data directory's secrets folder) in this PR, or as a follow-up?Testing
go test ./internal/config ./internal/secretpass. New tests cover the provider, LF/CRLF equality, error text without content,api_keyresolving as an override and surviving a save and aPersistableConfiground trip, an edit being persisted, an unresolvable reference failing the load with the file untouched, an env key winning without reading the reference, and serverenv/headers/argsexpanding on a copy. By hand with a built binary: a reference inapi_keyauthenticates,PATCH {"tools_limit": 21}is saved withapi_keystill the reference, a rotated file works after restart, and a missing file refuses to start.