Skip to content

Conversation

@saucow
Copy link
Contributor

@saucow saucow commented Nov 18, 2025

What I did

Changes

Secret Injection

Container MCPs (Local)

Secrets injected as se:// URIs, resolved by Docker Desktop at container runtime

Remote MCPs

  • Secrets first checked from configuration (file-based or se:// URI)
  • Falls back to Secrets Engine direct API query for Docker Desktop mode
  • Values expanded into HTTP headers for Bearer tokens and header interpolation

File-Based Secrets

Both local and remote servers now support file-based secrets via --secrets=path/to/.env:

docker mcp gateway run --secrets=secrets.env --servers=brave,apify

OAuth Tokens

OAuth tokens retrieved from Secrets Engine in Desktop mode. Falls back to credential helpers in CE mode. Token refresh monitoring also uses Secrets Engine.


Verbose Logging

Added --verbose flag support for debugging secret injection:

Local Containers:

    - BRAVE_API_KEY: se://docker/mcp/generic/brave.api_key   (Docker Desktop mode)
    - BRAVE_API_KEY: abc1****                                 (File-based mode)

Remote Servers:

    - APIFY_API_KEY: abc1****                                 (File-based mode)
    - Fetching secret: docker/mcp/generic/apify.api_key       (Direct SE API fallback)
    - Using OAuth token for: notion-remote                    (OAuth servers)

Note: Actual secret values are masked (first 4 chars + ****). Only se:// URIs are shown in full since they're just references, not actual secrets.


Commands Removed

  • docker mcp policy set/dump - Policy management incompatible with Secrets Engine access control
  • docker mcp secret export - Replaced by Secrets Engine queries

Commands Changed

docker mcp secret set

Stores secrets via docker pass to OS Keychain under docker/mcp/generic/ namespace.

docker mcp secret ls

Queries Secrets Engine HTTP API instead of JFS socket. JSON output format compatible with Docker Desktop UI.

docker mcp secret rm

Deletes secrets via docker pass rm. Only removes docker-pass provider secrets. Legacy secrets must be removed via OS credential tools.


Testing

# Docker Desktop mode (se:// URIs)
docker mcp gateway run --verbose

# File-based mode
echo "brave.api_key=your-key" > secrets.env
echo "apify.api_key=your-key" >> secrets.env
docker mcp gateway run --secrets=secrets.env --servers=brave,apify --verbose

Benehiko and others added 4 commits November 17, 2025 13:29
* cmd/secret: use new store

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>

* Add temporary secrets engine client

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>

* Lint fixes + remove tests + don't return cred value

* update docs

---------

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Co-authored-by: Saurabh Davala <saurabh.davala@docker.com>
@saucow saucow requested a review from Benehiko November 18, 2025 04:35
@saucow saucow changed the title Secrets engine injection Remove JFS references + secrets engine injection Nov 18, 2025
@saucow saucow changed the base branch from secrets-engine to main November 24, 2025 22:54
@saucow saucow marked this pull request as ready for review November 25, 2025 01:46
@saucow saucow requested a review from a team as a code owner November 25, 2025 01:46
@saucow saucow requested a review from bobbyhouse December 1, 2025 16:46
Copy link
Collaborator

@slimslenderslacks slimslenderslacks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can merge this to a release/4.54 branch. But we can't merge this to main yet. No one outside docker can run this version of the gateway.

I'm also concerned that we're breaking compose file secrets.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of the entire backup package. It's superseded by profiles and secrets and now that the policy dump and set commands are gone, can we verify whether this entire package can be removed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I think we are safe to remove it. Could be another PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, will address in a follow-up PR

for _, secret := range c.config.Spec.Secrets {
env[secret.Env] = c.config.Secrets[secret.Name]
for _, s := range c.config.Spec.Secrets {
value := getSecretValue(ctx, s.Name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no err here. Don't need the value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to directly set

}
flags := cmd.Flags()
flags.StringVar(&opts.Provider, "provider", "", "Supported: credstore, oauth/<provider>")
_ = flags.MarkDeprecated("provider", "option will be ignored")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there something we can say about how providers are now supported with "docker pass"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - updated to say: all secrets now stored via docker pass in OS Keychain"

}
// Secrets no longer read during configuration load
// Instead, se:// URIs are passed to containers and resolved at runtime
secrets := make(map[string]string)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be where we construct the se:// uris so that we do it in only one place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes added a helper method: buildSecretsURIs here

if err != nil {
return Configuration{}, fmt.Errorf("reading MCP Toolkit's secrets: %w", err)
}
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this breaks some compose examples because we're not changing the --secrets options to the gateway yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, restored the: readSecretsFromFile flow. To verify I did the following locally:

  • Created secrets.env file:
brave.api_key=your-brave-api-key
apify.api_key=your-apify-api-key
  • Ran following command:
docker-mcp gateway run \
    --secrets=secrets.env \
    --servers=brave,apify \
    --verbose

@saucow saucow changed the base branch from main to release/4.54 December 1, 2025 21:36
Copy link
Contributor

@cmrigney cmrigney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! The only blocking thing is the chore (because it can cause bugs). I haven't been able to test it locally yet, but everything else in the code lgtm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I think we are safe to remove it. Could be another PR though.

},
Version: version.Version,
}
cmd.SetContext(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What was the reason for this change? Curious only because I'm pretty sure this cmd is different than the cmd inside of PersistentPreRunE. e.g. the one in that function is the actual subcommand that got run.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point, reverted this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point, reverted this

`

func secretCommand(docker docker.Client) *cobra.Command {
func secretCommand(_ docker.Client) *cobra.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non-blocking): Could we just remove this param?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no longer needed, removed

Short: "Manage secrets",
Short: "Manage secrets in the local OS Keychain",
Example: strings.Trim(setSecretExample, "\n"),
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: I unfortunately just learned the hard way that this can break things. See Slack thread.
I used an isSubcommandOf approach to work around this: https://github.com/docker/mcp-gateway/pull/266/files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, thanks for calling that out. Updated to have:

// Note: Using PersistentPreRunE in secretCommand would override this parent hook
			if isSubcommandOf(cmd, []string{"secret"}) {
				if err := desktop.CheckHasDockerPass(cmd.Context()); err != nil {
					return err
				}
			}

in: cmd/docker-mcp/commands/root.go

}

return secretsByName, nil
func (c *WorkingSetConfiguration) readDockerDesktopSecrets(_ context.Context, _ []workingset.Server) (map[string]string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Does this mean we'll have some more cleanup after we merge this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, since with secrets engine we don't need to directly read secrets anymore, this is all dead code. I've removed this method all-together and instead we are returning se:// URI's for secrets as these are "injected" when starting up the container. Ex:
docker run -d -e POSTGRES_PASSWORD=se://docker/mcp/generic/postgres_password -p 5432 postgres

Comment on lines 40 to 45
// TODO: Remove once Secrets Engine fixes pattern matching bug
patterns := []string{
fmt.Sprintf(`{"pattern": "%s*"}`, NamespaceGeneric), // Generic secrets (docker pass)
fmt.Sprintf(`{"pattern": "%s*"}`, NamespaceOAuth), // OAuth tokens
fmt.Sprintf(`{"pattern": "%s*"}`, NamespaceOAuthDCR), // DCR configs
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't been able to reproduce this. This won't match secrets that have nested namespaces, e.g. docker/mcp/generic/myapp/a-secret-key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, updated to have ** pattern for each type. Once we start getting all secrets when requesting docker/mcp/** will udpate

Copy link
Contributor

@cmrigney cmrigney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code lgtm, but I couldn't get it to work. Not sure if I'm doing something wrong or my setup is broken? I've got the mcp-fixes build and built your branch. There were a few issues I noticed:

  1. My mcp secrets didn't appear to be migrated over to the new system. Is that expected?

  2. I'm unable to run docker mcp secret ls, even though docker pass ls works.

Image

Another example after adding a secret.
Image

  1. When I set a secret in the MCP Toolkit UI, it does seem to save the secret. However, it shows blank in the field, like I never set it. Maybe this is related to the above?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants