This directory contains a CloudFormation template for setting up AWS IAM roles and permissions required for Nullify's AWS integration.
EKS Integration: The EKS integration created by this CloudFormation template only sets up the necessary IAM roles and trust policies. For the integration to be fully functional, you must deploy the Kubernetes cronjob resources separately using Helm charts or other Kubernetes deployment methods. The CloudFormation template alone does not deploy any Kubernetes resources. To scan EKS without running anything in the cluster, see Managed EKS scan.
This template creates:
- IAM Role with cross-account trust to Nullify's AWS account
- Multiple IAM managed policies (split due to size limits)
- Support for both direct role assumption and EKS OIDC integration
- Comprehensive AWS service read permissions with security controls
-
Obtain Configuration Values from Nullify Configure Page:
- Log in to your Nullify configure page
- Navigate to Configure > Integrations
- Select AWS integration to begin setup
- Note the provided values:
- External ID for your account
- Nullify's cross-account role ARN
- S3 bucket name (for Kubernetes integration)
- KMS ARN (optional, for key management operations): an alias ARN or a key ARN; both are accepted
-
AWS Requirements:
- AWS CLI configured with appropriate permissions
- IAM permissions to create roles and policies
-
For EKS Integration (optional):
- EKS cluster with OIDC provider enabled
- OIDC provider URL (see Getting EKS OIDC URL below)
- Helm charts or Kubernetes manifests to deploy the collector cronjobs
📖 Reference: For detailed setup instructions, see the Nullify AWS Integration Documentation.
Alternative: Contact Nullify Support for assistance with configuration values.
If you're enabling EKS integration, you'll need the OIDC provider URL from your EKS cluster.
# Get OIDC URL for your cluster
aws eks describe-cluster --name YOUR_CLUSTER_NAME --query 'cluster.identity.oidc.issuer' --output text
# Remove the https:// prefix for the CloudFormation parameter
aws eks describe-cluster --name YOUR_CLUSTER_NAME --query 'cluster.identity.oidc.issuer' --output text | sed 's|https://||'Example output:
https://oidc.eks.us-west-2.amazonaws.com/id/ABCDEF1234567890ABCDEF1234567890
For CloudFormation parameter (without https://):
oidc.eks.us-west-2.amazonaws.com/id/ABCDEF1234567890ABCDEF1234567890
- Go to Amazon EKS in the AWS Console
- Click on your cluster name
- Go to the Configuration tab
- Under Details, look for OpenID Connect provider URL
kubectl get configmap aws-auth -n kube-system -o yaml | grep "oidc"- Full URL format:
https://oidc.eks.REGION.amazonaws.com/id/CLUSTER_ID - For CloudFormation parameter: Use only the part after
https:// - Example: If full URL is
https://oidc.eks.us-west-2.amazonaws.com/id/ABC123, useoidc.eks.us-west-2.amazonaws.com/id/ABC123
# Check if OIDC is enabled
aws eks describe-cluster --name YOUR_CLUSTER_NAME --query 'cluster.identity.oidc'
# If null, enable OIDC provider
eksctl utils associate-iam-oidc-provider --cluster YOUR_CLUSTER_NAME --approve- Download
nullify-cloudformation-template.json - Go to CloudFormation console
- Create new stack
- Upload template file
- Fill in parameters with values from Nullify configure page
- Deploy stack
# Deploy the stack
# Note: ExternalID, CrossAccountRoleArn, and NullifyS3Bucket values are provided in the Nullify configure page
aws cloudformation create-stack \
--stack-name nullify-aws-integration \
--template-body file://nullify-cloudformation-template.json \
--parameters \
ParameterKey=CustomerName,ParameterValue=yourcompany \
ParameterKey=ExternalID,ParameterValue=YOUR-EXTERNAL-ID \
ParameterKey=CrossAccountRoleArn,ParameterValue=arn:aws:iam::NULLIFY-ACCOUNT:role/NULLIFY-ROLE \
ParameterKey=NullifyS3Bucket,ParameterValue=NULLIFY-BUCKET \
--capabilities CAPABILITY_NAMED_IAM
# Check deployment status
aws cloudformation describe-stacks \
--stack-name nullify-aws-integration \
--query 'Stacks[0].StackStatus'
# Get the role ARN
aws cloudformation describe-stacks \
--stack-name nullify-aws-integration \
--query 'Stacks[0].Outputs[?OutputKey==`RoleArn`].OutputValue' \
--output text# For EKS clusters, enable integration
# First, get your OIDC URL (see "Getting EKS OIDC URL" section above)
# Note: ExternalID, CrossAccountRoleArn, and NullifyS3Bucket values are provided in the Nullify configure page
OIDC_URL=$(aws eks describe-cluster --name YOUR_CLUSTER_NAME --query 'cluster.identity.oidc.issuer' --output text | sed 's|https://||')
aws cloudformation create-stack \
--stack-name nullify-aws-integration \
--template-body file://nullify-cloudformation-template.json \
--parameters \
ParameterKey=CustomerName,ParameterValue=yourcompany \
ParameterKey=ExternalID,ParameterValue=YOUR-EXTERNAL-ID \
ParameterKey=CrossAccountRoleArn,ParameterValue=arn:aws:iam::NULLIFY-ACCOUNT:role/NULLIFY-ROLE \
ParameterKey=NullifyS3Bucket,ParameterValue=NULLIFY-BUCKET \
ParameterKey=EnableEKSIntegration,ParameterValue=true \
ParameterKey=EKSOidcProviderURL,ParameterValue=$OIDC_URL \
--capabilities CAPABILITY_NAMED_IAM
# Or manually with a specific OIDC URL:
aws cloudformation create-stack \
--stack-name nullify-aws-integration \
--template-body file://nullify-cloudformation-template.json \
--parameters \
ParameterKey=CustomerName,ParameterValue=yourcompany \
ParameterKey=ExternalID,ParameterValue=YOUR-EXTERNAL-ID \
ParameterKey=CrossAccountRoleArn,ParameterValue=arn:aws:iam::NULLIFY-ACCOUNT:role/NULLIFY-ROLE \
ParameterKey=NullifyS3Bucket,ParameterValue=NULLIFY-BUCKET \
ParameterKey=EnableEKSIntegration,ParameterValue=true \
ParameterKey=EKSOidcProviderURL,ParameterValue=A78D8794A06CAE5791C5812CDB164C7D.gr7.ap-southeast-2.eks.amazonaws.com \
--capabilities CAPABILITY_NAMED_IAMPass the KMS ARN shown on the Nullify configure page as NullifyKMSKeyArn. Both forms are accepted:
- an alias ARN,
arn:aws:kms:<region>:<account>:alias/<name>, which the configure page shows today; - a key ARN,
arn:aws:kms:<region>:<account>:key/<key-id>, including multi-Regionkey/mrk-...keys. The configure page will show the key ARN once Nullify rolls that change out.
# Deploy with the KMS ARN for key management operations
aws cloudformation create-stack \
--stack-name nullify-aws-integration \
--template-body file://nullify-cloudformation-template.json \
--parameters \
ParameterKey=CustomerName,ParameterValue=yourcompany \
ParameterKey=ExternalID,ParameterValue=YOUR-EXTERNAL-ID \
ParameterKey=CrossAccountRoleArn,ParameterValue=arn:aws:iam::NULLIFY-ACCOUNT:role/NULLIFY-ROLE \
ParameterKey=NullifyS3Bucket,ParameterValue=NULLIFY-BUCKET \
ParameterKey=NullifyKMSKeyArn,ParameterValue=arn:aws:kms:us-west-2:123456789012:alias/NULLIFY-ALIAS \
--capabilities CAPABILITY_NAMED_IAMThe policy grants its actions on the ARN you pass in one statement, and on arn:aws:kms:<region>:<account>:key/* in that ARN's account and region in a second. IAM ignores alias ARNs for key operations, so a Nullify alias ARN needs that wildcard to work at all; a key ARN in your own account does not, and must not reach the rest of your keys. The second statement therefore carries the IAM condition StringNotEquals on aws:PrincipalAccount, which holds only when the ARN names an account other than the one you deploy into. A Nullify ARN of either form gets the wildcard; an ARN from your own account is granted alone.
The account check runs on every request rather than at deploy time because CloudFormation template Conditions accept only Ref, Fn::FindInMap and other conditions, so AWS::AccountId cannot be compared with an account parsed out of a parameter. The effective permissions are the same either way. Nullify's key policy is the real gate: the role can only use Nullify keys whose key policy allows it. Either ARN form produces a working grant, so an existing stack does not need updating when the configure page changes. The KMSPolicyResource stack output lists both resources.
Update an existing stack, keeping its current values:
aws cloudformation update-stack \
--stack-name nullify-aws-integration \
--template-body file://nullify-cloudformation-template.json \
--parameters \
ParameterKey=AWSRegion,UsePreviousValue=true \
ParameterKey=CrossAccountRoleArn,UsePreviousValue=true \
ParameterKey=CustomerName,UsePreviousValue=true \
ParameterKey=EKSOidcProviderURL,UsePreviousValue=true \
ParameterKey=EnableEKSIntegration,UsePreviousValue=true \
ParameterKey=ExternalID,UsePreviousValue=true \
ParameterKey=NullifyS3Bucket,UsePreviousValue=true \
ParameterKey=NullifyKMSKeyArn,UsePreviousValue=true \
--capabilities CAPABILITY_NAMED_IAMUsePreviousValue=true only works for parameters the stack's current template already has. A stack deployed from an older template version may lack EKSOidcProviderURL, EnableEKSIntegration or NullifyKMSKeyArn: for those, replace UsePreviousValue=true with ParameterValue=... (or drop the line to take the default), and keep --template-body file://nullify-cloudformation-template.json so the new template is used.
The Nullify configure page shows the upload target for the Helm chart's collector.s3.bucket. Which form it shows decides the setup:
- An S3 access point ARN (
arn:aws:s3:REGION:NULLIFY-ACCOUNT:accesspoint/NAME): first setNullifyS3AccessPointArnto that ARN in this stack, then use the same ARN ascollector.s3.bucket. Without the parameter, uploads fail withAccessDenied: a grant on the bucket does not cover requests made through an access point. - A bucket name: use it as
collector.s3.bucketand leaveNullifyS3AccessPointArnempty. That bucket isNullifyS3Bucket.
With EnableEKSIntegration=true and NullifyS3AccessPointArn set, the S3 access policy also allows s3:PutObject, s3:PutObjectAcl and s3:ListBucket on the access point and its /object/* path, and the NullifyS3Bucket grants stay.
ParameterKey=NullifyS3AccessPointArn,ParameterValue=arn:aws:s3:REGION:NULLIFY-ACCOUNT:accesspoint/NAME \On an existing stack, add that line to the update-stack command above.
| Output | Value |
|---|---|
RoleArn, IAMRoleArn |
ARN of the Nullify read-only role |
RoleName |
AWSIntegration-<CustomerName>-NullifyReadOnlyRole |
KubernetesGroupName |
nullify-readonly, the default group for the managed EKS scan |
KMSPolicyResource |
Resources of the KMS policy, comma-separated: NullifyKMSKeyArn, always granted, and key/* in its account and region, granted only when that account is not your own (only when NullifyKMSKeyArn is set) |
Nullify lists Kubernetes resources in your EKS cluster from Nullify's cloud-scan compute, using the read-only role this template creates. Nothing runs in the cluster. The scan only calls list, on 26 kinds:
| API group | Resources |
|---|---|
| core | nodes, namespaces, pods, services, persistentvolumeclaims, persistentvolumes, configmaps, resourcequotas, limitranges, serviceaccounts |
| apps | deployments, daemonsets, statefulsets, replicasets |
| batch | jobs |
| networking.k8s.io | ingresses, networkpolicies |
| discovery.k8s.io | endpointslices |
| rbac.authorization.k8s.io | roles, rolebindings, clusterroles, clusterrolebindings |
| admissionregistration.k8s.io | validatingwebhookconfigurations, mutatingwebhookconfigurations, validatingadmissionpolicies, validatingadmissionpolicybindings |
Kubernetes has no metadata-only permission for Secrets: list secrets returns values, so the ClusterRole does not grant list or get on secrets.
In-cluster collector (Helm nullify-k8s-collector) |
Managed scan | |
|---|---|---|
| Runs in the cluster | CronJob with an IRSA service account | Nothing |
| Cluster endpoint | Any, including private-only | Public endpoint that admits Nullify's egress IPs |
| AWS setup | EnableEKSIntegration=true, OIDC provider URL, NullifyS3Bucket (upload target) |
One access entry per cluster |
| Kubernetes setup | Helm release | ClusterRole and binding for group nullify-readonly |
| Upgrades | You upgrade the chart | None |
Both can run against the same cluster.
- The main stack above is deployed. Note its
CustomerName: the role isAWSIntegration-<CustomerName>-NullifyReadOnlyRole. - The cluster's authentication mode is
APIorAPI_AND_CONFIG_MAP:aws eks describe-cluster --name CLUSTER --query cluster.accessConfig.authenticationMode. Switching fromCONFIG_MAPis one-way. The script does it only with--allow-auth-mode-change. - The public endpoint is enabled. Private-only clusters are not supported; use the in-cluster collector.
- The operator running setup has
eks:DescribeCluster,eks:DescribeUpdate,eks:ListUpdates,eks:UpdateClusterConfig,eks:CreateAccessEntry,eks:DescribeAccessEntry,eks:DeleteAccessEntry,eks:ListAccessEntries,eks:ListAssociatedAccessPolicies,eks:DisassociateAccessPolicy(cleanup of leftover access policies),eks:TagResource,eks:UntagResource, CloudFormation permissions on the access stacks, and Kubernetes cluster-admin (to create the ClusterRole and to impersonate inverify). - AWS CLI v2, plus
kubectl.
Deploy nullify-eks-managed-scan-access.json once in each region that has clusters. Access entries are regional; the IAM role is global.
aws cloudformation deploy \
--region eu-west-1 \
--stack-name nullify-eks-managed-scan-access \
--template-file nullify-eks-managed-scan-access.json \
--capabilities CAPABILITY_AUTO_EXPAND \
--parameter-overrides \
CustomerName=yourcompany \
ClusterNames=prod-eu,staging-eu \
KubernetesAuthorization=RBACGroupCustomerNamemust match the main stack. If the role is ever recreated, update this stack too: EKS binds an access entry to the role's unique ID, so a recreated role is not recognised.- The template uses the
AWS::LanguageExtensionstransform, so it needsCAPABILITY_AUTO_EXPAND. Do not update it with--use-previous-template. - Resource logical IDs drop non-alphanumeric characters, so
prod-euandprodeucollide in one stack. Put them in separate stacks. - Removing a name from
ClusterNamesdeletes that cluster's access entry. - For many accounts or regions, use StackSets with a per-instance
ClusterNamesoverride. AWS documents transform support for self-managed StackSets; check service-managed (Organizations) StackSets before relying on them. - Entries are tagged
ManagedBy=nullify-connector-cloudformation. Without CloudFormation, the setup script creates them, taggedManagedBy=nullify-connector.
The access entry carries group nullify-readonly. Bind that group to a list-only ClusterRole in each cluster, using one of:
kubectl apply -f manifests/nullify-readonly-rbac.yamlfrom a checkout of this repository at a commit you reviewed;- the
nullify-k8s-readonly-accessHelm chart; - the same manifest committed to your GitOps repository (Flux, Argo CD).
AmazonEKSAdminViewPolicy is not offered. It grants Secret values, pods/log, and on EKS 1.34 and earlier exec via get pods/exec. Any leftover EKS access policy (AdminView, ClusterAdmin, Edit, View) fails verify; apply and remove disassociate them. Use the list-only ClusterRole in manifests/nullify-readonly-rbac.yaml or the nullify-k8s-readonly-access Helm chart (see #61).
Nullify connects from these IPs. Use the Nullify region that serves your tenant:
| Nullify region | Egress IPs |
|---|---|
ap-southeast-2 |
13.55.32.104, 3.105.146.106, 13.211.99.100 |
eu-central-1 |
18.198.60.231, 18.157.227.250, 18.185.152.197 |
us-east-2 |
52.15.146.50, 16.58.40.80, 3.133.15.210 |
aws eks update-cluster-config replaces publicAccessCidrs rather than appending to it, so the script merges:
- a list that contains
0.0.0.0/0is left unchanged andapplywarns (the script never adds0.0.0.0/0and never silently removes a customer's existing open entry;verifyfails if the list is open to the world); - otherwise the missing
/32s are appended, the result is refused above the EKS limit of 40 CIDRs, the list is re-read just before the update, and the CIDRs to add are recorded in the cluster tagnullify-pending-cidrsbefore the update and moved tonullify-added-cidrsonce it succeeds, soremovetakes out only those; - the id of that update is recorded in
nullify-pending-updateas soon asupdate-cluster-configreturns it, so a stopped run does not have to trustaws eks list-updatesto already show an update accepted seconds earlier; - if a run stops between the two (timeout, Ctrl-C, expired credentials), the next
applyorremovetreats the pending CIDRs thatpublicAccessCidrsholds as added and drops the rest. Both first describe the recorded update - or, for a record written without one, every updateaws eks list-updatesreturns - and stop, leaving the pending record, while anEndpointAccessUpdateis stillInProgress, because the cluster can reportACTIVEbefore the update lands. An update EKS no longer knows about is skipped, since it cannot be in progress; - the records use up to three cluster tags at once, so
applystops before changingpublicAccessCidrswhen the cluster has fewer free tags than it needs under the EKS limit of 50 tags per resource. Every tag key counts,aws:ones included: EKS documents the limit without exempting them, and a precheck that passes where the write fails would leavepublicAccessCidrschanged with no record of it; - a record longer than an EKS tag value (256 characters) continues in
nullify-added-cidrs-2,nullify-added-cidrs-3, and so on.
../scripts/setup-eks-managed-scan.sh does steps 1 (unless --skip-access-entry), 2 and 3 for one cluster, and verifies the result:
cd aws-integration-setup/scripts
# Print every change; make none
./setup-eks-managed-scan.sh plan --cluster prod-eu --region eu-west-1 \
--customer-name yourcompany --nullify-region eu-central-1 --skip-access-entry
./setup-eks-managed-scan.sh apply --cluster prod-eu --region eu-west-1 \
--customer-name yourcompany --nullify-region eu-central-1 --skip-access-entry
./setup-eks-managed-scan.sh verify --cluster prod-eu --region eu-west-1 \
--customer-name yourcompany --nullify-region eu-central-1Other flags: --role-arn instead of --customer-name, --authorization rbac (the only supported mode), --group, --allow-auth-mode-change, --kube-context NAME (use an existing kubeconfig context instead of a temporary one), --rbac-manifest PATH|URL (default: manifests/nullify-readonly-rbac.yaml from this checkout, or the same file at release tag nullify-k8s-readonly-access-v0.1.0 when the checkout lacks it; if that tag's file cannot be fetched the run stops and asks for --rbac-manifest), --skip-rbac when Helm or GitOps applies RBAC, --skip-network, and --dry-run with apply or remove. --help lists them all.
setup-eks-managed-scan.sh verify checks that:
- the authentication mode supports access entries, the access entry exists with only the expected group, and no EKS access policy is associated;
kubectl auth can-i list <resource> --all-namespaces --as nullify-verify --as-group nullify-readonlyanswersyesfor all 26 resources, andcreate pods,get/list secrets,get/create pods/exec,get/list pods/log,get/create pods/attach,get/create pods/portforward,get nodes/proxy, andescalate/bindon clusterroles do not.impersonateusers / serviceaccounts / groups is requirednowhenkubectl auth can-iaccepts those checks; otherwise it is skipped;publicAccessCidrsadmits Nullify's egress IPs and does not contain0.0.0.0/0.
can-i exercises Kubernetes RBAC only. It cannot see access-policy grants or prove that Nullify can reach the endpoint. Finish by confirming the cluster connects on the Nullify configure page.
| Symptom | Likely cause |
|---|---|
401 Unauthorized |
No access entry, an entry for a since-recreated role, or CONFIG_MAP authentication mode |
403 ... cannot list <resource> |
RBAC missing or incomplete for that resource |
| Timeout connecting to the endpoint | Nullify's IPs are not in publicAccessCidrs, or the endpoint is private-only |
KMS AccessDenied on collector upload |
NullifyKMSKeyArn is empty or not the value shown on the configure page. If it matches a Nullify ARN, the role already allows key/* in that account and region, so the denial comes from Nullify's key policy: contact Nullify support. An ARN in your own account is granted alone, and an alias ARN from your own account grants nothing, because IAM never resolves an alias ARN to a key |
Remove in this order so no access entry outlives the role:
- For each cluster:
./setup-eks-managed-scan.sh remove --cluster CLUSTER --region REGION --customer-name yourcompany. It deletes the RBAC manifest's objects, access entries taggedManagedBy=nullify-connector, a leftoverAmazonEKSAdminViewPolicyassociation if one is present (cleanup only), and only the CIDRs recorded innullify-added-cidrsplus any innullify-pending-cidrsthatpublicAccessCidrsstill holds. It refuses to leavepublicAccessCidrsempty, and refuses to settlenullify-pending-cidrswhile theEndpointAccessUpdateit names is stillInProgress(re-run once it finishes). If the public endpoint has since been disabled, it leaves the endpoint settings,publicAccessCidrsand both tags alone and says so;publicAccessCidrscan still hold Nullify's CIDRs when the endpoint is re-enabled, so re-runremovethen. It skips RBAC objects labelledapp.kubernetes.io/managed-by: Helm; pass--skip-rbacwhen Helm or GitOps (Flux, Argo CD) owns the RBAC. - In each region:
aws cloudformation delete-stack --region REGION --stack-name nullify-eks-managed-scan-access. - Delete the main stack.
../scripts/cleanup.sh --method cloudformation --stack-name nullify-aws-integration --region <stack-region> --eks-access-regions eu-west-1,us-east-1 does steps 2 and 3 in that order. Without --region, the main stack is looked up in the AWS CLI's configured region.