Skip to content

Commit 7705e77

Browse files
authored
Add inbound hosts to network isolation profile (#2030)
Fix #2023
1 parent a0f517c commit 7705e77

File tree

8 files changed

+145
-12
lines changed

8 files changed

+145
-12
lines changed

docs/server/docs.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/container/docker/client.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type deployOps interface {
116116
upstreamPort int,
117117
attachStdio bool,
118118
externalEndpointsConfig map[string]*network.EndpointSettings,
119+
networkPermissions *permissions.NetworkPermissions,
119120
) (int, error)
120121
}
121122

@@ -289,7 +290,8 @@ func (c *Client) DeployWorkload(
289290
if err != nil {
290291
return 0, err // extractFirstPort already wraps the error with context.
291292
}
292-
hostPort, err = c.ops.createIngressContainer(ctx, name, firstPortInt, attachStdio, externalEndpointsConfig)
293+
hostPort, err = c.ops.createIngressContainer(ctx, name, firstPortInt, attachStdio, externalEndpointsConfig,
294+
permissionProfile.Network)
293295
if err != nil {
294296
return 0, fmt.Errorf("failed to create ingress container: %v", err)
295297
}
@@ -1452,7 +1454,7 @@ func addEgressEnvVars(envVars map[string]string, egressContainerName string) map
14521454
}
14531455

14541456
func (c *Client) createIngressContainer(ctx context.Context, containerName string, upstreamPort int, attachStdio bool,
1455-
externalEndpointsConfig map[string]*network.EndpointSettings) (int, error) {
1457+
externalEndpointsConfig map[string]*network.EndpointSettings, networkPermissions *permissions.NetworkPermissions) (int, error) {
14561458
squidPort, err := networking.FindOrUsePort(upstreamPort + 1)
14571459
if err != nil {
14581460
return 0, fmt.Errorf("failed to find or use port %d: %v", squidPort, err)
@@ -1480,6 +1482,7 @@ func (c *Client) createIngressContainer(ctx context.Context, containerName strin
14801482
squidExposedPorts,
14811483
externalEndpointsConfig,
14821484
squidPortBindings,
1485+
networkPermissions,
14831486
)
14841487
if err != nil {
14851488
return 0, fmt.Errorf("failed to create ingress container: %v", err)

pkg/container/docker/client_deploy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (f *fakeDeployOps) createMcpContainer(
112112
return f.errMcp
113113
}
114114

115-
func (f *fakeDeployOps) createIngressContainer(_ context.Context, _ string, _ int, _ bool, _ map[string]*network.EndpointSettings) (int, error) {
115+
func (f *fakeDeployOps) createIngressContainer(_ context.Context, _ string, _ int, _ bool, _ map[string]*network.EndpointSettings, _ *permissions.NetworkPermissions) (int, error) {
116116
f.ingressCalled = true
117117
if f.errIngress != nil {
118118
return 0, f.errIngress

pkg/container/docker/squid.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ func createIngressSquidContainer(
3737
exposedPorts map[string]struct{},
3838
endpointsConfig map[string]*network.EndpointSettings,
3939
portBindings map[string][]runtime.PortBinding,
40+
networkPermissions *permissions.NetworkPermissions,
4041
) (string, error) {
41-
squidConfPath, err := createTempIngressSquidConf(containerName, upstreamPort, squidPort)
42+
squidConfPath, err := createTempIngressSquidConf(containerName, upstreamPort, squidPort, networkPermissions)
4243
if err != nil {
4344
return "", fmt.Errorf("failed to create temporary squid.conf: %v", err)
4445
}
@@ -270,12 +271,13 @@ func createTempIngressSquidConf(
270271
serverHostname string,
271272
upstreamPort int,
272273
squidPort int,
274+
networkPermissions *permissions.NetworkPermissions,
273275
) (string, error) {
274276
var sb strings.Builder
275277

276278
writeCommonConfig(&sb, serverHostname, proxyIngress)
277279

278-
writeIngressProxyConfig(&sb, serverHostname, upstreamPort, squidPort)
280+
writeIngressProxyConfig(&sb, serverHostname, upstreamPort, squidPort, networkPermissions)
279281
sb.WriteString("http_access deny all\n")
280282

281283
tmpFile, err := os.CreateTemp("", "squid-*.conf")
@@ -296,18 +298,37 @@ func createTempIngressSquidConf(
296298
return tmpFile.Name(), nil
297299
}
298300

299-
func writeIngressProxyConfig(sb *strings.Builder, serverHostname string, upstreamPort int, squidPort int) {
301+
func writeIngressProxyConfig(
302+
sb *strings.Builder,
303+
serverHostname string,
304+
upstreamPort int,
305+
squidPort int,
306+
networkPermissions *permissions.NetworkPermissions,
307+
) {
300308
portNum := strconv.Itoa(upstreamPort)
301309
squidPortNum := strconv.Itoa(squidPort)
302310
sb.WriteString(
303311
"\n# Reverse proxy setup for port " + portNum + "\n" +
304312
"http_port 0.0.0.0:" + squidPortNum + " accel defaultsite=" + serverHostname + "\n" +
305313
"cache_peer " + serverHostname + " parent " + portNum + " 0 no-query originserver name=origin_" +
306-
portNum + " connect-timeout=5 connect-fail-limit=5\n" +
307-
"acl site_" + portNum + " dstdomain " + serverHostname + "\n" +
314+
portNum + " connect-timeout=5 connect-fail-limit=5\n")
315+
316+
// Check if inbound network permissions are configured
317+
if networkPermissions != nil && networkPermissions.Inbound != nil && len(networkPermissions.Inbound.AllowHost) > 0 {
318+
// Use only the configured allowed hosts
319+
sb.WriteString("acl allowed_hosts dstdomain")
320+
for _, host := range networkPermissions.Inbound.AllowHost {
321+
sb.WriteString(" " + host)
322+
}
323+
sb.WriteString("\n")
324+
sb.WriteString("http_access allow allowed_hosts\n")
325+
} else {
326+
// Default: Allow container hostname, localhost, and 127.0.0.1
327+
sb.WriteString("acl site_" + portNum + " dstdomain " + serverHostname + "\n" +
308328
"acl local_dst dst 127.0.0.1\n" +
309329
"acl local_domain dstdomain localhost\n" +
310330
"http_access allow site_" + portNum + "\n" +
311331
"http_access allow local_dst\n" +
312332
"http_access allow local_domain\n")
333+
}
313334
}

pkg/container/docker/squid_test.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func TestCreateTempEgressSquidConf_WithACLs(t *testing.T) {
162162
func TestCreateTempIngressSquidConf_Basics(t *testing.T) {
163163
t.Parallel()
164164

165-
fp, err := createTempIngressSquidConf("svc-example", 8080, 18080)
165+
fp, err := createTempIngressSquidConf("svc-example", 8080, 18080, nil)
166166
require.NoError(t, err)
167167
t.Cleanup(func() { _ = os.Remove(fp) })
168168

@@ -183,6 +183,85 @@ func TestCreateTempIngressSquidConf_Basics(t *testing.T) {
183183
assert.Equal(t, os.FileMode(0o644), info.Mode().Perm())
184184
}
185185

186+
func TestCreateTempIngressSquidConf_WithOverrideHosts(t *testing.T) {
187+
t.Parallel()
188+
189+
networkPermissions := &permissions.NetworkPermissions{
190+
Inbound: &permissions.InboundNetworkPermissions{
191+
AllowHost: []string{"host.docker.internal", "*.internal", "api.example.com"},
192+
},
193+
}
194+
195+
fp, err := createTempIngressSquidConf("svc-example", 8080, 18080, networkPermissions)
196+
require.NoError(t, err)
197+
t.Cleanup(func() { _ = os.Remove(fp) })
198+
199+
b, err := os.ReadFile(fp)
200+
require.NoError(t, err)
201+
s := string(b)
202+
203+
assert.Contains(t, s, "visible_hostname svc-example-ingress")
204+
assert.Contains(t, s, "\n# Reverse proxy setup for port 8080\n")
205+
assert.Contains(t, s, "http_port 0.0.0.0:18080 accel defaultsite=svc-example")
206+
assert.Contains(t, s, "cache_peer svc-example parent 8080 0 no-query originserver name=origin_8080")
207+
208+
// Test that override mode is used - no default ACLs
209+
assert.NotContains(t, s, "acl site_8080 dstdomain svc-example")
210+
assert.NotContains(t, s, "acl local_dst dst 127.0.0.1")
211+
assert.NotContains(t, s, "acl local_domain dstdomain localhost")
212+
213+
// Test override hosts ACL
214+
assert.Contains(t, s, "acl allowed_hosts dstdomain host.docker.internal *.internal api.example.com")
215+
216+
// Test that only the override http_access rule is present
217+
assert.Contains(t, s, "http_access allow allowed_hosts")
218+
assert.NotContains(t, s, "http_access allow site_8080")
219+
assert.NotContains(t, s, "http_access allow local_dst")
220+
assert.NotContains(t, s, "http_access allow local_domain")
221+
222+
assert.True(t, strings.HasSuffix(strings.TrimSpace(s), "http_access deny all"))
223+
224+
info, err := os.Stat(fp)
225+
require.NoError(t, err)
226+
assert.Equal(t, os.FileMode(0o644), info.Mode().Perm())
227+
}
228+
229+
func TestCreateTempIngressSquidConf_EmptyInboundHosts(t *testing.T) {
230+
t.Parallel()
231+
232+
networkPermissions := &permissions.NetworkPermissions{
233+
Inbound: &permissions.InboundNetworkPermissions{
234+
AllowHost: []string{}, // Empty list
235+
},
236+
}
237+
238+
fp, err := createTempIngressSquidConf("svc-example", 8080, 18080, networkPermissions)
239+
require.NoError(t, err)
240+
t.Cleanup(func() { _ = os.Remove(fp) })
241+
242+
b, err := os.ReadFile(fp)
243+
require.NoError(t, err)
244+
s := string(b)
245+
246+
// Should not contain override ACL when list is empty
247+
assert.NotContains(t, s, "# Inbound network permissions override default behavior")
248+
assert.NotContains(t, s, "acl allowed_hosts")
249+
assert.NotContains(t, s, "http_access allow allowed_hosts")
250+
251+
// Should contain default ACLs and http_access rules
252+
assert.Contains(t, s, "acl site_8080 dstdomain svc-example")
253+
assert.Contains(t, s, "acl local_dst dst 127.0.0.1")
254+
assert.Contains(t, s, "acl local_domain dstdomain localhost")
255+
assert.Contains(t, s, "http_access allow site_8080")
256+
assert.Contains(t, s, "http_access allow local_dst")
257+
assert.Contains(t, s, "http_access allow local_domain")
258+
assert.True(t, strings.HasSuffix(strings.TrimSpace(s), "http_access deny all"))
259+
260+
info, err := os.Stat(fp)
261+
require.NoError(t, err)
262+
assert.Equal(t, os.FileMode(0o644), info.Mode().Perm())
263+
}
264+
186265
func TestGetSquidImage(t *testing.T) {
187266
t.Parallel()
188267

@@ -212,7 +291,7 @@ func TestTempFilesWrittenToSystemTempDir(t *testing.T) {
212291
require.NoError(t, err)
213292
t.Cleanup(func() { _ = os.Remove(fp1) })
214293

215-
fp2, err := createTempIngressSquidConf("s2", 8081, 18081)
294+
fp2, err := createTempIngressSquidConf("s2", 8081, 18081, nil)
216295
require.NoError(t, err)
217296
t.Cleanup(func() { _ = os.Remove(fp2) })
218297

pkg/permissions/profile.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ type Profile struct {
4848
type NetworkPermissions struct {
4949
// Outbound defines outbound network permissions
5050
Outbound *OutboundNetworkPermissions `json:"outbound,omitempty" yaml:"outbound,omitempty"`
51+
52+
// Inbound defines inbound network permissions
53+
Inbound *InboundNetworkPermissions `json:"inbound,omitempty" yaml:"inbound,omitempty"`
5154
}
5255

5356
// OutboundNetworkPermissions defines outbound network permissions
@@ -62,6 +65,12 @@ type OutboundNetworkPermissions struct {
6265
AllowPort []int `json:"allow_port,omitempty" yaml:"allow_port,omitempty"`
6366
}
6467

68+
// InboundNetworkPermissions defines inbound network permissions
69+
type InboundNetworkPermissions struct {
70+
// AllowHost is a list of allowed hosts for inbound connections
71+
AllowHost []string `json:"allow_host,omitempty" yaml:"allow_host,omitempty"`
72+
}
73+
6574
// NewProfile creates a new permission profile
6675
func NewProfile() *Profile {
6776
return &Profile{
@@ -74,6 +83,9 @@ func NewProfile() *Profile {
7483
AllowHost: []string{},
7584
AllowPort: []int{},
7685
},
86+
Inbound: &InboundNetworkPermissions{
87+
AllowHost: []string{},
88+
},
7789
},
7890
Privileged: false,
7991
}
@@ -109,6 +121,9 @@ func BuiltinNoneProfile() *Profile {
109121
AllowHost: []string{},
110122
AllowPort: []int{},
111123
},
124+
Inbound: &InboundNetworkPermissions{
125+
AllowHost: []string{},
126+
},
112127
},
113128
Privileged: false,
114129
}
@@ -126,6 +141,9 @@ func BuiltinNetworkProfile() *Profile {
126141
AllowHost: []string{},
127142
AllowPort: []int{},
128143
},
144+
Inbound: &InboundNetworkPermissions{
145+
AllowHost: []string{},
146+
},
129147
},
130148
Privileged: false,
131149
}

0 commit comments

Comments
 (0)