Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/server/docs.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/server/swagger.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions pkg/container/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ type deployOps interface {
upstreamPort int,
attachStdio bool,
externalEndpointsConfig map[string]*network.EndpointSettings,
networkPermissions *permissions.NetworkPermissions,
) (int, error)
}

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

func (c *Client) createIngressContainer(ctx context.Context, containerName string, upstreamPort int, attachStdio bool,
externalEndpointsConfig map[string]*network.EndpointSettings) (int, error) {
externalEndpointsConfig map[string]*network.EndpointSettings, networkPermissions *permissions.NetworkPermissions) (int, error) {
squidPort, err := networking.FindOrUsePort(upstreamPort + 1)
if err != nil {
return 0, fmt.Errorf("failed to find or use port %d: %v", squidPort, err)
Expand Down Expand Up @@ -1480,6 +1482,7 @@ func (c *Client) createIngressContainer(ctx context.Context, containerName strin
squidExposedPorts,
externalEndpointsConfig,
squidPortBindings,
networkPermissions,
)
if err != nil {
return 0, fmt.Errorf("failed to create ingress container: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/container/docker/client_deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (f *fakeDeployOps) createMcpContainer(
return f.errMcp
}

func (f *fakeDeployOps) createIngressContainer(_ context.Context, _ string, _ int, _ bool, _ map[string]*network.EndpointSettings) (int, error) {
func (f *fakeDeployOps) createIngressContainer(_ context.Context, _ string, _ int, _ bool, _ map[string]*network.EndpointSettings, _ *permissions.NetworkPermissions) (int, error) {
f.ingressCalled = true
if f.errIngress != nil {
return 0, f.errIngress
Expand Down
31 changes: 26 additions & 5 deletions pkg/container/docker/squid.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ func createIngressSquidContainer(
exposedPorts map[string]struct{},
endpointsConfig map[string]*network.EndpointSettings,
portBindings map[string][]runtime.PortBinding,
networkPermissions *permissions.NetworkPermissions,
) (string, error) {
squidConfPath, err := createTempIngressSquidConf(containerName, upstreamPort, squidPort)
squidConfPath, err := createTempIngressSquidConf(containerName, upstreamPort, squidPort, networkPermissions)
if err != nil {
return "", fmt.Errorf("failed to create temporary squid.conf: %v", err)
}
Expand Down Expand Up @@ -270,12 +271,13 @@ func createTempIngressSquidConf(
serverHostname string,
upstreamPort int,
squidPort int,
networkPermissions *permissions.NetworkPermissions,
) (string, error) {
var sb strings.Builder

writeCommonConfig(&sb, serverHostname, proxyIngress)

writeIngressProxyConfig(&sb, serverHostname, upstreamPort, squidPort)
writeIngressProxyConfig(&sb, serverHostname, upstreamPort, squidPort, networkPermissions)
sb.WriteString("http_access deny all\n")

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

func writeIngressProxyConfig(sb *strings.Builder, serverHostname string, upstreamPort int, squidPort int) {
func writeIngressProxyConfig(
sb *strings.Builder,
serverHostname string,
upstreamPort int,
squidPort int,
networkPermissions *permissions.NetworkPermissions,
) {
portNum := strconv.Itoa(upstreamPort)
squidPortNum := strconv.Itoa(squidPort)
sb.WriteString(
"\n# Reverse proxy setup for port " + portNum + "\n" +
"http_port 0.0.0.0:" + squidPortNum + " accel defaultsite=" + serverHostname + "\n" +
"cache_peer " + serverHostname + " parent " + portNum + " 0 no-query originserver name=origin_" +
portNum + " connect-timeout=5 connect-fail-limit=5\n" +
"acl site_" + portNum + " dstdomain " + serverHostname + "\n" +
portNum + " connect-timeout=5 connect-fail-limit=5\n")

// Check if inbound network permissions are configured
if networkPermissions != nil && networkPermissions.Inbound != nil && len(networkPermissions.Inbound.AllowHost) > 0 {
// Use only the configured allowed hosts
sb.WriteString("acl allowed_hosts dstdomain")
for _, host := range networkPermissions.Inbound.AllowHost {
sb.WriteString(" " + host)
}
sb.WriteString("\n")
sb.WriteString("http_access allow allowed_hosts\n")
} else {
// Default: Allow container hostname, localhost, and 127.0.0.1
sb.WriteString("acl site_" + portNum + " dstdomain " + serverHostname + "\n" +
"acl local_dst dst 127.0.0.1\n" +
"acl local_domain dstdomain localhost\n" +
"http_access allow site_" + portNum + "\n" +
"http_access allow local_dst\n" +
"http_access allow local_domain\n")
}
}
83 changes: 81 additions & 2 deletions pkg/container/docker/squid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func TestCreateTempEgressSquidConf_WithACLs(t *testing.T) {
func TestCreateTempIngressSquidConf_Basics(t *testing.T) {
t.Parallel()

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

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

func TestCreateTempIngressSquidConf_WithOverrideHosts(t *testing.T) {
t.Parallel()

networkPermissions := &permissions.NetworkPermissions{
Inbound: &permissions.InboundNetworkPermissions{
AllowHost: []string{"host.docker.internal", "*.internal", "api.example.com"},
},
}

fp, err := createTempIngressSquidConf("svc-example", 8080, 18080, networkPermissions)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(fp) })

b, err := os.ReadFile(fp)
require.NoError(t, err)
s := string(b)

assert.Contains(t, s, "visible_hostname svc-example-ingress")
assert.Contains(t, s, "\n# Reverse proxy setup for port 8080\n")
assert.Contains(t, s, "http_port 0.0.0.0:18080 accel defaultsite=svc-example")
assert.Contains(t, s, "cache_peer svc-example parent 8080 0 no-query originserver name=origin_8080")

// Test that override mode is used - no default ACLs
assert.NotContains(t, s, "acl site_8080 dstdomain svc-example")
assert.NotContains(t, s, "acl local_dst dst 127.0.0.1")
assert.NotContains(t, s, "acl local_domain dstdomain localhost")

// Test override hosts ACL
assert.Contains(t, s, "acl allowed_hosts dstdomain host.docker.internal *.internal api.example.com")

// Test that only the override http_access rule is present
assert.Contains(t, s, "http_access allow allowed_hosts")
assert.NotContains(t, s, "http_access allow site_8080")
assert.NotContains(t, s, "http_access allow local_dst")
assert.NotContains(t, s, "http_access allow local_domain")

assert.True(t, strings.HasSuffix(strings.TrimSpace(s), "http_access deny all"))

info, err := os.Stat(fp)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o644), info.Mode().Perm())
}

func TestCreateTempIngressSquidConf_EmptyInboundHosts(t *testing.T) {
t.Parallel()

networkPermissions := &permissions.NetworkPermissions{
Inbound: &permissions.InboundNetworkPermissions{
AllowHost: []string{}, // Empty list
},
}

fp, err := createTempIngressSquidConf("svc-example", 8080, 18080, networkPermissions)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(fp) })

b, err := os.ReadFile(fp)
require.NoError(t, err)
s := string(b)

// Should not contain override ACL when list is empty
assert.NotContains(t, s, "# Inbound network permissions override default behavior")
assert.NotContains(t, s, "acl allowed_hosts")
assert.NotContains(t, s, "http_access allow allowed_hosts")

// Should contain default ACLs and http_access rules
assert.Contains(t, s, "acl site_8080 dstdomain svc-example")
assert.Contains(t, s, "acl local_dst dst 127.0.0.1")
assert.Contains(t, s, "acl local_domain dstdomain localhost")
assert.Contains(t, s, "http_access allow site_8080")
assert.Contains(t, s, "http_access allow local_dst")
assert.Contains(t, s, "http_access allow local_domain")
assert.True(t, strings.HasSuffix(strings.TrimSpace(s), "http_access deny all"))

info, err := os.Stat(fp)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o644), info.Mode().Perm())
}

func TestGetSquidImage(t *testing.T) {
t.Parallel()

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

fp2, err := createTempIngressSquidConf("s2", 8081, 18081)
fp2, err := createTempIngressSquidConf("s2", 8081, 18081, nil)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(fp2) })

Expand Down
18 changes: 18 additions & 0 deletions pkg/permissions/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type Profile struct {
type NetworkPermissions struct {
// Outbound defines outbound network permissions
Outbound *OutboundNetworkPermissions `json:"outbound,omitempty" yaml:"outbound,omitempty"`

// Inbound defines inbound network permissions
Inbound *InboundNetworkPermissions `json:"inbound,omitempty" yaml:"inbound,omitempty"`
}

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

// InboundNetworkPermissions defines inbound network permissions
type InboundNetworkPermissions struct {
// AllowHost is a list of allowed hosts for inbound connections
AllowHost []string `json:"allow_host,omitempty" yaml:"allow_host,omitempty"`
}

// NewProfile creates a new permission profile
func NewProfile() *Profile {
return &Profile{
Expand All @@ -74,6 +83,9 @@ func NewProfile() *Profile {
AllowHost: []string{},
AllowPort: []int{},
},
Inbound: &InboundNetworkPermissions{
AllowHost: []string{},
},
},
Privileged: false,
}
Expand Down Expand Up @@ -109,6 +121,9 @@ func BuiltinNoneProfile() *Profile {
AllowHost: []string{},
AllowPort: []int{},
},
Inbound: &InboundNetworkPermissions{
AllowHost: []string{},
},
},
Privileged: false,
}
Expand All @@ -126,6 +141,9 @@ func BuiltinNetworkProfile() *Profile {
AllowHost: []string{},
AllowPort: []int{},
},
Inbound: &InboundNetworkPermissions{
AllowHost: []string{},
},
},
Privileged: false,
}
Expand Down