From eb9539c6a782cfcbe2fc91f7899bbc789154dc7c Mon Sep 17 00:00:00 2001 From: Anita Akaeze Date: Thu, 10 Oct 2024 09:50:45 -0700 Subject: [PATCH 1/6] bump kubernetes version for AKS (#4383) * bump kubernetes version for AKS * bump kubernetes version for AKS --- charts/consul/test/terraform/aks/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/consul/test/terraform/aks/variables.tf b/charts/consul/test/terraform/aks/variables.tf index b60273bac2..f124aac143 100644 --- a/charts/consul/test/terraform/aks/variables.tf +++ b/charts/consul/test/terraform/aks/variables.tf @@ -7,7 +7,7 @@ variable "location" { } variable "kubernetes_version" { - default = "1.27" + default = "1.28" description = "Kubernetes version supported on AKS" } From 8c232896748676ae125a2c4448498cb0794e351c Mon Sep 17 00:00:00 2001 From: Anita Akaeze Date: Fri, 11 Oct 2024 10:23:26 -0700 Subject: [PATCH 2/6] NO-JIRA: cleanup iamRoles as part of nightly test cleanup (#4382) * NO-JIRA: cleanup iam_roles as part of nightly test cleanup * remove reference to log, use fmt * fix panic in cleanup code * code review feedback * code review feedback --- hack/aws-acceptance-test-cleanup/main.go | 117 +++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/hack/aws-acceptance-test-cleanup/main.go b/hack/aws-acceptance-test-cleanup/main.go index 1680e9ca63..b9827a23af 100644 --- a/hack/aws-acceptance-test-cleanup/main.go +++ b/hack/aws-acceptance-test-cleanup/main.go @@ -81,6 +81,11 @@ func realMain(ctx context.Context) error { return err } + // Find IAM roles and delete + if err := cleanupIAMRoles(ctx, iamClient); err != nil { + return err + } + // Find OIDC providers to delete. oidcProvidersOutput, err := iamClient.ListOpenIDConnectProvidersWithContext(ctx, &iam.ListOpenIDConnectProvidersInput{}) if err != nil { @@ -759,6 +764,118 @@ func destroyBackoff(ctx context.Context, resourceKind string, resourceID string, }, backoff.WithContext(expoBackoff, ctx)) } +func cleanupIAMRoles(ctx context.Context, iamClient *iam.IAM) error { + var rolesToDelete []*iam.Role + rolesToDelete, err := filterIAMRolesWithPrefix(ctx, iamClient, "consul-k8s-") + if err != nil { + return fmt.Errorf("failed to list roles: %w", err) + } + + if len(rolesToDelete) == 0 { + fmt.Println("Found no iamRoles to clean up") + return nil + } else { + fmt.Printf("Found %d IAM roles to clean up\n", len(rolesToDelete)) + } + + // Delete filtered roles + for _, role := range rolesToDelete { + roleName := aws.StringValue(role.RoleName) + err := detachRolePolicies(iamClient, role.RoleName) + if err != nil { + fmt.Printf("Failed to detach policies for role %s: %v", roleName, err) + continue + } + + _, err = iamClient.DeleteRole(&iam.DeleteRoleInput{ + RoleName: role.RoleName, + }) + if err != nil { + fmt.Printf("Failed to delete role %s: %v", roleName, err) + } else { + fmt.Printf("Deleted role: %s", roleName) + } + } + + return nil +} + +// filterIAMRolesWithPrefix is a callback function used with ListRolesPages. +// It filters roles based on specified prefix and appends matching roles to rolesToDelete. +// +// Parameters: +// - page: A single page of IAM roles returned by the AWS API +// - lastPage: A boolean indicating whether this is the last page of results +// - rolesToDelete: A pointer to the slice where matching roles are accumulated +// - rolePrefix: The prefix to filter roles by +func filterIAMRolesWithPrefix(ctx context.Context, iamClient *iam.IAM, prefix string) ([]*iam.Role, error) { + var roles []*iam.Role + + err := iamClient.ListRolesPagesWithContext(ctx, &iam.ListRolesInput{}, + func(page *iam.ListRolesOutput, lastPage bool) bool { + for _, role := range page.Roles { + name := aws.StringValue(role.RoleName) + if strings.HasPrefix(name, prefix) { + roles = append(roles, role) + } + } + + return true + }) + if err != nil { + return nil, err + } + + return roles, nil +} + +func detachRolePolicies(iamClient *iam.IAM, roleName *string) error { + if roleName == nil { + return fmt.Errorf("roleName is nil") + } + + err := iamClient.ListAttachedRolePoliciesPages(&iam.ListAttachedRolePoliciesInput{ + RoleName: roleName, + }, func(page *iam.ListAttachedRolePoliciesOutput, lastPage bool) bool { + err := detachPoliciesFromRole(iamClient, roleName, page) + if err != nil { + fmt.Printf("Error detaching policies from role %s: %v", *roleName, err) + } + return !lastPage + }) + + if err != nil { + return err + } + + return nil +} + +// detachPoliciesFromRole detaches all policies from a given role. +// +// Parameters: +// - iamClient: The IAM service client +// - roleName: Pointer to the name of the role +// - page: A single page of attached policies returned by the AWS API +func detachPoliciesFromRole(iamClient *iam.IAM, roleName *string, page *iam.ListAttachedRolePoliciesOutput) error { + for _, policy := range page.AttachedPolicies { + if policy.PolicyArn == nil { + fmt.Printf("Warning: PolicyArn is nil for a policy attached to: %s", *roleName) + continue + } + + _, err := iamClient.DetachRolePolicy(&iam.DetachRolePolicyInput{ + RoleName: roleName, + PolicyArn: policy.PolicyArn, + }) + if err != nil { + fmt.Printf("Failed to detach policy %s from role %s: %v\n", aws.StringValue(policy.PolicyArn), *roleName, err) + } + } + + return nil +} + func cleanupPersistentVolumes(ctx context.Context, ec2Client *ec2.EC2) error { var nextToken *string var toDeleteVolumes []*ec2.Volume From 124e38b68aa79c0a665a1f32cc57c266f99f72a4 Mon Sep 17 00:00:00 2001 From: Isaac <10012479+jukie@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:52:45 -0600 Subject: [PATCH 3/6] Sync Catalog: Add k8s endpoint health state to consul health check (#3874) Signed-off-by: jukie <10012479+Jukie@users.noreply.github.com> --------- Signed-off-by: jukie <10012479+Jukie@users.noreply.github.com> --- .changelog/3874.txt | 3 + control-plane/catalog/to-consul/resource.go | 12 +- .../catalog/to-consul/resource_test.go | 163 ++++++++++++++---- 3 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 .changelog/3874.txt diff --git a/.changelog/3874.txt b/.changelog/3874.txt new file mode 100644 index 0000000000..c6928774ef --- /dev/null +++ b/.changelog/3874.txt @@ -0,0 +1,3 @@ +```release-note:sync-catalog +Add Endpoint health state to registered consul service +``` diff --git a/control-plane/catalog/to-consul/resource.go b/control-plane/catalog/to-consul/resource.go index 09e7e17200..9e48305a30 100644 --- a/control-plane/catalog/to-consul/resource.go +++ b/control-plane/catalog/to-consul/resource.go @@ -46,6 +46,7 @@ const ( // consulKubernetesCheckName is the name of health check in Consul for Kubernetes readiness status. consulKubernetesCheckName = "Kubernetes Readiness Check" kubernetesSuccessReasonMsg = "Kubernetes health checks passing" + kubernetesFailureReasonMsg = "Kubernetes health checks failing" ) type NodePortSyncType string @@ -693,7 +694,6 @@ func (t *ServiceResource) generateRegistrations(key string) { } } } - } } } @@ -800,11 +800,17 @@ func (t *ServiceResource) registerServiceInstance( Name: consulKubernetesCheckName, Namespace: baseService.Namespace, Type: consulKubernetesCheckType, - Status: consulapi.HealthPassing, ServiceID: serviceID(r.Service.Service, addr), - Output: kubernetesSuccessReasonMsg, } + // Consider endpoint health state for registered consul service + if endpoint.Conditions.Ready != nil && *endpoint.Conditions.Ready { + r.Check.Status = consulapi.HealthPassing + r.Check.Output = kubernetesSuccessReasonMsg + } else { + r.Check.Status = consulapi.HealthCritical + r.Check.Output = kubernetesFailureReasonMsg + } t.consulMap[key] = append(t.consulMap[key], &r) } } diff --git a/control-plane/catalog/to-consul/resource_test.go b/control-plane/catalog/to-consul/resource_test.go index 08b08ced2f..4951d1b107 100644 --- a/control-plane/catalog/to-consul/resource_test.go +++ b/control-plane/catalog/to-consul/resource_test.go @@ -26,6 +26,7 @@ import ( const nodeName1 = "ip-10-11-12-13.ec2.internal" const nodeName2 = "ip-10-11-12-14.ec2.internal" +const nodeName3 = "ip-10-11-12-15.ec2.internal" func init() { hclog.DefaultOptions.Level = hclog.Debug @@ -763,7 +764,7 @@ func TestServiceResource_lbRegisterEndpoints(t *testing.T) { closer := controller.TestControllerRun(&serviceResource) defer closer() - node1, _ := createNodes(t, client) + node1, _, _ := createNodes(t, client) // Insert the endpoint slice _, err := client.DiscoveryV1().EndpointSlices(metav1.NamespaceDefault).Create( @@ -845,7 +846,7 @@ func TestServiceResource_nodePort(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.2.3.4", actual[0].Service.Address) require.Equal(r, 30000, actual[0].Service.Port) @@ -854,7 +855,13 @@ func TestServiceResource_nodePort(t *testing.T) { require.Equal(r, "2.3.4.5", actual[1].Service.Address) require.Equal(r, 30000, actual[1].Service.Port) require.Equal(r, "k8s-sync", actual[1].Node) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.4.5.6", actual[2].Service.Address) + require.Equal(r, 30000, actual[2].Service.Port) + require.Equal(r, "k8s-sync", actual[2].Node) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -885,7 +892,7 @@ func TestServiceResource_nodePortPrefix(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "prefixfoo", actual[0].Service.Service) require.Equal(r, "1.2.3.4", actual[0].Service.Address) require.Equal(r, 30000, actual[0].Service.Port) @@ -894,7 +901,13 @@ func TestServiceResource_nodePortPrefix(t *testing.T) { require.Equal(r, "2.3.4.5", actual[1].Service.Address) require.Equal(r, 30000, actual[1].Service.Port) require.Equal(r, "k8s-sync", actual[1].Node) + require.Equal(r, "prefixfoo", actual[2].Service.Service) + require.Equal(r, "3.4.5.6", actual[2].Service.Address) + require.Equal(r, 30000, actual[2].Service.Port) + require.Equal(r, "k8s-sync", actual[2].Node) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -911,7 +924,7 @@ func TestServiceResource_nodePort_singleEndpoint(t *testing.T) { closer := controller.TestControllerRun(&serviceResource) defer closer() - node1, _ := createNodes(t, client) + node1, _, _ := createNodes(t, client) // Insert the endpoint slice _, err := client.DiscoveryV1().EndpointSlices(metav1.NamespaceDefault).Create( @@ -994,7 +1007,7 @@ func TestServiceResource_nodePortAnnotatedPort(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.2.3.4", actual[0].Service.Address) require.Equal(r, 30001, actual[0].Service.Port) @@ -1003,7 +1016,13 @@ func TestServiceResource_nodePortAnnotatedPort(t *testing.T) { require.Equal(r, "2.3.4.5", actual[1].Service.Address) require.Equal(r, 30001, actual[1].Service.Port) require.Equal(r, "k8s-sync", actual[1].Node) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.4.5.6", actual[2].Service.Address) + require.Equal(r, 30001, actual[2].Service.Port) + require.Equal(r, "k8s-sync", actual[2].Node) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1038,7 +1057,7 @@ func TestServiceResource_nodePortUnnamedPort(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.2.3.4", actual[0].Service.Address) require.Equal(r, 30000, actual[0].Service.Port) @@ -1047,7 +1066,13 @@ func TestServiceResource_nodePortUnnamedPort(t *testing.T) { require.Equal(r, "2.3.4.5", actual[1].Service.Address) require.Equal(r, 30000, actual[1].Service.Port) require.Equal(r, "k8s-sync", actual[1].Node) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.4.5.6", actual[2].Service.Address) + require.Equal(r, 30000, actual[2].Service.Port) + require.Equal(r, "k8s-sync", actual[2].Node) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1078,16 +1103,22 @@ func TestServiceResource_nodePort_internalOnlySync(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "4.5.6.7", actual[0].Service.Address) require.Equal(r, 30000, actual[0].Service.Port) require.Equal(r, "k8s-sync", actual[0].Node) require.Equal(r, "foo", actual[1].Service.Service) - require.Equal(r, "3.4.5.6", actual[1].Service.Address) + require.Equal(r, "5.6.7.8", actual[1].Service.Address) require.Equal(r, 30000, actual[1].Service.Port) require.Equal(r, "k8s-sync", actual[1].Node) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "6.7.8.9", actual[2].Service.Address) + require.Equal(r, 30000, actual[2].Service.Port) + require.Equal(r, "k8s-sync", actual[2].Node) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1104,7 +1135,7 @@ func TestServiceResource_nodePort_externalFirstSync(t *testing.T) { closer := controller.TestControllerRun(&serviceResource) defer closer() - node1, _ := createNodes(t, client) + node1, _, _ := createNodes(t, client) node1.Status = corev1.NodeStatus{ Addresses: []corev1.NodeAddress{ @@ -1126,7 +1157,7 @@ func TestServiceResource_nodePort_externalFirstSync(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "4.5.6.7", actual[0].Service.Address) require.Equal(r, 30000, actual[0].Service.Port) @@ -1135,7 +1166,13 @@ func TestServiceResource_nodePort_externalFirstSync(t *testing.T) { require.Equal(r, "2.3.4.5", actual[1].Service.Address) require.Equal(r, 30000, actual[1].Service.Port) require.Equal(r, "k8s-sync", actual[1].Node) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.4.5.6", actual[2].Service.Address) + require.Equal(r, 30000, actual[2].Service.Port) + require.Equal(r, "k8s-sync", actual[2].Node) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1166,16 +1203,22 @@ func TestServiceResource_clusterIP(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 8080, actual[0].Service.Port) + require.Equal(r, "us-west-2a", actual[0].Service.Meta["external-k8s-topology-zone"]) require.Equal(r, "foo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 8080, actual[1].Service.Port) - require.Equal(r, "us-west-2a", actual[0].Service.Meta["external-k8s-topology-zone"]) require.Equal(r, "us-west-2b", actual[1].Service.Meta["external-k8s-topology-zone"]) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 8080, actual[2].Service.Port) + require.Equal(r, "us-west-2c", actual[2].Service.Meta["external-k8s-topology-zone"]) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1206,15 +1249,19 @@ func TestServiceResource_clusterIP_healthCheck(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, consulKubernetesCheckName, actual[0].Check.Name) require.Equal(r, consulapi.HealthPassing, actual[0].Check.Status) require.Equal(r, kubernetesSuccessReasonMsg, actual[0].Check.Output) require.Equal(r, consulKubernetesCheckType, actual[0].Check.Type) require.Equal(r, consulKubernetesCheckName, actual[1].Check.Name) - require.Equal(r, consulapi.HealthPassing, actual[1].Check.Status) - require.Equal(r, kubernetesSuccessReasonMsg, actual[1].Check.Output) + require.Equal(r, consulapi.HealthCritical, actual[1].Check.Status) + require.Equal(r, kubernetesFailureReasonMsg, actual[1].Check.Output) require.Equal(r, consulKubernetesCheckType, actual[1].Check.Type) + require.Equal(r, consulKubernetesCheckName, actual[2].Check.Name) + require.Equal(r, consulapi.HealthCritical, actual[2].Check.Status) + require.Equal(r, kubernetesFailureReasonMsg, actual[2].Check.Output) + require.Equal(r, consulKubernetesCheckType, actual[2].Check.Type) }) } @@ -1246,14 +1293,19 @@ func TestServiceResource_clusterIPPrefix(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "prefixfoo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 8080, actual[0].Service.Port) require.Equal(r, "prefixfoo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 8080, actual[1].Service.Port) + require.Equal(r, "prefixfoo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 8080, actual[2].Service.Port) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1286,14 +1338,19 @@ func TestServiceResource_clusterIPAnnotatedPortName(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 2000, actual[0].Service.Port) require.Equal(r, "foo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 2000, actual[1].Service.Port) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 2000, actual[2].Service.Port) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1326,14 +1383,19 @@ func TestServiceResource_clusterIPAnnotatedPortNumber(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 4141, actual[0].Service.Port) require.Equal(r, "foo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 4141, actual[1].Service.Port) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 4141, actual[2].Service.Port) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1368,14 +1430,19 @@ func TestServiceResource_clusterIPUnnamedPorts(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 8080, actual[0].Service.Port) require.Equal(r, "foo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 8080, actual[1].Service.Port) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 8080, actual[2].Service.Port) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1439,14 +1506,19 @@ func TestServiceResource_clusterIPAllNamespaces(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 8080, actual[0].Service.Port) require.Equal(r, "foo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 8080, actual[1].Service.Port) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 8080, actual[2].Service.Port) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1482,14 +1554,19 @@ func TestServiceResource_clusterIPTargetPortNamed(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foo", actual[0].Service.Service) require.Equal(r, "1.1.1.1", actual[0].Service.Address) require.Equal(r, 2000, actual[0].Service.Port) require.Equal(r, "foo", actual[1].Service.Service) require.Equal(r, "2.2.2.2", actual[1].Service.Address) require.Equal(r, 2000, actual[1].Service.Port) + require.Equal(r, "foo", actual[2].Service.Service) + require.Equal(r, "3.3.3.3", actual[2].Service.Address) + require.Equal(r, 2000, actual[2].Service.Port) require.NotEqual(r, actual[0].Service.ID, actual[1].Service.ID) + require.NotEqual(r, actual[0].Service.ID, actual[2].Service.ID) + require.NotEqual(r, actual[1].Service.ID, actual[2].Service.ID) }) } @@ -1520,7 +1597,7 @@ func TestServiceResource_targetRefInMeta(t *testing.T) { syncer.Lock() defer syncer.Unlock() actual := syncer.Registrations - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, "foobar", actual[0].Service.Meta[ConsulK8SRefValue]) require.Equal(r, "pod", actual[0].Service.Meta[ConsulK8SRefKind]) require.Equal(r, nodeName1, actual[0].Service.Meta[ConsulK8SNodeName]) @@ -2013,7 +2090,7 @@ func TestServiceResource_addIngress(t *testing.T) { require.Equal(r, test.expectedAddress, actual[0].Service.Address) require.Equal(r, test.expectedPort, actual[0].Service.Port) } else { - require.Len(r, actual, 2) + require.Len(r, actual, 3) require.Equal(r, test.expectedAddress, actual[0].Service.Address) require.Equal(r, test.expectedPort, actual[0].Service.Port) } @@ -2085,8 +2162,8 @@ func clusterIPService(name, namespace string) *corev1.Service { } } -// createNodes calls the fake k8s client to create two Kubernetes nodes and returns them. -func createNodes(t *testing.T, client *fake.Clientset) (*corev1.Node, *corev1.Node) { +// createNodes calls the fake k8s client to create three Kubernetes nodes and returns them. +func createNodes(t *testing.T, client *fake.Clientset) (*corev1.Node, *corev1.Node, *corev1.Node) { // Insert the nodes node1 := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ @@ -2112,21 +2189,37 @@ func createNodes(t *testing.T, client *fake.Clientset) (*corev1.Node, *corev1.No Status: corev1.NodeStatus{ Addresses: []corev1.NodeAddress{ {Type: corev1.NodeExternalIP, Address: "2.3.4.5"}, - {Type: corev1.NodeInternalIP, Address: "3.4.5.6"}, - {Type: corev1.NodeInternalIP, Address: "6.7.8.9"}, + {Type: corev1.NodeInternalIP, Address: "5.6.7.8"}, + {Type: corev1.NodeInternalIP, Address: "8.9.10.11"}, }, }, } _, err = client.CoreV1().Nodes().Create(context.Background(), node2, metav1.CreateOptions{}) require.NoError(t, err) - return node1, node2 + node3 := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName3, + }, + + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "3.4.5.6"}, + {Type: corev1.NodeInternalIP, Address: "6.7.8.9"}, + {Type: corev1.NodeInternalIP, Address: "9.10.11.12"}, + }, + }, + } + _, err = client.CoreV1().Nodes().Create(context.Background(), node3, metav1.CreateOptions{}) + require.NoError(t, err) + return node1, node2, node3 } -// createEndpointSlices calls the fake k8s client to create an endpoint slices with two endpoints on different nodes. +// createEndpointSlices calls the fake k8s client to create an endpoint slices with three endpoints on different nodes. func createEndpointSlice(t *testing.T, client *fake.Clientset, serviceName string, namespace string) { node1 := nodeName1 node2 := nodeName2 + node3 := nodeName3 targetRef := corev1.ObjectReference{Kind: "pod", Name: "foobar"} _, err := client.DiscoveryV1().EndpointSlices(namespace).Create( @@ -2152,13 +2245,23 @@ func createEndpointSlice(t *testing.T, client *fake.Clientset, serviceName strin { Addresses: []string{"2.2.2.2"}, Conditions: discoveryv1.EndpointConditions{ - Ready: ptr.To(true), + Ready: nil, Serving: ptr.To(true), Terminating: ptr.To(false), }, NodeName: &node2, Zone: ptr.To("us-west-2b"), }, + { + Addresses: []string{"3.3.3.3"}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(false), + Serving: ptr.To(false), + Terminating: ptr.To(true), + }, + NodeName: &node3, + Zone: ptr.To("us-west-2c"), + }, }, Ports: []discoveryv1.EndpointPort{ { From 1e3bbd81b0429dd09c5727898736fbd936747adc Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Thu, 17 Oct 2024 13:26:21 -0400 Subject: [PATCH 4/6] Update changelog to include 1.6.0 (#4392) --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce1f25d0a..8882d2217e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 1.6.0 (October 16, 2024) + +> NOTE: Consul K8s 1.6.x is compatible with Consul 1.20.x and Consul Dataplane 1.6.x. Refer to our [compatibility matrix](https://developer.hashicorp.com/consul/docs/k8s/compatibility) for more info. + + +SECURITY: + +* Upgrade Go to use 1.22.7. This addresses CVE + [CVE-2024-34155](https://nvd.nist.gov/vuln/detail/CVE-2024-34155) [[GH-4313](https://github.com/hashicorp/consul-k8s/issues/4313)] + +IMPROVEMENTS: + +* dns-proxy: add the ability to deploy a DNS proxy within the kubernetes cluster that forwards DNS requests to the consul server and can be configured with an ACL token and make partition aware DNS requests. [[GH-4300](https://github.com/hashicorp/consul-k8s/issues/4300)] +* sync-catalog: expose prometheus scrape metrics on sync-catalog pods [[GH-4212](https://github.com/hashicorp/consul-k8s/issues/4212)] +* connect-inject: remove unnecessary resource permissions from connect-inject ClusterRole [[GH-4307](https://github.com/hashicorp/consul-k8s/issues/4307)] +* helm: Exclude gke namespaces from being connect-injected when the connect-inject: default: true value is set. [[GH-4333](https://github.com/hashicorp/consul-k8s/issues/4333)] + +BUG FIXES: + +* control-plane: add missing `$HOST_IP` environment variable to consul-dataplane sidecar containers [[GH-4277](https://github.com/hashicorp/consul-k8s/issues/4277)] +* helm: Fix ArgoCD hooks related annotations on server-acl-init Job, they must be added at Job definition and not template level. [[GH-3989](https://github.com/hashicorp/consul-k8s/issues/3989)] +* sync-catalog: Enable the user to purge the registered services by passing parent node and necessary filters. [[GH-4255](https://github.com/hashicorp/consul-k8s/issues/4255)] + ## 1.5.3 (August 30, 2024) SECURITY: From c64b7262ac74ff593060d5a83b9edef4d023d27d Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Thu, 17 Oct 2024 15:39:44 -0400 Subject: [PATCH 5/6] Prepare branch for future minor release (#4391) --- charts/consul/Chart.yaml | 10 +++++----- charts/consul/values.yaml | 6 +++--- version/version.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/charts/consul/Chart.yaml b/charts/consul/Chart.yaml index 3dc8d13015..7855b049c1 100644 --- a/charts/consul/Chart.yaml +++ b/charts/consul/Chart.yaml @@ -3,8 +3,8 @@ apiVersion: v2 name: consul -version: 1.6.0-dev -appVersion: 1.20-dev +version: 1.7.0-dev +appVersion: 1.21-dev kubeVersion: ">=1.22.0-0" description: Official HashiCorp Consul Chart home: https://www.consul.io @@ -16,11 +16,11 @@ annotations: artifacthub.io/prerelease: true artifacthub.io/images: | - name: consul - image: docker.mirror.hashicorp.services/hashicorppreview/consul:1.20-dev + image: docker.mirror.hashicorp.services/hashicorppreview/consul:1.21-dev - name: consul-k8s-control-plane - image: docker.mirror.hashicorp.services/hashicorppreview/consul-k8s-control-plane:1.6-dev + image: docker.mirror.hashicorp.services/hashicorppreview/consul-k8s-control-plane:1.7-dev - name: consul-dataplane - image: docker.mirror.hashicorp.services/hashicorppreview/consul-dataplane:1.6-dev + image: docker.mirror.hashicorp.services/hashicorppreview/consul-dataplane:1.7-dev - name: envoy image: envoyproxy/envoy:v1.25.11 artifacthub.io/license: MPL-2.0 diff --git a/charts/consul/values.yaml b/charts/consul/values.yaml index 4a6aa20060..b9f9907e59 100644 --- a/charts/consul/values.yaml +++ b/charts/consul/values.yaml @@ -66,7 +66,7 @@ global: # image: "hashicorp/consul-enterprise:1.10.0-ent" # ``` # @default: hashicorp/consul: - image: docker.mirror.hashicorp.services/hashicorppreview/consul:1.20-dev + image: docker.mirror.hashicorp.services/hashicorppreview/consul:1.21-dev # Array of objects containing image pull secret names that will be applied to each service account. # This can be used to reference image pull secrets if using a custom consul or consul-k8s-control-plane Docker image. @@ -86,7 +86,7 @@ global: # image that is used for functionality such as catalog sync. # This can be overridden per component. # @default: hashicorp/consul-k8s-control-plane: - imageK8S: docker.mirror.hashicorp.services/hashicorppreview/consul-k8s-control-plane:1.6-dev + imageK8S: docker.mirror.hashicorp.services/hashicorppreview/consul-k8s-control-plane:1.7-dev # The image pull policy used globally for images controlled by Consul (consul, consul-dataplane, consul-k8s, consul-telemetry-collector). # One of "IfNotPresent", "Always", "Never", and "". Refer to https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy @@ -793,7 +793,7 @@ global: # The name (and tag) of the consul-dataplane Docker image used for the # connect-injected sidecar proxies and mesh, terminating, and ingress gateways. # @default: hashicorp/consul-dataplane: - imageConsulDataplane: docker.mirror.hashicorp.services/hashicorppreview/consul-dataplane:1.6-dev + imageConsulDataplane: docker.mirror.hashicorp.services/hashicorppreview/consul-dataplane:1.7-dev # Configuration for running this Helm chart on the Red Hat OpenShift platform. # This Helm chart currently supports OpenShift v4.x+. diff --git a/version/version.go b/version/version.go index e5d773c66d..ff7207c456 100644 --- a/version/version.go +++ b/version/version.go @@ -17,7 +17,7 @@ var ( // // Version must conform to the format expected by // github.com/hashicorp/go-version for tests to work. - Version = "1.6.0" + Version = "1.7.0" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release From bf41f34fa57290461d9b8ad7c991a5347391b2bf Mon Sep 17 00:00:00 2001 From: Michael Zalimeni Date: Thu, 17 Oct 2024 18:17:25 -0400 Subject: [PATCH 6/6] [NET-11043] crd: support request normalization and header match options to prevent L7 intentions bypass (#4385) crd: support request normalization and header match options to prevent L7 intentions bypass * crd: support L7 intentions header match contains and ignoreCase * crd: support mesh http.incoming.requestNormalization * crd: remove requirement for mesh http.sanitizeXForwardedClientCert This is a boolean field, and should not be required. Removing the requirement allows for it to be omitted when other fields are specified. --- .changelog/4385.txt | 6 + acceptance/go.mod | 2 +- acceptance/go.sum | 4 +- charts/consul/templates/crd-meshes.yaml | 49 ++++++- .../templates/crd-serviceintentions.yaml | 9 ++ cli/go.mod | 2 +- cli/go.sum | 6 +- control-plane/api/v1alpha1/mesh_types.go | 125 +++++++++++++++++- control-plane/api/v1alpha1/mesh_types_test.go | 112 ++++++++++++++++ .../api/v1alpha1/serviceintentions_types.go | 28 ++-- .../v1alpha1/serviceintentions_types_test.go | 92 ++++++++----- control-plane/catalog/metrics/metrics.go | 3 + .../bases/consul.hashicorp.com_meshes.yaml | 49 ++++++- ...onsul.hashicorp.com_serviceintentions.yaml | 9 ++ control-plane/go.mod | 4 +- control-plane/go.sum | 4 +- control-plane/helper/test/test_util.go | 2 +- .../subcommand/common/metrics_util.go | 3 + 18 files changed, 449 insertions(+), 60 deletions(-) create mode 100644 .changelog/4385.txt diff --git a/.changelog/4385.txt b/.changelog/4385.txt new file mode 100644 index 0000000000..2d45c62919 --- /dev/null +++ b/.changelog/4385.txt @@ -0,0 +1,6 @@ +```release-note:security +crd: Add `http.incoming.requestNormalization` to the Mesh CRD to support configuring service traffic request normalization. +``` +```release-note:security +crd: Add `contains` and `ignoreCase` to the Intentions CRD to support configuring L7 Header intentions resilient to variable casing and multiple header values. +``` diff --git a/acceptance/go.mod b/acceptance/go.mod index 7d78edc277..9b2b9e4cdb 100644 --- a/acceptance/go.mod +++ b/acceptance/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/uuid v1.3.0 github.com/gruntwork-io/terratest v0.46.7 github.com/hashicorp/consul-k8s/control-plane v0.0.0-20240821160356-557f7c37e108 - github.com/hashicorp/consul/api v1.29.4 + github.com/hashicorp/consul/api v1.30.0 github.com/hashicorp/consul/sdk v0.16.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 diff --git a/acceptance/go.sum b/acceptance/go.sum index 18b2c1bcf0..cde3feb099 100644 --- a/acceptance/go.sum +++ b/acceptance/go.sum @@ -186,8 +186,8 @@ github.com/gruntwork-io/terratest v0.46.7 h1:oqGPBBO87SEsvBYaA0R5xOq+Lm2Xc5dmFVf github.com/gruntwork-io/terratest v0.46.7/go.mod h1:6gI5MlLeyF+SLwqocA5GBzcTix+XiuxCy1BPwKuT+WM= github.com/hashicorp/consul-k8s/control-plane v0.0.0-20240821160356-557f7c37e108 h1:5jSMtMGeY//hvkAefiomxP1Jqb5MtnKgsnlsZpEwiJE= github.com/hashicorp/consul-k8s/control-plane v0.0.0-20240821160356-557f7c37e108/go.mod h1:SY22WR9TJmlcK18Et2MAqy+kqAFJzbWFElN89vMTSiM= -github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE= -github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg= +github.com/hashicorp/consul/api v1.30.0 h1:ArHVMMILb1nQv8vZSGIwwQd2gtc+oSQZ6CalyiyH2XQ= +github.com/hashicorp/consul/api v1.30.0/go.mod h1:B2uGchvaXVW2JhFoS8nqTxMD5PBykr4ebY4JWHTTeLM= github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0= github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= diff --git a/charts/consul/templates/crd-meshes.yaml b/charts/consul/templates/crd-meshes.yaml index f81e61a2c5..9ecc1f3cbc 100644 --- a/charts/consul/templates/crd-meshes.yaml +++ b/charts/consul/templates/crd-meshes.yaml @@ -66,10 +66,55 @@ spec: http: description: HTTP defines the HTTP configuration for the service mesh. properties: + incoming: + description: Incoming configures settings for incoming HTTP traffic + to mesh proxies. + properties: + requestNormalization: + description: |- + RequestNormalizationMeshConfig contains options pertaining to the + normalization of HTTP requests processed by mesh proxies. + properties: + headersWithUnderscoresAction: + description: |- + HeadersWithUnderscoresAction sets the value of the \`headers_with_underscores_action\` option in the Envoy + listener's \`HttpConnectionManager\` under \`common_http_protocol_options\`. The default value of this option is + empty, which is equivalent to \`ALLOW\`. Refer to the Envoy documentation for more information on available + options. + type: string + insecureDisablePathNormalization: + description: |- + InsecureDisablePathNormalization sets the value of the \`normalize_path\` option in the Envoy listener's + `HttpConnectionManager`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is + set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to + RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions + with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with + non-normalized path values. + type: boolean + mergeSlashes: + description: |- + MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`. + The default value is \`false\`. This option controls the normalization of request URL paths by merging + consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path + match rules, we recommend enabling this setting to avoid match rule circumvention through non-normalized path + values, unless legitimate service traffic depends on allowing for repeat \`/\` characters, or upstream services + are configured to differentiate between single and multiple slashes. + type: boolean + pathWithEscapedSlashesAction: + description: |- + PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy + listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to + \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths + with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this + setting to avoid match rule circumvention through non-normalized path values, unless legitimate service traffic + depends on allowing for escaped \`/\` or \`\\\` characters, or upstream services are configured to differentiate + between escaped and unescaped slashes. Refer to the Envoy documentation for more information on available + options. + type: string + type: object + type: object sanitizeXForwardedClientCert: type: boolean - required: - - sanitizeXForwardedClientCert type: object peering: description: Peering defines the peering configuration for the service diff --git a/charts/consul/templates/crd-serviceintentions.yaml b/charts/consul/templates/crd-serviceintentions.yaml index 72159ec187..09daae2f6c 100644 --- a/charts/consul/templates/crd-serviceintentions.yaml +++ b/charts/consul/templates/crd-serviceintentions.yaml @@ -168,10 +168,19 @@ spec: If more than one is configured all must match for the overall match to apply. items: properties: + contains: + description: Contains matches if the header + with the given name contains this value. + type: string exact: description: Exact matches if the header with the given name is this value. type: string + ignoreCase: + description: IgnoreCase ignores the case of + the header value when matching with exact, + prefix, suffix, or contains. + type: boolean invert: description: Invert inverts the logic of the match. diff --git a/cli/go.mod b/cli/go.mod index f97cda4aef..fd44909f19 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -96,7 +96,7 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect - github.com/hashicorp/consul/api v1.29.4 // indirect + github.com/hashicorp/consul/api v1.30.0 // indirect github.com/hashicorp/consul/envoyextensions v0.7.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/cli/go.sum b/cli/go.sum index 9edb6c903b..1609e4bb0b 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -288,12 +288,10 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE= -github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg= +github.com/hashicorp/consul/api v1.30.0 h1:ArHVMMILb1nQv8vZSGIwwQd2gtc+oSQZ6CalyiyH2XQ= +github.com/hashicorp/consul/api v1.30.0/go.mod h1:B2uGchvaXVW2JhFoS8nqTxMD5PBykr4ebY4JWHTTeLM= github.com/hashicorp/consul/envoyextensions v0.7.3 h1:5Gn1Hj135NYNRBmB3IdwhkxIHQgEJPjXYPZcA+05rNY= github.com/hashicorp/consul/envoyextensions v0.7.3/go.mod h1:tya/kHsOBGaeAS9inAfUFJIEJ812c125cQD4MrLTt2s= -github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0= -github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/hashicorp/consul/troubleshoot v0.7.1 h1:IQYxC1qsV3jO74VZDyPi283Ufi84/mXSMm53U8dsN2M= diff --git a/control-plane/api/v1alpha1/mesh_types.go b/control-plane/api/v1alpha1/mesh_types.go index 4d8a14358b..f70167a912 100644 --- a/control-plane/api/v1alpha1/mesh_types.go +++ b/control-plane/api/v1alpha1/mesh_types.go @@ -87,7 +87,18 @@ type MeshTLSConfig struct { } type MeshHTTPConfig struct { - SanitizeXForwardedClientCert bool `json:"sanitizeXForwardedClientCert"` + SanitizeXForwardedClientCert bool `json:"sanitizeXForwardedClientCert,omitempty"` + // Incoming configures settings for incoming HTTP traffic to mesh proxies. + Incoming *MeshDirectionalHTTPConfig `json:"incoming,omitempty"` + // There is not currently an outgoing MeshDirectionalHTTPConfig, as + // the only required config for either direction at present is inbound + // request normalization. +} + +// MeshDirectionalHTTPConfig holds mesh configuration specific to HTTP +// requests for a given traffic direction. +type MeshDirectionalHTTPConfig struct { + RequestNormalization *RequestNormalizationMeshConfig `json:"requestNormalization,omitempty"` } type PeeringMeshConfig struct { @@ -117,6 +128,61 @@ type MeshDirectionalTLSConfig struct { CipherSuites []string `json:"cipherSuites,omitempty"` } +// RequestNormalizationMeshConfig contains options pertaining to the +// normalization of HTTP requests processed by mesh proxies. +type RequestNormalizationMeshConfig struct { + // InsecureDisablePathNormalization sets the value of the \`normalize_path\` option in the Envoy listener's + // `HttpConnectionManager`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is + // set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to + // RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions + // with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with + // non-normalized path values. + InsecureDisablePathNormalization bool `json:"insecureDisablePathNormalization,omitempty"` + // MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`. + // The default value is \`false\`. This option controls the normalization of request URL paths by merging + // consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path + // match rules, we recommend enabling this setting to avoid match rule circumvention through non-normalized path + // values, unless legitimate service traffic depends on allowing for repeat \`/\` characters, or upstream services + // are configured to differentiate between single and multiple slashes. + MergeSlashes bool `json:"mergeSlashes,omitempty"` + // PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy + // listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to + // \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths + // with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this + // setting to avoid match rule circumvention through non-normalized path values, unless legitimate service traffic + // depends on allowing for escaped \`/\` or \`\\\` characters, or upstream services are configured to differentiate + // between escaped and unescaped slashes. Refer to the Envoy documentation for more information on available + // options. + PathWithEscapedSlashesAction string `json:"pathWithEscapedSlashesAction,omitempty"` + // HeadersWithUnderscoresAction sets the value of the \`headers_with_underscores_action\` option in the Envoy + // listener's \`HttpConnectionManager\` under \`common_http_protocol_options\`. The default value of this option is + // empty, which is equivalent to \`ALLOW\`. Refer to the Envoy documentation for more information on available + // options. + HeadersWithUnderscoresAction string `json:"headersWithUnderscoresAction,omitempty"` +} + +// PathWithEscapedSlashesAction is an enum that defines the action to take when +// a request path contains escaped slashes. It mirrors exactly the set of options +// in Envoy's UriPathNormalizationOptions.PathWithEscapedSlashesAction enum. +// See github.com/envoyproxy/go-control-plane envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction. +const ( + PathWithEscapedSlashesActionDefault = "IMPLEMENTATION_SPECIFIC_DEFAULT" + PathWithEscapedSlashesActionKeep = "KEEP_UNCHANGED" + PathWithEscapedSlashesActionReject = "REJECT_REQUEST" + PathWithEscapedSlashesActionUnescapeAndRedirect = "UNESCAPE_AND_REDIRECT" + PathWithEscapedSlashesActionUnescapeAndForward = "UNESCAPE_AND_FORWARD" +) + +// HeadersWithUnderscoresAction is an enum that defines the action to take when +// a request contains headers with underscores. It mirrors exactly the set of +// options in Envoy's HttpProtocolOptions.HeadersWithUnderscoresAction enum. +// See github.com/envoyproxy/go-control-plane envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction. +const ( + HeadersWithUnderscoresActionAllow = "ALLOW" + HeadersWithUnderscoresActionRejectRequest = "REJECT_REQUEST" + HeadersWithUnderscoresActionDropHeader = "DROP_HEADER" +) + func (in *TransparentProxyMeshConfig) toConsul() capi.TransparentProxyMeshConfig { return capi.TransparentProxyMeshConfig{MeshDestinationsOnly: in.MeshDestinationsOnly} } @@ -227,6 +293,11 @@ func (in *Mesh) Validate(consulMeta common.ConsulMeta) error { errs = append(errs, in.Spec.TLS.validate(path.Child("tls"))...) errs = append(errs, in.Spec.Peering.validate(path.Child("peering"), consulMeta.PartitionsEnabled, consulMeta.Partition)...) + if in.Spec.HTTP != nil && + in.Spec.HTTP.Incoming != nil && + in.Spec.HTTP.Incoming.RequestNormalization != nil { + errs = append(errs, in.Spec.HTTP.Incoming.RequestNormalization.validate(path.Child("http", "incoming", "requestNormalization"))...) + } if len(errs) > 0 { return apierrors.NewInvalid( @@ -252,6 +323,28 @@ func (in *MeshHTTPConfig) toConsul() *capi.MeshHTTPConfig { } return &capi.MeshHTTPConfig{ SanitizeXForwardedClientCert: in.SanitizeXForwardedClientCert, + Incoming: in.Incoming.toConsul(), + } +} + +func (in *MeshDirectionalHTTPConfig) toConsul() *capi.MeshDirectionalHTTPConfig { + if in == nil { + return nil + } + return &capi.MeshDirectionalHTTPConfig{ + RequestNormalization: in.RequestNormalization.toConsul(), + } +} + +func (in *RequestNormalizationMeshConfig) toConsul() *capi.RequestNormalizationMeshConfig { + if in == nil { + return nil + } + return &capi.RequestNormalizationMeshConfig{ + InsecureDisablePathNormalization: in.InsecureDisablePathNormalization, + MergeSlashes: in.MergeSlashes, + PathWithEscapedSlashesAction: in.PathWithEscapedSlashesAction, + HeadersWithUnderscoresAction: in.HeadersWithUnderscoresAction, } } @@ -316,6 +409,36 @@ func (in *PeeringMeshConfig) validate(path *field.Path, partitionsEnabled bool, return errs } +func (in *RequestNormalizationMeshConfig) validate(path *field.Path) field.ErrorList { + if in == nil { + return nil + } + + var errs field.ErrorList + pathWithEscapedSlashesActions := []string{ + PathWithEscapedSlashesActionDefault, + PathWithEscapedSlashesActionKeep, + PathWithEscapedSlashesActionReject, + PathWithEscapedSlashesActionUnescapeAndRedirect, + PathWithEscapedSlashesActionUnescapeAndForward, + "", + } + headersWithUnderscoresActions := []string{ + HeadersWithUnderscoresActionAllow, + HeadersWithUnderscoresActionRejectRequest, + HeadersWithUnderscoresActionDropHeader, + "", + } + + if !sliceContains(pathWithEscapedSlashesActions, in.PathWithEscapedSlashesAction) { + errs = append(errs, field.Invalid(path.Child("pathWithEscapedSlashesAction"), in.PathWithEscapedSlashesAction, notInSliceMessage(pathWithEscapedSlashesActions))) + } + if !sliceContains(headersWithUnderscoresActions, in.HeadersWithUnderscoresAction) { + errs = append(errs, field.Invalid(path.Child("headersWithUnderscoresAction"), in.HeadersWithUnderscoresAction, notInSliceMessage(headersWithUnderscoresActions))) + } + return errs +} + // DefaultNamespaceFields has no behaviour here as meshes have no namespace specific fields. func (in *Mesh) DefaultNamespaceFields(_ common.ConsulMeta) { } diff --git a/control-plane/api/v1alpha1/mesh_types_test.go b/control-plane/api/v1alpha1/mesh_types_test.go index 7a925fb0ce..5911e25d4d 100644 --- a/control-plane/api/v1alpha1/mesh_types_test.go +++ b/control-plane/api/v1alpha1/mesh_types_test.go @@ -64,6 +64,14 @@ func TestMesh_MatchesConsul(t *testing.T) { }, HTTP: &MeshHTTPConfig{ SanitizeXForwardedClientCert: true, + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default + MergeSlashes: true, + PathWithEscapedSlashesAction: "REJECT_REQUEST", + HeadersWithUnderscoresAction: "DROP_HEADER", + }, + }, }, Peering: &PeeringMeshConfig{ PeerThroughMeshGateways: true, @@ -90,6 +98,14 @@ func TestMesh_MatchesConsul(t *testing.T) { }, HTTP: &capi.MeshHTTPConfig{ SanitizeXForwardedClientCert: true, + Incoming: &capi.MeshDirectionalHTTPConfig{ + RequestNormalization: &capi.RequestNormalizationMeshConfig{ + InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default + MergeSlashes: true, + PathWithEscapedSlashesAction: "REJECT_REQUEST", + HeadersWithUnderscoresAction: "DROP_HEADER", + }, + }, }, Peering: &capi.PeeringMeshConfig{ PeerThroughMeshGateways: true, @@ -168,6 +184,14 @@ func TestMesh_ToConsul(t *testing.T) { }, HTTP: &MeshHTTPConfig{ SanitizeXForwardedClientCert: true, + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default + MergeSlashes: true, + PathWithEscapedSlashesAction: "REJECT_REQUEST", + HeadersWithUnderscoresAction: "DROP_HEADER", + }, + }, }, Peering: &PeeringMeshConfig{ PeerThroughMeshGateways: true, @@ -194,6 +218,14 @@ func TestMesh_ToConsul(t *testing.T) { }, HTTP: &capi.MeshHTTPConfig{ SanitizeXForwardedClientCert: true, + Incoming: &capi.MeshDirectionalHTTPConfig{ + RequestNormalization: &capi.RequestNormalizationMeshConfig{ + InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default + MergeSlashes: true, + PathWithEscapedSlashesAction: "REJECT_REQUEST", + HeadersWithUnderscoresAction: "DROP_HEADER", + }, + }, }, Peering: &capi.PeeringMeshConfig{ PeerThroughMeshGateways: true, @@ -367,6 +399,76 @@ func TestMesh_Validate(t *testing.T) { }, }, }, + "http.incoming.requestNormalization.pathWithEscapedSlashesAction valid": { + input: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: MeshSpec{ + HTTP: &MeshHTTPConfig{ + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD", + }, + }, + }, + }, + }, + }, + "http.incoming.requestNormalization.pathWithEscapedSlashesAction invalid": { + input: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: MeshSpec{ + HTTP: &MeshHTTPConfig{ + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + PathWithEscapedSlashesAction: "foo", + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.http.incoming.requestNormalization.pathWithEscapedSlashesAction: Invalid value: "foo": must be one of "IMPLEMENTATION_SPECIFIC_DEFAULT", "KEEP_UNCHANGED", "REJECT_REQUEST", "UNESCAPE_AND_REDIRECT", "UNESCAPE_AND_FORWARD", ""`, + }, + }, + "http.incoming.requestNormalization.headerWithUnderscoresAction valid": { + input: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: MeshSpec{ + HTTP: &MeshHTTPConfig{ + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + HeadersWithUnderscoresAction: "REJECT_REQUEST", + }, + }, + }, + }, + }, + }, + "http.incoming.requestNormalization.headersWithUnderscoresAction invalid": { + input: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: MeshSpec{ + HTTP: &MeshHTTPConfig{ + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + HeadersWithUnderscoresAction: "bar", + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.http.incoming.requestNormalization.headersWithUnderscoresAction: Invalid value: "bar": must be one of "ALLOW", "REJECT_REQUEST", "DROP_HEADER", ""`, + }, + }, "multiple errors": { input: &Mesh{ ObjectMeta: metav1.ObjectMeta{ @@ -386,6 +488,14 @@ func TestMesh_Validate(t *testing.T) { Peering: &PeeringMeshConfig{ PeerThroughMeshGateways: true, }, + HTTP: &MeshHTTPConfig{ + Incoming: &MeshDirectionalHTTPConfig{ + RequestNormalization: &RequestNormalizationMeshConfig{ + PathWithEscapedSlashesAction: "foo", + HeadersWithUnderscoresAction: "bar", + }, + }, + }, }, }, consulMeta: common.ConsulMeta{ @@ -398,6 +508,8 @@ func TestMesh_Validate(t *testing.T) { `spec.tls.outgoing.tlsMinVersion: Invalid value: "foo": must be one of "TLS_AUTO", "TLSv1_0", "TLSv1_1", "TLSv1_2", "TLSv1_3", ""`, `spec.tls.outgoing.tlsMaxVersion: Invalid value: "bar": must be one of "TLS_AUTO", "TLSv1_0", "TLSv1_1", "TLSv1_2", "TLSv1_3", ""`, `spec.peering.peerThroughMeshGateways: Forbidden: "peerThroughMeshGateways" is only valid in the "default" partition`, + `spec.http.incoming.requestNormalization.pathWithEscapedSlashesAction: Invalid value: "foo": must be one of "IMPLEMENTATION_SPECIFIC_DEFAULT", "KEEP_UNCHANGED", "REJECT_REQUEST", "UNESCAPE_AND_REDIRECT", "UNESCAPE_AND_FORWARD", ""`, + `spec.http.incoming.requestNormalization.headersWithUnderscoresAction: Invalid value: "bar": must be one of "ALLOW", "REJECT_REQUEST", "DROP_HEADER", ""`, }, }, } diff --git a/control-plane/api/v1alpha1/serviceintentions_types.go b/control-plane/api/v1alpha1/serviceintentions_types.go index d393f72a2d..17d0a9cf2a 100644 --- a/control-plane/api/v1alpha1/serviceintentions_types.go +++ b/control-plane/api/v1alpha1/serviceintentions_types.go @@ -140,8 +140,12 @@ type IntentionHTTPHeaderPermission struct { Prefix string `json:"prefix,omitempty"` // Suffix matches if the header with the given name has this suffix. Suffix string `json:"suffix,omitempty"` + // Contains matches if the header with the given name contains this value. + Contains string `json:"contains,omitempty"` // Regex matches if the header with the given name matches this pattern. Regex string `json:"regex,omitempty"` + // IgnoreCase ignores the case of the header value when matching with exact, prefix, suffix, or contains. + IgnoreCase bool `json:"ignoreCase,omitempty"` // Invert inverts the logic of the match. Invert bool `json:"invert,omitempty"` } @@ -448,13 +452,15 @@ func (in IntentionHTTPHeaderPermissions) toConsul() []capi.IntentionHTTPHeaderPe var headerPermissions []capi.IntentionHTTPHeaderPermission for _, permission := range in { headerPermissions = append(headerPermissions, capi.IntentionHTTPHeaderPermission{ - Name: permission.Name, - Present: permission.Present, - Exact: permission.Exact, - Prefix: permission.Prefix, - Suffix: permission.Suffix, - Regex: permission.Regex, - Invert: permission.Invert, + Name: permission.Name, + Present: permission.Present, + Exact: permission.Exact, + Prefix: permission.Prefix, + Suffix: permission.Suffix, + Contains: permission.Contains, + Regex: permission.Regex, + Invert: permission.Invert, + IgnoreCase: permission.IgnoreCase, }) } @@ -568,10 +574,14 @@ func (in IntentionHTTPHeaderPermissions) validate(path *field.Path) field.ErrorL if permission.Present { hdrParts++ } - hdrParts += numNotEmpty(permission.Exact, permission.Regex, permission.Prefix, permission.Suffix) + hdrParts += numNotEmpty(permission.Exact, permission.Regex, permission.Prefix, permission.Suffix, permission.Contains) if hdrParts > 1 { asJson, _ := json.Marshal(in[i]) - errs = append(errs, field.Invalid(path.Index(i), string(asJson), `at most only one of exact, prefix, suffix, regex, or present may be configured.`)) + errs = append(errs, field.Invalid(path.Index(i), string(asJson), `at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`)) + } + if permission.IgnoreCase && (permission.Present || permission.Regex != "") { + asJson, _ := json.Marshal(in[i]) + errs = append(errs, field.Invalid(path.Index(i), string(asJson), `should set one of exact, prefix, suffix, or contains when using ignoreCase.`)) } } return errs diff --git a/control-plane/api/v1alpha1/serviceintentions_types_test.go b/control-plane/api/v1alpha1/serviceintentions_types_test.go index 8d0a6d907a..fb2ae743b5 100644 --- a/control-plane/api/v1alpha1/serviceintentions_types_test.go +++ b/control-plane/api/v1alpha1/serviceintentions_types_test.go @@ -151,13 +151,15 @@ func TestServiceIntentions_MatchesConsul(t *testing.T) { PathRegex: "/baz", Header: IntentionHTTPHeaderPermissions{ { - Name: "header", - Present: true, - Exact: "exact", - Prefix: "prefix", - Suffix: "suffix", - Regex: "regex", - Invert: true, + Name: "header", + Present: true, + Exact: "exact", + Prefix: "prefix", + Suffix: "suffix", + Contains: "contains", + Regex: "regex", + Invert: true, + IgnoreCase: true, }, }, Methods: []string{ @@ -232,13 +234,15 @@ func TestServiceIntentions_MatchesConsul(t *testing.T) { PathRegex: "/baz", Header: []capi.IntentionHTTPHeaderPermission{ { - Name: "header", - Present: true, - Exact: "exact", - Prefix: "prefix", - Suffix: "suffix", - Regex: "regex", - Invert: true, + Name: "header", + Present: true, + Exact: "exact", + Prefix: "prefix", + Suffix: "suffix", + Contains: "contains", + Regex: "regex", + Invert: true, + IgnoreCase: true, }, }, Methods: []string{ @@ -414,13 +418,15 @@ func TestServiceIntentions_ToConsul(t *testing.T) { PathRegex: "/baz", Header: IntentionHTTPHeaderPermissions{ { - Name: "header", - Present: true, - Exact: "exact", - Prefix: "prefix", - Suffix: "suffix", - Regex: "regex", - Invert: true, + Name: "header", + Present: true, + Exact: "exact", + Prefix: "prefix", + Suffix: "suffix", + Contains: "contains", + Regex: "regex", + Invert: true, + IgnoreCase: true, }, }, Methods: []string{ @@ -500,13 +506,15 @@ func TestServiceIntentions_ToConsul(t *testing.T) { PathRegex: "/baz", Header: []capi.IntentionHTTPHeaderPermission{ { - Name: "header", - Present: true, - Exact: "exact", - Prefix: "prefix", - Suffix: "suffix", - Regex: "regex", - Invert: true, + Name: "header", + Present: true, + Exact: "exact", + Prefix: "prefix", + Suffix: "suffix", + Contains: "contains", + Regex: "regex", + Invert: true, + IgnoreCase: true, }, }, Methods: []string{ @@ -1258,11 +1266,26 @@ func TestServiceIntentions_Validate(t *testing.T) { Suffix: "bar", Regex: "foo", }, + { + Name: "suffix-contains", + Suffix: "bar", + Contains: "foo", + }, { Name: "regex-present", Present: true, Regex: "foobar", }, + { + Name: "present-ignoreCase", + Present: true, + IgnoreCase: true, + }, + { + Name: "regex-ignoreCase", + Regex: "foobar", + IgnoreCase: true, + }, }, }, }, @@ -1273,11 +1296,14 @@ func TestServiceIntentions_Validate(t *testing.T) { }, namespacesEnabled: true, expectedErrMsgs: []string{ - `spec.sources[0].permissions[0].header[0]: Invalid value: "{\"name\":\"exact-present\",\"present\":true,\"exact\":\"foobar\"}": at most only one of exact, prefix, suffix, regex, or present may be configured.`, - `spec.sources[0].permissions[0].header[1]: Invalid value: "{\"name\":\"prefix-exact\",\"exact\":\"foobar\",\"prefix\":\"barfood\"}": at most only one of exact, prefix, suffix, regex, or present may be configured.`, - `spec.sources[0].permissions[0].header[2]: Invalid value: "{\"name\":\"suffix-prefix\",\"prefix\":\"foo\",\"suffix\":\"bar\"}": at most only one of exact, prefix, suffix, regex, or present may be configured.`, - `spec.sources[0].permissions[0].header[3]: Invalid value: "{\"name\":\"suffix-regex\",\"suffix\":\"bar\",\"regex\":\"foo\"}": at most only one of exact, prefix, suffix, regex, or present may be configured.`, - `spec.sources[0].permissions[0].header[4]: Invalid value: "{\"name\":\"regex-present\",\"present\":true,\"regex\":\"foobar\"}": at most only one of exact, prefix, suffix, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[0]: Invalid value: "{\"name\":\"exact-present\",\"present\":true,\"exact\":\"foobar\"}": at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[1]: Invalid value: "{\"name\":\"prefix-exact\",\"exact\":\"foobar\",\"prefix\":\"barfood\"}": at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[2]: Invalid value: "{\"name\":\"suffix-prefix\",\"prefix\":\"foo\",\"suffix\":\"bar\"}": at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[3]: Invalid value: "{\"name\":\"suffix-regex\",\"suffix\":\"bar\",\"regex\":\"foo\"}": at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[4]: Invalid value: "{\"name\":\"suffix-contains\",\"suffix\":\"bar\",\"contains\":\"foo\"}": at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[5]: Invalid value: "{\"name\":\"regex-present\",\"present\":true,\"regex\":\"foobar\"}": at most only one of exact, prefix, suffix, contains, regex, or present may be configured.`, + `spec.sources[0].permissions[0].header[6]: Invalid value: "{\"name\":\"present-ignoreCase\",\"present\":true,\"ignoreCase\":true}": should set one of exact, prefix, suffix, or contains when using ignoreCase.`, + `spec.sources[0].permissions[0].header[7]: Invalid value: "{\"name\":\"regex-ignoreCase\",\"regex\":\"foobar\",\"ignoreCase\":true}": should set one of exact, prefix, suffix, or contains when using ignoreCase.`, }, }, "invalid permissions.action": { diff --git a/control-plane/catalog/metrics/metrics.go b/control-plane/catalog/metrics/metrics.go index 17fabf0bdc..0979cb6f38 100644 --- a/control-plane/catalog/metrics/metrics.go +++ b/control-plane/catalog/metrics/metrics.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package metrics import ( diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_meshes.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_meshes.yaml index c5c15b3c5d..51ac810778 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_meshes.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_meshes.yaml @@ -62,10 +62,55 @@ spec: http: description: HTTP defines the HTTP configuration for the service mesh. properties: + incoming: + description: Incoming configures settings for incoming HTTP traffic + to mesh proxies. + properties: + requestNormalization: + description: |- + RequestNormalizationMeshConfig contains options pertaining to the + normalization of HTTP requests processed by mesh proxies. + properties: + headersWithUnderscoresAction: + description: |- + HeadersWithUnderscoresAction sets the value of the \`headers_with_underscores_action\` option in the Envoy + listener's \`HttpConnectionManager\` under \`common_http_protocol_options\`. The default value of this option is + empty, which is equivalent to \`ALLOW\`. Refer to the Envoy documentation for more information on available + options. + type: string + insecureDisablePathNormalization: + description: |- + InsecureDisablePathNormalization sets the value of the \`normalize_path\` option in the Envoy listener's + `HttpConnectionManager`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is + set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to + RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions + with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with + non-normalized path values. + type: boolean + mergeSlashes: + description: |- + MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`. + The default value is \`false\`. This option controls the normalization of request URL paths by merging + consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path + match rules, we recommend enabling this setting to avoid match rule circumvention through non-normalized path + values, unless legitimate service traffic depends on allowing for repeat \`/\` characters, or upstream services + are configured to differentiate between single and multiple slashes. + type: boolean + pathWithEscapedSlashesAction: + description: |- + PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy + listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to + \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths + with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this + setting to avoid match rule circumvention through non-normalized path values, unless legitimate service traffic + depends on allowing for escaped \`/\` or \`\\\` characters, or upstream services are configured to differentiate + between escaped and unescaped slashes. Refer to the Envoy documentation for more information on available + options. + type: string + type: object + type: object sanitizeXForwardedClientCert: type: boolean - required: - - sanitizeXForwardedClientCert type: object peering: description: Peering defines the peering configuration for the service diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml index 957295b18e..04b5c08522 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml @@ -164,10 +164,19 @@ spec: If more than one is configured all must match for the overall match to apply. items: properties: + contains: + description: Contains matches if the header + with the given name contains this value. + type: string exact: description: Exact matches if the header with the given name is this value. type: string + ignoreCase: + description: IgnoreCase ignores the case of + the header value when matching with exact, + prefix, suffix, or contains. + type: boolean invert: description: Invert inverts the logic of the match. diff --git a/control-plane/go.mod b/control-plane/go.mod index 3e23f2a593..f1cb7dd378 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/consul-k8s/control-plane/cni v0.0.0-20240226161840-f3842c41cb2b github.com/hashicorp/consul-k8s/version v0.0.0 github.com/hashicorp/consul-server-connection-manager v0.1.6 - github.com/hashicorp/consul/api v1.29.4 + github.com/hashicorp/consul/api v1.30.0 github.com/hashicorp/consul/sdk v0.16.1 github.com/hashicorp/go-bexpr v0.1.11 github.com/hashicorp/go-discover v0.0.0-20230519164032-214571b6a530 @@ -37,6 +37,7 @@ require ( github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.25.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 + golang.org/x/sync v0.8.0 golang.org/x/text v0.17.0 golang.org/x/time v0.3.0 gomodules.xyz/jsonpatch/v2 v2.4.0 @@ -146,7 +147,6 @@ require ( golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect - golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/tools v0.24.0 // indirect diff --git a/control-plane/go.sum b/control-plane/go.sum index 79bf109c1d..a733ec21ba 100644 --- a/control-plane/go.sum +++ b/control-plane/go.sum @@ -214,8 +214,8 @@ github.com/hashicorp/consul-k8s/control-plane/cni v0.0.0-20240226161840-f3842c41 github.com/hashicorp/consul-k8s/control-plane/cni v0.0.0-20240226161840-f3842c41cb2b/go.mod h1:9NKJHOcgmz/6P2y6MegNIOXhIKE/0ils/mHWd5sZgoU= github.com/hashicorp/consul-server-connection-manager v0.1.6 h1:ktj8Fi+dRXn9hhM+FXsfEJayhzzgTqfH08Ne5M6Fmug= github.com/hashicorp/consul-server-connection-manager v0.1.6/go.mod h1:HngMIv57MT+pqCVeRQMa1eTB5dqnyMm8uxjyv+Hn8cs= -github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE= -github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg= +github.com/hashicorp/consul/api v1.30.0 h1:ArHVMMILb1nQv8vZSGIwwQd2gtc+oSQZ6CalyiyH2XQ= +github.com/hashicorp/consul/api v1.30.0/go.mod h1:B2uGchvaXVW2JhFoS8nqTxMD5PBykr4ebY4JWHTTeLM= github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0= github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= diff --git a/control-plane/helper/test/test_util.go b/control-plane/helper/test/test_util.go index 542700c34f..9cd7944b11 100644 --- a/control-plane/helper/test/test_util.go +++ b/control-plane/helper/test/test_util.go @@ -282,7 +282,7 @@ func SetupK8sAuthMethodWithNamespaces(t *testing.T, consulClient *api.Client, se Description: "Kubernetes binding rule", AuthMethod: AuthMethod, BindType: api.BindingRuleBindTypeTemplatedPolicy, - BindName: api.ACLTemplatedPolicyWorkloadIdentityName, + BindName: "", //api.ACLTemplatedPolicyWorkloadIdentityName, TODO: remove w/ v2 code BindVars: &api.ACLTemplatedPolicyVariables{ Name: "${serviceaccount.name}", }, diff --git a/control-plane/subcommand/common/metrics_util.go b/control-plane/subcommand/common/metrics_util.go index d3866fdeef..343b4c9e51 100644 --- a/control-plane/subcommand/common/metrics_util.go +++ b/control-plane/subcommand/common/metrics_util.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package common import "strconv"