Repository: linkerd/linkerd2
Pull request #8264 (2022-04-15 01:01:27)

2.11: Destination controller updates
<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/bin/update-codegen.sh b/bin/update-codegen.sh
index e03a9a210..83cb9db9b 100755
--- a/bin/update-codegen.sh
+++ b/bin/update-codegen.sh
@@ -1,4 +1,4 @@
-#!/usr/bin/env sh
+#!/usr/bin/env bash
 
 set -eu
 
@@ -9,14 +9,18 @@ codegen_pkg=${GOPATH}/pkg/mod/k8s.io/code-generator@${gen_ver}
 
 # ROOT_PACKAGE :: the package that is the target for code generation
 ROOT_PACKAGE=github.com/linkerd/linkerd2
-# CUSTOM_RESOURCE_NAME :: the name of the custom resource that we're generating client code for
-CUSTOM_RESOURCE_NAME=serviceprofile
-# CUSTOM_RESOURCE_VERSION :: the version of the resource
-CUSTOM_RESOURCE_VERSION=v1alpha2
 
-rm -f "${rootdir}/controller/gen/apis/${CUSTOM_RESOURCE_NAME}/${CUSTOM_RESOURCE_VERSION}/zz_generated.deepcopy.go"
+crds=(serviceprofile:v1alpha2 server:v1beta1 serverauthorization:v1beta1)
+
+# remove previously generated code
 rm -rf "${rootdir}/controller/gen/client"
 rm -rf "${GOPATH}/src/${ROOT_PACKAGE}/controller/gen"
+for crd in "${crds[@]}"
+do
+  crd_path=$(tr : / <<< "$crd")
+  rm -f "${rootdir}/controller/gen/apis/${crd_path}/zz_generated.deepcopy.go"
+done
+
 
 chmod +x "${codegen_pkg}/generate-groups.sh"
 
@@ -25,8 +29,12 @@ GO111MODULE='on' "${codegen_pkg}/generate-groups.sh" \
   'deepcopy,client,informer,lister' \
   "${ROOT_PACKAGE}/controller/gen/client" \
   "${ROOT_PACKAGE}/controller/gen/apis" \
-  "${CUSTOM_RESOURCE_NAME}:${CUSTOM_RESOURCE_VERSION}" \
+  "${crds[*]}" \
   --go-header-file "${codegen_pkg}"/hack/boilerplate.go.txt
 
 # copy generated code out of GOPATH
 cp -R "${GOPATH}/src/${ROOT_PACKAGE}/controller/gen" 'controller/'
