From 7167c71f17d8dc56b0b3ed206e17c6fbd69a9cb2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:32:55 +0000 Subject: [PATCH 01/10] feat: Add support for aws_backup_logically_air_gapped_vault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implementation adds support for AWS Backup Logically Air Gapped Vault, providing enhanced backup isolation and compliance capabilities. Key features: - New vault_type variable supporting "standard" and "logically_air_gapped" - Mandatory retention configuration for air-gapped vaults - Backward compatibility with existing standard vault configurations - Comprehensive example configuration with compliance patterns - Enhanced outputs for both vault types Security enhancements: - Built-in retention enforcement for compliance requirements - Enhanced isolation for regulatory environments (SOX, PCI-DSS, HIPAA) - Immutable retention policies once configured Breaking changes: None - fully backward compatible Closes #236 ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Luis M. Gallardo D. --- examples/logically_air_gapped_vault/README.md | 132 ++++++++++++++++++ examples/logically_air_gapped_vault/main.tf | 37 +++++ .../logically_air_gapped_vault/outputs.tf | 53 +++++++ .../logically_air_gapped_vault/variables.tf | 72 ++++++++++ .../logically_air_gapped_vault/versions.tf | 10 ++ main.tf | 35 +++-- outputs.tf | 25 +++- variables.tf | 25 +++- 8 files changed, 376 insertions(+), 13 deletions(-) create mode 100644 examples/logically_air_gapped_vault/README.md create mode 100644 examples/logically_air_gapped_vault/main.tf create mode 100644 examples/logically_air_gapped_vault/outputs.tf create mode 100644 examples/logically_air_gapped_vault/variables.tf create mode 100644 examples/logically_air_gapped_vault/versions.tf diff --git a/examples/logically_air_gapped_vault/README.md b/examples/logically_air_gapped_vault/README.md new file mode 100644 index 0000000..5af53dc --- /dev/null +++ b/examples/logically_air_gapped_vault/README.md @@ -0,0 +1,132 @@ +# AWS Backup - Logically Air Gapped Vault + +This example shows how to create an AWS Backup plan with a **logically air gapped vault** for enhanced security and compliance requirements. + +## Features + +- **Logically Air Gapped Vault**: Enhanced isolation for backup data +- **Mandatory Retention Policies**: Built-in retention enforcement (7-year retention for compliance) +- **Compliance Ready**: Designed for SOX, PCI-DSS, HIPAA, and other regulatory requirements +- **Daily Backup Schedule**: Automated daily backups at 1 AM +- **Multi-Service Support**: Backs up DynamoDB tables, EBS volumes, and RDS databases + +## Key Differences from Standard Vault + +| Feature | Standard Vault | Logically Air Gapped Vault | +|---------|-----------------|---------------------------| +| **Encryption** | Optional KMS encryption | AWS-managed encryption (always on) | +| **Retention** | Optional vault lock | **Mandatory** min/max retention days | +| **Use Case** | General backup storage | High-security, compliance environments | +| **Recovery** | Standard recovery process | Enhanced security controls | + +## Usage + +To run this example you need to execute: + +```bash +$ terraform init +$ terraform plan +$ terraform apply +``` + +Note that this example creates resources which cost money. Run `terraform destroy` when you don't need these resources. + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 6.11.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.11.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [aws\_backup\_plan](#module\_aws\_backup\_plan) | ../../ | n/a | + +## Configuration + +The example demonstrates a compliance-focused backup configuration: + +- **Vault Type**: `logically_air_gapped` +- **Retention**: 2555 days (7 years) for both minimum and maximum +- **Schedule**: Daily backups at 1:00 AM UTC +- **Resources**: DynamoDB tables, EBS volumes, and RDS databases + +## Compliance Benefits + +### Enhanced Security +- Logically isolated from standard backup infrastructure +- Built-in retention enforcement prevents accidental deletion +- Enhanced audit trail for compliance reporting + +### Regulatory Compliance +- **SOX (Sarbanes-Oxley)**: Long-term retention of financial data +- **PCI-DSS**: Secure backup of payment processing systems +- **HIPAA**: Protected health information backup requirements +- **General Data Protection**: Write-once-read-many (WORM) characteristics + +## Example Terraform Configuration + +```hcl +module "compliance_backup" { + source = "lgallard/backup/aws" + + # Air Gapped Vault Configuration + vault_name = "compliance-air-gapped-vault" + vault_type = "logically_air_gapped" + min_retention_days = 2555 # 7 years + max_retention_days = 2555 # 7 years + + # Backup Plan + plan_name = "compliance-backup-plan" + rule_name = "daily-backup-rule" + rule_schedule = "cron(0 1 ? * * *)" # Daily at 1 AM + + # Resource Selection + selection_name = "critical-systems" + selection_resources = [ + "arn:aws:dynamodb:*:*:table/*", + "arn:aws:ec2:*:*:volume/*", + "arn:aws:rds:*:*:db:*" + ] + + tags = { + Environment = "production" + Purpose = "compliance" + Compliance = "SOX" + Owner = "data-governance-team" + } +} +``` + +## Important Notes + +1. **Retention Requirements**: Air gapped vaults **require** both `min_retention_days` and `max_retention_days` to be specified +2. **AWS Provider Version**: Requires AWS provider version >= 6.11.0 for air gapped vault support +3. **Cost Implications**: Air gapped vaults may have different pricing than standard vaults +4. **Recovery Process**: Recovery from air gapped vaults follows enhanced security procedures + +## Outputs + +| Name | Description | +|------|-------------| +| [vault\_id](#output\_vault\_id) | The name of the air gapped vault | +| [vault\_arn](#output\_vault\_arn) | The ARN of the air gapped vault | +| [vault\_type](#output\_vault\_type) | The type of vault created | +| [airgapped\_vault\_recovery\_points](#output\_airgapped\_vault\_recovery\_points) | The number of recovery points stored in the air gapped vault | +| [plan\_id](#output\_plan\_id) | The id of the backup plan | +| [plan\_arn](#output\_plan\_arn) | The ARN of the backup plan | + +## Security Considerations + +- Air gapped vaults provide enhanced isolation but cannot use custom KMS keys +- Retention policies are immutable once set +- Access patterns are logged for compliance auditing +- Recovery operations require additional authentication steps \ No newline at end of file diff --git a/examples/logically_air_gapped_vault/main.tf b/examples/logically_air_gapped_vault/main.tf new file mode 100644 index 0000000..b092b3b --- /dev/null +++ b/examples/logically_air_gapped_vault/main.tf @@ -0,0 +1,37 @@ +# +# Simple Backup plan with Logically Air Gapped Vault +# + +provider "aws" { + region = var.aws_region + + # Make it faster by skipping something + skip_metadata_api_check = true + skip_region_validation = true + skip_credentials_validation = true +} + +# Simple plan +module "aws_backup_plan" { + source = "../../" + + # Vault configuration - Air Gapped + vault_name = var.vault_name + vault_type = "logically_air_gapped" + min_retention_days = var.min_retention_days + max_retention_days = var.max_retention_days + + # Plan configuration + plan_name = var.plan_name + + # Rule configuration + rule_name = var.rule_name + rule_schedule = var.rule_schedule + + # Selection of resources + selection_name = var.selection_name + selection_resources = var.selection_resources + + # Common tags + tags = var.tags +} \ No newline at end of file diff --git a/examples/logically_air_gapped_vault/outputs.tf b/examples/logically_air_gapped_vault/outputs.tf new file mode 100644 index 0000000..8bb47ed --- /dev/null +++ b/examples/logically_air_gapped_vault/outputs.tf @@ -0,0 +1,53 @@ +# +# Outputs for Simple Backup plan with Logically Air Gapped Vault +# + +output "vault_id" { + description = "The name of the air gapped vault" + value = module.aws_backup_plan.vault_id +} + +output "vault_arn" { + description = "The ARN of the air gapped vault" + value = module.aws_backup_plan.vault_arn +} + +output "vault_type" { + description = "The type of vault created" + value = module.aws_backup_plan.vault_type +} + +output "airgapped_vault_id" { + description = "The name of the air gapped vault (specific output)" + value = module.aws_backup_plan.airgapped_vault_id +} + +output "airgapped_vault_arn" { + description = "The ARN of the air gapped vault (specific output)" + value = module.aws_backup_plan.airgapped_vault_arn +} + +output "airgapped_vault_recovery_points" { + description = "The number of recovery points stored in the air gapped vault" + value = module.aws_backup_plan.airgapped_vault_recovery_points +} + +output "plan_id" { + description = "The id of the backup plan" + value = module.aws_backup_plan.plan_id +} + +output "plan_arn" { + description = "The ARN of the backup plan" + value = module.aws_backup_plan.plan_arn +} + +output "plan_version" { + description = "Unique, randomly generated, Unicode, UTF-8 encoded string that serves as the version ID of the backup plan" + value = module.aws_backup_plan.plan_version +} + +output "plan_role" { + description = "The service role of the backup plan" + value = module.aws_backup_plan.plan_role +} \ No newline at end of file diff --git a/examples/logically_air_gapped_vault/variables.tf b/examples/logically_air_gapped_vault/variables.tf new file mode 100644 index 0000000..f1110f3 --- /dev/null +++ b/examples/logically_air_gapped_vault/variables.tf @@ -0,0 +1,72 @@ +# +# Variables for Simple Backup plan with Logically Air Gapped Vault +# + +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "vault_name" { + description = "Name of the backup vault to create" + type = string + default = "compliance-air-gapped-vault" +} + +variable "min_retention_days" { + description = "Minimum retention period that the vault retains its recovery points" + type = number + default = 2555 # 7 years for compliance +} + +variable "max_retention_days" { + description = "Maximum retention period that the vault retains its recovery points" + type = number + default = 2555 # 7 years for compliance +} + +variable "plan_name" { + description = "The display name of a backup plan" + type = string + default = "compliance-backup-plan" +} + +variable "rule_name" { + description = "An display name for a backup rule" + type = string + default = "daily-backup-rule" +} + +variable "rule_schedule" { + description = "A CRON expression specifying when AWS Backup initiates a backup job" + type = string + default = "cron(0 1 ? * * *)" # Daily at 1 AM +} + +variable "selection_name" { + description = "The display name of a resource selection document" + type = string + default = "selection" +} + +variable "selection_resources" { + description = "An array of strings that either contain Amazon Resource Names (ARNs) or match patterns of resources to assign to a backup plan" + type = list(string) + default = [ + "arn:aws:dynamodb:*:*:table/*", + "arn:aws:ec2:*:*:volume/*", + "arn:aws:rds:*:*:db:*" + ] +} + +variable "tags" { + description = "A mapping of tags to assign to the resource" + type = map(string) + default = { + Environment = "production" + Purpose = "compliance" + Compliance = "SOX" + Owner = "data-governance-team" + } +} \ No newline at end of file diff --git a/examples/logically_air_gapped_vault/versions.tf b/examples/logically_air_gapped_vault/versions.tf new file mode 100644 index 0000000..21b8607 --- /dev/null +++ b/examples/logically_air_gapped_vault/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.11.0" # Required for aws_backup_logically_air_gapped_vault + } + } +} \ No newline at end of file diff --git a/main.tf b/main.tf index 2d5a2e6..cbb83cf 100644 --- a/main.tf +++ b/main.tf @@ -2,15 +2,21 @@ # Organized locals for better maintainability and code clarity locals { # Resource creation conditions - should_create_vault = var.enabled && var.vault_name != null - should_create_lock = local.should_create_vault && var.locked - should_create_legacy_plan = var.enabled && length(var.plans) == 0 && length(local.rules) > 0 + should_create_vault = var.enabled && var.vault_name != null + should_create_standard_vault = local.should_create_vault && var.vault_type == "standard" + should_create_airgapped_vault = local.should_create_vault && var.vault_type == "logically_air_gapped" + should_create_lock = local.should_create_standard_vault && var.locked + should_create_legacy_plan = var.enabled && length(var.plans) == 0 && length(local.rules) > 0 # Validation helpers for vault lock configuration vault_lock_requirements_met = var.min_retention_days != null && var.max_retention_days != null retention_days_valid = local.vault_lock_requirements_met ? var.min_retention_days <= var.max_retention_days : true check_retention_days = var.locked ? (local.vault_lock_requirements_met && local.retention_days_valid) : true + # Vault reference helpers (dynamic based on vault type) + vault_name = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name, null) : null + vault_arn = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].arn, null) : local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].arn, null) : null + # Rule processing (matching existing logic for compatibility) rule = var.rule_name == null ? [] : [{ name = var.rule_name @@ -65,9 +71,9 @@ locals { ]) } -# AWS Backup vault with optimized timeouts +# AWS Backup vault (standard) with optimized timeouts resource "aws_backup_vault" "ab_vault" { - count = local.should_create_vault ? 1 : 0 + count = local.should_create_standard_vault ? 1 : 0 name = var.vault_name kms_key_arn = var.vault_kms_key_arn @@ -76,6 +82,17 @@ resource "aws_backup_vault" "ab_vault" { } +# AWS Backup logically air gapped vault +resource "aws_backup_logically_air_gapped_vault" "ab_airgapped_vault" { + count = local.should_create_airgapped_vault ? 1 : 0 + + name = var.vault_name + min_retention_days = var.min_retention_days + max_retention_days = var.max_retention_days + tags = var.tags + +} + # AWS Backup vault lock configuration resource "aws_backup_vault_lock_configuration" "ab_vault_lock_configuration" { count = local.should_create_lock ? 1 : 0 @@ -103,7 +120,7 @@ resource "aws_backup_plan" "ab_plan" { for_each = local.rules content { rule_name = try(rule.value.name, null) - target_vault_name = try(rule.value.target_vault_name, null) != null ? rule.value.target_vault_name : var.vault_name != null ? aws_backup_vault.ab_vault[0].name : "Default" + target_vault_name = try(rule.value.target_vault_name, null) != null ? rule.value.target_vault_name : var.vault_name != null ? local.vault_name : "Default" schedule = try(rule.value.schedule, null) start_window = try(rule.value.start_window, null) completion_window = try(rule.value.completion_window, null) @@ -153,7 +170,7 @@ resource "aws_backup_plan" "ab_plan" { tags = var.tags # First create the vault if needed - depends_on = [aws_backup_vault.ab_vault] + depends_on = [aws_backup_vault.ab_vault, aws_backup_logically_air_gapped_vault.ab_airgapped_vault] lifecycle { precondition { @@ -179,7 +196,7 @@ resource "aws_backup_plan" "ab_plans" { for_each = each.value.rules content { rule_name = try(rule.value.name, null) - target_vault_name = try(rule.value.target_vault_name, null) != null ? rule.value.target_vault_name : var.vault_name != null ? aws_backup_vault.ab_vault[0].name : "Default" + target_vault_name = try(rule.value.target_vault_name, null) != null ? rule.value.target_vault_name : var.vault_name != null ? local.vault_name : "Default" schedule = try(rule.value.schedule, null) start_window = try(rule.value.start_window, null) completion_window = try(rule.value.completion_window, null) @@ -229,7 +246,7 @@ resource "aws_backup_plan" "ab_plans" { tags = var.tags # First create the vault if needed - depends_on = [aws_backup_vault.ab_vault] + depends_on = [aws_backup_vault.ab_vault, aws_backup_logically_air_gapped_vault.ab_airgapped_vault] lifecycle { precondition { diff --git a/outputs.tf b/outputs.tf index 751e7ef..d8ff00e 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,12 +1,33 @@ # Vault output "vault_id" { description = "The name of the vault" - value = try(aws_backup_vault.ab_vault[0].id, null) + value = var.vault_type == "standard" ? try(aws_backup_vault.ab_vault[0].id, null) : try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].id, null) } output "vault_arn" { description = "The ARN of the vault" - value = try(aws_backup_vault.ab_vault[0].arn, null) + value = var.vault_type == "standard" ? try(aws_backup_vault.ab_vault[0].arn, null) : try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].arn, null) +} + +output "vault_type" { + description = "The type of vault created" + value = var.vault_type +} + +# Air Gapped Vault specific outputs +output "airgapped_vault_id" { + description = "The name of the air gapped vault" + value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].id, null) +} + +output "airgapped_vault_arn" { + description = "The ARN of the air gapped vault" + value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].arn, null) +} + +output "airgapped_vault_recovery_points" { + description = "The number of recovery points stored in the air gapped vault" + value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].recovery_points, null) } # Legacy Plan diff --git a/variables.tf b/variables.tf index 876051e..3acba97 100644 --- a/variables.tf +++ b/variables.tf @@ -41,6 +41,17 @@ variable "vault_force_destroy" { default = false } +variable "vault_type" { + description = "Type of backup vault to create. Valid values are 'standard' (default) or 'logically_air_gapped'" + type = string + default = "standard" + + validation { + condition = contains(["standard", "logically_air_gapped"], var.vault_type) + error_message = "The vault_type must be either 'standard' or 'logically_air_gapped'." + } +} + # # AWS Backup vault lock configuration # @@ -62,7 +73,7 @@ variable "changeable_for_days" { } variable "max_retention_days" { - description = "The maximum retention period that the vault retains its recovery points" + description = "The maximum retention period that the vault retains its recovery points. Required when vault_type is 'logically_air_gapped'" type = number default = null @@ -70,10 +81,15 @@ variable "max_retention_days" { condition = var.max_retention_days == null ? true : (var.max_retention_days >= 1 && var.max_retention_days <= 2555) error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." } + + validation { + condition = var.vault_type != "logically_air_gapped" || var.max_retention_days != null + error_message = "max_retention_days is required when vault_type is 'logically_air_gapped'." + } } variable "min_retention_days" { - description = "The minimum retention period that the vault retains its recovery points" + description = "The minimum retention period that the vault retains its recovery points. Required when vault_type is 'logically_air_gapped'" type = number default = null @@ -81,6 +97,11 @@ variable "min_retention_days" { condition = var.min_retention_days == null ? true : (var.min_retention_days >= 7 && var.min_retention_days <= 2555) error_message = "The min_retention_days must be between 7 and 2555 days (minimum 7 days for compliance requirements)." } + + validation { + condition = var.vault_type != "logically_air_gapped" || var.min_retention_days != null + error_message = "min_retention_days is required when vault_type is 'logically_air_gapped'." + } } # From 97f640fe4eab827551323c6afbfad984c14c238a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:49:44 +0000 Subject: [PATCH 02/10] fix: resolve pre-commit and validation issues - Remove invalid cross-variable validation blocks in variables.tf - Add proper lifecycle precondition for air-gapped vault validation - Remove unused local.vault_arn to fix TFLint warning - Fix end-of-file issues in example files - Apply proper terraform formatting Co-authored-by: Luis M. Gallardo D. --- examples/logically_air_gapped_vault/main.tf | 12 ++++++------ main.tf | 9 ++++++++- variables.tf | 8 -------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/examples/logically_air_gapped_vault/main.tf b/examples/logically_air_gapped_vault/main.tf index b092b3b..9142589 100644 --- a/examples/logically_air_gapped_vault/main.tf +++ b/examples/logically_air_gapped_vault/main.tf @@ -16,17 +16,17 @@ module "aws_backup_plan" { source = "../../" # Vault configuration - Air Gapped - vault_name = var.vault_name - vault_type = "logically_air_gapped" - min_retention_days = var.min_retention_days - max_retention_days = var.max_retention_days + vault_name = var.vault_name + vault_type = "logically_air_gapped" + min_retention_days = var.min_retention_days + max_retention_days = var.max_retention_days # Plan configuration plan_name = var.plan_name # Rule configuration - rule_name = var.rule_name - rule_schedule = var.rule_schedule + rule_name = var.rule_name + rule_schedule = var.rule_schedule # Selection of resources selection_name = var.selection_name diff --git a/main.tf b/main.tf index cbb83cf..b0ec3a1 100644 --- a/main.tf +++ b/main.tf @@ -13,9 +13,11 @@ locals { retention_days_valid = local.vault_lock_requirements_met ? var.min_retention_days <= var.max_retention_days : true check_retention_days = var.locked ? (local.vault_lock_requirements_met && local.retention_days_valid) : true + # Validation for air-gapped vault requirements + airgapped_vault_valid = var.vault_type != "logically_air_gapped" ? true : (var.min_retention_days != null && var.max_retention_days != null) + # Vault reference helpers (dynamic based on vault type) vault_name = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name, null) : null - vault_arn = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].arn, null) : local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].arn, null) : null # Rule processing (matching existing logic for compatibility) rule = var.rule_name == null ? [] : [{ @@ -178,6 +180,11 @@ resource "aws_backup_plan" "ab_plan" { error_message = "Windows VSS backup is enabled but no EC2 instances are selected for backup. Either disable windows_vss_backup or include EC2 instances in your backup selection." } + precondition { + condition = local.airgapped_vault_valid + error_message = "When vault_type is 'logically_air_gapped', both min_retention_days and max_retention_days must be specified." + } + # Add lifecycle validations at the plan level precondition { condition = local.lifecycle_validations diff --git a/variables.tf b/variables.tf index 3acba97..518f800 100644 --- a/variables.tf +++ b/variables.tf @@ -82,10 +82,6 @@ variable "max_retention_days" { error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." } - validation { - condition = var.vault_type != "logically_air_gapped" || var.max_retention_days != null - error_message = "max_retention_days is required when vault_type is 'logically_air_gapped'." - } } variable "min_retention_days" { @@ -98,10 +94,6 @@ variable "min_retention_days" { error_message = "The min_retention_days must be between 7 and 2555 days (minimum 7 days for compliance requirements)." } - validation { - condition = var.vault_type != "logically_air_gapped" || var.min_retention_days != null - error_message = "min_retention_days is required when vault_type is 'logically_air_gapped'." - } } # From ef197b3caafa6e2961000be5f634553005a9532d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:49:33 +0000 Subject: [PATCH 03/10] fix: address code review issues for air-gapped vault implementation - Fix critical bug: change min_retention_days default from 2555 to 7 days - Add sensitive flag to airgapped_vault_recovery_points output for security - Improve vault reference logic readability with multi-line formatting - Optimize validation logic using logical OR operator Co-authored-by: Luis M. Gallardo D. --- examples/logically_air_gapped_vault/variables.tf | 2 +- main.tf | 10 +++++++--- outputs.tf | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/logically_air_gapped_vault/variables.tf b/examples/logically_air_gapped_vault/variables.tf index f1110f3..20f96c5 100644 --- a/examples/logically_air_gapped_vault/variables.tf +++ b/examples/logically_air_gapped_vault/variables.tf @@ -17,7 +17,7 @@ variable "vault_name" { variable "min_retention_days" { description = "Minimum retention period that the vault retains its recovery points" type = number - default = 2555 # 7 years for compliance + default = 7 # AWS minimum, allows for flexible compliance needs } variable "max_retention_days" { diff --git a/main.tf b/main.tf index b0ec3a1..b971790 100644 --- a/main.tf +++ b/main.tf @@ -13,11 +13,15 @@ locals { retention_days_valid = local.vault_lock_requirements_met ? var.min_retention_days <= var.max_retention_days : true check_retention_days = var.locked ? (local.vault_lock_requirements_met && local.retention_days_valid) : true - # Validation for air-gapped vault requirements - airgapped_vault_valid = var.vault_type != "logically_air_gapped" ? true : (var.min_retention_days != null && var.max_retention_days != null) + # Validation for air-gapped vault requirements (moved to lifecycle precondition) + airgapped_vault_valid = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) # Vault reference helpers (dynamic based on vault type) - vault_name = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name, null) : null + vault_name = ( + local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : + local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name, null) : + null + ) # Rule processing (matching existing logic for compatibility) rule = var.rule_name == null ? [] : [{ diff --git a/outputs.tf b/outputs.tf index d8ff00e..dbf7157 100644 --- a/outputs.tf +++ b/outputs.tf @@ -26,8 +26,9 @@ output "airgapped_vault_arn" { } output "airgapped_vault_recovery_points" { - description = "The number of recovery points stored in the air gapped vault" + description = "The number of recovery points stored in the air gapped vault (sensitive for security)" value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].recovery_points, null) + sensitive = true } # Legacy Plan From d417123a5d799ae107da651167a6e26e5e503fa9 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:41:02 +0000 Subject: [PATCH 04/10] fix: address code review issues for air-gapped vault implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix documentation inconsistency in README.md (retention values) - Rename airgapped_vault_valid to vault_retention_valid for clarity - Add cross-validation between min/max retention days - Improve vault_name logic readability - Add validation to example variables for better error handling - Update comments to reflect configurable compliance periods ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Luis M. Gallardo D. --- examples/logically_air_gapped_vault/README.md | 6 +++--- .../logically_air_gapped_vault/variables.tf | 17 ++++++++++++++++- main.tf | 10 ++++------ variables.tf | 5 +++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/examples/logically_air_gapped_vault/README.md b/examples/logically_air_gapped_vault/README.md index 5af53dc..bafa653 100644 --- a/examples/logically_air_gapped_vault/README.md +++ b/examples/logically_air_gapped_vault/README.md @@ -55,7 +55,7 @@ Note that this example creates resources which cost money. Run `terraform destro The example demonstrates a compliance-focused backup configuration: - **Vault Type**: `logically_air_gapped` -- **Retention**: 2555 days (7 years) for both minimum and maximum +- **Retention**: 7 days minimum, 2555 days (7 years) maximum - **Schedule**: Daily backups at 1:00 AM UTC - **Resources**: DynamoDB tables, EBS volumes, and RDS databases @@ -81,8 +81,8 @@ module "compliance_backup" { # Air Gapped Vault Configuration vault_name = "compliance-air-gapped-vault" vault_type = "logically_air_gapped" - min_retention_days = 2555 # 7 years - max_retention_days = 2555 # 7 years + min_retention_days = 7 # AWS minimum for flexibility + max_retention_days = 2555 # 7 years for compliance # Backup Plan plan_name = "compliance-backup-plan" diff --git a/examples/logically_air_gapped_vault/variables.tf b/examples/logically_air_gapped_vault/variables.tf index 20f96c5..184f3a2 100644 --- a/examples/logically_air_gapped_vault/variables.tf +++ b/examples/logically_air_gapped_vault/variables.tf @@ -18,12 +18,27 @@ variable "min_retention_days" { description = "Minimum retention period that the vault retains its recovery points" type = number default = 7 # AWS minimum, allows for flexible compliance needs + + validation { + condition = var.min_retention_days >= 7 && var.min_retention_days <= 2555 + error_message = "The min_retention_days must be between 7 and 2555 days (minimum 7 days for compliance requirements)." + } } variable "max_retention_days" { description = "Maximum retention period that the vault retains its recovery points" type = number - default = 2555 # 7 years for compliance + default = 2555 # 7 years for compliance - configurable for different compliance needs + + validation { + condition = var.max_retention_days >= 1 && var.max_retention_days <= 2555 + error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." + } + + validation { + condition = var.min_retention_days <= var.max_retention_days + error_message = "The min_retention_days must be less than or equal to max_retention_days." + } } variable "plan_name" { diff --git a/main.tf b/main.tf index b971790..6f88f9d 100644 --- a/main.tf +++ b/main.tf @@ -14,13 +14,11 @@ locals { check_retention_days = var.locked ? (local.vault_lock_requirements_met && local.retention_days_valid) : true # Validation for air-gapped vault requirements (moved to lifecycle precondition) - airgapped_vault_valid = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) + vault_retention_valid = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) # Vault reference helpers (dynamic based on vault type) - vault_name = ( - local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : - local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name, null) : - null + vault_name = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : ( + local.should_create_airgapped_vault ? try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name, null) : null ) # Rule processing (matching existing logic for compatibility) @@ -185,7 +183,7 @@ resource "aws_backup_plan" "ab_plan" { } precondition { - condition = local.airgapped_vault_valid + condition = local.vault_retention_valid error_message = "When vault_type is 'logically_air_gapped', both min_retention_days and max_retention_days must be specified." } diff --git a/variables.tf b/variables.tf index 518f800..5a1ea76 100644 --- a/variables.tf +++ b/variables.tf @@ -82,6 +82,11 @@ variable "max_retention_days" { error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." } + validation { + condition = (var.max_retention_days == null || var.min_retention_days == null) ? true : var.min_retention_days <= var.max_retention_days + error_message = "The min_retention_days must be less than or equal to max_retention_days." + } + } variable "min_retention_days" { From 81c4da85188bbdddb67df1ba49e322328874a8f1 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:41:57 +0000 Subject: [PATCH 05/10] fix: address critical code review issues for air-gapped vault implementation - Move cross-validation logic from variable to lifecycle precondition to fix confusing error messages - Add sensitive flag to recovery points output in example to prevent information disclosure - Rename vault_retention_valid to airgapped_vault_requirements_met for clarity - Optimize depends_on to only reference relevant vault type resources - Document force_destroy limitations and AWS API constraints in README - Add comprehensive validation documentation for air-gapped vault constraints Co-authored-by: Luis M. Gallardo D. --- examples/logically_air_gapped_vault/README.md | 10 ++++++- .../logically_air_gapped_vault/outputs.tf | 3 +- main.tf | 28 ++++++++++++++----- variables.tf | 4 --- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/examples/logically_air_gapped_vault/README.md b/examples/logically_air_gapped_vault/README.md index bafa653..cfd0180 100644 --- a/examples/logically_air_gapped_vault/README.md +++ b/examples/logically_air_gapped_vault/README.md @@ -112,6 +112,7 @@ module "compliance_backup" { 2. **AWS Provider Version**: Requires AWS provider version >= 6.11.0 for air gapped vault support 3. **Cost Implications**: Air gapped vaults may have different pricing than standard vaults 4. **Recovery Process**: Recovery from air gapped vaults follows enhanced security procedures +5. **Destruction Limitations**: Air gapped vaults do not support `force_destroy` - vaults with recovery points cannot be destroyed until retention periods expire ## Outputs @@ -129,4 +130,11 @@ module "compliance_backup" { - Air gapped vaults provide enhanced isolation but cannot use custom KMS keys - Retention policies are immutable once set - Access patterns are logged for compliance auditing -- Recovery operations require additional authentication steps \ No newline at end of file +- Recovery operations require additional authentication steps + +## AWS Limitations for Air Gapped Vaults + +- **No KMS Support**: Custom KMS encryption keys are not supported (AWS-managed encryption only) +- **No Force Destroy**: Cannot use `force_destroy` parameter - vaults must be manually emptied before deletion +- **Limited Parameters**: Only supports `name`, `min_retention_days`, `max_retention_days`, `tags`, and `region` +- **API Constraints**: Some AWS Backup APIs may have throttling limitations for air gapped operations \ No newline at end of file diff --git a/examples/logically_air_gapped_vault/outputs.tf b/examples/logically_air_gapped_vault/outputs.tf index 8bb47ed..2036b70 100644 --- a/examples/logically_air_gapped_vault/outputs.tf +++ b/examples/logically_air_gapped_vault/outputs.tf @@ -28,8 +28,9 @@ output "airgapped_vault_arn" { } output "airgapped_vault_recovery_points" { - description = "The number of recovery points stored in the air gapped vault" + description = "The number of recovery points stored in the air gapped vault (sensitive for security)" value = module.aws_backup_plan.airgapped_vault_recovery_points + sensitive = true } output "plan_id" { diff --git a/main.tf b/main.tf index 6f88f9d..069e251 100644 --- a/main.tf +++ b/main.tf @@ -13,8 +13,11 @@ locals { retention_days_valid = local.vault_lock_requirements_met ? var.min_retention_days <= var.max_retention_days : true check_retention_days = var.locked ? (local.vault_lock_requirements_met && local.retention_days_valid) : true - # Validation for air-gapped vault requirements (moved to lifecycle precondition) - vault_retention_valid = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) + # Validation for air-gapped vault requirements + airgapped_vault_requirements_met = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) + + # Cross-validation for retention days (moved from variable validation) + retention_days_cross_valid = (var.min_retention_days == null || var.max_retention_days == null) ? true : var.min_retention_days <= var.max_retention_days # Vault reference helpers (dynamic based on vault type) vault_name = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : ( @@ -173,8 +176,11 @@ resource "aws_backup_plan" "ab_plan" { # Tags tags = var.tags - # First create the vault if needed - depends_on = [aws_backup_vault.ab_vault, aws_backup_logically_air_gapped_vault.ab_airgapped_vault] + # First create the vault if needed (only depend on the vault type being used) + depends_on = compact([ + local.should_create_standard_vault ? aws_backup_vault.ab_vault[0].name : null, + local.should_create_airgapped_vault ? aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name : null + ]) lifecycle { precondition { @@ -183,10 +189,15 @@ resource "aws_backup_plan" "ab_plan" { } precondition { - condition = local.vault_retention_valid + condition = local.airgapped_vault_requirements_met error_message = "When vault_type is 'logically_air_gapped', both min_retention_days and max_retention_days must be specified." } + precondition { + condition = local.retention_days_cross_valid + error_message = "The min_retention_days must be less than or equal to max_retention_days." + } + # Add lifecycle validations at the plan level precondition { condition = local.lifecycle_validations @@ -254,8 +265,11 @@ resource "aws_backup_plan" "ab_plans" { # Tags tags = var.tags - # First create the vault if needed - depends_on = [aws_backup_vault.ab_vault, aws_backup_logically_air_gapped_vault.ab_airgapped_vault] + # First create the vault if needed (only depend on the vault type being used) + depends_on = compact([ + local.should_create_standard_vault ? aws_backup_vault.ab_vault[0].name : null, + local.should_create_airgapped_vault ? aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name : null + ]) lifecycle { precondition { diff --git a/variables.tf b/variables.tf index 5a1ea76..f4011e8 100644 --- a/variables.tf +++ b/variables.tf @@ -82,10 +82,6 @@ variable "max_retention_days" { error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." } - validation { - condition = (var.max_retention_days == null || var.min_retention_days == null) ? true : var.min_retention_days <= var.max_retention_days - error_message = "The min_retention_days must be less than or equal to max_retention_days." - } } From d1fbc665d985ddea39a291a0a94f3fe7d0aacde2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:45:28 +0000 Subject: [PATCH 06/10] fix: address critical code review issues for air-gapped vault implementation - Remove circular dependency in cross-variable validation from example - Optimize dependency logic by replacing inefficient compact() with conditional dependencies - Consolidate validation logic using unified logical OR approach - Comment out hardcoded provider settings in example to prevent production issues Co-authored-by: Luis M. Gallardo D. --- examples/logically_air_gapped_vault/main.tf | 9 +++++---- .../logically_air_gapped_vault/variables.tf | 4 ---- main.tf | 18 ++++++++---------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/examples/logically_air_gapped_vault/main.tf b/examples/logically_air_gapped_vault/main.tf index 9142589..9209dd3 100644 --- a/examples/logically_air_gapped_vault/main.tf +++ b/examples/logically_air_gapped_vault/main.tf @@ -5,10 +5,11 @@ provider "aws" { region = var.aws_region - # Make it faster by skipping something - skip_metadata_api_check = true - skip_region_validation = true - skip_credentials_validation = true + # Note: The following settings are for development/testing only + # Remove these in production to ensure proper validation + # skip_metadata_api_check = true + # skip_region_validation = true + # skip_credentials_validation = true } # Simple plan diff --git a/examples/logically_air_gapped_vault/variables.tf b/examples/logically_air_gapped_vault/variables.tf index 184f3a2..318afc1 100644 --- a/examples/logically_air_gapped_vault/variables.tf +++ b/examples/logically_air_gapped_vault/variables.tf @@ -35,10 +35,6 @@ variable "max_retention_days" { error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." } - validation { - condition = var.min_retention_days <= var.max_retention_days - error_message = "The min_retention_days must be less than or equal to max_retention_days." - } } variable "plan_name" { diff --git a/main.tf b/main.tf index 069e251..6104358 100644 --- a/main.tf +++ b/main.tf @@ -16,8 +16,8 @@ locals { # Validation for air-gapped vault requirements airgapped_vault_requirements_met = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) - # Cross-validation for retention days (moved from variable validation) - retention_days_cross_valid = (var.min_retention_days == null || var.max_retention_days == null) ? true : var.min_retention_days <= var.max_retention_days + # Cross-validation for retention days (unified validation approach) + retention_days_cross_valid = (var.min_retention_days == null || var.max_retention_days == null) || var.min_retention_days <= var.max_retention_days # Vault reference helpers (dynamic based on vault type) vault_name = local.should_create_standard_vault ? try(aws_backup_vault.ab_vault[0].name, null) : ( @@ -177,10 +177,9 @@ resource "aws_backup_plan" "ab_plan" { tags = var.tags # First create the vault if needed (only depend on the vault type being used) - depends_on = compact([ - local.should_create_standard_vault ? aws_backup_vault.ab_vault[0].name : null, - local.should_create_airgapped_vault ? aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name : null - ]) + depends_on = local.should_create_standard_vault ? [aws_backup_vault.ab_vault[0]] : ( + local.should_create_airgapped_vault ? [aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0]] : null + ) lifecycle { precondition { @@ -266,10 +265,9 @@ resource "aws_backup_plan" "ab_plans" { tags = var.tags # First create the vault if needed (only depend on the vault type being used) - depends_on = compact([ - local.should_create_standard_vault ? aws_backup_vault.ab_vault[0].name : null, - local.should_create_airgapped_vault ? aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].name : null - ]) + depends_on = local.should_create_standard_vault ? [aws_backup_vault.ab_vault[0]] : ( + local.should_create_airgapped_vault ? [aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0]] : null + ) lifecycle { precondition { From fd0908184caf36810e21d8ded4edf78691693ba8 Mon Sep 17 00:00:00 2001 From: "Luis M. Gallardo D" Date: Mon, 15 Sep 2025 20:11:22 +0200 Subject: [PATCH 07/10] feat: add AWS Backup Logically Air Gapped Vault support - Add support for aws_backup_logically_air_gapped_vault resource - New vault_type variable supporting "standard" and "logically_air_gapped" - Mandatory retention configuration for air-gapped vaults - Backward compatibility with existing configurations - Comprehensive example with compliance patterns - Enhanced outputs for both vault types - Updated AWS provider requirement to >= 6.11.0 - Added comprehensive test suite for air-gapped vault functionality - Updated documentation with new vault type feature Closes #236 --- .github/feature-tracker/backup-features.json | 8 +- .github/workflows/feature-discovery.yml | 98 +++++++-------- CHANGELOG.md | 4 +- README.md | 53 +++++++- examples/logically_air_gapped_vault/README.md | 2 +- examples/logically_air_gapped_vault/main.tf | 4 +- .../logically_air_gapped_vault/outputs.tf | 13 +- .../logically_air_gapped_vault/variables.tf | 9 +- .../logically_air_gapped_vault/versions.tf | 4 +- main.tf | 30 ++--- outputs.tf | 11 +- test/examples_test.go | 1 + .../terraform/air_gapped_vault/main.tf | 45 +++++++ .../terraform/air_gapped_vault/outputs.tf | 29 +++++ .../terraform/air_gapped_vault/variables.tf | 38 ++++++ test/integration_test.go | 114 ++++++++++++++++++ versions.tf | 2 +- 17 files changed, 371 insertions(+), 94 deletions(-) create mode 100644 test/fixtures/terraform/air_gapped_vault/main.tf create mode 100644 test/fixtures/terraform/air_gapped_vault/outputs.tf create mode 100644 test/fixtures/terraform/air_gapped_vault/variables.tf diff --git a/.github/feature-tracker/backup-features.json b/.github/feature-tracker/backup-features.json index 4f1ff20..f4190a0 100644 --- a/.github/feature-tracker/backup-features.json +++ b/.github/feature-tracker/backup-features.json @@ -336,7 +336,7 @@ "status": "pending_creation" }, { - "resource": "aws_backup_logically_air_gapped_vault", + "resource": "aws_backup_logically_air_gapped_vault", "issue_type": "new-feature", "title": "feat: Add support for aws_backup_logically_air_gapped_vault", "created_date": "2025-09-01T01:45:00Z", @@ -344,7 +344,7 @@ }, { "resource": "aws_backup_region_settings", - "issue_type": "new-feature", + "issue_type": "new-feature", "title": "feat: Add support for aws_backup_region_settings", "created_date": "2025-09-01T01:45:00Z", "status": "pending_creation" @@ -352,7 +352,7 @@ { "resource": "aws_backup_restore_testing_plan", "issue_type": "new-feature", - "title": "feat: Add support for aws_backup_restore_testing_plan", + "title": "feat: Add support for aws_backup_restore_testing_plan", "created_date": "2025-09-01T01:45:00Z", "status": "pending_creation" }, @@ -360,7 +360,7 @@ "resource": "aws_backup_restore_testing_selection", "issue_type": "new-feature", "title": "feat: Add support for aws_backup_restore_testing_selection", - "created_date": "2025-09-01T01:45:00Z", + "created_date": "2025-09-01T01:45:00Z", "status": "pending_creation" } ], diff --git a/.github/workflows/feature-discovery.yml b/.github/workflows/feature-discovery.yml index 5854e7e..c0a56b8 100644 --- a/.github/workflows/feature-discovery.yml +++ b/.github/workflows/feature-discovery.yml @@ -109,22 +109,22 @@ jobs: GITHUB_TOKEN: ${{ secrets.CLAUDE_ISSUE_TOKEN }} run: | echo "๐Ÿ” Pre-discovery verification checks..." - + # Check GitHub token permissions (skip auth status check) echo "Verifying GitHub token permissions..." echo "โœ… GitHub token configured" - + # Test issue creation capability echo "Testing GitHub CLI issue operations..." gh issue list --limit 1 > /dev/null || echo "โš ๏ธ Issue operations may fail" - + # Verify MCP server accessibility echo "Testing Docker availability for Terraform MCP server..." docker --version - + echo "Testing NPX availability for Context7 MCP server..." npx --version - + # Verify tracker file state echo "Current feature tracker state:" if [ -f .github/feature-tracker/backup-features.json ]; then @@ -134,7 +134,7 @@ jobs: else echo "โš ๏ธ Feature tracker will be created" fi - + echo "โœ… Pre-verification complete" - name: Run Claude Code Feature Discovery @@ -272,7 +272,7 @@ jobs: ### Step 5: Generate Structured Output for Issue Creation **CRITICAL: DO NOT execute gh issue create commands directly.** - + Instead, create a structured JSON file for the post-process step to handle: ```bash @@ -299,7 +299,7 @@ jobs: } // For each new argument discovered: { - "type": "new_argument", + "type": "new_argument", "resource_name": "[EXISTING_RESOURCE_NAME]", "argument_name": "[ARGUMENT_NAME]", "description": "[ARGUMENT_DESCRIPTION]", @@ -347,13 +347,13 @@ jobs: if: steps.claude-discovery.conclusion != 'failure' run: | echo "๐Ÿ” Post-discovery verification..." - + # Check what Claude Code actually produced echo "Checking for structured output file..." if [ -f "/tmp/discovered-features.json" ]; then echo "โœ… Structured output file exists" echo "File size: $(wc -c < /tmp/discovered-features.json) bytes" - + # Validate JSON if jq empty /tmp/discovered-features.json 2>/dev/null; then echo "โœ… Valid JSON structure" @@ -369,7 +369,7 @@ jobs: echo "Checking for temp files:" ls -la /tmp/ | grep -E "(discovered|feature|backup)" || echo "No related temp files" fi - + # Check tracker file updates echo "Checking feature tracker updates..." if [ -f ".github/feature-tracker/backup-features.json" ]; then @@ -378,7 +378,7 @@ jobs: echo "Last scan: $LAST_SCAN" echo "Pending creation entries: $PENDING_COUNT" fi - + echo "โœ… Post-verification complete" - name: Create GitHub Issues from Structured Output @@ -388,33 +388,33 @@ jobs: GITHUB_TOKEN: ${{ secrets.CLAUDE_ISSUE_TOKEN }} run: | set -euo pipefail - + echo "๐Ÿ” Processing discovered features for issue creation..." - + DISCOVERED_FILE="/tmp/discovered-features.json" TRACKER_FILE=".github/feature-tracker/backup-features.json" ISSUES_CREATED=0 - + # Check if structured output exists if [ ! -f "$DISCOVERED_FILE" ]; then echo "โš ๏ธ No structured output found at $DISCOVERED_FILE" echo "Checking for pending_creation entries in tracker file..." - + if [ ! -f "$TRACKER_FILE" ]; then echo "Feature tracker file not found, skipping post-processing" exit 0 fi - + # Fallback: Extract pending creation features from tracker PENDING_FEATURES=$(jq -r '.issues_created[]? | select(.status == "pending_creation") | @base64' "$TRACKER_FILE" 2>/dev/null || echo "") - + if [ -z "$PENDING_FEATURES" ]; then echo "โœ… No features with pending_creation status found" exit 0 fi - + echo "๐Ÿ“ Found features with pending_creation status. Creating issues..." - + # Process pending features from tracker while IFS= read -r feature_data; do if [ -n "$feature_data" ]; then @@ -423,9 +423,9 @@ jobs: RESOURCE=$(echo "$FEATURE_JSON" | jq -r '.resource') TITLE=$(echo "$FEATURE_JSON" | jq -r '.title') ISSUE_TYPE=$(echo "$FEATURE_JSON" | jq -r '.issue_type // "new-feature"') - + echo "Creating issue for: $RESOURCE" - + # Create the issue ISSUE_URL=$(gh issue create \ --title "$TITLE" \ @@ -461,12 +461,12 @@ jobs: *Auto-generated by AWS Backup Feature Discovery Bot*" \ --label "enhancement,aws-backup,features,terraform" \ --assignee "lgallard") - + # Extract issue number from URL ISSUE_NUMBER=$(echo "$ISSUE_URL" | grep -o '[0-9]*$') echo "โœ… Created issue #$ISSUE_NUMBER for $RESOURCE: $ISSUE_URL" ISSUES_CREATED=$((ISSUES_CREATED + 1)) - + # Update the tracker file to mark as created jq --arg resource "$RESOURCE" --arg issue_num "$ISSUE_NUMBER" --arg issue_url "$ISSUE_URL" ' (.issues_created[] | select(.resource == $resource)) |= ( @@ -477,34 +477,34 @@ jobs: )' "$TRACKER_FILE" > "${TRACKER_FILE}.tmp" && mv "${TRACKER_FILE}.tmp" "$TRACKER_FILE" fi done <<< "$PENDING_FEATURES" - + echo "๐ŸŽฏ Fallback processing complete: Created $ISSUES_CREATED issues" echo "issues_created=$ISSUES_CREATED" >> $GITHUB_OUTPUT exit 0 fi - + # Process structured JSON output echo "๐Ÿ“‹ Processing structured output from Claude Code..." - + # Validate JSON structure if ! jq empty "$DISCOVERED_FILE" 2>/dev/null; then echo "โŒ Invalid JSON in discovered features file" exit 1 fi - + # Extract metadata SCAN_DATE=$(jq -r '.scan_metadata.scan_date // "unknown"' "$DISCOVERED_FILE") PROVIDER_VERSION=$(jq -r '.scan_metadata.provider_version // "latest"' "$DISCOVERED_FILE") FEATURE_COUNT=$(jq '.discovered_features | length' "$DISCOVERED_FILE") - + echo "Scan metadata: $SCAN_DATE, Provider: $PROVIDER_VERSION, Features: $FEATURE_COUNT" - + if [ "$FEATURE_COUNT" -eq 0 ]; then echo "โœ… No new features discovered" echo "issues_created=0" >> $GITHUB_OUTPUT exit 0 fi - + # Process each discovered feature jq -r '.discovered_features[] | @base64' "$DISCOVERED_FILE" | while IFS= read -r feature_data; do if [ -n "$feature_data" ]; then @@ -513,16 +513,16 @@ jobs: RESOURCE_NAME=$(echo "$FEATURE_JSON" | jq -r '.resource_name') ISSUE_TITLE=$(echo "$FEATURE_JSON" | jq -r '.issue_title') PRIORITY=$(echo "$FEATURE_JSON" | jq -r '.priority // "medium"') - + echo "Creating issue for $FEATURE_TYPE: $RESOURCE_NAME" - + # Build issue body based on type if [ "$FEATURE_TYPE" = "new_resource" ]; then DESCRIPTION=$(echo "$FEATURE_JSON" | jq -r '.description // "AWS Backup resource"') SECURITY_IMPACT=$(echo "$FEATURE_JSON" | jq -r '.security_impact // "To be evaluated"') ARGUMENTS=$(echo "$FEATURE_JSON" | jq -r '.arguments[]? // empty' | tr '\n' ' ') REGISTRY_URL=$(echo "$FEATURE_JSON" | jq -r '.terraform_registry_url // ""') - + ISSUE_BODY="## New AWS Backup Resource Request ### Resource Details @@ -550,12 +550,12 @@ jobs: --- *Auto-generated by AWS Backup Feature Discovery Bot*" - + elif [ "$FEATURE_TYPE" = "new_argument" ]; then ARGUMENT_NAME=$(echo "$FEATURE_JSON" | jq -r '.argument_name') DESCRIPTION=$(echo "$FEATURE_JSON" | jq -r '.description // "New argument"') IMPACT=$(echo "$FEATURE_JSON" | jq -r '.implementation_impact // "To be evaluated"') - + ISSUE_BODY="## New Argument Enhancement Request ### Enhancement Details @@ -577,21 +577,21 @@ jobs: --- *Auto-generated by AWS Backup Feature Discovery Bot*" fi - + # Create the GitHub issue ISSUE_URL=$(gh issue create \ --title "$ISSUE_TITLE" \ --body "$ISSUE_BODY" \ --label "enhancement,aws-backup,features,terraform" \ --assignee "lgallard") - + # Extract issue number ISSUE_NUMBER=$(echo "$ISSUE_URL" | grep -o '[0-9]*$') echo "โœ… Created issue #$ISSUE_NUMBER: $ISSUE_URL" ISSUES_CREATED=$((ISSUES_CREATED + 1)) fi done - + echo "๐ŸŽฏ Issue creation complete: Created $ISSUES_CREATED issues" echo "issues_created=$ISSUES_CREATED" >> $GITHUB_OUTPUT @@ -606,7 +606,7 @@ jobs: LOCKFILE="/tmp/feature-tracker.lock" TRACKER_FILE=".github/feature-tracker/backup-features.json" TEMP_FILE="${TRACKER_FILE}.tmp" - + # Get issues created count from previous step ISSUES_CREATED="${{ steps.create-issues-from-json.outputs.issues_created || '0' }}" @@ -623,7 +623,7 @@ jobs: flock -u 200 exit 0 fi - + # Only create PR if new issues were created (meaningful changes) if [ "$ISSUES_CREATED" -eq 0 ]; then echo "๐Ÿ“Š Tracker updated with metadata only - skipping PR creation" @@ -632,7 +632,7 @@ jobs: flock -u 200 exit 0 fi - + echo "๐Ÿš€ Creating PR for tracker updates with $ISSUES_CREATED new issues" # Validate JSON before committing @@ -662,24 +662,24 @@ jobs: BRANCH_NAME="feature-discovery/tracker-update-$(date +%Y%m%d-%H%M%S)" git checkout -b "$BRANCH_NAME" git push origin "$BRANCH_NAME" - + # Create pull request for tracker updates gh pr create \ --title "chore: update AWS Backup feature discovery tracker" \ --body "Automated update of feature discovery tracker database. - + **Scan Details:** - Scan completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC') - Provider version: ${{ inputs.provider_version || 'latest' }} - Workflow run: ${{ github.run_id }} - + This PR contains automated updates to the feature tracking database and can be safely merged. - + --- *Auto-generated by AWS Backup Feature Discovery workflow*" \ --label "aws-backup,ci-cd,configuration" \ --assignee "lgallard" - + echo "Created PR for tracker updates on branch: $BRANCH_NAME" # Release lock @@ -714,7 +714,7 @@ jobs: else echo "- โŒ **Feature Discovery**: Failed" >> $GITHUB_STEP_SUMMARY fi - + # Issue Creation Status if [ "${{ inputs.dry_run }}" = "true" ]; then echo "- ๐Ÿงช **Issue Creation**: Skipped (dry run mode)" >> $GITHUB_STEP_SUMMARY @@ -730,7 +730,7 @@ jobs: else echo "- โŒ **Issue Creation**: Failed or incomplete" >> $GITHUB_STEP_SUMMARY fi - + # Tracker Update Status if [ "${{ steps.claude-discovery.conclusion }}" = "success" ]; then ISSUES_COUNT="${{ steps.create-issues-from-json.outputs.issues_created || '0' }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4680e07..ea8cef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -500,7 +500,7 @@ FIXES: ENHANCEMENTS: * Allows attaching an already created IAM role to the Plan (thanks @samcre) -* Update README to include Terraform rsources used +* Update README to include Terraform resources used ## 0.7.0 (February 28, 2021) @@ -539,7 +539,7 @@ FIXES: ENHANCEMENTS: * Add option to define selections by tags only, without resource definition -* Now you can define selections with just resources, tags or boths. No need to define empty values. +* Now you can define selections with just resources, tags or both. No need to define empty values. * Add README to examples UPDATES: diff --git a/README.md b/README.md index 062818d..bec6157 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,39 @@ See [examples/organization_backup_policy/main.tf](examples/organization_backup_p See [examples/simple_audit_framework/main.tf](examples/simple_audit_framework/main.tf) for audit framework configuration. +### Logically Air Gapped Vault + +This module supports AWS Backup Logically Air Gapped Vaults for enhanced security and compliance requirements. Air-gapped vaults provide isolated storage with immutable retention policies. + +See [examples/logically_air_gapped_vault/main.tf](examples/logically_air_gapped_vault/main.tf) for air-gapped vault configuration. + +**Key Features:** +- **Enhanced Security**: Logical isolation from standard backup infrastructure +- **Immutable Retention**: Mandatory min/max retention policies that cannot be bypassed +- **Compliance Ready**: Supports SOX, PCI-DSS, HIPAA, and other regulatory requirements +- **AWS-Managed Encryption**: Built-in encryption (custom KMS keys not supported) + +**Usage:** +```hcl +module "compliance_backup" { + source = "lgallard/backup/aws" + + vault_name = "compliance-vault" + vault_type = "logically_air_gapped" + min_retention_days = 7 + max_retention_days = 2555 # 7 years for compliance + + # ... other configuration +} +``` + ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3.0 | -| [aws](#requirement\_aws) | >= 5.0.0 | +| [aws](#requirement\_aws) | >= 6.11.0 | ## Providers @@ -78,6 +104,7 @@ No modules. | Name | Type | |------|------| | [aws_backup_framework.ab_framework](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_framework) | resource | +| [aws_backup_logically_air_gapped_vault.ab_airgapped_vault](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_logically_air_gapped_vault) | resource | | [aws_backup_plan.ab_plan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_plan) | resource | | [aws_backup_plan.ab_plans](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_plan) | resource | | [aws_backup_report_plan.ab_report](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_report_plan) | resource | @@ -116,8 +143,8 @@ No modules. | [iam\_role\_arn](#input\_iam\_role\_arn) | If configured, the module will attach this role to selections, instead of creating IAM resources by itself | `string` | `null` | no | | [iam\_role\_name](#input\_iam\_role\_name) | Allow to set IAM role name, otherwise use predefined default | `string` | `""` | no | | [locked](#input\_locked) | Change to true to add a lock configuration for the backup vault | `bool` | `false` | no | -| [max\_retention\_days](#input\_max\_retention\_days) | The maximum retention period that the vault retains its recovery points | `number` | `null` | no | -| [min\_retention\_days](#input\_min\_retention\_days) | The minimum retention period that the vault retains its recovery points | `number` | `null` | no | +| [max\_retention\_days](#input\_max\_retention\_days) | The maximum retention period that the vault retains its recovery points. Required when vault\_type is 'logically\_air\_gapped' | `number` | `null` | no | +| [min\_retention\_days](#input\_min\_retention\_days) | The minimum retention period that the vault retains its recovery points. Required when vault\_type is 'logically\_air\_gapped' | `number` | `null` | no | | [notifications](#input\_notifications) | Notification block which defines backup vault events and the SNS Topic ARN to send AWS Backup notifications to. Leave it empty to disable notifications | `any` | `{}` | no | | [notifications\_disable\_sns\_policy](#input\_notifications\_disable\_sns\_policy) | Disable the creation of the SNS policy. Enable if you need to manage the policy elsewhere. | `bool` | `false` | no | | [org\_policy\_description](#input\_org\_policy\_description) | Description of the AWS Organizations backup policy | `string` | `"AWS Organizations backup policy"` | no | @@ -145,12 +172,16 @@ No modules. | [vault\_force\_destroy](#input\_vault\_force\_destroy) | A boolean that indicates that all recovery points stored in the vault are deleted so that the vault can be destroyed without error | `bool` | `false` | no | | [vault\_kms\_key\_arn](#input\_vault\_kms\_key\_arn) | The server-side encryption key that is used to protect your backups | `string` | `null` | no | | [vault\_name](#input\_vault\_name) | Name of the backup vault to create. If not given, AWS use default | `string` | `null` | no | +| [vault\_type](#input\_vault\_type) | Type of backup vault to create. Valid values are 'standard' (default) or 'logically\_air\_gapped' | `string` | `"standard"` | no | | [windows\_vss\_backup](#input\_windows\_vss\_backup) | Enable Windows VSS backup option and create a VSS Windows backup | `bool` | `false` | no | ## Outputs | Name | Description | |------|-------------| +| [airgapped\_vault\_arn](#output\_airgapped\_vault\_arn) | The ARN of the air gapped vault | +| [airgapped\_vault\_id](#output\_airgapped\_vault\_id) | The name of the air gapped vault | +| [airgapped\_vault\_recovery\_points](#output\_airgapped\_vault\_recovery\_points) | The number of recovery points stored in the air gapped vault (sensitive for security) | | [framework\_arn](#output\_framework\_arn) | The ARN of the backup framework | | [framework\_creation\_time](#output\_framework\_creation\_time) | The date and time that the backup framework was created | | [framework\_id](#output\_framework\_id) | The unique identifier of the backup framework | @@ -162,6 +193,22 @@ No modules. | [plans](#output\_plans) | Map of plans created and their attributes | | [vault\_arn](#output\_vault\_arn) | The ARN of the vault | | [vault\_id](#output\_vault\_id) | The name of the vault | +| [vault\_type](#output\_vault\_type) | The type of vault created | + + +## Known Issues + +During the development of the module, the following issues were found: + +### Error creating Backup Vault + +In case you get an error message similar to this one: + +``` +error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, +``` + +Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. ## Known Issues diff --git a/examples/logically_air_gapped_vault/README.md b/examples/logically_air_gapped_vault/README.md index cfd0180..6012bc5 100644 --- a/examples/logically_air_gapped_vault/README.md +++ b/examples/logically_air_gapped_vault/README.md @@ -137,4 +137,4 @@ module "compliance_backup" { - **No KMS Support**: Custom KMS encryption keys are not supported (AWS-managed encryption only) - **No Force Destroy**: Cannot use `force_destroy` parameter - vaults must be manually emptied before deletion - **Limited Parameters**: Only supports `name`, `min_retention_days`, `max_retention_days`, `tags`, and `region` -- **API Constraints**: Some AWS Backup APIs may have throttling limitations for air gapped operations \ No newline at end of file +- **API Constraints**: Some AWS Backup APIs may have throttling limitations for air gapped operations diff --git a/examples/logically_air_gapped_vault/main.tf b/examples/logically_air_gapped_vault/main.tf index 9209dd3..5347728 100644 --- a/examples/logically_air_gapped_vault/main.tf +++ b/examples/logically_air_gapped_vault/main.tf @@ -8,7 +8,7 @@ provider "aws" { # Note: The following settings are for development/testing only # Remove these in production to ensure proper validation # skip_metadata_api_check = true - # skip_region_validation = true + # skip_region_validation = true # skip_credentials_validation = true } @@ -35,4 +35,4 @@ module "aws_backup_plan" { # Common tags tags = var.tags -} \ No newline at end of file +} diff --git a/examples/logically_air_gapped_vault/outputs.tf b/examples/logically_air_gapped_vault/outputs.tf index 2036b70..f63f73d 100644 --- a/examples/logically_air_gapped_vault/outputs.tf +++ b/examples/logically_air_gapped_vault/outputs.tf @@ -27,11 +27,12 @@ output "airgapped_vault_arn" { value = module.aws_backup_plan.airgapped_vault_arn } -output "airgapped_vault_recovery_points" { - description = "The number of recovery points stored in the air gapped vault (sensitive for security)" - value = module.aws_backup_plan.airgapped_vault_recovery_points - sensitive = true -} +# Note: recovery_points attribute may not be available in all provider versions +# output "airgapped_vault_recovery_points" { +# description = "The number of recovery points stored in the air gapped vault (sensitive for security)" +# value = module.aws_backup_plan.airgapped_vault_recovery_points +# sensitive = true +# } output "plan_id" { description = "The id of the backup plan" @@ -51,4 +52,4 @@ output "plan_version" { output "plan_role" { description = "The service role of the backup plan" value = module.aws_backup_plan.plan_role -} \ No newline at end of file +} diff --git a/examples/logically_air_gapped_vault/variables.tf b/examples/logically_air_gapped_vault/variables.tf index 318afc1..54b6cbf 100644 --- a/examples/logically_air_gapped_vault/variables.tf +++ b/examples/logically_air_gapped_vault/variables.tf @@ -17,7 +17,7 @@ variable "vault_name" { variable "min_retention_days" { description = "Minimum retention period that the vault retains its recovery points" type = number - default = 7 # AWS minimum, allows for flexible compliance needs + default = 7 # AWS minimum, allows for flexible compliance needs validation { condition = var.min_retention_days >= 7 && var.min_retention_days <= 2555 @@ -28,13 +28,12 @@ variable "min_retention_days" { variable "max_retention_days" { description = "Maximum retention period that the vault retains its recovery points" type = number - default = 2555 # 7 years for compliance - configurable for different compliance needs + default = 2555 # 7 years for compliance - configurable for different compliance needs validation { condition = var.max_retention_days >= 1 && var.max_retention_days <= 2555 error_message = "The max_retention_days must be between 1 and 2555 days (7 years maximum for compliance)." } - } variable "plan_name" { @@ -52,7 +51,7 @@ variable "rule_name" { variable "rule_schedule" { description = "A CRON expression specifying when AWS Backup initiates a backup job" type = string - default = "cron(0 1 ? * * *)" # Daily at 1 AM + default = "cron(0 1 ? * * *)" # Daily at 1 AM } variable "selection_name" { @@ -80,4 +79,4 @@ variable "tags" { Compliance = "SOX" Owner = "data-governance-team" } -} \ No newline at end of file +} diff --git a/examples/logically_air_gapped_vault/versions.tf b/examples/logically_air_gapped_vault/versions.tf index 21b8607..a74a7ae 100644 --- a/examples/logically_air_gapped_vault/versions.tf +++ b/examples/logically_air_gapped_vault/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.11.0" # Required for aws_backup_logically_air_gapped_vault + version = ">= 6.11.0" # Required for aws_backup_logically_air_gapped_vault } } -} \ No newline at end of file +} diff --git a/main.tf b/main.tf index 6104358..fb5cc7e 100644 --- a/main.tf +++ b/main.tf @@ -2,11 +2,11 @@ # Organized locals for better maintainability and code clarity locals { # Resource creation conditions - should_create_vault = var.enabled && var.vault_name != null - should_create_standard_vault = local.should_create_vault && var.vault_type == "standard" - should_create_airgapped_vault = local.should_create_vault && var.vault_type == "logically_air_gapped" - should_create_lock = local.should_create_standard_vault && var.locked - should_create_legacy_plan = var.enabled && length(var.plans) == 0 && length(local.rules) > 0 + should_create_vault = var.enabled && var.vault_name != null + should_create_standard_vault = local.should_create_vault && var.vault_type == "standard" + should_create_airgapped_vault = local.should_create_vault && var.vault_type == "logically_air_gapped" + should_create_lock = local.should_create_standard_vault && var.locked + should_create_legacy_plan = var.enabled && length(var.plans) == 0 && length(local.rules) > 0 # Validation helpers for vault lock configuration vault_lock_requirements_met = var.min_retention_days != null && var.max_retention_days != null @@ -15,7 +15,7 @@ locals { # Validation for air-gapped vault requirements airgapped_vault_requirements_met = var.vault_type != "logically_air_gapped" || (var.min_retention_days != null && var.max_retention_days != null) - + # Cross-validation for retention days (unified validation approach) retention_days_cross_valid = (var.min_retention_days == null || var.max_retention_days == null) || var.min_retention_days <= var.max_retention_days @@ -176,10 +176,11 @@ resource "aws_backup_plan" "ab_plan" { # Tags tags = var.tags - # First create the vault if needed (only depend on the vault type being used) - depends_on = local.should_create_standard_vault ? [aws_backup_vault.ab_vault[0]] : ( - local.should_create_airgapped_vault ? [aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0]] : null - ) + # First create the vault if needed + depends_on = [ + aws_backup_vault.ab_vault, + aws_backup_logically_air_gapped_vault.ab_airgapped_vault + ] lifecycle { precondition { @@ -264,10 +265,11 @@ resource "aws_backup_plan" "ab_plans" { # Tags tags = var.tags - # First create the vault if needed (only depend on the vault type being used) - depends_on = local.should_create_standard_vault ? [aws_backup_vault.ab_vault[0]] : ( - local.should_create_airgapped_vault ? [aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0]] : null - ) + # First create the vault if needed + depends_on = [ + aws_backup_vault.ab_vault, + aws_backup_logically_air_gapped_vault.ab_airgapped_vault + ] lifecycle { precondition { diff --git a/outputs.tf b/outputs.tf index dbf7157..f0d95d4 100644 --- a/outputs.tf +++ b/outputs.tf @@ -25,11 +25,12 @@ output "airgapped_vault_arn" { value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].arn, null) } -output "airgapped_vault_recovery_points" { - description = "The number of recovery points stored in the air gapped vault (sensitive for security)" - value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].recovery_points, null) - sensitive = true -} +# Note: recovery_points attribute may not be available in all provider versions +# output "airgapped_vault_recovery_points" { +# description = "The number of recovery points stored in the air gapped vault (sensitive for security)" +# value = try(aws_backup_logically_air_gapped_vault.ab_airgapped_vault[0].recovery_points, null) +# sensitive = true +# } # Legacy Plan output "plan_id" { diff --git a/test/examples_test.go b/test/examples_test.go index 6479160..68def5d 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -27,6 +27,7 @@ func TestExamplesValidation(t *testing.T) { "aws_recommended_audit_framework", "complete_audit_framework", "simple_audit_framework", + "logically_air_gapped_vault", } for _, example := range examples { diff --git a/test/fixtures/terraform/air_gapped_vault/main.tf b/test/fixtures/terraform/air_gapped_vault/main.tf new file mode 100644 index 0000000..e0cfa24 --- /dev/null +++ b/test/fixtures/terraform/air_gapped_vault/main.tf @@ -0,0 +1,45 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.11.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +module "backup" { + source = "../../../../" + + # Vault configuration - Air Gapped + vault_name = var.vault_name + vault_type = var.vault_type + min_retention_days = var.min_retention_days + max_retention_days = var.max_retention_days + + # Plan configuration + plan_name = var.plan_name + + # Rule configuration + rule_name = "test-rule" + rule_schedule = "cron(0 2 ? * * *)" # Daily at 2 AM + + # Selection of resources for testing + selection_name = var.selection_name + selection_resources = [ + "arn:aws:ec2:*:*:volume/*", + "arn:aws:dynamodb:*:*:table/*" + ] + + # Tags for testing + tags = { + Environment = "test" + Purpose = "terratest" + VaultType = var.vault_type + } +} \ No newline at end of file diff --git a/test/fixtures/terraform/air_gapped_vault/outputs.tf b/test/fixtures/terraform/air_gapped_vault/outputs.tf new file mode 100644 index 0000000..2531496 --- /dev/null +++ b/test/fixtures/terraform/air_gapped_vault/outputs.tf @@ -0,0 +1,29 @@ +output "vault_id" { + description = "The name of the air gapped vault" + value = module.backup.vault_id +} + +output "vault_arn" { + description = "The ARN of the air gapped vault" + value = module.backup.vault_arn +} + +output "vault_type" { + description = "The type of vault created" + value = module.backup.vault_type +} + +output "plan_id" { + description = "The id of the backup plan" + value = module.backup.plan_id +} + +output "plan_arn" { + description = "The ARN of the backup plan" + value = module.backup.plan_arn +} + +output "plan_version" { + description = "Unique, randomly generated, Unicode, UTF-8 encoded string that serves as the version ID of the backup plan" + value = module.backup.plan_version +} \ No newline at end of file diff --git a/test/fixtures/terraform/air_gapped_vault/variables.tf b/test/fixtures/terraform/air_gapped_vault/variables.tf new file mode 100644 index 0000000..6ba2ec7 --- /dev/null +++ b/test/fixtures/terraform/air_gapped_vault/variables.tf @@ -0,0 +1,38 @@ +variable "aws_region" { + description = "AWS region for testing" + type = string + default = "us-east-1" +} + +variable "vault_name" { + description = "Name of the backup vault to create" + type = string +} + +variable "vault_type" { + description = "Type of backup vault to create" + type = string + default = "logically_air_gapped" +} + +variable "min_retention_days" { + description = "Minimum retention period that the vault retains its recovery points" + type = number + default = 7 +} + +variable "max_retention_days" { + description = "Maximum retention period that the vault retains its recovery points" + type = number + default = 30 +} + +variable "plan_name" { + description = "The display name of a backup plan" + type = string +} + +variable "selection_name" { + description = "The display name of a resource selection document" + type = string +} \ No newline at end of file diff --git a/test/integration_test.go b/test/integration_test.go index 2af7caa..3ef1540 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -688,3 +688,117 @@ func validateDynamoDBTableRestore(t *testing.T, client *dynamodb.DynamoDB, table t.Logf("DynamoDB table restore validation completed successfully") } + +// TestLogicallyAirGappedVault tests the creation and management of a logically air-gapped backup vault +func TestLogicallyAirGappedVault(t *testing.T) { + // Skip if running in CI without AWS credentials + if os.Getenv("CI") != "" && os.Getenv("AWS_ACCESS_KEY_ID") == "" { + t.Skip("Skipping integration test in CI without AWS credentials") + } + + t.Parallel() + + // Generate unique names for this test + planName := GenerateUniqueBackupPlanName(t) + vaultName := GenerateUniqueBackupVaultName(t) + "-airgapped" + selectionName := GenerateUniqueSelectionName(t) + + // Set up AWS session + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String("us-east-1"), + })) + + backupClient := backup.New(sess) + + terraformOptions := &terraform.Options{ + TerraformDir: "fixtures/terraform/air_gapped_vault", + Vars: map[string]interface{}{ + "plan_name": planName, + "vault_name": vaultName, + "selection_name": selectionName, + "vault_type": "logically_air_gapped", + "min_retention_days": 7, + "max_retention_days": 30, + "aws_region": "us-east-1", + }, + NoColor: true, + } + + // Clean up resources on test completion + defer terraform.Destroy(t, terraformOptions) + + // Deploy the infrastructure + terraform.InitAndApply(t, terraformOptions) + + // Get outputs + planId := terraform.Output(t, terraformOptions, "plan_id") + vaultId := terraform.Output(t, terraformOptions, "vault_id") + vaultArn := terraform.Output(t, terraformOptions, "vault_arn") + vaultType := terraform.Output(t, terraformOptions, "vault_type") + + // Verify plan was created + assert.NotEmpty(t, planId, "Backup plan ID should not be empty") + t.Logf("Created backup plan: %s", planId) + + // Verify air-gapped vault was created + assert.NotEmpty(t, vaultId, "Air-gapped vault ID should not be empty") + assert.NotEmpty(t, vaultArn, "Air-gapped vault ARN should not be empty") + assert.Equal(t, "logically_air_gapped", vaultType, "Vault type should be logically_air_gapped") + t.Logf("Created air-gapped vault: %s", vaultId) + + // Verify the air-gapped vault exists and has correct configuration + RetryableAWSOperation(t, "describe air-gapped vault", func() error { + input := &backup.DescribeBackupVaultInput{ + BackupVaultName: aws.String(vaultId), + } + + result, err := backupClient.DescribeBackupVault(input) + if err != nil { + return err + } + + // Verify vault properties + assert.Equal(t, vaultId, *result.BackupVaultName, "Vault name should match") + assert.Equal(t, vaultArn, *result.BackupVaultArn, "Vault ARN should match") + + // Note: AWS API doesn't always expose vault type in DescribeBackupVault + // The vault type is validated through Terraform outputs + t.Logf("Air-gapped vault verified: %s", *result.BackupVaultName) + + return nil + }) + + // Verify the backup plan exists and is associated with the air-gapped vault + RetryableAWSOperation(t, "describe backup plan", func() error { + input := &backup.GetBackupPlanInput{ + BackupPlanId: aws.String(planId), + } + + result, err := backupClient.GetBackupPlan(input) + if err != nil { + return err + } + + // Verify plan properties + assert.Equal(t, planId, *result.BackupPlanId, "Plan ID should match") + assert.NotEmpty(t, result.BackupPlan.Rules, "Plan should have rules") + + // Verify first rule targets the air-gapped vault + if len(result.BackupPlan.Rules) > 0 { + rule := result.BackupPlan.Rules[0] + assert.Equal(t, vaultId, *rule.TargetBackupVaultName, "Rule should target the air-gapped vault") + t.Logf("Backup plan rule verified: targets vault %s", *rule.TargetBackupVaultName) + } + + return nil + }) + + // Verify retention configuration for air-gapped vault + // Note: Retention validation is primarily enforced by Terraform lifecycle rules + // The actual retention behavior is managed by AWS Backup service + t.Logf("Air-gapped vault test completed successfully") + t.Logf("โœ… Vault Type: %s", vaultType) + t.Logf("โœ… Vault ID: %s", vaultId) + t.Logf("โœ… Plan ID: %s", planId) + t.Logf("โœ… Retention enforced through Terraform validation") +} diff --git a/versions.tf b/versions.tf index a9e6407..f35245c 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.0.0" + version = ">= 6.11.0" } } } From acb058d1f63187eb59100858916472dff92fb5d0 Mon Sep 17 00:00:00 2001 From: "Luis M. Gallardo D" Date: Mon, 15 Sep 2025 20:22:45 +0200 Subject: [PATCH 08/10] fix: pre-commit cleanup - add missing newlines and update docs --- README.md | 44 +++++++------------ .../terraform/air_gapped_vault/main.tf | 2 +- .../terraform/air_gapped_vault/outputs.tf | 2 +- .../terraform/air_gapped_vault/variables.tf | 2 +- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index bec6157..50dee8f 100644 --- a/README.md +++ b/README.md @@ -55,32 +55,6 @@ See [examples/organization_backup_policy/main.tf](examples/organization_backup_p See [examples/simple_audit_framework/main.tf](examples/simple_audit_framework/main.tf) for audit framework configuration. -### Logically Air Gapped Vault - -This module supports AWS Backup Logically Air Gapped Vaults for enhanced security and compliance requirements. Air-gapped vaults provide isolated storage with immutable retention policies. - -See [examples/logically_air_gapped_vault/main.tf](examples/logically_air_gapped_vault/main.tf) for air-gapped vault configuration. - -**Key Features:** -- **Enhanced Security**: Logical isolation from standard backup infrastructure -- **Immutable Retention**: Mandatory min/max retention policies that cannot be bypassed -- **Compliance Ready**: Supports SOX, PCI-DSS, HIPAA, and other regulatory requirements -- **AWS-Managed Encryption**: Built-in encryption (custom KMS keys not supported) - -**Usage:** -```hcl -module "compliance_backup" { - source = "lgallard/backup/aws" - - vault_name = "compliance-vault" - vault_type = "logically_air_gapped" - min_retention_days = 7 - max_retention_days = 2555 # 7 years for compliance - - # ... other configuration -} -``` - ## Requirements @@ -93,7 +67,7 @@ module "compliance_backup" { | Name | Version | |------|---------| -| [aws](#provider\_aws) | 6.3.0 | +| [aws](#provider\_aws) | 6.13.0 | ## Modules @@ -181,7 +155,6 @@ No modules. |------|-------------| | [airgapped\_vault\_arn](#output\_airgapped\_vault\_arn) | The ARN of the air gapped vault | | [airgapped\_vault\_id](#output\_airgapped\_vault\_id) | The name of the air gapped vault | -| [airgapped\_vault\_recovery\_points](#output\_airgapped\_vault\_recovery\_points) | The number of recovery points stored in the air gapped vault (sensitive for security) | | [framework\_arn](#output\_framework\_arn) | The ARN of the backup framework | | [framework\_creation\_time](#output\_framework\_creation\_time) | The date and time that the backup framework was created | | [framework\_id](#output\_framework\_id) | The unique identifier of the backup framework | @@ -271,6 +244,21 @@ error creating Backup Vault (): AccessDeniedException: status code: 403, request Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. +## Known Issues + +During the development of the module, the following issues were found: + +### Error creating Backup Vault + +In case you get an error message similar to this one: + +``` +error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, +``` + +Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. + + ## Testing This module includes comprehensive testing to ensure reliability and prevent regressions. diff --git a/test/fixtures/terraform/air_gapped_vault/main.tf b/test/fixtures/terraform/air_gapped_vault/main.tf index e0cfa24..dc8785e 100644 --- a/test/fixtures/terraform/air_gapped_vault/main.tf +++ b/test/fixtures/terraform/air_gapped_vault/main.tf @@ -42,4 +42,4 @@ module "backup" { Purpose = "terratest" VaultType = var.vault_type } -} \ No newline at end of file +} diff --git a/test/fixtures/terraform/air_gapped_vault/outputs.tf b/test/fixtures/terraform/air_gapped_vault/outputs.tf index 2531496..b36ed50 100644 --- a/test/fixtures/terraform/air_gapped_vault/outputs.tf +++ b/test/fixtures/terraform/air_gapped_vault/outputs.tf @@ -26,4 +26,4 @@ output "plan_arn" { output "plan_version" { description = "Unique, randomly generated, Unicode, UTF-8 encoded string that serves as the version ID of the backup plan" value = module.backup.plan_version -} \ No newline at end of file +} diff --git a/test/fixtures/terraform/air_gapped_vault/variables.tf b/test/fixtures/terraform/air_gapped_vault/variables.tf index 6ba2ec7..fa6f442 100644 --- a/test/fixtures/terraform/air_gapped_vault/variables.tf +++ b/test/fixtures/terraform/air_gapped_vault/variables.tf @@ -35,4 +35,4 @@ variable "plan_name" { variable "selection_name" { description = "The display name of a resource selection document" type = string -} \ No newline at end of file +} From 6c514d50ccc77b5599932a8451d4d7ee425a5b9a Mon Sep 17 00:00:00 2001 From: "Luis M. Gallardo D" Date: Mon, 15 Sep 2025 20:59:20 +0200 Subject: [PATCH 09/10] fix: update CI matrix to support AWS provider >= 6.11.0 for air gapped vault - Update aws_provider_version matrix from [5.0.0, 5.70.0] to [6.11.0, 6.70.0] - Add logically_air_gapped_vault example to validation matrix - Update documentation via terraform-docs - Fix CI failures with unsupported resource in older AWS provider versions --- .github/workflows/validate.yml | 5 +++-- README.md | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4f77d1f..4458efd 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: terraform_version: ['1.3.0', '1.5.0', '1.9.0'] - aws_provider_version: ['5.0.0', '5.70.0'] + aws_provider_version: ['6.11.0', '6.70.0'] steps: - name: Checkout @@ -72,7 +72,8 @@ jobs: 'multiple_plans', 'aws_recommended_audit_framework', 'complete_audit_framework', - 'simple_audit_framework' + 'simple_audit_framework', + 'logically_air_gapped_vault' ] steps: diff --git a/README.md b/README.md index 50dee8f..858b9cc 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,21 @@ error creating Backup Vault (): AccessDeniedException: status code: 403, request Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. +## Known Issues + +During the development of the module, the following issues were found: + +### Error creating Backup Vault + +In case you get an error message similar to this one: + +``` +error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, +``` + +Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. + + ## Testing This module includes comprehensive testing to ensure reliability and prevent regressions. From d366c6011ab120a63eaa92079a2e19c12e634a45 Mon Sep 17 00:00:00 2001 From: "Luis M. Gallardo D" Date: Mon, 15 Sep 2025 21:26:21 +0200 Subject: [PATCH 10/10] fix: resolve CI failures and pre-commit issues - Update AWS provider versions in CI matrix from 6.70.0 to 6.13.0 (latest available) - Fix terraform-docs generated documentation and remove duplicate sections - All pre-commit hooks now pass cleanly - Terraform validation will work with supported provider versions --- .github/workflows/validate.yml | 2 +- README.md | 90 ---------------------------------- 2 files changed, 1 insertion(+), 91 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4458efd..5d87f03 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: terraform_version: ['1.3.0', '1.5.0', '1.9.0'] - aws_provider_version: ['6.11.0', '6.70.0'] + aws_provider_version: ['6.11.0', '6.13.0'] steps: - name: Checkout diff --git a/README.md b/README.md index 858b9cc..a070e1b 100644 --- a/README.md +++ b/README.md @@ -184,96 +184,6 @@ error creating Backup Vault (): AccessDeniedException: status code: 403, request Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. -## Known Issues - -During the development of the module, the following issues were found: - -### Error creating Backup Vault - -In case you get an error message similar to this one: - -``` -error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, -``` - -Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. - - -## Known Issues - -During the development of the module, the following issues were found: - -### Error creating Backup Vault - -In case you get an error message similar to this one: - -``` -error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, -``` - -Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. - - -## Known Issues - -During the development of the module, the following issues were found: - -### Error creating Backup Vault - -In case you get an error message similar to this one: - -``` -error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, -``` - -Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. - - -## Known Issues - -During the development of the module, the following issues were found: - -### Error creating Backup Vault - -In case you get an error message similar to this one: - -``` -error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, -``` - -Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. - - -## Known Issues - -During the development of the module, the following issues were found: - -### Error creating Backup Vault - -In case you get an error message similar to this one: - -``` -error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, -``` - -Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. - - -## Known Issues - -During the development of the module, the following issues were found: - -### Error creating Backup Vault - -In case you get an error message similar to this one: - -``` -error creating Backup Vault (): AccessDeniedException: status code: 403, request id: 8e7e577e-5b74-4d4d-95d0-bf63e0b2cc2e, -``` - -Add the [required IAM permissions mentioned in the CreateBackupVault row](https://docs.aws.amazon.com/aws-backup/latest/devguide/access-control.html#backup-api-permissions-ref) to the role or user creating the Vault (the one running Terraform CLI). In particular make sure `kms` and `backup-storage` permissions are added. - - ## Testing This module includes comprehensive testing to ensure reliability and prevent regressions.