JIT Access for Amazon EKS with Kyverno
© 2026 Sonrai Security. All rights reserved.
Overview
Extend Cloud Permissions Firewall (CPF) Just-In-Time (JIT) Access to your Amazon Elastic Kubernetes Service (Amazon EKS) clusters. A Kyverno admission policy evaluates requests that create, update, or delete resources in the cluster. If the user's AWS IAM Identity Center permission set is enrolled in JIT, the policy submits a JIT request to Sonrai on their behalf and blocks the change until an approver grants access.
When a user without an active JIT session tries to change a resource, for example with kubectl apply or kubectl delete, the request is denied. The denial message includes:
JIT Access Request: PENDING | Approvers: approver1@example.com, approver2@example.com
Once an approver approves the request, the user re-runs the command and it succeeds for the duration of the JIT session (1 hour by default).
Kubernetes doesn't send read-only requests (such as kubectl get, list, or watch) to admission controllers, so this policy doesn't gate read access.
This integration is in Tech Preview and supports AWS JIT only. You deploy and manage Kyverno in your own clusters. Sonrai does not install or operate Kyverno for you.
How It Works
- A user authenticates to Amazon EKS with an IAM Identity Center permission set role (
AWSReservedSSO_<PermissionSetName>_<hash>). - Kyverno receives the admission request and reads the user's session name and role Amazon Resource Name (ARN) from the EKS identity.
- Kyverno queries Sonrai to check whether that permission set is enrolled in JIT for the account.
- If it is enrolled, Kyverno submits a JIT request (
SubmitJitRequest) to Sonrai. - Kyverno denies the request unless the user already has an active JIT session. Approvers are notified through your configured CPF workflow (for example, ChatOps).
| Scenario | Result |
|---|---|
| Permission set is enrolled and the user has no active JIT session | Denied. A JIT request is created and the approvers are shown. |
| Permission set is enrolled and the user has an active JIT session | Allowed |
| Permission set is not enrolled in JIT for this account | Allowed (policy skipped) |
| Request is not made by an IAM Identity Center role (for example, a service account or IAM role) | Allowed (policy skipped) |
The policy fails open if it can't reach Sonrai to check JIT enrollment (for example, a network outage or an expired token). Monitor the token rotation job and Kyverno logs so that enforcement is not silently disabled.
Planning and Preparation
Before you begin, make sure you have:
- An Amazon EKS cluster where users authenticate with AWS IAM Identity Center permission sets.
- Kyverno installed in the cluster. See Install Kyverno using Helm.
kubectlaccess to the cluster with permission to create cluster policies, ConfigMaps, and CronJobs in thekyvernonamespace.- JIT configured in CPF for the AWS account that hosts the cluster, with the relevant permission sets enrolled.
- Your Sonrai Org ID. Find it in your profile menu in the Sonrai UI.
- The Sonrai scope of the AWS account, in the form
aws/<root-id>/<ou-id>/.../<account-id>.
Use a dedicated App identity for this integration rather than a person's user account. The access key carries the permissions of the roles assigned to the App, so assign only what the integration needs.
Step 1: Generate a Sonrai API Token
Create an App identity for the integration and generate its access key from the Apps tab on the Users screen. App identities are non-person accounts for programmatic access to the Sonrai API, so the integration doesn't depend on any individual user's account. For step-by-step instructions, see Apps.
When you create the App identity:
- Name — use a name that identifies the integration, for example
k8s-jit. - Assign Role(s) — assign only the roles needed to submit JIT requests and read JIT configuration.
- Key expiry — select 30 days, the longest expiry available.
The access key value is shown only once, immediately after it's generated. Copy it before closing the window and treat it as a secret.
Step 2: Create the ConfigMap
Create the ConfigMap that Kyverno and the token rotation job read from. Replace the placeholders with your values.
kubectl create configmap sonrai-cpf-jit-graphql-cm \
--namespace=kyverno \
--from-literal=graphql_url=https://<yourOrgID>.sonraisecurity.com/graphql \
--from-literal=scope=<aws-account-scope> \
--from-literal=tokenValue=<token>
| Key | Description | Example |
|---|---|---|
graphql_url | Your Sonrai GraphQL endpoint | https://<yourOrgID>.sonraisecurity.com/graphql |
scope | Sonrai scope of the AWS account that hosts the cluster | aws/r-abcd/ou-abcd-11111111/123456789012 |
tokenValue | Token from Step 1 | — |
To replace the token manually later, patch the ConfigMap:
kubectl patch configmap sonrai-cpf-jit-graphql-cm \
--namespace=kyverno \
--type merge \
-p '{"data":{"tokenValue":"<token>"}}'
The ConfigMap holds a live Sonrai API token. Restrict who can read ConfigMaps in the kyverno namespace with Kubernetes role-based access control (RBAC), and enable audit logging for that namespace.
Step 3: Apply the Cluster Policy
Save the following as cluster-policy.yaml. The policy reads its settings from the ConfigMap in the kyverno namespace.
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sonrai-cpf-jit-cp
spec:
validationFailureAction: Enforce
background: false
rules:
- name: deny-user-requests
match:
any:
- resources:
kinds:
- "*"
exclude:
any:
- resources:
kinds:
- "Policy"
- "ClusterPolicy"
- "PolicyException"
- "ClusterPolicyException"
- "GenerateRequest"
- "AdmissionReport"
- "BackgroundScanReport"
- "ClusterAdmissionReport"
- "ClusterBackgroundScanReport"
context:
- name: configMap
configMap:
namespace: kyverno
name: sonrai-cpf-jit-graphql-cm
- name: ssoUser
variable:
value: "{{ request.userInfo.extra.sessionName[0] || '' }}"
- name: permissionSetArn
variable:
value: "{{ request.userInfo.extra.canonicalArn[0] || '' }}"
- name: permissionSetName
variable:
value: "{{ regex_replace_all('arn:aws:iam::([0-9]{12}):role/AWSReservedSSO_(.+)_[a-f0-9]+', '{{ permissionSetArn }}', '$2') }}"
- name: userAccount
variable:
value: "{{ regex_replace_all('arn:aws:iam::([0-9]{12}):role/(.+)', '{{ permissionSetArn }}', '$1') }}"
- name: nowTimeString
variable:
value: "{{ to_string(time_now_utc()) }}"
- name: graphqlQuery
apiCall:
default: {}
method: POST
service:
url: "{{ configMap.data.graphql_url }}"
headers:
- key: "Authorization"
value: "Bearer {{ configMap.data.tokenValue }}"
- key: "Content-Type"
value: "application/json"
data:
- key: query
value: |
mutation jr {
SubmitJitRequest(input: {
account: "{{ userAccount }}"
requesterEmail: "{{ ssoUser }}"
requestedDuration: 3600000
permissionSetName: "{{ permissionSetName }}"
permissionSetArn: "{{ permissionSetArn }}"
}) {
success
pondRequest {
status
comment
expireAt
approvers {
email
name
}
}
}
}
- name: enrolledSets
apiCall:
method: POST
jmesPath: "data.JitConfiguration.items[?permissionSets] || `[]`"
default: []
service:
url: "{{ configMap.data.graphql_url }}"
headers:
- key: "Authorization"
value: "Bearer {{ configMap.data.tokenValue }}"
- key: "Content-Type"
value: "application/json"
data:
- key: query
value: |
query enrolledsets {
JitConfiguration(
where: {
scope: {
value: "{{ configMap.data.scope }}"
}
active: {
value: true
}
}
) {
count
items {
scope @regex(
match: ".*/(.*)"
replace: "$1"
)
permissionSets {
id
}
}
}
}
- name: requestStatus
variable:
value: "{{ graphqlQuery.data.SubmitJitRequest.pondRequest.status || '' }}"
- name: approverEmails
variable:
value: "{{ graphqlQuery.data.SubmitJitRequest.pondRequest.approvers && join(', ', graphqlQuery.data.SubmitJitRequest.pondRequest.approvers[*].email) || '' }}"
preconditions:
all:
# Only evaluate requests made with an IAM Identity Center role
- key: "{{ permissionSetArn }}"
operator: NotEquals
value: ""
message: "Permission set ARN is missing in the token, skipping JIT validation"
# Only evaluate permission sets enrolled in JIT for this account
- key: "{{ length(enrolledSets[?scope == '{{ userAccount }}' && contains(permissionSets[].id, '{{ permissionSetName }}')]) }}"
operator: GreaterThan
value: 0
message: "No enrollment in JIT for account {{ userAccount }} and permission set {{ permissionSetName }}"
validate:
message: "{{ requestStatus == 'PENDING' && 'JIT Access Request: PENDING | Approvers: ' || 'JIT Access Request: ' }}{{ requestStatus == 'PENDING' && approverEmails || requestStatus || '' }}{{ requestStatus != 'PENDING' && ' - ' || '' }}{{ requestStatus != 'PENDING' && (graphqlQuery.data.SubmitJitRequest.pondRequest.comment || '') || '' }}"
deny:
conditions:
any:
# Deny while the request is pending approval
- key: "{{ requestStatus }}"
operator: Equals
value: "PENDING"
# Deny unless the user already has an active JIT session
- key: "{{ requestStatus != 'PENDING' && !contains(graphqlQuery.data.SubmitJitRequest.pondRequest.comment || '', 'already has an active JIT session') }}"
operator: Equals
value: true
Apply the policy:
kubectl apply -f cluster-policy.yaml
To change the length of a JIT session, edit requestedDuration in the policy. The value is in milliseconds (3600000 = 1 hour).
Step 4: Automate Token Rotation
Sonrai API tokens expire after a maximum of 30 days. The following manifest deploys a CronJob that uses the current token to generate a new one and writes it back to the ConfigMap. Its Role allows it to read and patch only the sonrai-cpf-jit-graphql-cm ConfigMap.
Save the following as token_rotation.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: token-rotation-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: configmap-updater
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "patch"]
resourceNames: ["sonrai-cpf-jit-graphql-cm"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: token-rotation-rolebinding
subjects:
- kind: ServiceAccount
name: token-rotation-sa
roleRef:
kind: Role
name: configmap-updater
apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: token-rotation
spec:
schedule: "0 0 1,26 * *" # Midnight on the 1st and 26th of each month
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
serviceAccountName: token-rotation-sa
restartPolicy: OnFailure
containers:
- name: token-rotator
image: alpine/kubectl
command:
- /bin/sh
- -c
- |
set -e
GRAPHQL_URL="${GRAPHQL_URL}"
CURRENT_TOKEN="${TOKEN_VALUE}"
echo "Calling GraphQL endpoint: ${GRAPHQL_URL}"
RESPONSE=$(curl -s -X POST "${GRAPHQL_URL}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${CURRENT_TOKEN}" \
-d '{
"query": "mutation createToken { GenerateSonraiUserToken(input: { expiresIn: 2592000, name: \"k8s-jit\" }) { name expireAt token } }"
}')
NEW_TOKEN=$(echo "${RESPONSE}" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "${NEW_TOKEN}" ]; then
echo "Failed to extract token from response"
exit 1
fi
echo "New token generated successfully"
kubectl patch configmap sonrai-cpf-jit-graphql-cm \
--type merge \
-p "{\"data\":{\"tokenValue\":\"${NEW_TOKEN}\"}}"
echo "ConfigMap updated successfully"
env:
- name: GRAPHQL_URL
valueFrom:
configMapKeyRef:
name: sonrai-cpf-jit-graphql-cm
key: graphql_url
- name: TOKEN_VALUE
valueFrom:
configMapKeyRef:
name: sonrai-cpf-jit-graphql-cm
key: tokenValue
Apply the manifest to the kyverno namespace (the same namespace as the ConfigMap):
kubectl apply -f token_rotation.yaml --namespace=kyverno
For production clusters, pin the alpine/kubectl image to a specific digest rather than the default tag.
Verify the Integration
- As a user whose permission set is enrolled in JIT, make a change in a test namespace, for example
kubectl create configmap jit-test --namespace=<test-namespace>. - Confirm the request is denied with a
JIT Access Request: PENDINGmessage that lists the approvers. - Approve the request in CPF or through ChatOps.
- Re-run the command and confirm it succeeds.
To confirm the token rotation job works, trigger it manually and review its logs:
kubectl create job --from=cronjob/token-rotation token-rotation-test --namespace=kyverno
kubectl logs job/token-rotation-test --namespace=kyverno
Break-Glass Access
If the policy blocks access unexpectedly, a cluster administrator can remove it. The policy excludes Kyverno policy resources, so deleting it is never blocked by the policy itself.
kubectl delete clusterpolicies.kyverno.io sonrai-cpf-jit-cp
Deleting the policy removes JIT enforcement from the entire cluster. Reapply cluster-policy.yaml as soon as the issue is resolved.
Frequently Asked Questions
| Question | Answer |
|---|---|
| Does this support GCP or Azure? | No. This integration supports AWS JIT on Amazon EKS only. |
| Which users are affected? | Only users who authenticate with an IAM Identity Center permission set that is enrolled in JIT for the cluster's AWS account. Service accounts and other IAM roles aren't evaluated. |
| How long does JIT access last? | 1 hour by default. Change requestedDuration in the policy to adjust it. |
| Why is my command denied with no approvers listed? | The JIT request was submitted but isn't pending. Review the status and comment in the denial message, then check the request in CPF. |
Does this block kubectl get? | No. Read-only requests don't pass through admission control, so only create, update, and delete requests are evaluated. |
| Does the policy add latency? | Yes. Each evaluated request makes two calls to the Sonrai API. Make sure your cluster can reach <yourOrgID>.sonraisecurity.com over HTTPS and that the Kyverno webhook timeout allows for the round trip. |