Skip to content

Commit 9592992

Browse files
ndeloofclaude
authored andcommitted
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 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent bb22a26 commit 9592992

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

‎pkg/compose/relay.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ func (s *composeService) createRelayContainer(ctx context.Context, project *type
332332
RestartPolicy: container.RestartPolicy{
333333
Name: container.RestartPolicyUnlessStopped,
334334
},
335+
// The relay only dials out and forwards bytes: it needs none of
336+
// Docker's default capabilities. NET_BIND_SERVICE is kept because a
337+
// route commonly targets a privileged port (e.g. 80, 443) that the
338+
// relay — running unprivileged as UID 65532 — must still be able to
339+
// listen on inside its own container.
340+
CapDrop: []string{"ALL"},
341+
CapAdd: []string{"NET_BIND_SERVICE"},
335342
}
336343

337344
// First network at creation, remaining ones connected afterwards — the

‎pkg/compose/relay_test.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package compose
1818

1919
import (
20+
"context"
2021
"testing"
2122

2223
"github.com/compose-spec/compose-go/v2/types"
@@ -276,6 +277,43 @@ func TestRemoveServiceRelayRemovesExisting(t *testing.T) {
276277
assert.NilError(t, svc.removeServiceRelay(t.Context(), "p", "db"))
277278
}
278279

280+
// The relay only dials out and forwards bytes: it gets none of Docker's
281+
// default capabilities except NET_BIND_SERVICE, kept because a route
282+
// commonly targets a privileged port the relay must still listen on.
283+
func TestCreateRelayContainerDropsCapabilities(t *testing.T) {
284+
mockCtrl := gomock.NewController(t)
285+
defer mockCtrl.Finish()
286+
apiMock, cli := prepareMocks(mockCtrl)
287+
tested, err := NewComposeService(cli)
288+
assert.NilError(t, err)
289+
svc := tested.(*composeService)
290+
291+
project := &types.Project{
292+
Name: "p",
293+
Networks: types.Networks{"default": {Name: "p_default"}},
294+
}
295+
db := types.ServiceConfig{Name: "db", Provider: &types.ServiceProviderConfig{Type: "test"}}
296+
297+
apiMock.EXPECT().ContainerList(gomock.Any(), gomock.Any()).
298+
Return(client.ContainerListResult{}, nil)
299+
300+
var got client.ContainerCreateOptions
301+
apiMock.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).
302+
DoAndReturn(func(_ context.Context, opts client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
303+
got = opts
304+
return client.ContainerCreateResult{ID: "relay-1"}, nil
305+
})
306+
apiMock.EXPECT().ContainerStart(gomock.Any(), "relay-1", gomock.Any()).
307+
Return(client.ContainerStartResult{}, nil)
308+
309+
endpoints := map[int]string{80: "host.docker.internal:49152"}
310+
err = svc.ensureServiceRelay(t.Context(), project, db, endpoints, []string{"default"})
311+
assert.NilError(t, err)
312+
313+
assert.DeepEqual(t, got.HostConfig.CapDrop, []string{"ALL"})
314+
assert.DeepEqual(t, got.HostConfig.CapAdd, []string{"NET_BIND_SERVICE"})
315+
}
316+
279317
// No relay ever existed for the service: nothing to do, and nothing calls
280318
// ContainerRemove (mockCtrl.Finish would fail an unexpected call anyway).
281319
func TestRemoveServiceRelayNoopWhenNoneExists(t *testing.T) {

0 commit comments

Comments
 (0)