Repository: linkerd/linkerd2
Pull request #4339 (2020-05-06 12:12:52)

Make gateway require TLS for incoming requests
This PR introduces a few changes with the aim of making the multicluster setup a bit more secure:

- The service mirror controller will now be injected
- The proxy on the gateway will require identity on the two ports that are exposed through the gateway (the incoming port and the probes port)
- The probe worker now uses the `l5d-require-id` when doing the HTTP get requests to the gateways to  inform the outbound proxy of the expected identity of the remote peer (the gateway)
- There are two sets of probes on two different ports. One is local and can work without identity (this is for Kubelet) and the other is remote and requires identity (for the prober that is part of service mirror).

This all should guarantee that non TLSed traffic coming from outside the cluster will be banned by the proxy that is injected on the gateway pod. I have tested that and it works. No port skipping is happening anywhere

This all depends on https://github.com/linkerd/linkerd2-proxy/pull/507

zaharidichev: > One possible solution is to define two sets of checks on separate ports. One will be for the traffic that is coming from outside the cluster( the liveliness prober) and will go through the proxy, while the other ones will be for local only traffic and will be coming from Kubelet and bypassing the proxy (we can do that with ` --skip-inbound-ports`, right? )

This is what I did, and it works pretty well actually
olix0r: @zaharidichev This sounds like a good workaround to unblock us, but I still have some reservations about this being a generally feasible approach... We also have to take care to ensure that this healthcheck port doesn't expose any possibility to route requests or access sensitive diagnostic information.

Can you help me think through the alternatives? Can we know the node's IP at pod-creation time and allow traffic that originates from the node?
zaharidichev: > Can you help me think through the alternatives? Can we know the node's IP at pod-creation time and allow traffic that originates from the node?

@olix0r Possibly, but as far as I take it,  pods can move nodes. I also feel it is not the best way to do all that but could not think of a much better way. Alternatively we can rely on something that is NOT an http get request at all for checking health locally. We can configure the healthcheks to actually execute a shell command on the container and check whether the nginx process is running. Now that I think about it.. this sounds like a better approach. 
olix0r: > Possibly, but as far as I take it, pods can move nodes

Not true. A deployment/replicaset creates a pod, and it's pinned to its node/ip/etc throughout the lifetime of the pod. For the pod to "move" it terminates and a new pod is scheduled on another node. The injector hooks into the pod creation event.
zaharidichev: I see, that makes sense. So I guess we can possibly do something like that. I will investigate. What about the possibility os just checking whether the process is alive through a shell command? 
grampelberg: > Can you help me think through the alternatives? Can we know the node's IP at pod-creation time and allow traffic that originates from the node?

We can use the downward API for this.
grampelberg: > (we can do that with --skip-inbound-ports, right? )

I'm concerned about the implications of this. AFAIK services of type `nodePort` would have IP addresses coming from the host and then skip the proxy. Anyone have a good idea on the implications of just skipping with the downward API?
zaharidichev: @grampelberg @olix0r I have updated the description to reflect the discussion from yesterday. I went along with hat we came up with yesterday and it works well. 

Patch:
diff --git a/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml b/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
index 16173bb2f..d06a75613 100644
--- a/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
+++ b/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
@@ -29,6 +29,13 @@ data:
             return 200 "healthy\n";
           }
       }
+      server {
+          listen     {{.Values.localProbePort}};
+          location {{.Values.localProbePath}} {
+            access_log off;
+            return 200 "healthy\n";
+          }
+      }    
     }
 ---    
 apiVersion: apps/v1
@@ -53,6 +60,7 @@ spec:
       annotations:
         {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}        
         linkerd.io/inject: enabled
+        config.linkerd.io/proxy-require-identity-inbound-ports: "{{.Values.probePort}},{{.Values.incomingPort}}"
       labels:
         app: {{.Values.gatewayName}}
     spec:
@@ -65,19 +73,21 @@ spec:
           readinessProbe:
             failureThreshold: 7
             httpGet:
-              path: {{.Values.probePath}}
-              port: {{.Values.probePort}}
+              path: {{.Values.localProbePath}}
+              port: {{.Values.localProbePort}}
           livenessProbe:
             httpGet:
-              path: {{.Values.probePath}}
-              port: {{.Values.probePort}}
-            initialDelaySeconds: 10  
+              path: {{.Values.localProbePath}}
+              port: {{.Values.localProbePort}}
+            initialDelaySeconds: 10
           image: {{.Values.nginxImage}}:{{.Values.nginxImageVersion}}
           ports:
             - name: incoming-port
               containerPort: {{.Values.incomingPort}}
             - name: probe-port
-              containerPort: {{.Values.probePort}}             
+              containerPort: {{.Values.probePort}}
+            - name: local-probe
+              containerPort: {{.Values.localProbePort}}              
           volumeMounts:
             - name: config
               mountPath: /etc/nginx
