Repository: istio/istio (go)

Issue:
TLS parameters cannot be configured with EnvoyFilter
**Bug description**
We are unable to configure TLS parameters (to control the TLS version, or set TLS ciphersuites, curves etc.) using an Envoy Filter.
Specifically, we would like to force the use of TLSv1_3 and of the CECPQ2 curve, a Post-Quantum key exchange available in BoringSSL, which works fine in stand-alone Envoy.
For that, we wrote a configuration patch in an EnvoyFilter. It should be sufficient to then apply it, but the behaviour of the function used to merge the configurations: `proto.merge`, is not correct. The resulting configuration is broken and essentially ignores the patch.


**What we observed**: 
Instead of merging our config with the main one:
For the clusters: it adds it after as a new item and is not taken into account.
For the listeners: it erases part of the main config, where the merge should have been done.
(Checked using `$ istioctl proxy-config cluster/listener`).


We wrote a fix and tested it, both for cluster and listener’s patch in the EnvoyFilter handling Go code.

We would like to open a PR after some community discussion.

Our fix can be used to configure any TLS parameters, and can be duplicated to be used for other configurations.



[ ] Docs
[ ] Installation
[X] Networking
[ ] Performance and Scalability
[ ] Extensions and Telemetry
[X] Security
[ ] Test and Release
[ ] User Experience
[ ] Developer Infrastructure
[ ] Upgrade


**Steps to reproduce the bug**
We have 2 services, Service1 and Service2, deployed with sidecars. Service1 sends a simple http get request to Service2. The request goes to (the egress port of) the sidecar of Service1, which initiates a TLS connection to (the ingress port of) the sidecar of Service2. We want this TLS connection to be TLS1.3 with curve CECPQ2.

Service1 ---(http)--> SidecarOfService1 ---(https)--> SidecarOfService2 ---(http)--> Service2  

The communication, as described above, is established between one sidecar to the other one with:
`$ kubectl exec -it <pod name of service1> -c service1 -- curl -v http://service2.default:8050/myservice/2`

Our EnvoyFilter:
```yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: my-protocol
  namespace: default
spec:
  configPatches:
  - applyTo: FILTER_CHAIN
    match:
      listener:
        filterChain:
          filter:
            name: "envoy.transport_sockets.tls"
    patch:
      operation: MERGE
      value:
        transport_socket:
          name: envoy.transport_sockets.tls
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
            common_tls_context:
              tls_params:
                tls_maximum_protocol_version: TLSv1_3
                tls_minimum_protocol_version: TLSv1_3
                ecdh_curves:
                - "CECPQ2"
  - applyTo: CLUSTER
    match:
      context: SIDECAR_OUTBOUND
    patch:
      operation: MERGE
      value:
        transport_socket:
          name: envoy.transport_sockets.tls
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
            common_tls_context:
              tls_params:
                tls_maximum_protocol_version: TLSv1_3
                tls_minimum_protocol_version: TLSv1_3
                ecdh_curves:
                - "CECPQ2"
```

**Version**
Output of `istioctl version --remote`
client version: 1.7.0
control plane version: 1.7.0
data plane version: 1.7.0 (4 proxies)

Output of `kubectl version --short`
Client Version: v1.18.6
Server Version: v1.17.5


**Related issues**
#13138 (and PR #27500) allows to set a baseline TLS configuration for the entire Mesh. With our fix, the user can be very flexible on the choice of the TLS parameters, including the TLS version, the cipher suites, the curves. In addition, these parameters can be applied to a smaller scope, like specific proxies. As one example, this will allow people to require TLS 1.3 to be used everywhere, for compliance reasons

#18169 #24330
Same root cause as us: the function proto.merge

Pull request description:
Fix to enable choice of TLS parameters within Envoy Filters
Fixes #28996

We would like to be able to configure any TLS parameters (TLS version, TLS ciphersuites, curves etc.) using Envoy Filters.
Specifically, we would like to be Post-Quantum secure, i.e. use TLSv1_3 and CECPQ2 curve, a Post-Quantum key exchange available in BoringSSL. It already works fine in stand-alone Envoy.
We encountered a bug when applying our EnvoyFilter: the behaviour of the function used to merge the configurations, proto.merge, is not correct. The resulting configuration is broken and essentially ignores the patch.

Instead of merging our config with the main one:
For the clusters: it is not able to match the main TransportSocket configuration with ours, so it adds it after as a new item and is not taken into account.
For the listeners: it erases part of the main config, where the merge should have been done, and results in a failure.
(Checked using $ istioctl proxy-config cluster/listener).

We propose the following fix, for clusters' and listeners' patches. It allows the merge of a deep configuration for TransportSockets. We added tests that are passing with our fix, and are not passing without it. It explicitly demonstrates the problem we encountered. The fix won't affect the users, it only affect the use of EnvoyFilter when a patch needs to be merged.

Our fix can be used to configure any TLS parameters, and can be duplicated to be used for other deep configurations that are failing.

[ ] Configuration Infrastructure
[ ] Docs
[ ] Installation
[X] Networking
[ ] Performance and Scalability
[ ] Policies and Telemetry
[X] Security
[ ] Test and Release
[ ] User Experience
[ ] Developer Infrastructure


Pull Request Attributes

[X] Does not have any changes that may affect Istio users.


