diff --git a/pkg/container/docker/envoy.go b/pkg/container/docker/envoy.go index ef4e298f49..60b0be40d9 100644 --- a/pkg/container/docker/envoy.go +++ b/pkg/container/docker/envoy.go @@ -22,9 +22,9 @@ import ( ) const ( - // defaultEnvoyImage is pinned by tag. Digest pinning is tracked in #5903. - // Override with TOOLHIVE_ENVOY_IMAGE. - defaultEnvoyImage = "envoyproxy/envoy-distroless:v1.32.3" + // defaultEnvoyImage is pinned by tag+digest for supply-chain integrity. + // Override with TOOLHIVE_ENVOY_IMAGE (accepts any docker pull reference). + defaultEnvoyImage = "envoyproxy/envoy-distroless:v1.32.3@sha256:375aab0d80b3c0e1b42a776b4cb1743ed79012032051d2da19cbc93ea884fb81" // Protobuf type URLs required by Envoy's protobuf-JSON bootstrap format. // Every typed_config field must carry an @type URL or Envoy will reject the @@ -56,16 +56,12 @@ func getEnvoyImage() string { // ── Bootstrap ──────────────────────────────────────────────────────────────── // envoyBootstrap is the top-level Envoy bootstrap configuration. +// The admin interface is intentionally omitted: Envoy does not start an admin +// server when the field is absent, eliminating the attack surface at runtime. type envoyBootstrap struct { - Admin *envoyAdmin `json:"admin,omitempty"` StaticResources envoyStaticResources `json:"static_resources"` } -// envoyAdmin configures the Envoy admin API endpoint. -type envoyAdmin struct { - Address envoyAddress `json:"address"` -} - // envoyStaticResources holds the static listeners and clusters. type envoyStaticResources struct { Listeners []envoyListener `json:"listeners,omitempty"` @@ -882,14 +878,6 @@ func (e *envoyProxy) SetupIngress(ctx context.Context, spec proxySpec, _ egressR egressContainerName := fmt.Sprintf("%s-egress", spec.WorkloadName) bootstrap := envoyBootstrap{ - Admin: &envoyAdmin{ - Address: envoyAddress{ - SocketAddress: envoySocketAddress{ - Address: "127.0.0.1", // loopback only — never 0.0.0.0 - PortValue: 9901, - }, - }, - }, StaticResources: envoyStaticResources{ Listeners: []envoyListener{buildEgressListener(spec)}, Clusters: []envoyCluster{buildEgressCluster()}, diff --git a/pkg/container/docker/envoy_test.go b/pkg/container/docker/envoy_test.go index 6353db211c..e5bfafdbd3 100644 --- a/pkg/container/docker/envoy_test.go +++ b/pkg/container/docker/envoy_test.go @@ -550,14 +550,6 @@ func TestWriteEnvoyBootstrap_FileMode(t *testing.T) { t.Parallel() b := envoyBootstrap{ - Admin: &envoyAdmin{ - Address: envoyAddress{ - SocketAddress: envoySocketAddress{ - Address: "127.0.0.1", - PortValue: 9901, - }, - }, - }, StaticResources: envoyStaticResources{}, } @@ -588,21 +580,13 @@ func TestWriteEnvoyBootstrap_FileMode(t *testing.T) { "bootstrap file must contain valid JSON") } -// TestEnvoyAdmin_LoopbackOnly asserts that the admin block written by -// writeEnvoyBootstrap binds only on the loopback address and never on -// 0.0.0.0 or an empty address that would expose admin to all interfaces. -func TestEnvoyAdmin_LoopbackOnly(t *testing.T) { +// TestEnvoyAdmin_Absent asserts that the admin interface is entirely absent from +// the generated bootstrap. Omitting the admin block causes Envoy to skip the +// admin server, removing the attack surface without affecting proxy behaviour. +func TestEnvoyAdmin_Absent(t *testing.T) { t.Parallel() b := envoyBootstrap{ - Admin: &envoyAdmin{ - Address: envoyAddress{ - SocketAddress: envoySocketAddress{ - Address: "127.0.0.1", - PortValue: 9901, - }, - }, - }, StaticResources: envoyStaticResources{}, } @@ -613,12 +597,12 @@ func TestEnvoyAdmin_LoopbackOnly(t *testing.T) { data, err := os.ReadFile(path) require.NoError(t, err) - s := string(data) - assert.Contains(t, s, "127.0.0.1", - "admin address must be loopback 127.0.0.1") - assert.NotContains(t, s, "0.0.0.0", - "admin address must NOT bind on 0.0.0.0") + var m map[string]any + require.NoError(t, json.Unmarshal(data, &m), "bootstrap must be valid JSON") + + _, hasAdmin := m["admin"] + assert.False(t, hasAdmin, "bootstrap must not contain an admin block") } // TestGetEnvoyImage verifies that getEnvoyImage returns the default image when @@ -752,7 +736,6 @@ func TestEnvoyBootstrap_ValidatesAgainstRealEnvoy(t *testing.T) { clusters = append(clusters, buildIngressCluster(tc.spec)) } b := envoyBootstrap{ - Admin: &envoyAdmin{Address: envoyAddress{SocketAddress: envoySocketAddress{Address: "127.0.0.1", PortValue: 9901}}}, StaticResources: envoyStaticResources{Listeners: listeners, Clusters: clusters}, } cfg, err := json.Marshal(b) diff --git a/pkg/container/images/registry.go b/pkg/container/images/registry.go index 1080c790e5..41fec7f754 100644 --- a/pkg/container/images/registry.go +++ b/pkg/container/images/registry.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -97,14 +98,30 @@ func (r *RegistryImageManager) PullImage(ctx context.Context, imageName string) return fmt.Errorf("failed to pull image from registry: %w", err) } - // Convert reference to tag for daemon.Write - tag, ok := ref.(name.Tag) - if !ok { - // If it's not a tag, try to convert to tag - tag, err = name.NewTag(ref.String()) + // Convert reference to a name.Tag for daemon.Write. + // name.ParseReference returns name.Digest (not name.Tag) for tag@digest + // references like "image:tag@sha256:...". In that case we extract the tag + // portion (everything before the '@') so daemon.Write can store the image + // under its human-readable tag, while the pull itself was verified against + // the digest by go-containerregistry. + var tag name.Tag + switch v := ref.(type) { + case name.Tag: + tag = v + case name.Digest: + refStr := ref.String() + if atIdx := strings.Index(refStr, "@"); atIdx >= 0 { + // tag@digest — use the portion before '@' as the tag reference. + tag, err = name.NewTag(refStr[:atIdx]) + } else { + // digest-only (no tag) — fall back to repository:latest. + tag, err = name.NewTag(v.Repository.String()) + } if err != nil { - return fmt.Errorf("failed to convert reference to tag: %w", err) + return fmt.Errorf("failed to derive tag from digest reference: %w", err) } + default: + return fmt.Errorf("unsupported image reference type %T", ref) } // Save the image to the local daemon