Skip to content

Commit

Permalink
Respect HTTP proxies when rendering Guardian policy
Browse files Browse the repository at this point in the history
  • Loading branch information
pasanw committed Aug 23, 2024
1 parent a0208eb commit a76e9c1
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 22 deletions.
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"strings"
"time"

"golang.org/x/net/http/httpproxy"

"github.com/cloudflare/cfssl/log"

v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
Expand Down Expand Up @@ -435,6 +437,7 @@ func main() {
ShutdownContext: ctx,
MultiTenant: multiTenant,
ElasticExternal: utils.UseExternalElastic(bootConfig),
HTTPProxyConfig: httpproxy.FromEnvironment(),
}

// Before we start any controllers, make sure our options are valid.
Expand Down
41 changes: 33 additions & 8 deletions pkg/controller/clusterconnection/clusterconnection_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"fmt"
"net"

"github.com/tigera/operator/pkg/url"

"github.com/go-logr/logr"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -120,6 +122,7 @@ func newReconciler(
Provider: p,
status: statusMgr,
clusterDomain: opts.ClusterDomain,
httpsProxy: opts.HTTPProxyConfig.HTTPSProxy,
tierWatchReady: tierWatchReady,
}
c.status.Run(opts.ShutdownContext)
Expand Down Expand Up @@ -184,6 +187,7 @@ type ReconcileConnection struct {
Provider operatorv1.Provider
status status.StatusManager
clusterDomain string
httpsProxy string
tierWatchReady *utils.ReadyFlag
}

