Skip to content

Conversation

@saucow
Copy link
Contributor

@saucow saucow commented Nov 18, 2025

What I did

Changes

Secret Injection

Container MCPs

Secrets injected as se:// URIs, resolved by Docker Desktop at container runtime. Gateway never holds secret values in memory.

Remote MCPs

Secrets queried from Secrets Engine and expanded into HTTP headers. Actual values required for Bearer tokens and header interpolation.

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.


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.


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

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