Repository: linkerd/linkerd2
Pull request #7885 (2022-02-14 22:56:21)

go: Enable `errorlint` checking
Since Go 1.13, errors may "wrap" other errors. [`errorlint`][el] checks
that error formatting and inspection is wrapping-aware.

This change enables `errorlint` in golangci-lint and updates all error
handling code to pass the lint. Some comparisons in tests have been left
unchanged (using `//nolint:errorlint` comments).

[el]: https://github.com/polyfloyd/go-errorlint

Signed-off-by: Oliver Gould <ver@buoyant.io>

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 3a31b984e..5d311e670 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -16,6 +16,7 @@ linters:
   - bodyclose
   - deadcode
   - depguard
+  - errorlint
   - exportloopref
   - goconst
   - gosimple
diff --git a/Cargo.lock b/Cargo.lock
index 091717ab0..68a24ca59 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -936,9 +936,9 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
 name = "mio"
-version = "0.7.14"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2"
 dependencies = [
  "libc",
  "log",
@@ -1687,9 +1687,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.16.1"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
+checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
 dependencies = [
  "bytes",
  "libc",
@@ -1697,9 +1697,10 @@ dependencies = [
  "mio",
  "num_cpus",
  "once_cell",
- "parking_lot 0.11.2",
+ "parking_lot 0.12.0",
  "pin-project-lite",
  "signal-hook-registry",
+ "socket2",
  "tokio-macros",
  "winapi",
 ]
diff --git a/charts/partials/templates/_proxy.tpl b/charts/partials/templates/_proxy.tpl
index 619ab4aaa..6da274732 100644
--- a/charts/partials/templates/_proxy.tpl
+++ b/charts/partials/templates/_proxy.tpl
@@ -61,10 +61,8 @@ env:
   valueFrom:
     fieldRef:
       fieldPath: status.podIPs
-{{ if .Values.proxy.podInboundPorts -}}
 - name: LINKERD2_PROXY_INBOUND_PORTS
   value: {{ .Values.proxy.podInboundPorts | quote }}
-{{ end -}}
 {{ if .Values.proxy.isGateway -}}
 - name: LINKERD2_PROXY_INBOUND_GATEWAY_SUFFIXES
   value: {{printf "svc.%s." .Values.clusterDomain}}
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index 2fff6e20f..77431e370 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -149,7 +149,7 @@ non-zero exit code.`,
 func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, stage string, options *checkOptions) error {
 	err := options.validate()
 	if err != nil {
-		return fmt.Errorf("Validation error when executing check command: %v", err)
+		return fmt.Errorf("Validation error when executing check command: %w", err)
 	}
 
 	if options.cliVersionOverride != "" {
@@ -171,7 +171,7 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 		}
 		values, installManifest, err = renderInstallManifest(cmd.Context())
 		if err != nil {
-			fmt.Fprint(os.Stderr, fmt.Errorf("Error rendering install manifest: %v", err))
+			fmt.Fprintf(os.Stderr, "Error rendering install manifest: %s\n", err)
 			os.Exit(1)
 		}
 	} else {
@@ -219,8 +219,7 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 
 	extensionSuccess, extensionWarning, err := runExtensionChecks(cmd, wout, werr, options)
 	if err != nil {
-		err = fmt.Errorf("failed to run extensions checks: %s", err)
-		fmt.Fprintln(werr, err)
+		fmt.Fprintf(werr, "Failed to run extensions checks: %s\n", err)
 		os.Exit(1)
 	}
 
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 30cd5716f..23d781e17 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -97,14 +97,14 @@ destination.`,
 
 			client, conn, err := destination.NewExternalClient(cmd.Context(), controlPlaneNamespace, k8sAPI)
 			if err != nil {
-				fmt.Fprint(os.Stderr, fmt.Errorf("Error creating destination client: %s", err))
+				fmt.Fprintf(os.Stderr, "Error creating destination client: %s\n", err)
 				os.Exit(1)
 			}
 			defer conn.Close()
 
 			endpoints, err := requestEndpointsFromAPI(client, args)
 			if err != nil {
-				fmt.Fprint(os.Stderr, fmt.Errorf("Destination API error: %s", err))
+				fmt.Fprintf(os.Stderr, "Destination API error: %s\n", err)
 				os.Exit(1)
 			}
 
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index 15ee2feaa..a3a05cf3c 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -191,7 +191,7 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
 
 	if ok, _ := report.Injectable(); !ok {
 		if errs := report.ThrowInjectError(); len(errs) > 0 {
-			return bytes, reports, fmt.Errorf("failed to inject %s%s%s: %v", report.Kind, slash, report.Name, concatErrors(errs, ", "))
+			return bytes, reports, fmt.Errorf("failed to inject %s%s%s: %w", report.Kind, slash, report.Name, concatErrors(errs, ", "))
 		}
 		return bytes, reports, nil
 	}
diff --git a/cli/cmd/inject_util.go b/cli/cmd/inject_util.go
index 63ca98eb6..60175f80e 100644
--- a/cli/cmd/inject_util.go
+++ b/cli/cmd/inject_util.go
@@ -61,10 +61,10 @@ func processYAML(in io.Reader, out io.Writer, report io.Writer, rt resourceTrans
 	for {
 		// Read a single YAML object
 		bytes, err := reader.Read()
-		if err == io.EOF {
-			break
-		}
 		if err != nil {
+			if errors.Is(err, io.EOF) {
+				break
+			}
 			return []error{err}
 		}
 
diff --git a/cli/cmd/install-cni-plugin.go b/cli/cmd/install-cni-plugin.go
index f8af8622c..e7932489b 100644
--- a/cli/cmd/install-cni-plugin.go
+++ b/cli/cmd/install-cni-plugin.go
@@ -80,7 +80,7 @@ func (options *cniPluginOptions) pluginImage() string {
 func newCmdInstallCNIPlugin() *cobra.Command {
 	options, err := newCNIInstallOptionsWithDefaults()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%s", err)
+		fmt.Fprintln(os.Stderr, err)
 		os.Exit(1)
 	}
 
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index 7fb02b0d9..e3aa93450 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -174,7 +174,7 @@ func newCmdInstallControlPlane(values *l5dcharts.Values) *cobra.Command {
 	installOnlyFlags, installOnlyFlagSet := makeInstallFlags(values)
 	installUpgradeFlags, installUpgradeFlagSet, err := makeInstallUpgradeFlags(values)
 	if err != nil {
-		fmt.Fprint(os.Stderr, err.Error())
+		fmt.Fprintln(os.Stderr, err.Error())
 		os.Exit(1)
 	}
 	proxyFlags, proxyFlagSet := makeProxyFlags(values)
@@ -450,7 +450,7 @@ func render(w io.Writer, values *l5dcharts.Values, stage string, valuesOverrides
 	// Attach the final values into the `Values` field for rendering to work
 	renderedTemplates, err := engine.Render(chart, fullValues)
 	if err != nil {
-		return fmt.Errorf("failed to render the template: %s", err)
+		return fmt.Errorf("failed to render the template: %w", err)
 	}
 
 	// Merge templates and inject
@@ -551,17 +551,21 @@ func errAfterRunningChecks(cniEnabled bool) error {
 	errMsgs := []string{}
 	hc.RunChecks(func(result *healthcheck.CheckResult) {
 		if result.Err != nil {
-			if ce, ok := result.Err.(*healthcheck.CategoryError); ok {
+			var ce healthcheck.CategoryError
+			if errors.As(result.Err, &ce) {
 				if ce.Category == healthcheck.KubernetesAPIChecks {
 					k8sAPIError = ce
-				} else if re, ok := ce.Err.(*healthcheck.ResourceError); ok {
-					// resource error, print in kind.group/name format
-					for _, res := range re.Resources {
-						errMsgs = append(errMsgs, res.String())
-					}
 				} else {
-					// unknown category error, just print it
-					errMsgs = append(errMsgs, result.Err.Error())
+					var re healthcheck.ResourceError
+					if errors.As(ce.Err, &re) {
+						// resource error, print in kind.group/name format
+						for _, res := range re.Resources {
+							errMsgs = append(errMsgs, res.String())
+						}
+					} else {
+						// unknown category error, just print it
+						errMsgs = append(errMsgs, result.Err.Error())
+					}
 				}
 			} else {
 				// unknown error, just print it
diff --git a/cli/cmd/options.go b/cli/cmd/options.go
index f6e28a948..058e720d0 100644
--- a/cli/cmd/options.go
+++ b/cli/cmd/options.go
@@ -547,7 +547,7 @@ func validateValues(ctx context.Context, k *k8s.KubernetesAPI, values *l5dcharts
 		}
 		_, err = externalIssuerData.VerifyAndBuildCreds()
 		if err != nil {
-			return fmt.Errorf("failed to validate issuer credentials: %s", err)
+			return fmt.Errorf("failed to validate issuer credentials: %w", err)
 		}
 	}
 
@@ -559,7 +559,7 @@ func validateValues(ctx context.Context, k *k8s.KubernetesAPI, values *l5dcharts
 		}
 		_, err := issuerData.VerifyAndBuildCreds()
 		if err != nil {
-			return fmt.Errorf("failed to validate issuer credentials: %s", err)
+			return fmt.Errorf("failed to validate issuer credentials: %w", err)
 		}
 	}
 
@@ -570,7 +570,7 @@ func validateProxyValues(values *l5dcharts.Values) error {
 	networks := strings.Split(values.ClusterNetworks, ",")
 	for _, network := range networks {
 		if _, _, err := net.ParseCIDR(network); err != nil {
-			return fmt.Errorf("cannot parse destination get networks: %s", err)
+			return fmt.Errorf("cannot parse destination get networks: %w", err)
 		}
 	}
 
@@ -672,7 +672,7 @@ func initializeIssuerCredentials(ctx context.Context, k *k8s.KubernetesAPI, valu
 		// No credentials have been supplied so we will generate them.
 		root, err := tls.GenerateRootCAWithDefaults(issuerName(values.IdentityTrustDomain))
 		if err != nil {
-			return fmt.Errorf("failed to generate root certificate for identity: %s", err)
+			return fmt.Errorf("failed to generate root certificate for identity: %w", err)
 		}
 		values.Identity.Issuer.TLS.KeyPEM = root.Cred.EncodePrivateKeyPEM()
 		values.Identity.Issuer.TLS.CrtPEM = root.Cred.Crt.EncodeCertificatePEM()
diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go
index a7a74184c..c9fb3b59e 100644
--- a/cli/cmd/upgrade.go
+++ b/cli/cmd/upgrade.go
@@ -205,17 +205,17 @@ func k8sClient(manifestsFile string) (*k8s.KubernetesAPI, error) {
 	if manifestsFile != "" {
 		readers, err := read(manifestsFile)
 		if err != nil {
-			return nil, fmt.Errorf("Failed to parse manifests from %s: %s", manifestsFile, err)
+			return nil, fmt.Errorf("failed to parse manifests from %s: %w", manifestsFile, err)
 		}
 
 		k, err = k8s.NewFakeAPIFromManifests(readers)
 		if err != nil {
-			return nil, fmt.Errorf("Failed to parse Kubernetes objects from manifest %s: %s", manifestsFile, err)
+			return nil, fmt.Errorf("failed to parse Kubernetes objects from manifest %s: %w", manifestsFile, err)
 		}
 	} else {
 		k, err = k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
 		if err != nil {
-			return nil, fmt.Errorf("Failed to create a kubernetes client: %s", err)
+			return nil, fmt.Errorf("failed to create a kubernetes client: %w", err)
 		}
 	}
 	return k, nil
diff --git a/cni-plugin/main.go b/cni-plugin/main.go
index bcc23c357..bc8bcfb66 100644
--- a/cni-plugin/main.go
+++ b/cni-plugin/main.go
@@ -101,23 +101,23 @@ func parseConfig(stdin []byte) (*PluginConf, error) {
 
 	logrus.Debugf("linkerd-cni: stdin to plugin: %v", string(stdin))
 	if err := json.Unmarshal(stdin, &conf); err != nil {
-		return nil, fmt.Errorf("linkerd-cni: failed to parse network configuration: %v", err)
+		return nil, fmt.Errorf("linkerd-cni: failed to parse network configuration: %w", err)
 	}
 
 	if conf.RawPrevResult != nil {
 		resultBytes, err := json.Marshal(conf.RawPrevResult)
 		if err != nil {
-			return nil, fmt.Errorf("linkerd-cni: could not serialize prevResult: %v", err)
+			return nil, fmt.Errorf("linkerd-cni: could not serialize prevResult: %w", err)
 		}
 
 		res, err := version.NewResult(conf.CNIVersion, resultBytes)
 		if err != nil {
-			return nil, fmt.Errorf("linkerd-cni: could not parse prevResult: %v", err)
+			return nil, fmt.Errorf("linkerd-cni: could not parse prevResult: %w", err)
 		}
 		conf.RawPrevResult = nil
 		conf.PrevResult, err = cniv1.NewResultFromResult(res)
 		if err != nil {
-			return nil, fmt.Errorf("linkerd-cni: could not convert result to version 1.0: %v", err)
+			return nil, fmt.Errorf("linkerd-cni: could not convert result to version 1.0: %w", err)
 		}
 		logrus.Debugf("linkerd-cni: prevResult: %v", conf.PrevResult)
 	}
@@ -206,7 +206,7 @@ func cmdAdd(args *skel.CmdArgs) error {
 			// Check if there are any overridden ports to be skipped
 			outboundSkipOverride, err := getAnnotationOverride(ctx, client, pod, k8s.ProxyIgnoreOutboundPortsAnnotation)
 			if err != nil {
-				logEntry.Errorf("linkerd-cni: could not retrieve overridden annotations: %v", err)
+				logEntry.Errorf("linkerd-cni: could not retrieve overridden annotations: %s", err)
 				return err
 			}
 
@@ -217,7 +217,7 @@ func cmdAdd(args *skel.CmdArgs) error {
 
 			inboundSkipOverride, err := getAnnotationOverride(ctx, client, pod, k8s.ProxyIgnoreInboundPortsAnnotation)
 			if err != nil {
-				logEntry.Errorf("linkerd-cni: could not retrieve overridden annotations: %v", err)
+				logEntry.Errorf("linkerd-cni: could not retrieve overridden annotations: %s", err)
 				return err
 			}
 
@@ -240,7 +240,7 @@ func cmdAdd(args *skel.CmdArgs) error {
 
 			err = iptables.ConfigureFirewall(*firewallConfiguration)
 			if err != nil {
-				logEntry.Errorf("linkerd-cni: could not configure firewall: %v", err)
+				logEntry.Errorf("linkerd-cni: could not configure firewall: %s", err)
 				return err
 			}
 		} else {
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index dafd17db0..37255c93e 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -423,7 +423,7 @@ func getInboundPort(podSpec *corev1.PodSpec) (uint32, error) {
 			addr := strings.Split(envVar.Value, ":")
 			port, err := strconv.ParseUint(addr[1], 10, 32)
 			if err != nil {
-				return 0, fmt.Errorf("failed to parse inbound port for proxy container: %s", err)
+				return 0, fmt.Errorf("failed to parse inbound port for proxy container: %w", err)
 			}
 			return uint32(port), nil
 		}
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 8fe3b1c6c..6e2fba270 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -3,6 +3,7 @@ package destination
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net"
 	"strconv"
@@ -153,7 +154,8 @@ func (s *server) Get(dest *pb.GetDestination, stream pb.Destination_GetServer) e
 
 	err = s.endpoints.Subscribe(service, port, instanceID, translator)
 	if err != nil {
-		if _, ok := err.(watcher.InvalidService); ok {
+		var ise watcher.InvalidService
+		if errors.As(err, &ise) {
 			log.Debugf("Invalid service %s", dest.GetPath())
 			return status.Errorf(codes.InvalidArgument, "Invalid authority: %s", dest.GetPath())
 		}
@@ -211,18 +213,18 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 
 			opaquePorts, err := getAnnotatedOpaquePorts(pod, s.defaultOpaquePorts)
 			if err != nil {
-				return fmt.Errorf("failed to get opaque ports for pod: %s", err)
+				return fmt.Errorf("failed to get opaque ports for pod: %w", err)
 			}
 			var address watcher.Address
 			var endpoint *pb.WeightedAddr
 			if pod != nil {
 				address, err = s.createAddress(pod, port)
 				if err != nil {
-					return fmt.Errorf("failed to create address: %s", err)
+					return fmt.Errorf("failed to create address: %w", err)
 				}
 				endpoint, err = s.createEndpoint(address, opaquePorts)
 				if err != nil {
-					return fmt.Errorf("failed to create endpoint: %s", err)
+					return fmt.Errorf("failed to create endpoint: %w", err)
 				}
 			}
 			translator := newEndpointProfileTranslator(pod, port, endpoint, stream, s.log)
@@ -261,16 +263,16 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 		if hostname != "" {
 			address, err := s.getEndpointByHostname(s.k8sAPI, hostname, service, port)
 			if err != nil {
-				return fmt.Errorf("failed to get pod for hostname %s: %v", hostname, err)
+				return fmt.Errorf("failed to get pod for hostname %s: %w", hostname, err)
 			}
 			opaquePorts, err := getAnnotatedOpaquePorts(address.Pod, s.defaultOpaquePorts)
 			if err != nil {
-				return fmt.Errorf("failed to get opaque ports for pod: %s", err)
+				return fmt.Errorf("failed to get opaque ports for pod: %w", err)
 			}
 			var endpoint *pb.WeightedAddr
 			endpoint, err = s.createEndpoint(*address, opaquePorts)
 			if err != nil {
-				return fmt.Errorf("failed to create endpoint: %s", err)
+				return fmt.Errorf("failed to create endpoint: %w", err)
 			}
 			translator := newEndpointProfileTranslator(address.Pod, port, endpoint, stream, s.log)
 
@@ -373,7 +375,7 @@ func (s *server) createAddress(pod *corev1.Pod, port uint32) (watcher.Address, e
 	}
 	err := watcher.SetToServerProtocol(s.k8sAPI, &address, port)
 	if err != nil {
-		return watcher.Address{}, fmt.Errorf("failed to set address OpaqueProtocol: %s", err)
+		return watcher.Address{}, fmt.Errorf("failed to set address OpaqueProtocol: %w", err)
 	}
 	return address, nil
 }
@@ -512,7 +514,7 @@ func getPodByIP(k8sAPI *k8s.API, podIP string, port uint32, log *logging.Entry)
 func getIndexedPods(k8sAPI *k8s.API, indexName string, podIP string) ([]*corev1.Pod, error) {
 	objs, err := k8sAPI.Pod().Informer().GetIndexer().ByIndex(indexName, podIP)
 	if err != nil {
-		return nil, fmt.Errorf("failed getting %s indexed pods: %s", indexName, err)
+		return nil, fmt.Errorf("failed getting %s indexed pods: %w", indexName, err)
 	}
 	pods := make([]*corev1.Pod, 0)
 	for _, obj := range objs {
@@ -562,11 +564,11 @@ func (s *server) parseContextToken(token string) contextToken {
 func profileID(authority string, ctxToken contextToken, clusterDomain string) (watcher.ProfileID, error) {
 	host, _, err := getHostAndPort(authority)
 	if err != nil {
-		return watcher.ProfileID{}, fmt.Errorf("invalid authority: %s", err)
+		return watcher.ProfileID{}, fmt.Errorf("invalid authority: %w", err)
 	}
 	service, _, err := parseK8sServiceName(host, clusterDomain)
 	if err != nil {
-		return watcher.ProfileID{}, fmt.Errorf("invalid k8s service name: %s", err)
+		return watcher.ProfileID{}, fmt.Errorf("invalid k8s service name: %w", err)
 	}
 	id := watcher.ProfileID{
 		Name:      fmt.Sprintf("%s.%s.svc.%s", service.Name, service.Namespace, clusterDomain),
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index 44e632076..86b5e8197 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -863,7 +863,7 @@ func (pp *portPublisher) newPodRefAddress(endpointPort Port, endpointIP, podName
 	}
 	pod, err := pp.k8sAPI.Pod().Lister().Pods(id.Namespace).Get(id.Name)
 	if err != nil {
-		return Address{}, PodID{}, fmt.Errorf("unable to fetch pod %v:%v", id, err)
+		return Address{}, PodID{}, fmt.Errorf("unable to fetch pod %v: %w", id, err)
 	}
 	ownerKind, ownerName := pp.k8sAPI.GetOwnerKindAndName(context.Background(), pod, false)
 	addr := Address{
@@ -1152,7 +1152,7 @@ func isValidSlice(es *discovery.EndpointSlice) bool {
 func SetToServerProtocol(k8sAPI *k8s.API, address *Address, port Port) error {
 	servers, err := k8sAPI.Srv().Lister().Servers("").List(labels.Everything())
 	if err != nil {
-		return fmt.Errorf("failed to list Servers: %s", err)
+		return fmt.Errorf("failed to list Servers: %w", err)
 	}
 	if address.Pod == nil {
 		return fmt.Errorf("endpoint not backed by Pod: %s:%d", address.IP, address.Port)
@@ -1160,7 +1160,7 @@ func SetToServerProtocol(k8sAPI *k8s.API, address *Address, port Port) error {
 	for _, server := range servers {
 		selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
 		if err != nil {
-			return fmt.Errorf("failed to create Selector: %s", err)
+			return fmt.Errorf("failed to create Selector: %w", err)
 		}
 		if server.Spec.ProxyProtocol == opaqueProtocol && selector.Matches(labels.Set(address.Pod.Labels)) {
 			var portMatch bool
diff --git a/controller/api/destination/watcher/k8s.go b/controller/api/destination/watcher/k8s.go
index 589bb56ef..d316195ba 100644
--- a/controller/api/destination/watcher/k8s.go
+++ b/controller/api/destination/watcher/k8s.go
@@ -63,7 +63,7 @@ func InitializeIndexers(k8sAPI *k8s.API) error {
 	}})
 
 	if err != nil {
-		return fmt.Errorf("could not create an indexer for services: %s", err)
+		return fmt.Errorf("could not create an indexer for services: %w", err)
 	}
 
 	err = k8sAPI.Pod().Informer().AddIndexers(cache.Indexers{PodIPIndex: func(obj interface{}) ([]string, error) {
@@ -81,7 +81,7 @@ func InitializeIndexers(k8sAPI *k8s.API) error {
 	}})
 
 	if err != nil {
-		return fmt.Errorf("could not create an indexer for pods: %s", err)
+		return fmt.Errorf("could not create an indexer for pods: %w", err)
 	}
 
 	err = k8sAPI.Pod().Informer().AddIndexers(cache.Indexers{HostIPIndex: func(obj interface{}) ([]string, error) {
@@ -106,7 +106,7 @@ func InitializeIndexers(k8sAPI *k8s.API) error {
 	}})
 
 	if err != nil {
-		return fmt.Errorf("could not create an indexer for pods: %s", err)
+		return fmt.Errorf("could not create an indexer for pods: %w", err)
 	}
 
 	return nil
diff --git a/controller/heartbeat/heartbeat.go b/controller/heartbeat/heartbeat.go
index 07f836711..53bc0a368 100644
--- a/controller/heartbeat/heartbeat.go
+++ b/controller/heartbeat/heartbeat.go
@@ -226,19 +226,19 @@ func Send(v url.Values) error {
 func send(client *http.Client, baseURL string, v url.Values) error {
 	req, err := http.NewRequest("GET", baseURL, nil)
 	if err != nil {
-		return fmt.Errorf("failed to create HTTP request for base URL [%s]: %s", baseURL, err)
+		return fmt.Errorf("failed to create HTTP request for base URL [%s]: %w", baseURL, err)
 	}
 	req.URL.RawQuery = v.Encode()
 
 	log.Infof("Sending heartbeat: %s", req.URL.String())
 	resp, err := client.Do(req)
 	if err != nil {
-		return fmt.Errorf("Check URL [%s] request failed with: %s", req.URL.String(), err)
+		return fmt.Errorf("check URL [%s] request failed with: %w", req.URL.String(), err)
 	}
 	defer resp.Body.Close()
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		return fmt.Errorf("failed to read response body: %s", err)
+		return fmt.Errorf("failed to read response body: %w", err)
 	}
 	if resp.StatusCode != http.StatusOK {
 		return fmt.Errorf("request failed with code %d; response body: %s", resp.StatusCode, string(body))
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index bdea3d723..7f33ecfa8 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -101,7 +101,7 @@ type API struct {
 func InitializeAPI(ctx context.Context, kubeConfig string, ensureClusterWideAccess bool, resources ...APIResource) (*API, error) {
 	config, err := k8s.GetConfig(kubeConfig, "")
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Kubernetes API client: %v", err)
+		return nil, fmt.Errorf("error configuring Kubernetes API client: %w", err)
 	}
 
 	dynamicClient, err := dynamic.NewForConfig(config)
diff --git a/controller/script/destination-client/main.go b/controller/script/destination-client/main.go
index d95270f06..f24605b87 100644
--- a/controller/script/destination-client/main.go
+++ b/controller/script/destination-client/main.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"errors"
 	"flag"
 	"io"
 
@@ -50,10 +51,10 @@ func get(client pb.DestinationClient, req *pb.GetDestination) {
 
 	for {
 		update, err := rsp.Recv()
-		if err == io.EOF {
-			break
-		}
 		if err != nil {
+			if errors.Is(err, io.EOF) {
+				break
+			}
 			log.Fatal(err.Error())
 		}
 
@@ -99,10 +100,10 @@ func getProfile(client pb.DestinationClient, req *pb.GetDestination) {
 
 	for {
 		update, err := rsp.Recv()
-		if err == io.EOF {
-			break
-		}
 		if err != nil {
+			if errors.Is(err, io.EOF) {
+				break
+			}
 			log.Fatal(err.Error())
 		}
 		log.Printf("%+v", update)
diff --git a/controller/webhook/server.go b/controller/webhook/server.go
index fedcc79c3..3ab83eaf9 100644
--- a/controller/webhook/server.go
+++ b/controller/webhook/server.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/tls"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -106,7 +107,7 @@ func getConfiguredServer(
 func (s *Server) Start() {
 	log.Infof("listening at %s", s.Server.Addr)
 	if err := s.ListenAndServeTLS("", ""); err != nil {
-		if err == http.ErrServerClosed {
+		if errors.Is(err, http.ErrServerClosed) {
 			return
 		}
 		log.Fatal(err)
diff --git a/go.mod b/go.mod
index 230dabe1e..e5fcb7017 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/ghodss/yaml v1.0.0
 	github.com/go-openapi/spec v0.19.5
 	github.com/golang/protobuf v1.5.2
-	github.com/gorilla/websocket v1.4.2
+	github.com/gorilla/websocket v1.5.0
 	github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/imdario/mergo v0.3.12
diff --git a/go.sum b/go.sum
index 17a09f268..f764145bd 100644
--- a/go.sum
+++ b/go.sum
@@ -502,8 +502,9 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
 github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b h1:NGgE5ELokSf2tZ/bydyDUKrvd/jP8lrAoPNeBuMOTOk=
 github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b/go.mod h1:zT/uzhdQGTqlwTq7Lpbj3JoJQWfPfIJ1tE0OidAmih8=
diff --git a/jaeger/cmd/check.go b/jaeger/cmd/check.go
index 26170bd48..e81b2e82f 100644
--- a/jaeger/cmd/check.go
+++ b/jaeger/cmd/check.go
@@ -192,7 +192,7 @@ code.`,
 func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions) error {
 	err := options.validate()
 	if err != nil {
-		return fmt.Errorf("Validation error when executing check command: %v", err)
+		return fmt.Errorf("Validation error when executing check command: %w", err)
 	}
 
 	hc := healthcheck.NewHealthChecker([]healthcheck.CategoryID{}, &healthcheck.Options{
@@ -208,8 +208,7 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 
 	err = hc.InitializeKubeAPIClient()
 	if err != nil {
-		err = fmt.Errorf("Error initializing k8s API client: %s", err)
-		fmt.Fprintln(werr, err)
+		fmt.Fprintf(werr, "Error initializing k8s API client: %s\n", err)
 		os.Exit(1)
 	}
 
diff --git a/multicluster/cmd/check.go b/multicluster/cmd/check.go
index 29e9de9ca..a76d6cc62 100644
--- a/multicluster/cmd/check.go
+++ b/multicluster/cmd/check.go
@@ -93,14 +93,13 @@ non-zero exit code.`,
 			// Get the multicluster extension namespace
 			kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to run multicluster check: %v\n", err)
+				fmt.Fprintf(os.Stderr, "failed to run multicluster check: %s\n", err)
 				os.Exit(1)
 			}
 
 			_, err = kubeAPI.GetNamespaceWithExtensionLabel(context.Background(), MulticlusterExtensionName)
 			if err != nil {
-				err = fmt.Errorf("%w; install by running `linkerd multicluster install | kubectl apply -f -`", err)
-				fmt.Fprintln(os.Stderr, err.Error())
+				fmt.Fprintf(os.Stderr, "%s; install by running `linkerd multicluster install | kubectl apply -f -`\n", err)
 				os.Exit(1)
 			}
 			return configureAndRunChecks(stdout, stderr, options)
@@ -124,7 +123,7 @@ non-zero exit code.`,
 func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions) error {
 	err := options.validate()
 	if err != nil {
-		return fmt.Errorf("Validation error when executing check command: %v", err)
+		return fmt.Errorf("Validation error when executing check command: %w", err)
 	}
 	checks := []healthcheck.CategoryID{
 		linkerdMulticlusterExtensionCheck,
@@ -141,15 +140,13 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 
 	err = linkerdHC.InitializeKubeAPIClient()
 	if err != nil {
-		err = fmt.Errorf("Error initializing k8s API client: %s", err)
-		fmt.Fprintln(werr, err)
+		fmt.Fprintf(werr, "Error initializing k8s API client: %s\n", err)
 		os.Exit(1)
 	}
 
 	err = linkerdHC.InitializeLinkerdGlobalConfig(context.Background())
 	if err != nil {
-		err = fmt.Errorf("Failed to fetch linkerd config: %s", err)
-		fmt.Fprintln(werr, err)
+		fmt.Fprintf(werr, "Failed to fetch linkerd config: %s\n", err)
 		os.Exit(1)
 	}
 
@@ -186,7 +183,7 @@ func multiclusterCategory(hc *healthChecker) *healthcheck.Category {
 			WithCheck(func(ctx context.Context) error {
 				localAnchors, err := tls.DecodePEMCertificates(hc.LinkerdConfig().IdentityTrustAnchorsPEM)
 				if err != nil {
-					return fmt.Errorf("Cannot parse source trust anchors: %s", err)
+					return fmt.Errorf("Cannot parse source trust anchors: %w", err)
 				}
 				return hc.checkRemoteClusterAnchors(ctx, localAnchors)
 			}))
@@ -296,7 +293,7 @@ func multiclusterCategory(hc *healthChecker) *healthcheck.Category {
 func (hc *healthChecker) checkLinkCRD(ctx context.Context) error {
 	err := hc.linkAccess(ctx)
 	if err != nil {
-		return fmt.Errorf("multicluster.linkerd.io/Link CRD is missing: %s", err)
+		return fmt.Errorf("multicluster.linkerd.io/Link CRD is missing: %w", err)
 	}
 	return nil
 }
@@ -322,14 +319,14 @@ func (hc *healthChecker) checkLinks(ctx context.Context) error {
 		return err
 	}
 	if len(links) == 0 {
-		return &healthcheck.SkipError{Reason: "no links detected"}
+		return healthcheck.SkipError{Reason: "no links detected"}
 	}
 	linkNames := []string{}
 	for _, l := range links {
 		linkNames = append(linkNames, fmt.Sprintf("\t* %s", l.TargetClusterName))
 	}
 	hc.links = links
-	return &healthcheck.VerboseSuccess{Message: strings.Join(linkNames, "\n")}
+	return healthcheck.VerboseSuccess{Message: strings.Join(linkNames, "\n")}
 }
 
 func (hc *healthChecker) checkRemoteClusterConnectivity(ctx context.Context) error {
@@ -339,34 +336,34 @@ func (hc *healthChecker) checkRemoteClusterConnectivity(ctx context.Context) err
 		// Load the credentials secret
 		secret, err := hc.KubeAPIClient().Interface.CoreV1().Secrets(link.Namespace).Get(ctx, link.ClusterCredentialsSecret, metav1.GetOptions{})
 		if err != nil {
-			errors = append(errors, fmt.Errorf("* secret: [%s/%s]: %s", link.Namespace, link.ClusterCredentialsSecret, err))
+			errors = append(errors, fmt.Errorf("* secret: [%s/%s]: %w", link.Namespace, link.ClusterCredentialsSecret, err))
 			continue
 		}
 		config, err := servicemirror.ParseRemoteClusterSecret(secret)
 		if err != nil {
-			errors = append(errors, fmt.Errorf("* secret: [%s/%s]: could not parse config secret: %s", secret.Namespace, secret.Name, err))
+			errors = append(errors, fmt.Errorf("* secret: [%s/%s]: could not parse config secret: %w", secret.Namespace, secret.Name, err))
 			continue
 		}
 		clientConfig, err := clientcmd.RESTConfigFromKubeConfig(config)
 		if err != nil {
-			errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: unable to parse api config: %s", secret.Namespace, secret.Name, link.TargetClusterName, err))
+			errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: unable to parse api config: %w", secret.Namespace, secret.Name, link.TargetClusterName, err))
 			continue
 		}
 		remoteAPI, err := k8s.NewAPIForConfig(clientConfig, "", []string{}, healthcheck.RequestTimeout)
 		if err != nil {
-			errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %s", secret.Namespace, secret.Name, link.TargetClusterName, err))
+			errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %w", secret.Namespace, secret.Name, link.TargetClusterName, err))
 			continue
 		}
 		// We use this call just to check connectivity.
 		_, err = remoteAPI.Discovery().ServerVersion()
 		if err != nil {
-			errors = append(errors, fmt.Errorf("* failed to connect to API for cluster: [%s]: %s", link.TargetClusterName, err))
+			errors = append(errors, fmt.Errorf("* failed to connect to API for cluster: [%s]: %w", link.TargetClusterName, err))
 			continue
 		}
 		verbs := []string{"get", "list", "watch"}
 		for _, verb := range verbs {
 			if err := healthcheck.CheckCanPerformAction(ctx, remoteAPI, verb, corev1.NamespaceAll, "", "v1", "services"); err != nil {
-				errors = append(errors, fmt.Errorf("* missing service permission [%s] for cluster [%s]: %s", verb, link.TargetClusterName, err))
+				errors = append(errors, fmt.Errorf("* missing service permission [%s] for cluster [%s]: %w", verb, link.TargetClusterName, err))
 			}
 		}
 		links = append(links, fmt.Sprintf("\t* %s", link.TargetClusterName))
@@ -375,9 +372,9 @@ func (hc *healthChecker) checkRemoteClusterConnectivity(ctx context.Context) err
 		return joinErrors(errors, 2)
 	}
 	if len(links) == 0 {
-		return &healthcheck.SkipError{Reason: "no links"}
+		return healthcheck.SkipError{Reason: "no links"}
 	}
-	return &healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
+	return healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
 }
 
 func (hc *healthChecker) checkRemoteClusterAnchors(ctx context.Context, localAnchors []*x509.Certificate) error {
@@ -439,9 +436,9 @@ func (hc *healthChecker) checkRemoteClusterAnchors(ctx context.Context, localAnc
 		return fmt.Errorf("Problematic clusters:\n    %s", strings.Join(errors, "\n    "))
 	}
 	if len(links) == 0 {
-		return &healthcheck.SkipError{Reason: "no links"}
+		return healthcheck.SkipError{Reason: "no links"}
 	}
-	return &healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
+	return healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
 }
 
 func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error {
@@ -506,9 +503,9 @@ func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error
 		return fmt.Errorf(strings.Join(errors, "\n"))
 	}
 	if len(links) == 0 {
-		return &healthcheck.SkipError{Reason: "no links"}
+		return healthcheck.SkipError{Reason: "no links"}
 	}
-	return &healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
+	return healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
 }
 
 func (hc *healthChecker) checkServiceMirrorController(ctx context.Context) error {
@@ -541,9 +538,9 @@ func (hc *healthChecker) checkServiceMirrorController(ctx context.Context) error
 		return joinErrors(errors, 2)
 	}
 	if len(clusterNames) == 0 {
-		return &healthcheck.SkipError{Reason: "no links"}
+		return healthcheck.SkipError{Reason: "no links"}
 	}
-	return &healthcheck.VerboseSuccess{Message: strings.Join(clusterNames, "\n")}
+	return healthcheck.VerboseSuccess{Message: strings.Join(clusterNames, "\n")}
 }
 
 func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context) error {
@@ -570,13 +567,13 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context)
 
 		vizNs, err := hc.KubeAPIClient().GetNamespaceWithExtensionLabel(ctx, vizCmd.ExtensionName)
 		if err != nil {
-			return &healthcheck.SkipError{Reason: "failed to fetch gateway metrics"}
+			return healthcheck.SkipError{Reason: "failed to fetch gateway metrics"}
 		}
 
 		// Check gateway liveness according to probes
 		vizClient, err := client.NewExternalClient(ctx, vizNs.Name, hc.KubeAPIClient())
 		if err != nil {
-			errors = append(errors, fmt.Errorf("failed to initialize viz client: %s", err))
+			errors = append(errors, fmt.Errorf("failed to initialize viz client: %w", err))
 			break
 		}
 		req := vizPb.GatewaysRequest{
@@ -585,7 +582,7 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context)
 		}
 		rsp, err := vizClient.Gateways(ctx, &req)
 		if err != nil {
-			errors = append(errors, fmt.Errorf("failed to fetch gateway metrics for %s.%s: %s", svc.Name, svc.Namespace, err))
+			errors = append(errors, fmt.Errorf("failed to fetch gateway metrics for %s.%s: %w", svc.Name, svc.Namespace, err))
 			continue
 		}
 		table := rsp.GetOk().GetGatewaysTable()
@@ -608,9 +605,9 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context)
 		return joinErrors(errors, 1)
 	}
 	if len(links) == 0 {
-		return &healthcheck.SkipError{Reason: "no links"}
+		return healthcheck.SkipError{Reason: "no links"}
 	}
-	return &healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
+	return healthcheck.VerboseSuccess{Message: strings.Join(links, "\n")}
 }
 
 func (hc *healthChecker) checkIfMirrorServicesHaveEndpoints(ctx context.Context) error {
@@ -631,7 +628,7 @@ func (hc *healthChecker) checkIfMirrorServicesHaveEndpoints(ctx context.Context)
 		return fmt.Errorf("Some mirror services do not have endpoints:\n    %s", strings.Join(servicesWithNoEndpoints, "\n    "))
 	}
 	if len(mirrorServices.Items) == 0 {
-		return &healthcheck.SkipError{Reason: "no mirror services"}
+		return healthcheck.SkipError{Reason: "no mirror services"}
 	}
 	return nil
 }
@@ -661,7 +658,7 @@ func (hc *healthChecker) checkForOrphanedServices(ctx context.Context) error {
 		}
 	}
 	if len(mirrorServices.Items) == 0 {
-		return &healthcheck.SkipError{Reason: "no mirror services"}
+		return healthcheck.SkipError{Reason: "no mirror services"}
 	}
 	if len(errors) > 0 {
 		return joinErrors(errors, 1)
diff --git a/multicluster/cmd/gateways.go b/multicluster/cmd/gateways.go
index e7f373c08..36901bb82 100644
--- a/multicluster/cmd/gateways.go
+++ b/multicluster/cmd/gateways.go
@@ -47,7 +47,7 @@ func newGatewaysCommand() *cobra.Command {
 
 			vizNs, err := k8sAPI.GetNamespaceWithExtensionLabel(ctx, vizCmd.ExtensionName)
 			if err != nil {
-				return fmt.Errorf("make sure the linkerd-viz extension is installed, using 'linkerd viz install' (%s)", err)
+				return fmt.Errorf("make sure the linkerd-viz extension is installed, using 'linkerd viz install' (%w)", err)
 			}
 
 			client, err := client.NewExternalClient(ctx, vizNs.Name, k8sAPI)
@@ -57,7 +57,7 @@ func newGatewaysCommand() *cobra.Command {
 
 			resp, err := requestGatewaysFromAPI(client, req)
 			if err != nil {
-				fmt.Fprint(os.Stderr, err.Error())
+				fmt.Fprintln(os.Stderr, err)
 				os.Exit(1)
 			}
 
@@ -76,7 +76,7 @@ func newGatewaysCommand() *cobra.Command {
 func requestGatewaysFromAPI(client pb.ApiClient, req *pb.GatewaysRequest) (*pb.GatewaysResponse, error) {
 	resp, err := client.Gateways(context.Background(), req)
 	if err != nil {
-		return nil, fmt.Errorf("Gateways API error: %v", err)
+		return nil, fmt.Errorf("Gateways API error: %w", err)
 	}
 	if e := resp.GetError(); e != nil {
 		return nil, fmt.Errorf("Gateways API response error: %v", e.Error)
diff --git a/multicluster/cmd/install.go b/multicluster/cmd/install.go
index a36f12661..e90c175ee 100644
--- a/multicluster/cmd/install.go
+++ b/multicluster/cmd/install.go
@@ -44,7 +44,7 @@ func newMulticlusterInstallCommand() *cobra.Command {
 	var ignoreCluster bool
 
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%s", err)
+		fmt.Fprintln(os.Stderr, err)
 		os.Exit(1)
 	}
 
diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go
index ad23551fa..79e284b5e 100644
--- a/multicluster/cmd/link.go
+++ b/multicluster/cmd/link.go
@@ -54,7 +54,7 @@ func newLinkCommand() *cobra.Command {
 	var valuesOptions valuespkg.Options
 
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%s", err)
+		fmt.Fprintln(os.Stderr, err)
 		os.Exit(1)
 	}
 
diff --git a/multicluster/cmd/service-mirror/main.go b/multicluster/cmd/service-mirror/main.go
index 33bdd0760..db266e180 100644
--- a/multicluster/cmd/service-mirror/main.go
+++ b/multicluster/cmd/service-mirror/main.go
@@ -155,7 +155,7 @@ func loadCredentials(ctx context.Context, link multicluster.Link, namespace stri
 	// Load the credentials secret
 	secret, err := k8sAPI.Interface.CoreV1().Secrets(namespace).Get(ctx, link.ClusterCredentialsSecret, metav1.GetOptions{})
 	if err != nil {
-		return nil, fmt.Errorf("Failed to load credentials secret %s: %s", link.ClusterCredentialsSecret, err)
+		return nil, fmt.Errorf("failed to load credentials secret %s: %w", link.ClusterCredentialsSecret, err)
 	}
 	return sm.ParseRemoteClusterSecret(secret)
 }
@@ -180,7 +180,7 @@ func restartClusterWatcher(
 
 	cfg, err := clientcmd.RESTConfigFromKubeConfig(creds)
 	if err != nil {
-		return fmt.Errorf("Unable to parse kube config: %s", err)
+		return fmt.Errorf("unable to parse kube config: %w", err)
 	}
 
 	clusterWatcher, err = servicemirror.NewRemoteClusterServiceWatcher(
@@ -194,17 +194,17 @@ func restartClusterWatcher(
 		enableHeadlessSvc,
 	)
 	if err != nil {
-		return fmt.Errorf("Unable to create cluster watcher: %s", err)
+		return fmt.Errorf("unable to create cluster watcher: %w", err)
 	}
 
 	err = clusterWatcher.Start(ctx)
 	if err != nil {
-		return fmt.Errorf("Failed to start cluster watcher: %s", err)
+		return fmt.Errorf("failed to start cluster watcher: %w", err)
 	}
 
 	workerMetrics, err := metrics.NewWorkerMetrics(link.TargetClusterName)
 	if err != nil {
-		return fmt.Errorf("Failed to create metrics for cluster watcher: %s", err)
+		return fmt.Errorf("failed to create metrics for cluster watcher: %w", err)
 	}
 	probeWorker = servicemirror.NewProbeWorker(fmt.Sprintf("probe-gateway-%s", link.TargetClusterName), &link.ProbeSpec, workerMetrics, link.TargetClusterName)
 	probeWorker.Start()
diff --git a/multicluster/cmd/unlink.go b/multicluster/cmd/unlink.go
index cc5826cc5..1a25a45fa 100644
--- a/multicluster/cmd/unlink.go
+++ b/multicluster/cmd/unlink.go
@@ -21,7 +21,7 @@ import (
 func newUnlinkCommand() *cobra.Command {
 	opts, err := newLinkOptionsWithDefault()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%s", err)
+		fmt.Fprintln(os.Stderr, err)
 		os.Exit(1)
 	}
 
diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go
index 0c9f70bc0..acf9b0d2c 100644
--- a/multicluster/service-mirror/cluster_watcher.go
+++ b/multicluster/service-mirror/cluster_watcher.go
@@ -2,6 +2,7 @@ package servicemirror
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"strings"
@@ -159,11 +160,11 @@ func NewRemoteClusterServiceWatcher(
 ) (*RemoteClusterServiceWatcher, error) {
 	remoteAPI, err := k8s.InitializeAPIForConfig(ctx, cfg, false, k8s.Svc, k8s.Endpoint)
 	if err != nil {
-		return nil, fmt.Errorf("cannot initialize api for target cluster %s: %s", clusterName, err)
+		return nil, fmt.Errorf("cannot initialize api for target cluster %s: %w", clusterName, err)
 	}
 	_, err = remoteAPI.Client.Discovery().ServerVersion()
 	if err != nil {
-		return nil, fmt.Errorf("cannot connect to api for target cluster %s: %s", clusterName, err)
+		return nil, fmt.Errorf("cannot connect to api for target cluster %s: %w", clusterName, err)
 	}
 
 	// Create k8s event recorder
@@ -273,7 +274,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupOrphanedServices(ctx context.Con
 
 	servicesOnLocalCluster, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
 	if err != nil {
-		innerErr := fmt.Errorf("failed to list services while cleaning up mirror services: %s", err)
+		innerErr := fmt.Errorf("failed to list services while cleaning up mirror services: %w", err)
 		if kerrors.IsNotFound(err) {
 			return innerErr
 		}
@@ -314,7 +315,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 
 	services, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
 	if err != nil {
-		innerErr := fmt.Errorf("could not retrieve mirrored services that need cleaning up: %s", err)
+		innerErr := fmt.Errorf("could not retrieve mirrored services that need cleaning up: %w", err)
 		if kerrors.IsNotFound(err) {
 			return innerErr
 		}
@@ -328,7 +329,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 			if kerrors.IsNotFound(err) {
 				continue
 			}
-			errors = append(errors, fmt.Errorf("Could not delete service %s/%s: %s", svc.Namespace, svc.Name, err))
+			errors = append(errors, fmt.Errorf("Could not delete service %s/%s: %w", svc.Namespace, svc.Name, err))
 		} else {
 			rcsw.log.Infof("Deleted service %s/%s", svc.Namespace, svc.Name)
 		}
@@ -336,7 +337,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 
 	endpoints, err := rcsw.localAPIClient.Endpoint().Lister().List(labels.Set(matchLabels).AsSelector())
 	if err != nil {
-		innerErr := fmt.Errorf("could not retrieve endpoints that need cleaning up: %s", err)
+		innerErr := fmt.Errorf("could not retrieve endpoints that need cleaning up: %w", err)
 		if kerrors.IsNotFound(err) {
 			return innerErr
 		}
@@ -348,7 +349,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 			if kerrors.IsNotFound(err) {
 				continue
 			}
-			errors = append(errors, fmt.Errorf("Could not delete endpoints %s/%s: %s", endpoint.Namespace, endpoint.Name, err))
+			errors = append(errors, fmt.Errorf("Could not delete endpoints %s/%s: %w", endpoint.Namespace, endpoint.Name, err))
 		} else {
 			rcsw.log.Infof("Deleted endpoints %s/%s", endpoint.Namespace, endpoint.Name)
 		}
@@ -370,7 +371,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.
 			rcsw.log.Debugf("Failed to delete mirror service %s/%s: %v", ev.Namespace, ev.Name, err)
 			return nil
 		}
-		errors = append(errors, fmt.Errorf("could not fetch service %s/%s: %s", ev.Namespace, localServiceName, err))
+		errors = append(errors, fmt.Errorf("could not fetch service %s/%s: %w", ev.Namespace, localServiceName, err))
 	}
 
 	// If the mirror service is headless, also delete its endpoint mirror
@@ -382,7 +383,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.
 		endpointMirrorServices, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
 		if err != nil {
 			if !kerrors.IsNotFound(err) {
-				errors = append(errors, fmt.Errorf("could not fetch endpoint mirrors for mirror service %s/%s: %s", ev.Namespace, localServiceName, err))
+				errors = append(errors, fmt.Errorf("could not fetch endpoint mirrors for mirror service %s/%s: %w", ev.Namespace, localServiceName, err))
 			}
 		}
 
@@ -390,7 +391,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.
 			err = rcsw.localAPIClient.Client.CoreV1().Services(endpointMirror.Namespace).Delete(ctx, endpointMirror.Name, metav1.DeleteOptions{})
 			if err != nil {
 				if !kerrors.IsNotFound(err) {
-					errors = append(errors, fmt.Errorf("could not delete endpoint mirror %s/%s: %s", endpointMirror.Namespace, endpointMirror.Name, err))
+					errors = append(errors, fmt.Errorf("could not delete endpoint mirror %s/%s: %w", endpointMirror.Namespace, endpointMirror.Name, err))
 				}
 			}
 		}
@@ -399,7 +400,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.
 	rcsw.log.Infof("Deleting mirrored service %s/%s", ev.Namespace, localServiceName)
 	if err := rcsw.localAPIClient.Client.CoreV1().Services(ev.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{}); err != nil {
 		if !kerrors.IsNotFound(err) {
-			errors = append(errors, fmt.Errorf("could not delete service: %s/%s: %s", ev.Namespace, localServiceName, err))
+			errors = append(errors, fmt.Errorf("could not delete service: %s/%s: %w", ev.Namespace, localServiceName, err))
 		}
 	}
 
@@ -753,21 +754,18 @@ func (rcsw *RemoteClusterServiceWatcher) processEvents(ctx context.Context) {
 		if err == nil {
 			rcsw.eventsQueue.Forget(event)
 		} else {
-			switch e := err.(type) {
-			case RetryableError:
-				{
-					rcsw.log.Warnf("Requeues: %d, Limit: %d for event %s", rcsw.eventsQueue.NumRequeues(event), rcsw.requeueLimit, event)
-					if (rcsw.eventsQueue.NumRequeues(event) < rcsw.requeueLimit) && !done {
-						rcsw.log.Errorf("Error processing %s (will retry): %s", event, e)
-						rcsw.eventsQueue.AddRateLimited(event)
-					} else {
-						rcsw.log.Errorf("Error processing %s (giving up): %s", event, e)
-						rcsw.eventsQueue.Forget(event)
-					}
+			var re RetryableError
+			if errors.As(err, &re) {
+				rcsw.log.Warnf("Requeues: %d, Limit: %d for event %s", rcsw.eventsQueue.NumRequeues(event), rcsw.requeueLimit, event)
+				if (rcsw.eventsQueue.NumRequeues(event) < rcsw.requeueLimit) && !done {
+					rcsw.log.Errorf("Error processing %s (will retry): %s", event, re)
+					rcsw.eventsQueue.AddRateLimited(event)
+				} else {
+					rcsw.log.Errorf("Error processing %s (giving up): %s", event, re)
+					rcsw.eventsQueue.Forget(event)
 				}
-			default:
-				rcsw.log.Errorf("Error processing %s (will not retry): %s", event, e)
-				rcsw.log.Error(e)
+			} else {
+				rcsw.log.Errorf("Error processing %s (will not retry): %s", event, err)
 			}
 		}
 		if done {
@@ -891,7 +889,7 @@ func (rcsw *RemoteClusterServiceWatcher) resolveGatewayAddress() ([]corev1.Endpo
 				IP: ipAddr.String(),
 			})
 		} else {
-			err = fmt.Errorf("Error resolving '%s': %s", addr, err)
+			err = fmt.Errorf("Error resolving '%s': %w", addr, err)
 			rcsw.log.Warn(err)
 			errors = append(errors, err)
 		}
@@ -1061,7 +1059,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleCreateOrUpdateEndpoints(
 		exportedService, err := rcsw.remoteAPIClient.Svc().Lister().Services(exportedEndpoints.Namespace).Get(exportedEndpoints.Name)
 		if err != nil {
 			return RetryableError{[]error{
-				fmt.Errorf("error retrieving exported service %s/%s: %v", exportedEndpoints.Namespace, exportedEndpoints.Name, err),
+				fmt.Errorf("error retrieving exported service %s/%s: %w", exportedEndpoints.Namespace, exportedEndpoints.Name, err),
 			}}
 		}
 		gatewayAddresses, err := rcsw.resolveGatewayAddress()
diff --git a/multicluster/service-mirror/cluster_watcher_headless.go b/multicluster/service-mirror/cluster_watcher_headless.go
index 3a3522d78..6a9f1941e 100644
--- a/multicluster/service-mirror/cluster_watcher_headless.go
+++ b/multicluster/service-mirror/cluster_watcher_headless.go
@@ -38,7 +38,7 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateHeadlessEndpoints(ctx con
 	exportedService, err := rcsw.remoteAPIClient.Svc().Lister().Services(exportedEndpoints.Namespace).Get(exportedEndpoints.Name)
 	if err != nil {
 		rcsw.log.Debugf("failed to retrieve exported service %s/%s when updating its headless mirror endpoints: %v", exportedEndpoints.Namespace, exportedEndpoints.Name, err)
-		return fmt.Errorf("error retrieving exported service %s/%s: %v", exportedEndpoints.Namespace, exportedEndpoints.Name, err)
+		return fmt.Errorf("error retrieving exported service %s/%s: %w", exportedEndpoints.Namespace, exportedEndpoints.Name, err)
 	}
 
 	// Check whether the endpoints should be processed for a headless exported
@@ -158,7 +158,7 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateHeadlessEndpoints(ctx con
 		err := rcsw.localAPIClient.Client.CoreV1().Services(service.Namespace).Delete(ctx, service.Name, metav1.DeleteOptions{})
 		if err != nil {
 			if !kerrors.IsNotFound(err) {
-				errors = append(errors, fmt.Errorf("error deleting Endpoint Mirror service %s/%s: %v", service.Namespace, service.Name, err))
+				errors = append(errors, fmt.Errorf("error deleting Endpoint Mirror service %s/%s: %w", service.Namespace, service.Name, err))
 			}
 		}
 	}
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index e23f8170a..7293d56d3 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -306,12 +306,12 @@ func (v *Values) ToMap() (map[string]interface{}, error) {
 	var valuesMap map[string]interface{}
 	rawValues, err := yaml.Marshal(v)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to marshal the values struct: %s", err)
+		return nil, fmt.Errorf("Failed to marshal the values struct: %w", err)
 	}
 
 	err = yaml.Unmarshal(rawValues, &valuesMap)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to Unmarshal Values into a map: %s", err)
+		return nil, fmt.Errorf("Failed to Unmarshal Values into a map: %w", err)
 	}
 
 	return valuesMap, nil
diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go
index 16d828170..b284ad883 100644
--- a/pkg/cmd/cmd.go
+++ b/pkg/cmd/cmd.go
@@ -55,7 +55,7 @@ func Uninstall(ctx context.Context, k8sAPI *k8s.KubernetesAPI, selector string)
 	}
 	for _, r := range resources {
 		if err := r.RenderResource(os.Stdout); err != nil {
-			return fmt.Errorf("error rendering Kubernetes resource: %v", err)
+			return fmt.Errorf("error rendering Kubernetes resource: %w", err)
 		}
 	}
 	return nil
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 3191b1d1b..2dbd0c256 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -20,12 +20,12 @@ func Values(filepath string) (*l5dcharts.Values, error) {
 	values := &l5dcharts.Values{}
 	configYaml, err := ioutil.ReadFile(filepath)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read config file: %s", err)
+		return nil, fmt.Errorf("failed to read config file: %w", err)
 	}
 
 	log.Debugf("%s config YAML: %s", filepath, configYaml)
 	if err = yaml.Unmarshal(configYaml, values); err != nil {
-		return nil, fmt.Errorf("failed to unmarshal JSON from: %s: %s", filepath, err)
+		return nil, fmt.Errorf("failed to unmarshal JSON from: %s: %w", filepath, err)
 	}
 	return values, err
 }
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index c387391bb..d9d605ca7 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -215,7 +215,7 @@ type ResourceError struct {
 
 // Error satisfies the error interface for ResourceError. The output is intended
 // for `linkerd check`.
-func (e *ResourceError) Error() string {
+func (e ResourceError) Error() string {
 	names := []string{}
 	for _, res := range e.Resources {
 		names = append(names, res.name)
@@ -231,13 +231,14 @@ type CategoryError struct {
 }
 
 // Error satisfies the error interface for CategoryError.
-func (e *CategoryError) Error() string {
+func (e CategoryError) Error() string {
 	return e.Err.Error()
 }
 
 // IsCategoryError returns true if passed in error is of type CategoryError and belong to the given category
 func IsCategoryError(err error, categoryID CategoryID) bool {
-	if ce, ok := err.(*CategoryError); ok {
+	var ce CategoryError
+	if errors.As(err, &ce) {
 		return ce.Category == categoryID
 	}
 	return false
@@ -249,7 +250,7 @@ type SkipError struct {
 }
 
 // Error satisfies the error interface for SkipError.
-func (e *SkipError) Error() string {
+func (e SkipError) Error() string {
 	return e.Reason
 }
 
@@ -261,7 +262,7 @@ type VerboseSuccess struct {
 
 // Error satisfies the error interface for VerboseSuccess.  Since VerboseSuccess
 // does not actually represent a failure, this returns the empty string.
-func (e *VerboseSuccess) Error() string {
+func (e VerboseSuccess) Error() string {
 	return ""
 }
 
@@ -822,7 +823,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 						err := hc.InitializeLinkerdGlobalConfig(ctx)
 						if err != nil {
 							if kerrors.IsNotFound(err) {
-								return &SkipError{Reason: configMapDoesNotExistSkipReason}
+								return SkipError{Reason: configMapDoesNotExistSkipReason}
 							}
 							return err
 						}
@@ -846,7 +847,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(ctx context.Context) error {
 						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
+							return SkipError{Reason: linkerdCNIDisabledSkipReason}
 						}
 						_, err := hc.kubeAPI.CoreV1().ConfigMaps(hc.CNINamespace).Get(ctx, linkerdCNIConfigMapName, metav1.GetOptions{})
 						return err
@@ -858,7 +859,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(ctx context.Context) error {
 						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
+							return SkipError{Reason: linkerdCNIDisabledSkipReason}
 						}
 						_, err := hc.kubeAPI.RbacV1().ClusterRoles().Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
 						if kerrors.IsNotFound(err) {
@@ -873,7 +874,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(ctx context.Context) error {
 						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
+							return SkipError{Reason: linkerdCNIDisabledSkipReason}
 						}
 						_, err := hc.kubeAPI.RbacV1().ClusterRoleBindings().Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
 						if kerrors.IsNotFound(err) {
@@ -888,7 +889,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(ctx context.Context) error {
 						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
+							return SkipError{Reason: linkerdCNIDisabledSkipReason}
 						}
 						_, err := hc.kubeAPI.CoreV1().ServiceAccounts(hc.CNINamespace).Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
 						if kerrors.IsNotFound(err) {
@@ -903,7 +904,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(ctx context.Context) (err error) {
 						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
+							return SkipError{Reason: linkerdCNIDisabledSkipReason}
 						}
 						hc.cniDaemonSet, err = hc.kubeAPI.Interface.AppsV1().DaemonSets(hc.CNINamespace).Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
 						if kerrors.IsNotFound(err) {
@@ -920,7 +921,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:               true,
 					check: func(ctx context.Context) (err error) {
 						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
+							return SkipError{Reason: linkerdCNIDisabledSkipReason}
 						}
 						hc.cniDaemonSet, err = hc.kubeAPI.Interface.AppsV1().DaemonSets(hc.CNINamespace).Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
 						if kerrors.IsNotFound(err) {
@@ -1007,7 +1008,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(context.Context) error {
 						if err := issuercerts.CheckCertAlgoRequirements(hc.issuerCert.Certificate); err != nil {
-							return fmt.Errorf("issuer certificate %s", err)
+							return fmt.Errorf("issuer certificate %w", err)
 						}
 						return nil
 					},
@@ -1018,7 +1019,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					fatal:       true,
 					check: func(ctx context.Context) error {
 						if err := issuercerts.CheckCertValidityPeriod(hc.issuerCert.Certificate); err != nil {
-							return fmt.Errorf("issuer certificate is %s", err)
+							return fmt.Errorf("issuer certificate is %w", err)
 						}
 						return nil
 					},
@@ -1029,7 +1030,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					hintAnchor:  "l5d-identity-issuer-cert-not-expiring-soon",
 					check: func(context.Context) error {
 						if err := issuercerts.CheckExpiringSoon(hc.issuerCert.Certificate); err != nil {
-							return fmt.Errorf("issuer certificate %s", err)
+							return fmt.Errorf("issuer certificate %w", err)
 						}
 						return nil
 					},
@@ -1127,14 +1128,14 @@ func (hc *HealthChecker) allCategories() []*Category {
 					check: func(ctx context.Context) (err error) {
 						anchors, err := hc.fetchWebhookCaBundle(ctx, k8s.PolicyValidatorWebhookConfigName)
 						if kerrors.IsNotFound(err) {
-							return &SkipError{Reason: "policy-validator not installed"}
+							return SkipError{Reason: "policy-validator not installed"}
 						}
 						if err != nil {
 							return err
 						}
 						cert, err := hc.FetchCredsFromSecret(ctx, hc.ControlPlaneNamespace, policyValidatorTLSSecretName)
 						if kerrors.IsNotFound(err) {
-							return &SkipError{Reason: "policy-validator not installed"}
+							return SkipError{Reason: "policy-validator not installed"}
 						}
 						if err != nil {
 							return err
@@ -1150,7 +1151,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 					check: func(ctx context.Context) error {
 						cert, err := hc.FetchCredsFromSecret(ctx, hc.ControlPlaneNamespace, policyValidatorTLSSecretName)
 						if kerrors.IsNotFound(err) {
-							return &SkipError{Reason: "policy-validator not installed"}
+							return SkipError{Reason: "policy-validator not installed"}
 						}
 						if err != nil {
 							return err
@@ -1403,7 +1404,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 						if policy != nil && *policy == admissionRegistration.Fail {
 							return hc.checkHAMetadataPresentOnKubeSystemNamespace(ctx)
 						}
-						return &SkipError{Reason: "not run for non HA installs"}
+						return SkipError{Reason: "not run for non HA installs"}
 					},
 				},
 				{
@@ -1415,7 +1416,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 						if hc.isHA() {
 							return hc.checkMinReplicasAvailable(ctx)
 						}
-						return &SkipError{Reason: "not run for non HA installs"}
+						return SkipError{Reason: "not run for non HA installs"}
 					},
 				},
 			},
@@ -1481,11 +1482,11 @@ func (hc *HealthChecker) CheckCertAndAnchors(cert *tls.Cred, trustAnchors []*x50
 
 	// check cert validity
 	if err := issuercerts.CheckCertValidityPeriod(cert.Certificate); err != nil {
-		return fmt.Errorf("certificate is %s", err)
+		return fmt.Errorf("certificate is %w", err)
 	}
 
 	if err := cert.Verify(tls.CertificatesToPool(trustAnchors), identityName, time.Time{}); err != nil {
-		return fmt.Errorf("cert is not issued by the trust anchor: %s", err)
+		return fmt.Errorf("cert is not issued by the trust anchor: %w", err)
 	}
 
 	return nil
@@ -1526,7 +1527,7 @@ func (hc *HealthChecker) CheckCertAndAnchorsExpiringSoon(cert *tls.Cred) error {
 
 	// check cert not expiring soon
 	if err := issuercerts.CheckExpiringSoon(cert.Certificate); err != nil {
-		return fmt.Errorf("certificate %s", err)
+		return fmt.Errorf("certificate %w", err)
 	}
 	return nil
 }
@@ -1616,7 +1617,8 @@ func (hc *HealthChecker) runCheck(category *Category, c *Checker, observer Check
 		ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
 		defer cancel()
 		err := c.check(ctx)
-		if se, ok := err.(*SkipError); ok {
+		var se SkipError
+		if errors.As(err, &se) {
 			log.Debugf("Skipping check: %s. Reason: %s", c.description, se.Reason)
 			return true
 		}
@@ -1627,10 +1629,11 @@ func (hc *HealthChecker) runCheck(category *Category, c *Checker, observer Check
 			Warning:     c.warning,
 			HintURL:     fmt.Sprintf("%s%s", category.hintBaseURL, c.hintAnchor),
 		}
-		if vs, ok := err.(*VerboseSuccess); ok {
+		var vs VerboseSuccess
+		if errors.As(err, &vs) {
 			checkResult.Description = fmt.Sprintf("%s\n%s", checkResult.Description, vs.Message)
 		} else if err != nil {
-			checkResult.Err = &CategoryError{category.ID, err}
+			checkResult.Err = CategoryError{category.ID, err}
 		}
 
 		if checkResult.Err != nil && time.Now().Before(c.retryDeadline) {
@@ -1875,7 +1878,7 @@ func (hc *HealthChecker) checkClusterNetworks(ctx context.Context) error {
 	}
 	if !podCIDRExists {
 		// DigitalOcean for example, doesn't expose spec.podCIDR (#6398)
-		return &SkipError{Reason: podCIDRUnavailableSkipReason}
+		return SkipError{Reason: podCIDRUnavailableSkipReason}
 	}
 	if len(badPodCIDRS) > 0 {
 		sort.Strings(badPodCIDRS)
@@ -2213,7 +2216,7 @@ func checkResources(resourceName string, objects []runtime.Object, expectedNames
 				}
 				resources = append(resources, res)
 			}
-			return &ResourceError{resourceName, resources}
+			return ResourceError{resourceName, resources}
 		}
 		return nil
 	}
@@ -2498,18 +2501,18 @@ func (hc *HealthChecker) checkCanCreateNonNamespacedResources(ctx context.Contex
 	for {
 		// Read single object YAML
 		objYAML, err := yamlReader.Read()
-		if err == io.EOF {
-			break
-		}
 		if err != nil {
-			return fmt.Errorf("error reading install manifest: %v", err)
+			if errors.Is(err, io.EOF) {
+				break
+			}
+			return fmt.Errorf("error reading install manifest: %w", err)
 		}
 
 		// Create unstructured object from YAML
 		objMap := map[string]interface{}{}
 		err = yaml.Unmarshal(objYAML, &objMap)
 		if err != nil {
-			return fmt.Errorf("error unmarshaling yaml object %s: %v", objYAML, err)
+			return fmt.Errorf("error unmarshaling yaml object %s: %w", objYAML, err)
 		}
 		if len(objMap) == 0 {
 			// Ignore header blocks with only comments
diff --git a/pkg/healthcheck/healthcheck_output.go b/pkg/healthcheck/healthcheck_output.go
index 03b3ddd3b..e93148378 100644
--- a/pkg/healthcheck/healthcheck_output.go
+++ b/pkg/healthcheck/healthcheck_output.go
@@ -155,7 +155,7 @@ func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, fl
 				if len(stderr.String()) > 0 {
 					err = errors.New(stderr.String())
 				} else {
-					err = fmt.Errorf("invalid extension check output from \"%s\" (JSON object expected):\n%s\n[%s]", command, stdout.String(), err)
+					err = fmt.Errorf("invalid extension check output from \"%s\" (JSON object expected):\n%s\n[%w]", command, stdout.String(), err)
 				}
 				results.Results = append(results.Results, CheckResult{
 					Category:    CategoryID(extensionCmd),
diff --git a/pkg/healthcheck/version.go b/pkg/healthcheck/version.go
index 3ad868e87..ef1093a9d 100644
--- a/pkg/healthcheck/version.go
+++ b/pkg/healthcheck/version.go
@@ -14,12 +14,12 @@ import (
 func GetServerVersion(ctx context.Context, controlPlaneNamespace string, kubeAPI *k8s.KubernetesAPI) (string, error) {
 	cm, err := config.FetchLinkerdConfigMap(ctx, kubeAPI, controlPlaneNamespace)
 	if err != nil {
-		return "", fmt.Errorf("failed to fetch linkerd-config: %s", err)
+		return "", fmt.Errorf("failed to fetch linkerd-config: %w", err)
 	}
 
 	values, err := linkerd2.ValuesFromConfigMap(cm)
 	if err != nil {
-		return "", fmt.Errorf("failed to load values from linkerd-config: %s", err)
+		return "", fmt.Errorf("failed to load values from linkerd-config: %w", err)
 	}
 
 	return values.LinkerdVersion, nil
diff --git a/pkg/identity/service.go b/pkg/identity/service.go
index 49530c366..cdc7b21fb 100644
--- a/pkg/identity/service.go
+++ b/pkg/identity/service.go
@@ -119,12 +119,12 @@ func (svc *Service) loadCredentials() (tls.Issuer, error) {
 	)
 
 	if err != nil {
-		return nil, fmt.Errorf("failed to read CA from disk: %s", err)
+		return nil, fmt.Errorf("failed to read CA from disk: %w", err)
 	}
 
 	// Don't verify with dns name as this is not a leaf certificate
 	if err := creds.Crt.Verify(svc.trustAnchors, "", time.Time{}); err != nil {
-		return nil, fmt.Errorf("failed to verify issuer credentials for '%s' with trust anchors: %s", svc.expectedName, err)
+		return nil, fmt.Errorf("failed to verify issuer credentials for '%s' with trust anchors: %w", svc.expectedName, err)
 	}
 
 	if !creds.Certificate.IsCA {
@@ -202,18 +202,20 @@ func (svc *Service) Certify(ctx context.Context, req *pb.CertifyRequest) (*pb.Ce
 	log.Debugf("Validating token for %s", reqIdentity)
 	tokIdentity, err := svc.validator.Validate(ctx, tok)
 	if err != nil {
-		switch e := err.(type) {
-		case NotAuthenticated:
-			log.Infof("authentication failed for %s: %s", reqIdentity, e)
-			return nil, status.Error(codes.FailedPrecondition, e.Error())
-		case InvalidToken:
-			log.Debugf("invalid token provided for %s: %s", reqIdentity, e)
-			return nil, status.Error(codes.InvalidArgument, e.Error())
-		default:
-			msg := fmt.Sprintf("error validating token for %s: %s", reqIdentity, e)
-			log.Error(msg)
-			return nil, status.Error(codes.Internal, msg)
+		var nae NotAuthenticated
+		if errors.As(err, &nae) {
+			log.Infof("authentication failed for %s: %s", reqIdentity, nae)
+			return nil, status.Error(codes.FailedPrecondition, nae.Error())
 		}
+		var ite InvalidToken
+		if errors.As(err, &ite) {
+			log.Debugf("invalid token provided for %s: %s", reqIdentity, ite)
+			return nil, status.Error(codes.InvalidArgument, ite.Error())
+		}
+
+		msg := fmt.Sprintf("error validating token for %s: %s", reqIdentity, err)
+		log.Error(msg)
+		return nil, status.Error(codes.Internal, msg)
 	}
 
 	// Ensure the requested identity matches the token's identity.
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index dee7df537..11c3ec0da 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -276,13 +276,13 @@ func (conf *ResourceConfig) GetPodPatch(injectProxy bool) ([]byte, error) {
 
 	values, err := conf.GetOverriddenValues()
 	if err != nil {
-		return nil, fmt.Errorf("could not generate Overridden Values: %s", err)
+		return nil, fmt.Errorf("could not generate Overridden Values: %w", err)
 	}
 
 	if values.ClusterNetworks != "" {
 		for _, network := range strings.Split(strings.Trim(values.ClusterNetworks, ","), ",") {
 			if _, _, err := net.ParseCIDR(network); err != nil {
-				return nil, fmt.Errorf("cannot parse destination get networks: %s", err)
+				return nil, fmt.Errorf("cannot parse destination get networks: %w", err)
 			}
 		}
 	}
diff --git a/pkg/issuercerts/issuercerts.go b/pkg/issuercerts/issuercerts.go
index 216d0040d..409b9315e 100644
--- a/pkg/issuercerts/issuercerts.go
+++ b/pkg/issuercerts/issuercerts.go
@@ -158,7 +158,7 @@ func CheckCertAlgoRequirements(cert *x509.Certificate) error {
 func (ic *IssuerCertData) VerifyAndBuildCreds() (*tls.Cred, error) {
 	creds, err := tls.ValidateAndCreateCreds(ic.IssuerCrt, ic.IssuerKey)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read CA: %s", err)
+		return nil, fmt.Errorf("failed to read CA: %w", err)
 	}
 
 	// we check the time validity of the issuer cert
diff --git a/pkg/k8s/api.go b/pkg/k8s/api.go
index 52f4f3185..eb08d435c 100644
--- a/pkg/k8s/api.go
+++ b/pkg/k8s/api.go
@@ -48,7 +48,7 @@ type KubernetesAPI struct {
 func NewAPI(configPath, kubeContext string, impersonate string, impersonateGroup []string, timeout time.Duration) (*KubernetesAPI, error) {
 	config, err := GetConfig(configPath, kubeContext)
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Kubernetes API client: %v", err)
+		return nil, fmt.Errorf("error configuring Kubernetes API client: %w", err)
 	}
 	return NewAPIForConfig(config, impersonate, impersonateGroup, timeout)
 }
@@ -73,24 +73,24 @@ func NewAPIForConfig(config *rest.Config, impersonate string, impersonateGroup [
 
 	clientset, err := kubernetes.NewForConfig(config)
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Kubernetes API clientset: %v", err)
+		return nil, fmt.Errorf("error configuring Kubernetes API clientset: %w", err)
 	}
 	apiextensions, err := apiextensionsclient.NewForConfig(config)
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Kubernetes API Extensions clientset: %v", err)
+		return nil, fmt.Errorf("error configuring Kubernetes API Extensions clientset: %w", err)
 	}
 	aggregatorClient, err := apiregistration.NewForConfig(config)
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Kubernetes API server aggregator: %v", err)
+		return nil, fmt.Errorf("error configuring Kubernetes API server aggregator: %w", err)
 	}
 	dynamicClient, err := dynamic.NewForConfig(config)
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Kubernetes Dynamic Client: %v", err)
+		return nil, fmt.Errorf("error configuring Kubernetes Dynamic Client: %w", err)
 	}
 
 	l5dCrdClient, err := crdclient.NewForConfig(config)
 	if err != nil {
-		return nil, fmt.Errorf("error configuring Linkerd CRD clientset: %v", err)
+		return nil, fmt.Errorf("error configuring Linkerd CRD clientset: %w", err)
 	}
 
 	return &KubernetesAPI{
@@ -108,7 +108,7 @@ func NewAPIForConfig(config *rest.Config, impersonate string, impersonateGroup [
 func (kubeAPI *KubernetesAPI) NewClient() (*http.Client, error) {
 	secureTransport, err := rest.TransportFor(kubeAPI.Config)
 	if err != nil {
-		return nil, fmt.Errorf("error instantiating Kubernetes API client: %v", err)
+		return nil, fmt.Errorf("error instantiating Kubernetes API client: %w", err)
 	}
 
 	return &http.Client{
diff --git a/pkg/k8s/fake.go b/pkg/k8s/fake.go
index a8cebcc6c..486632eda 100644
--- a/pkg/k8s/fake.go
+++ b/pkg/k8s/fake.go
@@ -2,6 +2,7 @@ package k8s
 
 import (
 	"bufio"
+	"errors"
 	"io"
 	"strings"
 
@@ -152,10 +153,10 @@ func newFakeClientSetsFromManifests(readers []io.Reader) (
 		for {
 			// Read a single YAML object
 			bytes, err := r.Read()
-			if err == io.EOF {
-				break
-			}
 			if err != nil {
+				if errors.Is(err, io.EOF) {
+					break
+				}
 				return nil, nil, nil, nil, err
 			}
 
diff --git a/pkg/k8s/portforward.go b/pkg/k8s/portforward.go
index 53805ee0a..f670de69e 100644
--- a/pkg/k8s/portforward.go
+++ b/pkg/k8s/portforward.go
@@ -192,7 +192,7 @@ func (pf *PortForward) run() error {
 
 	err = fw.ForwardPorts()
 	if err != nil {
-		err = fmt.Errorf("%s for %s/%s", err, pf.namespace, pf.podName)
+		err = fmt.Errorf("%w for %s/%s", err, pf.namespace, pf.podName)
 		return err
 	}
 	return nil
diff --git a/pkg/k8s/resource/resource.go b/pkg/k8s/resource/resource.go
index 951bfbec2..c98f9c03c 100644
--- a/pkg/k8s/resource/resource.go
+++ b/pkg/k8s/resource/resource.go
@@ -78,55 +78,55 @@ func FetchKubernetesResources(ctx context.Context, k *k8s.KubernetesAPI, options
 
 	clusterRoles, err := fetchClusterRoles(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch ClusterRole resources:%v", err)
+		return nil, fmt.Errorf("could not fetch ClusterRole resources: %w", err)
 	}
 	resources = append(resources, clusterRoles...)
 
 	clusterRoleBindings, err := fetchClusterRoleBindings(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch ClusterRoleBinding resources:%v", err)
+		return nil, fmt.Errorf("could not fetch ClusterRoleBinding resources: %w", err)
 	}
 	resources = append(resources, clusterRoleBindings...)
 
 	roles, err := fetchRoles(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch Roles:%v", err)
+		return nil, fmt.Errorf("could not fetch Roles: %w", err)
 	}
 	resources = append(resources, roles...)
 
 	roleBindings, err := fetchRoleBindings(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch RoleBindings:%v", err)
+		return nil, fmt.Errorf("could not fetch RoleBindings: %w", err)
 	}
 	resources = append(resources, roleBindings...)
 
 	crds, err := fetchCustomResourceDefinitions(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch CustomResourceDefinition resources:%v", err)
+		return nil, fmt.Errorf("could not fetch CustomResourceDefinition resources: %w", err)
 	}
 	resources = append(resources, crds...)
 
 	apiCRDs, err := fetchAPIRegistrationResources(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch APIService CRDs:%v", err)
+		return nil, fmt.Errorf("could not fetch APIService CRDs: %w", err)
 	}
 	resources = append(resources, apiCRDs...)
 
 	mutatinghooks, err := fetchMutatingWebhooksConfiguration(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch MutatingWebhookConfigurations:%v", err)
+		return nil, fmt.Errorf("could not fetch MutatingWebhookConfigurations: %w", err)
 	}
 	resources = append(resources, mutatinghooks...)
 
 	validationhooks, err := fetchValidatingWebhooksConfiguration(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch ValidatingWebhookConfiguration:%v", err)
+		return nil, fmt.Errorf("could not fetch ValidatingWebhookConfiguration: %w", err)
 	}
 	resources = append(resources, validationhooks...)
 
 	namespaces, err := fetchNamespace(ctx, k, options)
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch Namespace:%v", err)
+		return nil, fmt.Errorf("could not fetch Namespace: %w", err)
 	}
 	resources = append(resources, namespaces...)
 
diff --git a/pkg/profiles/openapi.go b/pkg/profiles/openapi.go
index 6d48ebb23..4d46a89ba 100644
--- a/pkg/profiles/openapi.go
+++ b/pkg/profiles/openapi.go
@@ -31,17 +31,17 @@ func RenderOpenAPI(fileName, namespace, name, clusterDomain string, w io.Writer)
 
 	bytes, err := ioutil.ReadAll(input)
 	if err != nil {
-		return fmt.Errorf("Error reading file: %s", err)
+		return fmt.Errorf("Error reading file: %w", err)
 	}
 	json, err := yaml.YAMLToJSON(bytes)
 	if err != nil {
-		return fmt.Errorf("Error parsing yaml: %s", err)
+		return fmt.Errorf("Error parsing yaml: %w", err)
 	}
 
 	swagger := spec.Swagger{}
 	err = swagger.UnmarshalJSON(json)
 	if err != nil {
-		return fmt.Errorf("Error parsing OpenAPI spec: %s", err)
+		return fmt.Errorf("Error parsing OpenAPI spec: %w", err)
 	}
 
 	profile := swaggerToServiceProfile(swagger, namespace, name, clusterDomain)
diff --git a/pkg/profiles/profiles.go b/pkg/profiles/profiles.go
index 6e4476f1d..99d32d308 100644
--- a/pkg/profiles/profiles.go
+++ b/pkg/profiles/profiles.go
@@ -54,38 +54,38 @@ func Validate(data []byte) error {
 	var serviceProfile sp.ServiceProfile
 	err := yaml.UnmarshalStrict(data, &serviceProfile)
 	if err != nil {
-		return fmt.Errorf("failed to validate ServiceProfile: %s", err)
+		return fmt.Errorf("failed to validate ServiceProfile: %w", err)
 	}
 
 	errs := validation.IsDNS1123Subdomain(serviceProfile.Name)
 	if len(errs) > 0 {
-		return fmt.Errorf("ServiceProfile \"%s\" has invalid name: %s", serviceProfile.Name, errs[0])
+		return fmt.Errorf("ServiceProfile %q has invalid name: %s", serviceProfile.Name, errs[0])
 	}
 
 	for _, route := range serviceProfile.Spec.Routes {
 		if route.Name == "" {
-			return fmt.Errorf("ServiceProfile \"%s\" has a route with no name", serviceProfile.Name)
+			return fmt.Errorf("ServiceProfile %q has a route with no name", serviceProfile.Name)
 		}
 		if route.Timeout != "" {
 			_, err := time.ParseDuration(route.Timeout)
 			if err != nil {
-				return fmt.Errorf("ServiceProfile \"%s\" has a route with an invalid timeout: %s", serviceProfile.Name, err)
+				return fmt.Errorf("ServiceProfile %q has a route with an invalid timeout: %w", serviceProfile.Name, err)
 			}
 		}
 		if route.Condition == nil {
-			return fmt.Errorf("ServiceProfile \"%s\" has a route with no condition", serviceProfile.Name)
+			return fmt.Errorf("ServiceProfile %q has a route with no condition", serviceProfile.Name)
 		}
 		err := ValidateRequestMatch(route.Condition)
 		if err != nil {
-			return fmt.Errorf("ServiceProfile \"%s\" has a route with an invalid condition: %s", serviceProfile.Name, err)
+			return fmt.Errorf("ServiceProfile %q has a route with an invalid condition: %w", serviceProfile.Name, err)
 		}
 		for _, rc := range route.ResponseClasses {
 			if rc.Condition == nil {
-				return fmt.Errorf("ServiceProfile \"%s\" has a response class with no condition", serviceProfile.Name)
+				return fmt.Errorf("ServiceProfile %q has a response class with no condition", serviceProfile.Name)
 			}
 			err = ValidateResponseMatch(rc.Condition)
 			if err != nil {
-				return fmt.Errorf("ServiceProfile \"%s\" has a response class with an invalid condition: %s", serviceProfile.Name, err)
+				return fmt.Errorf("ServiceProfile %q has a response class with an invalid condition: %w", serviceProfile.Name, err)
 			}
 		}
 	}
@@ -93,16 +93,16 @@ func Validate(data []byte) error {
 	rb := serviceProfile.Spec.RetryBudget
 	if rb != nil {
 		if rb.RetryRatio < 0 {
-			return fmt.Errorf("ServiceProfile \"%s\" RetryBudget RetryRatio must be non-negative: %f", serviceProfile.Name, rb.RetryRatio)
+			return fmt.Errorf("ServiceProfile %q RetryBudget RetryRatio must be non-negative: %f", serviceProfile.Name, rb.RetryRatio)
 		}
 
 		if rb.TTL == "" {
-			return fmt.Errorf("ServiceProfile \"%s\" RetryBudget missing TTL field", serviceProfile.Name)
+			return fmt.Errorf("ServiceProfile %q RetryBudget missing TTL field", serviceProfile.Name)
 		}
 
 		_, err := time.ParseDuration(rb.TTL)
 		if err != nil {
-			return fmt.Errorf("ServiceProfile \"%s\" RetryBudget: %s", serviceProfile.Name, err)
+			return fmt.Errorf("ServiceProfile %q RetryBudget: %w", serviceProfile.Name, err)
 		}
 	}
 
@@ -235,7 +235,7 @@ func readFile(fileName string) (io.Reader, error) {
 func writeProfile(profile sp.ServiceProfile, w io.Writer) error {
 	output, err := yaml.Marshal(profile)
 	if err != nil {
-		return fmt.Errorf("Error writing Service Profile: %s", err)
+		return fmt.Errorf("Error writing Service Profile: %w", err)
 	}
 	_, err = w.Write(output)
 	return err
diff --git a/pkg/protohttp/protohttp.go b/pkg/protohttp/protohttp.go
index 14eae8cad..76f3e8aac 100644
--- a/pkg/protohttp/protohttp.go
+++ b/pkg/protohttp/protohttp.go
@@ -69,9 +69,10 @@ func WriteErrorToHTTPResponse(w http.ResponseWriter, errorObtained error) {
 	statusCode := defaultHTTPErrorStatusCode
 	errorToReturn := errorObtained
 
-	if httpErr, ok := errorObtained.(HTTPError); ok {
-		statusCode = httpErr.Code
-		errorToReturn = httpErr.WrappedError
+	var he HTTPError
+	if errors.As(errorObtained, &he) {
+		statusCode = he.Code
+		errorToReturn = he.WrappedError
 	}
 
 	w.Header().Set(errorHeader, http.StatusText(statusCode))
@@ -158,7 +159,7 @@ func CheckIfResponseHasError(rsp *http.Response) error {
 
 		err := FromByteStreamToProtocolBuffers(reader, &apiError)
 		if err != nil {
-			return fmt.Errorf("Response has %s header [%s], but response body didn't contain protobuf error: %v", errorHeader, errorMsg, err)
+			return fmt.Errorf("response has %s header [%s], but response body didn't contain protobuf error: %w", errorHeader, errorMsg, err)
 		}
 
 		return errors.New(apiError.Error)
diff --git a/pkg/tls/cred.go b/pkg/tls/cred.go
index d88da77fc..0916dc3ac 100644
--- a/pkg/tls/cred.go
+++ b/pkg/tls/cred.go
@@ -97,7 +97,7 @@ func (crt *Crt) Verify(roots *x509.CertPool, name string, currentTime time.Time)
 	}
 
 	if crtExpiryError(err) {
-		return fmt.Errorf("%s - Current Time : %s - Invalid before %s - Invalid After %s", err, currentTime, crt.Certificate.NotBefore, crt.Certificate.NotAfter)
+		return fmt.Errorf("%w - Current Time : %s - Invalid before %s - Invalid After %s", err, currentTime, crt.Certificate.NotBefore, crt.Certificate.NotAfter)
 	}
 	return err
 }
@@ -232,10 +232,9 @@ func DecodePEMCrt(txt string) (*Crt, error) {
 }
 
 func crtExpiryError(err error) bool {
-	switch v := err.(type) {
-	case x509.CertificateInvalidError:
-		return v.Reason == x509.Expired
-	default:
-		return false
+	var cie x509.CertificateInvalidError
+	if errors.As(err, &cie) {
+		return cie.Reason == x509.Expired
 	}
+	return false
 }
diff --git a/pkg/tls/creds_watcher.go b/pkg/tls/creds_watcher.go
index 53bfeef58..ff393b8cd 100644
--- a/pkg/tls/creds_watcher.go
+++ b/pkg/tls/creds_watcher.go
@@ -78,7 +78,7 @@ LOOP:
 func (fscw *FsCredsWatcher) UpdateCert(certVal *atomic.Value) error {
 	creds, err := ReadPEMCreds(fscw.keyFilePath, fscw.certFilePath)
 	if err != nil {
-		return fmt.Errorf("failed to read cert from disk: %s", err)
+		return fmt.Errorf("failed to read cert from disk: %w", err)
 	}
 
 	certPEM := creds.EncodePEM()
diff --git a/pkg/version/channels.go b/pkg/version/channels.go
index 49ba52cd2..4b0ba6589 100644
--- a/pkg/version/channels.go
+++ b/pkg/version/channels.go
@@ -45,7 +45,7 @@ func (c Channels) Match(actualVersion string) error {
 
 	actual, err := parseChannelVersion(actualVersion)
 	if err != nil {
-		return fmt.Errorf("failed to parse actual version: %s", err)
+		return fmt.Errorf("failed to parse actual version: %w", err)
 	}
 
 	for _, cv := range c.array {
@@ -95,7 +95,7 @@ func getLatestVersions(ctx context.Context, client *http.Client, url string) (Ch
 	for c, v := range versionRsp {
 		cv, err := parseChannelVersion(v)
 		if err != nil {
-			return Channels{}, fmt.Errorf("unexpected versioncheck response: %s", err)
+			return Channels{}, fmt.Errorf("unexpected versioncheck response: %w", err)
 		}
 
 		if c != cv.channel {
diff --git a/pkg/version/version.go b/pkg/version/version.go
index 8ffa3a817..9949f5852 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -51,11 +51,11 @@ func match(expectedVersion, actualVersion string) error {
 
 	actual, err := parseChannelVersion(actualVersion)
 	if err != nil {
-		return fmt.Errorf("failed to parse actual version: %s", err)
+		return fmt.Errorf("failed to parse actual version: %w", err)
 	}
 	expected, err := parseChannelVersion(expectedVersion)
 	if err != nil {
-		return fmt.Errorf("failed to parse expected version: %s", err)
+		return fmt.Errorf("failed to parse expected version: %w", err)
 	}
 
 	if actual.channel != expected.channel {
diff --git a/proxy-identity/main.go b/proxy-identity/main.go
index d44af4c38..f9dbc5f1e 100644
--- a/proxy-identity/main.go
+++ b/proxy-identity/main.go
@@ -135,11 +135,11 @@ func generateAndStoreCSR(p, id string, key *ecdsa.PrivateKey) ([]byte, error) {
 	}
 	csrb, err := x509.CreateCertificateRequest(rand.Reader, &csr, key)
 	if err != nil {
-		return nil, fmt.Errorf("failed to create CSR: %s", err)
+		return nil, fmt.Errorf("failed to create CSR: %w", err)
 	}
 
 	if err = ioutil.WriteFile(p, csrb, 0600); err != nil {
-		return nil, fmt.Errorf("failed to write CSR: %s", err)
+		return nil, fmt.Errorf("failed to write CSR: %w", err)
 	}
 
 	return csrb, nil
diff --git a/viz/cmd/check.go b/viz/cmd/check.go
index 8a212047d..d5330a637 100644
--- a/viz/cmd/check.go
+++ b/viz/cmd/check.go
@@ -68,7 +68,7 @@ code.`,
 func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions) error {
 	err := options.validate()
 	if err != nil {
-		return fmt.Errorf("Validation error when executing check command: %v", err)
+		return fmt.Errorf("validation error when executing check command: %w", err)
 	}
 
 	hc := vizHealthCheck.NewHealthChecker([]healthcheck.CategoryID{}, &healthcheck.Options{
@@ -83,8 +83,7 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 	})
 	err = hc.InitializeKubeAPIClient()
 	if err != nil {
-		err = fmt.Errorf("Error initializing k8s API client: %s", err)
-		fmt.Fprintln(werr, err)
+		fmt.Fprintf(werr, "Error initializing k8s API client: %s\n", err)
 		os.Exit(1)
 	}
 
diff --git a/viz/cmd/edges.go b/viz/cmd/edges.go
index cc985451c..a95fa7b3e 100644
--- a/viz/cmd/edges.go
+++ b/viz/cmd/edges.go
@@ -122,7 +122,7 @@ func NewCmdEdges() *cobra.Command {
 
 			reqs, err := buildEdgesRequests(args, options)
 			if err != nil {
-				return fmt.Errorf("Error creating edges request: %s", err)
+				return fmt.Errorf("Error creating edges request: %w", err)
 			}
 
 			// The gRPC client is concurrency-safe, so we can reuse it in all the following goroutines
@@ -235,10 +235,10 @@ func edgesRespToRows(resp *pb.EdgesResponse) []*pb.Edge {
 func requestEdgesFromAPI(client pb.ApiClient, req *pb.EdgesRequest) (*pb.EdgesResponse, error) {
 	resp, err := client.Edges(context.Background(), req)
 	if err != nil {
-		return nil, fmt.Errorf("Edges API error: %+v", err)
+		return nil, fmt.Errorf("Edges API error: %w", err)
 	}
 	if e := resp.GetError(); e != nil {
-		return nil, fmt.Errorf("Edges API response error: %+v", e.Error)
+		return nil, fmt.Errorf("Edges API response error: %s", e.Error)
 	}
 	return resp, nil
 }
diff --git a/viz/cmd/profile.go b/viz/cmd/profile.go
index 26dd77f03..fbae413d2 100644
--- a/viz/cmd/profile.go
+++ b/viz/cmd/profile.go
@@ -165,7 +165,7 @@ func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapR
 	}
 	output, err := yaml.Marshal(profile)
 	if err != nil {
-		return fmt.Errorf("Error writing Service Profile: %s", err)
+		return fmt.Errorf("Error writing Service Profile: %w", err)
 	}
 	w.Write(output)
 	return nil
@@ -201,7 +201,7 @@ func routeSpecFromTap(tapByteStream *bufio.Reader, routeLimit int) []*sp.RouteSp
 		if err != nil {
 			// expected errors when hitting the tapDuration deadline
 			var e net.Error
-			if err != io.EOF &&
+			if !errors.Is(err, io.EOF) &&
 				!(errors.As(err, &e) && e.Timeout()) &&
 				!errors.Is(err, context.DeadlineExceeded) &&
 				!strings.HasSuffix(err.Error(), pkg.ErrClosedResponseBody) {
diff --git a/viz/cmd/routes.go b/viz/cmd/routes.go
index 0ac163402..9ee5b32b9 100644
--- a/viz/cmd/routes.go
+++ b/viz/cmd/routes.go
@@ -71,7 +71,7 @@ This command will only display traffic which is sent to a service that has a Ser
 			}
 			req, err := buildTopRoutesRequest(args[0], options)
 			if err != nil {
-				return fmt.Errorf("error creating metrics request while making routes request: %v", err)
+				return fmt.Errorf("error creating metrics request while making routes request: %w", err)
 			}
 
 			output, err := requestRouteStatsFromAPI(
@@ -113,7 +113,7 @@ This command will only display traffic which is sent to a service that has a Ser
 func requestRouteStatsFromAPI(client pb.ApiClient, req *pb.TopRoutesRequest, options *routesOptions) (string, error) {
 	resp, err := client.TopRoutes(context.Background(), req)
 	if err != nil {
-		return "", fmt.Errorf("TopRoutes API error: %v", err)
+		return "", fmt.Errorf("TopRoutes API error: %w", err)
 	}
 	if e := resp.GetError(); e != nil {
 		return "", errors.New(e.Error)
diff --git a/viz/cmd/stat.go b/viz/cmd/stat.go
index 818835360..bc42e1811 100644
--- a/viz/cmd/stat.go
+++ b/viz/cmd/stat.go
@@ -210,7 +210,7 @@ If no resource name is specified, displays stats about all resources of the spec
 
 			reqs, err := buildStatSummaryRequests(args, options)
 			if err != nil {
-				return fmt.Errorf("error creating metrics request while making stats request: %v", err)
+				return fmt.Errorf("error creating metrics request while making stats request: %w", err)
 			}
 
 			// The gRPC client is concurrency-safe, so we can reuse it in all the following goroutines
@@ -283,7 +283,7 @@ func respToRows(resp *pb.StatSummaryResponse) []*pb.StatTable_PodGroup_Row {
 func requestStatsFromAPI(client pb.ApiClient, req *pb.StatSummaryRequest) (*pb.StatSummaryResponse, error) {
 	resp, err := client.StatSummary(context.Background(), req)
 	if err != nil {
-		return nil, fmt.Errorf("StatSummary API error: %v", err)
+		return nil, fmt.Errorf("StatSummary API error: %w", err)
 	}
 	if e := resp.GetError(); e != nil {
 		return nil, fmt.Errorf("StatSummary API response error: %v", e.Error)
diff --git a/viz/cmd/tap.go b/viz/cmd/tap.go
index 78c1617ab..99ff23828 100644
--- a/viz/cmd/tap.go
+++ b/viz/cmd/tap.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -230,24 +231,24 @@ func NewCmdTap() *cobra.Command {
 
 			err := options.validate()
 			if err != nil {
-				return fmt.Errorf("validation error when executing tap command: %v", err)
+				return fmt.Errorf("validation error when executing tap command: %w", err)
 			}
 
 			req, err := pkg.BuildTapByResourceRequest(requestParams)
 			if err != nil {
-				fmt.Fprint(os.Stderr, err.Error())
+				fmt.Fprintln(os.Stderr, err.Error())
 				os.Exit(1)
 			}
 
 			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
 			if err != nil {
-				fmt.Fprint(os.Stderr, err.Error())
+				fmt.Fprintln(os.Stderr, err.Error())
 				os.Exit(1)
 			}
 
 			err = requestTapByResourceFromAPI(cmd.Context(), os.Stdout, k8sAPI, req, options)
 			if err != nil {
-				fmt.Fprint(os.Stderr, err.Error())
+				fmt.Fprintln(os.Stderr, err.Error())
 				os.Exit(1)
 			}
 
@@ -311,10 +312,10 @@ func renderTapEvents(tapByteStream *bufio.Reader, w io.Writer, render renderTapE
 		log.Debug("Waiting for data...")
 		event := tapPb.TapEvent{}
 		err := protohttp.FromByteStreamToProtocolBuffers(tapByteStream, &event)
-		if err == io.EOF {
-			break
-		}
 		if err != nil {
+			if errors.Is(err, io.EOF) {
+				break
+			}
 			fmt.Fprintln(os.Stderr, err)
 			break
 		}
diff --git a/viz/cmd/top.go b/viz/cmd/top.go
index 19a575dd7..1087153a4 100644
--- a/viz/cmd/top.go
+++ b/viz/cmd/top.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"bufio"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"sort"
@@ -478,7 +479,7 @@ func recvEvents(tapByteStream *bufio.Reader, eventCh chan<- *tapPb.TapEvent, clo
 		event := &tapPb.TapEvent{}
 		err := protohttp.FromByteStreamToProtocolBuffers(tapByteStream, event)
 		if err != nil {
-			if err == io.EOF {
+			if errors.Is(err, io.EOF) {
 				fmt.Println("Tap stream terminated")
 			} else if !strings.HasSuffix(err.Error(), pkg.ErrClosedResponseBody) {
 				fmt.Println(err.Error())
@@ -598,7 +599,7 @@ func newRow(req topRequest) (tableRow, error) {
 
 	latency, err := ptypes.Duration(req.rspEnd.GetSinceRequestInit())
 	if err != nil {
-		return tableRow{}, fmt.Errorf("error parsing duration %v: %s", req.rspEnd.GetSinceRequestInit(), err)
+		return tableRow{}, fmt.Errorf("error parsing duration %v: %w", req.rspEnd.GetSinceRequestInit(), err)
 	}
 	// TODO: Once tap events have a classification field, we should use that field
 	// instead of determining success here.
diff --git a/viz/metrics-api/grpc_server.go b/viz/metrics-api/grpc_server.go
index a5af5bf35..d8e64485e 100644
--- a/viz/metrics-api/grpc_server.go
+++ b/viz/metrics-api/grpc_server.go
@@ -84,7 +84,7 @@ func (s *grpcServer) ListPods(ctx context.Context, req *pb.ListPodsRequest) (*pb
 		var err error
 		labelSelector, err = labels.Parse(s)
 		if err != nil {
-			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+			return nil, fmt.Errorf("invalid label selector %q: %w", s, err)
 		}
 	}
 
diff --git a/viz/metrics-api/stat_summary.go b/viz/metrics-api/stat_summary.go
index c15a72a72..cdbd976e9 100644
--- a/viz/metrics-api/stat_summary.go
+++ b/viz/metrics-api/stat_summary.go
@@ -900,7 +900,7 @@ func getLabelSelector(req *pb.StatSummaryRequest) (labels.Selector, error) {
 		var err error
 		labelSelector, err = labels.Parse(s)
 		if err != nil {
-			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+			return nil, fmt.Errorf("invalid label selector %q: %w", s, err)
 		}
 	}
 	return labelSelector, nil
diff --git a/viz/metrics-api/top_routes.go b/viz/metrics-api/top_routes.go
index 13bae053f..ecf6f5c09 100644
--- a/viz/metrics-api/top_routes.go
+++ b/viz/metrics-api/top_routes.go
@@ -363,7 +363,7 @@ func getTopLabelSelector(req *pb.TopRoutesRequest) (labels.Selector, error) {
 		var err error
 		labelSelector, err = labels.Parse(s)
 		if err != nil {
-			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+			return nil, fmt.Errorf("invalid label selector %q: %w", s, err)
 		}
 	}
 	return labelSelector, nil
diff --git a/viz/pkg/healthcheck/healthcheck.go b/viz/pkg/healthcheck/healthcheck.go
index e92d85d90..86b67ec13 100644
--- a/viz/pkg/healthcheck/healthcheck.go
+++ b/viz/pkg/healthcheck/healthcheck.go
@@ -213,7 +213,7 @@ func (hc *HealthChecker) VizCategory() *healthcheck.Category {
 			Warning().
 			WithCheck(func(ctx context.Context) error {
 				if hc.externalPrometheusURL != "" {
-					return &healthcheck.SkipError{Reason: "prometheus is disabled"}
+					return healthcheck.SkipError{Reason: "prometheus is disabled"}
 				}
 
 				// Check for ClusterRoles
diff --git a/viz/tap/api/grpc_server.go b/viz/tap/api/grpc_server.go
index 651b99d52..fcaa1cc33 100644
--- a/viz/tap/api/grpc_server.go
+++ b/viz/tap/api/grpc_server.go
@@ -2,6 +2,7 @@ package api
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -323,11 +324,11 @@ func (s *GRPCTapServer) tapProxy(ctx context.Context, maxRps float32, match *pro
 		}
 		for { // Stream loop
 			event, err := rsp.Recv()
-			if err == io.EOF {
-				log.Debugf("[%s] proxy terminated the stream", addr)
-				break
-			}
 			if err != nil {
+				if errors.Is(err, io.EOF) {
+					log.Debugf("[%s] proxy terminated the stream", addr)
+					break
+				}
 				log.Errorf("[%s] encountered an error: %s", addr, err)
 				return
 			}
@@ -678,7 +679,7 @@ func getLabelSelector(req *tapPb.TapByResourceRequest) (labels.Selector, error)
 		var err error
 		labelSelector, err = labels.Parse(s)
 		if err != nil {
-			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+			return nil, fmt.Errorf("invalid label selector \"%s\": %w", s, err)
 		}
 	}
 	return labelSelector, nil
diff --git a/viz/tap/api/server.go b/viz/tap/api/server.go
index c94ba7708..1b5b56e55 100644
--- a/viz/tap/api/server.go
+++ b/viz/tap/api/server.go
@@ -5,6 +5,7 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -87,7 +88,7 @@ func NewServer(
 
 	lis, err := net.Listen("tcp", addr)
 	if err != nil {
-		return nil, fmt.Errorf("net.Listen failed with: %s", err)
+		return nil, fmt.Errorf("net.Listen failed with: %w", err)
 	}
 
 	s := &Server{
@@ -102,7 +103,7 @@ func NewServer(
 	httpServer.TLSConfig.GetCertificate = s.getCertificate
 
 	if err := watcher.UpdateCert(s.certValue); err != nil {
-		return nil, fmt.Errorf("Failed to initialized certificate: %s", err)
+		return nil, fmt.Errorf("failed to initialized certificate: %w", err)
 	}
 
 	go watcher.ProcessEvents(log, s.certValue, updateEvent, errEvent)
@@ -114,7 +115,7 @@ func NewServer(
 func (a *Server) Start(ctx context.Context) {
 	a.log.Infof("starting tap API server on %s", a.Server.Addr)
 	if err := a.ServeTLS(a.listener, "", ""); err != nil {
-		if err == http.ErrServerClosed {
+		if errors.Is(err, http.ErrServerClosed) {
 			return
 		}
 		a.log.Fatal(err)
@@ -167,13 +168,11 @@ func serverAuth(ctx context.Context, k8sAPI *k8s.API) (string, []string, string,
 	cm, err := k8sAPI.Client.CoreV1().
 		ConfigMaps(metav1.NamespaceSystem).
 		Get(ctx, pkgk8s.ExtensionAPIServerAuthenticationConfigMapName, metav1.GetOptions{})
-
 	if err != nil {
-		return "", nil, "", "", fmt.Errorf("failed to load [%s] config: %s", pkgk8s.ExtensionAPIServerAuthenticationConfigMapName, err)
+		return "", nil, "", "", fmt.Errorf("failed to load [%s] config: %w", pkgk8s.ExtensionAPIServerAuthenticationConfigMapName, err)
 	}
 
 	clientCAPem, ok := cm.Data[pkgk8s.ExtensionAPIServerAuthenticationRequestHeaderClientCAFileKey]
-
 	if !ok {
 		return "", nil, "", "", fmt.Errorf("no client CA cert available for apiextension-server")
 	}
diff --git a/viz/tap/pkg/requests.go b/viz/tap/pkg/requests.go
index 0d3a88b84..9836183d3 100644
--- a/viz/tap/pkg/requests.go
+++ b/viz/tap/pkg/requests.go
@@ -45,7 +45,7 @@ type TapRequestParams struct {
 func BuildTapByResourceRequest(params TapRequestParams) (*tapPb.TapByResourceRequest, error) {
 	target, err := util.BuildResource(params.Namespace, params.Resource)
 	if err != nil {
-		return nil, fmt.Errorf("target resource invalid: %s", err)
+		return nil, fmt.Errorf("target resource invalid: %w", err)
 	}
 	if !contains(util.ValidTargets, target.Type) {
 		return nil, fmt.Errorf("unsupported resource type [%s]", target.Type)
@@ -56,7 +56,7 @@ func BuildTapByResourceRequest(params TapRequestParams) (*tapPb.TapByResourceReq
 	if params.ToResource != "" {
 		destination, err := util.BuildResource(params.ToNamespace, params.ToResource)
 		if err != nil {
-			return nil, fmt.Errorf("destination resource invalid: %s", err)
+			return nil, fmt.Errorf("destination resource invalid: %w", err)
 		}
 		if !contains(ValidTapDestinations, destination.Type) {
 			return nil, fmt.Errorf("unsupported resource type [%s]", destination.Type)
diff --git a/web/srv/api_handlers.go b/web/srv/api_handlers.go
index 090597cdd..91198e993 100644
--- a/web/srv/api_handlers.go
+++ b/web/srv/api_handlers.go
@@ -270,7 +270,8 @@ func (h *handler) handleAPITap(w http.ResponseWriter, req *http.Request, p httpr
 			// If there was a [403] error when initiating a tap, close the
 			// socket with `ClosePolicyViolation` status code so that the error
 			// renders without the error prefix in the banner
-			if httpErr, ok := err.(protohttp.HTTPError); ok && httpErr.Code == http.StatusForbidden {
+			var he protohttp.HTTPError
+			if errors.Is(err, &he) && he.Code == http.StatusForbidden {
 				err := fmt.Errorf("missing authorization, visit %s to remedy", tappkg.TapRbacURL)
 				websocketError(ws, websocket.ClosePolicyViolation, err)
 				return
@@ -286,10 +287,10 @@ func (h *handler) handleAPITap(w http.ResponseWriter, req *http.Request, p httpr
 		for {
 			event := tapPb.TapEvent{}
 			err := protohttp.FromByteStreamToProtocolBuffers(reader, &event)
-			if err == io.EOF {
-				break
-			}
 			if err != nil {
+				if errors.Is(err, io.EOF) {
+					break
+				}
 				websocketError(ws, websocket.CloseInternalServerErr, err)
 				break
 			}


Test patch:
diff --git a/cli/cmd/testdata/inject_emojivoto_pod.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod.golden.yml
index 4a2163ad4..a136d951d 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod.golden.yml
@@ -57,6 +57,7 @@ spec:
       valueFrom:
         fieldRef:
           fieldPath: status.podIPs
+    - name: LINKERD2_PROXY_INBOUND_PORTS
     - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
       value: svc.cluster.local.
     - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE
diff --git a/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml
index 5ae047dd9..bfae69c79 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml
@@ -58,6 +58,7 @@ spec:
       valueFrom:
         fieldRef:
           fieldPath: status.podIPs
+    - name: LINKERD2_PROXY_INBOUND_PORTS
     - name: LINKERD2_PROXY_INGRESS_MODE
       value: "true"
     - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
diff --git a/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml
index f94ab82e1..a9539e344 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml
@@ -59,6 +59,7 @@ spec:
       valueFrom:
         fieldRef:
           fieldPath: status.podIPs
+    - name: LINKERD2_PROXY_INBOUND_PORTS
     - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
       value: svc.cluster.local.
     - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE
diff --git a/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml
index 31569eb6a..a03503071 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml
@@ -61,6 +61,7 @@ spec:
       valueFrom:
         fieldRef:
           fieldPath: status.podIPs
+    - name: LINKERD2_PROXY_INBOUND_PORTS
     - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
       value: svc.cluster.local.
     - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE
diff --git a/controller/k8s/api_test.go b/controller/k8s/api_test.go
index 93cfd67a0..77fcb119f 100644
--- a/controller/k8s/api_test.go
+++ b/controller/k8s/api_test.go
@@ -38,7 +38,7 @@ func newAPI(retry bool, resourceConfigs []string, extraConfigs ...string) (*API,
 
 	api, err := NewFakeAPI(k8sConfigs...)
 	if err != nil {
-		return nil, nil, fmt.Errorf("NewFakeAPI returned an error: %s", err)
+		return nil, nil, fmt.Errorf("NewFakeAPI returned an error: %w", err)
 	}
 
 	if retry {
@@ -937,7 +937,7 @@ status:
 			}
 
 			pods, err := api.GetPodsFor(k8sInputObj, false)
-			if err != exp.err {
+			if !errors.Is(err, exp.err) {
 				t.Fatalf("api.GetPodsFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
 			}
 
@@ -1293,7 +1293,7 @@ spec:
 			}
 
 			services, err := api.GetServicesFor(k8sInputObj, false)
-			if err != exp.err {
+			if !errors.Is(err, exp.err) {
 				t.Fatalf("api.GetServicesFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
 			}
 
diff --git a/controller/webhook/server_test.go b/controller/webhook/server_test.go
index 791ca2578..2db1a50f7 100644
--- a/controller/webhook/server_test.go
+++ b/controller/webhook/server_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"crypto/tls"
+	"errors"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
@@ -48,8 +49,8 @@ func TestShutdown(t *testing.T) {
 
 	go func() {
 		if err := testServer.ListenAndServe(); err != nil {
-			if err != http.ErrServerClosed {
-				t.Errorf("Expected server to be gracefully shutdown with error: %q", http.ErrServerClosed)
+			if !errors.Is(err, http.ErrServerClosed) {
+				t.Errorf("Unexpected error: %s", err)
 			}
 		}
 	}()
@@ -57,6 +58,6 @@ func TestShutdown(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 	if err := testServer.Shutdown(ctx); err != nil {
-		t.Fatal("Unexpected error: ", err)
+		t.Fatalf("Unexpected error: %s", err)
 	}
 }
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index ef4c97d5d..a0136a51b 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -142,7 +142,7 @@ func TestHealthChecker(t *testing.T) {
 			{
 				description: "skip",
 				check: func(context.Context) error {
-					return &SkipError{Reason: "needs skipping"}
+					return SkipError{Reason: "needs skipping"}
 				},
 				retryDeadline: time.Time{},
 			},
@@ -156,7 +156,7 @@ func TestHealthChecker(t *testing.T) {
 			{
 				description: "skipRpc",
 				check: func(context.Context) error {
-					return &SkipError{Reason: "needs skipping"}
+					return SkipError{Reason: "needs skipping"}
 				},
 				retryDeadline: time.Time{},
 			},
diff --git a/pkg/profiles/test_helper.go b/pkg/profiles/test_helper.go
index 31c685bc9..7df264d9b 100644
--- a/pkg/profiles/test_helper.go
+++ b/pkg/profiles/test_helper.go
@@ -47,11 +47,11 @@ func ServiceProfileYamlEquals(actual, expected v1alpha2.ServiceProfile) error {
 	if !reflect.DeepEqual(actual, expected) {
 		actualYaml, err := yaml.Marshal(actual)
 		if err != nil {
-			return fmt.Errorf("Service profile mismatch but failed to marshal actual service profile: %v", err)
+			return fmt.Errorf("Service profile mismatch but failed to marshal actual service profile: %w", err)
 		}
 		expectedYaml, err := yaml.Marshal(expected)
 		if err != nil {
-			return fmt.Errorf("Service profile mismatch but failed to marshal expected service profile: %v", err)
+			return fmt.Errorf("Service profile mismatch but failed to marshal expected service profile: %w", err)
 		}
 		return fmt.Errorf("Expected [%s] but got [%s]", string(expectedYaml), string(actualYaml))
 	}
diff --git a/pkg/protohttp/protohttp_test.go b/pkg/protohttp/protohttp_test.go
index 8ba0862b0..46fb2f03b 100644
--- a/pkg/protohttp/protohttp_test.go
+++ b/pkg/protohttp/protohttp_test.go
@@ -103,10 +103,11 @@ func TestHttpRequestToProto(t *testing.T) {
 			t.Fatalf("Expecting error, got nothing")
 		}
 
-		if httpErr, ok := err.(HTTPError); ok {
+		var he HTTPError
+		if errors.As(err, &he) {
 			expectedStatusCode := http.StatusBadRequest
-			if httpErr.Code != expectedStatusCode || httpErr.WrappedError == nil {
-				t.Fatalf("Expected error status to be [%d] and contain wrapper error, got status [%d] and error [%v]", expectedStatusCode, httpErr.Code, httpErr.WrappedError)
+			if he.Code != expectedStatusCode || he.WrappedError == nil {
+				t.Fatalf("Expected error status to be [%d] and contain wrapper error, got status [%d] and error [%s]", expectedStatusCode, he.Code, he.WrappedError)
 			}
 		} else {
 			t.Fatalf("Expected error to be httpError, got: %v", err)
diff --git a/test/integration/deep/egress/egress_test.go b/test/integration/deep/egress/egress_test.go
index 20bd03f07..52ad438eb 100644
--- a/test/integration/deep/egress/egress_test.go
+++ b/test/integration/deep/egress/egress_test.go
@@ -42,6 +42,7 @@ func TestEgressHttp(t *testing.T) {
 
 		err = TestHelper.CheckPods(ctx, ns, "egress-test", 1)
 		if err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
diff --git a/test/integration/deep/endpoints/endpoints_test.go b/test/integration/deep/endpoints/endpoints_test.go
index 8085b7542..7e76078b8 100644
--- a/test/integration/deep/endpoints/endpoints_test.go
+++ b/test/integration/deep/endpoints/endpoints_test.go
@@ -40,6 +40,7 @@ func TestGoodEndpoints(t *testing.T) {
 
 		err = TestHelper.CheckPods(ctx, ns, "nginx", 1)
 		if err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
@@ -55,7 +56,7 @@ func TestGoodEndpoints(t *testing.T) {
 				err = TestHelper.RetryFor(5*time.Second, func() error {
 					out, err = TestHelper.LinkerdRun("diagnostics", "endpoints", endpointCase.authority, "-ojson")
 					if err != nil {
-						return fmt.Errorf("failed to get endpoints for %s: %s", endpointCase.authority, err)
+						return fmt.Errorf("failed to get endpoints for %s: %w", endpointCase.authority, err)
 					}
 
 					re := regexp.MustCompile(endpointCase.expectedRE)
diff --git a/test/integration/deep/localhost/localhost_test.go b/test/integration/deep/localhost/localhost_test.go
index bcaf0ba3d..afada8e37 100644
--- a/test/integration/deep/localhost/localhost_test.go
+++ b/test/integration/deep/localhost/localhost_test.go
@@ -49,6 +49,7 @@ func TestLocalhostServer(t *testing.T) {
 		for _, deploy := range []string{"nginx", "slow-cooker"} {
 			err = TestHelper.CheckPods(ctx, ns, deploy, 1)
 			if err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
diff --git a/test/integration/deep/opaqueports/opaque_ports_test.go b/test/integration/deep/opaqueports/opaque_ports_test.go
index ad38c7fa8..883b9f93f 100644
--- a/test/integration/deep/opaqueports/opaque_ports_test.go
+++ b/test/integration/deep/opaqueports/opaque_ports_test.go
@@ -60,6 +60,7 @@ func TestOpaquePorts(t *testing.T) {
 		// with the rest of the test.
 		for _, deploy := range []string{opaquePodApp, opaqueSvcApp, opaqueUnmeshedSvcPod} {
 			if err := TestHelper.CheckPods(ctx, opaquePortsNs, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -82,11 +83,11 @@ func TestOpaquePorts(t *testing.T) {
 			err := TestHelper.RetryFor(30*time.Second, func() error {
 				pods, err := TestHelper.GetPods(ctx, opaquePortsNs, map[string]string{"app": opaquePodSC})
 				if err != nil || len(pods) == 0 {
-					return fmt.Errorf("error getting pods\n%s", err)
+					return fmt.Errorf("error getting pods\n%w", err)
 				}
 				metrics, err := getPodMetrics(pods[0], opaquePortsNs)
 				if err != nil {
-					return fmt.Errorf("error getting metrics for pod\n%s", err)
+					return fmt.Errorf("error getting metrics for pod\n%w", err)
 				}
 				if httpRequestTotalMetricRE.MatchString(metrics) {
 					return fmt.Errorf("expected not to find HTTP outbound requests when pod is opaque\n%s", metrics)
@@ -94,11 +95,11 @@ func TestOpaquePorts(t *testing.T) {
 				// Check the application metrics
 				pods, err = TestHelper.GetPods(ctx, opaquePortsNs, map[string]string{"app": opaquePodApp})
 				if err != nil {
-					return fmt.Errorf("error getting pods\n%s", err)
+					return fmt.Errorf("error getting pods\n%w", err)
 				}
 				metrics, err = getPodMetrics(pods[0], opaquePortsNs)
 				if err != nil {
-					return fmt.Errorf("error getting metrics for pod\n%s", err)
+					return fmt.Errorf("error getting metrics for pod\n%w", err)
 				}
 				if !tcpMetricRE.MatchString(metrics) {
 					return fmt.Errorf("failed to find expected TCP metric when pod is opaque\n%s", metrics)
@@ -108,7 +109,7 @@ func TestOpaquePorts(t *testing.T) {
 			})
 
 			if err != nil {
-				testutil.AnnotatedFatalf(t, "unexpected metric output", "unexpected metric output: %v", err)
+				testutil.AnnotatedFatalf(t, "unexpected metric output", "unexpected metric output: %s", err)
 			}
 		})
 
@@ -117,11 +118,11 @@ func TestOpaquePorts(t *testing.T) {
 			err := TestHelper.RetryFor(30*time.Second, func() error {
 				pods, err := TestHelper.GetPods(ctx, opaquePortsNs, map[string]string{"app": opaqueSvcSC})
 				if err != nil || len(pods) == 0 {
-					return fmt.Errorf("error getting pods\n%s", err)
+					return fmt.Errorf("error getting pods\n%w", err)
 				}
 				metrics, err := getPodMetrics(pods[0], opaquePortsNs)
 				if err != nil {
-					return fmt.Errorf("error getting metrics for pod\n%s", err)
+					return fmt.Errorf("error getting metrics for pod\n%w", err)
 				}
 				if httpRequestTotalMetricRE.MatchString(metrics) {
 					return fmt.Errorf("expected not to find HTTP outbound requests when service is opaque\n%s", metrics)
@@ -129,11 +130,11 @@ func TestOpaquePorts(t *testing.T) {
 				// Check the application metrics
 				pods, err = TestHelper.GetPods(ctx, opaquePortsNs, map[string]string{"app": opaqueSvcApp})
 				if err != nil {
-					return fmt.Errorf("error getting pods\n%s", err)
+					return fmt.Errorf("error getting pods\n%w", err)
 				}
 				metrics, err = getPodMetrics(pods[0], opaquePortsNs)
 				if err != nil {
-					return fmt.Errorf("error getting metrics for pod\n%s", err)
+					return fmt.Errorf("error getting metrics for pod\n%w", err)
 				}
 				if !tcpMetricRE.MatchString(metrics) {
 					return fmt.Errorf("failed to find expected TCP metric when pod is opaque\n%s", metrics)
@@ -154,11 +155,11 @@ func TestOpaquePorts(t *testing.T) {
 				pods, err := TestHelper.GetPods(ctx, opaquePortsNs,
 					map[string]string{"app": opaqueUnmeshedSvcSC})
 				if err != nil {
-					return fmt.Errorf("error getting pods\n%s", err)
+					return fmt.Errorf("error getting pods\n%w", err)
 				}
 				metrics, err := getPodMetrics(pods[0], opaquePortsNs)
 				if err != nil {
-					return fmt.Errorf("error getting metrics for pod\n%s", err)
+					return fmt.Errorf("error getting metrics for pod\n%w", err)
 				}
 
 				if httpRequestTotalUnmeshedRE.MatchString(metrics) {
diff --git a/test/integration/deep/skipports/skip_ports_test.go b/test/integration/deep/skipports/skip_ports_test.go
index 819eebb85..35581e45e 100644
--- a/test/integration/deep/skipports/skip_ports_test.go
+++ b/test/integration/deep/skipports/skip_ports_test.go
@@ -53,6 +53,7 @@ func TestSkipInboundPorts(t *testing.T) {
 		// Check all booksapp deployments are up and running
 		for _, deploy := range booksappDeployments {
 			if err := TestHelper.CheckPods(ctx, ns, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -67,7 +68,7 @@ func TestSkipInboundPorts(t *testing.T) {
 			err := TestHelper.RetryFor(30*time.Second, func() error {
 				pods, err := TestHelper.GetPods(ctx, ns, map[string]string{"app": "webapp"})
 				if err != nil {
-					return fmt.Errorf("error getting pods\n%s", err)
+					return fmt.Errorf("error getting pods\n%w", err)
 				}
 
 				podName := fmt.Sprintf("pod/%s", pods[0].Name)
@@ -75,7 +76,7 @@ func TestSkipInboundPorts(t *testing.T) {
 
 				metrics, err := TestHelper.LinkerdRun(cmd...)
 				if err != nil {
-					return fmt.Errorf("error getting metrics for pod\n%s", err)
+					return fmt.Errorf("error getting metrics for pod\n%w", err)
 				}
 
 				if httpResponseTotalMetricRE.MatchString(metrics) {
diff --git a/test/integration/external/externalissuer/external_issuer_test.go b/test/integration/external/externalissuer/external_issuer_test.go
index 069540bec..ef66b95a0 100644
--- a/test/integration/external/externalissuer/external_issuer_test.go
+++ b/test/integration/external/externalissuer/external_issuer_test.go
@@ -51,6 +51,7 @@ func verifyInstallApp(ctx context.Context, t *testing.T) {
 	}
 
 	if err := TestHelper.CheckPods(ctx, prefixedNs, TestAppBackendDeploymentName, 1); err != nil {
+		//nolint:errorlint
 		if rce, ok := err.(*testutil.RestartCountError); ok {
 			testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 		} else {
@@ -59,6 +60,7 @@ func verifyInstallApp(ctx context.Context, t *testing.T) {
 	}
 
 	if err := TestHelper.CheckPods(ctx, prefixedNs, "slow-cooker", 1); err != nil {
+		//nolint:errorlint
 		if rce, ok := err.(*testutil.RestartCountError); ok {
 			testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 		} else {
diff --git a/test/integration/external/externalresources/rabbitmq_test.go b/test/integration/external/externalresources/rabbitmq_test.go
index d7e91db11..661578ef4 100644
--- a/test/integration/external/externalresources/rabbitmq_test.go
+++ b/test/integration/external/externalresources/rabbitmq_test.go
@@ -37,6 +37,7 @@ func TestRabbitMQDeploy(t *testing.T) {
 			testutil.AnnotatedFatalf(t, "kubectl apply command failed", "'kubectl apply' command failed: %s", err)
 		}
 		if err := TestHelper.CheckPods(ctx, testNamespace, "rabbitmq", 1); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out %s", rce)
 			} else {
@@ -54,6 +55,7 @@ func TestRabbitMQDeploy(t *testing.T) {
 			testutil.AnnotatedFatalf(t, "kubectl apply command failed", "'kubectl apply' command failed: %s", err)
 		}
 		if err := TestHelper.CheckPods(ctx, testNamespace, "rabbitmq-client", 1); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out %s", rce)
 			} else {
@@ -66,11 +68,11 @@ func TestRabbitMQDeploy(t *testing.T) {
 		err = TestHelper.RetryFor(timeout, func() error {
 			out, err := TestHelper.Kubectl("", "-n", testNamespace, "logs", "-lapp=rabbitmq-client", "-crabbitmq-client")
 			if err != nil {
-				return fmt.Errorf("'kubectl logs -l app=rabbitmq-client -c rabbitmq-client' command failed\n%s", err)
+				return fmt.Errorf("'kubectl logs -l app=rabbitmq-client -c rabbitmq-client' command failed\n%w", err)
 			}
 			err = TestHelper.ValidateOutput(out, golden)
 			if err != nil {
-				return fmt.Errorf("received unexpected output\n%s", err.Error())
+				return fmt.Errorf("received unexpected output\n%w", err)
 			}
 			return nil
 		})
diff --git a/test/integration/external/install_test.go b/test/integration/external/install_test.go
index 33e87cd7b..171dda431 100644
--- a/test/integration/external/install_test.go
+++ b/test/integration/external/install_test.go
@@ -173,7 +173,7 @@ func TestCheckVizWithExternalPrometheus(t *testing.T) {
 	err = TestHelper.RetryFor(timeout, func() error {
 		out, err := TestHelper.LinkerdRun(cmd...)
 		if err != nil {
-			return fmt.Errorf("'linkerd viz check' command failed\n%s", err)
+			return fmt.Errorf("'linkerd viz check' command failed\n%w", err)
 		}
 
 		if out != expected.String() {
diff --git a/test/integration/external/stat/stat_test.go b/test/integration/external/stat/stat_test.go
index 01576e6e3..27405e074 100644
--- a/test/integration/external/stat/stat_test.go
+++ b/test/integration/external/stat/stat_test.go
@@ -199,6 +199,7 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 		// wait for deployments to start
 		for _, deploy := range []string{"backend", "failing", "slow-cooker"} {
 			if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -273,49 +274,49 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 func validateRowStats(name, expectedMeshCount, expectedStatus string, rowStats map[string]*testutil.RowStat, isAuthority bool) error {
 	stat, ok := rowStats[name]
 	if !ok {
-		return fmt.Errorf("No stats found for [%s]", name)
+		return fmt.Errorf("no stats found for [%s]", name)
 	}
 
 	if stat.Status != expectedStatus {
-		return fmt.Errorf("Expected status '%s' for '%s', got '%s'",
+		return fmt.Errorf("expected status '%s' for '%s', got '%s'",
 			expectedStatus, name, stat.Status)
 	}
 
 	if stat.Meshed != expectedMeshCount {
-		return fmt.Errorf("Expected mesh count [%s] for [%s], got [%s]",
+		return fmt.Errorf("expected mesh count [%s] for [%s], got [%s]",
 			expectedMeshCount, name, stat.Meshed)
 	}
 
 	expectedSuccessRate := "100.00%"
 	if stat.Success != expectedSuccessRate {
-		return fmt.Errorf("Expected success rate [%s] for [%s], got [%s]",
+		return fmt.Errorf("expected success rate [%s] for [%s], got [%s]",
 			expectedSuccessRate, name, stat.Success)
 	}
 
 	if !strings.HasSuffix(stat.Rps, "rps") {
-		return fmt.Errorf("Unexpected rps for [%s], got [%s]",
+		return fmt.Errorf("unexpected rps for [%s], got [%s]",
 			name, stat.Rps)
 	}
 
 	if !strings.HasSuffix(stat.P50Latency, "ms") {
-		return fmt.Errorf("Unexpected p50 latency for [%s], got [%s]",
+		return fmt.Errorf("unexpected p50 latency for [%s], got [%s]",
 			name, stat.P50Latency)
 	}
 
 	if !strings.HasSuffix(stat.P95Latency, "ms") {
-		return fmt.Errorf("Unexpected p95 latency for [%s], got [%s]",
+		return fmt.Errorf("unexpected p95 latency for [%s], got [%s]",
 			name, stat.P95Latency)
 	}
 
 	if !strings.HasSuffix(stat.P99Latency, "ms") {
-		return fmt.Errorf("Unexpected p99 latency for [%s], got [%s]",
+		return fmt.Errorf("unexpected p99 latency for [%s], got [%s]",
 			name, stat.P99Latency)
 	}
 
 	if stat.TCPOpenConnections != "-" && !isAuthority {
 		_, err := strconv.Atoi(stat.TCPOpenConnections)
 		if err != nil {
-			return fmt.Errorf("Error parsing number of TCP connections [%s]: %s", stat.TCPOpenConnections, err.Error())
+			return fmt.Errorf("error parsing number of TCP connections [%s]: %w", stat.TCPOpenConnections, err)
 		}
 	}
 
diff --git a/test/integration/install/install_test.go b/test/integration/install/install_test.go
index 0ba582014..e91e01d9c 100644
--- a/test/integration/install/install_test.go
+++ b/test/integration/install/install_test.go
@@ -107,6 +107,7 @@ func TestUpgradeTestAppWorksBeforeUpgrade(t *testing.T) {
 		testAppNamespace := "upgrade-test"
 		for _, deploy := range []string{"emoji", "voting", "web"} {
 			if err := TestHelper.CheckPods(ctx, testAppNamespace, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -773,7 +774,7 @@ func testCheckCommand(t *testing.T, stage, expectedVersion, namespace, cliVersio
 		out, err := TestHelper.LinkerdRun(cmd...)
 
 		if err != nil {
-			return fmt.Errorf("'linkerd check' command failed\n%s\n%s", err, out)
+			return fmt.Errorf("'linkerd check' command failed\n%w\n%s", err, out)
 		}
 
 		if !strings.Contains(out, expected) {
@@ -908,6 +909,7 @@ func TestRestarts(t *testing.T) {
 	}
 	for deploy, spec := range expectedDeployments {
 		if err := TestHelper.CheckPods(context.Background(), spec.Namespace, deploy, spec.Replicas); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
diff --git a/test/integration/install/smoke/install_smoke_test.go b/test/integration/install/smoke/install_smoke_test.go
index 81c3f15e2..f164902d9 100644
--- a/test/integration/install/smoke/install_smoke_test.go
+++ b/test/integration/install/smoke/install_smoke_test.go
@@ -59,6 +59,7 @@ func TestSmoke(t *testing.T) {
 		// Wait for pods to in smoke-test dpeloyment to come up
 		for _, deploy := range []string{"smoke-test-terminus", "smoke-test-gateway"} {
 			if err := TestHelper.CheckPods(ctx, ns, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -77,7 +78,7 @@ func TestSmoke(t *testing.T) {
 		err = TestHelper.RetryFor(timeout, func() error {
 			out, err := TestHelper.LinkerdRun(cmd...)
 			if err != nil {
-				return fmt.Errorf("'linkerd check' command failed\n%s\n%s", err, out)
+				return fmt.Errorf("'linkerd check' command failed\n%w\n%s", err, out)
 			}
 
 			if !strings.Contains(out, expected) {
diff --git a/test/integration/install/uninstall/uninstall_test.go b/test/integration/install/uninstall/uninstall_test.go
index 02489945d..0edfb441a 100644
--- a/test/integration/install/uninstall/uninstall_test.go
+++ b/test/integration/install/uninstall/uninstall_test.go
@@ -81,6 +81,7 @@ func TestResourcesPostInstall(t *testing.T) {
 	}
 	for deploy, spec := range expectedDeployments {
 		if err := TestHelper.CheckPods(ctx, spec.Namespace, deploy, spec.Replicas); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
diff --git a/test/integration/multicluster/install_test.go b/test/integration/multicluster/install_test.go
index b9911de5a..3132171e5 100644
--- a/test/integration/multicluster/install_test.go
+++ b/test/integration/multicluster/install_test.go
@@ -110,11 +110,11 @@ func TestCheckMulticluster(t *testing.T) {
 		err := TestHelper.RetryFor(timeout, func() error {
 			out, err := TestHelper.LinkerdRun(cmd...)
 			if err != nil {
-				return fmt.Errorf("'linkerd multicluster check' command failed\n%s", err)
+				return fmt.Errorf("'linkerd multicluster check' command failed\n%w", err)
 			}
 			err = TestHelper.ValidateOutput(out, golden)
 			if err != nil {
-				return fmt.Errorf("received unexpected output\n%s", err.Error())
+				return fmt.Errorf("received unexpected output\n%w", err)
 			}
 			return nil
 		})
diff --git a/test/integration/multicluster/target-statefulset/target_statefulset_test.go b/test/integration/multicluster/target-statefulset/target_statefulset_test.go
index 264c6ad52..37e01a852 100644
--- a/test/integration/multicluster/target-statefulset/target_statefulset_test.go
+++ b/test/integration/multicluster/target-statefulset/target_statefulset_test.go
@@ -65,7 +65,7 @@ func TestMulticlusterStatefulSetTargetTraffic(t *testing.T) {
 			// Check gateway metrics
 			metrics, err := TestHelper.LinkerdRun(dgCmd...)
 			if err != nil {
-				return fmt.Errorf("failed to get metrics for gateway deployment: %s", err)
+				return fmt.Errorf("failed to get metrics for gateway deployment: %w", err)
 			}
 
 			// If no match, it means there are no open tcp conns from gateway to
@@ -90,7 +90,7 @@ func TestMulticlusterStatefulSetTargetTraffic(t *testing.T) {
 			// Check gateway metrics
 			metrics, err := TestHelper.LinkerdRun(dgCmd...)
 			if err != nil {
-				return fmt.Errorf("failed to get metrics for gateway deployment: %s", err)
+				return fmt.Errorf("failed to get metrics for gateway deployment: %w", err)
 			}
 
 			// If no match, it means there are no outbound HTTP requests from
diff --git a/test/integration/multicluster/target2/target_test.go b/test/integration/multicluster/target2/target_test.go
index 341e09fe8..9e82364e8 100644
--- a/test/integration/multicluster/target2/target_test.go
+++ b/test/integration/multicluster/target2/target_test.go
@@ -34,7 +34,7 @@ func TestTargetTraffic(t *testing.T) {
 			"--container", "web-svc",
 		)
 		if err != nil {
-			return fmt.Errorf("%s\n%s", err, out)
+			return fmt.Errorf("%w\n%s", err, out)
 		}
 		// Check for expected error messages
 		for _, row := range strings.Split(out, "\n") {
diff --git a/test/integration/viz/edges/edges_test.go b/test/integration/viz/edges/edges_test.go
index 6c5987547..39f93d8a0 100644
--- a/test/integration/viz/edges/edges_test.go
+++ b/test/integration/viz/edges/edges_test.go
@@ -92,6 +92,7 @@ func TestDirectEdges(t *testing.T) {
 		}
 
 		if err := TestHelper.CheckPods(ctx, testNamespace, "terminus", 1); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
@@ -130,6 +131,7 @@ func TestDirectEdges(t *testing.T) {
 		}
 
 		if err := TestHelper.CheckPods(ctx, testNamespace, "slow-cooker", 1); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
@@ -156,7 +158,7 @@ func TestDirectEdges(t *testing.T) {
 			}
 			var buf bytes.Buffer
 			if err := tpl.Execute(&buf, vars); err != nil {
-				return fmt.Errorf("failed to parse direct_edges.golden template: %s", err)
+				return fmt.Errorf("failed to parse direct_edges.golden template: %w", err)
 			}
 
 			pods, err := TestHelper.Kubectl("", []string{"get", "pods", "-A"}...)
diff --git a/test/integration/viz/install_test.go b/test/integration/viz/install_test.go
index 13264dcf4..eb9599bf6 100644
--- a/test/integration/viz/install_test.go
+++ b/test/integration/viz/install_test.go
@@ -107,7 +107,7 @@ func TestCheckViz(t *testing.T) {
 	err = TestHelper.RetryFor(timeout, func() error {
 		out, err := TestHelper.LinkerdRun(cmd...)
 		if err != nil {
-			return fmt.Errorf("'linkerd viz check' command failed\n%s", err)
+			return fmt.Errorf("'linkerd viz check' command failed\n%w", err)
 		}
 
 		if out != expected.String() {
diff --git a/test/integration/viz/policy/policy_test.go b/test/integration/viz/policy/policy_test.go
index 5abc6973f..14a0d0795 100644
--- a/test/integration/viz/policy/policy_test.go
+++ b/test/integration/viz/policy/policy_test.go
@@ -64,6 +64,7 @@ func TestPolicy(t *testing.T) {
 		// wait for deployments to start
 		for _, deploy := range []string{"web", "emoji", "vote-bot", "voting"} {
 			if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -211,7 +212,7 @@ func validateAuthzRows(name string, rowStats map[string]*testutil.RowStat, isSer
 	if isServer {
 		_, err := strconv.Atoi(stat.TCPOpenConnections)
 		if err != nil {
-			return fmt.Errorf("Error parsing number of TCP connections [%s]: %s", stat.TCPOpenConnections, err.Error())
+			return fmt.Errorf("Error parsing number of TCP connections [%s]: %w", stat.TCPOpenConnections, err)
 		}
 	}
 
diff --git a/test/integration/viz/serviceprofiles/serviceprofiles_test.go b/test/integration/viz/serviceprofiles/serviceprofiles_test.go
index 1f8ab1dbf..7a1e860a1 100644
--- a/test/integration/viz/serviceprofiles/serviceprofiles_test.go
+++ b/test/integration/viz/serviceprofiles/serviceprofiles_test.go
@@ -58,6 +58,7 @@ func testProfiles(t *testing.T) {
 	// wait for deployments to start
 	for _, deploy := range []string{"t1", "t2", "t3", "gateway"} {
 		if err := TestHelper.CheckPods(ctx, testNamespace, deploy, 1); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*testutil.RestartCountError); ok {
 				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
@@ -228,7 +229,7 @@ func assertRouteStat(upstream, namespace, downstream string, t *testing.T, asser
 	err := TestHelper.RetryFor(timeout, func() error {
 		routes, err := getRoutes(upstream, namespace, []string{"--to", downstream})
 		if err != nil {
-			return fmt.Errorf("'linkerd routes' command failed: %s", err)
+			return fmt.Errorf("'linkerd routes' command failed: %w", err)
 		}
 
 		var testRoute *cmd2.JSONRouteStats
diff --git a/test/integration/viz/stat/stat_test.go b/test/integration/viz/stat/stat_test.go
index 28f3c6e2f..048437523 100644
--- a/test/integration/viz/stat/stat_test.go
+++ b/test/integration/viz/stat/stat_test.go
@@ -199,6 +199,7 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 		// wait for deployments to start
 		for _, deploy := range []string{"backend", "failing", "slow-cooker"} {
 			if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
@@ -315,7 +316,7 @@ func validateRowStats(name, expectedMeshCount, expectedStatus string, rowStats m
 	if stat.TCPOpenConnections != "-" && !isAuthority {
 		_, err := strconv.Atoi(stat.TCPOpenConnections)
 		if err != nil {
-			return fmt.Errorf("Error parsing number of TCP connections [%s]: %s", stat.TCPOpenConnections, err.Error())
+			return fmt.Errorf("Error parsing number of TCP connections [%s]: %w", stat.TCPOpenConnections, err)
 		}
 	}
 
diff --git a/test/integration/viz/tap/tap_test.go b/test/integration/viz/tap/tap_test.go
index 7ed75981b..2dfaa5f0c 100644
--- a/test/integration/viz/tap/tap_test.go
+++ b/test/integration/viz/tap/tap_test.go
@@ -86,6 +86,7 @@ func TestCliTap(t *testing.T) {
 		// wait for deployments to start
 		for _, deploy := range []string{"t1", "t2", "t3", "gateway"} {
 			if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
diff --git a/test/integration/viz/tracing/tracing_test.go b/test/integration/viz/tracing/tracing_test.go
index b5c11fcfe..15ff0fd07 100644
--- a/test/integration/viz/tracing/tracing_test.go
+++ b/test/integration/viz/tracing/tracing_test.go
@@ -70,7 +70,7 @@ func TestTracing(t *testing.T) {
 	err = TestHelper.RetryFor(timeout, func() error {
 		out, err := TestHelper.LinkerdRun(checkCmd...)
 		if err != nil {
-			return fmt.Errorf("'linkerd jaeger check' command failed\n%s\n%s", err, out)
+			return fmt.Errorf("'linkerd jaeger check' command failed\n%w\n%s", err, out)
 		}
 
 		pods, err := TestHelper.KubernetesHelper.GetPods(context.Background(), tracingNs, nil)
@@ -133,6 +133,7 @@ func TestTracing(t *testing.T) {
 			{ns: tracingNs, name: "jaeger"},
 		} {
 			if err := TestHelper.CheckPods(ctx, deploy.ns, deploy.name, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
diff --git a/test/integration/viz/trafficsplit/trafficsplit_test.go b/test/integration/viz/trafficsplit/trafficsplit_test.go
index a47543199..07c3a02b1 100644
--- a/test/integration/viz/trafficsplit/trafficsplit_test.go
+++ b/test/integration/viz/trafficsplit/trafficsplit_test.go
@@ -87,6 +87,7 @@ func TestTrafficSplitCliWithSP(t *testing.T) {
 		// wait for deployments to start
 		for _, deploy := range []string{"backend", "failing", "slow-cooker"} {
 			if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
+				//nolint:errorlint
 				if rce, ok := err.(*testutil.RestartCountError); ok {
 					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
 				} else {
diff --git a/testutil/install.go b/testutil/install.go
index 061378543..ea7f93e09 100644
--- a/testutil/install.go
+++ b/testutil/install.go
@@ -27,6 +27,7 @@ func TestResourcesPostInstall(namespace string, services []Service, deploys map[
 	// Tests Pods and Deployments
 	for deploy, spec := range deploys {
 		if err := h.CheckPods(ctx, spec.Namespace, deploy, spec.Replicas); err != nil {
+			//nolint:errorlint
 			if rce, ok := err.(*RestartCountError); ok {
 				AnnotatedWarn(t, "CheckPods timed-out", rce)
 			} else {
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index 48a0e38d8..76a039aab 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -344,6 +344,7 @@ func (h *KubernetesHelper) WaitUntilDeployReady(deploys map[string]DeploySpec) {
 	for deploy, spec := range deploys {
 		if err := h.CheckPods(ctx, spec.Namespace, deploy, 1); err != nil {
 			var out string
+			//nolint:errorlint
 			if rce, ok := err.(*RestartCountError); ok {
 				out = fmt.Sprintf("error running test: failed to wait for deploy/%s to become 'ready', too many restarts (%v)\n", deploy, rce)
 			} else {
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index eeb8a56cc..d7b52dbf6 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -421,7 +421,7 @@ type: kubernetes.io/tls`, base64.StdEncoding.EncodeToString([]byte(root)), base6
 func (h *TestHelper) LinkerdRun(arg ...string) (string, error) {
 	out, stderr, err := h.PipeToLinkerdRun("", arg...)
 	if err != nil {
-		return out, fmt.Errorf("command failed: linkerd %s\n%s\n%s", strings.Join(arg, " "), err, stderr)
+		return out, fmt.Errorf("command failed: linkerd %s\n%w\n%s", strings.Join(arg, " "), err, stderr)
 	}
 	return out, nil
 }
@@ -634,7 +634,7 @@ func (h *TestHelper) HTTPGetURL(url string) (string, error) {
 		defer resp.Body.Close()
 		bytes, err := ioutil.ReadAll(resp.Body)
 		if err != nil {
-			return fmt.Errorf("Error reading response body: %v", err)
+			return fmt.Errorf("Error reading response body: %w", err)
 		}
 		body = string(bytes)
 
@@ -763,7 +763,7 @@ func ParseRows(out string, expectedRowCount, expectedColumnCount int) (map[strin
 func ParseEvents(out string) ([]*corev1.Event, error) {
 	var list corev1.List
 	if err := json.Unmarshal([]byte(out), &list); err != nil {
-		return nil, fmt.Errorf("error unmarshaling list from `kubectl get events`: %s", err)
+		return nil, fmt.Errorf("error unmarshaling list from `kubectl get events`: %w", err)
 	}
 
 	if len(list.Items) == 0 {
@@ -774,7 +774,7 @@ func ParseEvents(out string) ([]*corev1.Event, error) {
 	for _, i := range list.Items {
 		var e corev1.Event
 		if err := json.Unmarshal(i.Raw, &e); err != nil {
-			return nil, fmt.Errorf("error unmarshaling list event from `kubectl get events`: %s", err)
+			return nil, fmt.Errorf("error unmarshaling list event from `kubectl get events`: %w", err)
 		}
 		events = append(events, &e)
 	}
diff --git a/viz/metrics-api/edges_test.go b/viz/metrics-api/edges_test.go
index 76cc63991..1b2520792 100644
--- a/viz/metrics-api/edges_test.go
+++ b/viz/metrics-api/edges_test.go
@@ -2,6 +2,7 @@ package api
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"testing"
 
@@ -66,7 +67,7 @@ func testEdges(t *testing.T, expectations []edgesExpected) {
 		}
 
 		rsp, err := fakeGrpcServer.Edges(context.TODO(), exp.req)
-		if err != exp.err {
+		if !errors.Is(err, exp.err) {
 			t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
 		}
 
diff --git a/viz/metrics-api/grpc_server_test.go b/viz/metrics-api/grpc_server_test.go
index 11d296604..379d2127e 100644
--- a/viz/metrics-api/grpc_server_test.go
+++ b/viz/metrics-api/grpc_server_test.go
@@ -2,6 +2,7 @@ package api
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"reflect"
 	"sort"
@@ -509,7 +510,7 @@ metadata:
 			k8sAPI.Sync(nil)
 
 			rsp, err := fakeGrpcServer.ListServices(context.TODO(), &pb.ListServicesRequest{})
-			if err != exp.err {
+			if !errors.Is(err, exp.err) {
 				t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
 			}
 
diff --git a/viz/metrics-api/stat_summary_test.go b/viz/metrics-api/stat_summary_test.go
index 04d572179..bc9d25588 100644
--- a/viz/metrics-api/stat_summary_test.go
+++ b/viz/metrics-api/stat_summary_test.go
@@ -71,7 +71,7 @@ func testStatSummary(t *testing.T, expectations []statSumExpected) {
 		}
 
 		rsp, err := fakeGrpcServer.StatSummary(context.TODO(), exp.req)
-		if err != exp.err {
+		if !errors.Is(err, exp.err) {
 			t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
 		}
 
diff --git a/viz/metrics-api/top_routes_test.go b/viz/metrics-api/top_routes_test.go
index a5a19608b..b07b72ba0 100644
--- a/viz/metrics-api/top_routes_test.go
+++ b/viz/metrics-api/top_routes_test.go
@@ -2,6 +2,7 @@ package api
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"sort"
 	"testing"
@@ -214,7 +215,7 @@ func testTopRoutes(t *testing.T, expectations []topRoutesExpected) {
 			}
 
 			rsp, err := fakeGrpcServer.TopRoutes(context.TODO(), exp.req)
-			if err != exp.err {
+			if !errors.Is(err, exp.err) {
 				t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
 			}
 
diff --git a/viz/tap/api/server_test.go b/viz/tap/api/server_test.go
index f9d9b1850..e7eb52e5d 100644
--- a/viz/tap/api/server_test.go
+++ b/viz/tap/api/server_test.go
@@ -26,7 +26,7 @@ func TestAPIServerAuth(t *testing.T) {
 		err            error
 	}{
 		{
-			err: fmt.Errorf("failed to load [%s] config: configmaps \"%s\" not found", k8sutils.ExtensionAPIServerAuthenticationConfigMapName, k8sutils.ExtensionAPIServerAuthenticationConfigMapName),
+			err: fmt.Errorf("failed to load [%s] config: configmaps %q not found", k8sutils.ExtensionAPIServerAuthenticationConfigMapName, k8sutils.ExtensionAPIServerAuthenticationConfigMapName),
 		},
 		{
 			k8sRes: []string{`
@@ -63,20 +63,28 @@ data:
 			}
 
 			clientCAPem, allowedNames, usernameHeader, groupHeader, err := serverAuth(ctx, k8sAPI)
-			if !reflect.DeepEqual(err, exp.err) {
-				t.Errorf("apiServerAuth returned unexpected error: %s, expected: %s", err, exp.err)
+
+			if err != nil && exp.err != nil {
+				if err.Error() != exp.err.Error() {
+					t.Errorf("apiServerAuth returned unexpected error: %q, expected: %q", err, exp.err)
+				}
+			} else if err != nil {
+				t.Fatalf("Unexpected error: %s", err)
+			} else if exp.err != nil {
+				t.Fatalf("Did not encounter expected error: %s", err)
 			}
+
 			if clientCAPem != exp.clientCAPem {
-				t.Errorf("apiServerAuth returned unexpected clientCAPem: %s, expected: %s", clientCAPem, exp.clientCAPem)
+				t.Errorf("apiServerAuth returned unexpected clientCAPem: %q, expected: %q", clientCAPem, exp.clientCAPem)
 			}
 			if !reflect.DeepEqual(allowedNames, exp.allowedNames) {
-				t.Errorf("apiServerAuth returned unexpected allowedNames: %s, expected: %s", allowedNames, exp.allowedNames)
+				t.Errorf("apiServerAuth returned unexpected allowedNames: %q, expected: %q", allowedNames, exp.allowedNames)
 			}
 			if usernameHeader != exp.usernameHeader {
-				t.Errorf("apiServerAuth returned unexpected usernameHeader: %s, expected: %s", usernameHeader, exp.usernameHeader)
+				t.Errorf("apiServerAuth returned unexpected usernameHeader: %q, expected: %q", usernameHeader, exp.usernameHeader)
 			}
 			if groupHeader != exp.groupHeader {
-				t.Errorf("apiServerAuth returned unexpected groupHeader: %s, expected: %s", groupHeader, exp.groupHeader)
+				t.Errorf("apiServerAuth returned unexpected groupHeader: %q, expected: %q", groupHeader, exp.groupHeader)
 			}
 		})
 	}