Community post originally published on GitHub by Shuting Zhao, a maintainer of Kyverno

When running workloads in Amazon Elastic Kubernetes Service (EKS), it is essential to ensure supply chain security by verifying container image signatures and other metadata. To achieve this, you can configure Kyverno, a CNCF policy engine designed for Kubernetes, to pull from ECR private registries for image verification. It’s possible to pass in the credentials via secrets, but that can get difficult to manage and automate across multiple clusters. In this blog post, we will explore an alternative method that simplifies the authentication process by leveraging Kyverno and IRSA (IAM Roles for Service Accounts) in EKS for image verification.

Applications, such as Kyverno, running within a Pod’s containers can utilize the AWS SDK to make API requests to AWS services by leveraging AWS Identity and Access Management (IAM) permissions. IAM roles for service accounts enable the management of credentials for these applications. Instead of manually creating and distributing AWS credentials to the containers, you can associate an IAM role with a Kubernetes service account and configure your Pods to utilize this service account. The detailed steps for this process can be found in the documentation. In this blog, we will guide you through the complete process of enabling IAM roles for the Kyverno service account and demonstrate how to verify this using the Kyverno verifyImages rule.

Setting up the EKS Cluster

First, you need to create an EKS cluster. You can then use the AWS CLI to update the kubeconfig file with the cluster details:

$ aws eks update-kubeconfig --region us-west-2 --name kyverno-irsa
Added new context arn:aws:eks:us-west-2:xxxxxxxxxxxx:cluster/kyverno-irsa to /Users/kyverno/.kube/config

Once the kubeconfig is updated, you can verify the cluster by running the following command:

$ kubectl get node
NAME                                          STATUS   ROLES    AGE   VERSION
ip-172-31-56-181.us-west-2.compute.internal   Ready    <none>   1h   v1.27.3-eks-a5565ad

Note: when you use IRSA, it updates the credential chain of the pod to use the IRSA token, however, the pod can still inherit the rights of the instance profile assigned to the worker node. You need to block access to instance metadata to prevent pods that do not use IRSA from inheriting the role assigned to the worker node.

You can follow this guidance to restrict access via the following command, for example:

aws ec2 modify-instance-metadata-options --instance-id <instance-id> --http-tokens required --http-put-response-hop-limit 1

Installing Kyverno

Once you have the cluster set up, you can use Helm to install Kyverno into the cluster:

helm upgrade --install kyverno kyverno/kyverno --namespace kyverno --create-namespace

Enabling IAM roles for service accounts

Creating an IAM OIDC Provider for the Cluster

To enable IRSA, you need to create an IAM OIDC provider for the EKS cluster. You can retrieve the OIDC issuer URL using the AWS CLI. The following command retrieves the provider for the cluster kyverno-irsa, replace it with your own cluster name:

export cluster_name=kyverno-irsa
oidc_id=$(aws eks describe-cluster --name $cluster_name --query "cluster.identity.oidc.issuer" --output text | cut -d '/' -f 5)

Determine whether an IAM OIDC provider with your cluster’s ID is already in your account.

aws iam list-open-id-connect-providers | grep $oidc_id | cut -d "/" -f4

If output is returned, then you already have an IAM OIDC provider for your cluster. If no output is returned, then there’s no OIDC provider is associated with the cluster. You can create one using the eksctl command:

$ eksctl utils associate-iam-oidc-provider --cluster $cluster_name --approve
2023-08-14 21:16:31 [ℹ]  will create IAM Open ID Connect provider for cluster "kyverno-irsa" in "us-west-2"
2023-08-14 21:16:33 [✔]  created IAM Open ID Connect provider for cluster "kyverno-irsa" in "us-west-2"

Configuring a Kubernetes Service Account to Assume an IAM Role

To associate an IAM role with a Kubernetes service account, you need to create an IAM policy for your IAM role. If you want to associate an existing IAM policy, you can skip this step.

Setup a custom policy with the following permissions, note that in production its best to not use a wildcard and specify resources:

