Repository: linkerd/linkerd2
Pull request #4528 (2020-06-01 22:58:20)

multicluster: Use the proxy as an HTTP gateway
This change introduces a new annotation,
config.linkerd.io/enable-gateway, that, when set, enables the proxy to
act as a gateway, routing all traffic targetting the inbound listener
through the outbound proxy.

This also removes the nginx default listener and gateway port of 4180,
instead using 4143 (the inbound port).

This includes an update to the proxy version v2.99.0.

The proxy can now operate as gateway, routing requests from its inbound
proxy to the outbound proxy, without passing the requests to a local
application. This supports Linkerd's multicluster feature by adding a
`Forwarded` header to propagate the original client identity and assist
in loop detection.

---

* Add loop detection to inbound & TCP forwarding (linkerd/linkerd2-proxy#527)
* Test loop detection (linkerd/linkerd2-proxy#532)
* fallback: Unwrap errors recursively (linkerd/linkerd2-proxy#534)
* app: Split inbound/outbound constructors into components (linkerd/linkerd2-proxy#533)
* Introduce a gateway between inbound and outbound (linkerd/linkerd2-proxy#540)
* gateway: Add a Forwarded header (linkerd/linkerd2-proxy#544)
* gateway: Return errors instead of responses (linkerd/linkerd2-proxy#547)
* Fail requests that loop through the gateway (linkerd/linkerd2-proxy#545)

zaharidichev: I verified that works with the k3d script
alpeb: Note that the merge brought the `"linkerd-gateway-probe": must be no more than 15 characters` problem

Patch:
diff --git a/.proxy-version b/.proxy-version
index 59b976d01..5ea2ba0d8 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.98.0
+v2.100.0
diff --git a/bin/docker-build-proxy b/bin/docker-build-proxy
index 5b4394e2d..90c56d735 100755
--- a/bin/docker-build-proxy
+++ b/bin/docker-build-proxy
@@ -28,10 +28,12 @@ get_extra_options() {
     for var in http_proxy https_proxy no_proxy; do
         [ -z "${!var:-}" ] || options="$options --build-arg '$var=${!var}'"
     done
-    echo $options
+    echo "$options"
 }
 
 tag=$(head_root_tag)
+# We want wordsplit for the extra options here:
+# shellcheck disable=SC2046
 docker_build proxy "$tag" "$dockerfile" \
   --build-arg "LINKERD_VERSION=$tag" \
   $(get_extra_options)
diff --git a/bin/lint b/bin/lint
index 79837ac99..25b0ae2c1 100755
--- a/bin/lint
+++ b/bin/lint
@@ -13,7 +13,10 @@ targetbin=$rootdir/target/bin
 cd "$rootdir"
 
 exe=
-if [ "$(uname -o)" = Msys ]; then
+if [ "$(uname -s)" = Darwin ]; then
+  # Darwin's uname doesn't support the -o flag so we short circuit here.
+  :;
+elif [ "$(uname -o)" = Msys ]; then
   exe=.exe
 fi
 
diff --git a/bin/shellcheck-all b/bin/shellcheck-all
index cec07eaf0..749f5823e 100755
--- a/bin/shellcheck-all
+++ b/bin/shellcheck-all
@@ -13,10 +13,6 @@ rootdir=$( cd "$bindir"/.. && pwd )
 # We want the word splitting for the shellcheck arguments
 # shellcheck disable=SC2046
 "$bindir"/shellcheck -x -P "$bindir" $(find "$rootdir" -type f \
-		! -path "$bindir"/docker-build-proxy \
-		! -path "$bindir"/_log.sh \
 		! -path "$bindir"/test-cleanup \
-		! -path "$bindir"/_test-run.sh \
-		! -path "$rootdir"/cni-plugin/deployment/scripts/install-cni.sh \
 		! -path "$rootdir"/.git/hooks/\*.sample \
 		| while read -r f; do [ "$(file -b --mime-type "$f")" = 'text/x-shellscript' ] && printf '%s\0' "$f"; done | xargs -0)
diff --git a/bin/update-codegen.sh b/bin/update-codegen.sh
index 2bb7d74f4..f498b4d60 100755
--- a/bin/update-codegen.sh
+++ b/bin/update-codegen.sh
@@ -21,11 +21,11 @@ rm -rf "${GOPATH}/src/${ROOT_PACKAGE}/controller/gen"
 chmod +x "${codegen_pkg}/generate-groups.sh"
 
 # run the code-generator entrypoint script
-GO111MODULE="on" "${codegen_pkg}/generate-groups.sh" \
-  "deepcopy,client,informer,lister" \
+GO111MODULE='on' "${codegen_pkg}/generate-groups.sh" \
+  'deepcopy,client,informer,lister' \
   "${ROOT_PACKAGE}/controller/gen/client" \
   "${ROOT_PACKAGE}/controller/gen/apis" \
   "${CUSTOM_RESOURCE_NAME}:${CUSTOM_RESOURCE_VERSION}"
 
 # copy generated code out of GOPATH
-cp -R "${GOPATH}/src/${ROOT_PACKAGE}/controller/gen" "controller/"
+cp -R "${GOPATH}/src/${ROOT_PACKAGE}/controller/gen" 'controller/'
diff --git a/charts/linkerd2-multicluster/README.md b/charts/linkerd2-multicluster/README.md
index f817df5b8..4770c764d 100644
--- a/charts/linkerd2-multicluster/README.md
+++ b/charts/linkerd2-multicluster/README.md
@@ -22,7 +22,7 @@ linkerd2-multicluster chart and their default values.
 |`gatewayName`                    | The name of the gateway that will be installed                                              |`linkerd-gateway`                             |
 |`gatewayNginxImage`              | The Nginx image                                                                             |`nginx`                                       |
 |`gatewayNginxImageVersion`       | The version of the Nginx image                                                              |`1.17`                                        |
-|`gatewayPort`                    | The port on which all the gateway will accept incoming traffic                              |`4180`                                        |
+|`gatewayPort`                    | The port on which all the gateway will accept incoming traffic                              |`4143`                                        |
 |`gatewayProbePath`               | The path that will be used by remote clusters for determining whether the gateway is alive  |`/health`                                     |
 |`gatewayProbePort`               | The port used for liveliness probing                                                        |`4181`                                        |
 |`gatewayProbeSeconds`            | The interval (in seconds) between liveness probes                                           |`3`                                           |
diff --git a/charts/linkerd2-multicluster/templates/gateway.yaml b/charts/linkerd2-multicluster/templates/gateway.yaml
index 7932f1116..14c894acb 100644
--- a/charts/linkerd2-multicluster/templates/gateway.yaml
+++ b/charts/linkerd2-multicluster/templates/gateway.yaml
@@ -11,12 +11,12 @@ data:
   nginx.conf: |-
     events {
     }
-    stream {
-      server {
-          listen     {{.Values.gatewayPort}};
-          proxy_pass 127.0.0.1:{{.Values.proxyOutboundPort}};
-      }
-    }
+    stream {                                                                                                                                                                                  
+       server {                                                                                                                                                                                
+           listen     4180;                                                                                                                                                 
+           proxy_pass 127.0.0.1:{{.Values.proxyOutboundPort}};                                                                                                                                 
+       }                                                                                                                                                                                       
+    } 
     http {
       server {
           listen     {{.Values.gatewayProbePort}};
@@ -56,7 +56,8 @@ spec:
       annotations:
         {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}        
         linkerd.io/inject: enabled
-        config.linkerd.io/proxy-require-identity-inbound-ports: "{{.Values.gatewayProbePort}},{{.Values.gatewayPort}}"
+        config.linkerd.io/proxy-require-identity-inbound-ports: "{{.Values.gatewayProbePort}},{{.Values.gatewayPort}},4180"
+        config.linkerd.io/enable-gateway: "true"
       labels:
         app: {{.Values.gatewayName}}
     spec:
@@ -78,9 +79,9 @@ spec:
             initialDelaySeconds: 10
           image: {{.Values.gatewayNginxImage}}:{{.Values.gatewayNginxImageVersion}}
           ports:
-            - name: linkerd-gateway
-              containerPort: {{.Values.gatewayPort}}
-            - name: linkerd-gateway-probe
+            - name: mc-relay
+              containerPort: 4180
+            - name: mc-probe
               containerPort: {{.Values.gatewayProbePort}}
             - name: local-probe
               containerPort: {{.Values.gatewayLocalProbePort}}              
@@ -102,10 +103,10 @@ metadata:
     {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
 spec:
   ports:
-  - name: linkerd-gateway
+  - name: mc-gateway
     port: {{.Values.gatewayPort}}    
     protocol: TCP
-  - name: linkerd-gateway-probe
+  - name: mc-probe
     port: {{.Values.gatewayProbePort}}    
     protocol: TCP
   selector:
diff --git a/charts/linkerd2-multicluster/values.yaml b/charts/linkerd2-multicluster/values.yaml
index 6fcfe6c1b..edf82a2f4 100644
--- a/charts/linkerd2-multicluster/values.yaml
+++ b/charts/linkerd2-multicluster/values.yaml
@@ -8,7 +8,7 @@ gatewayLocalProbePort: 8888
 gatewayName: linkerd-gateway
 gatewayNginxImage: nginx
 gatewayNginxImageVersion: 1.17
-gatewayPort: 4180
+gatewayPort: 4143
 gatewayProbePath: /health
 gatewayProbePort: 4181
 gatewayProbeSeconds: 3
diff --git a/charts/partials/templates/_proxy.tpl b/charts/partials/templates/_proxy.tpl
index 31fc83eb9..7203f1b82 100644
--- a/charts/partials/templates/_proxy.tpl
+++ b/charts/partials/templates/_proxy.tpl
@@ -18,12 +18,15 @@ env:
   value: 127.0.0.1:{{.Values.global.proxy.ports.outbound}}
 - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
   value: 0.0.0.0:{{.Values.global.proxy.ports.inbound}}
+{{ if .Values.global.proxy.isGateway -}}
+- name: LINKERD2_PROXY_INBOUND_GATEWAY_SUFFIXES
+  value: {{printf "svc.%s." .Values.global.clusterDomain}}
+{{ end -}}  
 - name: LINKERD2_PROXY_DESTINATION_GET_SUFFIXES
-  {{- $internalProfileSuffix := printf "svc.%s." .Values.global.clusterDomain }}
-  value: {{ternary "." $internalProfileSuffix .Values.global.proxy.enableExternalProfiles}}
+  value: {{printf "svc.%s." .Values.global.clusterDomain}}
 - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
-  {{- $internalProfileSuffix := printf "svc.%s." .Values.global.clusterDomain }}
-  value: {{ternary "." $internalProfileSuffix .Values.global.proxy.enableExternalProfiles}}
+  {{- $internalDomain := printf "svc.%s." .Values.global.clusterDomain }}
+  value: {{ternary "." $internalDomain .Values.global.proxy.enableExternalProfiles}}
 - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE
   value: 10000ms
 - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
diff --git a/cli/cmd/multicluster.go b/cli/cmd/multicluster.go
index a801a2d7a..f6a50b976 100644
--- a/cli/cmd/multicluster.go
+++ b/cli/cmd/multicluster.go
@@ -151,7 +151,7 @@ func buildMulticlusterInstallValues(opts *multiclusterInstallOptions) (*multiclu
 	}
 
 	if opts.gatewayProbePort == defaults.GatewayLocalProbePort {
-		return nil, fmt.Errorf("The probe port needs to be different from %d which is the local probe port", opts.gatewayProbePort)
+		return nil, fmt.Errorf("The probe port needs to be different from %d which is the multicluster probe port", opts.gatewayProbePort)
 	}
 
 	defaults.Namespace = opts.namespace
@@ -269,7 +269,7 @@ func newAllowCommand() *cobra.Command {
 
 	cmd.Flags().StringVar(&opts.namespace, "namespace", defaultMulticlusterNamespace, "The destination namespace for the service account.")
 	cmd.Flags().BoolVar(&opts.ignoreCluster, "ignore-cluster", false, "Ignore cluster configuration")
-	cmd.Flags().StringVar(&opts.serviceAccountName, "service-account-name", "", "The name of the remote access service account")
+	cmd.Flags().StringVar(&opts.serviceAccountName, "service-account-name", "", "The name of the multicluster access service account")
 
 	return cmd
 }
@@ -280,7 +280,7 @@ func newGatewaysCommand() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "gateways",
-		Short: "Display stats information about the remote gateways",
+		Short: "Display stats information about the gateways in target clusters",
 		Args:  cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			req := &pb.GatewaysRequest{
@@ -300,8 +300,8 @@ func newGatewaysCommand() *cobra.Command {
 		},
 	}
 
-	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "", "the name of the remote cluster")
-	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", "", "the namespace in which the gateway resides on the remote cluster")
+	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "", "the name of the target cluster")
+	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", "", "the namespace in which the gateway resides on the target cluster")
 	cmd.Flags().StringVarP(&opts.timeWindow, "time-window", "t", "1m", "Time window (for example: \"15s\", \"1m\", \"10m\", \"1h\"). Needs to be at least 15s.")
 
 	return cmd
@@ -364,13 +364,13 @@ func newMulticlusterInstallCommand() *cobra.Command {
 	cmd.Flags().Uint32Var(&options.gatewayProbeSeconds, "gateway-probe-seconds", options.gatewayProbeSeconds, "The interval at which the gateway will be checked for being alive in seconds")
 	cmd.Flags().Uint32Var(&options.gatewayProbePort, "gateway-probe-port", options.gatewayProbePort, "The liveness check port of the gateway")
 	cmd.Flags().BoolVar(&options.serviceMirror, "service-mirror", options.serviceMirror, "If the service-mirror component should be installed")
-	cmd.Flags().Uint32Var(&options.serviceMirrorRetryLimit, "service-mirror-retry-limit", options.serviceMirrorRetryLimit, "The number of times a failed update from the remote cluster is allowed to be retried")
+	cmd.Flags().Uint32Var(&options.serviceMirrorRetryLimit, "service-mirror-retry-limit", options.serviceMirrorRetryLimit, "The number of times a failed update from the target cluster is allowed to be retried")
 	cmd.Flags().StringVar(&options.logLevel, "log-level", options.logLevel, "Log level for the Multicluster components")
 	cmd.Flags().StringVar(&options.gatewayNginxImage, "gateway-nginx-image", options.gatewayNginxImage, "The nginx image to be used")
 	cmd.Flags().StringVar(&options.gatewayNginxVersion, "gateway-nginx-image-version", options.gatewayNginxVersion, "The version of nginx to be used")
 	cmd.Flags().StringVarP(&options.controlPlaneVersion, "control-plane-version", "", options.controlPlaneVersion, "(Development) Tag to be used for the control plane component images")
 	cmd.Flags().StringVar(&options.dockerRegistry, "registry", options.dockerRegistry, "Docker registry to pull images from")
-	cmd.Flags().BoolVar(&options.remoteMirrorCredentials, "remote-mirror-credentials", options.remoteMirrorCredentials, "Whether to install the default remote access service account")
+	cmd.Flags().BoolVar(&options.remoteMirrorCredentials, "service-mirror-credentials", options.remoteMirrorCredentials, "Whether to install the service account which can be used by service mirror components in source clusters to discover exported servivces")
 
 	// Hide developer focused flags in release builds.
 	release, err := version.IsReleaseChannel(version.Version)
@@ -707,7 +707,7 @@ func newExportServiceCommand() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "export-service",
-		Short: "Exposes a remote service to be mirrored",
+		Short: "Exposes a service to be mirrored",
 		RunE: func(cmd *cobra.Command, args []string) error {
 
 			if len(args) < 1 {
@@ -754,7 +754,7 @@ components on a cluster, manage credentials and link clusters together.`,
   linkerd --context=cluster-a cluster install | kubectl --context=cluster-a apply -f -
 
   # Extract mirroring cluster credentials from cluster A and install them on cluster B
-  linkerd --context=cluster-a cluster link --cluster-name=remote | kubectl apply --context=cluster-b -f -
+  linkerd --context=cluster-a cluster link --cluster-name=target | kubectl apply --context=cluster-b -f -
 
   # Export services from cluster to be available to other clusters
   kubectl get svc -o yaml | linkerd export-service - | kubectl apply -f -
diff --git a/cni-plugin/deployment/scripts/install-cni.sh b/cni-plugin/deployment/scripts/install-cni.sh
index a31e608f8..c1ecf2206 100755
--- a/cni-plugin/deployment/scripts/install-cni.sh
+++ b/cni-plugin/deployment/scripts/install-cni.sh
@@ -130,7 +130,7 @@ if [ -f "${SERVICE_ACCOUNT_PATH}/token" ]; then
     echo 'KUBERNETES_SERVICE_PORT not set'; exit 1;
   fi
 
-  if [ "${SKIP_TLS_VERIFY}" = "true" ]; then
+  if [ "${SKIP_TLS_VERIFY}" = 'true' ]; then
     TLS_CFG='insecure-skip-tls-verify: true'
   elif [ -f "${KUBE_CA_FILE}" ]; then
     TLS_CFG="certificate-authority-data: $(base64 "${KUBE_CA_FILE}" | tr -d '\n')"
@@ -166,8 +166,8 @@ EOF
 fi
 
 # Insert any of the supported "auto" parameters.
-grep "__KUBERNETES_SERVICE_HOST__" ${TMP_CONF} && sed -i s/__KUBERNETES_SERVICE_HOST__/"${KUBERNETES_SERVICE_HOST}"/g ${TMP_CONF}
-grep "__KUBERNETES_SERVICE_PORT__" ${TMP_CONF} && sed -i s/__KUBERNETES_SERVICE_PORT__/"${KUBERNETES_SERVICE_PORT}"/g ${TMP_CONF}
+grep '__KUBERNETES_SERVICE_HOST__' ${TMP_CONF} && sed -i s/__KUBERNETES_SERVICE_HOST__/"${KUBERNETES_SERVICE_HOST}"/g ${TMP_CONF}
+grep '__KUBERNETES_SERVICE_PORT__' ${TMP_CONF} && sed -i s/__KUBERNETES_SERVICE_PORT__/"${KUBERNETES_SERVICE_PORT}"/g ${TMP_CONF}
 sed -i s/__KUBERNETES_NODE_NAME__/"${KUBERNETES_NODE_NAME:-$(hostname)}"/g ${TMP_CONF}
 sed -i s/__KUBECONFIG_FILENAME__/"${KUBECONFIG_FILE_NAME}"/g ${TMP_CONF}
 sed -i s/__CNI_MTU__/"${CNI_MTU:-1500}"/g ${TMP_CONF}
@@ -192,9 +192,9 @@ if [ -e "${CNI_CONF_FILE}" ]; then
 fi
 
 # If the old config filename ends with .conf, rename it to .conflist, because it has changed to be a list
-filename=$(basename -- "${CNI_CONF_PATH}")
+filename=${CNI_CONF_PATH##*/}
 extension="${filename##*.}"
-if [ "${filename}" != "01-linkerd-cni.conf" ] && [ "${extension}" = "conf" ]; then
+if [ "${filename}" != '01-linkerd-cni.conf' ] && [ "${extension}" = 'conf' ]; then
   echo "Renaming ${CNI_CONF_PATH} extension to .conflist"
   CNI_CONF_PATH="${CNI_CONF_PATH}list"
 fi
@@ -214,7 +214,7 @@ echo "Created CNI config ${CNI_CONF_PATH}"
 # This prevents Kubernetes from restarting the pod repeatedly.
 should_sleep=${SLEEP:-"true"}
 echo "Done configuring CNI. Sleep=$should_sleep"
-while [ "${should_sleep}" = "true"  ]; do
+while [ "${should_sleep}" = 'true'  ]; do
   sleep infinity &
   wait $!
 done
diff --git a/controller/cmd/service-mirror/cluster_watcher.go b/controller/cmd/service-mirror/cluster_watcher.go
index dcc2807ee..b03e5fa09 100644
--- a/controller/cmd/service-mirror/cluster_watcher.go
+++ b/controller/cmd/service-mirror/cluster_watcher.go
@@ -175,7 +175,7 @@ func NewRemoteClusterServiceWatcher(
 ) (*RemoteClusterServiceWatcher, error) {
 	remoteAPI, err := k8s.InitializeAPIForConfig(cfg, false, k8s.Svc)
 	if err != nil {
-		return nil, fmt.Errorf("cannot initialize remote api for cluster %s: %s", clusterName, err)
+		return nil, fmt.Errorf("cannot initialize api for target cluster %s: %s", clusterName, err)
 	}
 	stopper := make(chan struct{})
 	return &RemoteClusterServiceWatcher{
@@ -271,7 +271,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupOrphanedServices() error {
 
 	servicesOnLocalCluster, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
 	if err != nil {
-		innerErr := fmt.Errorf("failed obtaining local services while GC-ing: %s", err)
+		innerErr := fmt.Errorf("failed to list services while cleaning up mirror services: %s", err)
 		if kerrors.IsNotFound(err) {
 			return innerErr
 		}
@@ -390,7 +390,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ev *RemoteSe
 // new gateway being assigned or additional ports exposed. This method takes care of that.
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUpdated(ev *RemoteServiceUpdated) error {
 	serviceInfo := fmt.Sprintf("%s/%s", ev.remoteUpdate.Namespace, ev.remoteUpdate.Name)
-	rcsw.log.Debugf("Updating remote mirrored service %s/%s", ev.localService.Namespace, ev.localService.Name)
+	rcsw.log.Debugf("Updating mirror service %s/%s", ev.localService.Namespace, ev.localService.Name)
 
 	gatewaySpec, err := rcsw.resolveGateway(&ev.gatewayData)
 	copiedEndpoints := ev.localEndpoints.DeepCopy()
@@ -490,7 +490,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteSe
 	gatewaySpec, err := rcsw.resolveGateway(&ev.gatewayData)
 	if err == nil {
 		// only if we resolve it, we are updating the endpoints addresses and ports
-		rcsw.log.Debugf("Resolved remote gateway [%v:%d] for %s", gatewaySpec.addresses, gatewaySpec.incomingPort, serviceInfo)
+		rcsw.log.Debugf("Resolved gateway [%v:%d] for %s", gatewaySpec.addresses, gatewaySpec.incomingPort, serviceInfo)
 		endpointsToCreate.Subsets = []corev1.EndpointSubset{
 			{
 				Addresses: gatewaySpec.addresses,
@@ -543,7 +543,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayDeleted(ev *RemoteGa
 
 	var errors []error
 	if len(affectedEndpoints) > 0 {
-		rcsw.log.Debugf("Nulling %d endpoints due to remote gateway [%s/%s] deletion", len(affectedEndpoints), ev.gatewayData.Namespace, ev.gatewayData.Name)
+		rcsw.log.Debugf("Nulling %d endpoints due to gateway [%s/%s] deletion", len(affectedEndpoints), ev.gatewayData.Namespace, ev.gatewayData.Name)
 		for _, ep := range affectedEndpoints {
 			updated := ep.DeepCopy()
 			updated.Subsets = nil
@@ -645,7 +645,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayCreated(event *Remot
 }
 
 func (rcsw *RemoteClusterServiceWatcher) updateAffectedServices(gatewaySpec GatewaySpec, affectedServices []*corev1.Service) error {
-	rcsw.log.Debugf("Updating %d services due to remote gateway [%s/%s] update", len(affectedServices), gatewaySpec.gatewayNamespace, gatewaySpec.gatewayName)
+	rcsw.log.Debugf("Updating %d services due to gateway [%s/%s] update", len(affectedServices), gatewaySpec.gatewayNamespace, gatewaySpec.gatewayName)
 	var errors []error
 	for _, svc := range affectedServices {
 		updatedService := svc.DeepCopy()
diff --git a/controller/cmd/service-mirror/config_watcher.go b/controller/cmd/service-mirror/config_watcher.go
index 2aabb1d2d..3d9f9283b 100644
--- a/controller/cmd/service-mirror/config_watcher.go
+++ b/controller/cmd/service-mirror/config_watcher.go
@@ -53,7 +53,7 @@ func NewRemoteClusterConfigWatcher(serviceMirrorNamespace string, secretsInforme
 				AddFunc: func(obj interface{}) {
 					secret := obj.(*corev1.Secret)
 					if err := rcw.registerRemoteCluster(secret); err != nil {
-						log.Errorf("Cannot register remote cluster: %s", err)
+						log.Errorf("Cannot register target cluster: %s", err)
 					}
 				},
 				DeleteFunc: func(obj interface{}) {
@@ -71,7 +71,7 @@ func NewRemoteClusterConfigWatcher(serviceMirrorNamespace string, secretsInforme
 						}
 					}
 					if err := rcw.unregisterRemoteCluster(secret, true); err != nil {
-						log.Errorf("Cannot unregister remote cluster: %s", err)
+						log.Errorf("Cannot unregister target cluster: %s", err)
 					}
 				},
 				UpdateFunc: func(old, new interface{}) {
@@ -80,12 +80,12 @@ func NewRemoteClusterConfigWatcher(serviceMirrorNamespace string, secretsInforme
 
 					if oldSecret.ResourceVersion != newSecret.ResourceVersion {
 						if err := rcw.unregisterRemoteCluster(oldSecret, false); err != nil {
-							log.Errorf("Cannot unregister remote cluster: %s", err)
+							log.Errorf("Cannot unregister target cluster: %s", err)
 							return
 						}
 
 						if err := rcw.registerRemoteCluster(newSecret); err != nil {
-							log.Errorf("Cannot register remote cluster: %s", err)
+							log.Errorf("Cannot register target cluster: %s", err)
 						}
 
 					}
diff --git a/controller/proxy-injector/fake/data/inject-sidecar-container-spec.yaml b/controller/proxy-injector/fake/data/inject-sidecar-container-spec.yaml
index b136c844d..9ed7d5aaf 100644
--- a/controller/proxy-injector/fake/data/inject-sidecar-container-spec.yaml
+++ b/controller/proxy-injector/fake/data/inject-sidecar-container-spec.yaml
@@ -12,7 +12,7 @@ env:
     - name: LINKERD2_PROXY_INBOUND_LISTENER
       value: tcp://0.0.0.0:4143
     - name: LINKERD2_PROXY_DESTINATION_GET_SUFFIXES
-      value: .
+      value: svc.cluster.local.
     - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
       value: .
     - name: LINKERD2_PROXY_POD_NAMESPACE
diff --git a/controller/proxy-injector/fake/data/pod-with-debug.patch.json b/controller/proxy-injector/fake/data/pod-with-debug.patch.json
index 03f4133c9..bdf69f886 100644
--- a/controller/proxy-injector/fake/data/pod-with-debug.patch.json
+++ b/controller/proxy-injector/fake/data/pod-with-debug.patch.json
@@ -32,57 +32,55 @@
   {
     "op": "add",
     "path": "/spec/initContainers/-",
-    "value":
-      {
-        "args": [
-          "--incoming-proxy-port",
-          "4143",
-          "--outgoing-proxy-port",
-          "4140",
-          "--proxy-uid",
-          "2102",
-          "--inbound-ports-to-ignore",
-          "4190,4191"
-        ],
-        "image": "gcr.io/linkerd-io/proxy-init:v1.3.3",
-        "imagePullPolicy": "IfNotPresent",
-        "name": "linkerd-init",
-        "resources": {
-          "limits": {
-            "cpu": "100m",
-            "memory": "50Mi"
-          },
-          "requests": {
-            "cpu": "10m",
-            "memory": "10Mi"
-          }
+    "value": {
+      "args": [
+        "--incoming-proxy-port",
+        "4143",
+        "--outgoing-proxy-port",
+        "4140",
+        "--proxy-uid",
+        "2102",
+        "--inbound-ports-to-ignore",
+        "4190,4191"
+      ],
+      "image": "gcr.io/linkerd-io/proxy-init:v1.3.3",
+      "imagePullPolicy": "IfNotPresent",
+      "name": "linkerd-init",
+      "resources": {
+        "limits": {
+          "cpu": "100m",
+          "memory": "50Mi"
+        },
+        "requests": {
+          "cpu": "10m",
+          "memory": "10Mi"
+        }
+      },
+      "securityContext": {
+        "allowPrivilegeEscalation": false,
+        "capabilities": {
+          "add": [
+            "NET_ADMIN",
+            "NET_RAW"
+          ]
         },
-        "securityContext": {
-          "allowPrivilegeEscalation": false,
-          "capabilities": {
-            "add": [
-              "NET_ADMIN",
-              "NET_RAW"
-            ]
-          },
-          "privileged": false,
-          "readOnlyRootFilesystem": true,
-          "runAsNonRoot": false,
-          "runAsUser": 0
-        },
-        "terminationMessagePolicy": "FallbackToLogsOnError"
-      }
+        "privileged": false,
+        "readOnlyRootFilesystem": true,
+        "runAsNonRoot": false,
+        "runAsUser": 0
+      },
+      "terminationMessagePolicy": "FallbackToLogsOnError"
+    }
   },
   {
     "op": "add",
     "path": "/spec/containers/-",
-    "value":
-      {
-        "image": "gcr.io/linkerd-io/debug:debug-image-version",
-        "imagePullPolicy": "IfNotPresent",
-        "name": "linkerd-debug",
-        "terminationMessagePolicy": "FallbackToLogsOnError"
-      }
+    "value": {
+      "image": "gcr.io/linkerd-io/debug:debug-image-version",
+      "imagePullPolicy": "IfNotPresent",
+      "name": "linkerd-debug",
+      "terminationMessagePolicy": "FallbackToLogsOnError"
+    }
   },
   {
     "op": "add",
@@ -92,104 +90,103 @@
   {
     "op": "add",
     "path": "/spec/containers/-",
-    "value":
-      {
-        "env": [
-          {
-            "name": "LINKERD2_PROXY_LOG",
-            "value": "warn,linkerd=info"
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_SVC_ADDR",
-            "value": "linkerd-dst.linkerd.svc.cluster.local:8086"
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_GET_NETWORKS",
-            "value": "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
-          },
-          {
-            "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
-            "value": "0.0.0.0:4190"
-          },
-          {
-            "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
-            "value": "0.0.0.0:4191"
-          },
-          {
-            "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
-            "value": "127.0.0.1:4140"
-          },
-          {
-            "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
-            "value": "0.0.0.0:4143"
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_GET_SUFFIXES",
-            "value": "."
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES",
-            "value": "."
-          },
-          {
-            "name": "LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE",
-            "value": "10000ms"
-          },
-          {
-            "name": "LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE",
-            "value": "10000ms"
-          },
-          {
-            "name": "_pod_ns",
-            "valueFrom": {
-              "fieldRef": {
-                "fieldPath": "metadata.namespace"
-              }
+    "value": {
+      "env": [
+        {
+          "name": "LINKERD2_PROXY_LOG",
+          "value": "warn,linkerd=info"
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_SVC_ADDR",
+          "value": "linkerd-dst.linkerd.svc.cluster.local:8086"
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_GET_NETWORKS",
+          "value": "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
+        },
+        {
+          "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
+          "value": "0.0.0.0:4190"
+        },
+        {
+          "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
+          "value": "0.0.0.0:4191"
+        },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
+          "value": "127.0.0.1:4140"
+        },
+        {
+          "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
+          "value": "0.0.0.0:4143"
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_GET_SUFFIXES",
+          "value": "svc.cluster.local."
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES",
+          "value": "."
+        },
+        {
+          "name": "LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE",
+          "value": "10000ms"
+        },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE",
+          "value": "10000ms"
+        },
+        {
+          "name": "_pod_ns",
+          "valueFrom": {
+            "fieldRef": {
+              "fieldPath": "metadata.namespace"
             }
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_CONTEXT",
-            "value": "ns:$(_pod_ns)"
-          },
-          {
-            "name": "LINKERD2_PROXY_IDENTITY_DISABLED",
-            "value": "disabled"
           }
-        ],
-        "image": "gcr.io/linkerd-io/proxy:dev-undefined",
-        "imagePullPolicy": "IfNotPresent",
-        "livenessProbe": {
-          "httpGet": {
-            "path": "/live",
-            "port": 4191
-          },
-          "initialDelaySeconds": 10
-        },
-        "name": "linkerd-proxy",
-        "ports": [
-          {
-            "containerPort": 4143,
-            "name": "linkerd-proxy"
-          },
-          {
-            "containerPort": 4191,
-            "name": "linkerd-admin"
-          }
-        ],
-        "readinessProbe": {
-          "httpGet": {
-            "path": "/ready",
-            "port": 4191
-          },
-          "initialDelaySeconds": 2
-        },
-        "resources": null,
-        "securityContext": {
-          "allowPrivilegeEscalation": false,
-          "readOnlyRootFilesystem": true,
-          "runAsUser": 2102
-        },
-        "terminationMessagePolicy": "FallbackToLogsOnError"
-      }
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_CONTEXT",
+          "value": "ns:$(_pod_ns)"
+        },
+        {
+          "name": "LINKERD2_PROXY_IDENTITY_DISABLED",
+          "value": "disabled"
+        }
+      ],
+      "image": "gcr.io/linkerd-io/proxy:dev-undefined",
+      "imagePullPolicy": "IfNotPresent",
+      "livenessProbe": {
+        "httpGet": {
+          "path": "/live",
+          "port": 4191
+        },
+        "initialDelaySeconds": 10
+      },
+      "name": "linkerd-proxy",
+      "ports": [
+        {
+          "containerPort": 4143,
+          "name": "linkerd-proxy"
+        },
+        {
+          "containerPort": 4191,
+          "name": "linkerd-admin"
+        }
+      ],
+      "readinessProbe": {
+        "httpGet": {
+          "path": "/ready",
+          "port": 4191
+        },
+        "initialDelaySeconds": 2
+      },
+      "resources": null,
+      "securityContext": {
+        "allowPrivilegeEscalation": false,
+        "readOnlyRootFilesystem": true,
+        "runAsUser": 2102
+      },
+      "terminationMessagePolicy": "FallbackToLogsOnError"
+    }
   }
 ]
diff --git a/controller/proxy-injector/fake/data/pod.patch.json b/controller/proxy-injector/fake/data/pod.patch.json
index 03919b8b6..fcc725dfe 100644
--- a/controller/proxy-injector/fake/data/pod.patch.json
+++ b/controller/proxy-injector/fake/data/pod.patch.json
@@ -32,46 +32,45 @@
   {
     "op": "add",
     "path": "/spec/initContainers/-",
-    "value":
-      {
-        "args": [
-          "--incoming-proxy-port",
-          "4143",
-          "--outgoing-proxy-port",
-          "4140",
-          "--proxy-uid",
-          "2102",
-          "--inbound-ports-to-ignore",
-          "4190,4191"
-        ],
-        "image": "gcr.io/linkerd-io/proxy-init:v1.3.3",
-        "imagePullPolicy": "IfNotPresent",
-        "name": "linkerd-init",
-        "resources": {
-          "limits": {
-            "cpu": "100m",
-            "memory": "50Mi"
-          },
-          "requests": {
-            "cpu": "10m",
-            "memory": "10Mi"
-          }
+    "value": {
+      "args": [
+        "--incoming-proxy-port",
+        "4143",
+        "--outgoing-proxy-port",
+        "4140",
+        "--proxy-uid",
+        "2102",
+        "--inbound-ports-to-ignore",
+        "4190,4191"
+      ],
+      "image": "gcr.io/linkerd-io/proxy-init:v1.3.3",
+      "imagePullPolicy": "IfNotPresent",
+      "name": "linkerd-init",
+      "resources": {
+        "limits": {
+          "cpu": "100m",
+          "memory": "50Mi"
+        },
+        "requests": {
+          "cpu": "10m",
+          "memory": "10Mi"
+        }
+      },
+      "securityContext": {
+        "allowPrivilegeEscalation": false,
+        "capabilities": {
+          "add": [
+            "NET_ADMIN",
+            "NET_RAW"
+          ]
         },
-        "securityContext": {
-          "allowPrivilegeEscalation": false,
-          "capabilities": {
-            "add": [
-              "NET_ADMIN",
-              "NET_RAW"
-            ]
-          },
-          "privileged": false,
-          "readOnlyRootFilesystem": true,
-          "runAsNonRoot": false,
-          "runAsUser": 0
-        },
-        "terminationMessagePolicy": "FallbackToLogsOnError"
-      }
+        "privileged": false,
+        "readOnlyRootFilesystem": true,
+        "runAsNonRoot": false,
+        "runAsUser": 0
+      },
+      "terminationMessagePolicy": "FallbackToLogsOnError"
+    }
   },
   {
     "op": "add",
@@ -81,104 +80,103 @@
   {
     "op": "add",
     "path": "/spec/containers/-",
-    "value":
-      {
-        "env": [
-          {
-            "name": "LINKERD2_PROXY_LOG",
-            "value": "warn,linkerd=info"
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_SVC_ADDR",
-            "value": "linkerd-dst.linkerd.svc.cluster.local:8086"
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_GET_NETWORKS",
-            "value": "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
-          },
-          {
-            "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
-            "value": "0.0.0.0:4190"
-          },
-          {
-            "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
-            "value": "0.0.0.0:4191"
-          },
-          {
-            "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
-            "value": "127.0.0.1:4140"
-          },
-          {
-            "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
-            "value": "0.0.0.0:4143"
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_GET_SUFFIXES",
-            "value": "."
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES",
-            "value": "."
-          },
-          {
-            "name": "LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE",
-            "value": "10000ms"
-          },
-          {
-            "name": "LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE",
-            "value": "10000ms"
-          },
-          {
-            "name": "_pod_ns",
-            "valueFrom": {
-              "fieldRef": {
-                "fieldPath": "metadata.namespace"
-              }
+    "value": {
+      "env": [
+        {
+          "name": "LINKERD2_PROXY_LOG",
+          "value": "warn,linkerd=info"
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_SVC_ADDR",
+          "value": "linkerd-dst.linkerd.svc.cluster.local:8086"
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_GET_NETWORKS",
+          "value": "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
+        },
+        {
+          "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
+          "value": "0.0.0.0:4190"
+        },
+        {
+          "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
+          "value": "0.0.0.0:4191"
+        },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
+          "value": "127.0.0.1:4140"
+        },
+        {
+          "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
+          "value": "0.0.0.0:4143"
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_GET_SUFFIXES",
+          "value": "svc.cluster.local."
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES",
+          "value": "."
+        },
+        {
+          "name": "LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE",
+          "value": "10000ms"
+        },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE",
+          "value": "10000ms"
+        },
+        {
+          "name": "_pod_ns",
+          "valueFrom": {
+            "fieldRef": {
+              "fieldPath": "metadata.namespace"
             }
-          },
-          {
-            "name": "LINKERD2_PROXY_DESTINATION_CONTEXT",
-            "value": "ns:$(_pod_ns)"
-          },
-          {
-            "name": "LINKERD2_PROXY_IDENTITY_DISABLED",
-            "value": "disabled"
           }
-        ],
-        "image": "gcr.io/linkerd-io/proxy:dev-undefined",
-        "imagePullPolicy": "IfNotPresent",
-        "livenessProbe": {
-          "httpGet": {
-            "path": "/live",
-            "port": 4191
-          },
-          "initialDelaySeconds": 10
-        },
-        "name": "linkerd-proxy",
-        "ports": [
-          {
-            "containerPort": 4143,
-            "name": "linkerd-proxy"
-          },
-          {
-            "containerPort": 4191,
-            "name": "linkerd-admin"
-          }
-        ],
-        "readinessProbe": {
-          "httpGet": {
-            "path": "/ready",
-            "port": 4191
-          },
-          "initialDelaySeconds": 2
-        },
-        "resources": null,
-        "securityContext": {
-          "allowPrivilegeEscalation": false,
-          "readOnlyRootFilesystem": true,
-          "runAsUser": 2102
-        },
-        "terminationMessagePolicy": "FallbackToLogsOnError"
-      }
+        },
+        {
+          "name": "LINKERD2_PROXY_DESTINATION_CONTEXT",
+          "value": "ns:$(_pod_ns)"
+        },
+        {
+          "name": "LINKERD2_PROXY_IDENTITY_DISABLED",
+          "value": "disabled"
+        }
+      ],
+      "image": "gcr.io/linkerd-io/proxy:dev-undefined",
+      "imagePullPolicy": "IfNotPresent",
+      "livenessProbe": {
+        "httpGet": {
+          "path": "/live",
+          "port": 4191
+        },
+        "initialDelaySeconds": 10
+      },
+      "name": "linkerd-proxy",
+      "ports": [
+        {
+          "containerPort": 4143,
+          "name": "linkerd-proxy"
+        },
+        {
+          "containerPort": 4191,
+          "name": "linkerd-admin"
+        }
+      ],
+      "readinessProbe": {
+        "httpGet": {
+          "path": "/ready",
+          "port": 4191
+        },
+        "initialDelaySeconds": 2
+      },
+      "resources": null,
+      "securityContext": {
+        "allowPrivilegeEscalation": false,
+        "readOnlyRootFilesystem": true,
+        "runAsUser": 2102
+      },
+      "terminationMessagePolicy": "FallbackToLogsOnError"
+    }
   }
 ]
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index 0b5471b09..0af09c6e3 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -112,6 +112,7 @@ type (
 		Trace                         *Trace        `json:"trace"`
 		UID                           int64         `json:"uid"`
 		WaitBeforeExitSeconds         uint64        `json:"waitBeforeExitSeconds"`
+		IsGateway                     bool          `json:"isGateway"`
 		RequireIdentityOnInboundPorts string        `json:"requireIdentityOnInboundPorts"`
 	}
 
diff --git a/pkg/healthcheck/healthcheck_multicluster.go b/pkg/healthcheck/healthcheck_multicluster.go
index cc208b322..c005cbdfa 100644
--- a/pkg/healthcheck/healthcheck_multicluster.go
+++ b/pkg/healthcheck/healthcheck_multicluster.go
@@ -118,21 +118,21 @@ func (hc *HealthChecker) multiClusterCategory() category {
 			},
 			{
 				description: "service mirror controller has required permissions",
-				hintAnchor:  "l5d-multicluster-local-rbac-correct",
+				hintAnchor:  "l5d-multicluster-source-rbac-correct",
 				check: func(context.Context) error {
 					return hc.checkServiceMirrorLocalRBAC()
 				},
 			},
 			{
-				description: "service mirror controller can access remote clusters",
-				hintAnchor:  "l5d-smc-remote-remote-clusters-access",
+				description: "service mirror controller can access target clusters",
+				hintAnchor:  "l5d-smc-target-clusters-access",
 				check: func(context.Context) error {
 					return hc.checkRemoteClusterConnectivity()
 				},
 			},
 			{
-				description: "all remote cluster gateways are alive",
-				hintAnchor:  "l5d-multicluster-remote-gateways-alive",
+				description: "all target cluster gateways are alive",
+				hintAnchor:  "l5d-multicluster-target-gateways-alive",
 				check: func(ctx context.Context) error {
 					return hc.checkRemoteClusterGatewaysHealth(ctx)
 				},
@@ -267,12 +267,12 @@ func (hc *HealthChecker) checkServiceMirrorLocalRBAC() error {
 
 func (hc *HealthChecker) checkRemoteClusterAnchors() error {
 	if len(hc.remoteClusterConfigs) == 0 {
-		return &SkipError{Reason: "no remote cluster configs"}
+		return &SkipError{Reason: "no target cluster configs"}
 	}
 
 	localAnchors, err := tls.DecodePEMCertificates(hc.linkerdConfig.Global.IdentityContext.TrustAnchorsPem)
 	if err != nil {
-		return fmt.Errorf("Cannot parse local trust anchors: %s", err)
+		return fmt.Errorf("Cannot parse source trust anchors: %s", err)
 	}
 
 	var offendingClusters []string
@@ -405,7 +405,7 @@ func (hc *HealthChecker) checkRemoteClusterConnectivity() error {
 		}
 
 		if len(secrets.Items) == 0 {
-			return &SkipError{Reason: "no remote cluster configs"}
+			return &SkipError{Reason: "no target cluster configs"}
 		}
 
 		var errors []string
@@ -425,7 +425,7 @@ func (hc *HealthChecker) checkRemoteClusterConnectivity() error {
 
 			remoteAPI, err := k8s.NewAPIForConfig(clientConfig, "", []string{}, requestTimeout)
 			if err != nil {
-				errors = append(errors, fmt.Sprintf("* secret: [%s/%s] cluster: [%s]: could not instantiate remote api: %s", secret.Namespace, secret.Name, config.ClusterName, err))
+				errors = append(errors, fmt.Sprintf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %s", secret.Namespace, secret.Name, config.ClusterName, err))
 				continue
 			}
 
@@ -474,7 +474,7 @@ func (hc *HealthChecker) checkRemoteClusterGatewaysHealth(ctx context.Context) e
 		var deadGateways []string
 		var aliveGateways []string
 		if len(rsp.GetOk().GatewaysTable.Rows) == 0 {
-			return &SkipError{Reason: "no remote gateways"}
+			return &SkipError{Reason: "no target gateways"}
 		}
 		for _, gtw := range rsp.GetOk().GatewaysTable.Rows {
 			if gtw.Alive {
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index 85689d22f..5b92ca82f 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -498,6 +498,7 @@ func (conf *ResourceConfig) injectPodSpec(values *patch) {
 		UID:                           conf.proxyUID(),
 		Resources:                     conf.proxyResourceRequirements(),
 		WaitBeforeExitSeconds:         conf.proxyWaitBeforeExitSeconds(),
+		IsGateway:                     conf.isGateway(),
 		RequireIdentityOnInboundPorts: conf.requireIdentityOnInboundPorts(),
 	}
 
@@ -806,6 +807,15 @@ func (conf *ResourceConfig) requireIdentityOnInboundPorts() string {
 	return conf.getOverride(k8s.ProxyRequireIdentityOnInboundPortsAnnotation)
 }
 
+func (conf *ResourceConfig) isGateway() bool {
+	if override := conf.getOverride(k8s.ProxyEnableGatewayAnnotation); override != "" {
+		value, err := strconv.ParseBool(override)
+		return err == nil && value
+	}
+
+	return false
+}
+
 func (conf *ResourceConfig) proxyWaitBeforeExitSeconds() uint64 {
 	if override := conf.getOverride(k8s.ProxyWaitBeforeExitSecondsAnnotation); override != "" {
 		waitBeforeExitSeconds, err := strconv.ParseUint(override, 10, 64)
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 2465074e6..31f03b125 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -195,6 +195,10 @@ const (
 	// to always require identity on inbound ports
 	ProxyRequireIdentityOnInboundPortsAnnotation = ProxyConfigAnnotationsPrefix + "/proxy-require-identity-inbound-ports"
 
+	// ProxyEnableGatewayAnnotation can be used to configure the proxy
+	// to operate as a gateway, routing requests that target the inbound router.
+	ProxyEnableGatewayAnnotation = ProxyConfigAnnotationsPrefix + "/enable-gateway"
+
 	// ProxyDisableIdentityAnnotation can be used to disable identity on the injected proxy.
 	ProxyDisableIdentityAnnotation = ProxyConfigAnnotationsPrefix + "/disable-identity"
 
@@ -433,10 +437,10 @@ const (
 	ConfigKeyName = "kubeconfig"
 
 	// GatewayPortName is the name of the incoming port of the gateway
-	GatewayPortName = "linkerd-gateway"
+	GatewayPortName = "mc-gateway"
 
 	// ProbePortName is the name of the probe port of the gateway
-	ProbePortName = "linkerd-gateway-probe"
+	ProbePortName = "mc-probe"
 
 	// ServiceMirrorLabel is the value used in the controller component label
 	ServiceMirrorLabel = "servicemirror"
diff --git a/pkg/servicemirror/util.go b/pkg/servicemirror/util.go
index 89ca1ef76..caa7b8a35 100644
--- a/pkg/servicemirror/util.go
+++ b/pkg/servicemirror/util.go
@@ -26,14 +26,14 @@ func ParseRemoteClusterSecret(secret *corev1.Secret) (*WatchedClusterConfig, err
 		return nil, fmt.Errorf("secret of type %s should contain key %s", consts.MirrorSecretType, consts.ConfigKeyName)
 	}
 	if !hasConfig {
-		return nil, fmt.Errorf("secret should contain remote cluster name as annotation %s", consts.RemoteClusterNameLabel)
+		return nil, fmt.Errorf("secret should contain target cluster name as annotation %s", consts.RemoteClusterNameLabel)
 	}
 	if !hasDomain {
-		return nil, fmt.Errorf("secret should contain remote cluster domain as annotation %s", consts.RemoteClusterDomainAnnotation)
+		return nil, fmt.Errorf("secret should contain target cluster domain as annotation %s", consts.RemoteClusterDomainAnnotation)
 	}
 
 	if !hasL5dNamespace {
-		return nil, fmt.Errorf("secret should contain remote linkerd installation namespace as annotation %s", consts.RemoteClusterLinkerdNamespaceAnnotation)
+		return nil, fmt.Errorf("secret should contain target cluster linkerd installation namespace as annotation %s", consts.RemoteClusterLinkerdNamespaceAnnotation)
 	}
 
 	return &WatchedClusterConfig{


Test patch:
diff --git a/bin/_test-run.sh b/bin/_test-run.sh
index 6c3f9e630..abe756288 100755
--- a/bin/_test-run.sh
+++ b/bin/_test-run.sh
@@ -26,7 +26,10 @@ init_test_run() {
   test_directory=$bindir/../test
   linkerd_version=$($linkerd_path version --client --short)
   linkerd_namespace=${2:-l5d-integration}
-  k8s_context=${3:-""}
+  k8s_context=${3:-''}
+  export linkerd_version
+  export linkerd_namespace
+  export k8s_context
 
   check_linkerd_binary
   check_if_k8s_reachable
@@ -76,24 +79,25 @@ helm_integration_tests() {
 }
 
 uninstall_integration_tests() {
-    run_test "$test_directory/uninstall/uninstall_test.go" --linkerd-namespace=$linkerd_namespace --uninstall=true
+    run_test "$test_directory/uninstall/uninstall_test.go" --linkerd-namespace="$linkerd_namespace" --uninstall=true
     cleanup
 }
 
 deep_integration_tests() {
-    run_test "$test_directory/install_test.go" --linkerd-namespace=$linkerd_namespace
-    run_test "$(go list $test_directory/.../...)" --linkerd-namespace=$linkerd_namespace
+    run_test "$test_directory/install_test.go" --linkerd-namespace="$linkerd_namespace"
+    while IFS= read -r line; do tests+=("$line"); done <<< "$(go list "$test_directory"/.../...)"
+    run_test "${tests[@]}" --linkerd-namespace="$linkerd_namespace"
     cleanup
 }
 
 custom_domain_integration_tests() {
-    run_test "$test_directory/install_test.go" --linkerd-namespace=$linkerd_namespace --cluster-domain="custom.domain"
+    run_test "$test_directory/install_test.go" --linkerd-namespace="$linkerd_namespace" --cluster-domain='custom.domain'
     cleanup
 }
 
 external_issuer_integration_tests() {
-    run_test "$test_directory/install_test.go" --linkerd-namespace=$linkerd_namespace-external-issuer --external-issuer=true
-    run_test "$test_directory/externalissuer/external_issuer_test.go" --linkerd-namespace=$linkerd_namespace-external-issuer --external-issuer=true
+    run_test "$test_directory/install_test.go" --linkerd-namespace="$linkerd_namespace-external-issuer" --external-issuer=true
+    run_test "$test_directory/externalissuer/external_issuer_test.go" --linkerd-namespace="$linkerd_namespace-external-issuer" --external-issuer=true
     cleanup
 }
 
@@ -120,14 +124,14 @@ check_linkerd_binary(){
 check_if_k8s_reachable(){
     printf 'Checking if there is a Kubernetes cluster available...'
     exit_code=0
-    kubectl --context=$k8s_context --request-timeout=5s get ns > /dev/null 2>&1
+    kubectl --context="$k8s_context" --request-timeout=5s get ns > /dev/null 2>&1
     exit_on_err 'error connecting to Kubernetes cluster'
     printf '[ok]\n'
 }
 
 check_if_l5d_exists() {
     printf 'Checking if Linkerd resources exist on cluster...'
-    resources=$(kubectl --context=$k8s_context get all,clusterrole,clusterrolebinding,mutatingwebhookconfigurations,validatingwebhookconfigurations,psp,crd -l linkerd.io/control-plane-ns --all-namespaces -oname)
+    resources=$(kubectl --context="$k8s_context" get all,clusterrole,clusterrolebinding,mutatingwebhookconfigurations,validatingwebhookconfigurations,psp,crd -l linkerd.io/control-plane-ns --all-namespaces -oname)
     if [ -n "$resources" ]; then
         printf '
 Linkerd resources exist on cluster:
@@ -141,7 +145,7 @@ Help:
 }
 
 cleanup() {
-    "$bindir"/test-cleanup $k8s_context > /dev/null 2>&1
+    "$bindir"/test-cleanup "$k8s_context" > /dev/null 2>&1
     exit_on_err 'error removing existing Linkerd resources'
 }
 
@@ -149,9 +153,9 @@ run_test(){
     filename=$1
     shift
 
-    printf 'Test script: [%s] Params: [%s]\n' "$(basename $filename 2>/dev/null || echo $filename )" "$*"
-    GO111MODULE=on go test --failfast --mod=readonly $filename --linkerd="$linkerd_path" --k8s-context="$k8s_context" --integration-tests "$@"
-    [ $? -ne 0 ] && exit 1
+    printf 'Test script: [%s] Params: [%s]\n' "${filename##*/}" "$*"
+    # Exit on failure here
+    GO111MODULE=on go test --failfast --mod=readonly "$filename" --linkerd="$linkerd_path" --k8s-context="$k8s_context" --integration-tests "$@" || exit 1
 }
 
 # Install the latest stable release.
@@ -178,8 +182,8 @@ install_stable() {
     exit_on_err 'install_stable() - linkerd check failed'
 
     #Now we need to install the app that will be used to verify that upgrade does not break anything
-    kubectl --context=$k8s_context create namespace "$test_app_namespace" > /dev/null 2>&1
-    kubectl --context=$k8s_context label namespaces "$test_app_namespace" 'linkerd.io/is-test-data-plane'='true' > /dev/null 2>&1
+    kubectl --context="$k8s_context" create namespace "$test_app_namespace" > /dev/null 2>&1
+    kubectl --context="$k8s_context" label namespaces "$test_app_namespace" 'linkerd.io/is-test-data-plane'='true' > /dev/null 2>&1
     (
         set -x
         "$linkerd_path" inject --linkerd-namespace="$stable_namespace" "$test_directory/testdata/upgrade_test.yaml" | kubectl --context="$k8s_context" apply --namespace="$test_app_namespace" -f - 2>&1
@@ -191,45 +195,48 @@ install_stable() {
 # this branch.
 # $1 - namespace to use for the stable release
 run_upgrade_test() {
-    local stable_namespace=$1
-    local stable_version=$(latest_stable)
+    local stable_namespace
+    local stable_version
+    stable_namespace=$1
+    stable_version=$(latest_stable)
 
-    install_stable $stable_namespace
-    run_test "$test_directory/install_test.go" --upgrade-from-version=$stable_version --linkerd-namespace=$stable_namespace
+    install_stable "$stable_namespace"
+    run_test "$test_directory/install_test.go" --upgrade-from-version="$stable_version" --linkerd-namespace="$stable_namespace"
 }
 
 setup_helm() {
     (
         set -e
         "$bindir"/helm-build
-        "$helm_path" --kube-context=$k8s_context repo add linkerd https://helm.linkerd.io/stable
+        "$helm_path" --kube-context="$k8s_context" repo add linkerd https://helm.linkerd.io/stable
     )
     exit_on_err 'error setting up Helm'
 }
 
 run_helm_upgrade_test() {
     setup_helm
-    local stable_version=$(latest_stable)
-    run_test "$test_directory/install_test.go" --linkerd-namespace=$linkerd_namespace-helm \
-        --helm-path="$helm_path" --helm-chart="$helm_chart" --helm-stable-chart="linkerd/linkerd2" --helm-release=$helm_release_name --upgrade-helm-from-version="$stable_version"
+    local stable_version
+    stable_version=$(latest_stable)
+    run_test "$test_directory/install_test.go" --linkerd-namespace="$linkerd_namespace-helm" \
+        --helm-path="$helm_path" --helm-chart="$helm_chart" --helm-stable-chart='linkerd/linkerd2' --helm-release="$helm_release_name" --upgrade-helm-from-version="$stable_version"
 }
 
 run_helm_test() {
     setup_helm
-    run_test "$test_directory/install_test.go" --linkerd-namespace=$linkerd_namespace-helm \
-        --helm-path="$helm_path" --helm-chart="$helm_chart" --helm-release=$helm_release_name
+    run_test "$test_directory/install_test.go" --linkerd-namespace="$linkerd_namespace-helm" \
+        --helm-path="$helm_path" --helm-chart="$helm_chart" --helm-release="$helm_release_name"
 }
 
 helm_cleanup() {
     (
         set -e
         # `helm delete` deletes $linkerd_namespace-helm
-        "$helm_path" --kube-context=$k8s_context delete $helm_release_name
+        "$helm_path" --kube-context="$k8s_context" delete "$helm_release_name"
         # `helm delete` doesn't wait for resources to be deleted, so we wait explicitly.
         # We wait for the namespace to be gone so the following call to `cleanup` doesn't fail when it attempts to delete
         # the same namespace that is already being deleted here (error thrown by the NamespaceLifecycle controller).
         # We don't have that problem with global resources, so no need to wait for them to be gone.
-        kubectl wait --for=delete ns/$linkerd_namespace-helm --timeout=120s
+        kubectl wait --for=delete ns/"$linkerd_namespace-helm" --timeout=120s
     )
     exit_on_err 'error cleaning up Helm'
 }
diff --git a/controller/cmd/service-mirror/cluster_watcher_test_util.go b/controller/cmd/service-mirror/cluster_watcher_test_util.go
index 84cc8a4b3..c2605163f 100644
--- a/controller/cmd/service-mirror/cluster_watcher_test_util.go
+++ b/controller/cmd/service-mirror/cluster_watcher_test_util.go
@@ -104,7 +104,7 @@ var createServiceWrongGatewaySpec = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "linkerd-gateway-wrong", 888, "", 111, "/path", 666),
+		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "mc-wrong", 888, "", 111, "/path", 666),
 	},
 }
 
@@ -130,7 +130,7 @@ var createServiceOkeGatewaySpec = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "linkerd-gateway", 888, "gateway-identity", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "mc-gateway", 888, "gateway-identity", defaultProbePort, defaultProbePath, defaultProbePeriod),
 	},
 }
 
@@ -193,7 +193,7 @@ var updateServiceToNewGateway = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("gateway-new", "gateway-ns", "currentGatewayResVersion", "0.0.0.0", "linkerd-gateway", 999, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		gatewayAsYaml("gateway-new", "gateway-ns", "currentGatewayResVersion", "0.0.0.0", "mc-gateway", 999, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
 	},
 	localResources: []string{
 		mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "past", "pastGatewayResVersion", []corev1.ServicePort{
@@ -269,7 +269,7 @@ var updateServiceWithChangedPorts = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "linkerd-gateway", 888, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "mc-gateway", 888, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
 	},
 	localResources: []string{
 		mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "past", "pastGatewayResVersion", []corev1.ServicePort{
diff --git a/test/inject/testdata/injected_params.golden b/test/inject/testdata/injected_params.golden
index 4986da1f0..1a65f894e 100644
--- a/test/inject/testdata/injected_params.golden
+++ b/test/inject/testdata/injected_params.golden
@@ -64,7 +64,7 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
           value: 0.0.0.0:678
         - name: LINKERD2_PROXY_DESTINATION_GET_SUFFIXES
-          value: .
+          value: svc.cluster.local.
         - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
           value: .
         - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE