Repository: Azure/aad-pod-identity
Pull request #310 (2019-07-25 19:34:13)

Whitelisting
<!-- Thank you for helping AAD Pod Identity with a pull request! -->

**Reason for Change**:
<!-- What does this PR improve or fix in AAD Pod Identity? Why is it needed? -->
There are instances where pods deployed in a cluster need to request token directly instead of using aad-pod-identity. The following changes are done to support that use case - 
- Introduce new CRD - `AzurePodIdentityException`.
- The CRD `spec.PodLabels` field accepts a list of labels. If a pod needs to bypass aad-pod-identity while requesting a token, the label defined in the exception CRD should also be added to the pod. If a match is found, then `NMI` will proxy the request for token without and send the response back without any validation.
- `AzurePodIdentityException` CRD and label matching is done per namespace, which means if the same labels need to be used in multiple namespaces for bypassing pod-identity, a CRD needs to be defined in each namespace required.

**Issue Fixed**:
<!-- If this PR fixes GitHub issue 1234, add "Fixes #1234" to the next line. -->
Feature https://github.com/Azure/aad-pod-identity/issues/284

**Notes for Reviewers**:


aramase: ```bash
Ran 15 of 15 Specs in 2374.242 seconds
SUCCESS! -- 15 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestAADPodIdentity (2374.24s)
PASS
ok  	github.com/Azure/aad-pod-identity/test/e2e	2374.271s
```

Patch:
diff --git a/Makefile b/Makefile
index 018ded3b..8963db20 100644
--- a/Makefile
+++ b/Makefile
@@ -91,7 +91,7 @@ build: clean build-nmi build-mic build-demo build-identity-validator
 
 .PHONY: deepcopy-gen
 deepcopy-gen:
-	deepcopy-gen -i ./pkg/apis/aadpodidentity/v1/ -o ../../../ -O aadpodidentity_deepcopy_generated -p aadpodidentity
+	deepcopy-gen -i ./pkg/apis/aadpodidentity/v1/ -o . -O aadpodidentity_deepcopy_generated -p aadpodidentity
 
 .PHONY: image-nmi
 image-nmi:
diff --git a/deploy/infra/deployment-rbac.yaml b/deploy/infra/deployment-rbac.yaml
index aa1e6a40..756e3c4b 100644
--- a/deploy/infra/deployment-rbac.yaml
+++ b/deploy/infra/deployment-rbac.yaml
@@ -203,4 +203,4 @@ spec:
         hostPath:
           path: /etc/kubernetes/azure.json
       nodeSelector:
-        beta.kubernetes.io/os: linux
+        beta.kubernetes.io/os: linux
\ No newline at end of file
diff --git a/pkg/apis/aadpodidentity/v1/aadpodidentity_deepcopy_generated.go b/pkg/apis/aadpodidentity/v1/aadpodidentity_deepcopy_generated.go
index cd1d2616..75b94051 100644
--- a/pkg/apis/aadpodidentity/v1/aadpodidentity_deepcopy_generated.go
+++ b/pkg/apis/aadpodidentity/v1/aadpodidentity_deepcopy_generated.go
@@ -91,30 +91,18 @@ func (in *AzureAssignedIdentitySpec) DeepCopyInto(out *AzureAssignedIdentitySpec
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	if in.AzureIdentityRef != nil {
 		in, out := &in.AzureIdentityRef, &out.AzureIdentityRef
-		if *in == nil {
-			*out = nil
-		} else {
-			*out = new(AzureIdentity)
-			(*in).DeepCopyInto(*out)
-		}
+		*out = new(AzureIdentity)
+		(*in).DeepCopyInto(*out)
 	}
 	if in.AzureBindingRef != nil {
 		in, out := &in.AzureBindingRef, &out.AzureBindingRef
-		if *in == nil {
-			*out = nil
-		} else {
-			*out = new(AzureIdentityBinding)
-			(*in).DeepCopyInto(*out)
-		}
+		*out = new(AzureIdentityBinding)
+		(*in).DeepCopyInto(*out)
 	}
 	if in.Replicas != nil {
 		in, out := &in.Replicas, &out.Replicas
-		if *in == nil {
-			*out = nil
-		} else {
-			*out = new(int32)
-			**out = **in
-		}
+		*out = new(int32)
+		**out = **in
 	}
 	return
 }
@@ -309,12 +297,8 @@ func (in *AzureIdentitySpec) DeepCopyInto(out *AzureIdentitySpec) {
 	out.ClientPassword = in.ClientPassword
 	if in.Replicas != nil {
 		in, out := &in.Replicas, &out.Replicas
-		if *in == nil {
-			*out = nil
-		} else {
-			*out = new(int32)
-			**out = **in
-		}
+		*out = new(int32)
+		**out = **in
 	}
 	return
 }
