Repository: kumahq/kuma
Pull request #10316 (2024-05-25 11:39:29)

fix(GatewayAPI): only enqueue Gateway reconciliations from routes if parent is a Gateway
For example, in GAMMA, the parentRef can also be a Service.
Signed-off-by: spacewander <spacewanderlzx@gmail.com>

### Checklist prior to review

<!--
Each of these sections need to be filled by the author when opening the PR.

If something doesn't apply please check the box and add a justification after the `--`
-->

- [ ] [Link to relevant issue][1] as well as docs and UI issues --
- [x] This will not break child repos: it doesn't hardcode values (.e.g "kumahq" as a image registry) and it will work on Windows, system specific functions like `syscall.Mkfifo` have equivalent implementation on the other OS --
- [ ] Tests (Unit test, E2E tests, manual test on universal and k8s) --
  - Don't forget `ci/` labels to run additional/fewer tests
- [ ] Do you need to update [`UPGRADE.md`](../blob/master/UPGRADE.md)? --
- [ ] Does it need to be backported according to the [backporting policy](../blob/master/CONTRIBUTING.md#backporting)? ([this](https://github.com/kumahq/kuma/actions/workflows/auto-backport.yaml) GH action will add "backport" label based on these [file globs](https://github.com/kumahq/kuma/blob/master/.github/workflows/auto-backport.yaml#L6), if you want to prevent it from adding the "backport" label use [no-backport-autolabel](https://github.com/kumahq/kuma/blob/master/.github/workflows/auto-backport.yaml#L8) label) --

<!--
> Changelog: skip
-->
<!--
Uncomment the above section to explicitly set a [`> Changelog:` entry here](https://github.com/kumahq/kuma/blob/master/CONTRIBUTING.md#submitting-a-patch)?
-->

[1]: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword



Patch:
diff --git a/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml b/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml
index 30236cb10..19ef3be3e 100644
--- a/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml
+++ b/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml
@@ -60,7 +60,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -171,13 +170,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -225,14 +224,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml b/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml
index 30236cb10..19ef3be3e 100644
--- a/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml
+++ b/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml
@@ -60,7 +60,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -171,13 +170,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -225,14 +224,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/go.mod b/go.mod
index 4ccb2f73b..7a1d39367 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/gruntwork-io/terratest v0.46.15
 	github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69
-	github.com/jackc/pgx/v5 v5.5.5
+	github.com/jackc/pgx/v5 v5.6.0
 	github.com/josephburnett/jd/v2 v2.0.0-20230813234251-7b2e87c80934
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/kennygrant/sanitize v1.2.4
diff --git a/go.sum b/go.sum
index 1b9b6536f..0dde02535 100644
--- a/go.sum
+++ b/go.sum
@@ -215,8 +215,8 @@ github.com/jackc/pgtype v1.14.2 h1:QBdZQTKpPdBlw2AdKwHEyqUcm/lrl2cwWAHjCMyln/o=
 github.com/jackc/pgtype v1.14.2/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
 github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
 github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
-github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
-github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
+github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
 github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
 github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
diff --git a/mk/check.mk b/mk/check.mk
index 5fc4e72ec..3f4fb7bdf 100644
--- a/mk/check.mk
+++ b/mk/check.mk
@@ -14,7 +14,7 @@ tidy:
 
 .PHONY: shellcheck
 shellcheck:
-	find . -name "*.sh" -not -path "./.git/*" -exec $(SHELLCHECK) -P SCRIPTDIR -x {} +
+	find . -name "*.sh" -not -path "./.git/*" $(SHELLCHECK_FIND_EXTRA_ARGS) -exec $(SHELLCHECK) -P SCRIPTDIR -x {} +
 
 .PHONY: golangci-lint
 golangci-lint: ## Dev: Runs golangci-lint linter
diff --git a/mk/dependencies/deps.lock b/mk/dependencies/deps.lock
index 9d3d9a9e8..5b6bb72da 100644
--- a/mk/dependencies/deps.lock
+++ b/mk/dependencies/deps.lock
@@ -1,1 +1,1 @@
-aa93aef4e96dccf4a394c17ef6d5b6d2df9d5ab4
+0ba3997a3b0428c45c0a974664f25156ffc11c7e
diff --git a/mk/dev.mk b/mk/dev.mk
index 48ee050ba..a04f23051 100644
--- a/mk/dev.mk
+++ b/mk/dev.mk
@@ -24,7 +24,7 @@ CI_TOOLS_BIN_DIR=$(CI_TOOLS_DIR)/bin
 K8S_MIN_VERSION = v1.23.17-k3s1
 K8S_MAX_VERSION = v1.30.0-k3s1
 export GO_VERSION=$(shell go mod edit -json | jq -r .Go)
-export GOLANGCI_LINT_VERSION=v1.56.1
+export GOLANGCI_LINT_VERSION=v1.59.0
 GOOS := $(shell go env GOOS)
 GOARCH := $(shell go env GOARCH)
 LATEST_RELEASE_BRANCH := $(shell $(CI_TOOLS_BIN_DIR)/yq e '.[] | select(.latest == true) | .branch' versions.yml)
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go
index 8c9ddb213..801e1f46e 100644
--- a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go
@@ -14,19 +14,30 @@ type MeshExternalService struct {
 	// Match defines traffic that should be routed through the sidecar.
 	Match Match `json:"match"`
 	// Extension struct for a plugin configuration, in the presence of an extension `endpoints` and `tls` are not required anymore - it's up to the extension to validate them independently.
-	Extension Extension `json:"extension,omitempty"`
+	Extension *Extension `json:"extension,omitempty"`
 	// Endpoints defines a list of destinations to send traffic to.
 	Endpoints []Endpoint `json:"endpoints,omitempty"`
 	// Tls provides a TLS configuration when proxy is resposible for a TLS origination
-	Tls Tls `json:"tls,omitempty"`
+	Tls *Tls `json:"tls,omitempty"`
 }
 
 // +kubebuilder:validation:Enum=HostnameGenerator
 type MatchType string
 
+const (
+	HostnameGeneratorType MatchType = "HostnameGenerator"
+)
+
 // +kubebuilder:validation:Enum=tcp;grpc;http;http2
 type ProtocolType string
 
+const (
+	TcpProtocol   ProtocolType = "tcp"
+	GrpcProtocol  ProtocolType = "grpc"
+	HttpProtocol  ProtocolType = "http"
+	Http2Protocol ProtocolType = "http2"
+)
+
 type Match struct {
 	// Type of the match, only `HostnameGenerator` is available at the moment.
 	// +kubebuilder:default=HostnameGenerator
@@ -47,7 +58,7 @@ type Extension struct {
 
 // +kubebuilder:validation:Minimum=1
 // +kubebuilder:validation:Maximum=65535
-type EndpointPort int
+type Port int
 
 type Endpoint struct {
 	// Address defines an address to which a user want to send a request. Is possible to provide `domain`, `ip` and `unix` sockets.
@@ -57,7 +68,7 @@ type Endpoint struct {
 	// +kubebuilder:validation:MinLength=1
 	Address string `json:"address"`
 	// Port of the endpoint
-	Port EndpointPort `json:"port"`
+	Port *Port `json:"port,omitempty"`
 }
 
 type Tls struct {
@@ -65,36 +76,58 @@ type Tls struct {
 	// +kubebuilder:default=false
 	Enabled bool `json:"enabled,omitempty"`
 	// Version section for providing version specification.
-	Version TlsVersion `json:"version"`
+	Version *Version `json:"version,omitempty"`
 	// AllowRenegotiation defines if TLS sessions will allow renegotiation.
 	// Setting this to true is not recommended for security reasons.
 	// +kubebuilder:default=false
-	AllowRenegotiation bool `json:"allowRenegotiation"`
+	AllowRenegotiation bool `json:"allowRenegotiation,omitempty"`
 	// Verification section for providing TLS verification details.
-	Verification Verification `json:"verification"`
+	Verification *Verification `json:"verification,omitempty"`
 }
 
 // +kubebuilder:validation:Enum=TLSAuto;TLS10;TLS11;TLS12;TLS13
-type TlsMinMaxVersion string
+type TlsVersion string
+
+const (
+	TLSVersionAuto TlsVersion = "TLSAuto"
+	TLSVersion10   TlsVersion = "TLS10"
+	TLSVersion11   TlsVersion = "TLS11"
+	TLSVersion12   TlsVersion = "TLS12"
+	TLSVersion13   TlsVersion = "TLS13"
+)
 
-type TlsVersion struct {
+var tlsVersionOrder = map[TlsVersion]int{
+	TLSVersion10: 0,
+	TLSVersion11: 1,
+	TLSVersion12: 2,
+	TLSVersion13: 3,
+}
+
+type Version struct {
 	// Min defines minimum supported version. One of `TLSAuto`, `TLS10`, `TLS11`, `TLS12`, `TLS13`.
 	// +kubebuilder:default=TLSAuto
-	Min TlsMinMaxVersion `json:"min"`
+	Min *TlsVersion `json:"min,omitempty"`
 	// Max defines maximum supported version. One of `TLSAuto`, `TLS10`, `TLS11`, `TLS12`, `TLS13`.
 	// +kubebuilder:default=TLSAuto
-	Max TlsMinMaxVersion `json:"max"`
+	Max *TlsVersion `json:"max,omitempty"`
 }
 
-// +kubebuilder:validation:Enum=SkipSAN;SkipCA;Secured;SkipALL
+// +kubebuilder:validation:Enum=SkipSAN;SkipCA;Secured;SkipAll
 type VerificationMode string
 
+const (
+	TLSVerificationSkipSAN VerificationMode = "SkipSAN"
+	TLSVerificationSkipCA  VerificationMode = "SkipCA"
+	TLSVerificationSkipAll VerificationMode = "SkipAll"
+	TLSVerificationSecured VerificationMode = "Secured"
+)
+
 type Verification struct {
-	// Mode defines if proxy should skip verification, one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default `Secured`.
+	// Mode defines if proxy should skip verification, one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default `Secured`.
 	// +kubebuilder:default=Secured
-	Mode VerificationMode `json:"mode,omitempty"`
+	Mode *VerificationMode `json:"mode,omitempty"`
 	// SubjectAltNames list of names to verify in the certificate.
-	SubjectAltNames []SANMatch `json:"subjectAltNames,omitempty"`
+	SubjectAltNames *[]SANMatch `json:"subjectAltNames,omitempty"`
 	// CaCert defines a certificate of CA.
 	CaCert *v1alpha1.DataSource `json:"caCert,omitempty"`
 	// ClientCert defines a certificate of a client.
@@ -106,6 +139,11 @@ type Verification struct {
 // +kubebuilder:validation:Enum=Exact;Prefix
 type SANMatchType string
 
+const (
+	SANMatchExact  SANMatchType = "Exact"
+	SANMatchPrefix SANMatchType = "Prefix"
+)
+
 type SANMatch struct {
 	// Type specifies matching type, one of `Exact`, `Prefix`. Default: `Exact`
 	// +kubebuilder:default=Exact
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/schema.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/schema.yaml
index 4b111714f..bd74a569d 100644
--- a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/schema.yaml
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/schema.yaml
@@ -31,7 +31,6 @@ properties:
               type: integer
           required:
             - address
-            - port
           type: object
         type: array
       extension:
@@ -131,12 +130,12 @@ properties:
                 type: object
               mode:
                 default: Secured
-                description: Mode defines if proxy should skip verification, one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default `Secured`.
+                description: Mode defines if proxy should skip verification, one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default `Secured`.
                 enum:
                   - SkipSAN
                   - SkipCA
                   - Secured
-                  - SkipALL
+                  - SkipAll
                 type: string
               subjectAltNames:
                 description: SubjectAltNames list of names to verify in the certificate.
@@ -180,14 +179,7 @@ properties:
                   - TLS12
                   - TLS13
                 type: string
-            required:
-              - max
-              - min
             type: object
-        required:
-          - allowRenegotiation
-          - verification
-          - version
         type: object
     required:
       - match
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator.go
index a4c06139c..2759edd57 100644
--- a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator.go
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator.go
@@ -1,10 +1,159 @@
 package v1alpha1
 
 import (
+	"fmt"
+	"math"
+	"slices"
+	"strings"
+
+	"github.com/asaskevich/govalidator"
+
 	"github.com/kumahq/kuma/pkg/core/validators"
 )
 
+var (
+	allMatchProtocols    = []string{string(TcpProtocol), string(GrpcProtocol), string(HttpProtocol), string(Http2Protocol)}
+	allTlsVersions       = []string{string(TLSVersionAuto), string(TLSVersion10), string(TLSVersion11), string(TLSVersion12), string(TLSVersion13)}
+	allVerificationModes = []string{string(TLSVerificationSkipSAN), string(TLSVerificationSkipCA), string(TLSVerificationSkipAll), string(TLSVerificationSecured)}
+	allSANMatchTypes     = []string{string(SANMatchPrefix), string(SANMatchExact)}
+)
+
 func (r *MeshExternalServiceResource) validate() error {
 	var verr validators.ValidationError
+	path := validators.RootedAt("spec")
+
+	verr.AddErrorAt(path.Field("match"), validateMatch(r.Spec.Match))
+	// when extension != nil then it's up to the extension to validate endpoints and tls
+	if r.Spec.Extension == nil {
+		if r.Spec.Endpoints != nil {
+			verr.AddErrorAt(path.Field("endpoints"), validateEndpoints(r.Spec.Endpoints))
+		}
+
+		if r.Spec.Tls != nil {
+			verr.AddErrorAt(path.Field("tls"), validateTls(r.Spec.Tls))
+		}
+	}
+
+	if r.Spec.Extension != nil && r.Spec.Extension.Type == "" {
+		verr.AddViolationAt(path.Field("extension").Field("type"), validators.MustNotBeEmpty)
+	}
+
 	return verr.OrNil()
 }
+
+func validateTls(tls *Tls) validators.ValidationError {
+	var verr validators.ValidationError
+
+	if tls.Version != nil {
+		verr.AddError(validators.RootedAt("version").String(), validateVersion(tls.Version))
+	}
+
+	if tls.Verification != nil {
+		path := validators.RootedAt("verification")
+		if tls.Verification.Mode != nil {
+			if !slices.Contains(allVerificationModes, string(*tls.Verification.Mode)) {
+				verr.AddErrorAt(path.Field("mode"), validators.MakeFieldMustBeOneOfErr("mode", allVerificationModes...))
+			}
+		}
+		if tls.Verification.SubjectAltNames != nil {
+			for i, san := range *tls.Verification.SubjectAltNames {
+				if !slices.Contains(allSANMatchTypes, string(san.Type)) {
+					verr.AddErrorAt(path.Field("subjectAltNames").Index(i).Field("type"), validators.MakeFieldMustBeOneOfErr("type", allSANMatchTypes...))
+				}
+			}
+		}
+
+		if tls.Verification.ClientCert != nil && tls.Verification.ClientKey == nil {
+			verr.AddViolation(path.Field("clientKey").String(), validators.MustBeDefined+" when clientCert is defined")
+		}
+		if tls.Verification.ClientCert == nil && tls.Verification.ClientKey != nil {
+			verr.AddViolation(path.Field("clientCert").String(), validators.MustBeDefined+" when clientKey is defined")
+		}
+	}
+
+	return verr
+}
+
+func validateVersion(version *Version) validators.ValidationError {
+	var verr validators.ValidationError
+	path := validators.Root()
+	specificMin := false
+	specificMax := false
+	if version.Min != nil {
+		if !slices.Contains(allTlsVersions, string(*version.Min)) {
+			verr.AddErrorAt(path.Field("min"), validators.MakeFieldMustBeOneOfErr("min", allTlsVersions...))
+		} else if *version.Min != TLSVersionAuto {
+			specificMin = true
+		}
+	}
+	if version.Max != nil {
+		if !slices.Contains(allTlsVersions, string(*version.Max)) {
+			verr.AddErrorAt(path.Field("max"), validators.MakeFieldMustBeOneOfErr("max", allTlsVersions...))
+		} else if *version.Max != TLSVersionAuto {
+			specificMax = true
+		}
+	}
+
+	if specificMin && specificMax && tlsVersionOrder[*version.Min] > tlsVersionOrder[*version.Max] {
+		verr.AddViolationAt(path.Field("min"), "min version must be lower than max")
+	}
+
+	return verr
+}
+
+func validateMatch(match Match) validators.ValidationError {
+	var verr validators.ValidationError
+	if match.Type != HostnameGeneratorType {
+		verr.AddViolation(validators.RootedAt("type").String(), fmt.Sprintf("unrecognized type '%s' - only '%s' is supported", match.Type, HostnameGeneratorType))
+	}
+	if match.Port == 0 || match.Port > math.MaxUint16 {
+		verr.AddViolationAt(validators.RootedAt("port"), "port must be a valid (1-65535)")
+	}
+	if !slices.Contains(allMatchProtocols, string(match.Protocol)) {
+		verr.AddErrorAt(validators.RootedAt("protocol"), validators.MakeFieldMustBeOneOfErr("protocol", allMatchProtocols...))
+	}
+
+	return verr
+}
+
+func validateEndpoints(endpoints []Endpoint) validators.ValidationError {
+	var verr validators.ValidationError
+
+	for i, endpoint := range endpoints {
+		if govalidator.IsIP(endpoint.Address) {
+			if endpoint.Port == nil {
+				verr.AddViolationAt(validators.Root().Index(i).Field("port"), validators.MustBeDefined+" when endpoint is an IP")
+			} else if *endpoint.Port == 0 || *endpoint.Port > math.MaxUint16 {
+				verr.AddViolationAt(validators.Root().Index(i).Field("port"), "port must be a valid (1-65535)")
+			}
+		}
+
+		if isValidUnixPath(endpoint.Address) {
+			if endpoint.Port != nil {
+				verr.AddViolationAt(validators.Root().Index(i).Field("port"), validators.MustNotBeDefined+" when endpoint is a unix path")
+			}
+		}
+
+		if govalidator.IsDNSName(endpoint.Address) {
+			if endpoint.Port == nil {
+				verr.AddViolationAt(validators.Root().Index(i).Field("port"), validators.MustBeDefined+" when endpoint is a hostname")
+			}
+		}
+
+		if !(govalidator.IsIP(endpoint.Address) || govalidator.IsDNSName(endpoint.Address) || isValidUnixPath(endpoint.Address)) {
+			verr.AddViolationAt(validators.Root().Index(i).Field("address"), "address has to be a valid IP or hostname or a unix path")
+		}
+	}
+
+	return verr
+}
+
+func isValidUnixPath(path string) bool {
+	if strings.HasPrefix(path, "unix://") {
+		parts := strings.Split(path, "unix://")
+		filePath := parts[1]
+		return govalidator.IsUnixFilePath(filePath)
+	} else {
+		return false
+	}
+}
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go
index ffe238d1d..fbb5750ee 100644
--- a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go
@@ -43,6 +43,11 @@ func (in *AddressOrigin) DeepCopy() *AddressOrigin {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Endpoint) DeepCopyInto(out *Endpoint) {
 	*out = *in
+	if in.Port != nil {
+		in, out := &in.Port, &out.Port
+		*out = new(Port)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint.
@@ -94,13 +99,23 @@ func (in *Match) DeepCopy() *Match {
 func (in *MeshExternalService) DeepCopyInto(out *MeshExternalService) {
 	*out = *in
 	out.Match = in.Match
-	in.Extension.DeepCopyInto(&out.Extension)
+	if in.Extension != nil {
+		in, out := &in.Extension, &out.Extension
+		*out = new(Extension)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Endpoints != nil {
 		in, out := &in.Endpoints, &out.Endpoints
 		*out = make([]Endpoint, len(*in))
-		copy(*out, *in)
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Tls != nil {
+		in, out := &in.Tls, &out.Tls
+		*out = new(Tls)
+		(*in).DeepCopyInto(*out)
 	}
-	in.Tls.DeepCopyInto(&out.Tls)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshExternalService.
@@ -152,8 +167,16 @@ func (in *SANMatch) DeepCopy() *SANMatch {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Tls) DeepCopyInto(out *Tls) {
 	*out = *in
-	out.Version = in.Version
-	in.Verification.DeepCopyInto(&out.Verification)
+	if in.Version != nil {
+		in, out := &in.Version, &out.Version
+		*out = new(Version)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Verification != nil {
+		in, out := &in.Verification, &out.Verification
+		*out = new(Verification)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tls.
@@ -166,28 +189,22 @@ func (in *Tls) DeepCopy() *Tls {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *TlsVersion) DeepCopyInto(out *TlsVersion) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsVersion.
-func (in *TlsVersion) DeepCopy() *TlsVersion {
-	if in == nil {
-		return nil
-	}
-	out := new(TlsVersion)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Verification) DeepCopyInto(out *Verification) {
 	*out = *in
+	if in.Mode != nil {
+		in, out := &in.Mode, &out.Mode
+		*out = new(VerificationMode)
+		**out = **in
+	}
 	if in.SubjectAltNames != nil {
 		in, out := &in.SubjectAltNames, &out.SubjectAltNames
-		*out = make([]SANMatch, len(*in))
-		copy(*out, *in)
+		*out = new([]SANMatch)
+		if **in != nil {
+			in, out := *in, *out
+			*out = make([]SANMatch, len(*in))
+			copy(*out, *in)
+		}
 	}
 	if in.CaCert != nil {
 		in, out := &in.CaCert, &out.CaCert
@@ -216,6 +233,31 @@ func (in *Verification) DeepCopy() *Verification {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Version) DeepCopyInto(out *Version) {
+	*out = *in
+	if in.Min != nil {
+		in, out := &in.Min, &out.Min
+		*out = new(TlsVersion)
+		**out = **in
+	}
+	if in.Max != nil {
+		in, out := &in.Max, &out.Max
+		*out = new(TlsVersion)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Version.
+func (in *Version) DeepCopy() *Version {
+	if in == nil {
+		return nil
+	}
+	out := new(Version)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VipStatus) DeepCopyInto(out *VipStatus) {
 	*out = *in
diff --git a/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml b/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml
index 30236cb10..19ef3be3e 100644
--- a/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml
+++ b/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml
@@ -60,7 +60,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -171,13 +170,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -225,14 +224,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/pkg/kds/mux/client.go b/pkg/kds/mux/client.go
index a6289602a..051915655 100644
--- a/pkg/kds/mux/client.go
+++ b/pkg/kds/mux/client.go
@@ -319,9 +319,8 @@ func (c *client) NeedLeaderElection() bool {
 }
 
 func tlsConfig(rootCaFile string, skipVerify bool) (*tls.Config, error) {
-	// #nosec G402 -- we let the user decide if they want to ignore verification
 	tlsConfig := &tls.Config{
-		InsecureSkipVerify: skipVerify,
+		InsecureSkipVerify: skipVerify, // #nosec G402 -- we let the user decide if they want to ignore verification
 		MinVersion:         tls.VersionTLS12,
 	}
 	if rootCaFile != "" {
diff --git a/pkg/plugins/resources/postgres/pgx_store.go b/pkg/plugins/resources/postgres/pgx_store.go
index 97ec525ee..256bae48e 100644
--- a/pkg/plugins/resources/postgres/pgx_store.go
+++ b/pkg/plugins/resources/postgres/pgx_store.go
@@ -294,8 +294,8 @@ func (r *pgxResourceStore) pickRoPool() *pgxpool.Pool {
 	if r.roPool == nil {
 		return r.pool
 	}
-	// #nosec G404 - math rand is enough
-	if rand.Int31n(101) <= int32(r.roRatio) {
+	randomPool := rand.Int31n(101) // #nosec G404 - math rand is enough
+	if randomPool <= int32(r.roRatio) {
 		return r.roPool
 	}
 	return r.pool
diff --git a/pkg/plugins/runtime/k8s/controllers/gatewayapi/gateway_controller.go b/pkg/plugins/runtime/k8s/controllers/gatewayapi/gateway_controller.go
index c91ad6cc1..b81b22f5e 100644
--- a/pkg/plugins/runtime/k8s/controllers/gatewayapi/gateway_controller.go
+++ b/pkg/plugins/runtime/k8s/controllers/gatewayapi/gateway_controller.go
@@ -181,6 +181,11 @@ func gatewaysForRoute(l logr.Logger) kube_handler.MapFunc {
 
 		var requests []kube_reconcile.Request
 		for _, parentRef := range route.Spec.ParentRefs {
+			// parentRef.Group & Kind won't be nil as they have a default value
+			if *parentRef.Group != gatewayapi.GroupName || *parentRef.Kind != "Gateway" {
+				continue
+			}
+
 			namespace := route.Namespace
 			if parentRef.Namespace != nil {
 				namespace = string(*parentRef.Namespace)
@@ -350,6 +355,11 @@ func (r *GatewayReconciler) SetupWithManager(mgr kube_ctrl.Manager) error {
 		var names []string
 
 		for _, parentRef := range route.Spec.ParentRefs {
+			// parentRef.Group & Kind won't be nil as they have a default value
+			if *parentRef.Group != gatewayapi.GroupName || *parentRef.Kind != "Gateway" {
+				continue
+			}
+
 			namespace := route.Namespace
 			if parentRef.Namespace != nil {
 				namespace = string(*parentRef.Namespace)
diff --git a/pkg/transparentproxy/ebpf/program.go b/pkg/transparentproxy/ebpf/program.go
index 19b91ca11..8314e66a0 100644
--- a/pkg/transparentproxy/ebpf/program.go
+++ b/pkg/transparentproxy/ebpf/program.go
@@ -41,8 +41,11 @@ func (p Program) LoadAndAttach(cfg config.InitializedConfig, programs embed.FS,
 	}
 
 	programPath := path.Join(cfg.Ebpf.ProgramsSourcePath, p.Name)
-	// #nosec
-	if err := os.WriteFile(programPath, programBytes, 0o744); err != nil {
+	if err := os.WriteFile(
+		programPath,
+		programBytes,
+		0o744, // #nosec G306
+	); err != nil {
 		return fmt.Errorf("writing program bytes to file failed with error: %s", err)
 	}
 
diff --git a/pkg/util/xds/stats_callbacks.go b/pkg/util/xds/stats_callbacks.go
index 08dfd8f7d..cef46fbf1 100644
--- a/pkg/util/xds/stats_callbacks.go
+++ b/pkg/util/xds/stats_callbacks.go
@@ -126,7 +126,7 @@ func NewStatsCallbacks(metrics prometheus.Registerer, dsType string, versionExtr
 	stats.versionsMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 		Name: dsType + "_client_versions",
 		Help: "Number of clients for each version. It only counts connections where they sent at least one request",
-	}, []string{"version"})
+	}, []string{"client_version"})
 	if err := metrics.Register(stats.versionsMetric); err != nil {
 		return nil, err
 	}
@@ -151,20 +151,11 @@ func (s *statsCallbacks) OnStreamClosed(streamID int64) {
 	}
 }
 
-func (s *statsCallbacks) OnStreamRequest(streamID int64, request DiscoveryRequest) error {
+func (s *statsCallbacks) OnStreamRequest(_ int64, request DiscoveryRequest) error {
 	if request.VersionInfo() == "" {
 		return nil // It's initial DiscoveryRequest to ask for resources. It's neither ACK nor NACK.
 	}
 
-	if ver := s.versionExtractor(request.Metadata()); ver != "" {
-		s.Lock()
-		if _, ok := s.versionsForStream[streamID]; !ok {
-			s.versionsForStream[streamID] = ver
-			s.versionsMetric.WithLabelValues(ver).Inc()
-		}
-		s.Unlock()
-	}
-
 	if request.HasErrors() {
 		s.requestsReceivedMetric.WithLabelValues(request.GetTypeUrl(), "NACK", classifyError(request.ErrorMsg())).Inc()
 	} else {
@@ -185,7 +176,16 @@ func (s *statsCallbacks) takeConfigTimeFromQueue(configVersion string) (time.Tim
 	return generatedTime, ok
 }
 
-func (s *statsCallbacks) OnStreamResponse(_ int64, _ DiscoveryRequest, response DiscoveryResponse) {
+func (s *statsCallbacks) OnStreamResponse(streamID int64, request DiscoveryRequest, response DiscoveryResponse) {
+	if ver := s.versionExtractor(request.Metadata()); ver != "" {
+		s.Lock()
+		if _, ok := s.versionsForStream[streamID]; !ok {
+			s.versionsForStream[streamID] = ver
+			s.versionsMetric.WithLabelValues(ver).Inc()
+		}
+		s.Unlock()
+	}
+
 	s.responsesSentMetric.WithLabelValues(response.GetTypeUrl()).Inc()
 }
 
@@ -206,20 +206,11 @@ func (s *statsCallbacks) OnDeltaStreamClosed(streamID int64) {
 	}
 }
 
-func (s *statsCallbacks) OnStreamDeltaRequest(streamID int64, request DeltaDiscoveryRequest) error {
+func (s *statsCallbacks) OnStreamDeltaRequest(_ int64, request DeltaDiscoveryRequest) error {
 	if request.GetResponseNonce() == "" {
 		return nil // It's initial DiscoveryRequest to ask for resources. It's neither ACK nor NACK.
 	}
 
-	if ver := s.versionExtractor(request.Metadata()); ver != "" {
-		s.Lock()
-		if _, ok := s.versionsForStream[streamID]; !ok {
-			s.versionsForStream[streamID] = ver
-			s.versionsMetric.WithLabelValues(ver).Inc()
-		}
-		s.Unlock()
-	}
-
 	if request.HasErrors() {
 		s.requestsReceivedMetric.WithLabelValues(request.GetTypeUrl(), "NACK", classifyError(request.ErrorMsg())).Inc()
 	} else {
@@ -233,7 +224,16 @@ func (s *statsCallbacks) OnStreamDeltaRequest(streamID int64, request DeltaDisco
 	return nil
 }
 
-func (s *statsCallbacks) OnStreamDeltaResponse(_ int64, _ DeltaDiscoveryRequest, response DeltaDiscoveryResponse) {
+func (s *statsCallbacks) OnStreamDeltaResponse(streamID int64, request DeltaDiscoveryRequest, response DeltaDiscoveryResponse) {
+	if ver := s.versionExtractor(request.Metadata()); ver != "" {
+		s.Lock()
+		if _, ok := s.versionsForStream[streamID]; !ok {
+			s.versionsForStream[streamID] = ver
+			s.versionsMetric.WithLabelValues(ver).Inc()
+		}
+		s.Unlock()
+	}
+
 	s.responsesSentMetric.WithLabelValues(response.GetTypeUrl()).Inc()
 }
 
diff --git a/tools/xds-client/stream/client.go b/tools/xds-client/stream/client.go
index a778f7499..665790dbe 100644
--- a/tools/xds-client/stream/client.go
+++ b/tools/xds-client/stream/client.go
@@ -43,9 +43,8 @@ func New(serverURL string) (*Client, error) {
 	case "grpc":
 		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
 	case "grpcs":
-		// #nosec G402 -- it's acceptable as this is only to be used in testing
 		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
-			InsecureSkipVerify: true,
+			InsecureSkipVerify: true, // #nosec G402 -- it's acceptable as this is only to be used in testing
 		})))
 	default:
 		return nil, errors.Errorf("unsupported scheme %q. Use one of %s", url.Scheme, []string{"grpc", "grpcs"})


Test patch:
diff --git a/.github/workflows/build-test-distribute.yaml b/.github/workflows/build-test-distribute.yaml
index d5df364d4..39a45cca2 100644
--- a/.github/workflows/build-test-distribute.yaml
+++ b/.github/workflows/build-test-distribute.yaml
@@ -51,7 +51,7 @@ jobs:
       - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
         with:
           args: --fix=false --verbose
-          version: v1.56.1
+          version: v1.59.0
       - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
         with:
           path: |
@@ -168,7 +168,7 @@ jobs:
       - name: Push security assets to cloudsmith
         id: push_security_assets
         if: ${{ needs.provenance.result == 'success' || needs.build_publish.result == 'success' }}
-        uses: cloudsmith-io/action@f04b4de7550751e32961ac16543116f8f5f9bfc2 # v0.6.6
+        uses: cloudsmith-io/action@468883caf5ea64dd980d5266ff72dcf281c83df6 # v0.6.7
         with:
           api-key: ${{ secrets.CLOUDSMITH_API_KEY }}
           command: "push"
diff --git a/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml b/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml
index 9b706acc9..cb0103f49 100644
--- a/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml
+++ b/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml
@@ -257,7 +257,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -368,13 +367,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -422,14 +421,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml b/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml
index 007a059f9..d27a6bab7 100644
--- a/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml
+++ b/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml
@@ -257,7 +257,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -368,13 +367,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -422,14 +421,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml b/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml
index f448d463f..ee6837026 100644
--- a/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml
+++ b/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml
@@ -277,7 +277,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -388,13 +387,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -442,14 +441,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml b/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml
index 90dd18452..574486f1a 100644
--- a/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml
+++ b/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml
@@ -1731,7 +1731,6 @@ spec:
                       type: integer
                   required:
                   - address
-                  - port
                   type: object
                 type: array
               extension:
@@ -1842,13 +1841,13 @@ spec:
                       mode:
                         default: Secured
                         description: Mode defines if proxy should skip verification,
-                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipALL`. Default
+                          one of `SkipSAN`, `SkipCA`, `Secured`, `SkipAll`. Default
                           `Secured`.
                         enum:
                         - SkipSAN
                         - SkipCA
                         - Secured
-                        - SkipALL
+                        - SkipAll
                         type: string
                       subjectAltNames:
                         description: SubjectAltNames list of names to verify in the
@@ -1896,14 +1895,7 @@ spec:
                         - TLS12
                         - TLS13
                         type: string
-                    required:
-                    - max
-                    - min
                     type: object
-                required:
-                - allowRenegotiation
-                - verification
-                - version
                 type: object
             required:
             - match
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/suite_test.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/suite_test.go
new file mode 100644
index 000000000..dd8f24208
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/suite_test.go
@@ -0,0 +1,11 @@
+package v1alpha1_test
+
+import (
+	"testing"
+
+	"github.com/kumahq/kuma/pkg/test"
+)
+
+func TestCommonAPI(t *testing.T) {
+	test.RunSpecs(t, "MeshExternalService Suite")
+}
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-invalid.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-invalid.input.yaml
new file mode 100644
index 000000000..74a3ca874
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-invalid.input.yaml
@@ -0,0 +1,23 @@
+match:
+  type: UnknownMatchType
+  port: 0
+  protocol: unknown
+endpoints:
+  - address: 1.1.1.1
+  - address: 1.1.1.2
+    port: 999999
+  - address: example.com
+  - address: unix:///tmp/example.sock
+    port: 80
+  - port: 90
+tls:
+  version:
+    min: TLS15
+    max: TLS16
+  verification:
+    mode: Unknown
+    subjectAltNames:
+      - type: Regex
+        value: .*.example.com
+    clientCert:
+      secret: "123"
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-invalid.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-invalid.output.yaml
new file mode 100644
index 000000000..0f7fddc86
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-invalid.output.yaml
@@ -0,0 +1,27 @@
+violations:
+- field: spec.match.type
+  message: unrecognized type 'UnknownMatchType' - only 'HostnameGenerator' is supported
+- field: spec.match.port
+  message: port must be a valid (1-65535)
+- field: spec.match.protocol
+  message: '"protocol" must be one of ["tcp", "grpc", "http", "http2"]'
+- field: spec.endpoints[0].port
+  message: must be defined when endpoint is an IP
+- field: spec.endpoints[1].port
+  message: port must be a valid (1-65535)
+- field: spec.endpoints[2].port
+  message: must be defined when endpoint is a hostname
+- field: spec.endpoints[3].port
+  message: must not be defined when endpoint is a unix path
+- field: spec.endpoints[4].address
+  message: address has to be a valid IP or hostname or a unix path
+- field: spec.tls.version.min
+  message: '"min" must be one of ["TLSAuto", "TLS10", "TLS11", "TLS12", "TLS13"]'
+- field: spec.tls.version.max
+  message: '"max" must be one of ["TLSAuto", "TLS10", "TLS11", "TLS12", "TLS13"]'
+- field: spec.tls.verification.mode
+  message: '"mode" must be one of ["SkipSAN", "SkipCA", "SkipAll", "Secured"]'
+- field: spec.tls.verification.subjectAltNames[0].type
+  message: '"type" must be one of ["Prefix", "Exact"]'
+- field: spec.tls.verification.clientKey
+  message: must be defined when clientCert is defined
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-without-extension-valid.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-without-extension-valid.input.yaml
new file mode 100644
index 000000000..bf48233cb
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-without-extension-valid.input.yaml
@@ -0,0 +1,27 @@
+match:
+  type: HostnameGenerator
+  port: 8080
+  protocol: http
+endpoints:
+  - address: 1.1.1.1
+    port: 12345
+  - address: example.com
+    port: 80
+  - address: unix:///tmp/example.sock
+tls:
+  version:
+    min: TLS12
+    max: TLS13
+  allowRenegotiation: false
+  verification:
+    subjectAltNames:
+      - type: Exact
+        value: example.com
+      - type: Prefix
+        value: "spiffe://example.local/ns/local"
+    caCert:
+      inline: dGVzdA==
+    clientCert:
+      secret: "123"
+    clientKey:
+      secret: "123"
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-without-extension-valid.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/full-without-extension-valid.output.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/min-higher-than-max-invalid.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/min-higher-than-max-invalid.input.yaml
new file mode 100644
index 000000000..b5a35ec16
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/min-higher-than-max-invalid.input.yaml
@@ -0,0 +1,11 @@
+match:
+  type: HostnameGenerator
+  port: 80
+  protocol: http
+endpoints:
+  - address: 1.1.1.1
+    port: 12345
+tls:
+  version:
+    min: TLS13
+    max: TLS12
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/min-higher-than-max-invalid.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/min-higher-than-max-invalid.output.yaml
new file mode 100644
index 000000000..b3aac5ed0
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/min-higher-than-max-invalid.output.yaml
@@ -0,0 +1,3 @@
+violations:
+- field: spec.tls.version.min
+  message: min version must be lower than max
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid-extension.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid-extension.input.yaml
new file mode 100644
index 000000000..58db1e8b5
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid-extension.input.yaml
@@ -0,0 +1,7 @@
+match:
+  type: HostnameGenerator
+  port: 80
+  protocol: http
+extension:
+  config:
+    example: true
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid-extension.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid-extension.output.yaml
new file mode 100644
index 000000000..c860d89c0
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid-extension.output.yaml
@@ -0,0 +1,3 @@
+violations:
+- field: spec.extension.type
+  message: must not be empty
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid.input.yaml
new file mode 100644
index 000000000..5fec34502
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid.input.yaml
@@ -0,0 +1,7 @@
+match:
+  type: HostnameGenerator
+  port: 80
+  protocol: unknown
+endpoints:
+  - address: 1.1.1.1
+    port: 12345
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid.output.yaml
new file mode 100644
index 000000000..8ce7444ee
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-invalid.output.yaml
@@ -0,0 +1,3 @@
+violations:
+- field: spec.match.protocol
+  message: '"protocol" must be one of ["tcp", "grpc", "http", "http2"]'
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-valid.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-valid.input.yaml
new file mode 100644
index 000000000..0fdf71429
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-valid.input.yaml
@@ -0,0 +1,7 @@
+match:
+  type: HostnameGenerator
+  port: 80
+  protocol: http
+endpoints:
+  - address: 1.1.1.1
+    port: 12345
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-valid.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/minimal-valid.output.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/missing-client-cert-invalid.input.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/missing-client-cert-invalid.input.yaml
new file mode 100644
index 000000000..4c3b36b04
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/missing-client-cert-invalid.input.yaml
@@ -0,0 +1,11 @@
+match:
+  type: HostnameGenerator
+  port: 8080
+  protocol: http
+endpoints:
+  - address: 1.1.1.1
+    port: 12345
+tls:
+  verification:
+    clientKey:
+      secret: "123"
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/missing-client-cert-invalid.output.yaml b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/missing-client-cert-invalid.output.yaml
new file mode 100644
index 000000000..9df65c7a6
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/testdata/missing-client-cert-invalid.output.yaml
@@ -0,0 +1,3 @@
+violations:
+- field: spec.tls.verification.clientCert
+  message: must be defined when clientKey is defined
diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator_test.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator_test.go
new file mode 100644
index 000000000..b74711f91
--- /dev/null
+++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/validator_test.go
@@ -0,0 +1,67 @@
+package v1alpha1_test
+
+import (
+	"os"
+	"path"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"sigs.k8s.io/yaml"
+
+	"github.com/kumahq/kuma/pkg/core/resources/apis/meshexternalservice/api/v1alpha1"
+	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
+	"github.com/kumahq/kuma/pkg/test/matchers"
+)
+
+var _ = Describe("MeshExternalService", func() {
+	Describe("Validate()", func() {
+		type testCase struct {
+			file string
+		}
+
+		DescribeTable("should validate all fields and return as much individual errors as possible",
+			func(given testCase) {
+				// setup
+				MeshExternalService := v1alpha1.NewMeshExternalServiceResource()
+
+				// when
+				contents, err := os.ReadFile(path.Join("testdata", given.file+".input.yaml"))
+				Expect(err).ToNot(HaveOccurred())
+				err = core_model.FromYAML(contents, &MeshExternalService.Spec)
+				Expect(err).ToNot(HaveOccurred())
+				// and
+				verr := MeshExternalService.Validate()
+				actual, err := yaml.Marshal(verr)
+				// have to do this otherwise valid cases will have null in the contents
+				if string(actual) == "null\n" {
+					actual = []byte{}
+				}
+				Expect(err).ToNot(HaveOccurred())
+
+				// then
+				Expect(actual).To(matchers.MatchGoldenYAML(path.Join("testdata", given.file+".output.yaml")))
+			},
+			Entry("minimal passing example", testCase{
+				file: "minimal-valid",
+			}),
+			Entry("full example without extension", testCase{
+				file: "full-without-extension-valid",
+			}),
+			Entry("minimal failing example with unknown port", testCase{
+				file: "minimal-invalid",
+			}),
+			Entry("failing example with missing extension type", testCase{
+				file: "minimal-invalid-extension",
+			}),
+			Entry("missing client-cert", testCase{
+				file: "missing-client-cert-invalid",
+			}),
+			Entry("full failing example", testCase{
+				file: "full-invalid",
+			}),
+			Entry("min tls version higher than max", testCase{
+				file: "min-higher-than-max-invalid",
+			}),
+		)
+	})
+})
diff --git a/pkg/plugins/runtime/k8s/controllers/meshservice_controller_test.go b/pkg/plugins/runtime/k8s/controllers/meshservice_controller_test.go
index 80d51398a..c07722034 100644
--- a/pkg/plugins/runtime/k8s/controllers/meshservice_controller_test.go
+++ b/pkg/plugins/runtime/k8s/controllers/meshservice_controller_test.go
@@ -94,8 +94,8 @@ var _ = Describe("MeshServiceController", func() {
 			outputFile: "01.meshservice.yaml",
 		}),
 		Entry("with service with mesh label", testCase{
-			inputFile:  "01.resources.yaml",
-			outputFile: "01.meshservice.yaml",
+			inputFile:  "02.resources.yaml",
+			outputFile: "02.meshservice.yaml",
 		}),
 		Entry("without mesh label and sidecar injection namespace", testCase{
 			inputFile:  "03.resources.yaml",
diff --git a/pkg/plugins/runtime/k8s/controllers/testdata/meshservice/02.meshservice.yaml b/pkg/plugins/runtime/k8s/controllers/testdata/meshservice/02.meshservice.yaml
index eed1e93f2..ed5daed00 100644
--- a/pkg/plugins/runtime/k8s/controllers/testdata/meshservice/02.meshservice.yaml
+++ b/pkg/plugins/runtime/k8s/controllers/testdata/meshservice/02.meshservice.yaml
@@ -1,6 +1,7 @@
 metadata:
   creationTimestamp: null
   labels:
+    k8s.kuma.io/service-name: example
     kuma.io/mesh: default
   name: example
   namespace: demo
@@ -19,7 +20,7 @@ spec:
     protocol: tcp
     targetPort: 8443
   selector: {}
-  status:
-    tls: {}
-    vips:
-    - ip: 192.168.0.1
+status:
+  tls: {}
+  vips:
+  - ip: 192.168.0.1
diff --git a/pkg/util/xds/stats_callbacks_test.go b/pkg/util/xds/stats_callbacks_test.go
index 87be7bbb7..0462ee249 100644
--- a/pkg/util/xds/stats_callbacks_test.go
+++ b/pkg/util/xds/stats_callbacks_test.go
@@ -111,17 +111,20 @@ var _ = Describe("Stats callbacks", func() {
 				VersionInfo: "123",
 				Node:        node,
 			}
-			err := adaptedCallbacks.OnStreamRequest(streamId, req)
+			resp := &envoy_discovery.DiscoveryResponse{
+				TypeUrl:     resource.RouteType,
+				VersionInfo: "123",
+			}
+			adaptedCallbacks.OnStreamResponse(context.Background(), streamId, req, resp)
 
 			// then
-			Expect(err).ToNot(HaveOccurred())
-			Expect(test_metrics.FindMetric(metrics, "xds_client_versions", "version", "1.0.0").GetGauge().GetValue()).To(Equal(1.0))
+			Expect(test_metrics.FindMetric(metrics, "xds_client_versions", "client_version", "1.0.0").GetGauge().GetValue()).To(Equal(1.0))
 
 			// when
 			adaptedCallbacks.OnStreamClosed(streamId, node)
 
 			// then
-			Expect(test_metrics.FindMetric(metrics, "xds_client_versions", "version", "1.0.0").GetGauge().GetValue()).To(Equal(0.0))
+			Expect(test_metrics.FindMetric(metrics, "xds_client_versions", "client_version", "1.0.0").GetGauge().GetValue()).To(Equal(0.0))
 		})
 
 		It("should track NACK", func() {
@@ -293,11 +296,14 @@ var _ = Describe("Stats callbacks", func() {
 				Node:          node,
 				ResponseNonce: "1",
 			}
-			err := adaptedCallbacks.OnStreamDeltaRequest(streamId, req)
+			resp := &envoy_discovery.DeltaDiscoveryResponse{
+				TypeUrl:           resource.RouteType,
+				SystemVersionInfo: "123",
+			}
+			adaptedCallbacks.OnStreamDeltaResponse(streamId, req, resp)
 
 			// then
-			Expect(err).ToNot(HaveOccurred())
-			Expect(test_metrics.FindMetric(metrics, "delta_xds_client_versions", "version", "1.0.0").GetGauge().GetValue()).To(Equal(1.0))
+			Expect(test_metrics.FindMetric(metrics, "delta_xds_client_versions", "client_version", "1.0.0").GetGauge().GetValue()).To(Equal(1.0))
 
 			// when
 			adaptedCallbacks.OnDeltaStreamClosed(streamId, node)
diff --git a/test/e2e_env/universal/auth/dp_auth.go b/test/e2e_env/universal/auth/dp_auth.go
index 9a286f8c0..465024b0f 100644
--- a/test/e2e_env/universal/auth/dp_auth.go
+++ b/test/e2e_env/universal/auth/dp_auth.go
@@ -103,6 +103,7 @@ data: %s`, base64.StdEncoding.EncodeToString([]byte(claims.ID)))
 		Eventually(func(g Gomega) {
 			// we need to trigger XDS config change for this DP to disconnect it
 			// this limitation may be lifted in the future
+			randomRetries := rand.Int()%100 + 1 // #nosec G404 -- this is for tests no need to use secure rand
 			yaml = fmt.Sprintf(`
 type: MeshRetry
 name: retry-policy
@@ -123,7 +124,7 @@ spec:
             maxInterval: 20m
           retryOn:
             - "5xx"
-`, rand.Int()%100+1) // #nosec G404 -- this is for tests no need to use secure rand
+`, randomRetries)
 			g.Expect(universal.Cluster.Install(YamlUniversal(yaml))).To(Succeed())
 
 			online, _, err := IsDataplaneOnline(universal.Cluster, meshName, serviceName)
diff --git a/test/framework/client/collect.go b/test/framework/client/collect.go
index 0c5a29f5e..149432cd9 100644
--- a/test/framework/client/collect.go
+++ b/test/framework/client/collect.go
@@ -372,10 +372,9 @@ func MakeDirectRequest(
 			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 				return dialer.Dial(network, u.Host)
 			},
-			// #nosec G402 -- Intentionally weak in tests
 			TLSClientConfig: &tls.Config{
 				ServerName:         req.Host,
-				InsecureSkipVerify: true,
+				InsecureSkipVerify: true,                 // #nosec G402 -- Intentionally weak in tests
 				NextProtos:         []string{"http/1.1"}, // ALPN is required by Envoy
 			},
 		}
diff --git a/test/framework/setup.go b/test/framework/setup.go
index 2be2b5493..435a567d6 100644
--- a/test/framework/setup.go
+++ b/test/framework/setup.go
@@ -914,10 +914,18 @@ func DumpTempCerts(names ...string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	if err := os.WriteFile(filepath.Join(path, "cert.pem"), []byte(fmt.Sprintf("---\n%s", cert)), os.ModePerm); err != nil {
+	if err := os.WriteFile(
+		filepath.Join(path, "cert.pem"),
+		[]byte(fmt.Sprintf("---\n%s", cert)),
+		os.ModePerm, // #nosec G306
+	); err != nil {
 		return "", err
 	}
-	if err := os.WriteFile(filepath.Join(path, "key.pem"), []byte(fmt.Sprintf("---\n%s", key)), os.ModePerm); err != nil {
+	if err := os.WriteFile(
+		filepath.Join(path, "key.pem"),
+		[]byte(fmt.Sprintf("---\n%s", key)),
+		os.ModePerm, // #nosec G306
+	); err != nil {
 		return "", err
 	}
 	return path, nil