diff --git a/demos/bookinfo/manifests/clusterlink/deny-server1-policy.yaml b/demos/bookinfo/manifests/clusterlink/deny-server1-policy.yaml index 9359f5526..c4c421e67 100644 --- a/demos/bookinfo/manifests/clusterlink/deny-server1-policy.yaml +++ b/demos/bookinfo/manifests/clusterlink/deny-server1-policy.yaml @@ -12,7 +12,7 @@ spec: [{ workloadSelector: { matchLabels: { - clusterlink/metadata.gatewayName: server1 + peer.clusterlink.net/name: server1 } } } diff --git a/demos/bookinfo/test.py b/demos/bookinfo/test.py index 5d420ba57..1700e7d68 100644 --- a/demos/bookinfo/test.py +++ b/demos/bookinfo/test.py @@ -132,8 +132,8 @@ def applyPolicy(cl:Cluster, type): cl.imports.update(reviewSvc, namespace, srcK8sSvcPort, ["peer2","peer3"], [reviewSvc,reviewSvc], [namespace,namespace],"static") elif type == "diff": cl.policies.delete(name="allow-all",namespace= namespace) - cl.policies.create(name="src1topeer2",namespace= namespace, action="allow", from_attribute=[{"workloadSelector": {"matchLabels": {"clusterlink/metadata.serviceName": srcSvc1}}}],to_attribute=[{"workloadSelector": {"matchLabels": {"clusterlink/metadata.serviceName": reviewSvc, "clusterlink/metadata.gatewayName": "peer2"}}}]) - cl.policies.create(name="src2topeer3",namespace= namespace, action="allow", from_attribute=[{"workloadSelector": {"matchLabels": {"clusterlink/metadata.serviceName": srcSvc2}}}],to_attribute=[{"workloadSelector": {"matchLabels": {"clusterlink/metadata.serviceName": reviewSvc, "clusterlink/metadata.gatewayName": "peer3"}}}]) + cl.policies.create(name="src1topeer2",namespace= namespace, action="allow", from_attribute=[{"workloadSelector": {"matchLabels": {"client.clusterlink.net/labels.app": srcSvc1}}}],to_attribute=[{"workloadSelector": {"matchLabels": {"export.clusterlink.net/name": reviewSvc, "peer.clusterlink.net/name": "peer2"}}}]) + cl.policies.create(name="src2topeer3",namespace= namespace, action="allow", from_attribute=[{"workloadSelector": {"matchLabels": {"client.clusterlink.net/labels.app": srcSvc2}}}],to_attribute=[{"workloadSelector": {"matchLabels": {"export.clusterlink.net/name": reviewSvc, "peer.clusterlink.net/name": "peer3"}}}]) elif type == "show": runcmd(f'kubectl get imports.clusterlink.net') elif type == "clean": diff --git a/demos/speedtest/testdata/policy/denyFromGw.yaml b/demos/speedtest/testdata/policy/denyFromGw.yaml index 50165a2dc..4851037ac 100644 --- a/demos/speedtest/testdata/policy/denyFromGw.yaml +++ b/demos/speedtest/testdata/policy/denyFromGw.yaml @@ -8,7 +8,7 @@ spec: from: [{ workloadSelector: { matchLabels: { - clusterlink/metadata.gatewayName: peer3 + peer.clusterlink.net/name: peer3 } } } diff --git a/demos/speedtest/testdata/policy/denyToSpeedtest.yaml b/demos/speedtest/testdata/policy/denyToSpeedtest.yaml index 080b3007c..20df87931 100644 --- a/demos/speedtest/testdata/policy/denyToSpeedtest.yaml +++ b/demos/speedtest/testdata/policy/denyToSpeedtest.yaml @@ -8,7 +8,7 @@ spec: from: [{ workloadSelector: { matchLabels: { - clusterlink/metadata.serviceName: firefox + client.clusterlink.net/labels.app: firefox } } } @@ -16,7 +16,7 @@ spec: to: [{ workloadSelector: { matchLabels: { - clusterlink/metadata.serviceName: openspeedtest + export.clusterlink.net/name: openspeedtest } } }] diff --git a/examples/policies/allowToReviews.json b/examples/policies/allowToReviews.json index f813d9b99..6f1b4acb3 100644 --- a/examples/policies/allowToReviews.json +++ b/examples/policies/allowToReviews.json @@ -16,7 +16,7 @@ { "workloadSelector": { "matchLabels": { - "clusterlink/metadata.serviceName": "reviews" + "export.clusterlink.net/name": "reviews" } } } diff --git a/examples/policies/deny_from_gw1.json b/examples/policies/deny_from_gw1.json index 402c13cbf..5ec25ba4f 100644 --- a/examples/policies/deny_from_gw1.json +++ b/examples/policies/deny_from_gw1.json @@ -11,7 +11,7 @@ { "workloadSelector": { "matchLabels": { - "clusterlink/metadata.gatewayName": "gw1" + "peer.clusterlink.net/name": "gw1" } } } diff --git a/pkg/controlplane/authz/connectivitypdp/connectivity_pdp.go b/pkg/controlplane/authz/connectivitypdp/connectivity_pdp.go index 111caad87..d7ab2a111 100644 --- a/pkg/controlplane/authz/connectivitypdp/connectivity_pdp.go +++ b/pkg/controlplane/authz/connectivitypdp/connectivity_pdp.go @@ -75,7 +75,7 @@ func NewPDP() *PDP { } } -// Returns a slice of copies of the non-privileged policies stored in the PDP. +// GetPrivilegedPolicies returns a slice of copies of the non-privileged policies stored in the PDP. func (pdp *PDP) GetPrivilegedPolicies() []v1alpha1.PrivilegedAccessPolicy { pols := pdp.privilegedPolicies.getPolicies() res := []v1alpha1.PrivilegedAccessPolicy{} @@ -88,7 +88,7 @@ func (pdp *PDP) GetPrivilegedPolicies() []v1alpha1.PrivilegedAccessPolicy { return res } -// Returns a slice of copies of the non-privileged policies stored in the PDP. +// GetPolicies returns a slice of copies of the non-privileged policies stored in the PDP. func (pdp *PDP) GetPolicies() []v1alpha1.AccessPolicy { pols := pdp.regularPolicies.getPolicies() res := []v1alpha1.AccessPolicy{} @@ -101,6 +101,13 @@ func (pdp *PDP) GetPolicies() []v1alpha1.AccessPolicy { return res } +// DependsOnClientAttrs returns whether the PDP holds a policy which depends on attributes +// the client workload (From field) may or may not have. +func (pdp *PDP) DependsOnClientAttrs() bool { + return pdp.privilegedPolicies.dependsOnClientAttrs() || + pdp.regularPolicies.dependsOnClientAttrs() +} + // AddOrUpdatePolicy adds an AccessPolicy to the PDP. // If a policy with the same name already exists in the PDP, // it is updated (including updating the Action field). @@ -172,6 +179,11 @@ func (pt *policyTier) getPolicies() connPolicyMap { return res } +// dependsOnClientAttrs returns whether any of the tier's policies has a From field which depends on specific attributes. +func (pt *policyTier) dependsOnClientAttrs() bool { + return pt.denyPolicies.dependsOnClientAttrs() || pt.allowPolicies.dependsOnClientAttrs() +} + // addPolicy adds an access policy to the given tier, based on its action. // Note that within a tier, no two policies can have the same name, even if one is deny and the other is allow. func (pt *policyTier) addPolicy(policyName types.NamespacedName, policySpec *v1alpha1.AccessPolicySpec) { @@ -259,6 +271,22 @@ func (cpm connPolicyMap) decide(src WorkloadAttrs, dest *DestinationDecision, pr return false, nil } +// dependsOnClientAttrs returns whether any of the policies has a From field which depends on specific attributes. +func (cpm connPolicyMap) dependsOnClientAttrs() bool { + for _, policySpec := range cpm { + for i := range policySpec.From { + selector := policySpec.From[i].WorkloadSelector + if selector == nil { + continue + } + if len(selector.MatchExpressions) > 0 || len(selector.MatchLabels) > 0 { + return true + } + } + } + return false +} + // accessPolicyDecide returns a policy's decision on a given connection. // If the policy matches the connection, a decision based on its Action is returned. // Otherwise, it returns an "undecided" value. diff --git a/pkg/controlplane/authz/connectivitypdp/connectivity_pdp_test.go b/pkg/controlplane/authz/connectivitypdp/connectivity_pdp_test.go index 4c8ffac77..89b4f964d 100644 --- a/pkg/controlplane/authz/connectivitypdp/connectivity_pdp_test.go +++ b/pkg/controlplane/authz/connectivitypdp/connectivity_pdp_test.go @@ -186,6 +186,61 @@ func getNameOfFirstPolicyInPDP(pdp *connectivitypdp.PDP, action v1alpha1.AccessP return "" } +func TestDependsOnClientAttrs(t *testing.T) { + emptyWorkloadSet := []v1alpha1.WorkloadSetOrSelector{{WorkloadSelector: &metav1.LabelSelector{}}} + nonEmptyWorkloadSet := []v1alpha1.WorkloadSetOrSelector{trivialWorkloadSet} + allowFromAllClientsPol := v1alpha1.AccessPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-all-clients", + Namespace: defaultNS, + }, + Spec: v1alpha1.AccessPolicySpec{ + Action: v1alpha1.AccessPolicyActionAllow, + From: emptyWorkloadSet, + To: nonEmptyWorkloadSet, + }, + } + allowFromSomeClientsPol := v1alpha1.AccessPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "reg", + Namespace: defaultNS, + }, + Spec: v1alpha1.AccessPolicySpec{ + Action: v1alpha1.AccessPolicyActionAllow, + From: nonEmptyWorkloadSet, + To: nonEmptyWorkloadSet, + }, + } + allowFromSomeClientsPrivPol := v1alpha1.PrivilegedAccessPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "priv"}, + Spec: v1alpha1.AccessPolicySpec{ + Action: v1alpha1.AccessPolicyActionDeny, + From: nonEmptyWorkloadSet, + To: nonEmptyWorkloadSet, + }, + } + + pdp := connectivitypdp.NewPDP() + // no policy -> no dependency on client attributes + require.False(t, pdp.DependsOnClientAttrs()) + + // adding a policy with allow-all From field -> still no dependency on client attributes + require.Nil(t, pdp.AddOrUpdatePolicy(connectivitypdp.PolicyFromCR(&allowFromAllClientsPol))) + require.False(t, pdp.DependsOnClientAttrs()) + + // adding a policy that only allows clients with specific labels -> now we have a dependency on client attributes + require.Nil(t, pdp.AddOrUpdatePolicy(connectivitypdp.PolicyFromCR(&allowFromSomeClientsPol))) + require.True(t, pdp.DependsOnClientAttrs()) + + // deleting this last policy -> no dependency on client attributes again + require.Nil(t, pdp.DeletePolicy(types.NamespacedName{Namespace: defaultNS, Name: allowFromSomeClientsPol.Name}, false)) + require.False(t, pdp.DependsOnClientAttrs()) + + // adding a privileged policy that only allows clients with specific labels -> a dependency on client attributes + require.Nil(t, pdp.AddOrUpdatePolicy(connectivitypdp.PolicyFromPrivilegedCR(&allowFromSomeClientsPrivPol))) + require.True(t, pdp.DependsOnClientAttrs()) +} + func TestDeleteNonexistingPolicies(t *testing.T) { pdp := connectivitypdp.NewPDP() err := pdp.DeletePolicy(types.NamespacedName{Name: "no-such-policy"}, true) diff --git a/pkg/controlplane/authz/manager.go b/pkg/controlplane/authz/manager.go index ffcb88530..94ad75942 100644 --- a/pkg/controlplane/authz/manager.go +++ b/pkg/controlplane/authz/manager.go @@ -40,10 +40,13 @@ const ( // the number of seconds a JWT access token is valid before it expires. jwtExpirySeconds = 5 - ServiceNameLabel = "clusterlink/metadata.serviceName" - ServiceNamespaceLabel = "clusterlink/metadata.serviceNamespace" - ServiceLabelsPrefix = "service/metadata." - GatewayNameLabel = "clusterlink/metadata.gatewayName" + ClientNamespaceLabel = "client.clusterlink.net/namespace" + ClientSALabel = "client.clusterlink.net/service-account" + ClientLabelsPrefix = "client.clusterlink.net/labels." + ServiceNameLabel = "export.clusterlink.net/name" + ServiceNamespaceLabel = "export.clusterlink.net/namespace" + ServiceLabelsPrefix = "export.clusterlink.net/labels." + PeerNameLabel = "peer.clusterlink.net/name" ) // egressAuthorizationRequest (from local dataplane) @@ -84,9 +87,10 @@ type ingressAuthorizationResponse struct { } type podInfo struct { - name string - namespace string - labels map[string]string + name string + namespace string + serviceAccount string + labels map[string]string } // Manager manages the authorization dataplane connections. @@ -166,7 +170,12 @@ func (m *Manager) addPod(pod *v1.Pod) { defer m.podLock.Unlock() podID := types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace} - m.podList[podID] = podInfo{name: pod.Name, namespace: pod.Namespace, labels: pod.Labels} + m.podList[podID] = podInfo{ + name: pod.Name, + namespace: pod.Namespace, + labels: pod.Labels, + serviceAccount: pod.Spec.ServiceAccountName, + } for _, ip := range pod.Status.PodIPs { // ignoring host-networked Pod IPs if ip.IP != pod.Status.HostIP { @@ -211,19 +220,35 @@ func (m *Manager) getPodInfoByIP(ip string) *podInfo { return nil } +func (m *Manager) getClientAttributes(req *egressAuthorizationRequest) connectivitypdp.WorkloadAttrs { + podInfo := m.getPodInfoByIP(req.IP) + if podInfo == nil { + m.logger.Infof("Pod has no info: IP=%v.", req.IP) + return nil + } + + clientAttrs := connectivitypdp.WorkloadAttrs{ + PeerNameLabel: m.getPeerName(), + ClientNamespaceLabel: podInfo.namespace, + ClientSALabel: podInfo.serviceAccount, + } + + for k, v := range podInfo.labels { + clientAttrs[ClientLabelsPrefix+k] = v + } + + m.logger.Debugf("Client attributes: %v.", clientAttrs) + + return clientAttrs +} + // authorizeEgress authorizes a request for accessing an imported service. func (m *Manager) authorizeEgress(ctx context.Context, req *egressAuthorizationRequest) (*egressAuthorizationResponse, error) { m.logger.Infof("Received egress authorization request: %v.", req) - srcAttributes := connectivitypdp.WorkloadAttrs{GatewayNameLabel: m.getPeerName()} - podInfo := m.getPodInfoByIP(req.IP) - if podInfo != nil { - srcAttributes[ServiceNamespaceLabel] = podInfo.namespace - - if src, ok := podInfo.labels["app"]; ok { // TODO: Add support for labels other than just the "app" key. - m.logger.Infof("Received egress authorization srcLabels[app]: %v.", podInfo.labels["app"]) - srcAttributes[ServiceNameLabel] = src - } + srcAttributes := m.getClientAttributes(req) + if len(srcAttributes) == 0 && m.connectivityPDP.DependsOnClientAttrs() { + return nil, fmt.Errorf("failed to extract client attributes, however, access policies depend on such attributes") } var imp v1alpha1.Import @@ -262,7 +287,7 @@ func (m *Manager) authorizeEgress(ctx context.Context, req *egressAuthorizationR dstAttributes[ServiceNameLabel] = importSource.ExportName dstAttributes[ServiceNamespaceLabel] = importSource.ExportNamespace - dstAttributes[GatewayNameLabel] = importSource.Peer + dstAttributes[PeerNameLabel] = importSource.Peer decision, err := m.connectivityPDP.Decide(srcAttributes, dstAttributes, req.ImportName.Namespace) if err != nil { @@ -371,12 +396,18 @@ func (m *Manager) authorizeIngress( dstAttributes := connectivitypdp.WorkloadAttrs{ ServiceNameLabel: export.Name, ServiceNamespaceLabel: export.Namespace, - GatewayNameLabel: m.getPeerName(), + PeerNameLabel: m.getPeerName(), } for k, v := range export.Labels { // add export labels to destination attributes dstAttributes[ServiceLabelsPrefix+k] = v } + // do not allow requests from clients with no attributes if the PDP has attribute-dependent policies + if len(req.SrcAttributes) == 0 && m.connectivityPDP.DependsOnClientAttrs() { + resp.Allowed = false + return resp, nil + } + decision, err := m.connectivityPDP.Decide(req.SrcAttributes, dstAttributes, req.ServiceName.Namespace) if err != nil { return nil, fmt.Errorf("error deciding on an ingress connection: %w", err) diff --git a/tests/e2e/k8s/services/httpecho/client.go b/tests/e2e/k8s/services/httpecho/client.go index 5fcf777f8..3fe3139a0 100644 --- a/tests/e2e/k8s/services/httpecho/client.go +++ b/tests/e2e/k8s/services/httpecho/client.go @@ -29,6 +29,8 @@ import ( "github.com/clusterlink-net/clusterlink/tests/e2e/k8s/util" ) +const EchoClientPodName = "echo-client" + func GetEchoValue(cluster *util.KindCluster, server *util.Service) (string, error) { port, err := cluster.ExposeNodeport(server) if err != nil { @@ -76,11 +78,12 @@ func GetEchoValue(cluster *util.KindCluster, server *util.Service) (string, erro } func RunClientInPod(cluster *util.KindCluster, server *util.Service) (string, error) { + url := "http://" + server.Name body, err := cluster.RunPod(&util.Pod{ - Name: "echo-client", + Name: EchoClientPodName, Namespace: server.Namespace, Image: "curlimages/curl", - Args: []string{"curl", "-s", "-m", "1", "http://" + server.Name}, + Args: []string{"curl", "-s", "-m", "10", "--retry", "10", "--retry-delay", "1", "--retry-all-errors", url}, }) return strings.TrimSpace(body), err } diff --git a/tests/e2e/k8s/test_policy.go b/tests/e2e/k8s/test_policy.go index 87f67a972..49a75301f 100644 --- a/tests/e2e/k8s/test_policy.go +++ b/tests/e2e/k8s/test_policy.go @@ -15,6 +15,7 @@ package k8s import ( "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/clusterlink-net/clusterlink/pkg/apis/clusterlink.net/v1alpha1" "github.com/clusterlink-net/clusterlink/pkg/controlplane/authz" @@ -24,40 +25,28 @@ import ( ) func (s *TestSuite) TestPolicyLabels() { - cl, err := s.fabric.DeployClusterlinks(2, nil) - require.Nil(s.T(), err) - - require.Nil(s.T(), cl[0].CreateService(&httpEchoService)) - require.Nil(s.T(), cl[0].CreateExport(&httpEchoService)) - require.Nil(s.T(), cl[1].CreatePeer(cl[0])) - - importedService := &util.Service{ - Name: httpEchoService.Name, - Port: 80, - Labels: httpEchoService.Labels, - } - require.Nil(s.T(), cl[1].CreateImport(importedService, cl[0], httpEchoService.Name)) + cl, importedService := s.createTwoClustersWithEchoSvc() // 1. Create a policy that allows traffic only to the echo service at cl[0] - apply in cl[1] (on egress) // In addition, create a policy to only allow traffic from cl[1] - apply in cl[0] (on ingress) allowEchoPolicyName := "allow-access-to-echo-svc" srcLabels := map[string]string{ // allow traffic only from cl1 - authz.GatewayNameLabel: cl[1].Name(), + authz.PeerNameLabel: cl[1].Name(), } dstLabels := map[string]string{ // allow traffic only to echo in cl1 authz.ServiceNameLabel: httpEchoService.Name, - authz.GatewayNameLabel: cl[0].Name(), + authz.PeerNameLabel: cl[0].Name(), authz.ServiceLabelsPrefix + "env": "test", } allowEchoPolicy := util.NewPolicy(allowEchoPolicyName, v1alpha1.AccessPolicyActionAllow, srcLabels, dstLabels) require.Nil(s.T(), cl[1].CreatePolicy(allowEchoPolicy)) - srcLabels = map[string]string{authz.GatewayNameLabel: cl[1].Name()} // allow traffic only from cl1 - dstLabels = map[string]string{authz.GatewayNameLabel: cl[0].Name()} // allow traffic only to cl0 + srcLabels = map[string]string{authz.PeerNameLabel: cl[1].Name()} // allow traffic only from cl1 + dstLabels = map[string]string{authz.PeerNameLabel: cl[0].Name()} // allow traffic only to cl0 specificSrcPeerPolicy := util.NewPolicy("specific-peer", v1alpha1.AccessPolicyActionAllow, srcLabels, dstLabels) require.Nil(s.T(), cl[0].CreatePolicy(specificSrcPeerPolicy)) - data, err := cl[1].AccessService(httpecho.GetEchoValue, importedService, true, nil) + data, err := cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) require.Nil(s.T(), err) require.Equal(s.T(), cl[0].Name(), data) @@ -67,29 +56,29 @@ func (s *TestSuite) TestPolicyLabels() { denyEchoPolicy := util.NewPolicy(denyEchoPolicyName, v1alpha1.AccessPolicyActionDeny, nil, dstLabels) require.Nil(s.T(), cl[1].CreatePolicy(denyEchoPolicy)) - _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) - require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.ErrorIs(s.T(), err, &util.PodFailedError{}) // 3. Delete deny policy - connection is now allowed again require.Nil(s.T(), cl[1].DeletePolicy(denyEchoPolicyName)) - data, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, nil) + data, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) require.Nil(s.T(), err) require.Equal(s.T(), cl[0].Name(), data) // 4. Add a "deny peer cl0" policy in cl[1] - should have a higher priority and so block the connection denyCl0PolicyName := "deny-access-to-cl0" - dstLabels = map[string]string{authz.GatewayNameLabel: cl[0].Name()} + dstLabels = map[string]string{authz.PeerNameLabel: cl[0].Name()} denyCl0Policy := util.NewPolicy(denyCl0PolicyName, v1alpha1.AccessPolicyActionDeny, nil, dstLabels) require.Nil(s.T(), cl[1].CreatePolicy(denyCl0Policy)) - _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) - require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.ErrorIs(s.T(), err, &util.PodFailedError{}) // 5. Delete deny policy - connection is now allowed again require.Nil(s.T(), cl[1].DeletePolicy(denyCl0PolicyName)) - data, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, nil) + data, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) require.Nil(s.T(), err) require.Equal(s.T(), cl[0].Name(), data) @@ -98,13 +87,13 @@ func (s *TestSuite) TestPolicyLabels() { denyCl1Policy := util.NewPolicy(denyCl1PolicyName, v1alpha1.AccessPolicyActionDeny, srcLabels, nil) require.Nil(s.T(), cl[0].CreatePolicy(denyCl1Policy)) - _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) - require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.ErrorIs(s.T(), err, &util.PodFailedError{}) // 7. Delete deny policy in cl[0] - connection is now allowed again require.Nil(s.T(), cl[0].DeletePolicy(denyCl1PolicyName)) - data, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, nil) + data, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) require.Nil(s.T(), err) require.Equal(s.T(), cl[0].Name(), data) @@ -113,13 +102,13 @@ func (s *TestSuite) TestPolicyLabels() { attrsWithBadSvcName := map[string]string{ authz.ServiceNameLabel: "bad-svc", - authz.GatewayNameLabel: cl[0].Name(), + authz.PeerNameLabel: cl[0].Name(), } badSvcPolicy := util.NewPolicy("bad-svc", v1alpha1.AccessPolicyActionAllow, nil, attrsWithBadSvcName) require.Nil(s.T(), cl[1].CreatePolicy(badSvcPolicy)) - _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) - require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.ErrorIs(s.T(), err, &util.PodFailedError{}) // 9. Add an allow policy in cl[1], but with a wrong service label - connection should still be denied attrsWithBadSvcLabels := map[string]string{ @@ -128,8 +117,8 @@ func (s *TestSuite) TestPolicyLabels() { badLabelPolicy := util.NewPolicy("bad-label", v1alpha1.AccessPolicyActionAllow, nil, attrsWithBadSvcLabels) require.Nil(s.T(), cl[1].CreatePolicy(badLabelPolicy)) - _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) - require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.ErrorIs(s.T(), err, &util.PodFailedError{}) // 10. Add an allow policy in cl[1], now with the right service label - connection should be allowed attrsWithGoodSvcLabels := map[string]string{ @@ -138,29 +127,67 @@ func (s *TestSuite) TestPolicyLabels() { GoodLabelPolicy := util.NewPolicy("good-label", v1alpha1.AccessPolicyActionAllow, nil, attrsWithGoodSvcLabels) require.Nil(s.T(), cl[1].CreatePolicy(GoodLabelPolicy)) - data, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, nil) + data, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) require.Nil(s.T(), err) require.Equal(s.T(), cl[0].Name(), data) } -func (s *TestSuite) TestPrivilegedPolicies() { - cl, err := s.fabric.DeployClusterlinks(2, nil) +func (s *TestSuite) TestPodAttributes() { + cl, importedService := s.createTwoClustersWithEchoSvc() + require.Nil(s.T(), cl[0].CreatePolicy(util.PolicyAllowAll)) + require.Nil(s.T(), cl[1].CreatePolicy(util.PolicyAllowAll)) + + // 1. Sanity - just test that a pod can connect to echo service + data, err := cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) require.Nil(s.T(), err) + require.Equal(s.T(), cl[0].Name(), data) - require.Nil(s.T(), cl[0].CreateService(&httpEchoService)) - require.Nil(s.T(), cl[0].CreateExport(&httpEchoService)) - require.Nil(s.T(), cl[0].CreatePolicy(util.PolicyAllowAll)) - require.Nil(s.T(), cl[1].CreatePeer(cl[0])) + // 2. Denying clients with a service account which is different from the Pod's SA - connection should work + srcLabels := map[string]string{ + authz.ClientSALabel: "non-default", + } + denyNonDefaultSA := util.NewPolicy("deny-non-default-sa", v1alpha1.AccessPolicyActionDeny, srcLabels, nil) + require.Nil(s.T(), cl[0].CreatePolicy(denyNonDefaultSA)) + require.Nil(s.T(), cl[1].CreatePolicy(denyNonDefaultSA)) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.Nil(s.T(), err) - importedService := &util.Service{ - Name: httpEchoService.Name, - Port: 80, + // 3. Egress only - denying clients with a SA which equals the Pod's SA - connection should fail + srcLabels = map[string]string{ + authz.ClientSALabel: "default", } - require.Nil(s.T(), cl[1].CreateImport(importedService, cl[0], httpEchoService.Name)) + denyDefaultSA := util.NewPolicy("deny-default-sa", v1alpha1.AccessPolicyActionDeny, srcLabels, nil) + require.Nil(s.T(), cl[1].CreatePolicy(denyDefaultSA)) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.NotNil(s.T(), err) + require.Nil(s.T(), cl[1].DeletePolicy(denyDefaultSA.Name)) // revert + + // 4. Ingress only - denying clients with a SA which equals the Pod's SA - connection should fail + require.Nil(s.T(), cl[0].CreatePolicy(denyDefaultSA)) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.NotNil(s.T(), err) + require.Nil(s.T(), cl[0].DeletePolicy(denyDefaultSA.Name)) // revert + + // 5. Egress only - denying client-pods with a label different from the client pod - connection should work + selRequirement := metav1.LabelSelectorRequirement{ + Key: authz.ClientLabelsPrefix + "app", + Operator: metav1.LabelSelectorOpNotIn, + Values: []string{httpecho.EchoClientPodName}, + } + labelSelector := metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{selRequirement}} + denyOthers := util.NewPolicyFromLabelSelectors("deny-others", v1alpha1.AccessPolicyActionDeny, &labelSelector, nil) + require.Nil(s.T(), cl[1].CreatePolicy(denyOthers)) + _, err = cl[1].AccessService(httpecho.RunClientInPod, importedService, false, nil) + require.Nil(s.T(), err) +} + +func (s *TestSuite) TestPrivilegedPolicies() { + cl, importedService := s.createTwoClustersWithEchoSvc() + require.Nil(s.T(), cl[0].CreatePolicy(util.PolicyAllowAll)) dstLabels := map[string]string{ authz.ServiceNameLabel: httpEchoService.Name, - authz.GatewayNameLabel: cl[0].Name(), + authz.PeerNameLabel: cl[0].Name(), } privDenyPolicyName := "priv-deny" @@ -180,7 +207,7 @@ func (s *TestSuite) TestPrivilegedPolicies() { require.Nil(s.T(), cl[1].CreatePolicy(regAllowPolicy)) // 1. privileged deny has highest priority -> connection is denied - _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) + _, err := cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) // 2. deleting privileged deny -> privileged allow now has highest priority -> connection is allowed @@ -209,3 +236,20 @@ func (s *TestSuite) TestPrivilegedPolicies() { _, err = cl[1].AccessService(httpecho.GetEchoValue, importedService, true, &services.ConnectionResetError{}) require.ErrorIs(s.T(), err, &services.ConnectionResetError{}) } + +func (s *TestSuite) createTwoClustersWithEchoSvc() ([]*util.ClusterLink, *util.Service) { + cl, err := s.fabric.DeployClusterlinks(2, nil) + require.Nil(s.T(), err) + + require.Nil(s.T(), cl[0].CreateService(&httpEchoService)) + require.Nil(s.T(), cl[0].CreateExport(&httpEchoService)) + require.Nil(s.T(), cl[1].CreatePeer(cl[0])) + + importedService := &util.Service{ + Name: httpEchoService.Name, + Port: 80, + Labels: httpEchoService.Labels, + } + require.Nil(s.T(), cl[1].CreateImport(importedService, cl[0], httpEchoService.Name)) + return cl, importedService +} diff --git a/tests/e2e/k8s/util/policies.go b/tests/e2e/k8s/util/policies.go index d4b36ba4a..2c8be1db4 100644 --- a/tests/e2e/k8s/util/policies.go +++ b/tests/e2e/k8s/util/policies.go @@ -26,6 +26,26 @@ func NewPolicy( action v1alpha1.AccessPolicyAction, from, to map[string]string, ) *v1alpha1.AccessPolicy { + return NewPolicyFromLabelSelectors( + name, + action, + &metav1.LabelSelector{MatchLabels: from}, + &metav1.LabelSelector{MatchLabels: to}, + ) +} + +func NewPolicyFromLabelSelectors( + name string, + action v1alpha1.AccessPolicyAction, + from, to *metav1.LabelSelector, +) *v1alpha1.AccessPolicy { + if from == nil { + from = &metav1.LabelSelector{MatchLabels: nil} + } + if to == nil { + to = &metav1.LabelSelector{MatchLabels: nil} + } + return &v1alpha1.AccessPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -33,10 +53,10 @@ func NewPolicy( Spec: v1alpha1.AccessPolicySpec{ Action: action, From: v1alpha1.WorkloadSetOrSelectorList{{ - WorkloadSelector: &metav1.LabelSelector{MatchLabels: from}, + WorkloadSelector: from, }}, To: v1alpha1.WorkloadSetOrSelectorList{{ - WorkloadSelector: &metav1.LabelSelector{MatchLabels: to}, + WorkloadSelector: to, }}, }, } diff --git a/website/content/en/docs/main/tutorials/bookinfo/index.md b/website/content/en/docs/main/tutorials/bookinfo/index.md index 3398df09c..df6f4bed2 100644 --- a/website/content/en/docs/main/tutorials/bookinfo/index.md +++ b/website/content/en/docs/main/tutorials/bookinfo/index.md @@ -189,7 +189,7 @@ spec: to: - workloadSelector: { matchLabels: { - clusterlink/metadata.gatewayName: server1 + peer.clusterlink.net/name: server1 } } " | kubectl apply -f - @@ -223,7 +223,7 @@ spec: to: - workloadSelector: { matchLabels: { - clusterlink/metadata.gatewayName: server1 + peer.clusterlink.net/name: server1 } } " | kubectl delete -f -