cat >notation-signer-policy.json <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "signer:GetSigningProfile",
                "signer:ListSigningProfiles",
                "signer:SignPayload",
                "signer:GetRevocationStatus",
                "signer:DescribeSigningJob",
                "signer:ListSigningJobs"
            ],
            "Resource": "*"
        }
    ]
}
EOF

Create the IAM policy:

aws iam create-policy --policy-name notation-signer-policy --policy-document file://notation-signer-policy.json

To configure a Kubernetes service account to assume an IAM role, you can use the eksctl command to create an IAM service account.

If your Kyverno is installed with default configurations, you can run the following command directly to create the IAM service account. Otherwise, replace the service account name and namespace with your custom values.

$ eksctl create iamserviceaccount --override-existing-serviceaccounts kyverno-admission-controller --namespace kyverno --cluster kyverno-irsa --role-name kyverno-irsa --attach-policy-arn arn:aws:iam::xxxxxxxxxxxx:policy/notation-signer-policy --approve
2023-08-14 21:18:17 [ℹ]  1 iamserviceaccount (kyverno/kyverno-admission-controller) was included (based on the include/exclude rules)
2023-08-14 21:18:17 [!]  metadata of serviceaccounts that exist in Kubernetes will be updated, as --override-existing-serviceaccounts was set
2023-08-14 21:18:17 [ℹ]  1 task: { 
    2 sequential sub-tasks: { 
        create IAM role for serviceaccount "kyverno/kyverno-admission-controller",
        create serviceaccount "kyverno/kyverno-admission-controller",
    } }2023-08-14 21:18:17 [ℹ]  building iamserviceaccount stack "eksctl-kyverno-irsa-addon-iamserviceaccount-kyverno-kyverno-admission-controller"
2023-08-14 21:18:17 [ℹ]  deploying stack "eksctl-kyverno-irsa-addon-iamserviceaccount-kyverno-kyverno-admission-controller"
2023-08-14 21:18:18 [ℹ]  waiting for CloudFormation stack "eksctl-kyverno-irsa-addon-iamserviceaccount-kyverno-kyverno-admission-controller"
2023-08-14 21:18:54 [ℹ]  waiting for CloudFormation stack "eksctl-kyverno-irsa-addon-iamserviceaccount-kyverno-kyverno-admission-controller"
2023-08-14 21:18:55 [ℹ]  serviceaccount "kyverno/kyverno-admission-controller" already exists
2023-08-14 21:18:55 [ℹ]  updated serviceaccount "kyverno/kyverno-admission-controller"

After creating the IAM service account, you can verify that the role and service account are configured correctly.

Confirm that the IAM role’s trust policy is configured correctly:

$ aws iam get-role --role-name kyverno-irsa --query Role.AssumeRolePolicyDocument
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/2EA2DE9A6C72778FA517C24D7BBE2916"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.us-west-2.amazonaws.com/id/2EA2DE9A6C72778FA517C24D7BBE2916:aud": "sts.amazonaws.com",
                    "oidc.eks.us-west-2.amazonaws.com/id/2EA2DE9A6C72778FA517C24D7BBE2916:sub": "system:serviceaccount:kyverno:kyverno-admission-controller"
                }
            }
        }
    ]
}

Confirm that the policy that you attached to your role in a previous step is attached to the role:

$ aws iam list-attached-role-policies --role-name kyverno-irsa --query AttachedPolicies --output text
arn:aws:iam::xxxxxxxxxxxx:policy/notation-signer-policy notation-signer-policy

Confirm that the Kyverno service account is annotated with the role:

$ kubectl describe serviceaccount kyverno-admission-controller -n kyverno

Name:                kyverno-admission-controller
Namespace:           kyverno
Annotations:         eks.amazonaws.com/role-arn: arn:aws:iam::xxxxxxxxxxxx:role/kyverno-irsa

Confirm that the environment variables are injected to the admission controller:

$ kubectl get pod -n kyverno -l app.kubernetes.io/component=admission-controller -o yaml | grep AWS -A2
      - name: AWS_STS_REGIONAL_ENDPOINTS
        value: regional
      - name: AWS_DEFAULT_REGION
        value: us-west-2
      - name: AWS_REGION
        value: us-west-2
      - name: AWS_ROLE_ARN
        value: arn:aws:iam::xxxxxxxxxxxx:role/kyverno-irsa
      - name: AWS_WEB_IDENTITY_TOKEN_FILE
        value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

