mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
fix(provider): drop the relay's capabilities to the strict minimum
The relay only dials an upstream and forwards bytes: it needs none of Docker's default Linux capabilities. Run it with CapDrop: ["ALL"], keeping only NET_BIND_SERVICE back via CapAdd since a route commonly targets a privileged port (e.g. 80, 443) that the relay -- running unprivileged as UID 65532 -- must still be able to listen on inside its own container. Validated end-to-end against a real provider project: a container on the relay's network reaches the provider through its published, low port with the hardened capability set in place. Co-Authored-By: Claude Sonnet 5 <[email protected]> Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
committed by
Guillaume Lours
co-authored by
Claude Sonnet 5
parent
bb22a260e8
commit
95929928c1
@@ -332,6 +332,13 @@ func (s *composeService) createRelayContainer(ctx context.Context, project *type
|
||||
RestartPolicy: container.RestartPolicy{
|
||||
Name: container.RestartPolicyUnlessStopped,
|
||||
},
|
||||
// The relay only dials out and forwards bytes: it needs none of
|
||||
// Docker's default capabilities. NET_BIND_SERVICE is kept because a
|
||||
// route commonly targets a privileged port (e.g. 80, 443) that the
|
||||
// relay — running unprivileged as UID 65532 — must still be able to
|
||||
// listen on inside its own container.
|
||||
CapDrop: []string{"ALL"},
|
||||
CapAdd: []string{"NET_BIND_SERVICE"},
|
||||
}
|
||||
|
||||
// First network at creation, remaining ones connected afterwards — the
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
@@ -276,6 +277,43 @@ func TestRemoveServiceRelayRemovesExisting(t *testing.T) {
|
||||
assert.NilError(t, svc.removeServiceRelay(t.Context(), "p", "db"))
|
||||
}
|
||||
|
||||
// The relay only dials out and forwards bytes: it gets none of Docker's
|
||||
// default capabilities except NET_BIND_SERVICE, kept because a route
|
||||
// commonly targets a privileged port the relay must still listen on.
|
||||
func TestCreateRelayContainerDropsCapabilities(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
apiMock, cli := prepareMocks(mockCtrl)
|
||||
tested, err := NewComposeService(cli)
|
||||
assert.NilError(t, err)
|
||||
svc := tested.(*composeService)
|
||||
|
||||
project := &types.Project{
|
||||
Name: "p",
|
||||
Networks: types.Networks{"default": {Name: "p_default"}},
|
||||
}
|
||||
db := types.ServiceConfig{Name: "db", Provider: &types.ServiceProviderConfig{Type: "test"}}
|
||||
|
||||
apiMock.EXPECT().ContainerList(gomock.Any(), gomock.Any()).
|
||||
Return(client.ContainerListResult{}, nil)
|
||||
|
||||
var got client.ContainerCreateOptions
|
||||
apiMock.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(_ context.Context, opts client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
got = opts
|
||||
return client.ContainerCreateResult{ID: "relay-1"}, nil
|
||||
})
|
||||
apiMock.EXPECT().ContainerStart(gomock.Any(), "relay-1", gomock.Any()).
|
||||
Return(client.ContainerStartResult{}, nil)
|
||||
|
||||
endpoints := map[int]string{80: "host.docker.internal:49152"}
|
||||
err = svc.ensureServiceRelay(t.Context(), project, db, endpoints, []string{"default"})
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, got.HostConfig.CapDrop, []string{"ALL"})
|
||||
assert.DeepEqual(t, got.HostConfig.CapAdd, []string{"NET_BIND_SERVICE"})
|
||||
}
|
||||
|
||||
// No relay ever existed for the service: nothing to do, and nothing calls
|
||||
// ContainerRemove (mockCtrl.Finish would fail an unexpected call anyway).
|
||||
func TestRemoveServiceRelayNoopWhenNoneExists(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user