+
+# Temporary fix for https://github.com/kubernetes/code-generator/issues/135
+sed -i 's/Group: \"server\"/Group: \"policy.linkerd.io\"/g' "${rootdir}/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go"
+sed -i 's/Group: \"serverauthorization\"/Group: \"policy.linkerd.io\"/g' "${rootdir}/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization.go"
diff --git a/controller/api/destination/endpoint_profile_translator.go b/controller/api/destination/endpoint_profile_translator.go
new file mode 100644
index 000000000..cbebcf2cb
--- /dev/null
+++ b/controller/api/destination/endpoint_profile_translator.go
@@ -0,0 +1,63 @@
+package destination
+
+import (
+	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
+	"github.com/sirupsen/logrus"
+	v1 "k8s.io/api/core/v1"
+)
+
+type endpointProfileTranslator struct {
+	pod      *v1.Pod
+	port     uint32
+	endpoint *pb.WeightedAddr
+	stream   pb.Destination_GetProfileServer
+	log      *logrus.Entry
+}
+
+// newEndpointProfileTranslator translates protocol updates to
+// DestinationProfiles for endpoints. When a Server on the cluster is updated
+// it is possible that it selects an endpoint that is being watched, if that
+// is the case then an update will be sent to the client if the Server has
+// changed the endpoint's supported protocol—mainly being opaque or not.
+func newEndpointProfileTranslator(pod *v1.Pod, port uint32, endpoint *pb.WeightedAddr, stream pb.Destination_GetProfileServer, log *logrus.Entry) *endpointProfileTranslator {
+	return &endpointProfileTranslator{
+		pod:      pod,
+		port:     port,
+		endpoint: endpoint,
+		stream:   stream,
+		log:      log,
+	}
+}
+
+func (ept *endpointProfileTranslator) UpdateProtocol(opaqueProtocol bool) {
+	// The protocol for an endpoint should only be updated if there is a pod,
+	// endpoint, and the endpoint has a protocol hint. If there is an endpoint
+	// but it does not have a protocol hint, that means we could not determine
+	// if it has a peer proxy so a opaque traffic would not be supported.
+	if ept.pod != nil && ept.endpoint != nil && ept.endpoint.ProtocolHint != nil {
+		if !opaqueProtocol {
+			ept.endpoint.ProtocolHint.OpaqueTransport = nil
+		} else if ept.endpoint.ProtocolHint.OpaqueTransport == nil {
+			port, err := getInboundPort(&ept.pod.Spec)
+			if err != nil {
+				ept.log.Error(err)
+			} else {
+				ept.endpoint.ProtocolHint.OpaqueTransport = &pb.ProtocolHint_OpaqueTransport{
+					InboundPort: port,
+				}
+			}
+		}
+
+	}
+	profile := ept.createDefaultProfile(opaqueProtocol)
+	ept.log.Debugf("sending protocol update: %+v", profile)
+	ept.stream.Send(profile)
+}
+
+func (ept *endpointProfileTranslator) createDefaultProfile(opaqueProtocol bool) *pb.DestinationProfile {
+	return &pb.DestinationProfile{
+		RetryBudget:    defaultRetryBudget(),
+		Endpoint:       ept.endpoint,
+		OpaqueProtocol: opaqueProtocol,
+	}
+}
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index c40a487ff..b5feedaa9 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -2,6 +2,7 @@ package destination
 
 import (
 	"fmt"
+	"reflect"
 	"strconv"
 	"strings"
 
@@ -114,19 +115,18 @@ func (et *endpointTranslator) sendFilteredUpdate(set watcher.AddressSet) {
 // The client will receive only endpoints with the same topology label value as the source node,
 // the order of labels is based on the topological preference elicited from the K8s service.
 func (et *endpointTranslator) filterAddresses() watcher.AddressSet {
+	filtered := make(map[watcher.ID]watcher.Address)
 	if len(et.availableEndpoints.TopologicalPref) == 0 {
-		allAvailEndpoints := make(map[watcher.ID]watcher.Address)
 		for k, v := range et.availableEndpoints.Addresses {
-			allAvailEndpoints[k] = v
+			filtered[k] = v
 		}
 		return watcher.AddressSet{
-			Addresses: allAvailEndpoints,
+			Addresses: filtered,
 			Labels:    et.availableEndpoints.Labels,
 		}
 	}
 
 	et.log.Debugf("Filtering through address set with preference %v", et.availableEndpoints.TopologicalPref)
-	filtered := make(map[watcher.ID]watcher.Address)
 	for _, pref := range et.availableEndpoints.TopologicalPref {
 		// '*' as a topology preference means all endpoints
 		if pref == "*" {
@@ -155,8 +155,14 @@ func (et *endpointTranslator) filterAddresses() watcher.AddressSet {
 		}
 	}
 
-	// if we have no filtered endpoints or the '*' preference then no topology pref is satisfied
-	return newEmptyAddressSet()
+	// If there were no filtered addresses, then fall to using all endpoints
+	for k, v := range et.availableEndpoints.Addresses {
+		filtered[k] = v
+	}
+	return watcher.AddressSet{
+		Addresses: filtered,
+		Labels:    et.availableEndpoints.Labels,
+	}
 }
 
 // diffEndpoints calculates the difference between the filtered set of endpoints in the current (Add/Remove) operation
@@ -166,9 +172,14 @@ func (et *endpointTranslator) diffEndpoints(filtered watcher.AddressSet) (watche
 	add := make(map[watcher.ID]watcher.Address)
 	remove := make(map[watcher.ID]watcher.Address)
 
-	for id, address := range filtered.Addresses {
-		if _, ok := et.filteredSnapshot.Addresses[id]; !ok {
-			add[id] = address
+	for id, new := range filtered.Addresses {
+		old, ok := et.filteredSnapshot.Addresses[id]
+		if !ok {
+			add[id] = new
+		} else {
+			if !reflect.DeepEqual(old, new) {
+				add[id] = new
+			}
 		}
 	}
 
@@ -212,26 +223,16 @@ func (et *endpointTranslator) sendClientAdd(set watcher.AddressSet) {
 	addrs := []*pb.WeightedAddr{}
 	for _, address := range set.Addresses {
 		var (
-			wa  *pb.WeightedAddr
-			err error
+			wa          *pb.WeightedAddr
+			opaquePorts map[uint32]struct{}
+			err         error
 		)
 		if address.Pod != nil {
-			opaquePorts, ok, getErr := getPodOpaquePortsAnnotations(address.Pod)
-			if getErr != nil {
-				et.log.Errorf("failed getting opaque ports annotation for pod: %s", getErr)
-			}
-			// If the opaque ports annotation was not set, then set the
-			// endpoint's opaque ports to the default value.
-			if !ok {
-				opaquePorts = et.defaultOpaquePorts
-			}
-
-			skippedInboundPorts, skippedErr := getPodSkippedInboundPortsAnnotations(address.Pod)
-			if skippedErr != nil {
-				et.log.Errorf("failed getting ignored inbound ports annotation for pod: %s", err)
+			opaquePorts, err = getAnnotatedOpaquePorts(address.Pod, et.defaultOpaquePorts)
+			if err != nil {
+				et.log.Errorf("failed to get opaque ports for pod %s/%s: %s", address.Pod.Namespace, address.Pod.Name, err)
 			}
-
-			wa, err = toWeightedAddr(address, opaquePorts, skippedInboundPorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.log)
+			wa, err = createWeightedAddr(address, opaquePorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.log)
 		} else {
 			var authOverride *pb.AuthorityOverride
 			if address.AuthorityOverride != "" {
@@ -321,27 +322,55 @@ func toAddr(address watcher.Address) (*net.TcpAddress, error) {
 	}, nil
 }
 
-func toWeightedAddr(address watcher.Address, opaquePorts, skippedInboundPorts map[uint32]struct{}, enableH2Upgrade bool, identityTrustDomain string, controllerNS string, log *logging.Entry) (*pb.WeightedAddr, error) {
+func createWeightedAddr(address watcher.Address, opaquePorts map[uint32]struct{}, enableH2Upgrade bool, identityTrustDomain string, controllerNS string, log *logging.Entry) (*pb.WeightedAddr, error) {
+
+	tcpAddr, err := toAddr(address)
+	if err != nil {
+		return nil, err
+	}
+
+	weightedAddr := pb.WeightedAddr{
+		Addr:         tcpAddr,
+		Weight:       defaultWeight,
+		MetricLabels: map[string]string{},
+	}
+
+	// If the address is not backed by a pod, there is no additional metadata
+	// to add.
+	if address.Pod == nil {
+		return &weightedAddr, nil
+	}
+
+	skippedInboundPorts, err := getPodSkippedInboundPortsAnnotations(address.Pod)
+	if err != nil {
+		log.Errorf("failed to get ignored inbound ports annotation for pod: %s", err)
+	}
+
 	controllerNSLabel := address.Pod.Labels[k8s.ControllerNSLabel]
 	sa, ns := k8s.GetServiceAccountAndNS(address.Pod)
-	labels := k8s.GetPodLabels(address.OwnerKind, address.OwnerName, address.Pod)
+	weightedAddr.MetricLabels = k8s.GetPodLabels(address.OwnerKind, address.OwnerName, address.Pod)
 	_, isSkippedInboundPort := skippedInboundPorts[address.Port]
+
 	// If the pod is controlled by any Linkerd control plane, then it can be
 	// hinted that this destination knows H2 (and handles our orig-proto
 	// translation)
-	hint := &pb.ProtocolHint{}
+	weightedAddr.ProtocolHint = &pb.ProtocolHint{}
 	if controllerNSLabel != "" && !isSkippedInboundPort {
 		if enableH2Upgrade {
-			hint.Protocol = &pb.ProtocolHint_H2_{
+			weightedAddr.ProtocolHint.Protocol = &pb.ProtocolHint_H2_{
 				H2: &pb.ProtocolHint_H2{},
 			}
 		}
-		if _, ok := opaquePorts[address.Port]; ok {
+		// If address is set as opaque by a Server, or its port is set as
+		// opaque by annotation or default value, then hint its proxy's
+		// inbound port.
+		_, opaquePort := opaquePorts[address.Port]
+		if address.OpaqueProtocol || opaquePort {
 			port, err := getInboundPort(&address.Pod.Spec)
 			if err != nil {
 				log.Error(err)
 			} else {
-				hint.OpaqueTransport = &pb.ProtocolHint_OpaqueTransport{
+				weightedAddr.ProtocolHint.OpaqueTransport = &pb.ProtocolHint_OpaqueTransport{
 					InboundPort: port,
 				}
 			}
@@ -353,14 +382,13 @@ func toWeightedAddr(address watcher.Address, opaquePorts, skippedInboundPorts ma
 	//
 	// TODO this should be relaxed to match a trust domain annotation so that
 	// multiple meshes can participate in identity if they share trust roots.
-	var identity *pb.TlsIdentity
 	if identityTrustDomain != "" &&
 		controllerNSLabel == controllerNS &&
 		address.Pod.Annotations[k8s.IdentityModeAnnotation] == k8s.IdentityModeDefault &&
 		!isSkippedInboundPort {
 
 		id := fmt.Sprintf("%s.%s.serviceaccount.identity.%s.%s", sa, ns, controllerNSLabel, identityTrustDomain)
-		identity = &pb.TlsIdentity{
+		weightedAddr.TlsIdentity = &pb.TlsIdentity{
 			Strategy: &pb.TlsIdentity_DnsLikeIdentity_{
 				DnsLikeIdentity: &pb.TlsIdentity_DnsLikeIdentity{
 					Name: id,
@@ -369,18 +397,7 @@ func toWeightedAddr(address watcher.Address, opaquePorts, skippedInboundPorts ma
 		}
 	}
 
-	tcpAddr, err := toAddr(address)
-	if err != nil {
-		return nil, err
-	}
-
-	return &pb.WeightedAddr{
-		Addr:         tcpAddr,
-		Weight:       defaultWeight,
-		MetricLabels: labels,
-		TlsIdentity:  identity,
-		ProtocolHint: hint,
-	}, nil
+	return &weightedAddr, nil
 }
 
 func getK8sNodeTopology(nodes coreinformers.NodeInformer, srcNode string) (map[string]string, error) {
diff --git a/controller/api/destination/profile_translator.go b/controller/api/destination/profile_translator.go
index 42422d6a9..80e110509 100644
--- a/controller/api/destination/profile_translator.go
+++ b/controller/api/destination/profile_translator.go
@@ -22,16 +22,14 @@ type profileTranslator struct {
 	log                *logging.Entry
 	fullyQualifiedName string
 	port               uint32
-	endpoint           *pb.WeightedAddr
 }
 
-func newProfileTranslator(stream pb.Destination_GetProfileServer, log *logging.Entry, fqn string, port uint32, endpoint *pb.WeightedAddr) *profileTranslator {
+func newProfileTranslator(stream pb.Destination_GetProfileServer, log *logging.Entry, fqn string, port uint32) *profileTranslator {
 	return &profileTranslator{
 		stream:             stream,
 		log:                log.WithField("component", "profile-translator"),
 		fullyQualifiedName: fqn,
 		port:               port,
-		endpoint:           endpoint,
 	}
 }
 
@@ -40,7 +38,7 @@ func (pt *profileTranslator) Update(profile *sp.ServiceProfile) {
 		pt.stream.Send(pt.defaultServiceProfile())
 		return
 	}
-	destinationProfile, err := pt.toServiceProfile(profile)
+	destinationProfile, err := pt.createDestinationProfile(profile)
 	if err != nil {
 		pt.log.Error(err)
 		return
@@ -54,7 +52,6 @@ func (pt *profileTranslator) defaultServiceProfile() *pb.DestinationProfile {
 		Routes:             []*pb.Route{},
 		RetryBudget:        defaultRetryBudget(),
 		FullyQualifiedName: pt.fullyQualifiedName,
-		Endpoint:           pt.endpoint,
 	}
 }
 
@@ -78,9 +75,9 @@ func toDuration(d time.Duration) *duration.Duration {
 	}
 }
 
-// toServiceProfile returns a Proxy API DestinationProfile, given a
+// createDestinationProfile returns a Proxy API DestinationProfile, given a
 // ServiceProfile.
-func (pt *profileTranslator) toServiceProfile(profile *sp.ServiceProfile) (*pb.DestinationProfile, error) {
+func (pt *profileTranslator) createDestinationProfile(profile *sp.ServiceProfile) (*pb.DestinationProfile, error) {
 	routes := make([]*pb.Route, 0)
 	for _, route := range profile.Spec.Routes {
 		pbRoute, err := toRoute(profile, route)
@@ -108,7 +105,6 @@ func (pt *profileTranslator) toServiceProfile(profile *sp.ServiceProfile) (*pb.D
 		RetryBudget:        budget,
 		DstOverrides:       toDstOverrides(profile.Spec.DstOverrides, pt.port),
 		FullyQualifiedName: pt.fullyQualifiedName,
-		Endpoint:           pt.endpoint,
 		OpaqueProtocol:     opaqueProtocol,
 	}, nil
 }
@@ -256,7 +252,7 @@ func toResponseMatch(rspMatch *sp.ResponseMatch) (*pb.ResponseMatch, error) {
 	}
 
 	if len(matches) == 0 {
-		return nil, errors.New("A response match must have a field set")
+		return nil, errors.New("a response match must have a field set")
 	}
 	if len(matches) == 1 {
 		return matches[0], nil
@@ -350,7 +346,7 @@ func toRequestMatch(reqMatch *sp.RequestMatch) (*pb.RequestMatch, error) {
 	}
 
 	if len(matches) == 0 {
-		return nil, errors.New("A request match must have a field set")
+		return nil, errors.New("a request match must have a field set")
 	}
 	if len(matches) == 1 {
 		return matches[0], nil
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 2f89091ef..bf47b570e 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -10,7 +10,6 @@ import (
 
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
-	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	labels "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/prometheus"
@@ -31,6 +30,7 @@ type (
 		endpoints     *watcher.EndpointsWatcher
 		opaquePorts   *watcher.OpaquePortsWatcher
 		profiles      *watcher.ProfileWatcher
+		servers       *watcher.ServerWatcher
 		trafficSplits *watcher.TrafficSplitWatcher
 		nodes         coreinformers.NodeInformer
 
@@ -84,12 +84,14 @@ func NewServer(
 	opaquePorts := watcher.NewOpaquePortsWatcher(k8sAPI, log, defaultOpaquePorts)
 	profiles := watcher.NewProfileWatcher(k8sAPI, log)
 	trafficSplits := watcher.NewTrafficSplitWatcher(k8sAPI, log)
+	servers := watcher.NewServerWatcher(k8sAPI, log)
 
 	srv := server{
 		pb.UnimplementedDestinationServer{},
 		endpoints,
 		opaquePorts,
 		profiles,
+		servers,
 		trafficSplits,
 		k8sAPI.Node(),
 		enableH2Upgrade,
@@ -210,14 +212,35 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 				return err
 			}
 
-			// The IP may or may not map to a pod (pod argument can be nil). If
-			// pod is not nil we will return a single endpoint in the
-			// DestinationProfile response, otherwise we return a default
-			// profile response.
-			err = s.sendEndpointProfile(stream, pod, port)
+			opaquePorts, err := getAnnotatedOpaquePorts(pod, s.defaultOpaquePorts)
 			if err != nil {
-				log.Debugf("Failed to send profile response for endpoint %s:%d: %v", ip.String(), port, err)
-				return err
+				return fmt.Errorf("failed to get opaque ports for pod: %s", err)
+			}
+			var address watcher.Address
+			var endpoint *pb.WeightedAddr
+			if pod != nil {
+				address, err = s.createAddress(pod, port)
+				if err != nil {
+					return fmt.Errorf("failed to create address: %s", err)
+				}
+				endpoint, err = s.createEndpoint(address, opaquePorts)
+				if err != nil {
+					return fmt.Errorf("failed to create endpoint: %s", err)
+				}
+			}
+			translator := newEndpointProfileTranslator(pod, port, endpoint, stream, s.log)
+
+			// If the endpoint's port is annotated as opaque, we don't need to
+			// subscribe for updates because it will always be opaque
+			// regardless of any Servers that may select it.
+			if _, ok := opaquePorts[port]; ok {
+				translator.UpdateProtocol(true)
+			} else if pod == nil {
+				translator.UpdateProtocol(false)
+			} else {
+				translator.UpdateProtocol(address.OpaqueProtocol)
+				s.servers.Subscribe(pod, port, translator)
+				defer s.servers.Unsubscribe(pod, port, translator)
 			}
 
 			select {
@@ -225,7 +248,6 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 			case <-stream.Context().Done():
 				log.Debugf("GetProfile(%+v) cancelled", dest)
 			}
-
 			return nil
 		}
 	} else {
@@ -240,23 +262,38 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 		// name. When we fetch the profile using a pod's DNS name, we want to
 		// return an endpoint in the profile response.
 		if hostname != "" {
-			pod, err := getPodByHostname(s.k8sAPI, hostname, service)
+			address, err := s.getEndpointByHostname(s.k8sAPI, hostname, service, port)
 			if err != nil {
-				log.Errorf("Failed to get pod for hostname %s: %v", hostname, err)
+				return fmt.Errorf("failed to get pod for hostname %s: %v", hostname, err)
 			}
-
-			err = s.sendEndpointProfile(stream, pod, port)
+			opaquePorts, err := getAnnotatedOpaquePorts(address.Pod, s.defaultOpaquePorts)
 			if err != nil {
-				log.Debugf("Failed to send profile response for host %s: %v", hostname, err)
-				return err
+				return fmt.Errorf("failed to get opaque ports for pod: %s", err)
+			}
+			var endpoint *pb.WeightedAddr
+			endpoint, err = s.createEndpoint(*address, opaquePorts)
+			if err != nil {
+				return fmt.Errorf("failed to create endpoint: %s", err)
+			}
+			translator := newEndpointProfileTranslator(address.Pod, port, endpoint, stream, s.log)
+
+			// If the endpoint's port is annotated as opaque, we don't need to
+			// subscribe for updates because it will always be opaque
+			// regardless of any Servers that may select it.
+			if _, ok := opaquePorts[port]; ok {
+				translator.UpdateProtocol(true)
+			} else if address.Pod == nil {
+				translator.UpdateProtocol(false)
+			} else {
+				translator.UpdateProtocol(address.OpaqueProtocol)
+				s.servers.Subscribe(address.Pod, port, translator)
+				defer s.servers.Unsubscribe(address.Pod, port, translator)
 			}
-
 			select {
 			case <-s.shutdown:
 			case <-stream.Context().Done():
 				log.Debugf("GetProfile(%+v) cancelled", dest)
 			}
-
 			return nil
 		}
 
@@ -266,7 +303,7 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 	// We build up the pipeline of profile updaters backwards, starting from
 	// the translator which takes profile updates, translates them to protobuf
 	// and pushes them onto the gRPC stream.
-	translator := newProfileTranslator(stream, log, fqn, port, nil)
+	translator := newProfileTranslator(stream, log, fqn, port)
 
 	// The traffic split adaptor merges profile updates with traffic split
 	// updates and publishes the result to the profile translator.
@@ -340,62 +377,35 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 	return nil
 }
 
-// sendEndpointProfile sends a DestinationProfile response back to the client.
-// If the pod argument is provided, the profile sent to the client will
-// include an endpoint. Otherwise, the default profile is sent.
-func (s *server) sendEndpointProfile(stream pb.Destination_GetProfileServer, pod *corev1.Pod, port uint32) error {
-	log := s.log
-	var endpoint *pb.WeightedAddr
-	opaquePorts := make(map[uint32]struct{})
-	var err error
-	if pod != nil {
-		podSet := podToAddressSet(s.k8sAPI, pod).WithPort(port)
-		podID := watcher.PodID{
-			Namespace: pod.Namespace,
-			Name:      pod.Name,
-		}
-		var ok bool
-		opaquePorts, ok, err = getPodOpaquePortsAnnotations(pod)
-		if err != nil {
-			log.Errorf("failed to get opaque ports annotation for pod: %s", err)
-		}
-
-		// If the opaque ports annotation was not set, then set the
-		// endpoint's opaque ports to the default value.
-		if !ok {
-			opaquePorts = s.defaultOpaquePorts
-		}
+func (s *server) createAddress(pod *corev1.Pod, port uint32) (watcher.Address, error) {
+	ownerKind, ownerName := s.k8sAPI.GetOwnerKindAndName(context.Background(), pod, true)
+	address := watcher.Address{
+		IP:        pod.Status.PodIP,
+		Port:      port,
+		Pod:       pod,
+		OwnerName: ownerName,
+		OwnerKind: ownerKind,
+	}
+	err := watcher.SetToServerProtocol(s.k8sAPI, &address, port)
+	if err != nil {
+		return watcher.Address{}, fmt.Errorf("failed to set address OpaqueProtocol: %s", err)
+	}
+	return address, nil
+}
 
-		skippedInboundPorts, err := getPodSkippedInboundPortsAnnotations(pod)
-		if err != nil {
-			log.Errorf("failed to get ignored inbound ports annotation for pod: %s", err)
-		}
+func (s *server) createEndpoint(address watcher.Address, opaquePorts map[uint32]struct{}) (*pb.WeightedAddr, error) {
+	weightedAddr, err := createWeightedAddr(address, opaquePorts, s.enableH2Upgrade, s.identityTrustDomain, s.controllerNS, s.log)
+	if err != nil {
+		return nil, err
+	}
 
-		endpoint, err = toWeightedAddr(podSet.Addresses[podID], opaquePorts, skippedInboundPorts, s.enableH2Upgrade, s.identityTrustDomain, s.controllerNS, s.log)
-		if err != nil {
-			return err
-		}
-		// `Get` doesn't include the namespace in the per-endpoint
-		// metadata, so it needs to be special-cased.
-		endpoint.MetricLabels["namespace"] = pod.Namespace
-	}
-
-	// Send the default profile without subscribing for future updates. The
-	// profile response will also include an endpoint if the IP (or hostname)
-	// sent in the profile request maps to a pod.
-	translator := newProfileTranslator(stream, log, "", port, endpoint)
-
-	// If there are opaque ports then update the profile translator
-	// with a service profile that has those values
-	if len(opaquePorts) != 0 {
-		sp := sp.ServiceProfile{}
-		sp.Spec.OpaquePorts = opaquePorts
-		translator.Update(&sp)
-	} else {
-		translator.Update(nil)
+	// `Get` doesn't include the namespace in the per-endpoint
+	// metadata, so it needs to be special-cased.
+	if address.Pod != nil {
+		weightedAddr.MetricLabels["namespace"] = address.Pod.Namespace
 	}
 
-	return nil
+	return weightedAddr, err
 }
 
 // getSvcID returns the service that corresponds to a Cluster IP address if one
@@ -428,27 +438,38 @@ func getSvcID(k8sAPI *k8s.API, clusterIP string, log *logging.Entry) (*watcher.S
 	return service, nil
 }
 
-// getPodByHostname returns a pod that maps to the given hostname (or an
+// getEndpointByHostname returns a pod that maps to the given hostname (or an
 // instanceID). The hostname is generally the prefix of the pod's DNS name;
 // since it may be arbitrary we need to look at the corresponding service's
 // Endpoints object to see whether the hostname matches a pod.
-func getPodByHostname(k8sAPI *k8s.API, hostname string, svcID watcher.ServiceID) (*corev1.Pod, error) {
+func (s *server) getEndpointByHostname(k8sAPI *k8s.API, hostname string, svcID watcher.ServiceID, port uint32) (*watcher.Address, error) {
 	ep, err := k8sAPI.Endpoint().Lister().Endpoints(svcID.Namespace).Get(svcID.Name)
 	if err != nil {
 		return nil, err
 	}
 
-	//TODO: add support for headless services with non-pod endpoints.
 	for _, subset := range ep.Subsets {
 		for _, addr := range subset.Addresses {
-			if addr.TargetRef == nil || addr.TargetRef.Kind != "Pod" {
-				continue
-			}
 
 			if hostname == addr.Hostname {
-				podName := addr.TargetRef.Name
-				podNamespace := addr.TargetRef.Namespace
-				return k8sAPI.Pod().Lister().Pods(podNamespace).Get(podName)
+				if addr.TargetRef != nil && addr.TargetRef.Kind == "Pod" {
+					podName := addr.TargetRef.Name
+					podNamespace := addr.TargetRef.Namespace
+					pod, err := k8sAPI.Pod().Lister().Pods(podNamespace).Get(podName)
+					if err != nil {
+						return nil, err
+					}
+					address, err := s.createAddress(pod, port)
+					if err != nil {
+						return nil, err
+					}
+					return &address, nil
+				}
+				return &watcher.Address{
+					IP:   addr.IP,
+					Port: port,
+				}, nil
+
 			}
 		}
 	}
@@ -527,26 +548,6 @@ func podReceivingTraffic(pod *corev1.Pod) bool {
 	return !podTerminating && !podTerminated
 }
 
-// podToAddressSet converts a Pod spec into a set of Addresses.
-func podToAddressSet(k8sAPI *k8s.API, pod *corev1.Pod) *watcher.AddressSet {
-	ownerKind, ownerName := k8sAPI.GetOwnerKindAndName(context.Background(), pod, true)
-	return &watcher.AddressSet{
-		Addresses: map[watcher.PodID]watcher.Address{
-			{
-				Name:      pod.Name,
-				Namespace: pod.Namespace,
-			}: {
-				IP:        pod.Status.PodIP,
-				Port:      0, // Will be set by individual subscriptions
-				Pod:       pod,
-				OwnerName: ownerName,
-				OwnerKind: ownerKind,
-			},
-		},
-		Labels: map[string]string{"namespace": pod.Namespace},
-	}
-}
-
 ////////////
 /// util ///
 ////////////
@@ -595,7 +596,7 @@ func profileID(authority string, ctxToken contextToken, clusterDomain string) (w
 func getHostAndPort(authority string) (string, watcher.Port, error) {
 	hostPort := strings.Split(authority, ":")
 	if len(hostPort) > 2 {
-		return "", 0, fmt.Errorf("Invalid destination %s", authority)
+		return "", 0, fmt.Errorf("invalid destination %s", authority)
 	}
 	host := hostPort[0]
 	port := 80
@@ -603,7 +604,7 @@ func getHostAndPort(authority string) (string, watcher.Port, error) {
 		var err error
 		port, err = strconv.Atoi(hostPort[1])
 		if err != nil || port <= 0 || port > 65535 {
-			return "", 0, fmt.Errorf("Invalid port %s", hostPort[1])
+			return "", 0, fmt.Errorf("invalid port %s", hostPort[1])
 		}
 	}
 	return host, watcher.Port(port), nil
@@ -660,22 +661,25 @@ func hasSuffix(slice []string, suffix []string) bool {
 	return true
 }
 
-func getPodOpaquePortsAnnotations(pod *corev1.Pod) (map[uint32]struct{}, bool, error) {
+func getAnnotatedOpaquePorts(pod *corev1.Pod, defaultPorts map[uint32]struct{}) (map[uint32]struct{}, error) {
+	if pod == nil {
+		return defaultPorts, nil
+	}
 	annotation, ok := pod.Annotations[labels.ProxyOpaquePortsAnnotation]
 	if !ok {
-		return nil, false, nil
+		return defaultPorts, nil
 	}
 	opaquePorts := make(map[uint32]struct{})
 	if annotation != "" {
 		for _, portStr := range util.ParseContainerOpaquePorts(annotation, pod.Spec.Containers) {
 			port, err := strconv.ParseUint(portStr, 10, 32)
 			if err != nil {
-				return nil, true, err
+				return nil, err
 			}
 			opaquePorts[uint32(port)] = struct{}{}
 		}
 	}
-	return opaquePorts, true, nil
+	return opaquePorts, nil
 }
 
 func getPodSkippedInboundPortsAnnotations(pod *corev1.Pod) (map[uint32]struct{}, error) {
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index 6877622eb..4d4b20a52 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
@@ -14,7 +15,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	discovery "k8s.io/api/discovery/v1beta1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	k8slabels "k8s.io/apimachinery/pkg/labels"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/tools/cache"
 )
@@ -28,6 +30,8 @@ const (
 	targetCluster          = "target_cluster"
 	targetService          = "target_service"
 	targetServiceNamespace = "target_service_namespace"
+
+	opaqueProtocol = "opaque"
 )
 
 const endpointTargetRefPod = "Pod"
@@ -50,6 +54,7 @@ type (
 		Identity          string
 		AuthorityOverride string
 		TopologyLabels    map[string]string
+		OpaqueProtocol    bool
 	}
 
 	// AddressSet is a set of Address, indexed by ID.
@@ -151,6 +156,12 @@ func NewEndpointsWatcher(k8sAPI *k8s.API, log *logging.Entry, enableEndpointSlic
 		UpdateFunc: func(_, obj interface{}) { ew.addService(obj) },
 	})
 
+	k8sAPI.Srv().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    ew.addServer,
+		DeleteFunc: ew.deleteServer,
+		UpdateFunc: func(_, obj interface{}) { ew.addServer(obj) },
+	})
+
 	if ew.enableEndpointSlices {
 		ew.log.Debugf("Watching EndpointSlice resources")
 		k8sAPI.ES().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -410,6 +421,24 @@ func (ew *EndpointsWatcher) getServicePublisher(id ServiceID) (sp *servicePublis
 	return
 }
 
+func (ew *EndpointsWatcher) addServer(obj interface{}) {
+	ew.Lock()
+	defer ew.Unlock()
+	server := obj.(*v1beta1.Server)
+	for _, sp := range ew.publishers {
+		sp.updateServer(server, true)
+	}
+}
+
+func (ew *EndpointsWatcher) deleteServer(obj interface{}) {
+	ew.Lock()
+	defer ew.Unlock()
+	server := obj.(*v1beta1.Server)
+	for _, sp := range ew.publishers {
+		sp.updateServer(server, false)
+	}
+}
+
 ////////////////////////
 /// servicePublisher ///
 ////////////////////////
@@ -546,7 +575,7 @@ func (sp *servicePublisher) newPortPublisher(srcPort Port, hostname string) *por
 
 	if port.enableEndpointSlices {
 		matchLabels := map[string]string{discovery.LabelServiceName: sp.id.Name}
-		selector := k8slabels.Set(matchLabels).AsSelector()
+		selector := labels.Set(matchLabels).AsSelector()
 
 		sliceList, err := sp.k8sAPI.ES().Lister().EndpointSlices(sp.id.Namespace).List(selector)
 		if err != nil && !apierrors.IsNotFound(err) {
@@ -574,6 +603,17 @@ func (sp *servicePublisher) metricsLabels(port Port, hostname string) prometheus
 	return endpointsLabels(sp.id.Namespace, sp.id.Name, strconv.Itoa(int(port)), hostname)
 }
 
+func (sp *servicePublisher) updateServer(server *v1beta1.Server, isAdd bool) {
+	selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+	if err != nil {
+		sp.log.Errorf("failed to create Selector: %s", err)
+		return
+	}
+	for _, pp := range sp.ports {
+		pp.updateServer(server, selector, isAdd)
+	}
+}
+
 /////////////////////
 /// portPublisher ///
 /////////////////////
@@ -700,15 +740,13 @@ func metricLabels(resource interface{}) map[string]string {
 }
 
 func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) AddressSet {
-	addressSet := AddressSet{
-		TopologicalPref: pp.TopologyPref,
-		Labels:          metricLabels(es),
-		Addresses:       make(map[ID]Address),
-	}
-
 	resolvedPort := pp.resolveESTargetPort(es.Ports)
 	if resolvedPort == undefinedEndpointPort {
-		return addressSet
+		return AddressSet{
+			Labels:          metricLabels(es),
+			Addresses:       make(map[ID]Address),
+			TopologicalPref: pp.TopologyPref,
+		}
 	}
 
 	serviceID, err := getEndpointSliceServiceID(es)
@@ -716,6 +754,7 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 		pp.log.Errorf("Could not fetch resource service name:%v", err)
 	}
 
+	addresses := make(map[ID]Address)
 	for _, endpoint := range es.Endpoints {
 		if endpoint.Hostname != nil {
 			if pp.hostname != "" && pp.hostname != *endpoint.Hostname {
@@ -735,15 +774,13 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 
 				identity := es.Annotations[consts.RemoteGatewayIdentity]
 				address, id := pp.newServiceRefAddress(resolvedPort, IPAddr, serviceID.Name, es.Namespace)
-				address.Identity, address.AuthorityOverride = authorityOverride, identity
+				address.Identity, address.AuthorityOverride = identity, authorityOverride
 
 				for k, v := range endpoint.Topology {
 					address.TopologyLabels[k] = v
 				}
-
-				addressSet.Addresses[id] = address
+				addresses[id] = address
 			}
-
 			continue
 		}
 
@@ -754,18 +791,24 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
 				}
-
+				err = SetToServerProtocol(pp.k8sAPI, &address, resolvedPort)
+				if err != nil {
+					pp.log.Errorf("failed to set address OpaqueProtocol: %s", err)
+					continue
+				}
 				for k, v := range endpoint.Topology {
 					address.TopologyLabels[k] = v
 				}
-
-				addressSet.Addresses[id] = address
+				addresses[id] = address
 			}
 		}
 
 	}
-
-	return addressSet
+	return AddressSet{
+		Addresses:       addresses,
+		Labels:          metricLabels(es),
+		TopologicalPref: pp.TopologyPref,
+	}
 }
 
 func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) AddressSet {
@@ -800,8 +843,10 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) Addre
 					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
 				}
+				err = SetToServerProtocol(pp.k8sAPI, &address, resolvedPort)
 				if err != nil {
-					pp.log.Errorf("failed to set opaque port annotation on pod: %s", err)
+					pp.log.Errorf("failed to set address OpaqueProtocol: %s", err)
+					continue
 				}
 				addresses[id] = address
 			}
@@ -859,7 +904,11 @@ func (pp *portPublisher) resolveESTargetPort(slicePorts []discovery.EndpointPort
 		return Port(pp.targetPort.IntVal)
 	case intstr.String:
 		for _, p := range slicePorts {
-			if *p.Name == pp.targetPort.StrVal {
+			name := ""
+			if p.Name != nil {
+				name = *p.Name
+			}
+			if name == pp.targetPort.StrVal {
 				return Port(*p.Port)
 			}
 		}
@@ -886,7 +935,7 @@ func (pp *portPublisher) updatePort(targetPort namedPort) {
 
 	if pp.enableEndpointSlices {
 		matchLabels := map[string]string{discovery.LabelServiceName: pp.id.Name}
-		selector := k8slabels.Set(matchLabels).AsSelector()
+		selector := labels.Set(matchLabels).AsSelector()
 
 		endpointSlices, err := pp.k8sAPI.ES().Lister().EndpointSlices(pp.id.Namespace).List(selector)
 		if err == nil {
@@ -978,6 +1027,41 @@ func (pp *portPublisher) unsubscribe(listener EndpointUpdateListener) {
 	pp.metrics.setSubscribers(len(pp.listeners))
 }
 
+func (pp *portPublisher) updateServer(server *v1beta1.Server, selector labels.Selector, isAdd bool) {
+	for id, address := range pp.addresses.Addresses {
+		if address.Pod != nil && selector.Matches(labels.Set(address.Pod.Labels)) {
+			var portMatch bool
+			switch server.Spec.Port.Type {
+			case intstr.Int:
+				if server.Spec.Port.IntVal == int32(address.Port) {
+					portMatch = true
+				}
+			case intstr.String:
+				for _, c := range address.Pod.Spec.Containers {
+					for _, p := range c.Ports {
+						if p.ContainerPort == int32(address.Port) && p.Name == server.Spec.Port.StrVal {
+							portMatch = true
+						}
+					}
+				}
+			default:
+				continue
+			}
+			if portMatch {
+				if isAdd && server.Spec.ProxyProtocol == opaqueProtocol {
+					address.OpaqueProtocol = true
+				} else {
+					address.OpaqueProtocol = false
+				}
+				pp.addresses.Addresses[id] = address
+			}
+		}
+	}
+	for _, listener := range pp.listeners {
+		listener.Add(pp.addresses)
+	}
+}
+
 ////////////
 /// util ///
 ////////////
@@ -1012,7 +1096,6 @@ func getTargetPort(service *corev1.Service, port Port) namedPort {
 	// port spec's name as the target port
 	for _, portSpec := range service.Spec.Ports {
 		if portSpec.Port == int32(port) {
-
 			return intstr.FromString(portSpec.Name)
 		}
 	}
@@ -1098,3 +1181,45 @@ func isValidSlice(es *discovery.EndpointSlice) bool {
 
 	return true
 }
+
+// SetToServerProtocol sets the address's OpaqueProtocol field based off any
+// Servers that select it and override the expected protocol.
+func SetToServerProtocol(k8sAPI *k8s.API, address *Address, port Port) error {
+	servers, err := k8sAPI.Srv().Lister().Servers("").List(labels.Everything())
+	if err != nil {
+		return fmt.Errorf("failed to list Servers: %s", err)
+	}
+	if address.Pod == nil {
+		return fmt.Errorf("endpoint not backed by Pod: %s:%d", address.IP, address.Port)
+	}
+	for _, server := range servers {
+		selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+		if err != nil {
+			return fmt.Errorf("failed to create Selector: %s", err)
+		}
+		if server.Spec.ProxyProtocol == opaqueProtocol && selector.Matches(labels.Set(address.Pod.Labels)) {
+			var portMatch bool
+			switch server.Spec.Port.Type {
+			case intstr.Int:
+				if server.Spec.Port.IntVal == int32(port) {
+					portMatch = true
+				}
+			case intstr.String:
+				for _, c := range address.Pod.Spec.Containers {
+					for _, p := range c.Ports {
+						if p.ContainerPort == int32(port) && p.Name == server.Spec.Port.StrVal {
+							portMatch = true
+						}
+					}
+				}
+			default:
+				continue
+			}
+			if portMatch {
+				address.OpaqueProtocol = true
+				return nil
+			}
+		}
+	}
+	return nil
+}
diff --git a/controller/api/destination/watcher/opaque_ports_watcher.go b/controller/api/destination/watcher/opaque_ports_watcher.go
index 809bd62ca..6f553c33c 100644
--- a/controller/api/destination/watcher/opaque_ports_watcher.go
+++ b/controller/api/destination/watcher/opaque_ports_watcher.go
@@ -8,7 +8,6 @@ import (
 	"github.com/linkerd/linkerd2/controller/k8s"
 	labels "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/util"
-	log "github.com/sirupsen/logrus"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/tools/cache"
@@ -212,7 +211,7 @@ func parseServiceOpaquePorts(annotation string, sps []corev1.ServicePort) []stri
 		} else {
 			pr, err := ports.ParsePortRange(pr)
 			if err != nil {
-				log.Warnf("Invalid port range [%v]: %s", pr, err)
+				logging.Warnf("Invalid port range [%v]: %s", pr, err)
 				continue
 			}
 			for i := pr.LowerBound; i <= pr.UpperBound; i++ {
diff --git a/controller/api/destination/watcher/server_watcher.go b/controller/api/destination/watcher/server_watcher.go
new file mode 100644
index 000000000..a8e91ccf8
--- /dev/null
+++ b/controller/api/destination/watcher/server_watcher.go
@@ -0,0 +1,151 @@
+package watcher
+
+import (
+	"sync"
+
+	"github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	"github.com/linkerd/linkerd2/controller/k8s"
+	logging "github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/client-go/tools/cache"
+)
+
+// ServerWatcher watches all the servers in the cluster. When there is an
+// update, it only sends updates to listeners if their endpoint's protocol
+// is changed by the Server.
+type ServerWatcher struct {
+	subscriptions map[podPort][]ServerUpdateListener
+	k8sAPI        *k8s.API
+	log           *logging.Entry
+	sync.RWMutex
+}
+
+type podPort struct {
+	pod  *corev1.Pod
+	port Port
+}
+
+// ServerUpdateListener is the interface that subscribers must implement.
+type ServerUpdateListener interface {
+	// UpdateProtocol takes a bool which is set to true if the endpoint is
+	// opaque and false otherwise. This value is used to send a
+	// DestinationProfile update to listeners for that endpoint.
+	UpdateProtocol(bool)
+}
+
+// NewServerWatcher creates a new ServerWatcher.
+func NewServerWatcher(k8sAPI *k8s.API, log *logging.Entry) *ServerWatcher {
+	sw := &ServerWatcher{
+		subscriptions: make(map[podPort][]ServerUpdateListener),
+		k8sAPI:        k8sAPI,
+		log:           log,
+	}
+	k8sAPI.Srv().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    sw.addServer,
+		DeleteFunc: sw.deleteServer,
+		UpdateFunc: func(_, obj interface{}) { sw.addServer(obj) },
+	})
+	return sw
+}
+
+// Subscribe subscribes a listener for any Server updates that may select the
+// endpoint and change its expected protocol.
+func (sw *ServerWatcher) Subscribe(pod *corev1.Pod, port Port, listener ServerUpdateListener) {
+	sw.Lock()
+	defer sw.Unlock()
+	pp := podPort{
+		pod:  pod,
+		port: port,
+	}
+	listeners, ok := sw.subscriptions[pp]
+	if !ok {
+		sw.subscriptions[pp] = []ServerUpdateListener{listener}
+		return
+	}
+	listeners = append(listeners, listener)
+	sw.subscriptions[pp] = listeners
+}
+
+// Unsubscribe unsubcribes a listener from any Server updates.
+func (sw *ServerWatcher) Unsubscribe(pod *corev1.Pod, port Port, listener ServerUpdateListener) {
+	sw.Lock()
+	defer sw.Unlock()
+	pp := podPort{
+		pod:  pod,
+		port: port,
+	}
+	listeners, ok := sw.subscriptions[pp]
+	if !ok {
+		sw.log.Errorf("cannot unsubscribe from unknown Pod: %s/%s:%d", pod.Namespace, pod.Name, port)
+		return
+	}
+	for i, l := range listeners {
+		if l == listener {
+			n := len(listeners)
+			listeners[i] = listeners[n-1]
+			listeners[n-1] = nil
+			listeners = listeners[:n-1]
+		}
+	}
+	sw.subscriptions[pp] = listeners
+}
+
+func (sw *ServerWatcher) addServer(obj interface{}) {
+	server := obj.(*v1beta1.Server)
+	selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+	if err != nil {
+		sw.log.Errorf("failed to create Selector: %s", err)
+		return
+	}
+	sw.updateServer(server, selector, true)
+}
+
+func (sw *ServerWatcher) deleteServer(obj interface{}) {
+	server := obj.(*v1beta1.Server)
+	selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+	if err != nil {
+		sw.log.Errorf("failed to create Selector: %s", err)
+		return
+	}
+	sw.updateServer(server, selector, false)
+}
+
+func (sw *ServerWatcher) updateServer(server *v1beta1.Server, selector labels.Selector, isAdd bool) {
+	sw.Lock()
+	defer sw.Unlock()
+	for pp, listeners := range sw.subscriptions {
+		if selector.Matches(labels.Set(pp.pod.Labels)) {
+			var portMatch bool
+			switch server.Spec.Port.Type {
+			case intstr.Int:
+				if server.Spec.Port.IntVal == int32(pp.port) {
+					portMatch = true
+				}
+			case intstr.String:
+				for _, c := range pp.pod.Spec.Containers {
+					for _, p := range c.Ports {
+						if p.ContainerPort == int32(pp.port) && p.Name == server.Spec.Port.StrVal {
+							portMatch = true
+						}
+					}
+				}
+			default:
+				continue
+			}
+			if portMatch {
+				var isOpaque bool
+				if isAdd && server.Spec.ProxyProtocol == opaqueProtocol {
+					isOpaque = true
+				} else {
+					isOpaque = false
+				}
+				for _, listener := range listeners {
+					listener.UpdateProtocol(isOpaque)
+				}
+			}
+		}
+	}
+}
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index 521cf1ed8..6102a49f8 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -95,14 +95,14 @@ func Main(args []string) {
 			ctx,
 			*kubeConfigPath,
 			true,
-			k8s.Endpoint, k8s.ES, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node,
+			k8s.Endpoint, k8s.ES, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node, k8s.Srv,
 		)
 	} else {
 		k8sAPI, err = k8s.InitializeAPI(
 			ctx,
 			*kubeConfigPath,
 			true,
-			k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node,
+			k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node, k8s.Srv,
 		)
 	}
 	if err != nil {
diff --git a/controller/gen/apis/server/register.go b/controller/gen/apis/server/register.go
new file mode 100644
index 000000000..1b137b300
--- /dev/null
+++ b/controller/gen/apis/server/register.go
@@ -0,0 +1,4 @@
+package server
+
+// GroupName identifies the API Group Name for a Server.
+const GroupName = "policy.linkerd.io"
diff --git a/controller/gen/apis/server/v1beta1/doc.go b/controller/gen/apis/server/v1beta1/doc.go
new file mode 100644
index 000000000..ffd023fe6
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/doc.go
@@ -0,0 +1,3 @@
+// +k8s:deepcopy-gen=package
+
+package v1beta1
diff --git a/controller/gen/apis/server/v1beta1/register.go b/controller/gen/apis/server/v1beta1/register.go
new file mode 100644
index 000000000..175a7ce81
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/register.go
@@ -0,0 +1,49 @@
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/linkerd/linkerd2/controller/gen/apis/server"
+)
+
+var (
+	// SchemeGroupVersion is the identifier for the API which includes the name
+	// of the group and the version of the API.
+	SchemeGroupVersion = schema.GroupVersion{
+		Group:   server.GroupName,
+		Version: "v1beta1",
+	}
+
+	// SchemeBuilder collects functions that add things to a scheme. It's to
+	// allow code to compile without explicitly referencing generated types.
+	// You should declare one in each package that will have generated deep
+	// copy or conversion functions.
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+
+	// AddToScheme applies all the stored functions to the scheme. A non-nil error
+	// indicates that one function failed and the attempt was abandoned.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified
+// GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&Server{},
+		&ServerList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/controller/gen/apis/server/v1beta1/types.go b/controller/gen/apis/server/v1beta1/types.go
new file mode 100644
index 000000000..099fa929c
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/types.go
@@ -0,0 +1,45 @@
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+// +genclient
+// +genclient:noStatus
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +groupName=policy.linkerd.io
+
+type Server struct {
+	// TypeMeta is the metadata for the resource, like kind and apiversion
+	metav1.TypeMeta `json:",inline"`
+
+	// ObjectMeta contains the metadata for the particular object, including
+	// things like...
+	//  - name
+	//  - namespace
+	//  - self link
+	//  - labels
+	//  - ... etc ...
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// Spec is the custom resource spec
+	Spec ServerSpec `json:"spec"`
+}
+
+// ServerSpec specifies a Server resource.
+type ServerSpec struct {
+	PodSelector   *metav1.LabelSelector `json:"podSelector,omitempty"`
+	Port          intstr.IntOrString    `json:"port,omitempty"`
+	ProxyProtocol string                `json:"proxyProtocol,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// ServerList is a list of Server resources.
+type ServerList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []Server `json:"items"`
+}
diff --git a/controller/gen/apis/server/v1beta1/zz_generated.deepcopy.go b/controller/gen/apis/server/v1beta1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..9a6fa4257
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/zz_generated.deepcopy.go
@@ -0,0 +1,109 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Server) DeepCopyInto(out *Server) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Server.
+func (in *Server) DeepCopy() *Server {
+	if in == nil {
+		return nil
+	}
+	out := new(Server)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Server) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerList) DeepCopyInto(out *ServerList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Server, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerList.
+func (in *ServerList) DeepCopy() *ServerList {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ServerList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerSpec) DeepCopyInto(out *ServerSpec) {
+	*out = *in
+	if in.PodSelector != nil {
+		in, out := &in.PodSelector, &out.PodSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	out.Port = in.Port
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSpec.
+func (in *ServerSpec) DeepCopy() *ServerSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/controller/gen/apis/serverauthorization/register.go b/controller/gen/apis/serverauthorization/register.go
new file mode 100644
index 000000000..05a68ae74
--- /dev/null
+++ b/controller/gen/apis/serverauthorization/register.go
@@ -0,0 +1,4 @@
+package serverauthorization
+
+// GroupName identifies the API Group Name for a ServerAuthorization.
+const GroupName = "policy.linkerd.io"
diff --git a/controller/gen/apis/serverauthorization/v1beta1/doc.go b/controller/gen/apis/serverauthorization/v1beta1/doc.go
new file mode 100644
index 000000000..ffd023fe6
--- /dev/null
+++ b/controller/gen/apis/serverauthorization/v1beta1/doc.go
@@ -0,0 +1,3 @@
+// +k8s:deepcopy-gen=package
+
+package v1beta1
diff --git a/controller/gen/apis/serverauthorization/v1beta1/register.go b/controller/gen/apis/serverauthorization/v1beta1/register.go
new file mode 100644
index 000000000..6a94f302b
--- /dev/null
+++ b/controller/gen/apis/serverauthorization/v1beta1/register.go
@@ -0,0 +1,49 @@
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization"
+)
+
+var (
+	// SchemeGroupVersion is the identifier for the API which includes the name
+	// of the group and the version of the API.
+	SchemeGroupVersion = schema.GroupVersion{
+		Group:   serverauthorization.GroupName,
+		Version: "v1beta1",
+	}
+
+	// SchemeBuilder collects functions that add things to a scheme. It's to
+	// allow code to compile without explicitly referencing generated types.
+	// You should declare one in each package that will have generated deep
+	// copy or conversion functions.
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+
+	// AddToScheme applies all the stored functions to the scheme. A non-nil error
+	// indicates that one function failed and the attempt was abandoned.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified
+// GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&ServerAuthorization{},
+		&ServerAuthorizationList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/controller/gen/apis/serverauthorization/v1beta1/types.go b/controller/gen/apis/serverauthorization/v1beta1/types.go
new file mode 100644
index 000000000..f2a4d68e3
--- /dev/null
+++ b/controller/gen/apis/serverauthorization/v1beta1/types.go
@@ -0,0 +1,76 @@
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +genclient
+// +genclient:noStatus
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +groupName=policy.linkerd.io
+
+type ServerAuthorization struct {
+	// TypeMeta is the metadata for the resource, like kind and apiversion
+	metav1.TypeMeta `json:",inline"`
+
+	// ObjectMeta contains the metadata for the particular object, including
+	// things like...
+	//  - name
+	//  - namespace
+	//  - self link
+	//  - labels
+	//  - ... etc ...
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// Spec is the custom resource spec
+	Spec ServerAuthorizationSpec `json:"spec"`
+}
+
+// ServerAuthorizationSpec specifies a ServerAuthorization resource.
+type ServerAuthorizationSpec struct {
+	Server Server `json:"server,omitempty"`
+	Client Client `json:"client,omitempty"`
+}
+
+// Server is the Server that a ServerAuthorization uses.
+type Server struct {
+	Name     string                `json:"name,omitempty"`
+	Selector *metav1.LabelSelector `json:"selector,omitempty"`
+}
+
+// Client describes which clients a ServerAuthorization authorizes.
+type Client struct {
+	Networks        []*Cidr  `json:"networks,omitempty"`
+	MeshTLS         *MeshTLS `json:"meshTLS,omitempty"`
+	Unauthenticated bool     `json:"unauthenticated,omitempty"`
+}
+
+// Cidr describes which client CIDRs a ServerAuthorization authorizes.
+type Cidr struct {
+	Cidr   string   `json:"cidr,omitempty"`
+	Except []string `json:"except,omitempty"`
+}
+
+// MeshTLS describes which meshed clients are authorized.
+type MeshTLS struct {
+	UnauthenticatedTLS bool                  `json:"unauthenticatedTLS,omitempty"`
+	Identities         []string              `json:"identities,omitempty"`
+	ServiceAccounts    []*ServiceAccountName `json:"serviceAccounts,omitempty"`
+}
+
+// ServiceAccountName is the structure of a service account name.
+
+type ServiceAccountName struct {
+	Name      string `json:"name,omitempty"`
+	Namespace string `json:"namespace,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// ServerAuthorizationList is a list of Server resources.
+type ServerAuthorizationList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []ServerAuthorization `json:"items"`
+}
diff --git a/controller/gen/apis/serverauthorization/v1beta1/zz_generated.deepcopy.go b/controller/gen/apis/serverauthorization/v1beta1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..d723446fc
--- /dev/null
+++ b/controller/gen/apis/serverauthorization/v1beta1/zz_generated.deepcopy.go
@@ -0,0 +1,227 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Cidr) DeepCopyInto(out *Cidr) {
+	*out = *in
+	if in.Except != nil {
+		in, out := &in.Except, &out.Except
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cidr.
+func (in *Cidr) DeepCopy() *Cidr {
+	if in == nil {
+		return nil
+	}
+	out := new(Cidr)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Client) DeepCopyInto(out *Client) {
+	*out = *in
+	if in.Networks != nil {
+		in, out := &in.Networks, &out.Networks
+		*out = make([]*Cidr, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Cidr)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+	if in.MeshTLS != nil {
+		in, out := &in.MeshTLS, &out.MeshTLS
+		*out = new(MeshTLS)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Client.
+func (in *Client) DeepCopy() *Client {
+	if in == nil {
+		return nil
+	}
+	out := new(Client)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MeshTLS) DeepCopyInto(out *MeshTLS) {
+	*out = *in
+	if in.Identities != nil {
+		in, out := &in.Identities, &out.Identities
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.ServiceAccounts != nil {
+		in, out := &in.ServiceAccounts, &out.ServiceAccounts
+		*out = make([]*ServiceAccountName, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(ServiceAccountName)
+				**out = **in
+			}
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshTLS.
+func (in *MeshTLS) DeepCopy() *MeshTLS {
+	if in == nil {
+		return nil
+	}
+	out := new(MeshTLS)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Server) DeepCopyInto(out *Server) {
+	*out = *in
+	if in.Selector != nil {
+		in, out := &in.Selector, &out.Selector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Server.
+func (in *Server) DeepCopy() *Server {
+	if in == nil {
+		return nil
+	}
+	out := new(Server)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerAuthorization) DeepCopyInto(out *ServerAuthorization) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerAuthorization.
+func (in *ServerAuthorization) DeepCopy() *ServerAuthorization {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerAuthorization)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ServerAuthorization) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerAuthorizationList) DeepCopyInto(out *ServerAuthorizationList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ServerAuthorization, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerAuthorizationList.
+func (in *ServerAuthorizationList) DeepCopy() *ServerAuthorizationList {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerAuthorizationList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ServerAuthorizationList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerAuthorizationSpec) DeepCopyInto(out *ServerAuthorizationSpec) {
+	*out = *in
+	in.Server.DeepCopyInto(&out.Server)
+	in.Client.DeepCopyInto(&out.Client)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerAuthorizationSpec.
+func (in *ServerAuthorizationSpec) DeepCopy() *ServerAuthorizationSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerAuthorizationSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServiceAccountName) DeepCopyInto(out *ServiceAccountName) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountName.
+func (in *ServiceAccountName) DeepCopy() *ServiceAccountName {
+	if in == nil {
+		return nil
+	}
+	out := new(ServiceAccountName)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/controller/gen/apis/serviceprofile/v1alpha2/register.go b/controller/gen/apis/serviceprofile/v1alpha2/register.go
index 6b3847a80..5d490b7a1 100644
--- a/controller/gen/apis/serviceprofile/v1alpha2/register.go
+++ b/controller/gen/apis/serviceprofile/v1alpha2/register.go
@@ -5,13 +5,13 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
-	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile"
+	"github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile"
 )
 
 // SchemeGroupVersion is the identifier for the API which includes
 // the name of the group and the version of the API
 var SchemeGroupVersion = schema.GroupVersion{
-	Group:   sp.GroupName,
+	Group:   serviceprofile.GroupName,
 	Version: "v1alpha2",
 }
 
diff --git a/controller/gen/client/clientset/versioned/clientset.go b/controller/gen/client/clientset/versioned/clientset.go
index 5bbbcba45..2480a53ca 100644
--- a/controller/gen/client/clientset/versioned/clientset.go
+++ b/controller/gen/client/clientset/versioned/clientset.go
@@ -21,6 +21,8 @@ package versioned
 import (
 	"fmt"
 
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serviceprofile/v1alpha2"
 	discovery "k8s.io/client-go/discovery"
 	rest "k8s.io/client-go/rest"
@@ -29,6 +31,8 @@ import (
 
 type Interface interface {
 	Discovery() discovery.DiscoveryInterface
+	ServerV1beta1() serverv1beta1.ServerV1beta1Interface
+	ServerauthorizationV1beta1() serverauthorizationv1beta1.ServerauthorizationV1beta1Interface
 	LinkerdV1alpha2() linkerdv1alpha2.LinkerdV1alpha2Interface
 }
 
@@ -36,7 +40,19 @@ type Interface interface {
 // version included in a Clientset.
 type Clientset struct {
 	*discovery.DiscoveryClient
-	linkerdV1alpha2 *linkerdv1alpha2.LinkerdV1alpha2Client
+	serverV1beta1              *serverv1beta1.ServerV1beta1Client
+	serverauthorizationV1beta1 *serverauthorizationv1beta1.ServerauthorizationV1beta1Client
+	linkerdV1alpha2            *linkerdv1alpha2.LinkerdV1alpha2Client
+}
+
+// ServerV1beta1 retrieves the ServerV1beta1Client
+func (c *Clientset) ServerV1beta1() serverv1beta1.ServerV1beta1Interface {
+	return c.serverV1beta1
+}
+
+// ServerauthorizationV1beta1 retrieves the ServerauthorizationV1beta1Client
+func (c *Clientset) ServerauthorizationV1beta1() serverauthorizationv1beta1.ServerauthorizationV1beta1Interface {
+	return c.serverauthorizationV1beta1
 }
 
 // LinkerdV1alpha2 retrieves the LinkerdV1alpha2Client
@@ -65,6 +81,14 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 	}
 	var cs Clientset
 	var err error
+	cs.serverV1beta1, err = serverv1beta1.NewForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
+	cs.serverauthorizationV1beta1, err = serverauthorizationv1beta1.NewForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
 	cs.linkerdV1alpha2, err = linkerdv1alpha2.NewForConfig(&configShallowCopy)
 	if err != nil {
 		return nil, err
@@ -81,6 +105,8 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 // panics if there is an error in the config.
 func NewForConfigOrDie(c *rest.Config) *Clientset {
 	var cs Clientset
+	cs.serverV1beta1 = serverv1beta1.NewForConfigOrDie(c)
+	cs.serverauthorizationV1beta1 = serverauthorizationv1beta1.NewForConfigOrDie(c)
 	cs.linkerdV1alpha2 = linkerdv1alpha2.NewForConfigOrDie(c)
 
 	cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
@@ -90,6 +116,8 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
 // New creates a new Clientset for the given RESTClient.
 func New(c rest.Interface) *Clientset {
 	var cs Clientset
+	cs.serverV1beta1 = serverv1beta1.New(c)
+	cs.serverauthorizationV1beta1 = serverauthorizationv1beta1.New(c)
 	cs.linkerdV1alpha2 = linkerdv1alpha2.New(c)
 
 	cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
diff --git a/controller/gen/client/clientset/versioned/fake/clientset_generated.go b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
index a1bc3d5cf..67d65cb26 100644
--- a/controller/gen/client/clientset/versioned/fake/clientset_generated.go
+++ b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
@@ -20,6 +20,10 @@ package fake
 
 import (
 	clientset "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1"
+	fakeserverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1"
+	fakeserverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serviceprofile/v1alpha2"
 	fakelinkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serviceprofile/v1alpha2/fake"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -76,6 +80,16 @@ func (c *Clientset) Tracker() testing.ObjectTracker {
 
 var _ clientset.Interface = &Clientset{}
 
+// ServerV1beta1 retrieves the ServerV1beta1Client
+func (c *Clientset) ServerV1beta1() serverv1beta1.ServerV1beta1Interface {
+	return &fakeserverv1beta1.FakeServerV1beta1{Fake: &c.Fake}
+}
+
+// ServerauthorizationV1beta1 retrieves the ServerauthorizationV1beta1Client
+func (c *Clientset) ServerauthorizationV1beta1() serverauthorizationv1beta1.ServerauthorizationV1beta1Interface {
+	return &fakeserverauthorizationv1beta1.FakeServerauthorizationV1beta1{Fake: &c.Fake}
+}
+
 // LinkerdV1alpha2 retrieves the LinkerdV1alpha2Client
 func (c *Clientset) LinkerdV1alpha2() linkerdv1alpha2.LinkerdV1alpha2Interface {
 	return &fakelinkerdv1alpha2.FakeLinkerdV1alpha2{Fake: &c.Fake}
diff --git a/controller/gen/client/clientset/versioned/fake/register.go b/controller/gen/client/clientset/versioned/fake/register.go
index d69dce2d5..53c05792f 100644
--- a/controller/gen/client/clientset/versioned/fake/register.go
+++ b/controller/gen/client/clientset/versioned/fake/register.go
@@ -19,6 +19,8 @@ limitations under the License.
 package fake
 
 import (
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
@@ -31,6 +33,8 @@ var scheme = runtime.NewScheme()
 var codecs = serializer.NewCodecFactory(scheme)
 
 var localSchemeBuilder = runtime.SchemeBuilder{
+	serverv1beta1.AddToScheme,
+	serverauthorizationv1beta1.AddToScheme,
 	linkerdv1alpha2.AddToScheme,
 }
 
diff --git a/controller/gen/client/clientset/versioned/scheme/register.go b/controller/gen/client/clientset/versioned/scheme/register.go
index 662997a96..9a51167aa 100644
--- a/controller/gen/client/clientset/versioned/scheme/register.go
+++ b/controller/gen/client/clientset/versioned/scheme/register.go
@@ -19,6 +19,8 @@ limitations under the License.
 package scheme
 
 import (
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
@@ -31,6 +33,8 @@ var Scheme = runtime.NewScheme()
 var Codecs = serializer.NewCodecFactory(Scheme)
 var ParameterCodec = runtime.NewParameterCodec(Scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
+	serverv1beta1.AddToScheme,
+	serverauthorizationv1beta1.AddToScheme,
 	linkerdv1alpha2.AddToScheme,
 }
 
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/doc.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/doc.go
new file mode 100644
index 000000000..771101956
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1beta1
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/doc.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/doc.go
new file mode 100644
index 000000000..16f443990
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go
new file mode 100644
index 000000000..f9a6f1db0
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go
@@ -0,0 +1,130 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeServers implements ServerInterface
+type FakeServers struct {
+	Fake *FakeServerV1beta1
+	ns   string
+}
+
+var serversResource = schema.GroupVersionResource{Group: "policy.linkerd.io", Version: "v1beta1", Resource: "servers"}
+
+var serversKind = schema.GroupVersionKind{Group: "policy.linkerd.io", Version: "v1beta1", Kind: "Server"}
+
+// Get takes name of the server, and returns the corresponding server object, and an error if there is any.
+func (c *FakeServers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(serversResource, c.ns, name), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
+
+// List takes label and field selectors, and returns the list of Servers that match those selectors.
+func (c *FakeServers) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ServerList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(serversResource, serversKind, c.ns, opts), &v1beta1.ServerList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta1.ServerList{ListMeta: obj.(*v1beta1.ServerList).ListMeta}
+	for _, item := range obj.(*v1beta1.ServerList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested servers.
+func (c *FakeServers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(serversResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a server and creates it.  Returns the server's representation of the server, and an error, if there is any.
+func (c *FakeServers) Create(ctx context.Context, server *v1beta1.Server, opts v1.CreateOptions) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(serversResource, c.ns, server), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
+
+// Update takes the representation of a server and updates it. Returns the server's representation of the server, and an error, if there is any.
+func (c *FakeServers) Update(ctx context.Context, server *v1beta1.Server, opts v1.UpdateOptions) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(serversResource, c.ns, server), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
+
+// Delete takes name of the server and deletes it. Returns an error if one occurs.
+func (c *FakeServers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(serversResource, c.ns, name), &v1beta1.Server{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeServers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(serversResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta1.ServerList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched server.
+func (c *FakeServers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(serversResource, c.ns, name, pt, data, subresources...), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server_client.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server_client.go
new file mode 100644
index 000000000..ccb82dc2b
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server_client.go
@@ -0,0 +1,40 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeServerV1beta1 struct {
+	*testing.Fake
+}
+
+func (c *FakeServerV1beta1) Servers(namespace string) v1beta1.ServerInterface {
+	return &FakeServers{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeServerV1beta1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/generated_expansion.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/generated_expansion.go
new file mode 100644
index 000000000..386f7bb3c
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+type ServerExpansion interface{}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/server.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server.go
new file mode 100644
index 000000000..0296afa77
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server.go
@@ -0,0 +1,178 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	"time"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	scheme "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// ServersGetter has a method to return a ServerInterface.
+// A group's client should implement this interface.
+type ServersGetter interface {
+	Servers(namespace string) ServerInterface
+}
+
+// ServerInterface has methods to work with Server resources.
+type ServerInterface interface {
+	Create(ctx context.Context, server *v1beta1.Server, opts v1.CreateOptions) (*v1beta1.Server, error)
+	Update(ctx context.Context, server *v1beta1.Server, opts v1.UpdateOptions) (*v1beta1.Server, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.Server, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.ServerList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Server, err error)
+	ServerExpansion
+}
+
+// servers implements ServerInterface
+type servers struct {
+	client rest.Interface
+	ns     string
+}
+
+// newServers returns a Servers
+func newServers(c *ServerV1beta1Client, namespace string) *servers {
+	return &servers{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the server, and returns the corresponding server object, and an error if there is any.
+func (c *servers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("servers").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of Servers that match those selectors.
+func (c *servers) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ServerList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta1.ServerList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested servers.
+func (c *servers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a server and creates it.  Returns the server's representation of the server, and an error, if there is any.
+func (c *servers) Create(ctx context.Context, server *v1beta1.Server, opts v1.CreateOptions) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(server).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a server and updates it. Returns the server's representation of the server, and an error, if there is any.
+func (c *servers) Update(ctx context.Context, server *v1beta1.Server, opts v1.UpdateOptions) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("servers").
+		Name(server.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(server).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the server and deletes it. Returns an error if one occurs.
+func (c *servers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("servers").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *servers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched server.
+func (c *servers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("servers").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/server_client.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server_client.go
new file mode 100644
index 000000000..f3a7369b1
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server_client.go
@@ -0,0 +1,89 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type ServerV1beta1Interface interface {
+	RESTClient() rest.Interface
+	ServersGetter
+}
+
+// ServerV1beta1Client is used to interact with features provided by the server group.
+type ServerV1beta1Client struct {
+	restClient rest.Interface
+}
+
+func (c *ServerV1beta1Client) Servers(namespace string) ServerInterface {
+	return newServers(c, namespace)
+}
+
+// NewForConfig creates a new ServerV1beta1Client for the given config.
+func NewForConfig(c *rest.Config) (*ServerV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return &ServerV1beta1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new ServerV1beta1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *ServerV1beta1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new ServerV1beta1Client for the given RESTClient.
+func New(c rest.Interface) *ServerV1beta1Client {
+	return &ServerV1beta1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1beta1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *ServerV1beta1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/doc.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/doc.go
new file mode 100644
index 000000000..771101956
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1beta1
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/doc.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/doc.go
new file mode 100644
index 000000000..16f443990
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization.go
new file mode 100644
index 000000000..48f564719
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization.go
@@ -0,0 +1,130 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeServerAuthorizations implements ServerAuthorizationInterface
+type FakeServerAuthorizations struct {
+	Fake *FakeServerauthorizationV1beta1
+	ns   string
+}
+
+var serverauthorizationsResource = schema.GroupVersionResource{Group: "policy.linkerd.io", Version: "v1beta1", Resource: "serverauthorizations"}
+
+var serverauthorizationsKind = schema.GroupVersionKind{Group: "policy.linkerd.io", Version: "v1beta1", Kind: "ServerAuthorization"}
+
+// Get takes name of the serverAuthorization, and returns the corresponding serverAuthorization object, and an error if there is any.
+func (c *FakeServerAuthorizations) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.ServerAuthorization, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(serverauthorizationsResource, c.ns, name), &v1beta1.ServerAuthorization{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.ServerAuthorization), err
+}
+
+// List takes label and field selectors, and returns the list of ServerAuthorizations that match those selectors.
+func (c *FakeServerAuthorizations) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ServerAuthorizationList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(serverauthorizationsResource, serverauthorizationsKind, c.ns, opts), &v1beta1.ServerAuthorizationList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta1.ServerAuthorizationList{ListMeta: obj.(*v1beta1.ServerAuthorizationList).ListMeta}
+	for _, item := range obj.(*v1beta1.ServerAuthorizationList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested serverAuthorizations.
+func (c *FakeServerAuthorizations) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(serverauthorizationsResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a serverAuthorization and creates it.  Returns the server's representation of the serverAuthorization, and an error, if there is any.
+func (c *FakeServerAuthorizations) Create(ctx context.Context, serverAuthorization *v1beta1.ServerAuthorization, opts v1.CreateOptions) (result *v1beta1.ServerAuthorization, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(serverauthorizationsResource, c.ns, serverAuthorization), &v1beta1.ServerAuthorization{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.ServerAuthorization), err
+}
+
+// Update takes the representation of a serverAuthorization and updates it. Returns the server's representation of the serverAuthorization, and an error, if there is any.
+func (c *FakeServerAuthorizations) Update(ctx context.Context, serverAuthorization *v1beta1.ServerAuthorization, opts v1.UpdateOptions) (result *v1beta1.ServerAuthorization, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(serverauthorizationsResource, c.ns, serverAuthorization), &v1beta1.ServerAuthorization{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.ServerAuthorization), err
+}
+
+// Delete takes name of the serverAuthorization and deletes it. Returns an error if one occurs.
+func (c *FakeServerAuthorizations) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(serverauthorizationsResource, c.ns, name), &v1beta1.ServerAuthorization{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeServerAuthorizations) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(serverauthorizationsResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta1.ServerAuthorizationList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched serverAuthorization.
+func (c *FakeServerAuthorizations) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ServerAuthorization, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(serverauthorizationsResource, c.ns, name, pt, data, subresources...), &v1beta1.ServerAuthorization{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.ServerAuthorization), err
+}
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization_client.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization_client.go
new file mode 100644
index 000000000..315ebf6ea
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/fake/fake_serverauthorization_client.go
@@ -0,0 +1,40 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeServerauthorizationV1beta1 struct {
+	*testing.Fake
+}
+
+func (c *FakeServerauthorizationV1beta1) ServerAuthorizations(namespace string) v1beta1.ServerAuthorizationInterface {
+	return &FakeServerAuthorizations{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeServerauthorizationV1beta1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/generated_expansion.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/generated_expansion.go
new file mode 100644
index 000000000..2adc96f7b
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+type ServerAuthorizationExpansion interface{}
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/serverauthorization.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/serverauthorization.go
new file mode 100644
index 000000000..fd75467a8
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/serverauthorization.go
@@ -0,0 +1,178 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	"time"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
+	scheme "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// ServerAuthorizationsGetter has a method to return a ServerAuthorizationInterface.
+// A group's client should implement this interface.
+type ServerAuthorizationsGetter interface {
+	ServerAuthorizations(namespace string) ServerAuthorizationInterface
+}
+
+// ServerAuthorizationInterface has methods to work with ServerAuthorization resources.
+type ServerAuthorizationInterface interface {
+	Create(ctx context.Context, serverAuthorization *v1beta1.ServerAuthorization, opts v1.CreateOptions) (*v1beta1.ServerAuthorization, error)
+	Update(ctx context.Context, serverAuthorization *v1beta1.ServerAuthorization, opts v1.UpdateOptions) (*v1beta1.ServerAuthorization, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.ServerAuthorization, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.ServerAuthorizationList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ServerAuthorization, err error)
+	ServerAuthorizationExpansion
+}
+
+// serverAuthorizations implements ServerAuthorizationInterface
+type serverAuthorizations struct {
+	client rest.Interface
+	ns     string
+}
+
+// newServerAuthorizations returns a ServerAuthorizations
+func newServerAuthorizations(c *ServerauthorizationV1beta1Client, namespace string) *serverAuthorizations {
+	return &serverAuthorizations{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the serverAuthorization, and returns the corresponding serverAuthorization object, and an error if there is any.
+func (c *serverAuthorizations) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.ServerAuthorization, err error) {
+	result = &v1beta1.ServerAuthorization{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of ServerAuthorizations that match those selectors.
+func (c *serverAuthorizations) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ServerAuthorizationList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta1.ServerAuthorizationList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested serverAuthorizations.
+func (c *serverAuthorizations) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a serverAuthorization and creates it.  Returns the server's representation of the serverAuthorization, and an error, if there is any.
+func (c *serverAuthorizations) Create(ctx context.Context, serverAuthorization *v1beta1.ServerAuthorization, opts v1.CreateOptions) (result *v1beta1.ServerAuthorization, err error) {
+	result = &v1beta1.ServerAuthorization{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(serverAuthorization).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a serverAuthorization and updates it. Returns the server's representation of the serverAuthorization, and an error, if there is any.
+func (c *serverAuthorizations) Update(ctx context.Context, serverAuthorization *v1beta1.ServerAuthorization, opts v1.UpdateOptions) (result *v1beta1.ServerAuthorization, err error) {
+	result = &v1beta1.ServerAuthorization{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		Name(serverAuthorization.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(serverAuthorization).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the serverAuthorization and deletes it. Returns an error if one occurs.
+func (c *serverAuthorizations) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *serverAuthorizations) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched serverAuthorization.
+func (c *serverAuthorizations) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ServerAuthorization, err error) {
+	result = &v1beta1.ServerAuthorization{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("serverauthorizations").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/serverauthorization_client.go b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/serverauthorization_client.go
new file mode 100644
index 000000000..5b5f330d9
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1/serverauthorization_client.go
@@ -0,0 +1,89 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
+	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type ServerauthorizationV1beta1Interface interface {
+	RESTClient() rest.Interface
+	ServerAuthorizationsGetter
+}
+
+// ServerauthorizationV1beta1Client is used to interact with features provided by the serverauthorization group.
+type ServerauthorizationV1beta1Client struct {
+	restClient rest.Interface
+}
+
+func (c *ServerauthorizationV1beta1Client) ServerAuthorizations(namespace string) ServerAuthorizationInterface {
+	return newServerAuthorizations(c, namespace)
+}
+
+// NewForConfig creates a new ServerauthorizationV1beta1Client for the given config.
+func NewForConfig(c *rest.Config) (*ServerauthorizationV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return &ServerauthorizationV1beta1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new ServerauthorizationV1beta1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *ServerauthorizationV1beta1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new ServerauthorizationV1beta1Client for the given RESTClient.
+func New(c rest.Interface) *ServerauthorizationV1beta1Client {
+	return &ServerauthorizationV1beta1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1beta1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *ServerauthorizationV1beta1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/controller/gen/client/informers/externalversions/factory.go b/controller/gen/client/informers/externalversions/factory.go
index c969b5b72..5f9b5e62d 100644
--- a/controller/gen/client/informers/externalversions/factory.go
+++ b/controller/gen/client/informers/externalversions/factory.go
@@ -25,6 +25,8 @@ import (
 
 	versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
 	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	server "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server"
+	serverauthorization "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serverauthorization"
 	serviceprofile "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serviceprofile"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
@@ -172,9 +174,19 @@ type SharedInformerFactory interface {
 	ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
 	WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
 
+	Server() server.Interface
+	Serverauthorization() serverauthorization.Interface
 	Linkerd() serviceprofile.Interface
 }
 
+func (f *sharedInformerFactory) Server() server.Interface {
+	return server.New(f, f.namespace, f.tweakListOptions)
+}
+
+func (f *sharedInformerFactory) Serverauthorization() serverauthorization.Interface {
+	return serverauthorization.New(f, f.namespace, f.tweakListOptions)
+}
+
 func (f *sharedInformerFactory) Linkerd() serviceprofile.Interface {
 	return serviceprofile.New(f, f.namespace, f.tweakListOptions)
 }
diff --git a/controller/gen/client/informers/externalversions/generic.go b/controller/gen/client/informers/externalversions/generic.go
index e99de6a56..7166b697f 100644
--- a/controller/gen/client/informers/externalversions/generic.go
+++ b/controller/gen/client/informers/externalversions/generic.go
@@ -21,6 +21,8 @@ package externalversions
 import (
 	"fmt"
 
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
 	cache "k8s.io/client-go/tools/cache"
@@ -56,6 +58,14 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	case v1alpha2.SchemeGroupVersion.WithResource("serviceprofiles"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Linkerd().V1alpha2().ServiceProfiles().Informer()}, nil
 
+		// Group=server, Version=v1beta1
+	case v1beta1.SchemeGroupVersion.WithResource("servers"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Server().V1beta1().Servers().Informer()}, nil
+
+		// Group=serverauthorization, Version=v1beta1
+	case serverauthorizationv1beta1.SchemeGroupVersion.WithResource("serverauthorizations"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Serverauthorization().V1beta1().ServerAuthorizations().Informer()}, nil
+
 	}
 
 	return nil, fmt.Errorf("no informer found for %v", resource)
diff --git a/controller/gen/client/informers/externalversions/server/interface.go b/controller/gen/client/informers/externalversions/server/interface.go
new file mode 100644
index 000000000..d50785364
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/server/interface.go
@@ -0,0 +1,46 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package server
+
+import (
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta1"
+)
+
+// Interface provides access to each of this group's versions.
+type Interface interface {
+	// V1beta1 provides access to shared informers for resources in V1beta1.
+	V1beta1() v1beta1.Interface
+}
+
+type group struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// V1beta1 returns a new v1beta1.Interface.
+func (g *group) V1beta1() v1beta1.Interface {
+	return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/controller/gen/client/informers/externalversions/server/v1beta1/interface.go b/controller/gen/client/informers/externalversions/server/v1beta1/interface.go
new file mode 100644
index 000000000..717142453
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/server/v1beta1/interface.go
@@ -0,0 +1,45 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// Servers returns a ServerInformer.
+	Servers() ServerInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// Servers returns a ServerInformer.
+func (v *version) Servers() ServerInformer {
+	return &serverInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/controller/gen/client/informers/externalversions/server/v1beta1/server.go b/controller/gen/client/informers/externalversions/server/v1beta1/server.go
new file mode 100644
index 000000000..4267426a4
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/server/v1beta1/server.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	time "time"
+
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/listers/server/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// ServerInformer provides access to a shared informer and lister for
+// Servers.
+type ServerInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta1.ServerLister
+}
+
+type serverInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewServerInformer constructs a new informer for Server type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewServerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredServerInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredServerInformer constructs a new informer for Server type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredServerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ServerV1beta1().Servers(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ServerV1beta1().Servers(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&serverv1beta1.Server{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *serverInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredServerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *serverInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&serverv1beta1.Server{}, f.defaultInformer)
+}
+
+func (f *serverInformer) Lister() v1beta1.ServerLister {
+	return v1beta1.NewServerLister(f.Informer().GetIndexer())
+}
diff --git a/controller/gen/client/informers/externalversions/serverauthorization/interface.go b/controller/gen/client/informers/externalversions/serverauthorization/interface.go
new file mode 100644
index 000000000..8210c66dd
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/serverauthorization/interface.go
@@ -0,0 +1,46 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package serverauthorization
+
+import (
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serverauthorization/v1beta1"
+)
+
+// Interface provides access to each of this group's versions.
+type Interface interface {
+	// V1beta1 provides access to shared informers for resources in V1beta1.
+	V1beta1() v1beta1.Interface
+}
+
+type group struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// V1beta1 returns a new v1beta1.Interface.
+func (g *group) V1beta1() v1beta1.Interface {
+	return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/controller/gen/client/informers/externalversions/serverauthorization/v1beta1/interface.go b/controller/gen/client/informers/externalversions/serverauthorization/v1beta1/interface.go
new file mode 100644
index 000000000..ca166b0ac
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/serverauthorization/v1beta1/interface.go
@@ -0,0 +1,45 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// ServerAuthorizations returns a ServerAuthorizationInformer.
+	ServerAuthorizations() ServerAuthorizationInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// ServerAuthorizations returns a ServerAuthorizationInformer.
+func (v *version) ServerAuthorizations() ServerAuthorizationInformer {
+	return &serverAuthorizationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/controller/gen/client/informers/externalversions/serverauthorization/v1beta1/serverauthorization.go b/controller/gen/client/informers/externalversions/serverauthorization/v1beta1/serverauthorization.go
new file mode 100644
index 000000000..2d9833511
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/serverauthorization/v1beta1/serverauthorization.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	time "time"
+
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
+	versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/listers/serverauthorization/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// ServerAuthorizationInformer provides access to a shared informer and lister for
+// ServerAuthorizations.
+type ServerAuthorizationInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta1.ServerAuthorizationLister
+}
+
+type serverAuthorizationInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewServerAuthorizationInformer constructs a new informer for ServerAuthorization type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewServerAuthorizationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredServerAuthorizationInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredServerAuthorizationInformer constructs a new informer for ServerAuthorization type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredServerAuthorizationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ServerauthorizationV1beta1().ServerAuthorizations(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ServerauthorizationV1beta1().ServerAuthorizations(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&serverauthorizationv1beta1.ServerAuthorization{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *serverAuthorizationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredServerAuthorizationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *serverAuthorizationInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&serverauthorizationv1beta1.ServerAuthorization{}, f.defaultInformer)
+}
+
+func (f *serverAuthorizationInformer) Lister() v1beta1.ServerAuthorizationLister {
+	return v1beta1.NewServerAuthorizationLister(f.Informer().GetIndexer())
+}
diff --git a/controller/gen/client/listers/server/v1beta1/expansion_generated.go b/controller/gen/client/listers/server/v1beta1/expansion_generated.go
new file mode 100644
index 000000000..195815706
--- /dev/null
+++ b/controller/gen/client/listers/server/v1beta1/expansion_generated.go
@@ -0,0 +1,27 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+// ServerListerExpansion allows custom methods to be added to
+// ServerLister.
+type ServerListerExpansion interface{}
+
+// ServerNamespaceListerExpansion allows custom methods to be added to
+// ServerNamespaceLister.
+type ServerNamespaceListerExpansion interface{}
diff --git a/controller/gen/client/listers/server/v1beta1/server.go b/controller/gen/client/listers/server/v1beta1/server.go
new file mode 100644
index 000000000..c2486f96b
--- /dev/null
+++ b/controller/gen/client/listers/server/v1beta1/server.go
@@ -0,0 +1,99 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// ServerLister helps list Servers.
+// All objects returned here must be treated as read-only.
+type ServerLister interface {
+	// List lists all Servers in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1beta1.Server, err error)
+	// Servers returns an object that can list and get Servers.
+	Servers(namespace string) ServerNamespaceLister
+	ServerListerExpansion
+}
+
+// serverLister implements the ServerLister interface.
+type serverLister struct {
+	indexer cache.Indexer
+}
+
+// NewServerLister returns a new ServerLister.
+func NewServerLister(indexer cache.Indexer) ServerLister {
+	return &serverLister{indexer: indexer}
+}
+
+// List lists all Servers in the indexer.
+func (s *serverLister) List(selector labels.Selector) (ret []*v1beta1.Server, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.Server))
+	})
+	return ret, err
+}
+
+// Servers returns an object that can list and get Servers.
+func (s *serverLister) Servers(namespace string) ServerNamespaceLister {
+	return serverNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// ServerNamespaceLister helps list and get Servers.
+// All objects returned here must be treated as read-only.
+type ServerNamespaceLister interface {
+	// List lists all Servers in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1beta1.Server, err error)
+	// Get retrieves the Server from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1beta1.Server, error)
+	ServerNamespaceListerExpansion
+}
+
+// serverNamespaceLister implements the ServerNamespaceLister
+// interface.
+type serverNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all Servers in the indexer for a given namespace.
+func (s serverNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.Server, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.Server))
+	})
+	return ret, err
+}
+
+// Get retrieves the Server from the indexer for a given namespace and name.
+func (s serverNamespaceLister) Get(name string) (*v1beta1.Server, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta1.Resource("server"), name)
+	}
+	return obj.(*v1beta1.Server), nil
+}
diff --git a/controller/gen/client/listers/serverauthorization/v1beta1/expansion_generated.go b/controller/gen/client/listers/serverauthorization/v1beta1/expansion_generated.go
new file mode 100644
index 000000000..866915eae
--- /dev/null
+++ b/controller/gen/client/listers/serverauthorization/v1beta1/expansion_generated.go
@@ -0,0 +1,27 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+// ServerAuthorizationListerExpansion allows custom methods to be added to
+// ServerAuthorizationLister.
+type ServerAuthorizationListerExpansion interface{}
+
+// ServerAuthorizationNamespaceListerExpansion allows custom methods to be added to
+// ServerAuthorizationNamespaceLister.
+type ServerAuthorizationNamespaceListerExpansion interface{}
diff --git a/controller/gen/client/listers/serverauthorization/v1beta1/serverauthorization.go b/controller/gen/client/listers/serverauthorization/v1beta1/serverauthorization.go
new file mode 100644
index 000000000..8f35be7e8
--- /dev/null
+++ b/controller/gen/client/listers/serverauthorization/v1beta1/serverauthorization.go
@@ -0,0 +1,99 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// ServerAuthorizationLister helps list ServerAuthorizations.
+// All objects returned here must be treated as read-only.
+type ServerAuthorizationLister interface {
+	// List lists all ServerAuthorizations in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1beta1.ServerAuthorization, err error)
+	// ServerAuthorizations returns an object that can list and get ServerAuthorizations.
+	ServerAuthorizations(namespace string) ServerAuthorizationNamespaceLister
+	ServerAuthorizationListerExpansion
+}
+
+// serverAuthorizationLister implements the ServerAuthorizationLister interface.
+type serverAuthorizationLister struct {
+	indexer cache.Indexer
+}
+
+// NewServerAuthorizationLister returns a new ServerAuthorizationLister.
+func NewServerAuthorizationLister(indexer cache.Indexer) ServerAuthorizationLister {
+	return &serverAuthorizationLister{indexer: indexer}
+}
+
+// List lists all ServerAuthorizations in the indexer.
+func (s *serverAuthorizationLister) List(selector labels.Selector) (ret []*v1beta1.ServerAuthorization, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.ServerAuthorization))
+	})
+	return ret, err
+}
+
+// ServerAuthorizations returns an object that can list and get ServerAuthorizations.
+func (s *serverAuthorizationLister) ServerAuthorizations(namespace string) ServerAuthorizationNamespaceLister {
+	return serverAuthorizationNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// ServerAuthorizationNamespaceLister helps list and get ServerAuthorizations.
+// All objects returned here must be treated as read-only.
+type ServerAuthorizationNamespaceLister interface {
+	// List lists all ServerAuthorizations in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1beta1.ServerAuthorization, err error)
+	// Get retrieves the ServerAuthorization from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1beta1.ServerAuthorization, error)
+	ServerAuthorizationNamespaceListerExpansion
+}
+
+// serverAuthorizationNamespaceLister implements the ServerAuthorizationNamespaceLister
+// interface.
+type serverAuthorizationNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all ServerAuthorizations in the indexer for a given namespace.
+func (s serverAuthorizationNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.ServerAuthorization, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.ServerAuthorization))
+	})
+	return ret, err
+}
+
+// Get retrieves the ServerAuthorization from the indexer for a given namespace and name.
+func (s serverAuthorizationNamespaceLister) Get(name string) (*v1beta1.ServerAuthorization, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta1.Resource("serverauthorization"), name)
+	}
+	return obj.(*v1beta1.ServerAuthorization), nil
+}
diff --git a/controller/heartbeat/heartbeat.go b/controller/heartbeat/heartbeat.go
index 9c2b4092b..14f7307a4 100644
--- a/controller/heartbeat/heartbeat.go
+++ b/controller/heartbeat/heartbeat.go
@@ -60,13 +60,13 @@ func K8sValues(ctx context.Context, kubeAPI *k8s.KubernetesAPI, controlPlaneName
 		return v
 	}
 
-	spClient, err := pkgK8s.NewSpClientSet(kubeAPI.Config)
+	l5dCrdClient, err := pkgK8s.NewL5DCRDClient(kubeAPI.Config)
 	if err != nil {
-		log.Errorf("Failed to create service profile client: %s", err)
+		log.Errorf("Failed to create Linkerd CRD client: %s", err)
 		return v
 	}
 
-	spList, err := spClient.LinkerdV1alpha2().ServiceProfiles("").List(ctx, v1.ListOptions{})
+	spList, err := l5dCrdClient.LinkerdV1alpha2().ServiceProfiles("").List(ctx, v1.ListOptions{})
 	if err != nil {
 		log.Errorf("Failed to get service profiles: %s", err)
 		return v
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index 82cf4556a..64d6b8596 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -10,8 +10,9 @@ import (
 	"k8s.io/client-go/rest"
 
 	spv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
-	spclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
-	sp "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions"
+	l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	l5dcrdinformer "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions"
+	srvinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta1"
 	spinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
@@ -65,6 +66,8 @@ const (
 	Node
 	Secret
 	ES // EndpointSlice resource
+	Srv
+	Saz
 )
 
 // API provides shared informers for all Kubernetes objects
@@ -90,11 +93,12 @@ type API struct {
 	ts       tsinformers.TrafficSplitInformer
 	node     coreinformers.NodeInformer
 	secret   coreinformers.SecretInformer
+	srv      srvinformers.ServerInformer
 
-	syncChecks        []cache.InformerSynced
-	sharedInformers   informers.SharedInformerFactory
-	spSharedInformers sp.SharedInformerFactory
-	tsSharedInformers ts.SharedInformerFactory
+	syncChecks            []cache.InformerSynced
+	sharedInformers       informers.SharedInformerFactory
+	l5dCrdSharedInformers l5dcrdinformer.SharedInformerFactory
+	tsSharedInformers     ts.SharedInformerFactory
 
 	gauges []prometheus.GaugeFunc
 }
@@ -140,22 +144,28 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 		}
 	}
 
-	// check for need and access to ServiceProfiles
-	var spClient *spclient.Clientset
+	// check for need and access to Linkerd CRD clients
+	var l5dCrdClient *l5dcrdclient.Clientset
 	for _, res := range resources {
-		if res == SP {
+		switch {
+		case res == SP:
 			err := k8s.ServiceProfilesAccess(ctx, k8sClient)
 			if err != nil {
 				return nil, err
 			}
-
-			spClient, err = NewSpClientSet(kubeConfig)
+		case res == Srv:
+			err := k8s.ServersAccess(ctx, k8sClient)
 			if err != nil {
 				return nil, err
 			}
-
-			break
+		default:
+			continue
 		}
+		l5dCrdClient, err = NewL5DCRDClient(kubeConfig)
+		if err != nil {
+			return nil, err
+		}
+		break
 	}
 
 	// TrafficSplits
@@ -171,7 +181,7 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 		}
 	}
 
-	api := NewAPI(k8sClient, dynamicClient, spClient, tsClient, resources...)
+	api := NewAPI(k8sClient, dynamicClient, l5dCrdClient, tsClient, resources...)
 	for _, gauge := range api.gauges {
 		prometheus.Register(gauge)
 	}
@@ -182,15 +192,15 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 func NewAPI(
 	k8sClient kubernetes.Interface,
 	dynamicClient dynamic.Interface,
-	spClient spclient.Interface,
+	l5dCrdClient l5dcrdclient.Interface,
 	tsClient tsclient.Interface,
 	resources ...APIResource,
 ) *API {
 	sharedInformers := informers.NewSharedInformerFactory(k8sClient, 10*time.Minute)
 
-	var spSharedInformers sp.SharedInformerFactory
-	if spClient != nil {
-		spSharedInformers = sp.NewSharedInformerFactory(spClient, 10*time.Minute)
+	var l5dCrdSharedInformers l5dcrdinformer.SharedInformerFactory
+	if l5dCrdClient != nil {
+		l5dCrdSharedInformers = l5dcrdinformer.NewSharedInformerFactory(l5dCrdClient, 10*time.Minute)
 	}
 
 	var tsSharedInformers ts.SharedInformerFactory
@@ -199,12 +209,12 @@ func NewAPI(
 	}
 
 	api := &API{
-		Client:            k8sClient,
-		DynamicClient:     dynamicClient,
-		syncChecks:        make([]cache.InformerSynced, 0),
-		sharedInformers:   sharedInformers,
-		spSharedInformers: spSharedInformers,
-		tsSharedInformers: tsSharedInformers,
+		Client:                k8sClient,
+		DynamicClient:         dynamicClient,
+		syncChecks:            make([]cache.InformerSynced, 0),
+		sharedInformers:       sharedInformers,
+		l5dCrdSharedInformers: l5dCrdSharedInformers,
+		tsSharedInformers:     tsSharedInformers,
 	}
 
 	for _, resource := range resources {
@@ -258,12 +268,19 @@ func NewAPI(
 			api.syncChecks = append(api.syncChecks, api.rs.Informer().HasSynced)
 			api.addInformerSizeGauge("replica_set", api.rs.Informer())
 		case SP:
-			if spSharedInformers == nil {
-				panic("SP shared informer not configured")
+			if l5dCrdSharedInformers == nil {
+				panic("Linkerd CRD shared informer not configured")
 			}
-			api.sp = spSharedInformers.Linkerd().V1alpha2().ServiceProfiles()
+			api.sp = l5dCrdSharedInformers.Linkerd().V1alpha2().ServiceProfiles()
 			api.syncChecks = append(api.syncChecks, api.sp.Informer().HasSynced)
 			api.addInformerSizeGauge("service_profile", api.sp.Informer())
+		case Srv:
+			if l5dCrdSharedInformers == nil {
+				panic("Linkerd CRD shared informer not configured")
+			}
+			api.srv = l5dCrdSharedInformers.Server().V1beta1().Servers()
+			api.syncChecks = append(api.syncChecks, api.srv.Informer().HasSynced)
+			api.addInformerSizeGauge("server", api.srv.Informer())
 		case SS:
 			api.ss = sharedInformers.Apps().V1().StatefulSets()
 			api.syncChecks = append(api.syncChecks, api.ss.Informer().HasSynced)
@@ -296,8 +313,8 @@ func NewAPI(
 func (api *API) Sync(stopCh <-chan struct{}) {
 	api.sharedInformers.Start(stopCh)
 
-	if api.spSharedInformers != nil {
-		api.spSharedInformers.Start(stopCh)
+	if api.l5dCrdSharedInformers != nil {
+		api.l5dCrdSharedInformers.Start(stopCh)
 	}
 
 	if api.tsSharedInformers != nil {
@@ -411,6 +428,14 @@ func (api *API) SP() spinformers.ServiceProfileInformer {
 	return api.sp
 }
 
+// Srv provides access to a shared informer and lister for Servers.
+func (api *API) Srv() srvinformers.ServerInformer {
+	if api.srv == nil {
+		panic("Srv informer not configured")
+	}
+	return api.srv
+}
+
 // MWC provides access to a shared informer and lister for MutatingWebhookConfigurations.
 func (api *API) MWC() arinformers.MutatingWebhookConfigurationInformer {
 	if api.mwc == nil {
diff --git a/controller/k8s/clientset.go b/controller/k8s/clientset.go
index 4a1afc680..01b4f826c 100644
--- a/controller/k8s/clientset.go
+++ b/controller/k8s/clientset.go
@@ -1,7 +1,7 @@
 package k8s
 
 import (
-	spclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
 	"github.com/linkerd/linkerd2/pkg/prometheus"
 	tsclient "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned"
 	"k8s.io/client-go/rest"
@@ -16,10 +16,10 @@ func wrapTransport(config *rest.Config, telemetryName string) *rest.Config {
 	return config
 }
 
-// NewSpClientSet returns a Kubernetes ServiceProfile client for the given
+// NewL5DCRDClient returns a Linkerd controller client for the given
 // configuration.
-func NewSpClientSet(kubeConfig *rest.Config) (*spclient.Clientset, error) {
-	return spclient.NewForConfig(wrapTransport(kubeConfig, "sp"))
+func NewL5DCRDClient(kubeConfig *rest.Config) (*l5dcrdclient.Clientset, error) {
+	return l5dcrdclient.NewForConfig(wrapTransport(kubeConfig, "l5dCrd"))
 }
 
 // NewTsClientSet returns a Kubernetes TrafficSplit client for the given
diff --git a/controller/script/destination-client/main.go b/controller/script/destination-client/main.go
index 52aab54fc..d95270f06 100644
--- a/controller/script/destination-client/main.go
+++ b/controller/script/destination-client/main.go
@@ -17,6 +17,7 @@ func main() {
 	addr := flag.String("addr", ":8086", "address of destination service")
 	path := flag.String("path", "strest-server.default.svc.cluster.local:8888", "destination path")
 	method := flag.String("method", "get", "which gRPC method to invoke")
+	token := flag.String("token", "", "context token")
 	flag.Parse()
 
 	client, conn, err := destination.NewClient(*addr)
@@ -26,8 +27,9 @@ func main() {
 	defer conn.Close()
 
 	req := &pb.GetDestination{
-		Scheme: "k8s",
-		Path:   *path,
+		Scheme:       "k8s",
+		Path:         *path,
+		ContextToken: *token,
 	}
 
 	switch *method {
@@ -69,7 +71,11 @@ func get(client pb.DestinationClient, req *pb.GetDestination) {
 					log.Printf("  - protocol hint: UNKNOWN")
 				}
 				log.Printf("  - identity: %s", addr.GetTlsIdentity())
+				if ot := addr.GetProtocolHint().GetOpaqueTransport(); ot != nil {
+					log.Printf("  - opaque transport port: %d", ot.GetInboundPort())
+				}
 			}
+
 			log.Println()
 		case *pb.Update_Remove:
 			log.Println("Remove:")
diff --git a/controller/script/policy-client/main.go b/controller/script/policy-client/main.go
new file mode 100644
index 000000000..6b0050802
--- /dev/null
+++ b/controller/script/policy-client/main.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"os"
+
+	"github.com/ghodss/yaml"
+
+	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func main() {
+	namespace := flag.String("namespace", "", "namespace of resource to get")
+	flag.Parse()
+
+	config, err := k8s.GetConfig("", "")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error configuring Kubernetes API client: %v", err)
+		os.Exit(1)
+	}
+	client := versioned.NewForConfigOrDie(config)
+
+	srvWatch, err := client.ServerV1beta1().Servers(*namespace).Watch(context.Background(), metav1.ListOptions{})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to watch Servers: %s", err)
+		os.Exit(1)
+	}
+	sazWatch, err := client.ServerauthorizationV1beta1().ServerAuthorizations(*namespace).Watch(context.Background(), metav1.ListOptions{})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to watch ServerAuthorizations: %s", err)
+		os.Exit(1)
+	}
+	srvUpdates := srvWatch.ResultChan()
+	sazUpdates := sazWatch.ResultChan()
+
+	for {
+		var update interface{}
+		select {
+		case u := <-srvUpdates:
+			update = u
+		case u := <-sazUpdates:
+			update = u
+		}
+		b, _ := yaml.Marshal(update)
+		fmt.Println(string(b))
+	}
+}
diff --git a/controller/webhook/server.go b/controller/webhook/server.go
index 7e0a7f612..40c346879 100644
--- a/controller/webhook/server.go
+++ b/controller/webhook/server.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/tls"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"sync/atomic"
 
@@ -59,8 +60,10 @@ func NewServer(
 	}()
 
 	server := &http.Server{
-		Addr:      addr,
-		TLSConfig: &tls.Config{},
+		Addr: addr,
+		TLSConfig: &tls.Config{
+			MinVersion: tls.VersionTLS12,
+		},
 	}
 
 	eventBroadcaster := record.NewBroadcaster()
@@ -133,7 +136,12 @@ func (s *Server) serve(res http.ResponseWriter, req *http.Request) {
 		return
 	}
 
-	response := s.processReq(req.Context(), data)
+	response, err := s.processReq(req.Context(), data)
+	if err != nil {
+		http.Error(res, err.Error(), http.StatusBadRequest)
+		return
+	}
+
 	responseJSON, err := json.Marshal(response)
 	if err != nil {
 		http.Error(res, err.Error(), http.StatusInternalServerError)
@@ -146,20 +154,15 @@ func (s *Server) serve(res http.ResponseWriter, req *http.Request) {
 	}
 }
 
-func (s *Server) processReq(ctx context.Context, data []byte) *admissionv1beta1.AdmissionReview {
+func (s *Server) processReq(ctx context.Context, data []byte) (*admissionv1beta1.AdmissionReview, error) {
 	admissionReview, err := decode(data)
 	if err != nil {
-		log.Errorf("failed to decode data. Reason: %s", err)
-		admissionReview.Response = &admissionv1beta1.AdmissionResponse{
-			UID:     admissionReview.Request.UID,
-			Allowed: false,
-			Result: &metav1.Status{
-				Message: err.Error(),
-			},
-		}
-		return admissionReview
+		return nil, fmt.Errorf("failed to decode admission review request: %w", err)
+	}
+	if admissionReview.Request == nil || admissionReview.Request.UID == "" {
+		return nil, fmt.Errorf("invalid admission review request")
 	}
-	log.Infof("received admission review request %s", admissionReview.Request.UID)
+	log.Infof("received admission review request %q", admissionReview.Request.UID)
 	log.Debugf("admission request: %+v", admissionReview.Request)
 
 	admissionResponse, err := s.handler(ctx, s.api, admissionReview.Request, s.recorder)
@@ -172,11 +175,11 @@ func (s *Server) processReq(ctx context.Context, data []byte) *admissionv1beta1.
 				Message: err.Error(),
 			},
 		}
-		return admissionReview
+		return admissionReview, nil
 	}
 	admissionReview.Response = admissionResponse
 
-	return admissionReview
+	return admissionReview, nil
 }
 
 // Shutdown initiates a graceful shutdown of the underlying HTTP server.
diff --git a/pkg/k8s/authz.go b/pkg/k8s/authz.go
index c01150edb..af441d19f 100644
--- a/pkg/k8s/authz.go
+++ b/pkg/k8s/authz.go
@@ -110,6 +110,24 @@ func ServiceProfilesAccess(ctx context.Context, k8sClient kubernetes.Interface)
 	return errors.New("ServiceProfile CRD not found")
 }
 
+// ServersAccess checks whether the Server CRD is installed on the cluster
+// and the client is authorized to access Servers.
+func ServersAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
+	groupVersion := fmt.Sprintf("%s/%s", PolicyAPIGroup, PolicyAPIVersion)
+	res, err := k8sClient.Discovery().ServerResourcesForGroupVersion(groupVersion)
+	if err != nil {
+		return err
+	}
+	if res.GroupVersion == groupVersion {
+		for _, apiRes := range res.APIResources {
+			if apiRes.Kind == Server {
+				return ResourceAuthz(ctx, k8sClient, "", "list", PolicyAPIGroup, "", "servers", "")
+			}
+		}
+	}
+	return errors.New("Server CRD not found")
+}
+
 // EndpointSliceAccess verifies whether the K8s cluster has
 // access to EndpointSlice resources.
 func EndpointSliceAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
diff --git a/pkg/k8s/fake.go b/pkg/k8s/fake.go
index 359027d59..cb1254b99 100644
--- a/pkg/k8s/fake.go
+++ b/pkg/k8s/fake.go
@@ -93,6 +93,8 @@ func NewFakeClientSets(configs ...string) (
 			spObjs = append(spObjs, obj)
 		case TrafficSplit:
 			tsObjs = append(tsObjs, obj)
+		case Server:
+			spObjs = append(spObjs, obj)
 		default:
 			objs = append(objs, obj)
 		}
diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go
index ea8f3ce11..ede1f436a 100644
--- a/pkg/k8s/k8s.go
+++ b/pkg/k8s/k8s.go
@@ -28,6 +28,9 @@ const (
 	Server                = "server"
 	ServerAuthorization   = "serverauthorization"
 
+	PolicyAPIGroup   = "policy.linkerd.io"
+	PolicyAPIVersion = "v1beta1"
+
 	ServiceProfileAPIVersion = "linkerd.io/v1alpha2"
 	ServiceProfileKind       = "ServiceProfile"
 
diff --git a/pkg/k8s/policy.go b/pkg/k8s/policy.go
index 810986dec..06c6a7a71 100644
--- a/pkg/k8s/policy.go
+++ b/pkg/k8s/policy.go
@@ -6,10 +6,11 @@ import (
 	"os"
 	"strings"
 
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/kubernetes"
 )
 
@@ -22,18 +23,10 @@ type ServerAndAuthorization struct {
 type id struct{ name, namespace string }
 
 // SazGVR is the GroupVersionResource for the ServerAuthorization resource.
-var SazGVR = schema.GroupVersionResource{
-	Group:    "policy.linkerd.io",
-	Version:  "v1alpha1",
-	Resource: "serverauthorizations",
-}
+var SazGVR = serverauthorizationv1beta1.SchemeGroupVersion.WithResource("serverauthorizations")
 
 // ServerGVR is the GroupVersionResource for the Server resource.
-var ServerGVR = schema.GroupVersionResource{
-	Group:    "policy.linkerd.io",
-	Version:  "v1alpha1",
-	Resource: "servers",
-}
+var ServerGVR = serverv1beta1.SchemeGroupVersion.WithResource("servers")
 
 // ServerAuthorizationsForResource returns a list of Server-ServerAuthorization
 // pairs which select pods belonging to the given resource.
diff --git a/viz/tap/api/sever.go b/viz/tap/api/sever.go
index d4bd092fa..c94ba7708 100644
--- a/viz/tap/api/sever.go
+++ b/viz/tap/api/sever.go
@@ -72,6 +72,7 @@ func NewServer(
 		TLSConfig: &tls.Config{
 			ClientAuth: tls.VerifyClientCertIfGiven,
 			ClientCAs:  clientCertPool,
+			MinVersion: tls.VersionTLS12,
 		},
 	}
 


Test patch:
diff --git a/controller/api/destination/endpoint_translator_test.go b/controller/api/destination/endpoint_translator_test.go
index 05ee17db7..5c5426130 100644
--- a/controller/api/destination/endpoint_translator_test.go
+++ b/controller/api/destination/endpoint_translator_test.go
@@ -168,6 +168,20 @@ func TestEndpointTranslatorForRemoteGateways(t *testing.T) {
 		}
 	})
 
+	t.Run("Recovers after emptying address et", func(t *testing.T) {
+		mockGetServer, translator := makeEndpointTranslator(t)
+
+		translator.Add(mkAddressSetForServices(remoteGatewayWithNoTLS))
+		translator.Remove(mkAddressSetForServices(remoteGatewayWithNoTLS))
+		translator.Add(mkAddressSetForServices(remoteGatewayWithNoTLS))
+
+		expectedNumUpdates := 3
+		actualNumUpdates := len(mockGetServer.updatesReceived)
+		if actualNumUpdates != expectedNumUpdates {
+			t.Fatalf("Expecting [%d] updates, got [%d]. Updates: %v", expectedNumUpdates, actualNumUpdates, mockGetServer.updatesReceived)
+		}
+	})
+
 	t.Run("Sends TlsIdentity when enabled", func(t *testing.T) {
 		expectedTLSIdentity := &pb.TlsIdentity_DnsLikeIdentity{
 			Name: "some-identity",
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index 5a153d5d1..7c6ad5f69 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -25,6 +25,7 @@ const podIP1 = "172.17.0.12"
 const podIP2 = "172.17.0.13"
 const podIPOpaque = "172.17.0.14"
 const podIPSkipped = "172.17.0.15"
+const podIPPolicy = "172.17.0.16"
 const podIPStatefulSet = "172.17.13.15"
 const port uint32 = 8989
 const opaquePort uint32 = 4242
@@ -313,12 +314,52 @@ status:
   podIP: 172.17.13.15`,
 	}
 
+	policyResources := []string{
+		`
+apiVersion: v1
+kind: Pod
+metadata:
+  labels:
+    linkerd.io/control-plane-ns: linkerd
+    app: policy-test
+  name: pod-policyResources
+  namespace: ns
+status:
+  phase: Running
+  podIP: 172.17.0.16
+spec:
+  containers:
+    - name: linkerd-proxy
+      env:
+      - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
+        value: 0.0.0.0:4143
+    - name: app
+      image: nginx
+      ports:
+      - containerPort: 80
+        name: http
+        protocol: TCP`,
+		`
+apiVersion: policy.linkerd.io/v1beta1
+kind: Server
+metadata:
+  name: srv
+  namespace: ns
+spec:
+  podSelector:
+    matchLabels:
+      app: policy-test
+  port: 80
+  proxyProtocol: opaque`,
+	}
+
 	res := append(meshedPodResources, clientSP...)
 	res = append(res, unmeshedPod)
 	res = append(res, meshedOpaquePodResources...)
 	res = append(res, meshedOpaqueServiceResources...)
 	res = append(res, meshedSkippedPodResource...)
 	res = append(res, meshedStatefulSetPodResource...)
+	res = append(res, policyResources...)
 	k8sAPI, err := k8s.NewFakeAPI(res...)
 	if err != nil {
 		t.Fatalf("NewFakeAPI returned an error: %s", err)
@@ -342,6 +383,7 @@ status:
 	opaquePorts := watcher.NewOpaquePortsWatcher(k8sAPI, log, defaultOpaquePorts)
 	profiles := watcher.NewProfileWatcher(k8sAPI, log)
 	trafficSplits := watcher.NewTrafficSplitWatcher(k8sAPI, log)
+	servers := watcher.NewServerWatcher(k8sAPI, log)
 
 	// Sync after creating watchers so that the the indexers added get updated
 	// properly
@@ -352,6 +394,7 @@ status:
 		endpoints,
 		opaquePorts,
 		profiles,
+		servers,
 		trafficSplits,
 		k8sAPI.Node(),
 		true,
@@ -917,7 +960,7 @@ func TestGetProfiles(t *testing.T) {
 		if first.Endpoint.ProtocolHint == nil {
 			t.Fatalf("Expected protocol hint but found none")
 		}
-		if first.Endpoint.ProtocolHint.GetOpaqueTransport().InboundPort != 4143 {
+		if first.Endpoint.ProtocolHint.GetOpaqueTransport().GetInboundPort() != 4143 {
 			t.Fatalf("Expected pod to support opaque traffic on port 4143")
 		}
 		if first.Endpoint.Addr.String() != epAddr.String() {
@@ -988,6 +1031,46 @@ func TestGetProfiles(t *testing.T) {
 			t.Fatalf("Expected TLS identity for %s to be nil but got %+v", path, addr.TlsIdentity)
 		}
 	})
+
+	t.Run("Return profile with opaque protocol when using Pod IP selected by a Server", func(t *testing.T) {
+		server := makeServer(t)
+		stream := &bufferingGetProfileStream{
+			updates:          []*pb.DestinationProfile{},
+			MockServerStream: util.NewMockServerStream(),
+		}
+		stream.Cancel()
+
+		_, err := toAddress(podIPPolicy, 80)
+		if err != nil {
+			t.Fatalf("Got error: %s", err)
+		}
+		err = server.GetProfile(&pb.GetDestination{
+			Scheme: "k8s",
+			Path:   fmt.Sprintf("%s:%d", podIPPolicy, 80),
+		}, stream)
+		if err != nil {
+			t.Fatalf("Got error: %s", err)
+		}
+
+		// Test that the first update has a destination profile with an
+		// opaque protocol and opaque transport.
+		if len(stream.updates) == 0 {
+			t.Fatalf("Expected at least 1 update but got 0")
+		}
+		update := stream.updates[0]
+		if update.Endpoint == nil {
+			t.Fatalf("Expected response to have endpoint field")
+		}
+		if !update.OpaqueProtocol {
+			t.Fatalf("Expected port %d to be an opaque protocol, but it was not", 80)
+		}
+		if update.Endpoint.ProtocolHint == nil {
+			t.Fatalf("Expected protocol hint but found none")
+		}
+		if update.Endpoint.ProtocolHint.GetOpaqueTransport().GetInboundPort() != 4143 {
+			t.Fatalf("Expected pod to support opaque traffic on port 4143")
+		}
+	})
 }
 
 func TestTokenStructure(t *testing.T) {
diff --git a/controller/api/destination/test_helper.go b/controller/api/destination/test_helper.go
index 5dbd91e50..3d86713be 100644
--- a/controller/api/destination/test_helper.go
+++ b/controller/api/destination/test_helper.go
@@ -25,7 +25,7 @@ func (c *MockAPIClient) Get(ctx context.Context, in *destinationPb.GetDestinatio
 // GetProfile provides a mock of a destination API method
 func (c *MockAPIClient) GetProfile(ctx context.Context, _ *destinationPb.GetDestination, _ ...grpc.CallOption) (destinationPb.Destination_GetProfileClient, error) {
 	// Not implemented through this client. The proxies use the gRPC server directly instead.
-	return nil, errors.New("Not implemented")
+	return nil, errors.New("not implemented")
 }
 
 // MockDestinationGetClient satisfies the Destination_GetClient gRPC interface.
diff --git a/controller/api/destination/watcher/endpoints_watcher_test.go b/controller/api/destination/watcher/endpoints_watcher_test.go
index 992bef815..4dc7453f5 100644
--- a/controller/api/destination/watcher/endpoints_watcher_test.go
+++ b/controller/api/destination/watcher/endpoints_watcher_test.go
@@ -1544,6 +1544,7 @@ func TestEndpointsWatcherServiceMirrors(t *testing.T) {
 		expectedAddresses                []string
 		expectedNoEndpoints              bool
 		expectedNoEndpointsServiceExists bool
+		enableEndpointSlices             bool
 	}{
 		{
 			k8sConfigs: []string{`
@@ -1586,6 +1587,45 @@ subsets:
 			k8sConfigs: []string{`
 apiVersion: v1
 kind: Service
+metadata:
+  name: name1-remote
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`,
+				`
+apiVersion: discovery.k8s.io/v1beta1
+kind: EndpointSlice
+metadata:
+  name: name1-remote-xxxx
+  namespace: ns
+  annotations:
+    mirror.linkerd.io/remote-gateway-identity: "gateway-identity-1"
+    mirror.linkerd.io/remote-svc-fq-name: "name1-remote-fq"
+  labels:
+    mirror.linkerd.io/mirrored-service: "true"
+    kubernetes.io/service-name: name1-remote
+endpoints:
+- addresses:
+  - 172.17.0.12
+ports:
+- port: 8989`,
+			},
+			serviceType: "mirrored service with identity and endpoint slices",
+			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
+			port:        8989,
+			expectedAddresses: []string{
+				"172.17.0.12:8989/gateway-identity-1/name1-remote-fq:8989",
+			},
+			expectedNoEndpoints:              false,
+			expectedNoEndpointsServiceExists: false,
+			enableEndpointSlices:             true,
+		},
+		{
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Service
 metadata:
   name: name1-remote
   namespace: ns
@@ -1701,7 +1741,7 @@ subsets:
 				t.Fatalf("NewFakeAPI returned an error: %s", err)
 			}
 
-			watcher := NewEndpointsWatcher(k8sAPI, logging.WithField("test", t.Name()), false)
+			watcher := NewEndpointsWatcher(k8sAPI, logging.WithField("test", t.Name()), tt.enableEndpointSlices)
 
 			k8sAPI.Sync(nil)
 
diff --git a/controller/api/destination/watcher/profile_watcher_test.go b/controller/api/destination/watcher/profile_watcher_test.go
index d40ce5431..fa6b6242f 100644
--- a/controller/api/destination/watcher/profile_watcher_test.go
+++ b/controller/api/destination/watcher/profile_watcher_test.go
@@ -94,6 +94,8 @@ func TestProfileWatcherUpdates(t *testing.T) {
 
 			actualProfiles := make([]*sp.ServiceProfileSpec, 0)
 
+			listener.mu.RLock()
+			defer listener.mu.RUnlock()
 			for _, profile := range listener.Profiles {
 				if profile == nil {
 					actualProfiles = append(actualProfiles, nil)
diff --git a/controller/api/destination/watcher/test_util.go b/controller/api/destination/watcher/test_util.go
index 5d3253a5e..31e178bcb 100644
--- a/controller/api/destination/watcher/test_util.go
+++ b/controller/api/destination/watcher/test_util.go
@@ -3,6 +3,7 @@ package watcher
 import (
 	"encoding/json"
 	"reflect"
+	"sync"
 	"testing"
 
 	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
@@ -32,6 +33,7 @@ func (dpl *DeletingProfileListener) Update(profile *sp.ServiceProfile) {
 // in a slice.  Useful for unit tests.
 type BufferingProfileListener struct {
 	Profiles []*sp.ServiceProfile
+	mu       sync.RWMutex
 }
 
 // NewBufferingProfileListener creates a new BufferingProfileListener.
@@ -43,6 +45,8 @@ func NewBufferingProfileListener() *BufferingProfileListener {
 
 // Update stores the update in the internal buffer.
 func (bpl *BufferingProfileListener) Update(profile *sp.ServiceProfile) {
+	bpl.mu.Lock()
+	defer bpl.mu.Unlock()
 	bpl.Profiles = append(bpl.Profiles, profile)
 }
 
diff --git a/controller/k8s/test_helper.go b/controller/k8s/test_helper.go
index ef8adc655..661028dfc 100644
--- a/controller/k8s/test_helper.go
+++ b/controller/k8s/test_helper.go
@@ -33,5 +33,6 @@ func NewFakeAPI(configs ...string) (*API, error) {
 		TS,
 		Node,
 		ES,
+		Srv,
 	), nil
 }
diff --git a/controller/webhook/server_test.go b/controller/webhook/server_test.go
index 1e8e08827..791ca2578 100644
--- a/controller/webhook/server_test.go
+++ b/controller/webhook/server_test.go
@@ -14,8 +14,10 @@ import (
 )
 
 var mockHTTPServer = &http.Server{
-	Addr:      ":0",
-	TLSConfig: &tls.Config{},
+	Addr: ":0",
+	TLSConfig: &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	},
 }
 
 func TestServe(t *testing.T) {