Patch:
diff --git a/galley/pkg/config/analysis/diag/message.go b/galley/pkg/config/analysis/diag/message.go
index de566637e3..4da9aeee0f 100644
--- a/galley/pkg/config/analysis/diag/message.go
+++ b/galley/pkg/config/analysis/diag/message.go
@@ -87,7 +87,7 @@ func (m *Message) Unstructured(includeOrigin bool) map[string]interface{} {
 	if m.DocRef != "" {
 		docQueryString = fmt.Sprintf("?ref=%s", m.DocRef)
 	}
-	result["documentation_url"] = fmt.Sprintf("%s/%s/%s", url.ConfigAnalysis, strings.ToLower(m.Type.Code()), docQueryString)
+	result["documentationUrl"] = fmt.Sprintf("%s/%s/%s", url.ConfigAnalysis, strings.ToLower(m.Type.Code()), docQueryString)
 
 	return result
 }
diff --git a/go.mod b/go.mod
index 4176800edf..3e2d23e715 100644
--- a/go.mod
+++ b/go.mod
@@ -100,7 +100,7 @@ require (
 	gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
 	helm.sh/helm/v3 v3.4.2
 	honnef.co/go/tools v0.0.1-2020.1.5 // indirect
-	istio.io/api v0.0.0-20210201080711-e51932d6679a
+	istio.io/api v0.0.0-20210202013920-4da173f69734
 	istio.io/client-go v0.0.0-20200908160912-f99162621a1a
 	istio.io/gogo-genproto v0.0.0-20210121190807-0ad02665e756
 	istio.io/pkg v0.0.0-20201230223204-2d0a1c8bd9e5
diff --git a/go.sum b/go.sum
index 4c987cb53b..51c7379713 100644
--- a/go.sum
+++ b/go.sum
@@ -1324,8 +1324,8 @@ honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75
 honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 istio.io/api v0.0.0-20190515205759-982e5c3888c6/go.mod h1:hhLFQmpHia8zgaM37vb2ml9iS5NfNfqZGRt1pS9aVEo=
 istio.io/api v0.0.0-20200812202721-24be265d41c3/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64=
-istio.io/api v0.0.0-20210201080711-e51932d6679a h1:cV9aAkv0tqfce1BsJZZ8VJE+RlBAzBnaEIn8zOOa/0s=
-istio.io/api v0.0.0-20210201080711-e51932d6679a/go.mod h1:2Yy1/rmZ2uz0mRHRoP8ofrSihTrgROzLnYVygc5AxD0=
+istio.io/api v0.0.0-20210202013920-4da173f69734 h1:5jEfgr08M1S5f9zMpeFqLq8lEocxPlK5ZvcJf9reF44=
+istio.io/api v0.0.0-20210202013920-4da173f69734/go.mod h1:2Yy1/rmZ2uz0mRHRoP8ofrSihTrgROzLnYVygc5AxD0=
 istio.io/client-go v0.0.0-20200908160912-f99162621a1a h1:clPn0fz+rXq5Ytj6Ppb1ygUKeU0RImT4ZbT1oMd1G04=
 istio.io/client-go v0.0.0-20200908160912-f99162621a1a/go.mod h1:SO65MWt7I45dvUwuDowoiB0SVcGpfWZfUTlopvYpbZc=
 istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs=
diff --git a/istioctl/cmd/tag.go b/istioctl/cmd/tag.go
index 74c4779ddb..ea88ac1359 100644
--- a/istioctl/cmd/tag.go
+++ b/istioctl/cmd/tag.go
@@ -29,6 +29,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 
 	"istio.io/api/label"
+	"istio.io/istio/operator/cmd/mesh"
 	"istio.io/istio/operator/pkg/helm"
 	"istio.io/istio/pkg/kube"
 )