diff --git a/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml b/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
index adf8f764b..5011cc92d 100644
--- a/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
+++ b/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
@@ -12,14 +12,13 @@ metadata:
   annotations:
     {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
 rules:
-- apiGroups:
-  - ""
-  resources:
-  - services
-  verbs:
-  - list
-  - get
-  - watch
+- apiGroups: [""]
+  resources: ["services"]
+  verbs: ["list", "get", "watch"]
+- apiGroups: [""]
+  resources: ["configmaps"]
+  verbs: ["get"]
+  resourceNames: ["linkerd-config"]  
 ---
 apiVersion: v1
 kind: ServiceAccount
diff --git a/charts/linkerd2-multicluster-remote-setup/values.yaml b/charts/linkerd2-multicluster-remote-setup/values.yaml
index 85abfd267..fc4559d8f 100644
--- a/charts/linkerd2-multicluster-remote-setup/values.yaml
+++ b/charts/linkerd2-multicluster-remote-setup/values.yaml
@@ -12,4 +12,6 @@ linkerdVersion: {version}
 createdByAnnotation: linkerd.io/created-by
 nginxImageVersion: 1.17
 nginxImage: nginx
-proxyOutboundPort: 4140
\ No newline at end of file
+proxyOutboundPort: 4140
+localProbePath: /health-local
+localProbePort: 8888
\ No newline at end of file
diff --git a/charts/linkerd2-service-mirror/templates/service-mirror.yaml b/charts/linkerd2-service-mirror/templates/service-mirror.yaml
index abf10511b..a4d41ce52 100644
--- a/charts/linkerd2-service-mirror/templates/service-mirror.yaml
+++ b/charts/linkerd2-service-mirror/templates/service-mirror.yaml
@@ -48,6 +48,8 @@ spec:
       {{.Values.controllerComponentLabel}}: linkerd-service-mirror
   template:
     metadata:
+      annotations:
+        linkerd.io/inject: enabled
       labels:
         {{.Values.controllerComponentLabel}}: linkerd-service-mirror
     spec:
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 92cc7ffce..341844142 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -48,6 +48,7 @@ global:
     # See https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks
     # for more info on container lifecycle hooks.
     waitBeforeExitSeconds: 0
+    requireIdentityOnInboundPorts: ""
 
   # proxy-init configuration
   proxyInit:
diff --git a/charts/partials/templates/_proxy.tpl b/charts/partials/templates/_proxy.tpl
index ac94b58c0..31fc83eb9 100644
--- a/charts/partials/templates/_proxy.tpl
+++ b/charts/partials/templates/_proxy.tpl
@@ -1,5 +1,9 @@
 {{ define "partials.proxy" -}}
 env:
+{{ if .Values.global.proxy.requireIdentityOnInboundPorts -}}
+- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_IDENTITY
+  value: "{{.Values.global.proxy.requireIdentityOnInboundPorts}}"
+{{ end -}}  
 - name: LINKERD2_PROXY_LOG
   value: {{.Values.global.proxy.logLevel}}
 - name: LINKERD2_PROXY_DESTINATION_SVC_ADDR
diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go
index 174f0e9ba..6bc7a88d4 100644
--- a/cli/cmd/cluster.go
+++ b/cli/cmd/cluster.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"github.com/linkerd/linkerd2/cli/table"
+	configPb "github.com/linkerd/linkerd2/controller/gen/config"
 	pb "github.com/linkerd/linkerd2/controller/gen/public"
 	"github.com/linkerd/linkerd2/pkg/charts"
 	"github.com/linkerd/linkerd2/pkg/charts/multicluster"
@@ -91,14 +92,23 @@ func newSetupRemoteClusterOptionsWithDefault() (*setupRemoteClusterOptions, erro
 
 }
 
-func buildMulticlusterSetupValues(opts *setupRemoteClusterOptions) (*multicluster.Values, error) {
-
+func getLinkerdConfigMap() (*configPb.All, error) {
 	kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
 	if err != nil {
 		return nil, err
 	}
 
 	_, global, err := healthcheck.FetchLinkerdConfigMap(kubeAPI, controlPlaneNamespace)
+	if err != nil {
+		return nil, err
+	}
+
+	return global, nil
+}
+
+func buildMulticlusterSetupValues(opts *setupRemoteClusterOptions) (*multicluster.Values, error) {
+
+	global, err := getLinkerdConfigMap()
 	if err != nil {
 		if kerrors.IsNotFound(err) {
 			return nil, errors.New("you need Linkerd to be installed in order to setup a remote cluster")
@@ -111,6 +121,10 @@ func buildMulticlusterSetupValues(opts *setupRemoteClusterOptions) (*multicluste
 		return nil, err
 	}
 
+	if opts.probePort == defaults.LocalProbePort {
+		return nil, fmt.Errorf("The probe port needs to be different from %d which is the local probe port", defaults.LocalProbePort)
+	}
+
 	defaults.GatewayName = opts.gatewayName
 	defaults.GatewayNamespace = opts.gatewayNamespace
 	defaults.IdentityTrustDomain = global.Global.IdentityContext.TrustDomain
@@ -235,6 +249,15 @@ func newGetCredentialsCommand() *cobra.Command {
 		Short:  "Get cluster credentials as a secret",
 		Args:   cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
+
+			_, err := getLinkerdConfigMap()
+			if err != nil {
+				if kerrors.IsNotFound(err) {
+					return errors.New("you need Linkerd to be installed on a cluster in order to get its credentials")
+				}
+				return err
+			}
+
 			rules := clientcmd.NewDefaultClientConfigLoadingRules()
 			rules.ExplicitPath = kubeconfigPath
 			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
@@ -310,8 +333,9 @@ func newGetCredentialsCommand() *cobra.Command {
 				ObjectMeta: metav1.ObjectMeta{
 					Name: fmt.Sprintf("cluster-credentials-%s", opts.clusterName),
 					Annotations: map[string]string{
-						k8s.RemoteClusterNameLabel:        opts.clusterName,
-						k8s.RemoteClusterDomainAnnotation: opts.remoteClusterDomain,
+						k8s.RemoteClusterNameLabel:                  opts.clusterName,
+						k8s.RemoteClusterDomainAnnotation:           opts.remoteClusterDomain,
+						k8s.RemoteClusterLinkerdNamespaceAnnotation: controlPlaneNamespace,
 					},
 				},
 				Data: map[string][]byte{
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index aa98c847e..b2b077c03 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -131,6 +131,9 @@ sub-folders, or coming from stdin.`,
 	flags.StringVar(&options.traceCollectorSvcAccount, "trace-collector-svc-account", options.traceCollectorSvcAccount,
 		"Service account associated with the Trace collector instance")
 
+	flags.StringSliceVar(&options.requireIdentityOnInboundPorts, "require-identity-on-inbound-ports", options.requireIdentityOnInboundPorts,
+		"Inbound ports on which the proxy should require identity")
+
 	cmd.PersistentFlags().AddFlagSet(flags)
 
 	return cmd
@@ -418,6 +421,10 @@ func (options *proxyConfigOptions) overrideConfigs(configs *cfg.All, overrideAnn
 		overrideAnnotations[k8s.ProxyDisableIdentityAnnotation] = strconv.FormatBool(true)
 	}
 
+	if len(options.requireIdentityOnInboundPorts) > 0 {
+		overrideAnnotations[k8s.ProxyRequireIdentityOnInboundPortsAnnotation] = strings.Join(options.requireIdentityOnInboundPorts, ",")
+	}
+
 	if options.disableTap {
 		overrideAnnotations[k8s.ProxyDisableTapAnnotation] = strconv.FormatBool(true)
 	}
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 4f98d3997..26465d81b 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"os"
 	"regexp"
@@ -216,36 +217,42 @@ func getDefaultNamespace() string {
 // install and inject commands. All fields in this struct should have
 // corresponding flags added in the addProxyConfigFlags func later in this file.
 type proxyConfigOptions struct {
-	proxyVersion             string
-	proxyImage               string
-	initImage                string
-	initImageVersion         string
-	debugImage               string
-	debugImageVersion        string
-	dockerRegistry           string
-	imagePullPolicy          string
-	ignoreInboundPorts       []string
-	ignoreOutboundPorts      []string
-	proxyUID                 int64
-	proxyLogLevel            string
-	proxyInboundPort         uint
-	proxyOutboundPort        uint
-	proxyControlPort         uint
-	proxyAdminPort           uint
-	proxyCPURequest          string
-	proxyMemoryRequest       string
-	proxyCPULimit            string
-	proxyMemoryLimit         string
-	enableExternalProfiles   bool
-	traceCollector           string
-	traceCollectorSvcAccount string
-	waitBeforeExitSeconds    uint64
-	ignoreCluster            bool // not validated by validate()
-	disableIdentity          bool
-	disableTap               bool
+	proxyVersion                  string
+	proxyImage                    string
+	initImage                     string
+	initImageVersion              string
+	debugImage                    string
+	debugImageVersion             string
+	dockerRegistry                string
+	imagePullPolicy               string
+	ignoreInboundPorts            []string
+	ignoreOutboundPorts           []string
+	proxyUID                      int64
+	proxyLogLevel                 string
+	proxyInboundPort              uint
+	proxyOutboundPort             uint
+	proxyControlPort              uint
+	proxyAdminPort                uint
+	proxyCPURequest               string
+	proxyMemoryRequest            string
+	proxyCPULimit                 string
+	proxyMemoryLimit              string
+	enableExternalProfiles        bool
+	traceCollector                string
+	traceCollectorSvcAccount      string
+	waitBeforeExitSeconds         uint64
+	ignoreCluster                 bool // not validated by validate()
+	disableIdentity               bool
+	requireIdentityOnInboundPorts []string
+	disableTap                    bool
 }
 
 func (options *proxyConfigOptions) validate() error {
+
+	if options.disableIdentity && len(options.requireIdentityOnInboundPorts) > 0 {
+		return errors.New("Identity must be enabled when  --require-identity-on-inbound-ports is specified")
+	}
+
 	if options.proxyVersion != "" && !alphaNumDashDot.MatchString(options.proxyVersion) {
 		return fmt.Errorf("%s is not a valid version", options.proxyVersion)
 	}
diff --git a/controller/cmd/service-mirror/probe_manager.go b/controller/cmd/service-mirror/probe_manager.go
index 19d054dd9..cedb9f14a 100644
--- a/controller/cmd/service-mirror/probe_manager.go
+++ b/controller/cmd/service-mirror/probe_manager.go
@@ -131,6 +131,7 @@ func gatewayToProbeSpec(gatewaySpec GatewaySpec) *probeSpec {
 		path:            gatewaySpec.path,
 		port:            gatewaySpec.port,
 		periodInSeconds: gatewaySpec.periodInSeconds,
+		gatewayIdentity: gatewaySpec.identity,
 	}
 }
 
diff --git a/controller/cmd/service-mirror/probe_worker.go b/controller/cmd/service-mirror/probe_worker.go
index c48f75113..af70e4bc6 100644
--- a/controller/cmd/service-mirror/probe_worker.go
+++ b/controller/cmd/service-mirror/probe_worker.go
@@ -7,8 +7,8 @@ import (
 	"sync"
 	"time"
 
+	consts "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
-
 	logging "github.com/sirupsen/logrus"
 )
 
@@ -19,6 +19,7 @@ type probeSpec struct {
 	path            string
 	port            uint32
 	periodInSeconds uint32
+	gatewayIdentity string
 }
 
 // ProbeWorker is responsible for monitoring gateways using a probe specification
@@ -128,8 +129,16 @@ func (pw *ProbeWorker) doProbe() {
 		client := http.Client{
 			Timeout: httpGatewayTimeoutMillis * time.Millisecond,
 		}
+
+		req, err := http.NewRequest("GET", fmt.Sprintf("http://%s:%d/%s", ipToTry, pw.probeSpec.port, pw.probeSpec.path), nil)
+		if err != nil {
+			pw.log.Debugf("Could not create a GET request to gateway: %s", err)
+			return
+		}
+
+		req.Header.Set(consts.RequireIDHeader, pw.probeSpec.gatewayIdentity)
 		start := time.Now()
-		resp, err := client.Get(fmt.Sprintf("http://%s:%d/%s", ipToTry, pw.probeSpec.port, pw.probeSpec.path))
+		resp, err := client.Do(req)
 		end := time.Since(start)
 		if err != nil {
 			pw.log.Errorf("Problem connecting with gateway. Marking as unhealthy %s", err)
diff --git a/controller/tap/server.go b/controller/tap/server.go
index 607da7cec..93e69ee72 100644
--- a/controller/tap/server.go
+++ b/controller/tap/server.go
@@ -29,7 +29,6 @@ import (
 	"k8s.io/client-go/tools/cache"
 )
 
-const requireIDHeader = "l5d-require-id"
 const ipIndex = "ip"
 const defaultMaxRps = 100.0
 
@@ -139,7 +138,7 @@ func (s *GRPCTapServer) TapByResource(req *public.TapByResourceRequest, stream p
 
 		// pass the header metadata into the request context
 		ctx := stream.Context()
-		ctx = metadata.AppendToOutgoingContext(ctx, requireIDHeader, name)
+		ctx = metadata.AppendToOutgoingContext(ctx, pkgK8s.RequireIDHeader, name)
 
 		// initiate a tap on the pod
 		go s.tapProxy(ctx, rpsPerPod, match, extract, pod.Status.PodIP, events)
diff --git a/pkg/admin/admin.go b/pkg/admin/admin.go
index 0bd9d7660..1790b2330 100644
--- a/pkg/admin/admin.go
+++ b/pkg/admin/admin.go
@@ -5,7 +5,6 @@ import (
 	"net/http"
 	"net/http/pprof"
 	"strings"
-	"time"
 
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	log "github.com/sirupsen/logrus"
@@ -23,14 +22,7 @@ func StartServer(addr string) {
 		promHandler: promhttp.Handler(),
 	}
 
-	s := &http.Server{
-		Addr:         addr,
-		Handler:      h,
-		ReadTimeout:  10 * time.Second,
-		WriteTimeout: 10 * time.Second,
-	}
-
-	log.Fatal(s.ListenAndServe())
+	log.Fatal(http.ListenAndServe(addr, h))
 }
 
 func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index 7ef3ed525..0af45b191 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -99,19 +99,20 @@ type (
 
 	// Proxy contains the fields to set the proxy sidecar container
 	Proxy struct {
-		Capabilities           *Capabilities `json:"capabilities"`
-		Component              string        `json:"component"`
-		DisableIdentity        bool          `json:"disableIdentity"`
-		DisableTap             bool          `json:"disableTap"`
-		EnableExternalProfiles bool          `json:"enableExternalProfiles"`
-		Image                  *Image        `json:"image"`
-		LogLevel               string        `json:"logLevel"`
-		SAMountPath            *SAMountPath  `json:"saMountPath"`
-		Ports                  *Ports        `json:"ports"`
-		Resources              *Resources    `json:"resources"`
-		Trace                  *Trace        `json:"trace"`
-		UID                    int64         `json:"uid"`
-		WaitBeforeExitSeconds  uint64        `json:"waitBeforeExitSeconds"`
+		Capabilities                  *Capabilities `json:"capabilities"`
+		Component                     string        `json:"component"`
+		DisableIdentity               bool          `json:"disableIdentity"`
+		DisableTap                    bool          `json:"disableTap"`
+		EnableExternalProfiles        bool          `json:"enableExternalProfiles"`
+		Image                         *Image        `json:"image"`
+		LogLevel                      string        `json:"logLevel"`
+		SAMountPath                   *SAMountPath  `json:"saMountPath"`
+		Ports                         *Ports        `json:"ports"`
+		Resources                     *Resources    `json:"resources"`
+		Trace                         *Trace        `json:"trace"`
+		UID                           int64         `json:"uid"`
+		WaitBeforeExitSeconds         uint64        `json:"waitBeforeExitSeconds"`
+		RequireIdentityOnInboundPorts string        `json:"requireIdentityOnInboundPorts"`
 	}
 
 	// ProxyInit contains the fields to set the proxy-init container
diff --git a/pkg/charts/multicluster/values.go b/pkg/charts/multicluster/values.go
index 3c12177a1..3043e23cc 100644
--- a/pkg/charts/multicluster/values.go
+++ b/pkg/charts/multicluster/values.go
@@ -29,6 +29,8 @@ type Values struct {
 	NginxImage              string `json:"nginxImage"`
 	LinkerdVersion          string `json:"linkerdVersion"`
 	CreatedByAnnotation     string `json:"createdByAnnotation"`
+	LocalProbePath          string `json:"localProbePath"`
+	LocalProbePort          uint32 `json:"localProbePort"`
 }
 
 // NewValues returns a new instance of the Values type.
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 1caee99c3..ece79a8e5 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -362,18 +362,19 @@ type HealthChecker struct {
 	*Options
 
 	// these fields are set in the process of running checks
-	kubeAPI          *k8s.KubernetesAPI
-	kubeVersion      *k8sVersion.Info
-	controlPlanePods []corev1.Pod
-	apiClient        public.APIClient
-	latestVersions   version.Channels
-	serverVersion    string
-	linkerdConfig    *configPb.All
-	uuid             string
-	issuerCert       *tls.Cred
-	trustAnchors     []*x509.Certificate
-	cniDaemonSet     *appsv1.DaemonSet
-	serviceMirrorNs  string
+	kubeAPI              *k8s.KubernetesAPI
+	kubeVersion          *k8sVersion.Info
+	controlPlanePods     []corev1.Pod
+	apiClient            public.APIClient
+	latestVersions       version.Channels
+	serverVersion        string
+	linkerdConfig        *configPb.All
+	uuid                 string
+	issuerCert           *tls.Cred
+	trustAnchors         []*x509.Certificate
+	cniDaemonSet         *appsv1.DaemonSet
+	serviceMirrorNs      string
+	remoteClusterConfigs []*sm.WatchedClusterConfig
 }
 
 // NewHealthChecker returns an initialized HealthChecker
@@ -1302,6 +1303,14 @@ func (hc *HealthChecker) allCategories() []category {
 						return hc.checkRemoteClusterGatewaysHealth(ctx)
 					},
 				},
+				{
+					description: "clusters share trust anchors",
+					hintAnchor:  "l5d-clusters-share-anchors",
+					fatal:       true,
+					check: func(ctx context.Context) error {
+						return hc.checkRemoteClusterAnchors()
+					},
+				},
 			},
 		},
 	}
@@ -1651,6 +1660,71 @@ func (hc *HealthChecker) checkServiceMirrorLocalRBAC() error {
 	return &SkipError{Reason: "not checking muticluster"}
 }
 
+func (hc *HealthChecker) checkRemoteClusterAnchors() error {
+	if len(hc.remoteClusterConfigs) == 0 {
+		return &SkipError{Reason: "no remote cluster configs"}
+	}
+
+	localAnchors, err := tls.DecodePEMCertificates(hc.linkerdConfig.Global.IdentityContext.TrustAnchorsPem)
+	if err != nil {
+		return fmt.Errorf("Cannot parse local trust anchors: %s", err)
+	}
+
+	var offendingClusters []string
+	for _, cfg := range hc.remoteClusterConfigs {
+
+		clientConfig, err := clientcmd.RESTConfigFromKubeConfig(cfg.APIConfig)
+		if err != nil {
+			offendingClusters = append(offendingClusters, fmt.Sprintf("* %s: unable to parse api config", cfg.ClusterName))
+			continue
+		}
+
+		remoteAPI, err := k8s.NewAPIForConfig(clientConfig, "", []string{}, requestTimeout)
+		if err != nil {
+			offendingClusters = append(offendingClusters, fmt.Sprintf("* %s: unable to instantiate api", cfg.ClusterName))
+			continue
+		}
+
+		_, cfMap, err := FetchLinkerdConfigMap(remoteAPI, cfg.LinkerdNamespace)
+		if err != nil {
+			offendingClusters = append(offendingClusters, fmt.Sprintf("* %s: unable to fetch anchors: %s", cfg.ClusterName, err))
+			continue
+		}
+		remoteAnchors, err := tls.DecodePEMCertificates(cfMap.Global.IdentityContext.TrustAnchorsPem)
+		if err != nil {
+			offendingClusters = append(offendingClusters, fmt.Sprintf("* %s: cannot parse trust anchors", cfg.ClusterName))
+			continue
+		}
+
+		// we fail early if the lens are not the same. If they are the
+		// same, we can only compare certs one way and be sure we have
+		// identical anchors
+		if len(remoteAnchors) != len(localAnchors) {
+			offendingClusters = append(offendingClusters, fmt.Sprintf("* %s", cfg.ClusterName))
+			continue
+		}
+
+		localAnchorsMap := make(map[string]*x509.Certificate)
+		for _, c := range localAnchors {
+			localAnchorsMap[string(c.Signature)] = c
+		}
+
+		for _, remote := range remoteAnchors {
+			local, ok := localAnchorsMap[string(remote.Signature)]
+			if !ok || !local.Equal(remote) {
+				offendingClusters = append(offendingClusters, fmt.Sprintf("* %s", cfg.ClusterName))
+				break
+			}
+		}
+	}
+
+	if len(offendingClusters) > 0 {
+		return fmt.Errorf("Problematic clusters:\n\t%s", strings.Join(offendingClusters, "\n\t"))
+	}
+
+	return nil
+}
+
 func (hc *HealthChecker) checkRemoteClusterConnectivity() error {
 	if hc.Options.ShouldCheckMulticluster {
 		options := metav1.ListOptions{
@@ -1703,6 +1777,8 @@ func (hc *HealthChecker) checkRemoteClusterConnectivity() error {
 				errors = append(errors, fmt.Sprintf("* cluster: [%s]: Insufficient Service permissions: %s", config.ClusterName, err))
 			}
 
+			hc.remoteClusterConfigs = append(hc.remoteClusterConfigs, config)
+
 		}
 
 		if len(errors) > 0 {
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index c73036495..f396912c0 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -61,6 +61,7 @@ var (
 		k8s.ProxyMemoryRequestAnnotation,
 		k8s.ProxyUIDAnnotation,
 		k8s.ProxyVersionOverrideAnnotation,
+		k8s.ProxyRequireIdentityOnInboundPortsAnnotation,
 		k8s.ProxyIgnoreInboundPortsAnnotation,
 		k8s.ProxyIgnoreOutboundPortsAnnotation,
 		k8s.ProxyTraceCollectorSvcAddrAnnotation,
@@ -200,6 +201,11 @@ func (conf *ResourceConfig) ParseMetaAndYAML(bytes []byte) (*Report, error) {
 // GetPatch returns the JSON patch containing the proxy and init containers specs, if any.
 // If injectProxy is false, only the config.linkerd.io annotations are set.
 func (conf *ResourceConfig) GetPatch(injectProxy bool) ([]byte, error) {
+
+	if conf.requireIdentityOnInboundPorts() != "" && conf.identityContext() == nil {
+		return nil, fmt.Errorf("%s cannot be set when identity is disabled", k8s.ProxyRequireIdentityOnInboundPortsAnnotation)
+	}
+
 	clusterDomain := conf.configs.GetGlobal().GetClusterDomain()
 	if clusterDomain == "" {
 		clusterDomain = "cluster.local"
@@ -488,9 +494,10 @@ func (conf *ResourceConfig) injectPodSpec(values *patch) {
 			Inbound:  conf.proxyInboundPort(),
 			Outbound: conf.proxyOutboundPort(),
 		},
-		UID:                   conf.proxyUID(),
-		Resources:             conf.proxyResourceRequirements(),
-		WaitBeforeExitSeconds: conf.proxyWaitBeforeExitSeconds(),
+		UID:                           conf.proxyUID(),
+		Resources:                     conf.proxyResourceRequirements(),
+		WaitBeforeExitSeconds:         conf.proxyWaitBeforeExitSeconds(),
+		RequireIdentityOnInboundPorts: conf.requireIdentityOnInboundPorts(),
 	}
 
 	if v := conf.pod.meta.Annotations[k8s.ProxyEnableDebugAnnotation]; v != "" {
@@ -785,6 +792,10 @@ func (conf *ResourceConfig) tapDisabled() bool {
 	return false
 }
 
+func (conf *ResourceConfig) requireIdentityOnInboundPorts() string {
+	return conf.getOverride(k8s.ProxyRequireIdentityOnInboundPortsAnnotation)
+}
+
 func (conf *ResourceConfig) proxyWaitBeforeExitSeconds() uint64 {
 	if override := conf.getOverride(k8s.ProxyWaitBeforeExitSecondsAnnotation); override != "" {
 		waitBeforeExitSeconds, err := strconv.ParseUint(override, 10, 64)
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index f90b7e637..35b1e619c 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -38,6 +38,10 @@ const (
 	// contains the value of the "--requestheader-client-ca-file" flag.
 	ExtensionAPIServerAuthenticationRequestHeaderClientCAFileKey = "requestheader-client-ca-file"
 
+	// RequireIDHeader signals to the proxy that a certain identity should be expected
+	// of the remote peer
+	RequireIDHeader = "l5d-require-id"
+
 	// ControllerNSLabel is injected into mesh-enabled apps, identifying the
 	// namespace of the Linkerd control plane.
 	ControllerNSLabel = Prefix + "/control-plane-ns"
@@ -187,6 +191,10 @@ const (
 	// ProxyVersionOverrideAnnotation can be used to override the proxy version config.
 	ProxyVersionOverrideAnnotation = ProxyConfigAnnotationsPrefix + "/proxy-version"
 
+	// ProxyRequireIdentityOnInboundPortsAnnotation can be used to configure the proxy
+	// to always require identity on inbound ports
+	ProxyRequireIdentityOnInboundPortsAnnotation = ProxyConfigAnnotationsPrefix + "/proxy-require-identity-inbound-ports"
+
 	// ProxyDisableIdentityAnnotation can be used to disable identity on the injected proxy.
 	ProxyDisableIdentityAnnotation = ProxyConfigAnnotationsPrefix + "/disable-identity"
 
@@ -369,6 +377,10 @@ const (
 	// using custom cluster domains
 	RemoteClusterDomainAnnotation = SvcMirrorPrefix + "/remote-cluster-domain"
 
+	// RemoteClusterLinkerdNamespaceAnnotation is present on the secret
+	// carrying the config of the remote cluster
+	RemoteClusterLinkerdNamespaceAnnotation = SvcMirrorPrefix + "/remote-cluster-l5d-ns"
+
 	// RemoteResourceVersionAnnotation is the last observed remote resource
 	// version of a mirrored resource. Useful when doing updates
 	RemoteResourceVersionAnnotation = SvcMirrorPrefix + "/remote-resource-version"
diff --git a/pkg/servicemirror/util.go b/pkg/servicemirror/util.go
index b2928a9d6..89ca1ef76 100644
--- a/pkg/servicemirror/util.go
+++ b/pkg/servicemirror/util.go
@@ -9,9 +9,10 @@ import (
 
 // WatchedClusterConfig contains the needed data to identify a remote cluster
 type WatchedClusterConfig struct {
-	APIConfig     []byte
-	ClusterName   string
-	ClusterDomain string
+	APIConfig        []byte
+	ClusterName      string
+	ClusterDomain    string
+	LinkerdNamespace string
 }
 
 // ParseRemoteClusterSecret extracts the credentials used to access the remote cluster
@@ -19,6 +20,8 @@ func ParseRemoteClusterSecret(secret *corev1.Secret) (*WatchedClusterConfig, err
 	clusterName, hasClusterName := secret.Annotations[consts.RemoteClusterNameLabel]
 	config, hasConfig := secret.Data[consts.ConfigKeyName]
 	domain, hasDomain := secret.Annotations[consts.RemoteClusterDomainAnnotation]
+	l5dNamespace, hasL5dNamespace := secret.Annotations[consts.RemoteClusterLinkerdNamespaceAnnotation]
+
 	if !hasClusterName {
 		return nil, fmt.Errorf("secret of type %s should contain key %s", consts.MirrorSecretType, consts.ConfigKeyName)
 	}
@@ -29,9 +32,14 @@ func ParseRemoteClusterSecret(secret *corev1.Secret) (*WatchedClusterConfig, err
 		return nil, fmt.Errorf("secret should contain remote cluster domain as annotation %s", consts.RemoteClusterDomainAnnotation)
 	}
 
+	if !hasL5dNamespace {
+		return nil, fmt.Errorf("secret should contain remote linkerd installation namespace as annotation %s", consts.RemoteClusterLinkerdNamespaceAnnotation)
+	}
+
 	return &WatchedClusterConfig{
-		APIConfig:     config,
-		ClusterName:   clusterName,
-		ClusterDomain: domain,
+		APIConfig:        config,
+		ClusterName:      clusterName,
+		ClusterDomain:    domain,
+		LinkerdNamespace: l5dNamespace,
 	}, nil
 }


Test patch:
diff --git a/controller/tap/server_test.go b/controller/tap/server_test.go
index 5ded1e7f9..24dbfc27c 100644
--- a/controller/tap/server_test.go
+++ b/controller/tap/server_test.go
@@ -392,8 +392,8 @@ status:
 					t.Fatalf("FromIncomingContext failed given: %+v", mockProxyTapServer.ctx)
 				}
 
-				if !reflect.DeepEqual(md.Get(requireIDHeader), []string{exp.requireID}) {
-					t.Fatalf("Unexpected l5d-require-id header [%+v] expected [%+v]", md.Get(requireIDHeader), []string{exp.requireID})
+				if !reflect.DeepEqual(md.Get(pkgK8s.RequireIDHeader), []string{exp.requireID}) {
+					t.Fatalf("Unexpected l5d-require-id header [%+v] expected [%+v]", md.Get(pkgK8s.RequireIDHeader), []string{exp.requireID})
 				}
 			}
 
diff --git a/pkg/inject/inject_test.go b/pkg/inject/inject_test.go
index bd949ed28..042a01306 100644
--- a/pkg/inject/inject_test.go
+++ b/pkg/inject/inject_test.go
@@ -16,24 +16,25 @@ import (
 )
 
 type expectedProxyConfigs struct {
-	identityContext            *config.IdentityContext
-	image                      string
-	imagePullPolicy            string
-	proxyVersion               string
-	controlPort                int32
-	inboundPort                int32
-	adminPort                  int32
-	outboundPort               int32
-	proxyWaitBeforeExitSeconds uint64
-	logLevel                   string
-	resourceRequirements       *l5dcharts.Resources
-	proxyUID                   int64
-	initImage                  string
-	initImagePullPolicy        string
-	initVersion                string
-	inboundSkipPorts           string
-	outboundSkipPorts          string
-	trace                      *l5dcharts.Trace
+	identityContext               *config.IdentityContext
+	image                         string
+	imagePullPolicy               string
+	proxyVersion                  string
+	controlPort                   int32
+	inboundPort                   int32
+	adminPort                     int32
+	outboundPort                  int32
+	proxyWaitBeforeExitSeconds    uint64
+	logLevel                      string
+	resourceRequirements          *l5dcharts.Resources
+	proxyUID                      int64
+	initImage                     string
+	initImagePullPolicy           string
+	initVersion                   string
+	inboundSkipPorts              string
+	outboundSkipPorts             string
+	requireIdentityOnInboundPorts string
+	trace                         *l5dcharts.Trace
 }
 
 func TestConfigAccessors(t *testing.T) {
@@ -89,27 +90,28 @@ func TestConfigAccessors(t *testing.T) {
 				Template: corev1.PodTemplateSpec{
 					ObjectMeta: metav1.ObjectMeta{
 						Annotations: map[string]string{
-							k8s.ProxyDisableIdentityAnnotation:          "true",
-							k8s.ProxyImageAnnotation:                    "gcr.io/linkerd-io/proxy",
-							k8s.ProxyImagePullPolicyAnnotation:          "Always",
-							k8s.ProxyInitImageAnnotation:                "gcr.io/linkerd-io/proxy-init",
-							k8s.ProxyControlPortAnnotation:              "4000",
-							k8s.ProxyInboundPortAnnotation:              "5000",
-							k8s.ProxyAdminPortAnnotation:                "5001",
-							k8s.ProxyOutboundPortAnnotation:             "5002",
-							k8s.ProxyIgnoreInboundPortsAnnotation:       "4222,6222",
-							k8s.ProxyIgnoreOutboundPortsAnnotation:      "8079,8080",
-							k8s.ProxyCPURequestAnnotation:               "0.15",
-							k8s.ProxyMemoryRequestAnnotation:            "120",
-							k8s.ProxyCPULimitAnnotation:                 "1.5",
-							k8s.ProxyMemoryLimitAnnotation:              "256",
-							k8s.ProxyUIDAnnotation:                      "8500",
-							k8s.ProxyLogLevelAnnotation:                 "debug,linkerd2_proxy=debug",
-							k8s.ProxyEnableExternalProfilesAnnotation:   "false",
-							k8s.ProxyVersionOverrideAnnotation:          proxyVersionOverride,
-							k8s.ProxyTraceCollectorSvcAddrAnnotation:    "oc-collector.tracing:55678",
-							k8s.ProxyTraceCollectorSvcAccountAnnotation: "default",
-							k8s.ProxyWaitBeforeExitSecondsAnnotation:    "123",
+							k8s.ProxyDisableIdentityAnnotation:               "true",
+							k8s.ProxyImageAnnotation:                         "gcr.io/linkerd-io/proxy",
+							k8s.ProxyImagePullPolicyAnnotation:               "Always",
+							k8s.ProxyInitImageAnnotation:                     "gcr.io/linkerd-io/proxy-init",
+							k8s.ProxyControlPortAnnotation:                   "4000",
+							k8s.ProxyInboundPortAnnotation:                   "5000",
+							k8s.ProxyAdminPortAnnotation:                     "5001",
+							k8s.ProxyOutboundPortAnnotation:                  "5002",
+							k8s.ProxyIgnoreInboundPortsAnnotation:            "4222,6222",
+							k8s.ProxyIgnoreOutboundPortsAnnotation:           "8079,8080",
+							k8s.ProxyCPURequestAnnotation:                    "0.15",
+							k8s.ProxyMemoryRequestAnnotation:                 "120",
+							k8s.ProxyCPULimitAnnotation:                      "1.5",
+							k8s.ProxyMemoryLimitAnnotation:                   "256",
+							k8s.ProxyUIDAnnotation:                           "8500",
+							k8s.ProxyLogLevelAnnotation:                      "debug,linkerd2_proxy=debug",
+							k8s.ProxyEnableExternalProfilesAnnotation:        "false",
+							k8s.ProxyVersionOverrideAnnotation:               proxyVersionOverride,
+							k8s.ProxyTraceCollectorSvcAddrAnnotation:         "oc-collector.tracing:55678",
+							k8s.ProxyTraceCollectorSvcAccountAnnotation:      "default",
+							k8s.ProxyWaitBeforeExitSecondsAnnotation:         "123",
+							k8s.ProxyRequireIdentityOnInboundPortsAnnotation: "8888,9999",
 						},
 					},
 					Spec: corev1.PodSpec{},
@@ -145,6 +147,7 @@ func TestConfigAccessors(t *testing.T) {
 					CollectorSvcAddr:    "oc-collector.tracing:55678",
 					CollectorSvcAccount: "default.tracing",
 				},
+				requireIdentityOnInboundPorts: "8888,9999",
 			},
 		},
 		{id: "use defaults",
@@ -417,6 +420,13 @@ func TestConfigAccessors(t *testing.T) {
 				}
 			})
 
+			t.Run("proxyRequireIdentityOnInboundPorts", func(t *testing.T) {
+				expected := testCase.expected.requireIdentityOnInboundPorts
+				if actual := resourceConfig.requireIdentityOnInboundPorts(); expected != actual {
+					t.Errorf("Expected: %v Actual: %v", expected, actual)
+				}
+			})
+
 			t.Run("proxyTraceCollectorService", func(t *testing.T) {
 				var expected *l5dcharts.Trace
 				if testCase.expected.trace != nil {
diff --git a/test/edges/edges_test.go b/test/edges/edges_test.go
index ace1d3493..c7b7774f0 100644
--- a/test/edges/edges_test.go
+++ b/test/edges/edges_test.go
@@ -65,42 +65,42 @@ func TestDirectEdges(t *testing.T) {
 	testNamespace := TestHelper.GetTestNamespace("direct-edges-test")
 	err := TestHelper.CreateDataPlaneNamespaceIfNotExists(testNamespace, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", testNamespace, err)
+		testutil.AnnotatedFatalf(t, "failed to create namespace", "failed to create %s namespace: %s", testNamespace, err)
 	}
 
 	// inject terminus
 
 	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/terminus.yaml")
 	if err != nil {
-		t.Fatalf("'linkerd %s' command failed with %s: %s\n", "inject", err.Error(), stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed", "'linkerd inject' command failed with %s: %s\n", err, stderr)
 	}
 
 	// deploy terminus
 
 	out, err = TestHelper.KubectlApply(out, testNamespace)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "kubectl apply command failed", "kubectl apply command failed\n%s", out)
 	}
 
 	if err := TestHelper.CheckPods(testNamespace, "terminus", 1); err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, "CheckPods timed-out", err)
 	}
 
 	if err := TestHelper.CheckDeployment(testNamespace, "terminus", 1); err != nil {
-		t.Errorf("Error validating deployment [%s]:\n%s", "terminus", err)
+		testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", "terminus", err)
 	}
 
 	// get terminus pod ip
 
 	ip, err := TestHelper.Kubectl("", "-n", testNamespace, "get", "pod", "-ojsonpath=\"{.items[*].status.podIP}\"")
 	if err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, "'kubectl get pod' command failed", err)
 	}
 	ip = strings.Trim(ip, "\"") // strip quotes
 
 	b, err := ioutil.ReadFile("testdata/slow-cooker.yaml")
 	if err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, "error reading file slow-cooker.yaml", err)
 	}
 
 	slowcooker := string(b)
@@ -110,29 +110,30 @@ func TestDirectEdges(t *testing.T) {
 
 	out, stderr, err = TestHelper.PipeToLinkerdRun(slowcooker, "inject", "--manual", "-")
 	if err != nil {
-		t.Fatalf("'linkerd %s' command failed with %s: %s\n", "inject", err.Error(), stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd 'inject' command failed", "'linkerd %s' command failed with %s: %s\n", "inject", err.Error(), stderr)
 	}
 
 	// deploy slow cooker
 
 	out, err = TestHelper.KubectlApply(out, testNamespace)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "kubectl apply command failed", "kubectl apply command failed\n%s", out)
 	}
 
 	if err := TestHelper.CheckPods(testNamespace, "slow-cooker", 1); err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, "CheckPods timed-out", err)
 	}
 
 	if err := TestHelper.CheckDeployment(testNamespace, "slow-cooker", 1); err != nil {
-		t.Errorf("Error validating deployment [%s]:\n%s", "terminus", err)
+		testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "error validating deployment [%s]:\n%s", "terminus", err)
 	}
 
 	// check edges
-	err = TestHelper.RetryFor(50*time.Second, func() error {
+	timeout := 50 * time.Second
+	err = TestHelper.RetryFor(timeout, func() error {
 		out, stderr, err = TestHelper.LinkerdRun("-n", testNamespace, "-o", "json", "edges", "deploy")
 		if err != nil {
-			return fmt.Errorf("linkerd %s command failed with %s: %s", "edges", err.Error(), stderr)
+			return fmt.Errorf("linkerd %s command failed with %s: %s", "edges", err, stderr)
 		}
 
 		tpl := template.Must(template.ParseFiles("testdata/direct_edges.golden"))
@@ -156,6 +157,6 @@ func TestDirectEdges(t *testing.T) {
 	})
 
 	if err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, fmt.Sprintf("timed-out checking edges (%s)", timeout), err)
 	}
 }
diff --git a/test/egress/egress_test.go b/test/egress/egress_test.go
index 2216db3cc..e6404d1d1 100644
--- a/test/egress/egress_test.go
+++ b/test/egress/egress_test.go
@@ -26,22 +26,22 @@ func TestMain(m *testing.M) {
 func TestEgressHttp(t *testing.T) {
 	out, stderr, err := TestHelper.LinkerdRun("inject", "testdata/proxy.yaml")
 	if err != nil {
-		t.Fatalf("Unexpected error: %v\n%s", err, stderr)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v\n%s", err, stderr)
 	}
 
 	prefixedNs := TestHelper.GetTestNamespace("egress-test")
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
+		testutil.AnnotatedFatalf(t, "failed to create namespace", "failed to create %s namespace: %s", prefixedNs, err)
 	}
 	out, err = TestHelper.KubectlApply(out, prefixedNs)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s", err, out)
 	}
 
 	err = TestHelper.CheckPods(prefixedNs, "egress-test", 1)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v", err)
+		testutil.AnnotatedFatal(t, "CheckPods timed-out", err)
 	}
 
 	testCase := func(url, methodToUse string) {
@@ -53,17 +53,17 @@ func TestEgressHttp(t *testing.T) {
 			}
 			out, err := TestHelper.Kubectl("", cmd...)
 			if err != nil {
-				t.Fatalf("Failed to exec %s: %s (%s)", cmd, err, out)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to exec %s", cmd), "failed to exec %s: %s (%s)", cmd, err, out)
 			}
 
 			var status int
 			_, err = fmt.Sscanf(out, "%d", &status)
 			if err != nil {
-				t.Fatalf("Failed to parse status code (%s): %s", out, err)
+				testutil.AnnotatedFatalf(t, "failed to parse status code", "failed to parse status code (%s): %s", out, err)
 			}
 
 			if status < 100 || status >= 500 {
-				t.Fatalf("Got HTTP error code: %d\n", status)
+				testutil.Fatalf(t, "got HTTP error code: %d\n", status)
 			}
 		})
 	}
diff --git a/test/endpoints/endpoints_test.go b/test/endpoints/endpoints_test.go
index 6bf96a4b0..723256d06 100644
--- a/test/endpoints/endpoints_test.go
+++ b/test/endpoints/endpoints_test.go
@@ -36,19 +36,19 @@ func TestGoodEndpoints(t *testing.T) {
 	}
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v\nError output: %s", err, stderr)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v\nError output: %s", err, stderr)
 	}
 
 	tpl := template.Must(template.ParseFiles("testdata/linkerd_endpoints.golden"))
 	vars := struct{ Ns string }{ns}
 	var b bytes.Buffer
 	if err := tpl.Execute(&b, vars); err != nil {
-		t.Fatalf("failed to parse linkerd_endpoints.golden template: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to parse linkerd_endpoints.golden template", "failed to parse linkerd_endpoints.golden template: %s", err)
 	}
 
 	r := regexp.MustCompile(b.String())
 	if !r.MatchString(out) {
-		t.Errorf("Expected output:\n%s\nactual:\n%s", b.String(), out)
+		testutil.AnnotatedErrorf(t, "unexpected output", "expected output:\n%s\nactual:\n%s", b.String(), out)
 	}
 }
 
@@ -56,13 +56,13 @@ func TestGoodEndpoints(t *testing.T) {
 func TestBadEndpoints(t *testing.T) {
 	_, stderr, err := TestHelper.LinkerdRun("endpoints", "foo")
 	if err == nil {
-		t.Fatalf("was expecting an error: %v", err)
+		testutil.AnnotatedFatalf(t, "was expecting an error", "was expecting an error: %v", err)
 	}
 	stderrOut := strings.Split(stderr, "\n")
 	if len(stderrOut) == 0 {
-		t.Fatalf("unexpected output: %s", stderr)
+		testutil.AnnotatedFatalf(t, "unexpected output", "unexpected output: %s", stderr)
 	}
 	if stderrOut[0] != "Error: Destination API error: Invalid authority: foo" {
-		t.Errorf("unexpected error string: %s", stderrOut[0])
+		testutil.AnnotatedErrorf(t, "unexpected error string", "unexpected error string: %s", stderrOut[0])
 	}
 }
diff --git a/test/externalissuer/external_issuer_test.go b/test/externalissuer/external_issuer_test.go
index 5541f69ee..0aaf613cc 100644
--- a/test/externalissuer/external_issuer_test.go
+++ b/test/externalissuer/external_issuer_test.go
@@ -39,30 +39,30 @@ func TestExternalIssuer(t *testing.T) {
 func verifyInstallApp(t *testing.T) {
 	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/external_issuer_application.yaml")
 	if err != nil {
-		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed", "'linkerd inject' command failed\n%s\n%s", out, stderr)
 	}
 
 	prefixedNs := TestHelper.GetTestNamespace(TestAppNamespaceSuffix)
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
+		testutil.AnnotatedFatalf(t, "failed to create namespace", "failed to create %s namespace: %s", prefixedNs, err)
 	}
 	out, err = TestHelper.KubectlApply(out, prefixedNs)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed", "'kubectl apply' command failed\n%s", out)
 	}
 
 	if err := TestHelper.CheckPods(prefixedNs, TestAppBackendDeploymentName, 1); err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, "CheckPods timed-out", err)
 	}
 
 	if err := TestHelper.CheckPods(prefixedNs, "slow-cooker", 1); err != nil {
-		t.Error(err)
+		testutil.AnnotatedError(t, "CheckPods timed-out", err)
 	}
 }
 
-func checkAppWoks(t *testing.T) error {
-	return TestHelper.RetryFor(40*time.Second, func() error {
+func checkAppWoks(t *testing.T, timeout time.Duration) error {
+	return TestHelper.RetryFor(timeout, func() error {
 		args := []string{"stat", "deploy", "-n", TestHelper.GetTestNamespace(TestAppNamespaceSuffix), "--from", "deploy/slow-cooker", "-t", "1m"}
 		out, stderr, err := TestHelper.LinkerdRun(args...)
 		if err != nil {
@@ -83,9 +83,10 @@ func checkAppWoks(t *testing.T) error {
 }
 
 func verifyAppWorksBeforeCertRotation(t *testing.T) {
-	err := checkAppWoks(t)
+	timeout := 40 * time.Second
+	err := checkAppWoks(t, timeout)
 	if err != nil {
-		t.Fatalf("Received error while ensuring test app works (before cert rotation): %s", err)
+		testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out while ensuring test app works (before cert rotation) (%s)", timeout), err)
 	}
 }
 
@@ -95,7 +96,7 @@ func verifyRotateExternalCerts(t *testing.T) {
 	// created
 	secretWithUpdatedData, err := TestHelper.KubernetesHelper.GetSecret(TestHelper.GetLinkerdNamespace(), k8s.IdentityIssuerSecretName+"-new")
 	if err != nil {
-		t.Fatalf("failed to fetch new secret data resource: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to fetch new secret data resource", "failed to fetch new secret data resource: %s", err)
 	}
 
 	roots := secretWithUpdatedData.Data[k8s.IdentityIssuerTrustAnchorsNameExternal]
@@ -103,19 +104,20 @@ func verifyRotateExternalCerts(t *testing.T) {
 	key := secretWithUpdatedData.Data[corev1.TLSPrivateKeyKey]
 
 	if err = TestHelper.CreateTLSSecret(k8s.IdentityIssuerSecretName, string(roots), string(crt), string(key)); err != nil {
-		t.Fatalf("failed to update linkerd-identity-issuer resource: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to update linkerd-identity-issuer resource", "failed to update linkerd-identity-issuer resource: %s", err)
 	}
 }
 
 func verifyIdentityServiceReloadsIssuerCert(t *testing.T) {
 	// check that the identity service has received an IssuerUpdated event
-	err := TestHelper.RetryFor(90*time.Second, func() error {
+	timeout := 90 * time.Second
+	err := TestHelper.RetryFor(timeout, func() error {
 		out, err := TestHelper.Kubectl("",
 			"--namespace", TestHelper.GetLinkerdNamespace(),
 			"get", "events", "--field-selector", "reason=IssuerUpdated", "-ojson",
 		)
 		if err != nil {
-			t.Errorf("kubectl get events command failed with %s\n%s", err, out)
+			testutil.AnnotatedErrorf(t, "'kubectl get events' command failed", "'kubectl get events' command failed with %s\n%s", err, out)
 		}
 
 		events, err := testutil.ParseEvents(out)
@@ -137,7 +139,7 @@ func verifyIdentityServiceReloadsIssuerCert(t *testing.T) {
 		return nil
 	})
 	if err != nil {
-		t.Fatal(err.Error())
+		testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out verifying identity svc reloads issuer cert (%s)", timeout), err)
 	}
 
 }
@@ -151,8 +153,9 @@ func ensureNewCSRSAreServed() {
 }
 
 func verifyAppWorksAfterCertRotation(t *testing.T) {
-	err := checkAppWoks(t)
+	timeout := 40 * time.Second
+	err := checkAppWoks(t, timeout)
 	if err != nil {
-		t.Fatalf("Received error while ensuring test app works (after cert rotation): %s", err)
+		testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out ensuring test app works (after cert rotation) (%s)", timeout), err)
 	}
 }
diff --git a/test/get/get_test.go b/test/get/get_test.go
index 1474d6a5f..39466c175 100644
--- a/test/get/get_test.go
+++ b/test/get/get_test.go
@@ -52,79 +52,80 @@ var (
 func TestCliGet(t *testing.T) {
 	out, stderr, err := TestHelper.LinkerdRun("inject", "testdata/to_be_injected_application.yaml")
 	if err != nil {
-		t.Fatalf("Unexpected error: %v\n%s", err, stderr)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v\n%s", err, stderr)
 	}
 
 	prefixedNs := TestHelper.GetTestNamespace("get-test")
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
+		testutil.AnnotatedFatalf(t, "failed to create namespace", "failed to create %s namespace: %s", prefixedNs, err)
 	}
 	out, err = TestHelper.KubectlApply(out, prefixedNs)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s", err, out)
 	}
 
 	bytes, err := ioutil.ReadFile("testdata/not_to_be_injected_application.yaml")
 	if err != nil {
-		t.Fatalf("Unexpected error: %v", err)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v", err)
 	}
 
 	out, err = TestHelper.KubectlApply(string(bytes), prefixedNs)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s", err, out)
 	}
 
 	// wait for pods to start
 	for deploy, replicas := range deployReplicas {
 		if err := TestHelper.CheckPods(prefixedNs, deploy, replicas); err != nil {
-			t.Error(fmt.Errorf("Error validating pods for deploy [%s]:\n%s", deploy, err))
+			testutil.AnnotatedError(t, "CheckPods timed-out",
+				fmt.Errorf("Error validating pods for deploy [%s]:\n%s", deploy, err))
 		}
 	}
 
 	t.Run("get pods from --all-namespaces", func(t *testing.T) {
 		out, stderr, err = TestHelper.LinkerdRun("get", "pods", "--all-namespaces")
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s\n%s", err, out, stderr)
+			testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s\n%s", err, out, stderr)
 		}
 
 		err := checkPodOutput(out, deployReplicas, "", prefixedNs)
 		if err != nil {
-			t.Fatalf("Pod output check failed:\n%s\nCommand output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "pod output check failed", "pod output check failed:\n%s\nCommand output:\n%s", err, out)
 		}
 	})
 
 	t.Run("get pods from the linkerd namespace", func(t *testing.T) {
 		out, stderr, err = TestHelper.LinkerdRun("get", "pods", "-n", TestHelper.GetLinkerdNamespace())
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s\n%s", err, out, stderr)
+			testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s\n%s", err, out, stderr)
 		}
 
 		err := checkPodOutput(out, linkerdPods, "linkerd-heartbeat", TestHelper.GetLinkerdNamespace())
 		if err != nil {
-			t.Fatalf("Pod output check failed:\n%s\nCommand output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "pod output check failed", "pod output check failed:\n%s\nCommand output:\n%s", err, out)
 		}
 	})
 
 	t.Run("get pods from the default namespace of current context", func(t *testing.T) {
 		out, err := TestHelper.Kubectl("", "config", "set-context", "--namespace="+TestHelper.GetLinkerdNamespace(), "--current")
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s", err, out)
 		}
 
 		out, stderr, err = TestHelper.LinkerdRun("get", "pods")
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s\n%s", err, out, stderr)
+			testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s\n%s", err, out, stderr)
 		}
 
 		err = checkPodOutput(out, linkerdPods, "linkerd-heartbeat", TestHelper.GetLinkerdNamespace())
 		if err != nil {
-			t.Fatalf("Pod output check failed:\n%s\nCommand output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "pod output check failed", "pod output check failed:\n%s\nCommand output:\n%s", err, out)
 		}
 
 		out, err = TestHelper.Kubectl("", "config", "set-context", "--namespace=default", "--current")
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s", err, out)
 		}
 	})
 }
diff --git a/test/inject/inject_test.go b/test/inject/inject_test.go
index 078c0156d..cf949fffe 100644
--- a/test/inject/inject_test.go
+++ b/test/inject/inject_test.go
@@ -44,12 +44,12 @@ func TestInjectManual(t *testing.T) {
 	}
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v: %s", stderr, err)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v: %s", stderr, err)
 	}
 
 	err = validateInject(out, "injected_default.golden")
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "received unexpected output", "received unexpected output\n%s", err.Error())
 	}
 }
 
@@ -83,12 +83,12 @@ func TestInjectManualParams(t *testing.T) {
 
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Unexpected error: %v: %s", stderr, err)
+		testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v: %s", stderr, err)
 	}
 
 	err = validateInject(out, "injected_params.golden")
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "received unexpected output", "received unexpected output\n%s", err.Error())
 	}
 }
 
@@ -96,7 +96,7 @@ func TestInjectAutoNamespaceOverrideAnnotations(t *testing.T) {
 	// Check for Namespace level override of proxy Configurations
 	injectYAML, err := testutil.ReadFile("testdata/inject_test.yaml")
 	if err != nil {
-		t.Fatalf("failed to read inject test file: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to read inject test file", "failed to read inject test file: %s", err)
 	}
 
 	injectNS := "inj-ns-override-test"
@@ -114,7 +114,7 @@ func TestInjectAutoNamespaceOverrideAnnotations(t *testing.T) {
 	ns := TestHelper.GetTestNamespace(injectNS)
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(ns, nsAnnotations)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", ns, err)
+		testutil.AnnotatedFatalf(t, "failed to create namespace", "failed to create %s namespace: %s", ns, err)
 	}
 
 	// patch injectYAML with unique name and pod annotations
@@ -126,22 +126,25 @@ func TestInjectAutoNamespaceOverrideAnnotations(t *testing.T) {
 
 	patchedYAML, err := patchDeploy(injectYAML, deployName, podAnnotations)
 	if err != nil {
-		t.Fatalf("failed to patch inject test YAML in namespace %s for deploy/%s: %s", ns, deployName, err)
+		testutil.AnnotatedFatalf(t, "failed to patch inject test YAML",
+			"failed to patch inject test YAML in namespace %s for deploy/%s: %s", ns, deployName, err)
 	}
 
 	o, err := TestHelper.Kubectl(patchedYAML, "--namespace", ns, "create", "-f", "-")
 	if err != nil {
-		t.Fatalf("failed to create deploy/%s in namespace %s for  %s: %s", deployName, ns, err, o)
+		testutil.AnnotatedFatalf(t, "failed to create deployment", "failed to create deploy/%s in namespace %s for  %s: %s", deployName, ns, err, o)
 	}
 
 	o, err = TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=available", "--timeout=120s", "deploy/"+deployName)
 	if err != nil {
-		t.Fatalf("failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", deployName, ns, err, o)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to wait for condition=available for deploy/%s in namespace %s", deployName, ns),
+			"failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", deployName, ns, err, o)
 	}
 
 	pods, err := TestHelper.GetPodsForDeployment(ns, deployName)
 	if err != nil {
-		t.Fatalf("failed to get pods for namespace %s: %s", ns, err)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to get pods for namespace %s", ns),
+			"failed to get pods for namespace %s: %s", ns, err)
 	}
 
 	containers := pods[0].Spec.Containers
@@ -149,19 +152,19 @@ func TestInjectAutoNamespaceOverrideAnnotations(t *testing.T) {
 
 	// Match the pod configuration with the namespace level overrides
 	if proxyContainer.Resources.Requests["memory"] != resource.MustParse(nsProxyMemReq) {
-		t.Fatalf("proxy memory resource request falied to match with namespace level override")
+		testutil.Fatalf(t, "proxy memory resource request falied to match with namespace level override")
 	}
 
 	// Match with proxy level override
 	if proxyContainer.Resources.Requests["cpu"] != resource.MustParse(podProxyCPUReq) {
-		t.Fatalf("proxy cpu resource request falied to match with pod level override")
+		testutil.Fatalf(t, "proxy cpu resource request falied to match with pod level override")
 	}
 }
 
 func TestInjectAutoAnnotationPermutations(t *testing.T) {
 	injectYAML, err := testutil.ReadFile("testdata/inject_test.yaml")
 	if err != nil {
-		t.Fatalf("failed to read inject test file: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to read inject test file", "failed to read inject test file: %s", err)
 	}
 
 	injectNS := "inject-test"
@@ -181,7 +184,8 @@ func TestInjectAutoAnnotationPermutations(t *testing.T) {
 
 		err = TestHelper.CreateDataPlaneNamespaceIfNotExists(ns, nsAnnotations)
 		if err != nil {
-			t.Fatalf("failed to create %s namespace with annotation %s: %s", ns, nsAnnotation, err)
+			testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace with annotation %s", ns, nsAnnotation),
+				"failed to create %s namespace with annotation %s: %s", ns, nsAnnotation, err)
 		}
 
 		for _, podAnnotation := range injectAnnotations {
@@ -195,12 +199,14 @@ func TestInjectAutoAnnotationPermutations(t *testing.T) {
 
 			patchedYAML, err := patchDeploy(injectYAML, name, podAnnotations)
 			if err != nil {
-				t.Fatalf("failed to patch inject test YAML in namespace %s for deploy/%s: %s", ns, name, err)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to patch inject test YAML in namespace %s for deploy/%s", ns, name),
+					"failed to patch inject test YAML in namespace %s for deploy/%s: %s", ns, name, err)
 			}
 
 			o, err := TestHelper.Kubectl(patchedYAML, "--namespace", ns, "create", "-f", "-")
 			if err != nil {
-				t.Fatalf("failed to create deploy/%s in namespace %s for  %s: %s", name, ns, err, o)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create deploy/%s in namespace %s", name, ns),
+					"failed to create deploy/%s in namespace %s for %s: %s", name, ns, err, o)
 			}
 		}
 	}
@@ -223,16 +229,18 @@ func TestInjectAutoAnnotationPermutations(t *testing.T) {
 
 			o, err := TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=available", "--timeout=120s", "deploy/"+name)
 			if err != nil {
-				t.Fatalf("failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", name, ns, err, o)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to wait for condition=available for deploy/%s in namespace %s", name, ns),
+					"failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", name, ns, err, o)
 			}
 
 			pods, err := TestHelper.GetPodsForDeployment(ns, name)
 			if err != nil {
-				t.Fatalf("failed to get pods for namespace %s: %s", ns, err)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to get pods for namespace %s", ns),
+					"failed to get pods for namespace %s: %s", ns, err)
 			}
 
 			if len(pods) != 1 {
-				t.Fatalf("expected 1 pod for namespace %s, got %d", ns, len(pods))
+				testutil.Fatalf(t, "expected 1 pod for namespace %s, got %d", ns, len(pods))
 			}
 
 			shouldBeInjected := false
@@ -254,29 +262,29 @@ func TestInjectAutoAnnotationPermutations(t *testing.T) {
 
 			if shouldBeInjected {
 				if len(containers) != 2 {
-					t.Fatalf("expected 2 containers for pod %s/%s, got %d", ns, pods[0].GetName(), len(containers))
+					testutil.Fatalf(t, "expected 2 containers for pod %s/%s, got %d", ns, pods[0].GetName(), len(containers))
 				}
 				if containers[0].Name != containerName && containers[1].Name != containerName {
-					t.Fatalf("expected bb-terminus container in pod %s/%s, got %+v", ns, pods[0].GetName(), containers[0])
+					testutil.Fatalf(t, "expected bb-terminus container in pod %s/%s, got %+v", ns, pods[0].GetName(), containers[0])
 				}
 				if containers[0].Name != k8s.ProxyContainerName && containers[1].Name != k8s.ProxyContainerName {
-					t.Fatalf("expected %s container in pod %s/%s, got %+v", ns, pods[0].GetName(), k8s.ProxyContainerName, containers[0])
+					testutil.Fatalf(t, "expected %s container in pod %s/%s, got %+v", ns, pods[0].GetName(), k8s.ProxyContainerName, containers[0])
 				}
 				if len(initContainers) != 1 {
-					t.Fatalf("expected 1 init container for pod %s/%s, got %d", ns, pods[0].GetName(), len(initContainers))
+					testutil.Fatalf(t, "expected 1 init container for pod %s/%s, got %d", ns, pods[0].GetName(), len(initContainers))
 				}
 				if initContainers[0].Name != k8s.InitContainerName {
-					t.Fatalf("expected %s init container in pod %s/%s, got %+v", ns, pods[0].GetName(), k8s.InitContainerName, initContainers[0])
+					testutil.Fatalf(t, "expected %s init container in pod %s/%s, got %+v", ns, pods[0].GetName(), k8s.InitContainerName, initContainers[0])
 				}
 			} else {
 				if len(containers) != 1 {
-					t.Fatalf("expected 1 container for pod %s/%s, got %d", ns, pods[0].GetName(), len(containers))
+					testutil.Fatalf(t, "expected 1 container for pod %s/%s, got %d", ns, pods[0].GetName(), len(containers))
 				}
 				if containers[0].Name != containerName {
-					t.Fatalf("expected bb-terminus container in pod %s/%s, got %s", ns, pods[0].GetName(), containers[0].Name)
+					testutil.Fatalf(t, "expected bb-terminus container in pod %s/%s, got %s", ns, pods[0].GetName(), containers[0].Name)
 				}
 				if len(initContainers) != 0 {
-					t.Fatalf("expected 0 init containers for pod %s/%s, got %d", ns, pods[0].GetName(), len(initContainers))
+					testutil.Fatalf(t, "expected 0 init containers for pod %s/%s, got %d", ns, pods[0].GetName(), len(initContainers))
 				}
 			}
 		}
@@ -286,7 +294,8 @@ func TestInjectAutoAnnotationPermutations(t *testing.T) {
 func TestInjectAutoPod(t *testing.T) {
 	podYAML, err := testutil.ReadFile("testdata/pod.yaml")
 	if err != nil {
-		t.Fatalf("failed to read inject test file: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to read inject test file",
+			"failed to read inject test file: %s", err)
 	}
 
 	injectNS := "inject-pod-test"
@@ -298,30 +307,32 @@ func TestInjectAutoPod(t *testing.T) {
 	ns := TestHelper.GetTestNamespace(injectNS)
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(ns, nsAnnotations)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", ns, err)
+		testutil.AnnotatedFatalf(t, "failed to create namespace", "failed to create %s namespace: %s", ns, err)
 	}
 
 	o, err := TestHelper.Kubectl(podYAML, "--namespace", ns, "create", "-f", "-")
 	if err != nil {
-		t.Fatalf("failed to create pod/%s in namespace %s for %s: %s", podName, ns, err, o)
+		testutil.AnnotatedFatalf(t, "failed to create pod",
+			"failed to create pod/%s in namespace %s for %s: %s", podName, ns, err, o)
 	}
 
 	o, err = TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=initialized", "--timeout=120s", "pod/"+podName)
 	if err != nil {
-		t.Fatalf("failed to wait for condition=initialized for pod/%s in namespace %s: %s: %s", podName, ns, err, o)
+		testutil.AnnotatedFatalf(t, "failed to wait for condition=initialized",
+			"failed to wait for condition=initialized for pod/%s in namespace %s: %s: %s", podName, ns, err, o)
 	}
 
 	pods, err := TestHelper.GetPods(ns, map[string]string{"app": podName})
 	if err != nil {
-		t.Fatalf("failed to get pods for namespace %s: %s", ns, err)
+		testutil.AnnotatedFatalf(t, "failed to get pods", "failed to get pods for namespace %s: %s", ns, err)
 	}
 	if len(pods) != 1 {
-		t.Fatalf("wrong number of pods returned for namespace %s: %d", ns, len(pods))
+		testutil.Fatalf(t, "wrong number of pods returned for namespace %s: %d", ns, len(pods))
 	}
 
 	containers := pods[0].Spec.Containers
 	if proxyContainer := getProxyContainer(containers); proxyContainer == nil {
-		t.Fatalf("pod in namespaces %s wasn't injected", ns)
+		testutil.Fatalf(t, "pod in namespaces %s wasn't injected", ns)
 	}
 }
 
diff --git a/test/install_test.go b/test/install_test.go
index fc1dc8a20..16c0544b4 100644
--- a/test/install_test.go
+++ b/test/install_test.go
@@ -117,7 +117,7 @@ func TestVersionPreInstall(t *testing.T) {
 
 	err := TestHelper.CheckVersion(version)
 	if err != nil {
-		t.Fatalf("Version command failed\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "Version command failed", "Version command failed\n%s", err.Error())
 	}
 }
 
@@ -134,12 +134,12 @@ func TestCheckPreInstall(t *testing.T) {
 	golden := "check.pre.golden"
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Check command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd check' command failed", "'linkerd check' command failed\n%s\n%s", out, stderr)
 	}
 
 	err = TestHelper.ValidateOutput(out, golden)
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "received unexpected output", "received unexpected output\n%s", err.Error())
 	}
 }
 
@@ -163,16 +163,17 @@ func TestUpgradeTestAppWorksBeforeUpgrade(t *testing.T) {
 		testAppNamespace := TestHelper.GetTestNamespace("upgrade-test")
 		for _, deploy := range []string{"emoji", "voting", "web"} {
 			if err := TestHelper.CheckPods(testAppNamespace, deploy, 1); err != nil {
-				t.Error(err)
+				testutil.AnnotatedError(t, "CheckPods timed-out", err)
 			}
 
 			if err := TestHelper.CheckDeployment(testAppNamespace, deploy, 1); err != nil {
-				t.Error(fmt.Errorf("Error validating deployment [%s]:\n%s", deploy, err))
+				testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 			}
 		}
 
 		if err := exerciseTestAppEndpoint("/api/list", testAppNamespace); err != nil {
-			t.Fatalf("Error exercising test app endpoint before upgrade %s", err)
+			testutil.AnnotatedFatalf(t, "error exercising test app endpoint before upgrade",
+				"error exercising test app endpoint before upgrade %s", err)
 		}
 	} else {
 		t.Skip("Skipping for non upgrade test")
@@ -184,7 +185,8 @@ func TestRetrieveUidPreUpgrade(t *testing.T) {
 		var err error
 		configMapUID, err = TestHelper.KubernetesHelper.GetConfigUID(TestHelper.GetLinkerdNamespace())
 		if err != nil || configMapUID == "" {
-			t.Fatalf("Error retrieving linkerd-config's uid %s", err)
+			testutil.AnnotatedFatalf(t, "error retrieving linkerd-config's uid",
+				"error retrieving linkerd-config's uid: %s", err)
 		}
 	}
 }
@@ -214,14 +216,15 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 
 		err := TestHelper.CreateControlPlaneNamespaceIfNotExists(TestHelper.GetLinkerdNamespace())
 		if err != nil {
-			t.Fatalf("failed to create %s namespace: %s", TestHelper.GetLinkerdNamespace(), err)
+			testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", TestHelper.GetLinkerdNamespace()),
+				"failed to create %s namespace: %s", TestHelper.GetLinkerdNamespace(), err)
 		}
 
 		identity := fmt.Sprintf("identity.%s.%s", TestHelper.GetLinkerdNamespace(), TestHelper.GetClusterDomain())
 
 		root, err := tls.GenerateRootCAWithDefaults(identity)
 		if err != nil {
-			t.Fatal(err)
+			testutil.AnnotatedFatal(t, "error generating root CA", err)
 		}
 
 		// instead of passing the roots and key around we generate
@@ -235,12 +238,12 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 			root.Cred.Crt.EncodeCertificatePEM(),
 			root.Cred.Crt.EncodeCertificatePEM(),
 			root.Cred.EncodePrivateKeyPEM()); err != nil {
-			t.Fatal(err)
+			testutil.AnnotatedFatal(t, "error creating TLS secret", err)
 		}
 
 		crt2, err := root.GenerateCA(identity, -1)
 		if err != nil {
-			t.Fatal(err)
+			testutil.AnnotatedFatal(t, "error generating CA", err)
 		}
 
 		if err = TestHelper.CreateTLSSecret(
@@ -248,7 +251,7 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 			root.Cred.Crt.EncodeCertificatePEM(),
 			crt2.Cred.EncodeCertificatePEM(),
 			crt2.Cred.EncodePrivateKeyPEM()); err != nil {
-			t.Fatal(err)
+			testutil.AnnotatedFatal(t, "error creating TLS secret (-new)", err)
 		}
 	}
 
@@ -258,13 +261,15 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 		// test 2-stage install during upgrade
 		out, stderr, err := TestHelper.LinkerdRun(cmd, "config")
 		if err != nil {
-			t.Fatalf("linkerd upgrade config command failed\n%s\n%s", out, stderr)
+			testutil.AnnotatedFatalf(t, "'linkerd upgrade config' command failed",
+				"'linkerd upgrade config' command failed\n%s\n%s", out, stderr)
 		}
 
 		// apply stage 1
 		out, err = TestHelper.KubectlApply(out, "")
 		if err != nil {
-			t.Fatalf("kubectl apply command failed\n%s", out)
+			testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+				"kubectl apply command failed\n%s", out)
 		}
 
 		// prepare for stage 2
@@ -274,7 +279,8 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 	exec := append([]string{cmd}, args...)
 	out, stderr, err := TestHelper.LinkerdRun(exec...)
 	if err != nil {
-		t.Fatalf("linkerd install command failed: \n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd install' command failed",
+			"'linkerd install' command failed: \n%s\n%s", out, stderr)
 	}
 
 	// test `linkerd upgrade --from-manifests`
@@ -285,12 +291,14 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 			"-oyaml",
 		)
 		if err != nil {
-			t.Fatalf("kubectl get command failed with %s\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "'kubectl get' command failed",
+				"'kubectl get' command failed with %s\n%s", err, out)
 		}
 		exec = append(exec, "--from-manifests", "-")
 		upgradeFromManifests, stderr, err := TestHelper.PipeToLinkerdRun(manifests, exec...)
 		if err != nil {
-			t.Fatalf("linkerd upgrade --from-manifests command failed with %s\n%s\n%s", err, stderr, upgradeFromManifests)
+			testutil.AnnotatedFatalf(t, "'linkerd upgrade --from-manifests' command failed",
+				"'linkerd upgrade --from-manifests' command failed with %s\n%s\n%s", err, stderr, upgradeFromManifests)
 		}
 
 		if out != upgradeFromManifests {
@@ -298,18 +306,21 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 			exec := append([]string{cmd}, args...)
 			out, stderr, err := TestHelper.LinkerdRun(exec...)
 			if err != nil {
-				t.Fatalf("command failed: %v\n%s\n%s", exec, out, stderr)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("command failed: %v", exec),
+					"command failed: %v\n%s\n%s", exec, out, stderr)
 			}
 
 			if out != upgradeFromManifests {
-				t.Fatalf("manifest upgrade differs from k8s upgrade.\nk8s upgrade:\n%s\nmanifest upgrade:\n%s", out, upgradeFromManifests)
+				testutil.AnnotatedFatalf(t, "manifest upgrade differs from k8s upgrade",
+					"manifest upgrade differs from k8s upgrade.\nk8s upgrade:\n%s\nmanifest upgrade:\n%s", out, upgradeFromManifests)
 			}
 		}
 	}
 
 	out, err = TestHelper.KubectlApply(out, "")
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 }
@@ -351,7 +362,8 @@ func TestInstallHelm(t *testing.T) {
 	var err error
 	helmTLSCerts, err = tls.GenerateRootCAWithDefaults(cn)
 	if err != nil {
-		t.Fatalf("failed to generate root certificate for identity: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to generate root certificate for identity",
+			"failed to generate root certificate for identity: %s", err)
 	}
 
 	var chartToInstall string
@@ -366,7 +378,8 @@ func TestInstallHelm(t *testing.T) {
 	}
 
 	if stdout, stderr, err := TestHelper.HelmInstall(chartToInstall, args...); err != nil {
-		t.Fatalf("helm install command failed\n%s\n%s", stdout, stderr)
+		testutil.AnnotatedFatalf(t, "'helm install' command failed",
+			"'helm install' command failed\n%s\n%s", stdout, stderr)
 	}
 }
 
@@ -374,23 +387,25 @@ func TestResourcesPostInstall(t *testing.T) {
 	// Tests Namespace
 	err := TestHelper.CheckIfNamespaceExists(TestHelper.GetLinkerdNamespace())
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "received unexpected output",
+			"received unexpected output\n%s", err)
 	}
 
 	// Tests Services
 	for _, svc := range linkerdSvcs {
 		if err := TestHelper.CheckService(TestHelper.GetLinkerdNamespace(), svc); err != nil {
-			t.Error(fmt.Errorf("Error validating service [%s]:\n%s", svc, err))
+			testutil.AnnotatedErrorf(t, fmt.Sprintf("error validating service [%s]", svc),
+				"rrror validating service [%s]:\n%s", svc, err)
 		}
 	}
 
 	// Tests Pods and Deployments
 	for deploy, spec := range testutil.LinkerdDeployReplicas {
 		if err := TestHelper.CheckPods(TestHelper.GetLinkerdNamespace(), deploy, spec.Replicas); err != nil {
-			t.Fatal(fmt.Errorf("Error validating pods for deploy [%s]:\n%s", deploy, err))
+			testutil.AnnotatedFatalf(t, "CheckPods timed-out", "Error validating pods for deploy [%s]:\n%s", deploy, err)
 		}
 		if err := TestHelper.CheckDeployment(TestHelper.GetLinkerdNamespace(), deploy, spec.Replicas); err != nil {
-			t.Fatal(fmt.Errorf("Error validating deploy [%s]:\n%s", deploy, err))
+			testutil.AnnotatedFatalf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 		}
 	}
 }
@@ -406,7 +421,8 @@ func TestCheckHelmStableBeforeUpgrade(t *testing.T) {
 		"create", "serviceaccount", "linkerd-smi-metrics",
 	)
 	if err != nil {
-		t.Fatalf("linkerd-smi-metrics SA creation failed: %s", err)
+		testutil.AnnotatedFatalf(t, "linkerd-smi-metrics SA creation failed",
+			"linkerd-smi-metrics SA creation failed: %s", err)
 	}
 	_, err = TestHelper.Kubectl("",
 		"--namespace", TestHelper.GetLinkerdNamespace(),
@@ -414,7 +430,8 @@ func TestCheckHelmStableBeforeUpgrade(t *testing.T) {
 		"linkerd.io/control-plane-ns="+TestHelper.GetLinkerdNamespace(),
 	)
 	if err != nil {
-		t.Fatalf("linkerd-smi-metrics SA labeling failed: %s", err)
+		testutil.AnnotatedFatalf(t, "linkerd-smi-metrics SA labeling failed",
+			"linkerd-smi-metrics SA labeling failed: %s", err)
 	}
 
 	testCheckCommand(t, "", TestHelper.UpgradeHelmFromVersion(), "", TestHelper.UpgradeHelmFromVersion())
@@ -431,7 +448,8 @@ func TestUpgradeHelm(t *testing.T) {
 		"delete", "serviceaccount", "linkerd-smi-metrics",
 	)
 	if err != nil {
-		t.Fatalf("linkerd-smi-metrics SA deletion failed: %s", err)
+		testutil.AnnotatedFatalf(t, "linkerd-smi-metrics SA deletion failed",
+			"linkerd-smi-metrics SA deletion failed: %s", err)
 	}
 	time.Sleep(3 * time.Second)
 
@@ -442,7 +460,8 @@ func TestUpgradeHelm(t *testing.T) {
 	}
 	args = append(args, helmOverridesEdge(helmTLSCerts)...)
 	if stdout, stderr, err := TestHelper.HelmUpgrade(TestHelper.GetHelmChart(), args...); err != nil {
-		t.Fatalf("helm upgrade command failed\n%s\n%s", stdout, stderr)
+		testutil.AnnotatedFatalf(t, "'helm upgrade' command failed",
+			"'helm upgrade' command failed\n%s\n%s", stdout, stderr)
 	}
 }
 
@@ -450,10 +469,11 @@ func TestRetrieveUidPostUpgrade(t *testing.T) {
 	if TestHelper.UpgradeFromVersion() != "" {
 		newConfigMapUID, err := TestHelper.KubernetesHelper.GetConfigUID(TestHelper.GetLinkerdNamespace())
 		if err != nil || newConfigMapUID == "" {
-			t.Fatalf("Error retrieving linkerd-config's uid %s", err)
+			testutil.AnnotatedFatalf(t, "error retrieving linkerd-config's uid",
+				"error retrieving linkerd-config's uid: %s", err)
 		}
 		if configMapUID != newConfigMapUID {
-			t.Fatalf(
+			testutil.AnnotatedFatalf(t, "linkerd-config's uid after upgrade doesn't match its value before the upgrade",
 				"linkerd-config's uid after upgrade [%s] doesn't match its value before the upgrade [%s]",
 				newConfigMapUID, configMapUID,
 			)
@@ -464,7 +484,8 @@ func TestRetrieveUidPostUpgrade(t *testing.T) {
 func TestVersionPostInstall(t *testing.T) {
 	err := TestHelper.CheckVersion(TestHelper.GetVersion())
 	if err != nil {
-		t.Fatalf("Version command failed\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "Version command failed",
+			"Version command failed\n%s", err.Error())
 	}
 }
 
@@ -482,7 +503,8 @@ func testCheckCommand(t *testing.T, stage string, expectedVersion string, namesp
 		golden = "check.golden"
 	}
 
-	err := TestHelper.RetryFor(time.Minute, func() error {
+	timeout := time.Minute
+	err := TestHelper.RetryFor(timeout, func() error {
 		if cliVersionOverride != "" {
 			cliVOverride := []string{"--cli-version-override", cliVersionOverride}
 			cmd = append(cmd, cliVOverride...)
@@ -490,18 +512,18 @@ func testCheckCommand(t *testing.T, stage string, expectedVersion string, namesp
 		out, stderr, err := TestHelper.LinkerdRun(cmd...)
 
 		if err != nil {
-			return fmt.Errorf("Check command failed\n%s\n%s", stderr, out)
+			return fmt.Errorf("'linkerd check' command failed\n%s\n%s", stderr, out)
 		}
 
 		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%s", err.Error())
 		}
 
 		return nil
 	})
 	if err != nil {
-		t.Fatal(err.Error())
+		testutil.AnnotatedFatal(t, fmt.Sprintf("'linkerd check' command timed-out (%s)", timeout), err)
 	}
 }
 
@@ -518,7 +540,8 @@ func TestUpgradeTestAppWorksAfterUpgrade(t *testing.T) {
 	if TestHelper.UpgradeFromVersion() != "" {
 		testAppNamespace := TestHelper.GetTestNamespace("upgrade-test")
 		if err := exerciseTestAppEndpoint("/api/vote?choice=:policeman:", testAppNamespace); err != nil {
-			t.Fatalf("Error exercising test app endpoint after upgrade %s", err)
+			testutil.AnnotatedFatalf(t, "error exercising test app endpoint after upgrade",
+				"error exercising test app endpoint after upgrade %s", err)
 		}
 	} else {
 		t.Skip("Skipping for non upgrade test")
@@ -530,12 +553,14 @@ func TestInstallSP(t *testing.T) {
 
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("linkerd install-sp command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd install-sp' command failed",
+			"'linkerd install-sp' command failed\n%s\n%s", out, stderr)
 	}
 
 	out, err = TestHelper.KubectlApply(out, TestHelper.GetLinkerdNamespace())
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 }
 
@@ -546,27 +571,32 @@ func TestDashboard(t *testing.T) {
 	outputStream, err := TestHelper.LinkerdRunStream("dashboard", "-p",
 		strconv.Itoa(dashboardPort), "--show", "url")
 	if err != nil {
-		t.Fatalf("Error running command:\n%s", err)
+		testutil.AnnotatedFatalf(t, "error running command",
+			"error running command:\n%s", err)
 	}
 	defer outputStream.Stop()
 
 	outputLines, err := outputStream.ReadUntil(4, 1*time.Minute)
 	if err != nil {
-		t.Fatalf("Error running command:\n%s", err)
+		testutil.AnnotatedFatalf(t, "error running command",
+			"error running command:\n%s", err)
 	}
 
 	output := strings.Join(outputLines, "")
 	if !strings.Contains(output, dashboardURL) {
-		t.Fatalf("Dashboard command failed. Expected url [%s] not present", dashboardURL)
+		testutil.AnnotatedFatalf(t,
+			"dashboard command failed. Expected url [%s] not present", dashboardURL)
 	}
 
 	resp, err := TestHelper.HTTPGetURL(dashboardURL + "/api/version")
 	if err != nil {
-		t.Fatalf("Unexpected error: %v", err)
+		testutil.AnnotatedFatalf(t, "unexpected error",
+			"unexpected error: %v", err)
 	}
 
 	if !strings.Contains(resp, TestHelper.GetVersion()) {
-		t.Fatalf("Dashboard command failed. Expected response [%s] to contain version [%s]",
+		testutil.AnnotatedFatalf(t, "dashboard command failed; response doesn't contain expected version",
+			"dashboard command failed. Expected response [%s] to contain version [%s]",
 			resp, TestHelper.GetVersion())
 	}
 }
@@ -574,7 +604,8 @@ func TestDashboard(t *testing.T) {
 func TestInject(t *testing.T) {
 	resources, err := testutil.ReadFile("testdata/smoke_test.yaml")
 	if err != nil {
-		t.Fatalf("failed to read smoke test file: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to read smoke test file",
+			"failed to read smoke test file: %s", err)
 	}
 
 	for _, tc := range injectionCases {
@@ -586,7 +617,8 @@ func TestInject(t *testing.T) {
 
 			err := TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, tc.annotations)
 			if err != nil {
-				t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", prefixedNs),
+					"failed to create %s namespace: %s", prefixedNs, err)
 			}
 
 			if tc.injectArgs != nil {
@@ -597,12 +629,14 @@ func TestInject(t *testing.T) {
 				var injectReport string
 				out, injectReport, err = TestHelper.LinkerdRun(cmd...)
 				if err != nil {
-					t.Fatalf("linkerd inject command failed: %s\n%s", err, out)
+					testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+						"'linkerd inject' command failed: %s\n%s", err, out)
 				}
 
 				err = TestHelper.ValidateOutput(injectReport, "inject.report.golden")
 				if err != nil {
-					t.Fatalf("Received unexpected output\n%s", err.Error())
+					testutil.AnnotatedFatalf(t, "received unexpected output",
+						"received unexpected output\n%s", err.Error())
 				}
 			} else {
 				out = resources
@@ -610,29 +644,33 @@ func TestInject(t *testing.T) {
 
 			out, err = TestHelper.KubectlApply(out, prefixedNs)
 			if err != nil {
-				t.Fatalf("kubectl apply command failed\n%s", out)
+				testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+					"'kubectl apply' command failed\n%s", out)
 			}
 
 			for _, deploy := range []string{"smoke-test-terminus", "smoke-test-gateway"} {
 				err = TestHelper.CheckPods(prefixedNs, deploy, 1)
 				if err != nil {
-					t.Fatalf("Unexpected error: %v", err)
+					testutil.AnnotatedFatal(t, "CheckPods timed-out", err)
 				}
 			}
 
 			url, err := TestHelper.URLFor(prefixedNs, "smoke-test-gateway", 8080)
 			if err != nil {
-				t.Fatalf("Failed to get URL: %s", err)
+				testutil.AnnotatedFatalf(t, "failed to get URL",
+					"failed to get URL: %s", err)
 			}
 
 			output, err := TestHelper.HTTPGetURL(url)
 			if err != nil {
-				t.Fatalf("Unexpected error: %v %s", err, output)
+				testutil.AnnotatedFatalf(t, "unexpected error",
+					"unexpected error: %v %s", err, output)
 			}
 
 			expectedStringInPayload := "\"payload\":\"BANANA\""
 			if !strings.Contains(output, expectedStringInPayload) {
-				t.Fatalf("Expected application response to contain string [%s], but it was [%s]",
+				testutil.AnnotatedFatalf(t, "application response doesn't contain the expected response",
+					"expected application response to contain string [%s], but it was [%s]",
 					expectedStringInPayload, output)
 			}
 		})
@@ -642,7 +680,8 @@ func TestInject(t *testing.T) {
 func TestServiceProfileDeploy(t *testing.T) {
 	bbProto, err := TestHelper.HTTPGetURL("https://raw.githubusercontent.com/BuoyantIO/bb/master/api.proto")
 	if err != nil {
-		t.Fatalf("Unexpected error: %v %s", err, bbProto)
+		testutil.AnnotatedFatalf(t, "unexpected error",
+			"unexpected error: %v %s", err, bbProto)
 	}
 
 	for _, tc := range injectionCases {
@@ -653,12 +692,14 @@ func TestServiceProfileDeploy(t *testing.T) {
 			cmd := []string{"profile", "-n", prefixedNs, "--proto", "-", "smoke-test-terminus-svc"}
 			bbSP, stderr, err := TestHelper.PipeToLinkerdRun(bbProto, cmd...)
 			if err != nil {
-				t.Fatalf("Unexpected error: %v %s", err, stderr)
+				testutil.AnnotatedFatalf(t, "unexpected error",
+					"unexpected error: %v %s", err, stderr)
 			}
 
 			out, err := TestHelper.KubectlApply(bbSP, prefixedNs)
 			if err != nil {
-				t.Fatalf("kubectl apply command failed: %s\n%s", err, out)
+				testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+					"'kubectl apply' command failed: %s\n%s", err, out)
 			}
 		})
 	}
@@ -704,7 +745,8 @@ func TestLogs(t *testing.T) {
 					"--container", container,
 				)
 				if err != nil {
-					t.Errorf("Error running command:\n%s", err)
+					testutil.AnnotatedErrorf(t, "error running command",
+						"error running command:\n%s", err)
 				}
 				defer outputStream.Stop()
 				// Ignore the error returned, since ReadUntil will return an error if it
@@ -715,7 +757,7 @@ func TestLogs(t *testing.T) {
 					// produce log lines.
 					outputLines, _ = outputStream.ReadUntil(10000, 30*time.Second)
 					if len(outputLines) == 0 {
-						t.Errorf("No logs found for %s", name)
+						testutil.AnnotatedErrorf(t, "no logs found for %s", name)
 					}
 				}
 
@@ -728,7 +770,8 @@ func TestLogs(t *testing.T) {
 							if proxy {
 								t.Logf("Found unexpected proxy error in %s log: %s", name, line)
 							} else {
-								t.Errorf("Found unexpected controller error in %s log: %s", name, line)
+								testutil.AnnotatedErrorf(t,
+									"Found unexpected controller error in %s log: %s", name, line)
 							}
 						}
 					}
@@ -740,7 +783,7 @@ func TestLogs(t *testing.T) {
 		}
 	}
 	if !hasClientGoLogs {
-		t.Errorf("Didn't find any client-go entries")
+		testutil.AnnotatedError(t, "didn't find any client-go entries")
 	}
 }
 
@@ -750,12 +793,13 @@ func TestEvents(t *testing.T) {
 		"get", "events", "-ojson",
 	)
 	if err != nil {
-		t.Errorf("kubectl get events command failed with %s\n%s", err, out)
+		testutil.AnnotatedErrorf(t, "'kubectl get events' command failed",
+			"'kubectl get events' command failed with %s\n%s", err, out)
 	}
 
 	events, err := testutil.ParseEvents(out)
 	if err != nil {
-		t.Fatal(err)
+		testutil.AnnotatedError(t, "error parsing events", err)
 	}
 
 	var unknownEvents []string
@@ -771,14 +815,16 @@ func TestEvents(t *testing.T) {
 	}
 
 	if len(unknownEvents) > 0 {
-		t.Errorf("Found unexpected warning events:\n%s", strings.Join(unknownEvents, "\n"))
+		testutil.AnnotatedErrorf(t, "found unexpected warning events",
+			"found unexpected warning events:\n%s", strings.Join(unknownEvents, "\n"))
 	}
 }
 
 func TestRestarts(t *testing.T) {
 	for deploy, spec := range testutil.LinkerdDeployReplicas {
 		if err := TestHelper.CheckPods(TestHelper.GetLinkerdNamespace(), deploy, spec.Replicas); err != nil {
-			t.Fatal(fmt.Errorf("Error validating pods [%s]:\n%s", deploy, err))
+			testutil.AnnotatedFatalf(t, fmt.Sprintf("error validating pods [%s]", deploy),
+				"error validating pods [%s]:\n%s", deploy, err)
 		}
 	}
 }
diff --git a/test/routes/routes_test.go b/test/routes/routes_test.go
index 976a981b3..69ad2f8c2 100644
--- a/test/routes/routes_test.go
+++ b/test/routes/routes_test.go
@@ -1,6 +1,7 @@
 package get
 
 import (
+	"fmt"
 	"os"
 	"strings"
 	"testing"
@@ -31,7 +32,7 @@ func TestRoutes(t *testing.T) {
 	cmd := []string{"routes", "--namespace", TestHelper.GetLinkerdNamespace(), "deploy"}
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Routes command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd routes' command failed", "'linkerd routes' command failed\n%s\n%s", out, stderr)
 	}
 
 	routeStrings := []struct {
@@ -60,7 +61,8 @@ func TestRoutes(t *testing.T) {
 	for _, r := range routeStrings {
 		count := strings.Count(out, r.s)
 		if count != r.c {
-			t.Fatalf("Expected %d occurrences of \"%s\", got %d\n%s", r.c, r.s, count, out)
+			testutil.AnnotatedFatalf(t, fmt.Sprintf("expected %d occurrences of \"%s\", got %d", r.c, r.s, count),
+				"expected %d occurrences of \"%s\", got %d\n%s", r.c, r.s, count, out)
 		}
 	}
 
@@ -71,11 +73,11 @@ func TestRoutes(t *testing.T) {
 
 	out, stderr, err = TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Routes command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd routes' command failed", "'linkerd routes' command failed\n%s\n%s", out, stderr)
 	}
 
 	err = TestHelper.ValidateOutput(out, golden)
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err)
+		testutil.AnnotatedFatalf(t, "received unexpected output", "received unexpected output\n%s", err)
 	}
 }
diff --git a/test/serviceaccounts/serviceaccounts_test.go b/test/serviceaccounts/serviceaccounts_test.go
index e187b66cf..730cafdd4 100644
--- a/test/serviceaccounts/serviceaccounts_test.go
+++ b/test/serviceaccounts/serviceaccounts_test.go
@@ -44,7 +44,8 @@ func TestServiceAccountsMatch(t *testing.T) {
 		"--output", "name",
 	)
 	if err != nil {
-		t.Fatalf("Error retrieving list of service accounts: %s", err)
+		testutil.AnnotatedFatalf(t, "Error retrieving list of service accounts",
+			"error retrieving list of service accounts: %s", err)
 	}
 	names := strings.Split(strings.TrimSpace(res), "\n")
 	var saNames []string
@@ -53,7 +54,7 @@ func TestServiceAccountsMatch(t *testing.T) {
 	}
 	// disregard `default` and `linkerd-heartbeat`
 	if len(saNames)-2 != len(expectedNames) || !namesMatch(saNames) {
-		t.Fatalf("The service account list doesn't match the expected list: %s", expectedNames)
+		testutil.Fatalf(t, "the service account list doesn't match the expected list: %s", expectedNames)
 	}
 
 	res, err = TestHelper.Kubectl("",
@@ -62,7 +63,8 @@ func TestServiceAccountsMatch(t *testing.T) {
 		"--output", "jsonpath={.subjects[*].name}",
 	)
 	if err != nil {
-		t.Fatalf("Error retrieving list of linkerd-psp rolebindings: %s", err)
+		testutil.AnnotatedFatalf(t, "error retrieving list of linkerd-psp rolebindings",
+			"error retrieving list of linkerd-psp rolebindings: %s", err)
 	}
 	saNamesPSP := strings.Split(res, " ")
 	// disregard `linkerd-heartbeat`
diff --git a/test/serviceprofiles/serviceprofiles_test.go b/test/serviceprofiles/serviceprofiles_test.go
index aa8bc7668..a73825f1a 100644
--- a/test/serviceprofiles/serviceprofiles_test.go
+++ b/test/serviceprofiles/serviceprofiles_test.go
@@ -35,26 +35,28 @@ func TestServiceProfiles(t *testing.T) {
 	testNamespace := TestHelper.GetTestNamespace("serviceprofile-test")
 	err := TestHelper.CreateDataPlaneNamespaceIfNotExists(testNamespace, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", testNamespace, err)
+		testutil.Fatalf(t, "failed to create %s namespace: %s", testNamespace, err)
 	}
 	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/tap_application.yaml")
 	if err != nil {
-		t.Fatalf("'linkerd %s' command failed with %s: %s\n", "inject", err.Error(), stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed with %s: %s\n", err, stderr)
 	}
 
 	out, err = TestHelper.KubectlApply(out, testNamespace)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	// wait for deployments to start
 	for _, deploy := range []string{"t1", "t2", "t3", "gateway"} {
 		if err := TestHelper.CheckPods(testNamespace, deploy, 1); err != nil {
-			t.Error(err)
+			testutil.AnnotatedError(t, "CheckPods timed-out", err)
 		}
 
 		if err := TestHelper.CheckDeployment(testNamespace, deploy, 1); err != nil {
-			t.Error(fmt.Errorf("Error validating deployment [%s]:\n%s", deploy, err))
+			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 		}
 	}
 
@@ -80,7 +82,8 @@ func TestServiceProfiles(t *testing.T) {
 		t.Run(tc.sourceName, func(t *testing.T) {
 			routes, err := getRoutes(tc.deployName, tc.namespace, []string{})
 			if err != nil {
-				t.Fatalf("routes command failed: %s\n", err)
+				testutil.AnnotatedFatalf(t, "'linkerd routes' command failed",
+					"'linkerd routes' command failed: %s\n", err)
 			}
 
 			initialExpectedRoutes := []string{"[DEFAULT]"}
@@ -108,17 +111,20 @@ func TestServiceProfiles(t *testing.T) {
 			cmd = append(cmd, tc.args...)
 			out, stderr, err := TestHelper.LinkerdRun(cmd...)
 			if err != nil {
-				t.Fatalf("'linkerd %s' command failed with %s: %s\n", cmd, err.Error(), stderr)
+				testutil.AnnotatedFatalf(t, fmt.Sprintf("'linkerd %s' command failed", cmd),
+					"'linkerd %s' command failed with %s: %s\n", cmd, err, stderr)
 			}
 
 			_, err = TestHelper.KubectlApply(out, tc.namespace)
 			if err != nil {
-				t.Fatalf("kubectl apply command failed:\n%s", err)
+				testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+					"'kubectl apply' command failed:\n%s", err)
 			}
 
 			routes, err = getRoutes(tc.deployName, tc.namespace, []string{})
 			if err != nil {
-				t.Fatalf("routes command failed: %s\n", err)
+				testutil.AnnotatedFatalf(t, "'linkerd routes' command failed",
+					"'linkerd routes' command failed: %s\n", err)
 			}
 
 			assertExpectedRoutes(tc.expectedRoutes, routes, t)
@@ -137,12 +143,14 @@ func TestServiceProfileMetrics(t *testing.T) {
 
 	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", testYAML)
 	if err != nil {
-		t.Errorf("'linkerd %s' command failed with %s: %s\n", "inject", err.Error(), stderr)
+		testutil.AnnotatedErrorf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed with %s: %s\n", err, stderr)
 	}
 
 	out, err = TestHelper.KubectlApply(out, testNamespace)
 	if err != nil {
-		t.Errorf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedErrorf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	cmd := []string{
@@ -156,12 +164,14 @@ func TestServiceProfileMetrics(t *testing.T) {
 
 	out, stderr, err = TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Errorf("'linkerd %s' command failed with %s: %s\n", cmd, err.Error(), stderr)
+		testutil.AnnotatedErrorf(t, fmt.Sprintf("'linkerd %s' command failed", cmd),
+			"'linkerd %s' command failed with %s: %s\n", cmd, err, stderr)
 	}
 
 	_, err = TestHelper.KubectlApply(out, testNamespace)
 	if err != nil {
-		t.Errorf("kubectl apply command failed:\n%s", err)
+		testutil.AnnotatedErrorf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", err)
 	}
 
 	assertRouteStat(testUpstreamDeploy, testNamespace, testDownstreamDeploy, t, func(stat *cmd2.JSONRouteStats) error {
@@ -176,7 +186,8 @@ func TestServiceProfileMetrics(t *testing.T) {
 	// Grab the output and convert it to a service profile object for modification
 	err = yaml.Unmarshal([]byte(out), profile)
 	if err != nil {
-		t.Errorf("unable to unmarshall YAML: %s", err.Error())
+		testutil.AnnotatedErrorf(t, "unable to unmarshall YAML",
+			"unable to unmarshall YAML: %s", err)
 	}
 
 	// introduce retry in the service profile
@@ -189,12 +200,14 @@ func TestServiceProfileMetrics(t *testing.T) {
 
 	bytes, err := yaml.Marshal(profile)
 	if err != nil {
-		t.Errorf("error marshalling service profile: %s", bytes)
+		testutil.AnnotatedErrorf(t, "error marshalling service profile",
+			"error marshalling service profile: %s", bytes)
 	}
 
 	out, err = TestHelper.KubectlApply(string(bytes), testNamespace)
 	if err != nil {
-		t.Errorf("kubectl apply command failed:\n%s :%s", err, out)
+		testutil.AnnotatedErrorf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed:\n%s :%s", err, out)
 	}
 
 	assertRouteStat(testUpstreamDeploy, testNamespace, testDownstreamDeploy, t, func(stat *cmd2.JSONRouteStats) error {
@@ -207,10 +220,11 @@ func TestServiceProfileMetrics(t *testing.T) {
 
 func assertRouteStat(upstream, namespace, downstream string, t *testing.T, assertFn func(stat *cmd2.JSONRouteStats) error) {
 	const routePath = "GET /testpath"
-	err := TestHelper.RetryFor(2*time.Minute, func() error {
+	timeout := 2 * time.Minute
+	err := TestHelper.RetryFor(timeout, func() error {
 		routes, err := getRoutes(upstream, namespace, []string{"--to", downstream})
 		if err != nil {
-			return fmt.Errorf("routes command failed: %s", err)
+			return fmt.Errorf("'linkerd routes' command failed: %s", err)
 		}
 
 		var testRoute *cmd2.JSONRouteStats
@@ -230,14 +244,14 @@ func assertRouteStat(upstream, namespace, downstream string, t *testing.T, asser
 	})
 
 	if err != nil {
-		t.Error(err.Error())
+		testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out asserting route stat (%s)", timeout), err)
 	}
 }
 
 func assertExpectedRoutes(expected []string, actual []*cmd2.JSONRouteStats, t *testing.T) {
 
 	if len(expected) != len(actual) {
-		t.Errorf("mismatch routes count. Expected %d, Actual %d", len(expected), len(actual))
+		testutil.Errorf(t, "mismatch routes count. Expected %d, Actual %d", len(expected), len(actual))
 	}
 
 	for _, expectedRoute := range expected {
@@ -253,7 +267,7 @@ func assertExpectedRoutes(expected []string, actual []*cmd2.JSONRouteStats, t *t
 			for _, route := range actual {
 				sb.WriteString(fmt.Sprintf("%s ", route.Route))
 			}
-			t.Errorf("Expected route %s not found in %+v", expectedRoute, sb.String())
+			testutil.Errorf(t, "expected route %s not found in %+v", expectedRoute, sb.String())
 		}
 	}
 }
@@ -279,7 +293,7 @@ func getRoutes(deployName, namespace string, additionalArgs []string) ([]*cmd2.J
 	var list map[string][]*cmd2.JSONRouteStats
 	err = yaml.Unmarshal([]byte(out), &list)
 	if err != nil {
-		return nil, fmt.Errorf(fmt.Sprintf("Error: %s stderr: %s", err.Error(), stderr))
+		return nil, fmt.Errorf(fmt.Sprintf("Error: %s stderr: %s", err, stderr))
 	}
 
 	if deployment, ok := list[deployName]; ok {
diff --git a/test/stat/stat_test.go b/test/stat/stat_test.go
index ab1435efc..1e80c9dd2 100644
--- a/test/stat/stat_test.go
+++ b/test/stat/stat_test.go
@@ -36,19 +36,21 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 
 	pods, err := TestHelper.GetPodNamesForDeployment(TestHelper.GetLinkerdNamespace(), "linkerd-prometheus")
 	if err != nil {
-		t.Fatalf("Failed to get pods for prometheus: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to get pods for prometheus",
+			"failed to get pods for prometheus: %s", err)
 	}
 	if len(pods) != 1 {
-		t.Fatalf("Expected 1 pod for prometheus, got %d", len(pods))
+		testutil.Fatalf(t, "expected 1 pod for prometheus, got %d", len(pods))
 	}
 	prometheusPod := pods[0]
 
 	pods, err = TestHelper.GetPodNamesForDeployment(TestHelper.GetLinkerdNamespace(), "linkerd-controller")
 	if err != nil {
-		t.Fatalf("Failed to get pods for controller: %s", err)
+		testutil.AnnotatedFatalf(t, "failed to get pods for controller",
+			"failed to get pods for controller: %s", err)
 	}
 	if len(pods) != 1 {
-		t.Fatalf("Expected 1 pod for controller, got %d", len(pods))
+		testutil.Fatalf(t, "expected 1 pod for controller, got %d", len(pods))
 	}
 	controllerPod := pods[0]
 
@@ -119,14 +121,16 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 		},
 	} {
 		tt := tt // pin
+		timeout := 20 * time.Second
 		t.Run("linkerd "+strings.Join(tt.args, " "), func(t *testing.T) {
-			err := TestHelper.RetryFor(20*time.Second, func() error {
+			err := TestHelper.RetryFor(timeout, func() error {
 				// Use a short time window so that transient errors at startup
 				// fall out of the window.
 				tt.args = append(tt.args, "-t", "30s")
 				out, stderr, err := TestHelper.LinkerdRun(tt.args...)
 				if err != nil {
-					t.Fatalf("Unexpected stat error: %s\n%s", err, out)
+					testutil.AnnotatedFatalf(t, "unexpected stat error",
+						"unexpected stat error: %s\n%s", err, out)
 				}
 				fmt.Println(stderr)
 
@@ -148,7 +152,7 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 				return nil
 			})
 			if err != nil {
-				t.Fatal(err.Error())
+				testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking stats (%s)", timeout), err)
 			}
 		})
 	}
diff --git a/test/tap/tap_test.go b/test/tap/tap_test.go
index b2f697d2e..2bea554e4 100644
--- a/test/tap/tap_test.go
+++ b/test/tap/tap_test.go
@@ -80,90 +80,97 @@ var (
 func TestCliTap(t *testing.T) {
 	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/tap_application.yaml")
 	if err != nil {
-		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed\n%s\n%s", out, stderr)
 	}
 
 	prefixedNs := TestHelper.GetTestNamespace("tap-test")
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", prefixedNs),
+			"failed to create %s namespace: %s", prefixedNs, err)
 	}
 	out, err = TestHelper.KubectlApply(out, prefixedNs)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	// wait for deployments to start
 	for _, deploy := range []string{"t1", "t2", "t3", "gateway"} {
 		if err := TestHelper.CheckPods(prefixedNs, deploy, 1); err != nil {
-			t.Error(err)
+			testutil.AnnotatedError(t, "CheckPods timed-out", err)
 		}
 
 		if err := TestHelper.CheckDeployment(prefixedNs, deploy, 1); err != nil {
-			t.Error(fmt.Errorf("Error validating deployment [%s]:\n%s", deploy, err))
+			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 		}
 	}
 
 	t.Run("tap a deployment", func(t *testing.T) {
 		events, err := tap("deploy/t1", "--namespace", prefixedNs)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "tap failed", err)
 		}
 
 		err = validateExpected(events, expectedT1)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "validating tap failed", err)
 		}
 	})
 
 	t.Run("tap a deployment using context namespace", func(t *testing.T) {
 		out, err := TestHelper.Kubectl("", "config", "set-context", "--namespace="+prefixedNs, "--current")
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "unexpected error",
+				"unexpected error: %v output:\n%s", err, out)
 		}
 
 		events, err := tap("deploy/t1")
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "tap failed using context namespace", err)
 		}
 
 		err = validateExpected(events, expectedT1)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "validating tap failed using context namespace", err)
 		}
 
 		out, err = TestHelper.Kubectl("", "config", "set-context", "--namespace=default", "--current")
 		if err != nil {
-			t.Fatalf("Unexpected error: %v output:\n%s", err, out)
+			testutil.AnnotatedFatalf(t, "unexpected error",
+				"unexpected error: %v output:\n%s", err, out)
 		}
 	})
 
 	t.Run("tap a disabled deployment", func(t *testing.T) {
 		out, stderr, err := TestHelper.LinkerdRun("tap", "deploy/t4", "--namespace", prefixedNs)
 		if out != "" {
-			t.Fatalf("Unexpected output: %s", out)
+			testutil.AnnotatedFatalf(t, "unexpected output",
+				"unexpected output: %s", out)
 		}
 		if err == nil {
-			t.Fatal("Expected an error, got none")
+			testutil.Fatal(t, "expected an error, got none")
 		}
 		if stderr == "" {
-			t.Fatal("Expected an error, got none")
+			testutil.Fatal(t, "expected an error, got none")
 		}
 		expectedErr := "Error: all pods found for deployment/t4 have tapping disabled"
 		if errs := strings.Split(stderr, "\n"); errs[0] != expectedErr {
-			t.Fatalf("Expected [%s], got: %s", expectedErr, errs[0])
+			testutil.AnnotatedFatalf(t, "unexpected error",
+				"expected [%s], got: %s", expectedErr, errs[0])
 		}
 	})
 
 	t.Run("tap a service call", func(t *testing.T) {
 		events, err := tap("deploy/gateway", "--to", "svc/t2-svc", "--namespace", prefixedNs)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "failed tapping a service call", err)
 		}
 
 		err = validateExpected(events, expectedT2)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "failed validating tapping a service call", err)
 		}
 	})
 
@@ -171,45 +178,46 @@ func TestCliTap(t *testing.T) {
 		deploy := "t3"
 		pods, err := TestHelper.GetPodNamesForDeployment(prefixedNs, deploy)
 		if err != nil {
-			t.Fatalf("Failed to get pods for deployment [%s]\n%s", deploy, err)
+			testutil.AnnotatedFatalf(t, "failed to get pods for deployment t3",
+				"failed to get pods for deployment [%s]\n%s", deploy, err)
 		}
 
 		if len(pods) != 1 {
-			t.Fatalf("Expected exactly one pod for deployment [%s], got:\n%v", deploy, pods)
+			testutil.Fatalf(t, "expected exactly one pod for deployment [%s], got:\n%v", deploy, pods)
 		}
 
 		events, err := tap("pod/"+pods[0], "--namespace", prefixedNs)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error tapping pod", err)
 		}
 
 		err = validateExpected(events, expectedT3)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error validating pod tap", err)
 		}
 	})
 
 	t.Run("filter tap events by method", func(t *testing.T) {
 		events, err := tap("deploy/gateway", "--namespace", prefixedNs, "--method", "GET")
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error filtering tap events by method", err)
 		}
 
 		err = validateExpected(events, expectedGateway)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error validating filtered tap events by method", err)
 		}
 	})
 
 	t.Run("filter tap events by authority", func(t *testing.T) {
 		events, err := tap("deploy/gateway", "--namespace", prefixedNs, "--authority", "t1-svc:9090")
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error filtering tap events by authority", err)
 		}
 
 		err = validateExpected(events, expectedT1)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error validating filtered tap events by authority", err)
 		}
 	})
 
diff --git a/test/tracing/tracing_test.go b/test/tracing/tracing_test.go
index fb47608a5..bc7f7ec2e 100644
--- a/test/tracing/tracing_test.go
+++ b/test/tracing/tracing_test.go
@@ -45,39 +45,46 @@ func TestTracing(t *testing.T) {
 	// Tracing Components
 	out, stderr, err := TestHelper.LinkerdRun("inject", "testdata/tracing.yaml")
 	if err != nil {
-		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed\n%s\n%s", out, stderr)
 	}
 
 	tracingNs := TestHelper.GetTestNamespace("tracing")
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(tracingNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", tracingNs, err)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", tracingNs),
+			"failed to create %s namespace: %s", tracingNs, err)
 	}
 	out, err = TestHelper.KubectlApply(out, tracingNs)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	// Emojivoto components
 	emojivotoNs := TestHelper.GetTestNamespace("emojivoto")
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(emojivotoNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", emojivotoNs, err)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", emojivotoNs),
+			"failed to create %s namespace: %s", emojivotoNs, err)
 	}
 
 	emojivotoYaml, err := testutil.ReadFile("testdata/emojivoto.yaml")
 	if err != nil {
-		t.Fatalf("failed to read emojivoto yaml\n%s\n", err)
+		testutil.AnnotatedFatalf(t, "failed to read emojivoto yaml",
+			"failed to read emojivoto yaml\n%s\n", err)
 	}
 	emojivotoYaml = strings.ReplaceAll(emojivotoYaml, "___TRACING_NS___", tracingNs)
 	out, stderr, err = TestHelper.PipeToLinkerdRun(emojivotoYaml, "inject", "-")
 	if err != nil {
-		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed\n%s\n%s", out, stderr)
 	}
 
 	out, err = TestHelper.KubectlApply(out, emojivotoNs)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	// Ingress components
@@ -85,23 +92,27 @@ func TestTracing(t *testing.T) {
 	ingressNs := emojivotoNs
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(ingressNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", ingressNs, err)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", ingressNs),
+			"failed to create %s namespace: %s", ingressNs, err)
 	}
 
 	ingressYaml, err := testutil.ReadFile("testdata/ingress.yaml")
 	if err != nil {
-		t.Fatalf("failed to read ingress yaml\n%s\n", err)
+		testutil.AnnotatedFatalf(t, "failed to read ingress yaml",
+			"failed to read ingress yaml\n%s\n", err)
 	}
 	ingressYaml = strings.ReplaceAll(ingressYaml, "___INGRESS_NAMESPACE___", ingressNs)
 	ingressYaml = strings.ReplaceAll(ingressYaml, "___TRACING_NS___", tracingNs)
 	out, stderr, err = TestHelper.PipeToLinkerdRun(ingressYaml, "inject", "-")
 	if err != nil {
-		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed\n%s\n%s", out, stderr)
 	}
 
 	out, err = TestHelper.KubectlApply(out, ingressNs)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	// wait for deployments to start
@@ -115,11 +126,11 @@ func TestTracing(t *testing.T) {
 		tracingNs:   "jaeger",
 	} {
 		if err := TestHelper.CheckPods(ns, deploy, 1); err != nil {
-			t.Error(err)
+			testutil.AnnotatedError(t, "CheckPods timed-out", err)
 		}
 
 		if err := TestHelper.CheckDeployment(ns, deploy, 1); err != nil {
-			t.Error(fmt.Errorf("Error validating deployment [%s]:\n%s", deploy, err))
+			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 		}
 	}
 
@@ -127,9 +138,10 @@ func TestTracing(t *testing.T) {
 
 		url, err := TestHelper.URLFor(tracingNs, "jaeger", 16686)
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, "error building URL", err)
 		}
-		err = TestHelper.RetryFor(120*time.Second, func() error {
+		timeout := 120 * time.Second
+		err = TestHelper.RetryFor(timeout, func() error {
 			tracesJSON, err := TestHelper.HTTPGetURL(url + "/api/traces?lookback=1h&service=nginx")
 			if err != nil {
 				return err
@@ -148,7 +160,7 @@ func TestTracing(t *testing.T) {
 			return nil
 		})
 		if err != nil {
-			t.Fatal(err)
+			testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking trace (%s)", timeout), err)
 		}
 	})
 }
diff --git a/test/trafficsplit/trafficsplit_test.go b/test/trafficsplit/trafficsplit_test.go
index 670f7d408..b8512daa3 100644
--- a/test/trafficsplit/trafficsplit_test.go
+++ b/test/trafficsplit/trafficsplit_test.go
@@ -149,36 +149,40 @@ func validateExpectedTsOutput(rows map[string]*statTsRow, expectedBackendSvc, ex
 func TestTrafficSplitCli(t *testing.T) {
 	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/traffic_split_application.yaml")
 	if err != nil {
-		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+			"'linkerd inject' command failed\n%s\n%s", out, stderr)
 	}
 
 	prefixedNs := TestHelper.GetTestNamespace("trafficsplit-test")
 	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, nil)
 	if err != nil {
-		t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", prefixedNs),
+			"failed to create %s namespace: %s", prefixedNs, err)
 	}
 	out, err = TestHelper.KubectlApply(out, prefixedNs)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 
 	// wait for deployments to start
 	for _, deploy := range []string{"backend", "failing", "slow-cooker"} {
 		if err := TestHelper.CheckPods(prefixedNs, deploy, 1); err != nil {
-			t.Error(err)
+			testutil.AnnotatedError(t, "CheckPods timed-out", err)
 		}
 
 		if err := TestHelper.CheckDeployment(prefixedNs, deploy, 1); err != nil {
-			t.Error(fmt.Errorf("Error validating deployment [%s]:\n%s", deploy, err))
+			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 		}
 	}
 
 	t.Run("ensure traffic is sent to one backend only", func(t *testing.T) {
-		err := TestHelper.RetryFor(40*time.Second, func() error {
+		timeout := 40 * time.Second
+		err := TestHelper.RetryFor(timeout, func() error {
 
 			rows, err := statTrafficSplit("deploy/slow-cooker", prefixedNs)
 			if err != nil {
-				t.Fatal(err.Error())
+				testutil.AnnotatedFatal(t, "error ensuring traffic is sent to one backend only", err)
 			}
 			expectedBackendSvcOutput := &statTsRow{
 				name:    "backend-traffic-split",
@@ -207,7 +211,7 @@ func TestTrafficSplitCli(t *testing.T) {
 		})
 
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out ensuring traffic is sent to one backend only (%s)", timeout), err)
 		}
 	})
 
@@ -217,20 +221,23 @@ func TestTrafficSplitCli(t *testing.T) {
 		updatedTsResource, err := testutil.ReadFile(updatedTsResourceFile)
 
 		if err != nil {
-			t.Fatalf("Cannot read updated traffic split resource: %s, %s", updatedTsResource, err)
+			testutil.AnnotatedFatalf(t, "cannot read updated traffic split resource",
+				"cannot read updated traffic split resource: %s, %s", updatedTsResource, err)
 		}
 
 		out, err := TestHelper.KubectlApply(updatedTsResource, prefixedNs)
 		if err != nil {
-			t.Fatalf("Failed to update traffic split resource: %s\n %s", err, out)
+			testutil.AnnotatedFatalf(t, "failed to update traffic split resource",
+				"failed to update traffic split resource: %s\n %s", err, out)
 		}
 	})
 
 	t.Run("ensure traffic is sent to both backends", func(t *testing.T) {
-		err := TestHelper.RetryFor(40*time.Second, func() error {
+		timeout := 40 * time.Second
+		err := TestHelper.RetryFor(timeout, func() error {
 			rows, err := statTrafficSplit("deploy/slow-cooker", prefixedNs)
 			if err != nil {
-				t.Fatal(err.Error())
+				testutil.AnnotatedFatal(t, "error ensuring traffic is sent to both backends", err)
 			}
 			expectedBackendSvcOutput := &statTsRow{
 				name:    "backend-traffic-split",
@@ -256,7 +263,7 @@ func TestTrafficSplitCli(t *testing.T) {
 		})
 
 		if err != nil {
-			t.Fatal(err.Error())
+			testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out ensuring traffic is sent to both backends (%s)", timeout), err)
 		}
 	})
 }
diff --git a/test/uninstall/uninstall_test.go b/test/uninstall/uninstall_test.go
index ed8e19102..afe8f0153 100644
--- a/test/uninstall/uninstall_test.go
+++ b/test/uninstall/uninstall_test.go
@@ -29,12 +29,14 @@ func TestInstall(t *testing.T) {
 
 	out, stderr, err := TestHelper.LinkerdRun(args...)
 	if err != nil {
-		t.Fatalf("linkerd install command failed: \n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd install' command failed",
+			"'linkerd install' command failed: \n%s\n%s", out, stderr)
 	}
 
 	out, err = TestHelper.KubectlApply(out, "")
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 }
 
@@ -42,16 +44,18 @@ func TestResourcesPostInstall(t *testing.T) {
 	// Tests Namespace
 	err := TestHelper.CheckIfNamespaceExists(TestHelper.GetLinkerdNamespace())
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "received unexpected output",
+			"received unexpected output\n%s", err.Error())
 	}
 
 	// Tests Pods and Deployments
 	for deploy, spec := range testutil.LinkerdDeployReplicas {
 		if err := TestHelper.CheckPods(TestHelper.GetLinkerdNamespace(), deploy, spec.Replicas); err != nil {
-			t.Fatal(fmt.Errorf("Error validating pods for deploy [%s]:\n%s", deploy, err))
+			testutil.AnnotatedFatal(t, "CheckPods timed-out",
+				fmt.Errorf("Error validating pods for deploy [%s]:\n%s", deploy, err))
 		}
 		if err := TestHelper.CheckDeployment(TestHelper.GetLinkerdNamespace(), deploy, spec.Replicas); err != nil {
-			t.Fatal(fmt.Errorf("Error validating deploy [%s]:\n%s", deploy, err))
+			testutil.AnnotatedFatalf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
 		}
 	}
 }
@@ -60,13 +64,15 @@ func TestUninstall(t *testing.T) {
 	args := []string{"uninstall"}
 	out, stderr, err := TestHelper.LinkerdRun(args...)
 	if err != nil {
-		t.Fatalf("linkerd install command failed: \n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "'linkerd install' command failed",
+			"'linkerd install' command failed: \n%s\n%s", out, stderr)
 	}
 
 	args = []string{"delete", "-f", "-"}
 	out, err = TestHelper.Kubectl(out, args...)
 	if err != nil {
-		t.Fatalf("kubectl apply command failed\n%s", out)
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
 	}
 }
 
@@ -75,11 +81,13 @@ func TestCheckPostUninstall(t *testing.T) {
 	golden := "check.pre.golden"
 	out, stderr, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
-		t.Fatalf("Check command failed\n%s\n%s", out, stderr)
+		testutil.AnnotatedFatalf(t, "check command failed",
+			"check command failed\n%s\n%s", out, stderr)
 	}
 
 	err = TestHelper.ValidateOutput(out, golden)
 	if err != nil {
-		t.Fatalf("Received unexpected output\n%s", err.Error())
+		testutil.AnnotatedFatalf(t, "received unexpected output",
+			"received unexpected output\n%s", err.Error())
 	}
 }
diff --git a/testutil/annotations.go b/testutil/annotations.go
index e82856b7f..d7bbb3296 100644
--- a/testutil/annotations.go
+++ b/testutil/annotations.go
@@ -13,15 +13,20 @@ const (
 	rootPath = "/linkerd2/"
 )
 
-func echoAnnotation(args ...interface{}) {
+func echoAnnotation(t *testing.T, args ...interface{}) {
 	if _, ok := os.LookupEnv(envFlag); ok {
 		_, fileName, fileLine, ok := runtime.Caller(2)
 		if !ok {
 			panic("Couldn't recover runtime info")
 		}
 		fileName = fileName[strings.LastIndex(fileName, rootPath)+len(rootPath):]
+		// In case of coming from `t.Run(testName, ...)`, only take the first part
+		// of the name; the following parts might not be as generic
+		parts := strings.Split(t.Name(), "/")
+		testName := parts[0]
 		for _, arg := range args {
-			fmt.Printf("::error file=%s,line=%d::%s\n", fileName, fileLine, arg)
+			msg := fmt.Sprintf("%s - %s", testName, arg)
+			fmt.Printf("::error file=%s,line=%d::%s\n", fileName, fileLine, msg)
 		}
 	}
 }
@@ -31,7 +36,7 @@ func echoAnnotation(args ...interface{}) {
 // as a Github annotation when the envFlag environment variable is set
 func Error(t *testing.T, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(args...)
+	echoAnnotation(t, args...)
 	t.Error(args...)
 }
 
@@ -39,7 +44,7 @@ func Error(t *testing.T, args ...interface{}) {
 // will be used as the Github annotation
 func AnnotatedError(t *testing.T, msg string, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(msg)
+	echoAnnotation(t, msg)
 	t.Error(args...)
 }
 
@@ -49,7 +54,7 @@ func AnnotatedError(t *testing.T, msg string, args ...interface{}) {
 // environment variable is set
 func Errorf(t *testing.T, format string, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(fmt.Sprintf(format, args...))
+	echoAnnotation(t, fmt.Sprintf(format, args...))
 	t.Errorf(format, args...)
 }
 
@@ -57,7 +62,7 @@ func Errorf(t *testing.T, format string, args ...interface{}) {
 // will be used as the Github annotation
 func AnnotatedErrorf(t *testing.T, msg, format string, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(msg)
+	echoAnnotation(t, msg)
 	t.Errorf(format, args...)
 }
 
@@ -66,15 +71,15 @@ func AnnotatedErrorf(t *testing.T, msg, format string, args ...interface{}) {
 // as a Github annotation when the envFlag environment variable is set
 func Fatal(t *testing.T, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(args)
+	echoAnnotation(t, args)
 	t.Fatal(args...)
 }
 
 // AnnotatedFatal is similar to Fatal() but it also admits a msg string that
 // will be used as the Github annotation
-func AnnotatedFatal(t *testing.T, msg, format string, args ...interface{}) {
+func AnnotatedFatal(t *testing.T, msg string, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(msg)
+	echoAnnotation(t, msg)
 	t.Fatal(args...)
 }
 
@@ -84,7 +89,7 @@ func AnnotatedFatal(t *testing.T, msg, format string, args ...interface{}) {
 // environment variable is set
 func Fatalf(t *testing.T, format string, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(fmt.Sprintf(format, args...))
+	echoAnnotation(t, fmt.Sprintf(format, args...))
 	t.Fatalf(format, args...)
 }
 
@@ -92,6 +97,6 @@ func Fatalf(t *testing.T, format string, args ...interface{}) {
 // will be used as the Github annotation
 func AnnotatedFatalf(t *testing.T, msg, format string, args ...interface{}) {
 	t.Helper()
-	echoAnnotation(msg)
+	echoAnnotation(t, msg)
 	t.Fatalf(format, args...)
 }
diff --git a/testutil/annotations_test.go b/testutil/annotations_test.go
index 56a8a75d0..359ecee38 100644
--- a/testutil/annotations_test.go
+++ b/testutil/annotations_test.go
@@ -48,7 +48,7 @@ func TestError(t *testing.T) {
 	Error(&testing.T{}, msg)
 	out := restoreStdout(outC, origStdout)
 
-	if strings.TrimSpace(out) != "::error file=testutil/annotations_test.go,line=48::This is an error" {
+	if strings.TrimSpace(out) != "::error file=testutil/annotations_test.go,line=48:: - This is an error" {
 		t.Fatalf("unexpected stdout content: %s", out)
 	}
 }
@@ -63,7 +63,7 @@ func TestAnnotatedErrorf(t *testing.T) {
 	AnnotatedErrorf(&testing.T{}, msgDesc, msgFormat, str)
 	out := restoreStdout(outC, origStdout)
 
-	if strings.TrimSpace(out) != "::error file=testutil/annotations_test.go,line=63::This is a generic error" {
+	if strings.TrimSpace(out) != "::error file=testutil/annotations_test.go,line=63:: - This is a generic error" {
 		t.Fatalf("unexpected stdout content: %s", out)
 	}
 }