diff --git a/aws-integration-setup/terraform-v6/README.md b/aws-integration-setup/terraform-v6/README.md new file mode 100644 index 0000000..697e5c0 --- /dev/null +++ b/aws-integration-setup/terraform-v6/README.md @@ -0,0 +1,108 @@ +# Nullify AWS Integration Terraform (AWS Provider v6) + +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. + +## What Changed 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: + +```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 = "..." +} +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) +- AWS provider ~> 6.0 +- Kubernetes provider ~> 2.20 + +## 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 +├── examples/ +│ ├── basic/ # AWS IAM only example +│ └── multi-cluster-complete/ # Full multi-cluster EKS example +├── versions.tf # AWS ~> 6.0, Kubernetes ~> 2.20 +├── variables.tf +├── main.tf +├── outputs.tf +├── providers.tf +├── terraform.tfvars.example +└── README.md +``` + +## Quick Start + +### 1. AWS-Only Integration + +```bash +cd examples/basic/ +cp terraform.tfvars.example terraform.tfvars +# Edit terraform.tfvars with your values +terraform init && terraform apply +``` + +### 2. Multi-Cluster EKS Integration + +```bash +cd examples/multi-cluster-complete/ +cp terraform.tfvars.example terraform.tfvars +# Edit with your cluster ARNs and values +terraform init && terraform apply +``` + +## Required Variables + +- `customer_name`: Your company/customer name (used in resource naming) +- `external_id`: External ID for cross-account access (provided by Nullify configure page) +- `nullify_role_arn`: Nullify's cross-account role ARN (provided by Nullify configure page) + +## Optional Variables + +- `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) +- `enable_kubernetes_integration`: Set to `true` for EKS integration +- `eks_cluster_arns`: List of EKS cluster ARNs to integrate with +- `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) +- `tags`: Resource tags + +## Validation + +```bash +terraform fmt -recursive +terraform validate +terraform plan +``` diff --git a/aws-integration-setup/terraform-v6/examples/basic/main.tf b/aws-integration-setup/terraform-v6/examples/basic/main.tf new file mode 100644 index 0000000..97a6ee3 --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/basic/main.tf @@ -0,0 +1,47 @@ +# Basic Example: Nullify AWS Integration (AWS resources only) +# This example shows the minimal setup for AWS integration without Kubernetes + +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +module "nullify_aws_integration" { + source = "../../modules/nullify-aws-integration" + + # Required variables + customer_name = var.customer_name + external_id = var.external_id + nullify_role_arn = var.nullify_role_arn + + # AWS Configuration + aws_region = var.aws_region + s3_bucket_name = var.s3_bucket_name + kms_key_arn = var.kms_key_arn + + # Kubernetes integration disabled - no Kubernetes provider needed + enable_kubernetes_integration = false + + # Custom tags + tags = var.tags +} + +output "role_arn" { + description = "ARN of the created IAM role" + value = module.nullify_aws_integration.role_arn +} + +output "deployment_summary" { + description = "Summary of the deployment" + value = module.nullify_aws_integration.deployment_summary +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/examples/basic/terraform.tfvars.example b/aws-integration-setup/terraform-v6/examples/basic/terraform.tfvars.example new file mode 100644 index 0000000..4e44e69 --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/basic/terraform.tfvars.example @@ -0,0 +1,28 @@ +# Basic Example - AWS-only integration (no Kubernetes) +# Copy this file to terraform.tfvars and fill in your values. + +# REQUIRED: Customer name (provided by Nullify) +customer_name = "acme-corp" + +# REQUIRED: External ID for cross-account role assumption (provided by Nullify) +external_id = "your-external-id-from-nullify" + +# REQUIRED: Nullify cross-account role ARN (provided by Nullify) +nullify_role_arn = "arn:aws:iam::123456789012:role/NullifyRole" + +# AWS region where resources are deployed +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" + +# Tags to apply to all AWS resources +tags = { + Environment = "production" + Team = "security" + Project = "nullify-integration" + ManagedBy = "Terraform" +} diff --git a/aws-integration-setup/terraform-v6/examples/basic/variables.tf b/aws-integration-setup/terraform-v6/examples/basic/variables.tf new file mode 100644 index 0000000..7b4741d --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/basic/variables.tf @@ -0,0 +1,49 @@ +variable "customer_name" { + type = string + description = "The name of the customer to create the role for" + default = "acme-corp" + + validation { + condition = can(regex("^[a-zA-Z][a-zA-Z0-9_-]*$", var.customer_name)) + error_message = "Customer name must start with a letter and can only contain letters, numbers, underscores, and hyphens" + } +} + +variable "external_id" { + type = string + description = "The external ID for the role (provided by Nullify)" +} + +variable "nullify_role_arn" { + type = string + description = "The Nullify cross-account role ARN" +} + +variable "aws_region" { + type = string + description = "The AWS region where resources are deployed" + default = "ap-southeast-2" +} + +variable "s3_bucket_name" { + type = string + description = "The name of the S3 bucket for storing scan results (optional)" + default = "" +} + +variable "tags" { + type = map(string) + description = "Tags to apply to AWS resources" + default = { + Environment = "production" + Team = "security" + Project = "nullify-integration" + ManagedBy = "Terraform" + } +} + +variable "kms_key_arn" { + type = string + description = "The ARN of the KMS key for key management operations (optional, provided by Nullify if needed)" + default = "" +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/main.tf b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/main.tf new file mode 100644 index 0000000..4195bad --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/main.tf @@ -0,0 +1,94 @@ +terraform { + required_version = ">= 1.10" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.20" + } + } +} + +provider "aws" { + region = var.aws_region +} + +data "aws_eks_cluster" "primary" { + name = element(split("/", var.eks_cluster_arns[0]), length(split("/", var.eks_cluster_arns[0])) - 1) +} + +# aws_eks_cluster_auth was removed as a data source in AWS provider v6. +# Use the ephemeral resource instead (requires Terraform >= 1.10). +ephemeral "aws_eks_cluster_auth" "primary" { + name = element(split("/", var.eks_cluster_arns[0]), length(split("/", var.eks_cluster_arns[0])) - 1) +} + +data "aws_eks_cluster" "secondary" { + name = element(split("/", var.eks_cluster_arns[1]), length(split("/", var.eks_cluster_arns[1])) - 1) +} + +ephemeral "aws_eks_cluster_auth" "secondary" { + name = element(split("/", var.eks_cluster_arns[1]), length(split("/", var.eks_cluster_arns[1])) - 1) +} + +provider "kubernetes" { + alias = "primary" + host = data.aws_eks_cluster.primary.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.primary.certificate_authority[0].data) + token = ephemeral.aws_eks_cluster_auth.primary.token +} + +provider "kubernetes" { + alias = "secondary" + host = data.aws_eks_cluster.secondary.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.secondary.certificate_authority[0].data) + token = ephemeral.aws_eks_cluster_auth.secondary.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 = var.aws_region + s3_bucket_name = var.s3_bucket_name + kms_key_arn = var.kms_key_arn + + # Multiple Kubernetes Clusters Configuration + enable_kubernetes_integration = true + eks_cluster_arns = var.eks_cluster_arns + kubernetes_namespace = var.kubernetes_namespace + tags = var.tags +} + +module "k8s_resources_primary" { + source = "../../modules/k8s-resources" + providers = { + kubernetes = kubernetes.primary + } + iam_role_arn = module.nullify_aws_integration.role_arn + s3_bucket_name = var.s3_bucket_name + kms_key_arn = var.kms_key_arn + aws_region = var.aws_region + kubernetes_namespace = var.kubernetes_namespace + cronjob_schedule = var.cronjob_schedule + collector_image = var.collector_image +} + +module "k8s_resources_secondary" { + source = "../../modules/k8s-resources" + providers = { + kubernetes = kubernetes.secondary + } + iam_role_arn = module.nullify_aws_integration.role_arn + s3_bucket_name = var.s3_bucket_name + kms_key_arn = var.kms_key_arn + aws_region = var.aws_region + kubernetes_namespace = var.kubernetes_namespace + cronjob_schedule = var.cronjob_schedule + collector_image = var.collector_image +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/outputs.tf b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/outputs.tf new file mode 100644 index 0000000..1ddbfea --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/outputs.tf @@ -0,0 +1,38 @@ +# AWS Integration Outputs +output "iam_role_arn" { + description = "ARN of the IAM Role with cross-account read access" + value = module.nullify_aws_integration.role_arn +} + +output "deployment_summary" { + description = "Summary of the complete Nullify integration deployment" + value = module.nullify_aws_integration.deployment_summary +} + +output "cluster_integration_summary" { + description = "Summary of all cluster integrations" + value = module.nullify_aws_integration.cluster_integration_summary +} + +# Kubernetes Resources +output "k8s_resources_primary" { + description = "Kubernetes resources deployed to primary cluster" + value = { + namespace_name = module.k8s_resources_primary.namespace_name + service_account_name = module.k8s_resources_primary.service_account_name + cluster_role_name = module.k8s_resources_primary.cluster_role_name + cluster_role_binding_name = module.k8s_resources_primary.cluster_role_binding_name + cronjob_name = module.k8s_resources_primary.cronjob_name + } +} + +output "k8s_resources_secondary" { + description = "Kubernetes resources deployed to secondary cluster" + value = { + namespace_name = module.k8s_resources_secondary.namespace_name + service_account_name = module.k8s_resources_secondary.service_account_name + cluster_role_name = module.k8s_resources_secondary.cluster_role_name + cluster_role_binding_name = module.k8s_resources_secondary.cluster_role_binding_name + cronjob_name = module.k8s_resources_secondary.cronjob_name + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/terraform.tfvars.example b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/terraform.tfvars.example new file mode 100644 index 0000000..7b0e6f5 --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/terraform.tfvars.example @@ -0,0 +1,42 @@ +# Multi-Cluster Example - EKS integration with multiple clusters +# Copy this file to terraform.tfvars and fill in your values. + +# REQUIRED: Customer name (provided by Nullify) +customer_name = "acme-corp" + +# REQUIRED: External ID for cross-account role assumption (provided by Nullify) +external_id = "your-external-id-from-nullify" + +# REQUIRED: Nullify cross-account role ARN (provided by Nullify) +nullify_role_arn = "arn:aws:iam::123456789012:role/NullifyRole" + +# REQUIRED: EKS cluster ARNs to integrate (must provide at least 2 for this example) +eks_cluster_arns = [ + "arn:aws:eks:ap-southeast-2:123456789012:cluster/primary-cluster", + "arn:aws:eks:us-east-1:123456789012:cluster/secondary-cluster" +] + +# Primary AWS region (where IAM resources are created) +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" + +# Kubernetes namespace for Nullify resources +kubernetes_namespace = "nullify" + +# Cron schedule for the collector +cronjob_schedule = "0 0 * * *" + +# Collector image +collector_image = "nullify/k8s-collector:latest" + +# Tags to apply to all AWS resources +tags = { + ManagedBy = "Terraform" + Purpose = "NullifyIntegration" + Environment = "production" +} diff --git a/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/variables.tf b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/variables.tf new file mode 100644 index 0000000..3b2b42b --- /dev/null +++ b/aws-integration-setup/terraform-v6/examples/multi-cluster-complete/variables.tf @@ -0,0 +1,77 @@ +# Simplified Multi-Cluster Example Variables + +variable "customer_name" { + type = string + description = "The name of the customer to create the role for" + + validation { + condition = can(regex("^[a-zA-Z][a-zA-Z0-9_-]*$", var.customer_name)) + error_message = "Customer name must start with a letter and can only contain letters, numbers, underscores, and hyphens" + } +} + +variable "external_id" { + type = string + description = "The external ID for the role (provided by Nullify)" +} + +variable "nullify_role_arn" { + type = string + description = "The Nullify cross-account role ARN" +} + +variable "eks_cluster_arns" { + type = list(string) + description = "List of ARNs of EKS clusters to integrate with (can be from different regions)" + + validation { + condition = length(var.eks_cluster_arns) > 0 + error_message = "You must provide at least one cluster ARN." + } +} + +variable "aws_region" { + type = string + description = "The primary AWS region for the integration (where IAM resources are created)" + default = "ap-southeast-2" +} + +variable "s3_bucket_name" { + type = string + description = "The name of the S3 bucket for storing scan results (optional)" + default = "" +} + +variable "kubernetes_namespace" { + type = string + description = "The Kubernetes namespace for Nullify resources" + default = "nullify" +} + +variable "cronjob_schedule" { + type = string + description = "Cron schedule for the Kubernetes collector job" + default = "*/5 * * * *" +} + +variable "collector_image" { + type = string + description = "Docker image for the Kubernetes collector" + default = "nullify/k8s-collector:latest" +} + +variable "tags" { + type = map(string) + description = "Tags to apply to AWS resources" + default = { + ManagedBy = "Terraform" + Purpose = "NullifyIntegration" + Environment = "Production" + } +} + +variable "kms_key_arn" { + type = string + description = "The ARN of the KMS key for key management operations (optional, provided by Nullify if needed)" + default = "" +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/main.tf b/aws-integration-setup/terraform-v6/main.tf new file mode 100644 index 0000000..d34aa41 --- /dev/null +++ b/aws-integration-setup/terraform-v6/main.tf @@ -0,0 +1,19 @@ +module "nullify_aws_integration" { + source = "./modules/nullify-aws-integration" + + # Required variables + customer_name = var.customer_name + external_id = var.external_id + nullify_role_arn = var.nullify_role_arn + + # Optional variables with defaults + aws_region = var.aws_region + s3_bucket_name = var.s3_bucket_name + kms_key_arn = var.kms_key_arn + enable_kubernetes_integration = var.enable_kubernetes_integration + eks_cluster_arns = var.eks_cluster_arns + kubernetes_namespace = var.kubernetes_namespace + service_account_name = var.service_account_name + cronjob_schedule = var.cronjob_schedule + tags = var.tags +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/k8s-resources/main.tf b/aws-integration-setup/terraform-v6/modules/k8s-resources/main.tf new file mode 100644 index 0000000..238b300 --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/k8s-resources/main.tf @@ -0,0 +1,225 @@ +resource "kubernetes_namespace" "nullify" { + metadata { + name = var.kubernetes_namespace + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/managed-by" = "terraform" + } + } +} + +resource "kubernetes_service_account" "nullify_collector_sa" { + metadata { + name = var.service_account_name + namespace = kubernetes_namespace.nullify.metadata[0].name + + annotations = { + "eks.amazonaws.com/role-arn" = var.iam_role_arn + } + + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/component" = "k8s-collector" + "app.kubernetes.io/managed-by" = "terraform" + } + } +} + +resource "kubernetes_cluster_role" "nullify_readonly_role" { + metadata { + name = "nullify-k8s-collector-role" + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/component" = "rbac" + "app.kubernetes.io/managed-by" = "terraform" + } + } + + rule { + api_groups = [""] + resources = [ + "pods", + "services", + "endpoints", + "namespaces", + "nodes", + "persistentvolumes", + "persistentvolumeclaims", + "serviceaccounts", + ] + verbs = ["get", "list"] + } + + rule { + api_groups = ["networking.k8s.io"] + resources = ["ingresses", "networkpolicies"] + verbs = ["get", "list"] + } + + rule { + api_groups = ["apps"] + resources = [ + "deployments", + "replicasets", + "statefulsets", + "daemonsets" + ] + verbs = ["get", "list"] + } + + rule { + api_groups = ["rbac.authorization.k8s.io"] + resources = [ + "roles", + "rolebindings", + "clusterroles", + "clusterrolebindings" + ] + verbs = ["get", "list"] + } + + rule { + api_groups = ["storage.k8s.io"] + resources = ["storageclasses"] + verbs = ["get", "list"] + } + + rule { + api_groups = ["batch"] + resources = ["jobs", "cronjobs"] + verbs = ["get", "list"] + } + + rule { + api_groups = ["autoscaling"] + resources = ["horizontalpodautoscalers"] + verbs = ["get", "list"] + } + + rule { + api_groups = ["policy"] + resources = ["poddisruptionbudgets"] + verbs = ["get", "list"] + } + + rule { + api_groups = ["apiextensions.k8s.io"] + resources = ["customresourcedefinitions"] + verbs = ["get", "list"] + } +} + +resource "kubernetes_cluster_role_binding" "nullify_collector_binding" { + metadata { + name = "nullify-k8s-collector-binding" + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/component" = "rbac" + "app.kubernetes.io/managed-by" = "terraform" + } + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role.nullify_readonly_role.metadata[0].name + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.nullify_collector_sa.metadata[0].name + namespace = kubernetes_namespace.nullify.metadata[0].name + } +} + +resource "kubernetes_cron_job_v1" "k8s_collector" { + metadata { + name = "k8s-info-collector" + namespace = kubernetes_namespace.nullify.metadata[0].name + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/component" = "k8s-collector" + } + } + + spec { + schedule = var.cronjob_schedule + concurrency_policy = "Forbid" + successful_jobs_history_limit = 3 + failed_jobs_history_limit = 1 + + job_template { + metadata { + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/component" = "k8s-collector" + } + } + + spec { + active_deadline_seconds = 21600 + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "nullify" + "app.kubernetes.io/component" = "k8s-collector" + } + } + + spec { + service_account_name = kubernetes_service_account.nullify_collector_sa.metadata[0].name + restart_policy = "OnFailure" + + container { + name = "k8s-collector" + image = var.collector_image + + env { + name = "NULLIFY_S3_BUCKET_NAME" + value = var.s3_bucket_name + } + + env { + name = "NULLIFY_S3_KEY_PREFIX" + value = "k8s-collector" + } + + env { + name = "AWS_REGION" + value = var.aws_region + } + + dynamic "env" { + for_each = var.kms_key_arn != "" ? [1] : [] + content { + name = "NULLIFY_KMS_KEY_ARN" + value = var.kms_key_arn + } + } + + dynamic "env" { + for_each = var.enable_debug ? [1] : [] + content { + name = "ENABLE_DEBUG_LOG" + value = "true" + } + } + + resources { + requests = { + memory = "256Mi" + cpu = "100m" + } + limits = { + memory = "512Mi" + cpu = "500m" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/k8s-resources/outputs.tf b/aws-integration-setup/terraform-v6/modules/k8s-resources/outputs.tf new file mode 100644 index 0000000..feea808 --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/k8s-resources/outputs.tf @@ -0,0 +1,24 @@ +output "namespace_name" { + description = "Name of the created Kubernetes namespace" + value = kubernetes_namespace.nullify.metadata[0].name +} + +output "service_account_name" { + description = "Name of the created Kubernetes service account" + value = kubernetes_service_account.nullify_collector_sa.metadata[0].name +} + +output "cluster_role_name" { + description = "Name of the created cluster role" + value = kubernetes_cluster_role.nullify_readonly_role.metadata[0].name +} + +output "cluster_role_binding_name" { + description = "Name of the created cluster role binding" + value = kubernetes_cluster_role_binding.nullify_collector_binding.metadata[0].name +} + +output "cronjob_name" { + description = "Name of the created CronJob" + value = kubernetes_cron_job_v1.k8s_collector.metadata[0].name +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/k8s-resources/providers.tf b/aws-integration-setup/terraform-v6/modules/k8s-resources/providers.tf new file mode 100644 index 0000000..51aeaa9 --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/k8s-resources/providers.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.20" + } + } +} diff --git a/aws-integration-setup/terraform-v6/modules/k8s-resources/variables.tf b/aws-integration-setup/terraform-v6/modules/k8s-resources/variables.tf new file mode 100644 index 0000000..bfb724e --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/k8s-resources/variables.tf @@ -0,0 +1,52 @@ +variable "iam_role_arn" { + type = string + description = "The ARN of the IAM role for the service account annotation" +} + +variable "service_account_name" { + type = string + description = "The name of the Kubernetes service account" + default = "nullify-k8s-collector-sa" +} + +variable "s3_bucket_name" { + type = string + description = "The name of the S3 bucket for storing scan results" + default = "" +} + +variable "aws_region" { + type = string + description = "The AWS region" + default = "ap-southeast-2" +} + +variable "kubernetes_namespace" { + type = string + description = "The Kubernetes namespace for Nullify resources" + default = "nullify" +} + +variable "collector_image" { + type = string + description = "Docker image for the Kubernetes collector" + default = "nullify/k8s-collector:latest" +} + +variable "cronjob_schedule" { + type = string + description = "Cron schedule for the Kubernetes collector job" + default = "0 0 * * *" +} + +variable "kms_key_arn" { + type = string + description = "The ARN of the KMS key for key management operations (optional)" + default = "" +} + +variable "enable_debug" { + type = bool + description = "Enable debug logging for troubleshooting" + default = false +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/data.tf b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/data.tf new file mode 100644 index 0000000..46cbf2a --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/data.tf @@ -0,0 +1,597 @@ +data "aws_caller_identity" "current" {} + +data "aws_eks_cluster" "clusters" { + count = var.enable_kubernetes_integration ? length(var.eks_cluster_arns) : 0 + name = element(split("/", var.eks_cluster_arns[count.index]), length(split("/", var.eks_cluster_arns[count.index])) - 1) +} + +locals { + all_clusters_info = var.enable_kubernetes_integration ? [ + for i, cluster in data.aws_eks_cluster.clusters : { + oidc_id = split("/", cluster.identity[0].oidc[0].issuer)[4] + region = split(":", var.eks_cluster_arns[i])[3] # Extract region from ARN + } + ] : [] + + all_oidc_ids = [for cluster in local.all_clusters_info : cluster.oidc_id] + eks_oidc_provider_arns = var.enable_kubernetes_integration ? [ + for cluster in local.all_clusters_info : + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/oidc.eks.${cluster.region}.amazonaws.com/id/${cluster.oidc_id}" + ] : [] +} + +data "aws_iam_policy_document" "assume_role_policy" { + statement { + effect = "Allow" + principals { + type = "AWS" + identifiers = [local.nullify_role_arn] + } + actions = ["sts:AssumeRole"] + condition { + test = "StringEquals" + variable = "sts:ExternalId" + values = [var.external_id] + } + } + + dynamic "statement" { + for_each = var.enable_kubernetes_integration ? local.all_clusters_info : [] + content { + effect = "Allow" + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/oidc.eks.${statement.value.region}.amazonaws.com/id/${statement.value.oidc_id}"] + } + actions = ["sts:AssumeRoleWithWebIdentity"] + condition { + test = "StringEquals" + variable = "oidc.eks.${statement.value.region}.amazonaws.com/id/${statement.value.oidc_id}:sub" + values = [local.oidc_subject] + } + } + } +} + +data "aws_iam_policy_document" "readonly_policy_part1" { + statement { + effect = "Allow" + actions = [ + "a4b:List*", + "access-analyzer:GetAccessPreview", + "access-analyzer:GetAnalyzedResource", + "access-analyzer:GetAnalyzer", + "access-analyzer:GetArchiveRule", + "access-analyzer:GetFinding", + "access-analyzer:GetFindingsStatistics", + "access-analyzer:GetGeneratedPolicy", + "access-analyzer:List*", + "access-analyzer:ValidatePolicy", + "account:GetAccountInformation", + "account:GetAlternateContact", + "account:GetContactInformation", + "account:GetPrimaryEmail", + "account:GetRegionOptStatus", + "account:ListRegions", + "acm-pca:Describe*", + "acm-pca:List*", + "acm:Describe*", + "acm:List*", + "aiops:List*", + "airflow:List*", + "amplify:GetBranch", + "amplify:GetDomainAssociation", + "amplify:GetWebhook", + "amplify:List*", + "aoss:BatchGetLifecyclePolicy", + "aoss:BatchGetVpcEndpoint", + "aoss:GetAccessPolicy", + "aoss:GetAccountSettings", + "aoss:GetPoliciesStats", + "aoss:GetSecurityConfig", + "aoss:GetSecurityPolicy", + "aoss:List*", + "apigateway:GET", + "appconfig:List*", + "appfabric:List*", + "appflow:List*", + "application-autoscaling:Describe*", + "application-autoscaling:ListTagsForResource", + "application-signals:List*", + "application-signals:ListTagsForResource", + "applicationinsights:List*", + "appmesh:Describe*", + "appmesh:List*", + "apprunner:DescribeAutoScalingConfiguration", + "apprunner:DescribeCustomDomains", + "apprunner:DescribeObservabilityConfiguration", + "apprunner:DescribeService", + "apprunner:DescribeVpcConnector", + "apprunner:DescribeVpcIngressConnection", + "apprunner:DescribeWebAclForService", + "apprunner:List*", + "appstream:Describe*", + "appstream:List*", + "appsync:List*", + "apptest:ListTagsForResource", + "apptest:List*", + "aps:DescribeAlertManagerDefinition", + "aps:DescribeLoggingConfiguration", + "aps:DescribeRuleGroupsNamespace", + "aps:DescribeScraper", + "aps:DescribeWorkspace", + "aps:List*", + "aps:ListWorkspaces", + "aps:QueryMetrics", + "arc-zonal-shift:GetAutoshiftObserverNotificationStatus", + "arc-zonal-shift:ListAutoshifts", + "arc-zonal-shift:ListManagedResources", + "arc-zonal-shift:ListZonalShifts", + "artifact:ListAgreements", + "artifact:ListCustomerAgreements", + "artifact:ListReports", + "athena:List*", + "auditmanager:List*", + "auditmanager:ListTagsForResource", + "auditmanager:ValidateAssessmentReportIntegrity", + "autoscaling-plans:Describe*", + "autoscaling:Describe*", + "backup-gateway:List*", + "backup:Describe*", + "backup:List*", + "batch:Describe*", + "batch:List*", + "bedrock:List*", + "billingconductor:List*", + "cloud9:Describe*", + "cloud9:List*", + "clouddirectory:BatchRead", + "clouddirectory:List*", + "clouddirectory:LookupPolicy", + "cloudformation:Describe*", + "cloudformation:Detect*", + "cloudformation:Estimate*", + "cloudformation:List*", + "cloudformation:ValidateTemplate", + "cloudfront-keyvaluestore:Describe*", + "cloudfront-keyvaluestore:List*", + "cloudfront:Describe*", + "cloudfront:List*", + "cloudhsm:Describe*", + "cloudhsm:List*", + "cloudsearch:Describe*", + "cloudsearch:List*", + "cloudtrail:Describe*", + "cloudtrail:Get*", + "cloudtrail:List*", + "cloudtrail:LookupEvents", + "cloudwatch:Describe*", + "cloudwatch:GenerateQuery", + "codeartifact:List*", + "codebuild:Describe*", + "cognito-identity:Describe*", + "cognito-identity:GetIdentityPoolAnalytics", + "cognito-identity:GetIdentityPoolDailyAnalytics", + "cognito-identity:GetIdentityPoolRoles", + "cognito-identity:GetIdentityProviderDailyAnalytics", + "cognito-identity:List*", + "cognito-identity:Lookup*", + "cognito-idp:AdminList*", + "cognito-idp:Describe*", + "cognito-idp:List*", + "cognito-sync:Describe*", + "cognito-sync:List*", + "cognito-sync:QueryRecords", + "config:SelectResourceConfig", + "devops-guru:List*", + "discovery:Describe*", + "discovery:List*", + "dynamodb:Describe*", + "dynamodb:List*", + "ecr-public:BatchCheckLayerAvailability", + "ecr-public:DescribeImages", + "ecr-public:DescribeImageTags", + "ecr-public:DescribeRegistries", + "ecr-public:DescribeRepositories", + "ecr-public:GetRepositoryCatalogData", + "ecr-public:GetRepositoryPolicy", + "ecr-public:ListTagsForResource", + "ecr:BatchCheck*", + "ecr:Describe*", + "ecr:List*", + "ecs:Describe*", + "ecs:List*", + "eks:Describe*", + "eks:List*", + "elastic-inference:Describe*", + "elastic-inference:List*", + "elasticache:Describe*", + "elasticache:List*", + "elasticbeanstalk:Check*", + "elasticbeanstalk:Describe*", + "elasticbeanstalk:List*", + "elasticfilesystem:Describe*", + "elasticfilesystem:ListTagsForResource", + "elasticloadbalancing:Describe*", + "elasticmapreduce:Describe*", + "elasticmapreduce:GetBlockPublicAccessConfiguration", + "elasticmapreduce:List*", + "elasticmapreduce:View*", + "elastictranscoder:List*", + "elastictranscoder:Read*", + "events:Describe*", + "events:List*", + "events:ListTagsForResource", + "emr-containers:Describe*", + "emr-containers:List*", + "glue:Describe*", + "glue:List*", + "iam:Get*", + "iam:List*", + "iam:Simulate*", + "iam:SimulateCustomPolicy", + "iam:SimulatePrincipalPolicy", + "identity-sync:ListSyncFilters", + "identitystore:Describe*", + "iotfleetwise:List*", + "iotroborunner:List*", + "iotsitewise:Describe*", + "iotsitewise:List*", + "iotwireless:List*", + "ivs:List*", + "ivschat:List*", + "kafka:Describe*", + "kafka:List*", + "kafkaconnect:List*", + "kendra:BatchGetDocumentStatus", + "kendra:DescribeDataSource", + "kendra:DescribeExperience", + "kendra:DescribeFaq", + "kendra:DescribeIndex", + "kendra:DescribePrincipalMapping", + "kendra:DescribeQuerySuggestionsBlockList", + "kendra:DescribeQuerySuggestionsConfig", + "kendra:DescribeThesaurus", + "kendra:List*", + "kinesis:Describe*", + "kinesis:List*", + "kinesisanalytics:Describe*", + "kinesisanalytics:Discover*", + "kinesisanalytics:List*", + "kinesisvideo:Describe*", + "kinesisvideo:List*", + "kms:Describe*", + "kms:List*", + "lakeformation:Describe*", + "lakeformation:Get*", + "lakeformation:List*", + "lakeformation:Search*", + "lambda:Get*", + "lambda:List*", + "ec2:Describe*", + "ec2:GetTransitGateway*", + "ec2:SearchTransitGateway*" + ] + resources = ["*"] + } +} + +data "aws_iam_policy_document" "readonly_policy_part2" { + statement { + effect = "Allow" + actions = [ + "globalaccelerator:Describe*", + "globalaccelerator:List*", + "guardduty:Describe*", + "guardduty:Get*", + "guardduty:List*", + "launchwizard:Describe*", + "launchwizard:GetWorkload", + "launchwizard:List*", + "license-manager:List*", + "lightsail:Get*", + "logs:Describe*", + "logs:FilterLogEvents", + "logs:List*", + "logs:StartLiveTail", + "logs:StartQuery", + "logs:StopLiveTail", + "logs:StopQuery", + "logs:TestMetricFilter", + "machinelearning:Describe*", + "managedblockchain:GetMember", + "managedblockchain:GetNetwork", + "managedblockchain:GetNode", + "managedblockchain:GetProposal", + "managedblockchain:List*", + "mediaconnect:Describe*", + "mediaconnect:List*", + "mediaconvert:List*", + "medialive:Describe*", + "medialive:Get*", + "medialive:List*", + "mediapackage-vod:Describe*", + "mediapackage-vod:List*", + "mediapackage:Describe*", + "mediapackage:List*", + "mediapackagev2:GetChannel", + "mediapackagev2:GetChannelGroup", + "mediapackagev2:GetChannelPolicy", + "mediapackagev2:GetHeadObject", + "mediapackagev2:GetOriginEndpoint", + "mediapackagev2:GetOriginEndpointPolicy", + "mediapackagev2:List*", + "mediastore:Describe*", + "mediastore:List*", + "memorydb:Describe*", + "memorydb:List*", + "mq:Describe*", + "mq:List*", + "network-firewall:Describe*", + "network-firewall:List*", + "networkmanager:DescribeGlobalNetworks", + "networkmanager:Get*", + "networkmanager:List*", + "neptune:Describe*", + "neptune:List*", + "nimble:List*", + "notifications-contacts:List*", + "notifications:Get*", + "notifications:List*", + "notifications:GetManagedNotificationEvent", + "notifications:GetNotificationConfiguration", + "notifications:GetNotificationsAccessForOrganization", + "notifications:GetNotificationEvent", + "notifications:List*", + "one:ListUsers", + "opsworks-cm:Describe*", + "opsworks-cm:List*", + "opsworks:Describe*", + "organizations:Describe*", + "organizations:List*", + "outposts:List*", + "personalize:Describe*", + "personalize:List*", + "pi:DescribeDimensionKeys", + "pi:ListAvailableResourceDimensions", + "pi:ListAvailableResourceMetrics", + "pipes:DescribePipe", + "pipes:ListPipes", + "polly:Describe*", + "polly:List*", + "pricing:DescribeServices", + "pricing:ListPriceLists", + "qbusiness:List*", + "ram:List*", + "rbin:List*", + "rds:Describe*", + "rds:List*", + "redshift-serverless:List*", + "redshift:Describe*", + "redshift:List*", + "redshift:View*", + "rekognition:Describe*", + "rekognition:Detect*", + "rekognition:List*", + "resource-explorer-2:List*", + "resource-groups:List*", + "resource-groups:Search*", + "robomaker:BatchDescribe*", + "robomaker:Describe*", + "robomaker:List*", + "route53-recovery-cluster:Get*", + "route53-recovery-cluster:List*", + "route53-recovery-control-config:Describe*", + "route53-recovery-control-config:Get*", + "route53-recovery-control-config:List*", + "route53-recovery-readiness:Get*", + "route53-recovery-readiness:List*", + "route53:Get*", + "route53:List*", + "route53:Test*", + "route53domains:Check*", + "route53domains:Get*", + "route53domains:List*", + "route53domains:View*", + "route53profiles:Get*", + "route53profiles:List*", + "route53resolver:Get*", + "route53resolver:List*", + "rum:GetAppMonitor", + "rum:GetAppMonitorData", + "rum:ListAppMonitors", + "s3-object-lambda:List*", + "s3-outposts:Get*", + "s3-outposts:List*", + "s3:Describe*", + "s3:List*", + "s3:GetBucketLocation", + "sagemaker:Describe*", + "sagemaker:List*", + "scheduler:List*", + "schemas:Describe*", + "schemas:Get*", + "schemas:List*", + "schemas:Search*", + "sdb:List*", + "sdb:Select*", + "secretsmanager:GetResourcePolicy", + "secretsmanager:List*", + "securityhub:Describe*", + "securityhub:List*", + "securitylake:ListDataLakeExceptions", + "securitylake:ListDataLakes", + "securitylake:ListLogSources", + "securitylake:ListSubscribers", + "securitylake:ListTagsForResource", + "serverlessrepo:Get*", + "serverlessrepo:List*", + "serverlessrepo:SearchApplications", + "servicecatalog:Describe*", + "servicecatalog:List*", + "servicecatalog:Scan*", + "servicecatalog:Search*", + "servicediscovery:DiscoverInstances", + "servicediscovery:DiscoverInstancesRevision", + "servicediscovery:List*", + "servicequotas:List*", + "ses:Describe*", + "ses:List*", + "shield:Describe*", + "shield:List*", + "signer:DescribeSigningJob", + "signer:List*", + "signin:ListTrustedIdentityPropagationApplicationsForConsole", + "sms-voice:Describe*", + "sms-voice:List*", + "snowball:Describe*", + "snowball:List*", + "sns:Check*", + "sns:List*", + "sqs:List*", + "ssm-contacts:List*", + "ssm-sap:List*", + "ssm-quicksetup:List*", + "ssm:List*", + "sso-directory:List*", + "sso:List*", + "sso:Search*", + "states:Describe*", + "states:List*", + "states:ValidateStateMachineDefinition", + "storagegateway:Describe*", + "storagegateway:List*", + "sts:GetAccessKeyInfo", + "sts:GetCallerIdentity", + "sts:GetSessionToken", + "support:Describe*", + "support:SearchForCases", + "tag:Describe*", + "tag:Get*", + "tax:ListTaxRegistrations", + "timestream:Describe*", + "tnb:List*", + "transcribe:List*", + "transfer:Describe*", + "transfer:List*", + "transfer:TestIdentityProvider", + "translate:DescribeTextTranslationJob", + "translate:ListParallelData", + "translate:ListTerminologies", + "translate:ListTextTranslationJobs", + "trustedadvisor:Describe*", + "trustedadvisor:List*", + "verifiedpermissions:IsAuthorized", + "verifiedpermissions:IsAuthorizedWithToken", + "verifiedpermissions:List*", + "vpc-lattice:Get*", + "vpc-lattice:List*", + "waf-regional:List*", + "waf:List*", + "wafv2:CheckCapacity", + "wafv2:Describe*", + "wafv2:List*", + "wellarchitected:ExportLens", + "wellarchitected:Get*", + "wellarchitected:List*", + "workdocs:CheckAlias", + "workdocs:Describe*", + "workmail:Describe*", + "workmail:List*", + "workmail:Search*", + "workspaces-web:GetBrowserSettings", + "workspaces-web:GetIdentityProvider", + "workspaces-web:GetNetworkSettings", + "workspaces-web:List*", + "workspaces:Describe*" + ] + resources = ["*"] + } +} + +data "aws_iam_policy_document" "s3_access_policy" { + count = local.enable_s3_access ? 1 : 0 + + statement { + effect = "Allow" + actions = [ + "s3:PutObject", + "s3:ListBucket", + "s3:PutObjectAcl" + ] + resources = [ + local.s3_bucket_arn, + "${local.s3_bucket_arn}/*" + ] + } +} + +data "aws_iam_policy_document" "kms_access_policy" { + count = local.enable_kms_access ? 1 : 0 + + statement { + effect = "Allow" + actions = [ + "kms:DescribeKey", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo" + ] + resources = [var.kms_key_arn] + } +} + +data "aws_iam_policy_document" "deny_actions_policy" { + statement { + effect = "Deny" + actions = [ + "s3:GetObject", + "s3:GetObject*", + "s3:DeleteObject*", + "s3:RestoreObject", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:GetAuthorizationToken", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "ssm:GetParameter*", + "ssm:PutParameter*", + "ssm:DeleteParameter*", + "kms:Decrypt", + "lambda:InvokeFunction", + "lambda:InvokeAsync", + "sts:AssumeRole", + "iam:PassRole", + "iam:CreateRole", + "iam:DeleteRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:CreateUser", + "iam:DeleteUser", + "iam:CreateAccessKey", + "iam:DeleteAccessKey", + "iam:UpdateAccessKey", + "ec2:RunInstances", + "ec2:TerminateInstances", + "ec2:StopInstances", + "ec2:StartInstances", + "ec2:RebootInstances", + "ec2:CreateSnapshot", + "ec2:DeleteSnapshot", + "ec2:CreateImage", + "ec2:DeregisterImage", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DeleteLogGroup", + "logs:DeleteLogStream" + ] + resources = ["*"] + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/locals.tf b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/locals.tf new file mode 100644 index 0000000..69a332a --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/locals.tf @@ -0,0 +1,30 @@ +locals { + # Common naming + role_name_prefix = "AWSIntegration-${var.customer_name}" + role_name = "${local.role_name_prefix}-NullifyReadOnlyRole" + + # Policy names + readonly_policy_part1_name = "${local.role_name_prefix}-ReadOnlyAccess-Part1" + readonly_policy_part2_name = "${local.role_name_prefix}-ReadOnlyAccess-Part2" + s3_access_policy_name = "${local.role_name_prefix}-S3Access" + kms_access_policy_name = "${local.role_name_prefix}-KMSAccess" + deny_actions_policy_name = "${local.role_name_prefix}-DenyActions" + + # Cross-account role ARN (use directly) + nullify_role_arn = var.nullify_role_arn + + # OIDC subject for service account + oidc_subject = "system:serviceaccount:${var.kubernetes_namespace}:${var.service_account_name}" + + # S3 configuration + enable_s3_access = var.s3_bucket_name != "" + s3_bucket_arn = var.s3_bucket_name != "" ? "arn:aws:s3:::${var.s3_bucket_name}" : "" + + # KMS configuration + enable_kms_access = var.kms_key_arn != "" + + # Common tags + common_tags = merge(var.tags, { + Customer = var.customer_name + }) +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/main.tf b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/main.tf new file mode 100644 index 0000000..c45bb8c --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/main.tf @@ -0,0 +1,80 @@ +# IAM Role for Nullify integration +resource "aws_iam_role" "nullify_readonly_role" { + name = local.role_name + assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json + tags = local.common_tags + + lifecycle { + create_before_destroy = true + } +} + +# IAM Policies +resource "aws_iam_policy" "readonly_policy_part1" { + name = local.readonly_policy_part1_name + description = "Read-only access for AWS resources for Nullify (Part 1)" + policy = data.aws_iam_policy_document.readonly_policy_part1.json + tags = local.common_tags +} + +resource "aws_iam_policy" "readonly_policy_part2" { + name = local.readonly_policy_part2_name + description = "Read-only access for AWS resources for Nullify (Part 2)" + policy = data.aws_iam_policy_document.readonly_policy_part2.json + tags = local.common_tags +} + +resource "aws_iam_policy" "s3_access_policy" { + count = local.enable_s3_access ? 1 : 0 + + name = local.s3_access_policy_name + description = "S3 access policy for Nullify bucket" + policy = data.aws_iam_policy_document.s3_access_policy[0].json + tags = local.common_tags +} + +resource "aws_iam_policy" "kms_access_policy" { + count = local.enable_kms_access ? 1 : 0 + + name = local.kms_access_policy_name + description = "KMS access policy for Nullify key management operations" + policy = data.aws_iam_policy_document.kms_access_policy[0].json + tags = local.common_tags +} + +resource "aws_iam_policy" "deny_actions_policy" { + name = local.deny_actions_policy_name + description = "Policy to explicitly deny certain actions" + policy = data.aws_iam_policy_document.deny_actions_policy.json + tags = local.common_tags +} + +# Policy Attachments +resource "aws_iam_role_policy_attachment" "readonly_policy_part1" { + role = aws_iam_role.nullify_readonly_role.name + policy_arn = aws_iam_policy.readonly_policy_part1.arn +} + +resource "aws_iam_role_policy_attachment" "readonly_policy_part2" { + role = aws_iam_role.nullify_readonly_role.name + policy_arn = aws_iam_policy.readonly_policy_part2.arn +} + +resource "aws_iam_role_policy_attachment" "s3_access_policy" { + count = local.enable_s3_access ? 1 : 0 + + role = aws_iam_role.nullify_readonly_role.name + policy_arn = aws_iam_policy.s3_access_policy[0].arn +} + +resource "aws_iam_role_policy_attachment" "kms_access_policy" { + count = local.enable_kms_access ? 1 : 0 + + role = aws_iam_role.nullify_readonly_role.name + policy_arn = aws_iam_policy.kms_access_policy[0].arn +} + +resource "aws_iam_role_policy_attachment" "deny_actions_policy" { + role = aws_iam_role.nullify_readonly_role.name + policy_arn = aws_iam_policy.deny_actions_policy.arn +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/outputs.tf b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/outputs.tf new file mode 100644 index 0000000..77fb9e3 --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/outputs.tf @@ -0,0 +1,91 @@ +output "role_arn" { + description = "ARN of the IAM Role with cross-account read access" + value = aws_iam_role.nullify_readonly_role.arn +} + +output "role_name" { + description = "Name of the IAM Role created for Nullify" + value = aws_iam_role.nullify_readonly_role.name +} + +output "external_id" { + description = "External ID used for the IAM role" + value = var.external_id + sensitive = true +} + +output "s3_bucket_name" { + description = "Name of the S3 bucket for scan results (null if S3 not configured)" + value = var.s3_bucket_name != "" ? var.s3_bucket_name : null +} + +output "aws_region" { + description = "AWS region where resources are deployed" + value = var.aws_region +} + +output "customer_name" { + description = "Customer name used in resource naming" + value = var.customer_name +} + +# EKS/OIDC related outputs +output "all_oidc_ids" { + description = "List of all OIDC provider IDs for integrated clusters" + value = var.enable_kubernetes_integration ? local.all_oidc_ids : null +} + +output "all_oidc_provider_arns" { + description = "List of ARNs of all EKS OIDC providers used for the integration" + value = var.enable_kubernetes_integration ? local.eks_oidc_provider_arns : null +} + +output "cluster_integration_summary" { + description = "Summary of all cluster integrations" + value = var.enable_kubernetes_integration ? { + total_clusters = length(local.all_clusters_info) + clusters = [ + for cluster in local.all_clusters_info : { + region = cluster.region + oidc_id = cluster.oidc_id + } + ] + } : null +} + +output "all_clusters_info" { + description = "Complete information about all integrated clusters including regions" + value = var.enable_kubernetes_integration ? [ + for cluster in local.all_clusters_info : { + region = cluster.region + oidc_id = cluster.oidc_id + } + ] : null +} + +# Policy ARNs +output "policy_arns" { + description = "ARNs of all IAM policies created" + value = { + readonly_part1 = aws_iam_policy.readonly_policy_part1.arn + readonly_part2 = aws_iam_policy.readonly_policy_part2.arn + s3_access = local.enable_s3_access ? aws_iam_policy.s3_access_policy[0].arn : null + kms_access = local.enable_kms_access ? aws_iam_policy.kms_access_policy[0].arn : null + deny_actions = aws_iam_policy.deny_actions_policy.arn + } +} + +# Configuration summary +output "deployment_summary" { + description = "Summary of the Nullify integration deployment" + value = { + role_arn = aws_iam_role.nullify_readonly_role.arn + customer_name = var.customer_name + aws_region = var.aws_region + s3_bucket = var.s3_bucket_name != "" ? var.s3_bucket_name : null + s3_integration_enabled = local.enable_s3_access + kms_integration_enabled = local.enable_kms_access + kubernetes_integration = var.enable_kubernetes_integration + total_clusters_configured = var.enable_kubernetes_integration ? length(local.all_oidc_ids) : 0 + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/variables.tf b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/variables.tf new file mode 100644 index 0000000..9d5f937 --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/variables.tf @@ -0,0 +1,96 @@ +variable "customer_name" { + type = string + description = "The name of the customer to create the role for" + + validation { + condition = can(regex("^[a-zA-Z][a-zA-Z0-9_-]*$", var.customer_name)) + error_message = "Customer name must start with a letter and can only contain letters, numbers, underscores, and hyphens" + } +} + +variable "external_id" { + type = string + description = "The external ID for the role (provided by Nullify)" + + validation { + condition = length(var.external_id) >= 12 + error_message = "External ID must be at least 12 characters for security best practice" + } +} + +variable "nullify_role_arn" { + type = string + description = "The Nullify cross-account role ARN" + + validation { + condition = can(regex("^arn:aws:iam::[0-9]{12}:role/.+$", var.nullify_role_arn)) + error_message = "Must be a valid ARN for an IAM role in the format arn:aws:iam:::role/" + } +} + +variable "enable_kubernetes_integration" { + type = bool + description = "Whether to enable Kubernetes integration resources" + default = false +} + +variable "eks_cluster_arns" { + type = list(string) + description = "List of ARNs of EKS clusters to integrate with (OIDC provider IDs will be fetched automatically)" + default = [] + + validation { + condition = !var.enable_kubernetes_integration || (var.enable_kubernetes_integration && length(var.eks_cluster_arns) > 0) + error_message = "When Kubernetes integration is enabled, you must provide at least one cluster ARN in eks_cluster_arns" + } +} + +variable "aws_region" { + type = string + description = "The AWS region where resources are deployed" + default = "ap-southeast-2" +} + +variable "s3_bucket_name" { + type = string + description = "The name of the S3 bucket for storing scan results (optional, provided by Nullify if needed)" + default = "" +} + +variable "kubernetes_namespace" { + type = string + description = "The Kubernetes namespace for Nullify resources" + default = "nullify" +} + +variable "service_account_name" { + type = string + description = "The name of the Kubernetes service account" + default = "nullify-k8s-collector-sa" +} + +variable "cronjob_schedule" { + type = string + description = "Cron schedule for the Kubernetes collector job" + default = "0 0 * * *" +} + +variable "tags" { + type = map(string) + description = "Tags to apply to AWS resources" + default = { + ManagedBy = "Terraform" + Purpose = "NullifyIntegration" + } +} + +variable "kms_key_arn" { + type = string + description = "The ARN of the KMS key for key management operations (optional, provided by Nullify if needed)" + default = "" + + validation { + condition = var.kms_key_arn == "" || can(regex("^arn:aws:kms:[a-z0-9-]+:[0-9]{12}:(key/[a-f0-9-]+|alias/.+)$", var.kms_key_arn)) + error_message = "Must be a valid KMS key ARN (key ID or alias format) or empty string. Example: arn:aws:kms:region:account-id:key/key-id or arn:aws:kms:region:account-id:alias/alias-name" + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/versions.tf b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/versions.tf new file mode 100644 index 0000000..8be97e6 --- /dev/null +++ b/aws-integration-setup/terraform-v6/modules/nullify-aws-integration/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/outputs.tf b/aws-integration-setup/terraform-v6/outputs.tf new file mode 100644 index 0000000..d557ab0 --- /dev/null +++ b/aws-integration-setup/terraform-v6/outputs.tf @@ -0,0 +1,53 @@ +output "role_arn" { + description = "ARN of the IAM Role with cross-account read access" + value = module.nullify_aws_integration.role_arn +} + +output "role_name" { + description = "Name of the IAM Role created for Nullify" + value = module.nullify_aws_integration.role_name +} + +output "external_id" { + description = "External ID used for the IAM role" + value = module.nullify_aws_integration.external_id + sensitive = true +} + +output "s3_bucket_name" { + description = "Name of the S3 bucket for scan results" + value = module.nullify_aws_integration.s3_bucket_name +} + +output "aws_region" { + description = "AWS region where resources are deployed" + value = module.nullify_aws_integration.aws_region +} + +output "customer_name" { + description = "Customer name used in resource naming" + value = module.nullify_aws_integration.customer_name +} + +# Kubernetes-related outputs (only when enabled) +output "cluster_integration_summary" { + description = "Summary of all cluster integrations" + value = module.nullify_aws_integration.cluster_integration_summary +} + +output "all_clusters_info" { + description = "Complete information about all integrated clusters" + value = module.nullify_aws_integration.all_clusters_info +} + +# Policy ARNs +output "policy_arns" { + description = "ARNs of all IAM policies created" + value = module.nullify_aws_integration.policy_arns +} + +# Configuration summary +output "deployment_summary" { + description = "Summary of the Nullify integration deployment" + value = module.nullify_aws_integration.deployment_summary +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/providers.tf b/aws-integration-setup/terraform-v6/providers.tf new file mode 100644 index 0000000..7a1bc9f --- /dev/null +++ b/aws-integration-setup/terraform-v6/providers.tf @@ -0,0 +1,27 @@ +# AWS Provider +provider "aws" { + region = var.aws_region +} + +# Kubernetes Provider - configured for use with EKS +provider "kubernetes" { + # To use this provider, you need to configure it with your EKS cluster details + # Uncomment and configure the following lines for your specific EKS cluster: + + # host = data.aws_eks_cluster.cluster.endpoint + # cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data) + # token = ephemeral.aws_eks_cluster_auth.cluster.token + + # Or alternatively, use the AWS CLI configuration: + # config_path = "~/.kube/config" + # config_context = "your-eks-context" +} + +# Uncomment these if using the EKS cluster configuration above +# data "aws_eks_cluster" "cluster" { +# name = var.eks_cluster_name +# } +# +# ephemeral "aws_eks_cluster_auth" "cluster" { +# name = var.eks_cluster_name +# } \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/terraform.tfvars.example b/aws-integration-setup/terraform-v6/terraform.tfvars.example new file mode 100644 index 0000000..d9e15b0 --- /dev/null +++ b/aws-integration-setup/terraform-v6/terraform.tfvars.example @@ -0,0 +1,45 @@ +# Nullify AWS Integration - Example Configuration +# Copy this file to terraform.tfvars and fill in your values. +# Values marked REQUIRED must be provided. Others have sensible defaults. + +# REQUIRED: Customer name (provided by Nullify) +customer_name = "acme-corp" + +# REQUIRED: External ID for cross-account role assumption (provided by Nullify) +external_id = "your-external-id-from-nullify" + +# REQUIRED: Nullify cross-account role ARN (provided by Nullify) +nullify_role_arn = "arn:aws:iam::123456789012:role/NullifyRole" + +# AWS region where resources are deployed +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" + +# Enable Kubernetes integration (set to true if you have EKS clusters) +enable_kubernetes_integration = false + +# EKS cluster ARNs (required when enable_kubernetes_integration = true) +# eks_cluster_arns = [ +# "arn:aws:eks:ap-southeast-2:123456789012:cluster/my-cluster" +# ] + +# Kubernetes namespace for Nullify resources +# kubernetes_namespace = "nullify" + +# Kubernetes service account name +# service_account_name = "nullify-k8s-collector-sa" + +# Cron schedule for the collector (default: daily at midnight) +# cronjob_schedule = "0 0 * * *" + +# Tags to apply to all AWS resources +tags = { + ManagedBy = "Terraform" + Purpose = "NullifyIntegration" + Environment = "production" +} diff --git a/aws-integration-setup/terraform-v6/variables.tf b/aws-integration-setup/terraform-v6/variables.tf new file mode 100644 index 0000000..9d5f937 --- /dev/null +++ b/aws-integration-setup/terraform-v6/variables.tf @@ -0,0 +1,96 @@ +variable "customer_name" { + type = string + description = "The name of the customer to create the role for" + + validation { + condition = can(regex("^[a-zA-Z][a-zA-Z0-9_-]*$", var.customer_name)) + error_message = "Customer name must start with a letter and can only contain letters, numbers, underscores, and hyphens" + } +} + +variable "external_id" { + type = string + description = "The external ID for the role (provided by Nullify)" + + validation { + condition = length(var.external_id) >= 12 + error_message = "External ID must be at least 12 characters for security best practice" + } +} + +variable "nullify_role_arn" { + type = string + description = "The Nullify cross-account role ARN" + + validation { + condition = can(regex("^arn:aws:iam::[0-9]{12}:role/.+$", var.nullify_role_arn)) + error_message = "Must be a valid ARN for an IAM role in the format arn:aws:iam:::role/" + } +} + +variable "enable_kubernetes_integration" { + type = bool + description = "Whether to enable Kubernetes integration resources" + default = false +} + +variable "eks_cluster_arns" { + type = list(string) + description = "List of ARNs of EKS clusters to integrate with (OIDC provider IDs will be fetched automatically)" + default = [] + + validation { + condition = !var.enable_kubernetes_integration || (var.enable_kubernetes_integration && length(var.eks_cluster_arns) > 0) + error_message = "When Kubernetes integration is enabled, you must provide at least one cluster ARN in eks_cluster_arns" + } +} + +variable "aws_region" { + type = string + description = "The AWS region where resources are deployed" + default = "ap-southeast-2" +} + +variable "s3_bucket_name" { + type = string + description = "The name of the S3 bucket for storing scan results (optional, provided by Nullify if needed)" + default = "" +} + +variable "kubernetes_namespace" { + type = string + description = "The Kubernetes namespace for Nullify resources" + default = "nullify" +} + +variable "service_account_name" { + type = string + description = "The name of the Kubernetes service account" + default = "nullify-k8s-collector-sa" +} + +variable "cronjob_schedule" { + type = string + description = "Cron schedule for the Kubernetes collector job" + default = "0 0 * * *" +} + +variable "tags" { + type = map(string) + description = "Tags to apply to AWS resources" + default = { + ManagedBy = "Terraform" + Purpose = "NullifyIntegration" + } +} + +variable "kms_key_arn" { + type = string + description = "The ARN of the KMS key for key management operations (optional, provided by Nullify if needed)" + default = "" + + validation { + condition = var.kms_key_arn == "" || can(regex("^arn:aws:kms:[a-z0-9-]+:[0-9]{12}:(key/[a-f0-9-]+|alias/.+)$", var.kms_key_arn)) + error_message = "Must be a valid KMS key ARN (key ID or alias format) or empty string. Example: arn:aws:kms:region:account-id:key/key-id or arn:aws:kms:region:account-id:alias/alias-name" + } +} \ No newline at end of file diff --git a/aws-integration-setup/terraform-v6/versions.tf b/aws-integration-setup/terraform-v6/versions.tf new file mode 100644 index 0000000..2cf4201 --- /dev/null +++ b/aws-integration-setup/terraform-v6/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.20" + } + } +} \ No newline at end of file