@@ -39,7 +40,6 @@ const (
 	istioInjectionWebhookSuffix = "sidecar-injector.istio.io"
 	defaultRevisionName         = "default"
 	pilotDiscoveryChart         = "istio-control/istio-discovery"
-	chartsPath                  = "" // use compiled in charts for tag webhook gen
 	revisionTagTemplateName     = "revision-tags.yaml"
 
 	// help strings and long formatted user outputs
@@ -56,6 +56,7 @@ revision tag, use 'kubectl label namespace <NAMESPACE> istio.io/rev=%s'
 var (
 	// revision to point tag webhook at
 	revision         = ""
+	manifestsPath    = ""
 	overwrite        = false
 	skipConfirmation = false
 )
@@ -135,6 +136,7 @@ injection labels.`,
 	}
 
 	cmd.PersistentFlags().BoolVar(&overwrite, "overwrite", false, overrideHelpStr)
+	cmd.PersistentFlags().StringVarP(&manifestsPath, "manifests", "d", "", mesh.ManifestsFlagHelpStr)
 	cmd.PersistentFlags().BoolVarP(&skipConfirmation, "skip-confirmation", "y", false, skipConfirmationFlagHelpStr)
 	cmd.PersistentFlags().StringVarP(&revision, "revision", "r", "", revisionHelpStr)
 	_ = cmd.MarkPersistentFlagRequired("revision")
@@ -195,6 +197,7 @@ revision tag before removing using the "istioctl x tag list" command.
 		},
 	}
 
+	cmd.PersistentFlags().BoolVarP(&skipConfirmation, "skip-confirmation", "y", false, skipConfirmationFlagHelpStr)
 	return cmd
 }
 
@@ -233,7 +236,7 @@ func setTag(ctx context.Context, kubeClient kube.ExtendedClient, tag, revision s
 	if err != nil {
 		return fmt.Errorf("failed to create tag webhook config: %v", err)
 	}
-	tagWhYAML, err := tagWebhookYAML(tagWhConfig, chartsPath)
+	tagWhYAML, err := tagWebhookYAML(tagWhConfig, manifestsPath)
 	if err != nil {
 		return fmt.Errorf("failed to create tag webhook: %v", err)
 	}
diff --git a/manifests/charts/base/crds/crd-all.gen.yaml b/manifests/charts/base/crds/crd-all.gen.yaml
index 6399560063..faddc02a75 100644
--- a/manifests/charts/base/crds/crd-all.gen.yaml
+++ b/manifests/charts/base/crds/crd-all.gen.yaml
@@ -3057,6 +3057,7 @@ spec:
               - CUSTOM
               type: string
             provider:
+              description: Specifies detailed configuration of the CUSTOM action.
               properties:
                 name:
                   description: Specifies the name of the extension provider.
diff --git a/manifests/charts/base/files/gen-istio-cluster.yaml b/manifests/charts/base/files/gen-istio-cluster.yaml
index 8198bfe7a5..2fda970eed 100644
--- a/manifests/charts/base/files/gen-istio-cluster.yaml
+++ b/manifests/charts/base/files/gen-istio-cluster.yaml
@@ -3059,6 +3059,7 @@ spec:
               - CUSTOM
               type: string
             provider:
+              description: Specifies detailed configuration of the CUSTOM action.
               properties:
                 name:
                   description: Specifies the name of the extension provider.
diff --git a/operator/cmd/mesh/shared.go b/operator/cmd/mesh/shared.go
index e484034504..b5ee42fa4b 100644
--- a/operator/cmd/mesh/shared.go
+++ b/operator/cmd/mesh/shared.go
@@ -16,7 +16,6 @@
 package mesh
 
 import (
-	"context"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -26,8 +25,6 @@ import (
 	"time"
 
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
-	v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/scheme"
@@ -35,7 +32,6 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	"istio.io/api/label"
 	"istio.io/istio/operator/pkg/apis/istio/v1alpha1"
 	"istio.io/istio/operator/pkg/cache"
 	"istio.io/istio/operator/pkg/helmreconciler"
@@ -244,30 +240,7 @@ func getCRAndNamespaceFromFile(filePath string, l clog.Logger) (customResource s
 
 // createNamespace creates a namespace using the given k8s interface.
 func createNamespace(cs kubernetes.Interface, namespace string, network string) error {
-	if namespace == "" {
-		// Setup default namespace
-		namespace = istioDefaultNamespace
-	}
-	// check if the namespace already exists. If yes, do nothing. If no, create a new one.
-	_, err := cs.CoreV1().Namespaces().Get(context.TODO(), namespace, v12.GetOptions{})
-	if err != nil {
-		if errors.IsNotFound(err) {
-			ns := &v1.Namespace{ObjectMeta: v12.ObjectMeta{
-				Name: namespace,
-			}}
-			if network != "" {
-				ns.Labels[label.TopologyNetwork.Name] = network
-			}
-			_, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, v12.CreateOptions{})
-			if err != nil {
-				return fmt.Errorf("failed to create namespace %v: %v", namespace, err)
-			}
-		} else {
-			return fmt.Errorf("failed to check if namespace %v exists: %v", namespace, err)
-		}
-	}
-
-	return nil
+	return helmreconciler.CreateNamespace(cs, namespace, network)
 }
 
 // saveIOPToCluster saves the state in an IOP CR in the cluster.
diff --git a/operator/pkg/helmreconciler/reconciler.go b/operator/pkg/helmreconciler/reconciler.go
index c7ec4bc2ff..6b90075777 100644
--- a/operator/pkg/helmreconciler/reconciler.go
+++ b/operator/pkg/helmreconciler/reconciler.go
@@ -22,10 +22,10 @@ import (
 	"sync"
 	"time"
 
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
@@ -498,29 +498,23 @@ func (h *HelmReconciler) reportPrunedObjectKind() {
 	}
 }
 
-// createNamespace creates a namespace using the given k8s client.
-func (h *HelmReconciler) createNamespace(namespace string, network string) error {
+// CreateNamespace creates a namespace using the given k8s interface.
+func CreateNamespace(cs kubernetes.Interface, namespace string, network string) error {
 	if namespace == "" {
 		// Setup default namespace
 		namespace = name.IstioDefaultNamespace
 	}
-	// Check if the namespace already exists. If yes, do nothing. If no, create a new one.
-	// TODO(morvencao): consider to use the helm reconciler's client after Multi Namespace Cache
-	// supports get and create cluster scoped resources, see:
-	// https://github.com/kubernetes-sigs/controller-runtime/issues/934
-	_, err := h.clientSet.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
+	// check if the namespace already exists. If yes, do nothing. If no, create a new one.
+	_, err := cs.CoreV1().Namespaces().Get(context.TODO(), namespace, v12.GetOptions{})
 	if err != nil {
 		if errors.IsNotFound(err) {
-			ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+			ns := &v1.Namespace{ObjectMeta: v12.ObjectMeta{
 				Name: namespace,
-				Labels: map[string]string{
-					"istio-injection": "disabled",
-				},
 			}}
 			if network != "" {
 				ns.Labels[label.TopologyNetwork.Name] = network
 			}
-			_, err := h.clientSet.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
+			_, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, v12.CreateOptions{})
 			if err != nil {
 				return fmt.Errorf("failed to create namespace %v: %v", namespace, err)
 			}
@@ -532,6 +526,11 @@ func (h *HelmReconciler) createNamespace(namespace string, network string) error
 	return nil
 }
 
+// createNamespace creates a namespace using the given k8s client.
+func (h *HelmReconciler) createNamespace(namespace string, network string) error {
+	return CreateNamespace(h.clientSet, namespace, network)
+}
+
 func (h *HelmReconciler) networkName() string {
 	if h.iop == nil || h.iop.Spec == nil || h.iop.Spec.Values == nil {
 		return ""
diff --git a/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch.go b/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch.go
index 6b2eed459f..95611c84b9 100644
--- a/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch.go
+++ b/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch.go
@@ -15,11 +15,15 @@
 package envoyfilter
 
 import (
+	"fmt"
+
 	cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	"github.com/golang/protobuf/proto"
 
 	networking "istio.io/api/networking/v1alpha3"
 	"istio.io/istio/pilot/pkg/model"
+	"istio.io/istio/pilot/pkg/networking/util"
 	"istio.io/istio/pilot/pkg/util/runtime"
 	"istio.io/istio/pkg/config/host"
 	"istio.io/pkg/log"
@@ -39,12 +43,66 @@ func ApplyClusterMerge(pctx networking.EnvoyFilter_PatchContext, efw *model.Envo
 			continue
 		}
 		if commonConditionMatch(pctx, cp) && clusterMatch(c, cp) {
-			proto.Merge(c, cp.Value)
+
+			ret, err := mergeTransportSocketCluster(c, cp)
+			if err != nil {
+				log.Debugf("Merge of transport socket failed for cluster: %v", err)
+				continue
+			}
+			if !ret {
+				proto.Merge(c, cp.Value)
+			}
 		}
 	}
 	return c
 }
 
+// Test if the patch contains a config for TransportSocket
+func mergeTransportSocketCluster(c *cluster.Cluster, cp *model.EnvoyFilterConfigPatchWrapper) (bool, error) {
+
+	cpValueCast, okCpCast := (cp.Value).(*cluster.Cluster)
+	if !okCpCast {
+		return false, fmt.Errorf("cast of cp.Value failed: %v", okCpCast)
+	}
+
+	var tsmPatch *core.TransportSocket
+
+	// Test if the patch contains a config for TransportSocket
+	// and if the cluster contains a config for Transport Socket Matches
+	if cpValueCast.GetTransportSocket() != nil && c.GetTransportSocketMatches() != nil {
+		for _, tsm := range c.GetTransportSocketMatches() {
+			if tsm.GetTransportSocket() != nil && cpValueCast.GetTransportSocket().Name == tsm.GetTransportSocket().Name {
+				tsmPatch = tsm.GetTransportSocket()
+				break
+			}
+		}
+	} else if cpValueCast.GetTransportSocket() != nil && c.GetTransportSocket() != nil {
+		if cpValueCast.GetTransportSocket().Name == c.GetTransportSocket().Name {
+			tsmPatch = c.GetTransportSocket()
+		}
+	}
+
+	if tsmPatch != nil {
+		// Merge the patch and the cluster at a lower level
+		dstCluster := tsmPatch.GetTypedConfig()
+		srcPatch := cpValueCast.GetTransportSocket().GetTypedConfig()
+
+		if dstCluster != nil && srcPatch != nil {
+
+			retVal, errMerge := util.MergeAnyWithAny(dstCluster, srcPatch)
+			if errMerge != nil {
+				return false, fmt.Errorf("function MergeAnyWithAny failed for ApplyClusterMerge: %v", errMerge)
+			}
+
+			// Merge the above result with the whole cluster
+			proto.Merge(dstCluster, retVal)
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
 func ShouldKeepCluster(pctx networking.EnvoyFilter_PatchContext, efw *model.EnvoyFilterWrapper, c *cluster.Cluster) bool {
 	if efw == nil {
 		return true
diff --git a/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch.go b/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch.go
index 707eadd321..55a59dfafd 100644
--- a/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch.go
+++ b/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch.go
@@ -15,9 +15,11 @@
 package envoyfilter
 
 import (
+	"fmt"
+
 	xdslistener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
 	http_conn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
-	wellknown "github.com/envoyproxy/go-control-plane/pkg/wellknown"
+	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/ptypes"
 	"github.com/golang/protobuf/ptypes/any"
@@ -179,12 +181,57 @@ func doFilterChainOperation(patchContext networking.EnvoyFilter_PatchContext,
 			// nothing more to do in other patches as we removed this filter chain
 			return
 		} else if cp.Operation == networking.EnvoyFilter_Patch_MERGE {
-			proto.Merge(fc, cp.Value)
+
+			ret, err := mergeTransportSocketListener(fc, cp)
+			if err != nil {
+				log.Debugf("merge of transport socket failed for listener: %v", err)
+				continue
+			}
+			if !ret {
+				proto.Merge(fc, cp.Value)
+			}
 		}
 	}
 	doNetworkFilterListOperation(patchContext, patches, listener, fc)
 }
 
+// Test if the patch contains a config for TransportSocket
+func mergeTransportSocketListener(fc *xdslistener.FilterChain, cp *model.EnvoyFilterConfigPatchWrapper) (bool, error) {
+
+	cpValueCast, okCpCast := (cp.Value).(*xdslistener.FilterChain)
+	if !okCpCast {
+		return false, fmt.Errorf("cast of cp.Value failed: %v", okCpCast)
+	}
+
+	// Test if the patch contains a config for TransportSocket
+	applyPatch := false
+	if cpValueCast.GetTransportSocket() != nil {
+		// Test if the listener contains a config for TransportSocket
+		applyPatch = fc.GetTransportSocket() != nil && cpValueCast.GetTransportSocket().Name == fc.GetTransportSocket().Name
+	} else {
+		return false, nil
+	}
+
+	if applyPatch {
+		// Merge the patch and the listener at a lower level
+		dstListener := fc.GetTransportSocket().GetTypedConfig()
+		srcPatch := cpValueCast.GetTransportSocket().GetTypedConfig()
+
+		if dstListener != nil && srcPatch != nil {
+
+			retVal, errMerge := util.MergeAnyWithAny(dstListener, srcPatch)
+			if errMerge != nil {
+				return false, fmt.Errorf("function mergeAnyWithAny failed for doFilterChainOperation: %v", errMerge)
+			}
+
+			// Merge the above result with the whole listener
+			proto.Merge(dstListener, retVal)
+		}
+	}
+	// Default: we won't call proto.Merge() if the patch is transportSocket and the listener isn't
+	return true, nil
+}
+
 func doNetworkFilterListOperation(patchContext networking.EnvoyFilter_PatchContext,
 	patches map[networking.EnvoyFilter_ApplyTo][]*model.EnvoyFilterConfigPatchWrapper,
 	listener *xdslistener.Listener, fc *xdslistener.FilterChain) {


Test patch:
diff --git a/galley/pkg/config/analysis/diag/message_test.go b/galley/pkg/config/analysis/diag/message_test.go
index d60b7ec33d..41380c4bda 100644
--- a/galley/pkg/config/analysis/diag/message_test.go
+++ b/galley/pkg/config/analysis/diag/message_test.go
@@ -60,7 +60,7 @@ func TestMessageWithDocRef(t *testing.T) {
 	mt := NewMessageType(Error, "IST0042", "Cheese type not found: %q")
 	m := NewMessage(mt, nil, "Feta")
 	m.DocRef = "test-ref"
-	g.Expect(m.Unstructured(false)["documentation_url"]).To(Equal(url.ConfigAnalysis + "/ist0042/?ref=test-ref"))
+	g.Expect(m.Unstructured(false)["documentationUrl"]).To(Equal(url.ConfigAnalysis + "/ist0042/?ref=test-ref"))
 }
 
 func TestMessage_JSON(t *testing.T) {
@@ -69,7 +69,7 @@ func TestMessage_JSON(t *testing.T) {
 	m := NewMessage(mt, &resource.Instance{Origin: testOrigin{name: "toppings/cheese", ref: testReference{"path/to/file"}}}, "Feta")
 
 	j, _ := json.Marshal(&m)
-	g.Expect(string(j)).To(Equal(`{"code":"IST0042","documentation_url":"` + url.ConfigAnalysis + `/ist0042/"` +
+	g.Expect(string(j)).To(Equal(`{"code":"IST0042","documentationUrl":"` + url.ConfigAnalysis + `/ist0042/"` +
 		`,"level":"Error","message":"Cheese type not found: \"Feta\"","origin":"toppings/cheese","reference":"path/to/file"}`))
 }
 
diff --git a/galley/pkg/config/analysis/diag/messages_test.go b/galley/pkg/config/analysis/diag/messages_test.go
index 860c4aa580..372554d44f 100644
--- a/galley/pkg/config/analysis/diag/messages_test.go
+++ b/galley/pkg/config/analysis/diag/messages_test.go
@@ -132,7 +132,7 @@ func TestMessages_SetRefDoc(t *testing.T) {
 	msgs.SetDocRef("istioctl-awesome")
 
 	getDocURL := func(msg Message) string {
-		return msg.Unstructured(false)["documentation_url"].(string)
+		return msg.Unstructured(false)["documentationUrl"].(string)
 	}
 
 	g.Expect(getDocURL(msgs[0])).To(Equal(url.ConfigAnalysis + "/b1/?ref=istioctl-awesome"))
diff --git a/istioctl/pkg/util/formatting/formatter_test.go b/istioctl/pkg/util/formatting/formatter_test.go
index e2e9a15f20..797b631e45 100644
--- a/istioctl/pkg/util/formatting/formatter_test.go
+++ b/istioctl/pkg/util/formatting/formatter_test.go
@@ -89,14 +89,14 @@ func TestFormatter_PrintJSON(t *testing.T) {
 	expectedOutput := `[
 	{
 		"code": "B1",
-		"documentation_url": "` + url.ConfigAnalysis + `/b1/",
+		"documentationUrl": "` + url.ConfigAnalysis + `/b1/",
 		"level": "Error",
 		"message": "Explosion accident: the bubble is too big",
 		"origin": "SoapBubble"
 	},
 	{
 		"code": "C1",
-		"documentation_url": "` + url.ConfigAnalysis + `/c1/",
+		"documentationUrl": "` + url.ConfigAnalysis + `/c1/",
 		"level": "Warning",
 		"message": "Collapse danger: the castle is too old",
 		"origin": "GrandCastle"
@@ -124,12 +124,12 @@ func TestFormatter_PrintYAML(t *testing.T) {
 	output, _ := Print(msgs, YAMLFormat, false)
 
 	expectedOutput := `- code: B1
-  documentation_url: ` + url.ConfigAnalysis + `/b1/
+  documentationUrl: ` + url.ConfigAnalysis + `/b1/
   level: Error
   message: 'Explosion accident: the bubble is too big'
   origin: SoapBubble
 - code: C1
-  documentation_url: ` + url.ConfigAnalysis + `/c1/
+  documentationUrl: ` + url.ConfigAnalysis + `/c1/
   level: Warning
   message: 'Collapse danger: the castle is too old'
   origin: GrandCastle
diff --git a/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch_test.go b/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch_test.go
index eb9b521040..de7b41a593 100644
--- a/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch_test.go
+++ b/pilot/pkg/networking/core/v1alpha3/envoyfilter/cluster_patch_test.go
@@ -19,11 +19,13 @@ import (
 
 	cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
 	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/testing/protocmp"
 
 	networking "istio.io/api/networking/v1alpha3"
 	"istio.io/istio/pilot/pkg/model"
+	"istio.io/istio/pilot/pkg/networking/util"
 	"istio.io/istio/pilot/pkg/serviceregistry/memory"
 )
 
@@ -201,25 +203,149 @@ func TestClusterPatching(t *testing.T) {
 				Value:     buildPatchStruct(`{"lb_policy":"RING_HASH"}`),
 			},
 		},
+		{
+			ApplyTo: networking.EnvoyFilter_CLUSTER,
+			Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
+				Context: networking.EnvoyFilter_SIDECAR_OUTBOUND,
+			},
+			Patch: &networking.EnvoyFilter_Patch{
+				Operation: networking.EnvoyFilter_Patch_MERGE,
+				Value: buildPatchStruct(`
+				{"transport_socket":{
+					"name":"envoy.transport_sockets.tls",
+					"typed_config":{
+						"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
+						"common_tls_context":{
+							"tls_params":{
+								"tls_minimum_protocol_version":"TLSv1_3"}}}}}`),
+			},
+		},
 	}
 
 	sidecarOutboundIn := []*cluster.Cluster{
-		{Name: "cluster1", DnsLookupFamily: cluster.Cluster_V4_ONLY, LbPolicy: cluster.Cluster_ROUND_ROBIN},
+		{Name: "cluster1",
+			DnsLookupFamily: cluster.Cluster_V4_ONLY,
+			LbPolicy:        cluster.Cluster_ROUND_ROBIN,
+			TransportSocketMatches: []*cluster.Cluster_TransportSocketMatch{
+				{
+					Name: "tlsMode-istio",
+					TransportSocket: &core.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &core.TransportSocket_TypedConfig{
+							TypedConfig: util.MessageToAny(&tls.UpstreamTlsContext{
+								CommonTlsContext: &tls.CommonTlsContext{
+									TlsParams: &tls.TlsParameters{
+										EcdhCurves:                []string{"X25519"},
+										TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_1,
+									},
+									TlsCertificateCertificateProviderInstance: &tls.CommonTlsContext_CertificateProviderInstance{
+										InstanceName:    "instance",
+										CertificateName: "certificate",
+									},
+								},
+							}),
+						},
+					},
+				},
+			},
+		},
 		{Name: "cluster2",
 			Http2ProtocolOptions: &core.Http2ProtocolOptions{
 				AllowConnect:  true,
 				AllowMetadata: true,
 			}, LbPolicy: cluster.Cluster_MAGLEV,
 		},
+		{Name: "cluster3",
+			DnsLookupFamily: cluster.Cluster_V4_ONLY,
+			LbPolicy:        cluster.Cluster_ROUND_ROBIN,
+			TransportSocket: &core.TransportSocket{
+				Name: "envoy.transport_sockets.tls",
+				ConfigType: &core.TransportSocket_TypedConfig{
+					TypedConfig: util.MessageToAny(&tls.UpstreamTlsContext{
+						CommonTlsContext: &tls.CommonTlsContext{
+							TlsParams: &tls.TlsParameters{
+								EcdhCurves:                []string{"X25519"},
+								TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
+							},
+							TlsCertificateCertificateProviderInstance: &tls.CommonTlsContext_CertificateProviderInstance{
+								InstanceName:    "instance",
+								CertificateName: "certificate",
+							},
+						},
+					}),
+				},
+			},
+		},
 	}
 
 	sidecarOutboundOut := []*cluster.Cluster{
-		{Name: "cluster1", DnsLookupFamily: cluster.Cluster_V6_ONLY, LbPolicy: cluster.Cluster_RING_HASH},
+		{Name: "cluster1",
+			DnsLookupFamily: cluster.Cluster_V6_ONLY,
+			LbPolicy:        cluster.Cluster_RING_HASH,
+			TransportSocketMatches: []*cluster.Cluster_TransportSocketMatch{
+				{
+					Name: "tlsMode-istio",
+					TransportSocket: &core.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &core.TransportSocket_TypedConfig{
+							TypedConfig: util.MessageToAny(&tls.UpstreamTlsContext{
+								CommonTlsContext: &tls.CommonTlsContext{
+									TlsParams: &tls.TlsParameters{
+										EcdhCurves:                []string{"X25519"},
+										TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_3,
+									},
+									TlsCertificateCertificateProviderInstance: &tls.CommonTlsContext_CertificateProviderInstance{
+										InstanceName:    "instance",
+										CertificateName: "certificate",
+									},
+								},
+							}),
+						},
+					},
+				},
+			},
+		},
 		{Name: "cluster2",
 			Http2ProtocolOptions: &core.Http2ProtocolOptions{
 				AllowConnect:  true,
 				AllowMetadata: true,
-			}, LbPolicy: cluster.Cluster_RING_HASH, DnsLookupFamily: cluster.Cluster_V6_ONLY,
+			},
+			LbPolicy:        cluster.Cluster_RING_HASH,
+			DnsLookupFamily: cluster.Cluster_V6_ONLY,
+			TransportSocket: &core.TransportSocket{
+				Name: "envoy.transport_sockets.tls",
+				ConfigType: &core.TransportSocket_TypedConfig{
+					TypedConfig: util.MessageToAny(&tls.UpstreamTlsContext{
+						CommonTlsContext: &tls.CommonTlsContext{
+							TlsParams: &tls.TlsParameters{
+								TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_3,
+							},
+						},
+					}),
+				},
+			},
+		},
+		{Name: "cluster3",
+			DnsLookupFamily: cluster.Cluster_V6_ONLY,
+			LbPolicy:        cluster.Cluster_RING_HASH,
+			TransportSocket: &core.TransportSocket{
+				Name: "envoy.transport_sockets.tls",
+				ConfigType: &core.TransportSocket_TypedConfig{
+					TypedConfig: util.MessageToAny(&tls.UpstreamTlsContext{
+						CommonTlsContext: &tls.CommonTlsContext{
+							TlsParams: &tls.TlsParameters{
+								EcdhCurves:                []string{"X25519"},
+								TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_3,
+								TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
+							},
+							TlsCertificateCertificateProviderInstance: &tls.CommonTlsContext_CertificateProviderInstance{
+								InstanceName:    "instance",
+								CertificateName: "certificate",
+							},
+						},
+					}),
+				},
+			},
 		},
 		{Name: "new-cluster1"},
 		{Name: "new-cluster2"},
diff --git a/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch_test.go b/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch_test.go
index de530c070e..499f070bd6 100644
--- a/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch_test.go
+++ b/pilot/pkg/networking/core/v1alpha3/envoyfilter/listener_patch_test.go
@@ -26,6 +26,7 @@ import (
 	fault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
 	http_conn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
 	redis_proxy "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/redis_proxy/v3"
+	tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	wellknown "github.com/envoyproxy/go-control-plane/pkg/wellknown"
 	"github.com/gogo/protobuf/types"
 	"github.com/golang/protobuf/jsonpb"
@@ -626,6 +627,32 @@ func TestApplyListenerPatches(t *testing.T) {
 }`),
 			},
 		},
+		{
+			ApplyTo: networking.EnvoyFilter_FILTER_CHAIN,
+			Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
+				Context: networking.EnvoyFilter_SIDECAR_OUTBOUND,
+				ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
+					Listener: &networking.EnvoyFilter_ListenerMatch{
+						FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
+							Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
+								Name: "envoy.transport_sockets.tls"},
+						},
+					},
+				},
+			},
+			Patch: &networking.EnvoyFilter_Patch{
+				Operation: networking.EnvoyFilter_Patch_MERGE,
+				Value: buildPatchStruct(`
+					{"transport_socket":{
+						"name":"envoy.transport_sockets.tls",
+						"typed_config":{
+							"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
+							"common_tls_context":{
+								"tls_params":{
+									"tls_maximum_protocol_version":"TLSv1_3",
+									"tls_minimum_protocol_version":"TLSv1_2"}}}}}`),
+			},
+		},
 	}
 
 	sidecarOutboundIn := []*listener.Listener{
@@ -641,6 +668,23 @@ func TestApplyListenerPatches(t *testing.T) {
 				},
 			},
 			FilterChains: []*listener.FilterChain{
+				{
+					FilterChainMatch: &listener.FilterChainMatch{TransportProtocol: "tls"},
+					TransportSocket: &core.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &core.TransportSocket_TypedConfig{
+							TypedConfig: util.MessageToAny(&tls.DownstreamTlsContext{
+								CommonTlsContext: &tls.CommonTlsContext{
+									TlsParams: &tls.TlsParameters{
+										EcdhCurves:                []string{"X25519"},
+										TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_1,
+									},
+								},
+							}),
+						},
+					},
+					Filters: []*listener.Filter{{Name: "envoy.transport_sockets.tls"}},
+				},
 				{
 					Filters: []*listener.Filter{
 						{Name: "filter1"},
@@ -781,6 +825,24 @@ func TestApplyListenerPatches(t *testing.T) {
 				},
 			},
 			FilterChains: []*listener.FilterChain{
+				{
+					FilterChainMatch: &listener.FilterChainMatch{TransportProtocol: "tls"},
+					TransportSocket: &core.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &core.TransportSocket_TypedConfig{
+							TypedConfig: util.MessageToAny(&tls.DownstreamTlsContext{
+								CommonTlsContext: &tls.CommonTlsContext{
+									TlsParams: &tls.TlsParameters{
+										EcdhCurves:                []string{"X25519"},
+										TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
+										TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
+									},
+								},
+							}),
+						},
+					},
+					Filters: []*listener.Filter{{Name: "envoy.transport_sockets.tls"}},
+				},
 				{
 					Filters: []*listener.Filter{
 						{Name: "filter0"},
@@ -934,6 +996,16 @@ func TestApplyListenerPatches(t *testing.T) {
 				},
 			},
 			FilterChains: []*listener.FilterChain{
+				{
+					FilterChainMatch: &listener.FilterChainMatch{TransportProtocol: "tls"},
+					TransportSocket: &core.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &core.TransportSocket_TypedConfig{
+							TypedConfig: util.MessageToAny(&tls.DownstreamTlsContext{}),
+						},
+					},
+					Filters: []*listener.Filter{{Name: "envoy.transport_sockets.tls"}},
+				},
 				{
 					Filters: []*listener.Filter{
 						{Name: "filter1"},
@@ -960,6 +1032,23 @@ func TestApplyListenerPatches(t *testing.T) {
 				},
 			},
 			FilterChains: []*listener.FilterChain{
+				{
+					FilterChainMatch: &listener.FilterChainMatch{TransportProtocol: "tls"},
+					TransportSocket: &core.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &core.TransportSocket_TypedConfig{
+							TypedConfig: util.MessageToAny(&tls.DownstreamTlsContext{
+								CommonTlsContext: &tls.CommonTlsContext{
+									TlsParams: &tls.TlsParameters{
+										TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
+										TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
+									},
+								},
+							}),
+						},
+					},
+					Filters: []*listener.Filter{{Name: "envoy.transport_sockets.tls"}},
+				},
 				{
 					Filters: []*listener.Filter{
 						{Name: "filter0"},
diff --git a/pkg/test/framework/features/features.yaml b/pkg/test/framework/features/features.yaml
index fc6d777202..f918192472 100644
--- a/pkg/test/framework/features/features.yaml
+++ b/pkg/test/framework/features/features.yaml
@@ -129,6 +129,7 @@ features:
       uninstall_purge:
       postinstall_verify:
       revision_centric_view:
+      revision_tags:
     multicluster:
       multimaster:
       remote:
diff --git a/tests/integration/pilot/revisions/revision_tag_test.go b/tests/integration/pilot/revisions/revision_tag_test.go
new file mode 100644
index 0000000000..41ad745d3e
--- /dev/null
+++ b/tests/integration/pilot/revisions/revision_tag_test.go
@@ -0,0 +1,115 @@
+// +build integ
+// Copyright Istio Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package revisions
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"istio.io/api/label"
+	"istio.io/istio/pkg/test/env"
+	"istio.io/istio/pkg/test/framework"
+	"istio.io/istio/pkg/test/framework/components/echo"
+	"istio.io/istio/pkg/test/framework/components/echo/echoboot"
+	"istio.io/istio/pkg/test/framework/components/istioctl"
+	"istio.io/istio/pkg/test/framework/components/namespace"
+	kubetest "istio.io/istio/pkg/test/kube"
+)
+
+func TestRevisionTags(t *testing.T) {
+	framework.NewTest(t).
+		Features("installation.istioctl.revision_tags").
+		RequiresSingleCluster().
+		Run(func(ctx framework.TestContext) {
+			tcs := []struct {
+				name     string
+				tag      string
+				revision string
+				error    string
+			}{
+				{
+					"prod-tag-pointed-to-stable",
+					"prod",
+					"stable",
+					"",
+				},
+				{
+					"prod-tag-pointed-to-canary",
+					"prod",
+					"canary",
+					"",
+				},
+				{
+					"tag-pointed-to-non-existent-revision",
+					"prod",
+					"fake-revision",
+					"cannot modify tag",
+				},
+			}
+
+			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{Cluster: ctx.Clusters().Default()})
+			baseArgs := []string{"experimental", "tag"}
+			for _, tc := range tcs {
+				ctx.NewSubTest(tc.name).Run(func(ctx framework.TestContext) {
+					tagSetArgs := append(baseArgs, "set", tc.tag, "--revision", tc.revision)
+					tagSetArgs = append(tagSetArgs, "--manifests", filepath.Join(env.IstioSrc, "manifests"))
+					tagRemoveArgs := append(baseArgs, "remove", tc.tag, "-y")
+
+					_, cmdErr, _ := istioCtl.Invoke(tagSetArgs)
+					ctx.Cleanup(func() {
+						_, _, _ = istioCtl.Invoke(tagRemoveArgs)
+					})
+
+					if tc.error == "" && cmdErr != "" {
+						ctx.Fatalf("did not expect error, got %q", cmdErr)
+					}
+					if tc.error != "" {
+						if !strings.Contains(cmdErr, tc.error) {
+							ctx.Fatalf("expected error to contain %q, got %q", tc.error, cmdErr)
+						}
+						// found correct error, don't proceed
+						return
+					}
+
+					// build namespace labeled with tag and create echo in that namespace
+					revTagNs := namespace.NewOrFail(t, ctx, namespace.Config{
+						Prefix:   "rev-tag",
+						Inject:   true,
+						Revision: tc.tag,
+					})
+					echoboot.NewBuilder(ctx).WithConfig(echo.Config{
+						Service:   "rev-tag",
+						Namespace: revTagNs,
+					}).BuildOrFail(ctx)
+
+					fetch := kubetest.NewSinglePodFetch(ctx.Clusters().Default(),
+						revTagNs.Name(),
+						fmt.Sprintf("app=%s", "rev-tag"))
+					pods, err := fetch()
+					if err != nil {
+						ctx.Fatalf("error fetching pods: %v", err)
+					}
+
+					injectedRevision := pods[0].GetLabels()[label.IoIstioRev.Name]
+					if injectedRevision != tc.revision {
+						ctx.Fatalf("expected revision tag %q, got %q", tc.revision, injectedRevision)
+					}
+				})
+			}
+		})
+}