Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d006b8a
feat: grant wafv2:Get*, ec2:GetEbsEncryptionByDefault and the missing…
tim-thacker-nullify Sep 7, 2026
4932cdf
fix(aws): grant the 45 read actions the CSPM collectors call
tim-thacker-nullify Sep 7, 2026
3a1a4fa
fix(aws): scope the connector role to configuration reads only
tim-thacker-nullify Sep 7, 2026
0179faf
fix(aws): deny the data reads a second audit found still reachable
tim-thacker-nullify Sep 7, 2026
fa61e87
fix(k8s): drop the unused get verb and harden the terraform collector
tim-thacker-nullify Sep 7, 2026
950f2bd
fix(aws): cut the role to the actions the collectors call
tim-thacker-nullify Sep 7, 2026
ea6c719
fix(aws): enumerate ec2 and iam instead of wildcarding them
tim-thacker-nullify Sep 8, 2026
2841258
fix(aws): correct two invalid action names and a neptune coverage gap
tim-thacker-nullify Sep 8, 2026
aec23c2
fix(aws): grant KMS key/* for alias ARNs, pin the collector image and…
tim-thacker-nullify Sep 14, 2026
0c515b3
docs(aws): fix Terraform README drift and document KMS, access point …
tim-thacker-nullify Sep 14, 2026
9ac0299
ci: validate terraform-v6 and run each AWS row at its declared Terraf…
tim-thacker-nullify Sep 14, 2026
a0b117d
feat(aws): add Terraform modules for managed EKS scan access
tim-thacker-nullify Sep 14, 2026
1d45378
feat(aws): wire managed EKS scan into the root and examples
tim-thacker-nullify Sep 14, 2026
edf0825
docs(aws): document the managed EKS scan modules, RBAC, endpoint allo…
tim-thacker-nullify Sep 14, 2026
d3b6852
ci: validate the managed-scan module and examples and run terraform t…
tim-thacker-nullify Sep 14, 2026
dca000d
fix(aws): convert publicAccessCidrs to a list before building the upd…
tim-thacker-nullify Sep 14, 2026
59b3945
ci: run the terraform test suites at the lowest version that can run …
tim-thacker-nullify Sep 14, 2026
4996322
fix(terraform): name each collector's cluster and narrow the KMS grant
tim-thacker-nullify Sep 14, 2026
e3190b9
docs(terraform): note the cluster_name requirement in the upgrade notes
tim-thacker-nullify Sep 15, 2026
c251dba
fix(terraform): require KMS for the collector and narrow the key/* gr…
tim-thacker-nullify Sep 15, 2026
a90bf29
fix(terraform): drop the unencrypted-upload opt-out and reject collec…
tim-thacker-nullify Sep 15, 2026
a7dac03
fix(terraform): move the collector/managed-scan duplicate guard to th…
tim-thacker-nullify Sep 15, 2026
c20f948
fix(terraform): compare full cluster ARNs in the collector duplicate …
tim-thacker-nullify Sep 15, 2026
c74571b
docs(terraform): tell collector-cutover instructions to stop the coll…
tim-thacker-nullify Sep 15, 2026
796037b
merge: fold #81 into stack 1 root
tim-thacker-nullify Sep 15, 2026
c468820
merge: fold #83 into stack 1 root
tim-thacker-nullify Sep 15, 2026
ed5681a
merge: fold #89 into stack 1 root
tim-thacker-nullify Sep 15, 2026
2820407
merge: fold #91 into stack 1 root
tim-thacker-nullify Sep 15, 2026
7a16636
merge: fold #92 into stack 1 root
tim-thacker-nullify Sep 15, 2026
95deaad
merge: fold #93 into stack 1 root
tim-thacker-nullify Sep 15, 2026
fa505f0
merge: fold #94 into stack 1 root
tim-thacker-nullify Sep 15, 2026
cabde87
fix(aws): drop collector ClusterRole grants the collector never lists
tim-thacker-nullify Sep 15, 2026
52c291c
fix(aws): drop collector ClusterRole grants the collector never lists…
tim-thacker-nullify Sep 15, 2026
1d5cd17
fix(terraform): reconcile K8s scan-mode RBAC and address #81 review f…
tim-thacker-nullify Sep 15, 2026
d4e7a9c
fix(helm): collector chart namespace, service account and default-val…
tim-thacker-nullify Sep 15, 2026
5e171a6
merge: fold #58 into stack 1 root
tim-thacker-nullify Sep 15, 2026
c38a83b
fix(terraform): finish W58-2 in both collector-role trees
tim-thacker-nullify Sep 15, 2026
dc6abe9
merge: fold #101 into #58 (drop the 11 uncollected kinds from both Te…
tim-thacker-nullify Sep 15, 2026
f5380de
merge: bring #58 (with #101) into #78 and port #100's surviving fixes
tim-thacker-nullify Sep 15, 2026
ea9b305
docs(terraform): describe today's Secrets handling and list batch/job…
tim-thacker-nullify Sep 15, 2026
e02477b
fix(terraform): match #58's networking rule formatting and state char…
tim-thacker-nullify Sep 15, 2026
c217813
fix(aws): drop AdminView and tighten secret-shaped IAM
tim-thacker-nullify Sep 17, 2026
aa0627a
fix(aws): pin ssm:GetDocument to the Session Manager document partition
tim-thacker-nullify Sep 17, 2026
1229887
merge: bring #60 into #78 and take the rework's helm-release.yml
tim-thacker-nullify Sep 17, 2026
05202f3
fix(ci): compare partition-scoped IAM ARNs in the CFN/Terraform drift…
tim-thacker-nullify Sep 17, 2026
ac24227
fix(aws): deny SSM document bodies and pin the collector image
tim-thacker-nullify Sep 17, 2026
480a085
fix(k8s): drop secrets list so Secret values cannot be read
tim-thacker-nullify Sep 17, 2026
e8c2922
merge: resolve conflicts with main
tim-thacker-nullify Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 58 additions & 7 deletions .github/workflows/terraform-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
- "gcp-integration-setup/**"
- "aws-integration-setup/**"
- "azure-integration-setup/**"
- "scripts/check_iam_policy_drift.py"
- "scripts/**"
- ".github/workflows/terraform-validate.yml"
push:
branches:
Expand All @@ -21,7 +21,7 @@ on:
- "gcp-integration-setup/**"
- "aws-integration-setup/**"
- "azure-integration-setup/**"
- "scripts/check_iam_policy_drift.py"
- "scripts/**"
- ".github/workflows/terraform-validate.yml"

permissions:
Expand All @@ -34,6 +34,12 @@ jobs:
strategy:
fail-fast: false
matrix:
# AWS rows run the declared floor (Terraform 1.8, the version the
# terraform-test job also runs); examples that use ephemeral resources
# (v6 multi-cluster, v6 managed-scan) need 1.10. The three modules/
# directories each tree has are covered by the terraform-test job
# below instead, which already runs fmt-check, init and validate (via
# terraform test) against both Terraform versions with mocked values.
include:
- directory: gcp-integration-setup/terraform
terraform: 1.9.5
Expand All @@ -44,17 +50,21 @@ jobs:
- directory: gcp-integration-setup/terraform/examples/single-project
terraform: 1.9.5
- directory: aws-integration-setup/terraform
terraform: 1.9.5
terraform: 1.8.5
- directory: aws-integration-setup/terraform/examples/basic
terraform: 1.9.5
terraform: 1.8.5
- directory: aws-integration-setup/terraform/examples/multi-cluster-complete
terraform: 1.9.5
terraform: 1.8.5
- directory: aws-integration-setup/terraform/examples/managed-scan
terraform: 1.8.5
- directory: aws-integration-setup/terraform-v6
terraform: 1.9.5
terraform: 1.8.5
- directory: aws-integration-setup/terraform-v6/examples/basic
terraform: 1.9.5
terraform: 1.8.5
- directory: aws-integration-setup/terraform-v6/examples/multi-cluster-complete
terraform: 1.10.5
- directory: aws-integration-setup/terraform-v6/examples/managed-scan
terraform: 1.10.5
- directory: azure-integration-setup/terraform
terraform: 1.9.5
- directory: azure-integration-setup/terraform/examples/management-group
Expand All @@ -81,6 +91,47 @@ jobs:
working-directory: ${{ matrix.directory }}
run: terraform validate

# Module logic (preconditions, checks, RBAC rules, KMS grants) against mocked
# providers. No cloud credentials are used. 1.8.5 is the declared floor and
# the lowest version that runs this suite (mock_provider is 1.7+, strcontains
# in the tests is 1.8+), so every module is exercised at the floor it
# advertises as well as at 1.10.5.
terraform-test:
name: terraform test (${{ matrix.directory }}, tf ${{ matrix.terraform }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
terraform:
- 1.8.5
- 1.10.5
directory:
- aws-integration-setup/terraform/modules/eks-managed-scan-access
- aws-integration-setup/terraform/modules/k8s-resources
- aws-integration-setup/terraform/modules/nullify-aws-integration
- aws-integration-setup/terraform-v6/modules/eks-managed-scan-access
- aws-integration-setup/terraform-v6/modules/k8s-resources
- aws-integration-setup/terraform-v6/modules/nullify-aws-integration
steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false

- name: terraform fmt -check (including test files)
working-directory: ${{ matrix.directory }}
run: terraform fmt -check -recursive

- name: terraform init -backend=false
working-directory: ${{ matrix.directory }}
run: terraform init -backend=false

- name: terraform test
working-directory: ${{ matrix.directory }}
run: terraform test

# The CloudFormation template and both Terraform trees grant the same
# customer role from three independently maintained templates (ncc #58,
# whiskeylotus W58-3). This job parses the CFN JSON and fails if any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,6 @@
"s3:GetObject*",
"s3:DeleteObject*",
"s3:RestoreObject",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
Expand Down Expand Up @@ -480,6 +477,9 @@
"ec2:GetSnapshotBlockPublicAccessState",
"ec2:SearchTransitGateway*",
"ecr:Describe*",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"ecr:GetLifecyclePolicy",
"ecr:GetRepositoryPolicy",
"ecs:Describe*",
Expand Down
102 changes: 66 additions & 36 deletions aws-integration-setup/terraform-v6/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,43 @@

This directory mirrors `../terraform/` but targets **AWS Terraform provider v6** (`~> 6.0`). Use this if your project has already upgraded to AWS provider v6.

For AWS provider v5, use `../terraform/` instead.
For AWS provider v5, use `../terraform/` instead. `../terraform/README.md` documents the modules, variables, outputs, the managed EKS scan and its prerequisites; this file covers what differs.

## What Changed from v5
## What Differs from v5

The only breaking change that affects this configuration:

**`data "aws_eks_cluster_auth"` was removed in AWS provider v6.** It is replaced by the `ephemeral` resource variant, which requires Terraform >= 1.10:
- **Per-resource `region`.** AWS provider v6 accepts a `region` argument on most resources and data sources.
- `nullify-aws-integration` reads each EKS cluster in the region from its ARN, so `eks_cluster_arns` can mix regions with one provider. There is no `eks_oidc_issuer_urls` variable here.
- `eks-managed-scan-access` creates each access entry in its cluster's region, so one module instance covers every region. `managed_scan_cluster_arns` in the root can mix regions.
- **Ephemeral EKS token.** The `multi-cluster-complete` and `managed-scan` examples read the cluster token with `ephemeral "aws_eks_cluster_auth"`, which keeps it out of plan and state and needs Terraform >= 1.10:

```hcl
# v5 (removed in v6)
data "aws_eks_cluster_auth" "primary" {
name = "..."
}
token = data.aws_eks_cluster_auth.primary.token

# v6
ephemeral "aws_eks_cluster_auth" "primary" {
name = "..."
region = "eu-west-1"
name = "..."
}
token = ephemeral.aws_eks_cluster_auth.primary.token
```

This change is applied in `examples/multi-cluster-complete/main.tf`. The core `nullify-aws-integration` module and `k8s-resources` module are unaffected.

## Requirements

- Terraform >= 1.10 (for ephemeral resource support in the multi-cluster example)
- Terraform >= 1.8 (>= 1.10 for the `multi-cluster-complete` and `managed-scan` examples)
- AWS provider ~> 6.0
- Kubernetes provider ~> 2.20
- Kubernetes provider ~> 2.20 (only `modules/k8s-resources` and the `multi-cluster-complete` and `managed-scan` examples)

## Architecture

```
terraform-v6/
├── modules/
│ ├── nullify-aws-integration/ # AWS IAM resources only
│ │ ├── versions.tf # AWS provider ~> 6.0
│ │ ├── variables.tf
│ │ ├── locals.tf
│ │ ├── data.tf
│ │ ├── main.tf
│ │ └── outputs.tf
│ └── k8s-resources/ # Kubernetes resources only
│ ├── providers.tf
│ ├── variables.tf
│ ├── main.tf
│ └── outputs.tf
│ ├── eks-managed-scan-access/ # EKS access entries for the managed scan
│ └── k8s-resources/ # Collector and/or managed-scan RBAC
│ (each module has a tests/ terraform test suite)
├── examples/
│ ├── basic/ # AWS IAM only example
│ └── multi-cluster-complete/ # Full multi-cluster EKS example
├── versions.tf # AWS ~> 6.0, Kubernetes ~> 2.20
│ ├── managed-scan/ # One cluster, managed scan, no in-cluster agent
│ └── multi-cluster-complete/ # Two clusters, collector and/or managed scan
├── versions.tf # AWS ~> 6.0
├── variables.tf
├── main.tf
├── outputs.tf
Expand All @@ -72,15 +58,42 @@ cp terraform.tfvars.example terraform.tfvars
terraform init && terraform apply
```

### 2. Multi-Cluster EKS Integration
### 2. Managed EKS Scan (no in-cluster agent)

```bash
cd examples/managed-scan/
cp terraform.tfvars.example terraform.tfvars
# Edit with your cluster ARN and Nullify region
terraform init && terraform apply
```

### 3. Multi-Cluster EKS Integration

```bash
cd examples/multi-cluster-complete/
cp terraform.tfvars.example terraform.tfvars
# Edit with your cluster ARNs and values
# Edit with your cluster ARNs and values; set scan_mode for the managed scan
terraform init && terraform apply
```

## Managed EKS scan in one module instance

```hcl
module "nullify_eks_access" {
source = "./modules/eks-managed-scan-access"

principal_arn = module.nullify_aws_integration.role_arn
principal_unique_id = module.nullify_aws_integration.role_unique_id
nullify_region = "eu-central-1"
cluster_arns = [
"arn:aws:eks:eu-west-1:123456789012:cluster/prod",
"arn:aws:eks:us-east-1:123456789012:cluster/us-prod",
]
}
```

`authorization = "rbac"` is the only supported mode. It needs the list-only `nullify-readonly` ClusterRole on each cluster: apply it with `k8s-resources` (`enable_collector = false, enable_managed_scan_rbac = true`), or with any equivalent manifest of your own. `AmazonEKSAdminViewPolicy` is not supported; if an existing cluster still has it associated with the Nullify role, disassociate it. The clusters must already exist when you plan; see `../terraform/README.md`.

## Required Variables

- `customer_name`: Your company/customer name (used in resource naming)
Expand All @@ -91,13 +104,30 @@ terraform init && terraform apply

- `aws_region`: AWS region for IAM resources (default: ap-southeast-2)
- `s3_bucket_name`: S3 bucket for scan results (optional)
- `kms_key_arn`: KMS key ARN for key management operations (optional)
- `nullify_s3_access_point_arn`: S3 access point for collector uploads (optional)
- `kms_key_arn`: KMS key ARN or alias ARN from the Nullify configure page; see "KMS" in `../terraform/README.md`. Optional for this module; required by `k8s-resources` whenever `enable_collector` is true
- `enable_kubernetes_integration`: Set to `true` for EKS integration
- `eks_cluster_arns`: List of EKS cluster ARNs to integrate with
- `eks_cluster_arns`: List of EKS cluster ARNs to integrate with, in any region
- `collector_cluster_arns`: Override for which of those clusters `eks_managed_scan_access` treats as already running the collector; defaults to `eks_cluster_arns` when `enable_kubernetes_integration` is `true`. Set it explicitly when trust is granted ahead of the collector's actual deployment, or to decouple a collector-to-managed-scan cutover from revoking IRSA trust -- disable or remove the collector on that cluster first, before setting the override
- `enable_managed_scan`, `managed_scan_cluster_arns`, `nullify_region`, `managed_scan_authorization`, `managed_scan_kubernetes_group`: managed EKS scan
- `kubernetes_namespace`: Kubernetes namespace name (default: nullify)
- `cronjob_schedule`: Cron schedule for data collection (default: "0 0 * * *")
- `collector_image`: Docker image for collector (default: nullify/k8s-collector:latest)
- `service_account_name`: Collector service account name (default: nullify-k8s-collector-sa)
- `tags`: Resource tags
- `cronjob_schedule`: Deprecated and unused here; set the schedule on `k8s-resources`

`k8s-resources` requires `cluster_name` and `kms_key_arn` whenever `enable_collector` is true, with no unencrypted opt-out. `cluster_name` must match your actual cluster name exactly, must match a cluster registered on the Nullify configure page, and must differ per cluster: the collector uploads to `<prefix>/k8s-collector/<cluster_name>-data.json`, and Nullify joins that upload on the name. See "Kubernetes Resources" and "KMS" in `../terraform/README.md`.

A cluster cannot run the collector and the managed scan at once: the two paths register it under different identities, duplicating its inventory. This is enforced by `eks-managed-scan-access`'s `collector_cluster_arns` variable, not by a `k8s-resources` flag combination -- `enable_collector` and `enable_managed_scan_rbac` may be set together on one `k8s-resources` instance to stage a cutover, since the ClusterRole it applies is inert until an access entry uses it. See "Kubernetes Resources" in `../terraform/README.md`.

`k8s-resources` defaults `collector_image` to `public.ecr.aws/w4o2j2x4/integrations:k8s-collector-3.46.0`, the same tag the Helm chart deploys. Replace any explicit `nullify/k8s-collector:latest`: Nullify does not publish that Docker Hub image.

## Upgrade Notes

- `k8s-resources` now requires `cluster_name` while `enable_collector` is true, and sets it as the collector's `CLUSTER_NAME`. A deployment that never set it was uploading to `k8s-collector/default-name-data.json`; after this change it uploads to `k8s-collector/<cluster_name>-data.json`. The old object is in Nullify's bucket, which your role cannot read or delete: ask Nullify to remove the stale `default-name` cluster, and register the new name on the configure page before the next run.
- `k8s-resources` now requires `kms_key_arn` while `enable_collector` is true, with no opt-out. The shipped collector images refuse to upload without KMS encryption, so a deployment that left it empty was collecting successfully and then failing every upload. Set the value from the configure page; ask Nullify for a key if you have none.
- `k8s-resources` collector resources moved to `count` instances. `moved` blocks keep existing state, so a plan should show no changes; check it before applying.
- The KMS policy grants `key/*` in the account and region of `kms_key_arn` only when that ARN is an alias. A concrete key ARN is preferred: it grants that ARN only.
- The root no longer declares a `kubernetes` provider or `hashicorp/kubernetes` requirement: this tree's root never applied Kubernetes objects, so remove any provider configuration you added to satisfy it.

## Validation

Expand Down
2 changes: 1 addition & 1 deletion aws-integration-setup/terraform-v6/examples/basic/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This example shows the minimal setup for AWS integration without Kubernetes

terraform {
required_version = ">= 1.0"
required_version = ">= 1.8"

required_providers {
aws = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ aws_region = "ap-southeast-2"
# S3 bucket name for storing scan results (optional, provided by Nullify if needed)
# s3_bucket_name = "nullify-scan-results-acme-corp"

# KMS key ARN for encryption (optional, provided by Nullify if needed)
# kms_key_arn = "arn:aws:kms:ap-southeast-2:123456789012:key/12345678-1234-1234-1234-123456789012"
# KMS ARN from the Nullify configure page (optional). A key ARN or an alias ARN both work.
# The key lives in Nullify's account, not yours.
# kms_key_arn = "arn:aws:kms:<nullify-region>:<nullify-account-id>:key/<from-the-configure-page>"

# Tags to apply to all AWS resources
tags = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ variable "tags" {

variable "kms_key_arn" {
type = string
description = "The ARN of the KMS key for key management operations (optional, provided by Nullify if needed)"
description = "The KMS ARN shown on the Nullify configure page (optional): a key ARN or an alias ARN"
default = ""
}
}
72 changes: 72 additions & 0 deletions aws-integration-setup/terraform-v6/examples/managed-scan/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Managed EKS scan: Nullify lists Kubernetes resources from its own compute
# through an EKS access entry. Nothing runs in the cluster.

terraform {
required_version = ">= 1.10"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.20"
}
}
}

locals {
cluster_name = split("/", var.eks_cluster_arn)[1]
cluster_region = split(":", var.eks_cluster_arn)[3]
}

provider "aws" {
region = local.cluster_region
}

data "aws_eks_cluster" "this" {
name = local.cluster_name
}

ephemeral "aws_eks_cluster_auth" "this" {
name = local.cluster_name
}

provider "kubernetes" {
host = data.aws_eks_cluster.this.endpoint
cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data)
token = ephemeral.aws_eks_cluster_auth.this.token
}

module "nullify_aws_integration" {
source = "../../modules/nullify-aws-integration"
customer_name = var.customer_name
external_id = var.external_id
nullify_role_arn = var.nullify_role_arn
aws_region = local.cluster_region
tags = var.tags
}

module "eks_managed_scan_access" {
source = "../../modules/eks-managed-scan-access"
principal_arn = module.nullify_aws_integration.role_arn
principal_unique_id = module.nullify_aws_integration.role_unique_id
cluster_arns = [var.eks_cluster_arn]
nullify_region = var.nullify_region
authorization = var.authorization
kubernetes_group_name = var.kubernetes_group_name
tags = var.tags
}

# Creating the ClusterRole needs rights to grant every permission in it, which
# in practice means cluster-admin. Set manage_rbac = false when Helm, Flux or
# Argo CD applies the nullify-readonly ClusterRole instead.
module "k8s_resources" {
source = "../../modules/k8s-resources"
count = var.manage_rbac ? 1 : 0

enable_collector = false
enable_managed_scan_rbac = true
managed_scan_kubernetes_group = var.kubernetes_group_name
}
Loading
Loading