Skip to content

Commit 6333a1c

Browse files
authored
Allow SHA-256 digests in container image references (#3352)
The `container` field regex rejects `@sha256:` digest-pinned image references, blocking immutable container image pinning for supply-chain security. ### Changes - **Regex pattern** (`validation_schema.go`): Append optional `(@sha256:[a-fA-F0-9]{64})?` group to `containerPattern` - **JSON schema** (`mcp-gateway-config.schema.json`): Mirror the same pattern update - **Tests**: Add valid cases (`image:tag@sha256:...`, `image@sha256:...`) and invalid cases (short digest, wrong algorithm) Now accepts: ``` ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c67a0adca... ghcr.io/github/github-mcp-server@sha256:2763823c67a0adca... ``` No launcher changes needed — Docker already resolves the full `image:tag@sha256:...` reference passed through. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build2438908248/b514/launcher.test /tmp/go-build2438908248/b514/launcher.test -test.testlogfile=/tmp/go-build2438908248/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s go_.�� @v1.1.3/internal-errorsas 64/src/debug/dwa-ifaceassert x_amd64/vet --gdwarf-5 nal/version -o x_amd64/vet -plu�� /sdk@v1.43.0/tra-errorsas /sdk@v1.43.0/tra-ifaceassert x_amd64/vet -plugin-opt=-pasdocker telabs/wazero/ininfo -plugin-opt=-pas-stringintconv x_amd64/vet` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build2438908248/b496/config.test /tmp/go-build2438908248/b496/config.test -test.testlogfile=/tmp/go-build2438908248/b496/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2438908248/b393/vet.cfg g_.a --debug-prefix-map x_amd64/vet -I 0218966/b157/ -I x_amd64/vet` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build2438908248/b514/launcher.test /tmp/go-build2438908248/b514/launcher.test -test.testlogfile=/tmp/go-build2438908248/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s go_.�� @v1.1.3/internal-errorsas 64/src/debug/dwa-ifaceassert x_amd64/vet --gdwarf-5 nal/version -o x_amd64/vet -plu�� /sdk@v1.43.0/tra-errorsas /sdk@v1.43.0/tra-ifaceassert x_amd64/vet -plugin-opt=-pasdocker telabs/wazero/ininfo -plugin-opt=-pas-stringintconv x_amd64/vet` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build2438908248/b514/launcher.test /tmp/go-build2438908248/b514/launcher.test -test.testlogfile=/tmp/go-build2438908248/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s go_.�� @v1.1.3/internal-errorsas 64/src/debug/dwa-ifaceassert x_amd64/vet --gdwarf-5 nal/version -o x_amd64/vet -plu�� /sdk@v1.43.0/tra-errorsas /sdk@v1.43.0/tra-ifaceassert x_amd64/vet -plugin-opt=-pasdocker telabs/wazero/ininfo -plugin-opt=-pas-stringintconv x_amd64/vet` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build2438908248/b523/mcp.test /tmp/go-build2438908248/b523/mcp.test -test.testlogfile=/tmp/go-build2438908248/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s -p g_.a -trimpath x_amd64/vet -I ernal/proxy -I x_amd64/vet -uns�� .cfg /tmp/go-build1090218966/b063/vet-ifaceassert x_amd64/vet -D GOAMD64_v1 -o x_amd64/vet` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details>
2 parents 0ce7a72 + 86b59ae commit 6333a1c

4 files changed

Lines changed: 55 additions & 4 deletions

File tree

internal/config/schema/mcp-gateway-config.schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@
7474
},
7575
"container": {
7676
"type": "string",
77-
"description": "Container image for the MCP server (e.g., 'ghcr.io/example/mcp-server:latest'). This field is required for stdio servers per MCP Gateway Specification section 4.1.2.",
77+
"description": "Container image for the MCP server (e.g., 'ghcr.io/example/mcp-server:latest' or 'ghcr.io/example/mcp-server@sha256:<digest>'). This field is required for stdio servers per MCP Gateway Specification section 4.1.2.",
7878
"minLength": 1,
79-
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?$"
79+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?(@sha256:[a-fA-F0-9]{64})?$"
8080
},
8181
"entrypoint": {
8282
"type": "string",

internal/config/validation_schema.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func isTransientHTTPError(statusCode int) bool {
5959

6060
var (
6161
// Compile regex patterns from schema for additional validation
62-
containerPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?$`)
62+
containerPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?(@sha256:[a-fA-F0-9]{64})?$`)
6363
urlPattern = regexp.MustCompile(`^https?://.+`)
6464
mountPattern = regexp.MustCompile(`^[^:]+:[^:]+:(ro|rw)$`)
6565
domainVarPattern = regexp.MustCompile(`^\$\{[A-Z_][A-Z0-9_]*\}$`)
@@ -506,7 +506,7 @@ func validateStringPatterns(stdinCfg *StdinConfig) error {
506506
if server.Container != "" && !containerPattern.MatchString(server.Container) {
507507
return rules.InvalidPattern("container", server.Container,
508508
fmt.Sprintf("%s.container", jsonPath),
509-
"Use a valid container image format (e.g., 'ghcr.io/owner/image:tag' or 'owner/image:latest')")
509+
"Use a valid container image format (e.g., 'ghcr.io/owner/image:tag', 'owner/image:latest', or 'ghcr.io/owner/image:tag@sha256:<digest>')")
510510
}
511511

512512
// Validate mount patterns

internal/config/validation_schema_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,30 @@ func TestValidateStringPatterns(t *testing.T) {
365365
},
366366
shouldErr: false,
367367
},
368+
{
369+
name: "valid container pattern - tag with sha256 digest",
370+
config: &StdinConfig{
371+
MCPServers: map[string]*StdinServerConfig{
372+
"test": {
373+
Type: "stdio",
374+
Container: "ghcr.io/owner/image:v1.2.3@sha256:2763823c67a0adca3fce6e3bdfee41a674e3bf22f0e6b2eee94ed3a72ebcd519",
375+
},
376+
},
377+
},
378+
shouldErr: false,
379+
},
380+
{
381+
name: "valid container pattern - sha256 digest only",
382+
config: &StdinConfig{
383+
MCPServers: map[string]*StdinServerConfig{
384+
"test": {
385+
Type: "stdio",
386+
Container: "ghcr.io/owner/image@sha256:2763823c67a0adca3fce6e3bdfee41a674e3bf22f0e6b2eee94ed3a72ebcd519",
387+
},
388+
},
389+
},
390+
shouldErr: false,
391+
},
368392
{
369393
name: "invalid container pattern - starts with special char",
370394
config: &StdinConfig{

internal/config/validation_string_patterns_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ func TestValidateStringPatternsComprehensive(t *testing.T) {
6464
serverType: "",
6565
shouldError: false,
6666
},
67+
// Valid container patterns with SHA-256 digest
68+
{
69+
name: "valid container with tag and sha256 digest",
70+
container: "ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c67a0adca3fce6e3bdfee41a674e3bf22f0e6b2eee94ed3a72ebcd519",
71+
serverType: "stdio",
72+
shouldError: false,
73+
},
74+
{
75+
name: "valid container with sha256 digest only",
76+
container: "ghcr.io/github/github-mcp-server@sha256:2763823c67a0adca3fce6e3bdfee41a674e3bf22f0e6b2eee94ed3a72ebcd519",
77+
serverType: "stdio",
78+
shouldError: false,
79+
},
6780
// Invalid container patterns
6881
{
6982
name: "invalid container starts with special char",
@@ -86,6 +99,20 @@ func TestValidateStringPatternsComprehensive(t *testing.T) {
8699
shouldError: true,
87100
errorField: "container",
88101
},
102+
{
103+
name: "invalid container sha256 digest too short",
104+
container: "ghcr.io/github/github-mcp-server@sha256:short",
105+
serverType: "stdio",
106+
shouldError: true,
107+
errorField: "container",
108+
},
109+
{
110+
name: "invalid container wrong digest algorithm",
111+
container: "ghcr.io/github/github-mcp-server@md5:2763823c67a0adca3fce6e3bdfee41a674e3bf22f0e6b2eee94ed3a72ebcd519",
112+
serverType: "stdio",
113+
shouldError: true,
114+
errorField: "container",
115+
},
89116
{
90117
name: "invalid container empty string accepted (empty is valid)",
91118
container: "",

0 commit comments

Comments
 (0)