@@ -345,3 +329,105 @@ func (in *AzureIdentityStatus) DeepCopy() *AzureIdentityStatus {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AzurePodIdentityException) DeepCopyInto(out *AzurePodIdentityException) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePodIdentityException.
+func (in *AzurePodIdentityException) DeepCopy() *AzurePodIdentityException {
+	if in == nil {
+		return nil
+	}
+	out := new(AzurePodIdentityException)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AzurePodIdentityException) 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 *AzurePodIdentityExceptionList) DeepCopyInto(out *AzurePodIdentityExceptionList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	out.ListMeta = in.ListMeta
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]AzurePodIdentityException, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePodIdentityExceptionList.
+func (in *AzurePodIdentityExceptionList) DeepCopy() *AzurePodIdentityExceptionList {
+	if in == nil {
+		return nil
+	}
+	out := new(AzurePodIdentityExceptionList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AzurePodIdentityExceptionList) 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 *AzurePodIdentityExceptionSpec) DeepCopyInto(out *AzurePodIdentityExceptionSpec) {
+	*out = *in
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	if in.PodLabels != nil {
+		in, out := &in.PodLabels, &out.PodLabels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePodIdentityExceptionSpec.
+func (in *AzurePodIdentityExceptionSpec) DeepCopy() *AzurePodIdentityExceptionSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(AzurePodIdentityExceptionSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AzurePodIdentityExceptionStatus) DeepCopyInto(out *AzurePodIdentityExceptionStatus) {
+	*out = *in
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePodIdentityExceptionStatus.
+func (in *AzurePodIdentityExceptionStatus) DeepCopy() *AzurePodIdentityExceptionStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(AzurePodIdentityExceptionStatus)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/apis/aadpodidentity/v1/types.go b/pkg/apis/aadpodidentity/v1/types.go
index 89a92adf..cee42b89 100644
--- a/pkg/apis/aadpodidentity/v1/types.go
+++ b/pkg/apis/aadpodidentity/v1/types.go
@@ -70,6 +70,18 @@ type AzureAssignedIdentity struct {
 	Status AzureAssignedIdentityStatus `json:"Status"`
 }
 
+//AzurePodIdentityException contains the pod selectors for all pods that don't require
+// NMI to process and request token on their behalf.
+
+//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type AzurePodIdentityException struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   AzurePodIdentityExceptionSpec   `json:"spec"`
+	Status AzurePodIdentityExceptionStatus `json:"Status"`
+}
+
 /*** Lists ***/
 //+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 type AzureIdentityList struct {
@@ -95,6 +107,14 @@ type AzureAssignedIdentityList struct {
 	Items []AzureAssignedIdentity `json:"items"`
 }
 
+//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type AzurePodIdentityExceptionList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []AzurePodIdentityException `json:"items"`
+}
+
 /*** AzureIdentity ***/
 //+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 type IdentityType int
@@ -150,9 +170,10 @@ const (
 )
 
 const (
-	AzureIDResource         = "azureidentities"
-	AzureIDBindingResource  = "azureidentitybindings"
-	AzureAssignedIDResource = "azureassignedidentities"
+	AzureIDResource                = "azureidentities"
+	AzureIDBindingResource         = "azureidentitybindings"
+	AzureAssignedIDResource        = "azureassignedidentities"
+	AzureIdentityExceptionResource = "azurepodidentityexceptions"
 )
 
 // AzureIdentityBindingSpec matches the pod with the Identity.
@@ -191,3 +212,17 @@ type AzureAssignedIdentityStatus struct {
 	Status            string `json:"status"`
 	AvailableReplicas int32  `json:"availableReplicas"`
 }
+
+// AzurePodIdentityExceptionSpec matches pods with the selector defined.
+// If request originates from a pod that matches the selector, nmi will
+// proxy the request and send response back without any validation.
+type AzurePodIdentityExceptionSpec struct {
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+	PodLabels         map[string]string `json:"podLabels"`
+}
+
+// AzurePodIdentityExceptionStatus ...
+type AzurePodIdentityExceptionStatus struct {
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+	Status            string `json:"status"`
+}
diff --git a/pkg/crd/crd.go b/pkg/crd/crd.go
index a0c9c4cd..19177a08 100644
--- a/pkg/crd/crd.go
+++ b/pkg/crd/crd.go
@@ -20,15 +20,18 @@ import (
 	"k8s.io/client-go/tools/cache"
 )
 
+// Client represents all the watchers and informers
 type Client struct {
-	rest                *rest.RESTClient
-	BindingListWatch    *cache.ListWatch
-	BindingInformer     cache.SharedInformer
-	IDListWatch         *cache.ListWatch
-	IDInformer          cache.SharedInformer
-	AssignedIDListWatch *cache.ListWatch
+	rest                          *rest.RESTClient
+	BindingListWatch              *cache.ListWatch
+	BindingInformer               cache.SharedInformer
+	IDListWatch                   *cache.ListWatch
+	IDInformer                    cache.SharedInformer
+	AssignedIDListWatch           *cache.ListWatch
+	PodIdentityExceptionListWatch *cache.ListWatch
 }
 
+// ClientInt ...
 type ClientInt interface {
 	Start(exit <-chan struct{})
 	SyncCache(exit <-chan struct{})
@@ -39,8 +42,10 @@ type ClientInt interface {
 	ListAssignedIDs() (res *[]aadpodid.AzureAssignedIdentity, err error)
 	ListIds() (res *[]aadpodid.AzureIdentity, err error)
 	ListPodIds(podns, podname string) (map[string][]aadpodid.AzureIdentity, error)
+	ListPodIdentityExceptions(ns string) (res *[]aadpodid.AzurePodIdentityException, err error)
 }
 
+// NewCRDClientLite ...
 func NewCRDClientLite(config *rest.Config) (crdClient *Client, err error) {
 	restClient, err := newRestClient(config)
 	if err != nil {
@@ -49,13 +54,16 @@ func NewCRDClientLite(config *rest.Config) (crdClient *Client, err error) {
 	}
 
 	assignedIDListWatch := newAssignedIDListWatch(restClient)
+	podIdentityExceptionListWatch := newPodIdentityExceptionListWatch(restClient)
 
 	return &Client{
-		AssignedIDListWatch: assignedIDListWatch,
-		rest:                restClient,
+		AssignedIDListWatch:           assignedIDListWatch,
+		PodIdentityExceptionListWatch: podIdentityExceptionListWatch,
+		rest:                          restClient,
 	}, nil
 }
 
+// NewCRDClient returns a new crd client and error if any
 func NewCRDClient(config *rest.Config, eventCh chan aadpodid.EventType) (crdClient *Client, err error) {
 	restClient, err := newRestClient(config)
 	if err != nil {
@@ -103,7 +111,10 @@ func newRestClient(config *rest.Config) (r *rest.RESTClient, err error) {
 		&aadpodid.AzureIdentityBinding{},
 		&aadpodid.AzureIdentityBindingList{},
 		&aadpodid.AzureAssignedIdentity{},
-		&aadpodid.AzureAssignedIdentityList{})
+		&aadpodid.AzureAssignedIdentityList{},
+		&aadpodid.AzurePodIdentityException{},
+		&aadpodid.AzurePodIdentityExceptionList{},
+	)
 	crdconfig.NegotiatedSerializer = serializer.DirectCodecFactory{
 		CodecFactory: serializer.NewCodecFactory(s)}
 
@@ -182,12 +193,24 @@ func newAssignedIDListWatch(r *rest.RESTClient) *cache.ListWatch {
 	return cache.NewListWatchFromClient(r, aadpodid.AzureAssignedIDResource, v1.NamespaceAll, fields.Everything())
 }
 
+func newPodIdentityExceptionListWatch(r *rest.RESTClient) *cache.ListWatch {
+	optionsModifier := func(options *v1.ListOptions) {}
+	return cache.NewFilteredListWatchFromClient(
+		r,
+		aadpodid.AzureIdentityExceptionResource,
+		v1.NamespaceAll,
+		optionsModifier,
+	)
+}
+
+// Start ...
 func (c *Client) Start(exit <-chan struct{}) {
 	go c.BindingInformer.Run(exit)
 	go c.IDInformer.Run(exit)
 	glog.Info("CRD watchers started")
 }
 
+// SyncCache synchronizes cache
 func (c *Client) SyncCache(exit <-chan struct{}) {
 	if !cache.WaitForCacheSync(exit) {
 		panic("Cache could not be synchronized")
@@ -223,6 +246,7 @@ func (c *Client) CreateAssignedIdentity(assignedIdentity *aadpodid.AzureAssigned
 	return nil
 }
 
+// ListBindings returns a list of azureidentitybindings
 func (c *Client) ListBindings() (res *[]aadpodid.AzureIdentityBinding, err error) {
 	begin := time.Now()
 
@@ -235,6 +259,7 @@ func (c *Client) ListBindings() (res *[]aadpodid.AzureIdentityBinding, err error
 	return &ret.(*aadpodid.AzureIdentityBindingList).Items, nil
 }
 
+// ListAssignedIDs returns a list of azureassignedidentities
 func (c *Client) ListAssignedIDs() (res *[]aadpodid.AzureAssignedIdentity, err error) {
 	begin := time.Now()
 	ret, err := c.AssignedIDListWatch.List(v1.ListOptions{})
@@ -246,6 +271,7 @@ func (c *Client) ListAssignedIDs() (res *[]aadpodid.AzureAssignedIdentity, err e
 	return &ret.(*aadpodid.AzureAssignedIdentityList).Items, nil
 }
 
+// ListIds returns a list of azureidentities
 func (c *Client) ListIds() (res *[]aadpodid.AzureIdentity, err error) {
 	begin := time.Now()
 	ret, err := c.IDListWatch.List(v1.ListOptions{})
@@ -257,6 +283,20 @@ func (c *Client) ListIds() (res *[]aadpodid.AzureIdentity, err error) {
 	return &ret.(*aadpodid.AzureIdentityList).Items, nil
 }
 
+// ListPodIdentityExceptions returns list of azurepodidentityexceptions
+func (c *Client) ListPodIdentityExceptions(ns string) (res *[]aadpodid.AzurePodIdentityException, err error) {
+	begin := time.Now()
+	ret, err := c.PodIdentityExceptionListWatch.List(v1.ListOptions{
+		FieldSelector: "metadata.namespace=" + ns,
+	})
+	if err != nil {
+		glog.Error(err)
+		return nil, err
+	}
+	stats.Update(stats.ExceptionList, time.Since(begin))
+	return &ret.(*aadpodid.AzurePodIdentityExceptionList).Items, nil
+}
+
 // ListPodIds - given a pod with pod name space
 // returns a map with list of azure identities in each state
 func (c *Client) ListPodIds(podns, podname string) (map[string][]aadpodid.AzureIdentity, error) {
diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go
index a74c1290..3f11ec80 100644
--- a/pkg/k8s/client.go
+++ b/pkg/k8s/client.go
@@ -37,11 +37,13 @@ func getPodPhaseFilter() string {
 // Client api client
 type Client interface {
 	// GetPodInfo returns the pod name, namespace & replica set name for a given pod ip
-	GetPodInfo(podip string) (podns, podname, rsName string, err error)
+	GetPodInfo(podip string) (podns, podname, rsName string, selectors *metav1.LabelSelector, err error)
 	// ListPodIds pod matching azure identity or nil
 	ListPodIds(podns, podname string) (map[string][]aadpodid.AzureIdentity, error)
 	// GetSecret returns secret the secretRef represents
 	GetSecret(secretRef *v1.SecretReference) (*v1.Secret, error)
+	// ListPodIdentityExceptions returns list of azurepodidentityexceptions
+	ListPodIdentityExceptions(namespace string) (*[]aadpodid.AzurePodIdentityException, error)
 }
 
 // KubeClient k8s client
@@ -92,22 +94,23 @@ func (c *KubeClient) getReplicasetName(pod v1.Pod) string {
 }
 
 // GetPodInfo get pod ns,name from apiserver
-func (c *KubeClient) GetPodInfo(podip string) (podns, poddname, rsName string, err error) {
+func (c *KubeClient) GetPodInfo(podip string) (podns, poddname, rsName string, labels *metav1.LabelSelector, err error) {
 	if podip == "" {
-		return "", "", "", fmt.Errorf("podip is empty")
+		return "", "", "", nil, fmt.Errorf("podip is empty")
 	}
 
 	podList, err := c.getPodListRetry(podip, getPodListRetries, getPodListSleepTimeMilliseconds)
 
 	if err != nil {
-		return "", "", "", err
+		return "", "", "", nil, err
 	}
 	numMatching := len(podList.Items)
 	if numMatching == 1 {
-		return podList.Items[0].Namespace, podList.Items[0].Name, c.getReplicasetName(podList.Items[0]), nil
+		return podList.Items[0].Namespace, podList.Items[0].Name, c.getReplicasetName(podList.Items[0]), &metav1.LabelSelector{
+			MatchLabels: podList.Items[0].Labels}, nil
 	}
 
-	return "", "", "", fmt.Errorf("match failed, ip:%s matching pods:%v", podip, podList)
+	return "", "", "", nil, fmt.Errorf("match failed, ip:%s matching pods:%v", podip, podList)
 }
 
 func (c *KubeClient) getPodList(podip string) (*v1.PodList, error) {
@@ -181,6 +184,11 @@ func (c *KubeClient) ListPodIds(podns, podname string) (map[string][]aadpodid.Az
 	return c.CrdClient.ListPodIds(podns, podname)
 }
 
+// ListPodIdentityExceptions lists azurepodidentityexceptions
+func (c *KubeClient) ListPodIdentityExceptions(ns string) (*[]aadpodid.AzurePodIdentityException, error) {
+	return c.CrdClient.ListPodIdentityExceptions(ns)
+}
+
 // GetSecret returns secret the secretRef represents
 func (c *KubeClient) GetSecret(secretRef *v1.SecretReference) (*v1.Secret, error) {
 	secret, err := c.ClientSet.CoreV1().Secrets(secretRef.Namespace).Get(secretRef.Name, metav1.GetOptions{})
diff --git a/pkg/k8s/fake.go b/pkg/k8s/fake.go
index 802c98bb..ed0e981f 100644
--- a/pkg/k8s/fake.go
+++ b/pkg/k8s/fake.go
@@ -3,6 +3,7 @@ package k8s
 import (
 	aadpodid "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1"
 	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // FakeClient implements Interface
@@ -18,8 +19,8 @@ func NewFakeClient() (Client, error) {
 }
 
 // GetPodInfo returns fake pod name, namespace and replicaset
-func (c *FakeClient) GetPodInfo(podip string) (podns, podname, rsName string, err error) {
-	return "ns", "podname", "rsName", nil
+func (c *FakeClient) GetPodInfo(podip string) (podns, podname, rsName string, selectors *metav1.LabelSelector, err error) {
+	return "ns", "podname", "rsName", nil, nil
 }
 
 // ListPodIds for pod
@@ -27,6 +28,11 @@ func (c *FakeClient) ListPodIds(podns, podname string) (map[string][]aadpodid.Az
 	return nil, nil
 }
 
+// ListPodIdentityExceptions ...
+func (c *FakeClient) ListPodIdentityExceptions(ns string) (*[]aadpodid.AzurePodIdentityException, error) {
+	return nil, nil
+}
+
 // GetSecret returns secret the secretRef represents
 func (c *FakeClient) GetSecret(secretRef *v1.SecretReference) (*v1.Secret, error) {
 	return nil, nil
diff --git a/pkg/nmi/server/server.go b/pkg/nmi/server/server.go
index 963461ce..ecae44fa 100644
--- a/pkg/nmi/server/server.go
+++ b/pkg/nmi/server/server.go
@@ -17,12 +17,13 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/Azure/go-autorest/autorest/adal"
-
 	aadpodid "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1"
 	auth "github.com/Azure/aad-pod-identity/pkg/auth"
 	k8s "github.com/Azure/aad-pod-identity/pkg/k8s"
 	iptables "github.com/Azure/aad-pod-identity/pkg/nmi/iptables"
+	"github.com/Azure/aad-pod-identity/pkg/pod"
+	"github.com/Azure/go-autorest/autorest/adal"
+
 	log "github.com/sirupsen/logrus"
 )
 
@@ -266,15 +267,15 @@ func (s *Server) isMIC(podNS, rsName string) bool {
 	return false
 }
 
-func (s *Server) getMICToken(logger *log.Entry, rqClientID, rqResource string) ([]byte, int, error) {
+func (s *Server) getTokenForExceptedPod(logger *log.Entry, rqClientID, rqResource string) ([]byte, int, error) {
 	var token *adal.Token
 	var err error
 	// ClientID is empty, so we are going to use System assigned MSI
 	if rqClientID == "" {
-		logger.Infof("Fetching MIC token for system assigned MSI")
+		logger.Infof("Fetching token for system assigned MSI")
 		token, err = auth.GetServicePrincipalTokenFromMSI(rqResource)
 	} else { // User assigned identity usage.
-		logger.Infof("Fetching MIC token for user assigned MSI for id: %s", rqResource)
+		logger.Infof("Fetching token for user assigned MSI for id: %s", rqResource)
 		token, err = auth.GetServicePrincipalTokenFromMSIWithUserAssignedID(rqClientID, rqResource)
 	}
 	if err != nil {
@@ -305,17 +306,23 @@ func (s *Server) msiHandler(logger *log.Entry, w http.ResponseWriter, r *http.Re
 		http.Error(w, msg, http.StatusInternalServerError)
 		return
 	}
-	podns, podname, rsName, err := s.KubeClient.GetPodInfo(podIP)
+	podns, podname, rsName, selectors, err := s.KubeClient.GetPodInfo(podIP)
 	if err != nil {
 		logger.Errorf("missing podname for podip:%s, %+v", podIP, err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	exceptionList, err := s.KubeClient.ListPodIdentityExceptions(podns)
+	if err != nil {
+		logger.Errorf("getting list of azurepodidentityexceptions failed with error: %+v", err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 
 	// If its mic, then just directly get the token and pass back.
-	if s.isMIC(podns, rsName) {
-		logger.Infof("MIC pod token handling")
-		response, errorCode, err := s.getMICToken(logger, rqClientID, rqResource)
+	if pod.IsPodExcepted(selectors.MatchLabels, exceptionList) || s.isMIC(podns, rsName) {
+		logger.Infof("Exception pod %s/%s token handling", podns, podname)
+		response, errorCode, err := s.getTokenForExceptedPod(logger, rqClientID, rqResource)
 		if err != nil {
 			logger.Errorf("failed to get service principal token for pod:%s/%s.  Error code: %d. Error: %+v", podns, podname, errorCode, err)
 			http.Error(w, err.Error(), errorCode)
diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go
index a5ff061e..4caa9451 100644
--- a/pkg/pod/pod.go
+++ b/pkg/pod/pod.go
@@ -1,6 +1,7 @@
 package pod
 
 import (
+	"strings"
 	"time"
 
 	"github.com/Azure/aad-pod-identity/pkg/stats"
@@ -72,12 +73,14 @@ func (c *Client) syncCache(exit <-chan struct{}) {
 	glog.Infof("Pod cache synchronized. Took %s", time.Since(cacheSyncStarted).String())
 }
 
+// Start ...
 func (c *Client) Start(exit <-chan struct{}) {
 	go c.PodWatcher.Informer().Run(exit)
 	c.syncCache(exit)
 	glog.Info("Pod watcher started !!")
 }
 
+// GetPods returns list of all pods
 func (c *Client) GetPods() (pods []*corev1.Pod, err error) {
 	begin := time.Now()
 	crdReq, err := labels.NewRequirement(aadpodid.CRDLabelKey, selection.Exists, nil)
@@ -87,10 +90,28 @@ func (c *Client) GetPods() (pods []*corev1.Pod, err error) {
 	}
 	crdSelector := labels.NewSelector().Add(*crdReq)
 	listPods, err := c.PodWatcher.Lister().List(crdSelector)
-	//ClientSet.CoreV1().Pods("").List(v1.ListOptions{})
 	if err != nil {
 		return nil, err
 	}
 	stats.Put(stats.PodList, time.Since(begin))
 	return listPods, nil
 }
+
+// IsPodExcepted returns true if pod label is part of exception crd
+func IsPodExcepted(podLabels map[string]string, exceptionList *[]aadpodid.AzurePodIdentityException) bool {
+	return exceptionList != nil && len(*exceptionList) > 0 && labelInExceptionList(podLabels, *exceptionList)
+}
+
+// labelInExceptionList checks if the labels defined in azurepodidentityexception match label defined in pods
+func labelInExceptionList(podLabels map[string]string, exceptionList []aadpodid.AzurePodIdentityException) bool {
+	for _, podIdentityException := range exceptionList {
+		for exceptionLabelKey := range podIdentityException.Spec.PodLabels {
+			if val, ok := podLabels[exceptionLabelKey]; ok {
+				if strings.EqualFold(val, podLabels[exceptionLabelKey]) {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
diff --git a/pkg/stats/stats.go b/pkg/stats/stats.go
index d418c2fb..e7eb6d4e 100644
--- a/pkg/stats/stats.go
+++ b/pkg/stats/stats.go
@@ -17,6 +17,7 @@ const (
 	PodList              StatsType = "Pod listing"
 	BindingList          StatsType = "Binding listing"
 	IDList               StatsType = "ID listing"
+	ExceptionList        StatsType = "Pod Identity Exception listing"
 	AssignedIDList       StatsType = "Assigned ID listing"
 	CloudGet             StatsType = "Cloud provider get"
 	CloudPut             StatsType = "Cloud provider put"


Test patch:
diff --git a/pkg/mic/mic_test.go b/pkg/mic/mic_test.go
index 1b02e8a2..c61412be 100644
--- a/pkg/mic/mic_test.go
+++ b/pkg/mic/mic_test.go
@@ -372,7 +372,9 @@ func (c *TestCrdClient) CreateAssignedIdentity(assignedIdentity *aadpodid.AzureA
 func (c *TestCrdClient) UpdateAzureAssignedIdentityStatus(assignedIdentity *aadpodid.AzureAssignedIdentity, status string) error {
 	assignedIdentity.Status.Status = status
 	assignedIdentityToStore := *assignedIdentity //Make a copy to store in the map.
+	c.mu.Lock()
 	c.assignedIDMap[assignedIdentity.Name] = &assignedIdentityToStore
+	c.mu.Unlock()
 	return nil
 }
 
@@ -445,6 +447,11 @@ func (c *Client) ListPodIds(podns, podname string) (map[string][]aadpodid.AzureI
 	return map[string][]aadpodid.AzureIdentity{}, nil
 }
 
+// ListPodIdentityExceptions ...
+func (c *Client) ListPodIdentityExceptions(ns string) (*[]aadpodid.AzurePodIdentityException, error) {
+	return nil, nil
+}
+
 func (c *TestCrdClient) SetError(err error) {
 	c.err = &err
 }
diff --git a/test/common/k8s/azureidentitybinding/azureidentitybinding.go b/test/common/k8s/azureidentitybinding/azureidentitybinding.go
index 5592060b..085667cb 100644
--- a/test/common/k8s/azureidentitybinding/azureidentitybinding.go
+++ b/test/common/k8s/azureidentitybinding/azureidentitybinding.go
@@ -13,7 +13,7 @@ import (
 )
 
 // Create will create an Azure Identity Binding on a Kubernetes cluster
-func Create(name, templateOutputPath string) error {
+func Create(name, selector, templateOutputPath string) error {
 	t, err := template.New("aadpodidentitybinding.yaml").ParseFiles(path.Join("template", "aadpodidentitybinding.yaml"))
 	if err != nil {
 		return errors.Wrap(err, "Failed to parse aadpodidentitybinding.yaml")
@@ -28,9 +28,11 @@ func Create(name, templateOutputPath string) error {
 
 	// Go template parameters to be translated in test/e2e/template/aadpodidentitybinding.yaml
 	deployData := struct {
-		Name string
+		Name     string
+		Selector string
 	}{
 		name,
+		selector,
 	}
 	if err := t.Execute(deployFile, deployData); err != nil {
 		return errors.Wrap(err, "Failed to create a deployment file from aadpodidentitybinding.yaml")
diff --git a/test/common/k8s/azurepodidentityexception/azurepodidentityexception.go b/test/common/k8s/azurepodidentityexception/azurepodidentityexception.go
new file mode 100644
index 00000000..2d514c7e
--- /dev/null
+++ b/test/common/k8s/azurepodidentityexception/azurepodidentityexception.go
@@ -0,0 +1,78 @@
+package azurepodidentityexception
+
+import (
+	"encoding/json"
+	"html/template"
+	"os"
+	"os/exec"
+	"path"
+
+	aadpodid "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1"
+	"github.com/Azure/aad-pod-identity/test/common/util"
+	"github.com/pkg/errors"
+)
+
+// Create will create an Azure Identity Binding on a Kubernetes cluster
+func Create(name, templateOutputPath string, podLabels map[string]string) error {
+	t, err := template.New("aadpodidentityexception.yaml").ParseFiles(path.Join("template", "aadpodidentityexception.yaml"))
+	if err != nil {
+		return errors.Wrap(err, "Failed to parse aadpodidentityexception.yaml")
+	}
+
+	deployFilePath := path.Join(templateOutputPath, name+"-exception.yaml")
+	deployFile, err := os.Create(deployFilePath)
+	if err != nil {
+		return errors.Wrap(err, "Failed to create a deployment file from aadpodidentityexception.yaml")
+	}
+	defer deployFile.Close()
+
+	// Go template parameters to be translated in test/e2e/template/aadpodidentityexception.yaml
+	deployData := struct {
+		Name      string
+		PodLabels map[string]string
+	}{
+		name,
+		podLabels,
+	}
+	if err := t.Execute(deployFile, deployData); err != nil {
+		return errors.Wrap(err, "Failed to create a deployment file from aadpodidentityexception.yaml")
+	}
+
+	cmd := exec.Command("kubectl", "apply", "-f", deployFilePath)
+	util.PrintCommand(cmd)
+	_, err = cmd.CombinedOutput()
+	if err != nil {
+		return errors.Wrap(err, "Failed to deploy AzurePodIdentityException to the Kubernetes cluster")
+	}
+
+	return nil
+}
+
+// Delete will delete an AzurePodIdentityException from Kubernetes cluster
+func Delete(name, templateOutputPath string) error {
+	cmd := exec.Command("kubectl", "delete", "-f", path.Join(templateOutputPath, name+"-exception.yaml"), "--ignore-not-found")
+	util.PrintCommand(cmd)
+	_, err := cmd.CombinedOutput()
+	if err != nil {
+		return errors.Wrap(err, "Failed to delete AzurePodIdentityException from the Kubernetes cluster")
+	}
+
+	return nil
+}
+
+// GetAll will return a list of AzurePodIdentityException deployed on a Kubernetes cluster
+func GetAll() (*aadpodid.AzurePodIdentityExceptionList, error) {
+	cmd := exec.Command("kubectl", "get", "AzurePodIdentityException", "-ojson")
+	util.PrintCommand(cmd)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to get AzurePodIdentityException from the Kubernetes cluster")
+	}
+
+	list := aadpodid.AzurePodIdentityExceptionList{}
+	if err := json.Unmarshal(out, &list); err != nil {
+		return nil, errors.Wrap(err, "Failed to unmarshall json")
+	}
+
+	return &list, nil
+}
diff --git a/test/e2e/aadpodidentity_test.go b/test/e2e/aadpodidentity_test.go
index f761d9fe..2bdcbf82 100644
--- a/test/e2e/aadpodidentity_test.go
+++ b/test/e2e/aadpodidentity_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/Azure/aad-pod-identity/test/common/k8s/azureassignedidentity"
 	"github.com/Azure/aad-pod-identity/test/common/k8s/azureidentity"
 	"github.com/Azure/aad-pod-identity/test/common/k8s/azureidentitybinding"
+	"github.com/Azure/aad-pod-identity/test/common/k8s/azurepodidentityexception"
 	"github.com/Azure/aad-pod-identity/test/common/k8s/daemonset"
 	"github.com/Azure/aad-pod-identity/test/common/k8s/deploy"
 	"github.com/Azure/aad-pod-identity/test/common/k8s/infra"
@@ -680,7 +681,7 @@ var _ = Describe("Kubernetes cluster using aad-pod-identity", func() {
 		Expect(exists).To(Equal(true))
 	})
 
-	It("liveness probe test", func() {
+	It("should pass liveness probe test", func() {
 		pods, err := pod.GetAllNameByPrefix("mic")
 		Expect(err).NotTo(HaveOccurred())
 		Expect(pods).NotTo(BeNil())
@@ -705,6 +706,81 @@ var _ = Describe("Kubernetes cluster using aad-pod-identity", func() {
 			checkHealthProbe(p, "Active")
 		}
 	})
+
+	It("should pass validation by bypassing nmi using azurepodidentityexception crd", func() {
+		// Assign user assigned identity to every node
+		nodeList, err := node.GetAll()
+		Expect(err).NotTo(HaveOccurred())
+
+		enableUserAssignedIdentityOnCluster(nodeList, keyvaultIdentity)
+
+		err = azurepodidentityexception.Create(identityValidator, templateOutputPath, map[string]string{"app": "identity-validator"})
+		Expect(err).NotTo(HaveOccurred())
+
+		data := infra.IdentityValidatorTemplateData{
+			Name:                     identityValidator,
+			IdentityBinding:          "random",
+			Registry:                 cfg.Registry,
+			IdentityValidatorVersion: cfg.IdentityValidatorVersion,
+			Replicas:                 "1",
+		}
+
+		err = infra.CreateIdentityValidator(cfg.SubscriptionID, cfg.ResourceGroup, templateOutputPath, data)
+		Expect(err).NotTo(HaveOccurred())
+
+		ok, err := deploy.WaitOnReady(identityValidator)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(ok).To(Equal(true))
+
+		ok, err = azureassignedidentity.WaitOnLengthMatched(0)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(ok).To(Equal(true))
+
+		podName, err := pod.GetNameByPrefix(identityValidator)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(podName).NotTo(Equal(""))
+
+		identityClientID, err := azure.GetIdentityClientID(cfg.ResourceGroup, keyvaultIdentity)
+		Expect(err).NotTo(HaveOccurred())
+
+		cmdOutput, err := validateUserAssignedIdentityOnPod(podName, identityClientID)
+		Expect(errors.Wrap(err, string(cmdOutput))).NotTo(HaveOccurred())
+	})
+
+	It("should pass identity validation with correct identity and fail with wrong identity", func() {
+		// This test is to ensure when 2 identities for the pod exist, the
+		// correct identity is used based on the client id in the request.
+		// keyvault-identity has the right permissions to get and list secret
+		// keyvault-identity-5 only has permission to list and should fail to get secret.
+
+		setUpIdentityAndDeployment(keyvaultIdentity, "", "1")
+
+		err := azureidentity.CreateOnCluster(cfg.SubscriptionID, cfg.ResourceGroup, fmt.Sprintf("%s-%d", keyvaultIdentity, 5), templateOutputPath)
+		Expect(err).NotTo(HaveOccurred())
+
+		err = azureidentitybinding.Create(fmt.Sprintf("%s-%d", keyvaultIdentity, 5), keyvaultIdentity, templateOutputPath)
+		Expect(err).NotTo(HaveOccurred())
+
+		ok, err := azureassignedidentity.WaitOnLengthMatched(2)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(ok).To(Equal(true))
+
+		podName, err := pod.GetNameByPrefix(identityValidator)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(podName).NotTo(Equal(""))
+
+		identityClientID, err := azure.GetIdentityClientID(cfg.ResourceGroup, fmt.Sprintf("%s-%d", keyvaultIdentity, 5))
+		Expect(err).NotTo(HaveOccurred())
+
+		cmdOutput, err := validateUserAssignedIdentityOnPod(podName, identityClientID)
+		Expect(errors.Wrap(err, string(cmdOutput))).To(HaveOccurred())
+
+		identityClientID, err = azure.GetIdentityClientID(cfg.ResourceGroup, keyvaultIdentity)
+		Expect(err).NotTo(HaveOccurred())
+
+		cmdOutput, err = validateUserAssignedIdentityOnPod(podName, identityClientID)
+		Expect(errors.Wrap(err, string(cmdOutput))).NotTo(HaveOccurred())
+	})
 })
 
 func collectLogs(podName, dir string) {
@@ -865,7 +941,7 @@ func setUpIdentityAndDeployment(azureIdentityName, suffix, replicas string, tmpl
 	err := azureidentity.CreateOnCluster(cfg.SubscriptionID, cfg.ResourceGroup, azureIdentityName, templateOutputPath)
 	Expect(err).NotTo(HaveOccurred())
 
-	err = azureidentitybinding.Create(azureIdentityName, templateOutputPath)
+	err = azureidentitybinding.Create(azureIdentityName, azureIdentityName, templateOutputPath)
 	Expect(err).NotTo(HaveOccurred())
 
 	data := infra.IdentityValidatorTemplateData{
@@ -975,7 +1051,7 @@ func validateClusterWideUserAssignedIdentity(podName, identityClientID string) (
 }
 
 // enableUserAssignedIdentityOnCluster will assign an azure identity to all the worker nodes in a cluster
-func enableUserAssignedIdentityOnCluster(nodeList *node.List, azureIdentityName string) {
+func enableUserAssignedIdentityOnCluster(nodeList *node.List, identityName string) {
 	for _, n := range nodeList.Nodes {
 		if strings.Contains(n.Name, "master") {
 			continue
@@ -985,13 +1061,13 @@ func enableUserAssignedIdentityOnCluster(nodeList *node.List, azureIdentityName
 		m, err := getResourceManager(&n)
 		Expect(err).NotTo(HaveOccurred())
 
-		err = m.EnableUserAssignedIdentity(clusterIdentity)
+		err = m.EnableUserAssignedIdentity(identityName)
 		Expect(err).NotTo(HaveOccurred())
 	}
 }
 
 // removeUserAssignedIdentityFromCluster will remove an azure identity from all the worker nodes in a cluster
-func removeUserAssignedIdentityFromCluster(nodeList *node.List, azureIdentityName string) {
+func removeUserAssignedIdentityFromCluster(nodeList *node.List, identityName string) {
 	for _, n := range nodeList.Nodes {
 		if strings.Contains(n.Name, "master") {
 			continue
@@ -1001,7 +1077,7 @@ func removeUserAssignedIdentityFromCluster(nodeList *node.List, azureIdentityNam
 		m, err := getResourceManager(&n)
 		Expect(err).NotTo(HaveOccurred())
 
-		err = m.RemoveUserAssignedIdentity(clusterIdentity)
+		err = m.RemoveUserAssignedIdentity(identityName)
 		Expect(err).NotTo(HaveOccurred())
 	}
 }
diff --git a/test/e2e/identityvalidator/identityvalidator.go b/test/e2e/identityvalidator/identityvalidator.go
index a5f73ff7..66450eb0 100644
--- a/test/e2e/identityvalidator/identityvalidator.go
+++ b/test/e2e/identityvalidator/identityvalidator.go
@@ -67,6 +67,8 @@ func main() {
 
 // testClusterWideUserAssignedIdentity will verify whether cluster-wide user assigned identity is working properly
 func testClusterWideUserAssignedIdentity(logger *log.Entry, msiEndpoint, subscriptionID, resourceGroup, identityClientID string) error {
+	os.Setenv("AZURE_CLIENT_ID", identityClientID)
+	defer os.Unsetenv("AZURE_CLIENT_ID")
 	token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, azure.PublicCloud.ResourceManagerEndpoint, identityClientID)
 	if err != nil {
 		return errors.Wrapf(err, "Failed to get service principal token from user assigned identity")
@@ -85,6 +87,8 @@ func testClusterWideUserAssignedIdentity(logger *log.Entry, msiEndpoint, subscri
 
 // testUserAssignedIdentityOnPod will verify whether a pod identity is working properly
 func testUserAssignedIdentityOnPod(logger *log.Entry, msiEndpoint, identityClientID, keyvaultName, keyvaultSecretName, keyvaultSecretVersion string) error {
+	os.Setenv("AZURE_CLIENT_ID", identityClientID)
+	defer os.Unsetenv("AZURE_CLIENT_ID")
 	keyClient := keyvault.New()
 	authorizer, err := auth.NewAuthorizerFromEnvironment()
 	if err == nil {
diff --git a/test/e2e/setup.sh b/test/e2e/setup.sh
index 8d6b7b87..c9d978e6 100755
--- a/test/e2e/setup.sh
+++ b/test/e2e/setup.sh
@@ -61,3 +61,13 @@ for i in {0..4}; do
     # The following command might need a couple of retries to succeed
     role_assignment_retry "az role assignment create --role Reader --assignee "$IDENTITY_PRINCIPAL_ID" --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/$KEYVAULT_NAME""
 done
+
+# Create identity with not enough permissions to test failure path
+echo 'Creating a keyvault-identity-5 and assign only list policy...'
+az identity create -g "$RESOURCE_GROUP" -n keyvault-identity-5
+IDENTITY_CLIENT_ID=$(az identity show -g "$RESOURCE_GROUP" -n keyvault-identity-5  --query 'clientId' -otsv)
+IDENTITY_PRINCIPAL_ID=$(az identity show -g "$RESOURCE_GROUP" -n keyvault-identity-5 --query 'principalId' -otsv)
+az role assignment create --role 'Managed Identity Operator' --assignee "$AZURE_CLIENT_ID" --scope "/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.ManagedIdentity/userAssignedIdentities/keyvault-identity-5" || true
+az keyvault set-policy -n "$KEYVAULT_NAME" --secret-permissions list --spn "$IDENTITY_CLIENT_ID" || true
+# The following command might need a couple of retries to succeed
+role_assignment_retry "az role assignment create --role Reader --assignee "$IDENTITY_PRINCIPAL_ID" --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/$KEYVAULT_NAME""
diff --git a/test/e2e/template/aadpodidentitybinding.yaml b/test/e2e/template/aadpodidentitybinding.yaml
index 612dbe6a..35e334f2 100644
--- a/test/e2e/template/aadpodidentitybinding.yaml
+++ b/test/e2e/template/aadpodidentitybinding.yaml
@@ -4,4 +4,4 @@ metadata:
   name: {{.Name}}-binding
 spec:
   AzureIdentity: {{.Name}}
-  Selector: {{.Name}}
+  Selector: {{.Selector}}
diff --git a/test/e2e/template/aadpodidentityexception.yaml b/test/e2e/template/aadpodidentityexception.yaml
new file mode 100644
index 00000000..8cbd810f
--- /dev/null
+++ b/test/e2e/template/aadpodidentityexception.yaml
@@ -0,0 +1,9 @@
+apiVersion: "aadpodidentity.k8s.io/v1"
+kind: AzurePodIdentityException
+metadata:
+  name: {{.Name}}-exception
+spec:
+  PodLabels:
+  {{- range $key, $value := $.PodLabels }}
+    {{ $key }}: {{ $value -}}
+  {{ end }}
\ No newline at end of file
diff --git a/test/e2e/template/deployment-rbac.yaml b/test/e2e/template/deployment-rbac.yaml
index dc6bcf4e..70eaf192 100644
--- a/test/e2e/template/deployment-rbac.yaml
+++ b/test/e2e/template/deployment-rbac.yaml
@@ -41,6 +41,19 @@ spec:
     plural: azureidentities
   scope: Namespaced
 ---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: azurepodidentityexceptions.aadpodidentity.k8s.io
+spec:
+  group: aadpodidentity.k8s.io
+  version: v1
+  names:
+    kind: AzurePodIdentityException
+    singular: azurepodidentityexception
+    plural: azurepodidentityexceptions
+  scope: Namespaced
+---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
@@ -53,7 +66,7 @@ rules:
   resources: ["pods"]
   verbs: ["get", "list"]
 - apiGroups: ["aadpodidentity.k8s.io"]
-  resources: ["azureidentitybindings", "azureidentities"]
+  resources: ["azureidentitybindings", "azureidentities", "azurepodidentityexceptions"]
   verbs: ["get", "list"]
 - apiGroups: ["aadpodidentity.k8s.io"]
   resources: ["azureassignedidentities"]