If you do not see these environment variables, try restarting the pod to inject variables. If you still do not see these variables, follow this instruction to verify that your pod identity webhook configuration exists and is valid.

Verifying ECR private images using IRSA and Kyverno

To test IRSA works with Kyverno, you can create pods with signed and unsigned images respectively and verify container images signatures using the Kyverno policy. If the IAM role assumption is configured correctly, the pod should be deployed successfully. Otherwise, Kyverno will deny the request.

The test image used in this blog is signed by Notary. If you don’t have a signed images for testing, you can follow this guidance to sign a private ECR image using Notation.

You can inspect all signatures with Notation. The following is an inspection result of all signatures and signed artifacts for the test image xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation:v1:

✗ notation inspect xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation:v1
Inspecting all signatures for signed artifact
xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation@sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
└── application/vnd.cncf.notary.signature
    └── sha256:86abf8af48c152f871a5ea56a62a9302e145760089db926420e72c1bbd0de07d
        ├── media type: application/jose+json
        ├── signature algorithm: RSASSA-PSS-SHA-256
        ├── signed attributes
        │   ├── signingScheme: notary.x509
        │   └── signingTime: Fri Aug 11 16:37:40 2023
        ├── user defined attributes
        │   └── (empty)
        ├── unsigned attributes
        │   └── signingAgent: Notation/1.0.0
        ├── certificates
        │   └── SHA256 fingerprint: da1f2d7d648dfacc7ebd59f98a9f35c753c331d80ca4280bb94060f4af4a5357
        │       ├── issued to: CN=test,O=Notary,L=Seattle,ST=WA,C=US
        │       ├── issued by: CN=test,O=Notary,L=Seattle,ST=WA,C=US
        │       └── expiry: Thu May 19 21:15:18 2033
        └── signed artifact
            ├── media type: application/vnd.docker.distribution.manifest.v2+json
            ├── digest: sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
            └── size: 938

The following policy verifies the image signature for pods in test-shuting namespace, you can tune the policy to verify different images:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  annotations:
    pod-policies.kyverno.io/autogen-controllers: none
  name: test-irsa
spec:
  background: true
  rules:
  - match:
      resources:
        kinds:
        - Pod
        namespaces:
        - test-shuting
    name: check-digest
    verifyImages:
    - attestors:
      - count: 1
        entries:
        - certificates:
            cert: |-
              -----BEGIN CERTIFICATE-----
              ...
              ...
              ...
              -----END CERTIFICATE-----
      imageReferences:
      - xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting*
      mutateDigest: true
      required: true
      type: Notary
      verifyDigest: true
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30

Once the policy is installed in the cluster, you can create the pod using the signed image and check the creation passes through:

$ kubectl -n test-shuting run test --image=xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation:v1 --dry-run=server
pod/test created (server dry run)

Then if you create the pod using an unsigned image, the pod creation is blocked by Kyverno as it does not have any signatures associated with it:

$ kubectl -n test-shuting run test --image=xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation:v1-unsigned --dry-run=server

Error from server: admission webhook "mutate.kyverno.svc-fail" denied the request: 

resource Pod/test-shuting/test was blocked due to the following policies 

test-irsa:
  check-digest: 'failed to verify image xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation:v1-unsigned:
    .attestors[0].entries[0]: failed to verify xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation@sha256:74a98f0e4d750c9052f092a7f7a72de7b20f94f176a490088f7a744c76c53ea5:
    no signature is associated with "xxxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/test-shuting-notation@sha256:74a98f0e4d750c9052f092a7f7a72de7b20f94f176a490088f7a744c76c53ea5",
    make sure the artifact was signed successfully'

Conclusion

By leveraging Kyverno and IRSA, you can simplify the configuration of IAM role assumptions for Kubernetes service accounts in EKS. This approach enhances the security of the cluster by ensuring fine-grained access control to AWS resources. With the steps outlined in this blog post, you can easily set up and test IRSA in your EKS cluster.