Expand Down Expand Up @@ -345,7 +349,7 @@ func (r *ReconcileConnection) Reconcile(ctx context.Context, request reconcile.R
// The Tier has been created, which means that this controller's reconciliation should no longer be a dependency
// of the License being deployed. If NetworkPolicy requires license features, it should now be safe to validate
// License presence and sufficiency.
if networkPolicyRequiresEgressAccessControl(managementClusterConnection, log) {
if networkPolicyRequiresEgressAccessControl(managementClusterConnection.Spec.ManagementClusterAddr, r.httpsProxy, log) {
license, err := utils.FetchLicenseKey(ctx, r.Client)
if err != nil {
if k8serrors.IsNotFound(err) {
Expand All @@ -366,6 +370,7 @@ func (r *ReconcileConnection) Reconcile(ctx context.Context, request reconcile.R
ch := utils.NewComponentHandler(log, r.Client, r.Scheme, managementClusterConnection)
guardianCfg := &render.GuardianConfiguration{
URL: managementClusterConnection.Spec.ManagementClusterAddr,
HTTPSProxyURL: r.httpsProxy,
TunnelCAType: managementClusterConnection.Spec.TLS.CA,
PullSecrets: pullSecrets,
OpenShift: r.Provider.IsOpenShift(),
Expand Down Expand Up @@ -417,23 +422,43 @@ func fillDefaults(mcc *operatorv1.ManagementClusterConnection) {
}
}

func networkPolicyRequiresEgressAccessControl(connection *operatorv1.ManagementClusterConnection, log logr.Logger) bool {
if clusterAddrHasDomain, err := managementClusterAddrHasDomain(connection); err == nil && clusterAddrHasDomain {
return true
} else {
func networkPolicyRequiresEgressAccessControl(target string, httpsProxyURL string, log logr.Logger) bool {
var destinationHostPort string
if httpsProxyURL != "" {
// HTTPS proxy is specified as a URL.
proxyHostPort, err := url.ParseHostPortFromHTTPProxyURL(httpsProxyURL)
if err != nil {
log.Error(err, fmt.Sprintf(
"Failed to parse ManagementClusterAddr. Assuming %s does not require license feature %s",
"Failed to parse HTTP Proxy URL (%s). Assuming %s does not require license feature %s",
httpsProxyURL,
render.GuardianPolicyName,
common.EgressAccessControlFeature,
))
return false
}

destinationHostPort = proxyHostPort
} else {
// Target is already specified as host:port.
destinationHostPort = target
}

// Determine if the host in the host:port is a domain name.
hostPortHasDomain, err := hostPortUsesDomainName(destinationHostPort)
if err != nil {
log.Error(err, fmt.Sprintf(
"Failed to parse resolved host:port (%s) for remote tunnel endpoint. Assuming %s does not require license feature %s",
destinationHostPort,
render.GuardianPolicyName,
common.EgressAccessControlFeature,
))
return false
}
return hostPortHasDomain
}

func managementClusterAddrHasDomain(connection *operatorv1.ManagementClusterConnection) (bool, error) {
host, _, err := net.SplitHostPort(connection.Spec.ManagementClusterAddr)
func hostPortUsesDomainName(hostPort string) (bool, error) {
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
return false, err
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/controller/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

v1 "github.com/tigera/operator/api/v1"
"github.com/tigera/operator/pkg/common"
"golang.org/x/net/http/httpproxy"
)

// AddOptions are passed to controllers when added to the controller manager. They
Expand All @@ -42,4 +43,6 @@ type AddOptions struct {
// use external elasticsearch. When set, the operator will not install Elasticsearch
// and instead will configure the cluster to use an external Elasticsearch.
ElasticExternal bool

HTTPProxyConfig *httpproxy.Config
}
19 changes: 17 additions & 2 deletions pkg/render/guardian.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package render
import (
"net"

"github.com/tigera/operator/pkg/url"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
Expand Down Expand Up @@ -82,6 +84,7 @@ func GuardianPolicy(cfg *GuardianConfiguration) (Component, error) {
// GuardianConfiguration contains all the config information needed to render the component.
type GuardianConfiguration struct {
URL string
HTTPSProxyURL string
PullSecrets []*corev1.Secret
OpenShift bool
Installation *operatorv1.InstallationSpec
Expand Down Expand Up @@ -378,8 +381,20 @@ func guardianAllowTigeraPolicy(cfg *GuardianConfiguration) (*v3.NetworkPolicy, e
},
}...)

// Assumes address has the form "host:port", required by net.Dial for TCP.
host, port, err := net.SplitHostPort(cfg.URL)
var tunnelDestinationHostPort string
if cfg.HTTPSProxyURL != "" {
proxyHostPort, err := url.ParseHostPortFromHTTPProxyURL(cfg.HTTPSProxyURL)
if err != nil {
return nil, err
}

tunnelDestinationHostPort = proxyHostPort
} else {
// cfg.URL has host:port form
tunnelDestinationHostPort = cfg.URL
}

host, port, err := net.SplitHostPort(tunnelDestinationHostPort)
if err != nil {
return nil, err
}
Expand Down
90 changes: 78 additions & 12 deletions pkg/render/guardian_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
package render_test

import (
"fmt"
"net"
"net/url"
"strconv"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"

v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -47,7 +51,7 @@ var _ = Describe("Rendering tests", func() {
var g render.Component
var resources []client.Object

createGuardianConfig := func(i operatorv1.InstallationSpec, addr string, openshift bool) *render.GuardianConfiguration {
createGuardianConfig := func(i operatorv1.InstallationSpec, addr string, proxyAddr string, openshift bool) *render.GuardianConfiguration {
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -69,7 +73,8 @@ var _ = Describe("Rendering tests", func() {
bundle := certificateManager.CreateTrustedBundle()

return &render.GuardianConfiguration{
URL: addr,
URL: addr,
HTTPSProxyURL: proxyAddr,
PullSecrets: []*corev1.Secret{{
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -86,7 +91,7 @@ var _ = Describe("Rendering tests", func() {

Context("Guardian component", func() {
renderGuardian := func(i operatorv1.InstallationSpec) {
cfg = createGuardianConfig(i, "127.0.0.1:1234", false)
cfg = createGuardianConfig(i, "127.0.0.1:1234", "", false)
g = render.Guardian(cfg)
Expect(g.ResolveImages(nil)).To(BeNil())
resources, _ = g.Objects()
Expand Down Expand Up @@ -193,8 +198,8 @@ var _ = Describe("Rendering tests", func() {
guardianPolicy := testutils.GetExpectedPolicyFromFile("./testutils/expected_policies/guardian.json")
guardianPolicyForOCP := testutils.GetExpectedPolicyFromFile("./testutils/expected_policies/guardian_ocp.json")

renderGuardianPolicy := func(addr string, openshift bool) {
cfg := createGuardianConfig(operatorv1.InstallationSpec{Registry: "my-reg/"}, addr, openshift)
renderGuardianPolicy := func(addr string, proxyAddr string, openshift bool) {
cfg := createGuardianConfig(operatorv1.InstallationSpec{Registry: "my-reg/"}, addr, proxyAddr, openshift)
g, err := render.GuardianPolicy(cfg)
Expect(err).NotTo(HaveOccurred())
resources, _ = g.Objects()
Expand All @@ -213,7 +218,7 @@ var _ = Describe("Rendering tests", func() {

DescribeTable("should render allow-tigera policy",
func(scenario testutils.AllowTigeraScenario) {
renderGuardianPolicy("127.0.0.1:1234", scenario.OpenShift)
renderGuardianPolicy("127.0.0.1:1234", "", scenario.OpenShift)
policy := testutils.GetAllowTigeraPolicyFromResources(policyName, resources)
expectedPolicy := getExpectedPolicy(policyName, scenario)
Expect(policy).To(Equal(expectedPolicy))
Expand All @@ -224,13 +229,74 @@ var _ = Describe("Rendering tests", func() {

// The test matrix above validates against an IP-based management cluster address.
// Validate policy adaptation for domain-based management cluster address here.
It("should adapt Guardian policy if ManagementClusterAddr is domain-based", func() {
renderGuardianPolicy("mydomain.io:8080", false)

DescribeTable("should adapt Guardian policy based on destination type", func(destAddr, proxyAddr string) {
renderGuardianPolicy(destAddr, proxyAddr, false)
policy := testutils.GetAllowTigeraPolicyFromResources(policyName, resources)
Expect(policy.Spec.Egress).To(HaveLen(7))
managementClusterEgressRule := policy.Spec.Egress[5]
Expect(managementClusterEgressRule.Destination.Domains).To(Equal([]string{"mydomain.io"}))
Expect(managementClusterEgressRule.Destination.Ports).To(Equal(networkpolicy.Ports(8080)))
})

isProxied := proxyAddr != ""
var expectedHost string
var expectedPortString string
if isProxied {
uri, err := url.ParseRequestURI(proxyAddr)
Expect(err).NotTo(HaveOccurred())
expectedPortString = uri.Port()
if expectedPortString == "" {
expectedHost = uri.Host
if uri.Scheme == "http" {
expectedPortString = "80"
} else if uri.Scheme == "https" {
expectedPortString = "443"
}
} else {
host, port, err := net.SplitHostPort(uri.Host)
Expect(err).NotTo(HaveOccurred())
expectedHost = host
expectedPortString = port
}
} else {
host, port, err := net.SplitHostPort(destAddr)
Expect(err).NotTo(HaveOccurred())
expectedHost = host
expectedPortString = port
}
expectedPort, err := strconv.ParseUint(expectedPortString, 10, 16)
Expect(err).NotTo(HaveOccurred())

if net.ParseIP(expectedHost) != nil {
// Host is an IP.
Expect(managementClusterEgressRule.Destination.Nets).To(HaveLen(1))
Expect(managementClusterEgressRule.Destination.Nets[0]).To(Equal(fmt.Sprintf("%s/32", expectedHost)))
Expect(managementClusterEgressRule.Destination.Ports).To(Equal(networkpolicy.Ports(uint16(expectedPort))))
} else {
// Host is a domain name.
Expect(managementClusterEgressRule.Destination.Domains).To(Equal([]string{expectedHost}))
Expect(managementClusterEgressRule.Destination.Ports).To(Equal(networkpolicy.Ports(uint16(expectedPort))))
}
},
// https domain proxy, https domain port proxy, https ip proxy, https ip port proxy, http domain proxy, http domain port proxy, http ip proxy, http ip port proxy, no proxy
// domain, ip
Entry("domain host:port, no proxy", "mydomain.io:8080", ""),
Entry("domain host:port, http proxy domain host", "mydomain.io:8080", "http://myproxy.io/"),
Entry("domain host:port, https proxy domain host", "mydomain.io:8080", "https://myproxy.io/"),
Entry("domain host:port, http proxy domain host:port", "mydomain.io:8080", "http://myproxy.io:9000/"),
Entry("domain host:port, https proxy domain host:port", "mydomain.io:8080", "https://myproxy.io:9000/"),
Entry("domain host:port, http proxy ip host", "mydomain.io:8080", "http://10.0.0.1/"),
Entry("domain host:port, https proxy ip host", "mydomain.io:8080", "https://10.0.0.1/"),
Entry("domain host:port, http proxy ip host:port", "mydomain.io:8080", "http://10.0.0.1:9000/"),
Entry("domain host:port, https proxy ip host:port", "mydomain.io:8080", "https://10.0.0.1:9000/"),
Entry("ip host:port, no proxy", "192.168.0.01:8080", ""),
Entry("ip host:port, http proxy domain host", "192.168.0.01:8080", "http://myproxy.io/"),
Entry("ip host:port, https proxy domain host", "192.168.0.01:8080", "https://myproxy.io/"),
Entry("ip host:port, http proxy domain host:port", "192.168.0.01:8080", "http://myproxy.io:9000/"),
Entry("ip host:port, https proxy domain host:port", "192.168.0.01:8080", "https://myproxy.io:9000/"),
Entry("ip host:port, http proxy ip host", "192.168.0.01:8080", "http://10.0.0.1/"),
Entry("ip host:port, https proxy ip host", "192.168.0.01:8080", "https://10.0.0.1/"),
Entry("ip host:port, http proxy ip host:port", "192.168.0.01:8080", "http://10.0.0.1:9000/"),
Entry("ip host:port, https proxy ip host:port", "192.168.0.01:8080", "https://10.0.0.1:9000/"),
)
})
})
})
Expand Down
25 changes: 25 additions & 0 deletions pkg/url/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package url

import (
"fmt"
"net"
"net/url"
"strings"
)
Expand All @@ -18,3 +19,27 @@ func ParseEndpoint(endpoint string) (string, string, string, error) {
}
return url.Scheme, splits[0], splits[1], nil
}

func ParseHostPortFromHTTPProxyURL(proxyURL string) (string, error) {
parsedProxyURL, err := url.ParseRequestURI(proxyURL)
if err != nil {
return "", err
}

parsedScheme := parsedProxyURL.Scheme
if parsedScheme == "" || (parsedScheme != "http" && parsedScheme != "https") {
return "", fmt.Errorf("unexpected scheme for HTTP proxy URL: %s", parsedScheme)
}

if parsedProxyURL.Port() != "" {
// Host is already in host:port form.
return parsedProxyURL.Host, nil
}

// Scheme is either http or https at this point.
if parsedProxyURL.Scheme == "http" {
return net.JoinHostPort(parsedProxyURL.Host, "80"), nil
} else {
return net.JoinHostPort(parsedProxyURL.Host, "443"), nil
}
}

0 comments on commit a76e9c1

Please sign in to comment.