diff --git a/.github/workflows/firewall.yml b/.github/workflows/firewall.yml new file mode 100644 index 00000000..84884d72 --- /dev/null +++ b/.github/workflows/firewall.yml @@ -0,0 +1,77 @@ +name: Module:firewall +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - '.github/workflows/firewall.yml' + - 'terraform/firewall/**' + - '.github/actions/**' + +env: + terraform_workingdir: "terraform/firewall" + GH_TOKEN: ${{ secrets.GH_TOKEN }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + +jobs: + terraform-lint: + name: Run Terraform lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: "${{ env.terraform_workingdir }}" + + steps: + - uses: actions/checkout@v3 + - uses: hashicorp/setup-terraform@v2 + + - name: Terraform fmt + id: fmt + run: terraform fmt -check + continue-on-error: false + + terraform-sec: + name: Run Terraform tfsec + needs: + - terraform-lint + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Run tfsec with reviewdog output on the PR + uses: ./.github/actions/run-terraform-sec + + terratest: + name: Run Terratest + needs: + - terraform-sec + runs-on: ubuntu-latest + + defaults: + run: + working-directory: "${{ env.terraform_workingdir }}/test" + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18.2 + + - name: Setup Dependencies + run: go mod init test && go mod tidy + env: + GOPATH: "/home/runner/work/azure-labs-modules/azure-labs-modules/${{ env.terraform_workingdir }}" + + - name: Unit-test + run: go test -v -timeout 45m + env: + GOPATH: "/home/runner/work/azure-labs-modules/azure-labs-modules/${{ env.terraform_workingdir }}" \ No newline at end of file diff --git a/terraform/firewall/main.tf b/terraform/firewall/main.tf new file mode 100644 index 00000000..3adb9ea0 --- /dev/null +++ b/terraform/firewall/main.tf @@ -0,0 +1,105 @@ +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/firewall + +resource "azurerm_firewall" "adl_afw" { + name = "afw-${var.basename}" + location = var.location + resource_group_name = var.rg_name + sku_name = var.sku_name + sku_tier = var.sku_tier + zones = var.zones + threat_intel_mode = var.threat_intel_mode + firewall_policy_id = azurerm_firewall_policy.adl_afw_afwp[0].id + dynamic "ip_configuration" { + iterator = pip + for_each = var.sku_name != "AZFW_Hub" ? azurerm_public_ip.adl_afw_pip_config : [] + content { + name = lower("fw_ip_config_${pip.value.name}") + subnet_id = contains([pip.value.name], "pip-${var.basename}-1") ? var.subnet_id : null + public_ip_address_id = pip.value.id + } + } + dynamic "management_ip_configuration" { + for_each = var.management_subnet_id != null ? [1] : [] + content { + name = lower("fw_ip_management_${azurerm_public_ip.adl_afw_pip_mngmt[0].name}") + subnet_id = var.management_subnet_id != null ? var.management_subnet_id : null + public_ip_address_id = azurerm_public_ip.adl_afw_pip_mngmt[0].id + } + } + # dynamic "virtual_hub" { + # for_each = var.sku_name == "AZFW_Hub" ? [1] : [] + # content { + # virtual_hub_id = azurerm_virtual_hub.adl_afw_vhub[0].id + # public_ip_count = var.pip_count + # } + # } + tags = var.tags + + count = var.module_enabled ? 1 : 0 +} + +# Public IP for the firewall + +resource "azurerm_public_ip" "adl_afw_pip_config" { + count = var.sku_name != "AZFW_Hub" && var.module_enabled ? var.public_ip_count : 0 + name = "pip-${var.basename}-${count.index + 1}" + location = var.location + resource_group_name = var.rg_name + zones = var.zones + allocation_method = var.pip_allocation_method + sku = var.pip_sku + + tags = var.tags +} + +resource "azurerm_public_ip" "adl_afw_pip_mngmt" { + count = var.sku_name != "AZFW_Hub" && var.module_enabled ? 1 : 0 + name = "pip-${var.basename}-mngmt" + location = var.location + resource_group_name = var.rg_name + zones = var.zones + allocation_method = var.pip_allocation_method + sku = var.pip_sku + + tags = var.tags +} + +# Firewall policy + +resource "azurerm_firewall_policy" "adl_afw_afwp" { + name = "afwp-${var.basename}" + resource_group_name = var.rg_name + location = var.location + sku = var.sku_policy + + dynamic "dns" { + for_each = var.proxy_enabled != false ? [1] : [] + content { + proxy_enabled = true + servers = var.dns_servers != null ? var.dns_servers : null + } + } + tags = var.tags + + count = var.module_enabled ? 1 : 0 +} + +# # Virtual Hub + +# resource "azurerm_virtual_wan" "adl_afw_vwan" { +# name = "vwan-${var.basename}" +# resource_group_name = var.rg_name +# location = var.location + +# count = var.sku_name == "AZFW_Hub" && var.module_enabled ? 1 : 0 +# } + +# resource "azurerm_virtual_hub" "adl_afw_vhub" { +# name = "vhub-${var.basename}" +# resource_group_name = var.rg_name +# location = var.location +# virtual_wan_id = azurerm_virtual_wan.adl_afw_vwan[0].id +# address_prefix = "10.0.1.0/24" + +# count = var.sku_name == "AZFW_Hub" && var.module_enabled ? 1 : 0 +# } \ No newline at end of file diff --git a/terraform/firewall/outputs.tf b/terraform/firewall/outputs.tf new file mode 100644 index 00000000..5f56bdc2 --- /dev/null +++ b/terraform/firewall/outputs.tf @@ -0,0 +1,20 @@ +output "id" { + value = ( + length(azurerm_firewall.adl_afw) > 0 ? + azurerm_firewall.adl_afw[0].id : "" + ) +} + +output "name" { + value = ( + length(azurerm_firewall.adl_afw) > 0 ? + azurerm_firewall.adl_afw[0].name : "" + ) +} + +output "resource_group_name" { + value = ( + length(azurerm_firewall.adl_afw) > 0 ? + azurerm_firewall.adl_afw[0].resource_group_name : "" + ) +} \ No newline at end of file diff --git a/terraform/firewall/test/firewall.tf b/terraform/firewall/test/firewall.tf new file mode 100644 index 00000000..fc929ce7 --- /dev/null +++ b/terraform/firewall/test/firewall.tf @@ -0,0 +1,51 @@ +module "firewall" { + source = "../" + + basename = random_string.postfix.result + rg_name = module.local_rg.name + location = var.location + + subnet_id = module.local_snet_default.id + management_subnet_id = module.local_snet_mngmt.id + + tags = {} +} + +# Modules dependencies + +module "local_rg" { + source = "../../resource-group" + + basename = random_string.postfix.result + location = var.location + + tags = local.tags +} + +module "local_vnet" { + source = "../../virtual-network" + + rg_name = module.local_rg.name + basename = random_string.postfix.result + location = var.location + + address_space = ["10.0.0.0/16"] +} + +module "local_snet_default" { + source = "../../subnet" + + rg_name = module.local_rg.name + name = "AzureFirewallSubnet" + vnet_name = module.local_vnet.name + address_prefixes = ["10.0.6.0/24"] +} + +module "local_snet_mngmt" { + source = "../../subnet" + + rg_name = module.local_rg.name + name = "AzureFirewallManagementSubnet" + vnet_name = module.local_vnet.name + address_prefixes = ["10.0.5.0/24"] +} \ No newline at end of file diff --git a/terraform/firewall/test/locals.tf b/terraform/firewall/test/locals.tf new file mode 100644 index 00000000..36d8b3b3 --- /dev/null +++ b/terraform/firewall/test/locals.tf @@ -0,0 +1,7 @@ +locals { + tags = { + Project = "Azure/azure-data-labs-modules" + Module = "firewall" + Toolkit = "Terraform" + } +} \ No newline at end of file diff --git a/terraform/firewall/test/outputs.tf b/terraform/firewall/test/outputs.tf new file mode 100644 index 00000000..5df414c0 --- /dev/null +++ b/terraform/firewall/test/outputs.tf @@ -0,0 +1,11 @@ +output "id" { + value = module.firewall.id +} + +output "name" { + value = module.firewall.name +} + +output "resource_group_name" { + value = module.firewall.resource_group_name +} \ No newline at end of file diff --git a/terraform/firewall/test/providers.tf b/terraform/firewall/test/providers.tf new file mode 100644 index 00000000..0951994a --- /dev/null +++ b/terraform/firewall/test/providers.tf @@ -0,0 +1,19 @@ +terraform { + backend "azurerm" { + resource_group_name = "rg-adl-terraform-state" + storage_account_name = "stadlterraformstate" + container_name = "default" + key = "firewall.terraform.tfstate" + } + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "= 3.43.0" + } + } +} + +provider "azurerm" { + features {} +} \ No newline at end of file diff --git a/terraform/firewall/test/unit_test.go b/terraform/firewall/test/unit_test.go new file mode 100644 index 00000000..55336365 --- /dev/null +++ b/terraform/firewall/test/unit_test.go @@ -0,0 +1,36 @@ +package test + +import ( + "testing" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestModule(t *testing.T) { + t.Parallel() + + terraformOptions := &terraform.Options{ + TerraformDir: "./", + Lock: true, + LockTimeout: "1800s", + // VarFiles: []string{"terraform_unitest.tfvars"}, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // Is used mainly for debugging, fail early if plan is not possible + terraform.InitAndPlan(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Check if the outputs exist + assert := assert.New(t) + id := terraform.Output(t, terraformOptions, "id") + assert.NotNil(id) + name := terraform.Output(t, terraformOptions, "name") + assert.NotNil(name) + resource_group_name := terraform.Output(t, terraformOptions, "resource_group_name") + assert.NotNil(resource_group_name) +} \ No newline at end of file diff --git a/terraform/firewall/test/variables.tf b/terraform/firewall/test/variables.tf new file mode 100644 index 00000000..b537747b --- /dev/null +++ b/terraform/firewall/test/variables.tf @@ -0,0 +1,10 @@ +resource "random_string" "postfix" { + length = 8 + special = false + upper = false +} + +variable "location" { + type = string + default = "North Europe" +} \ No newline at end of file diff --git a/terraform/firewall/variables.tf b/terraform/firewall/variables.tf new file mode 100644 index 00000000..44cfa649 --- /dev/null +++ b/terraform/firewall/variables.tf @@ -0,0 +1,130 @@ +variable "basename" { + type = string + description = "Basename of the module." + validation { + condition = can(regex("^[-.\\w]{1,52}$", var.basename)) && can(regex("[\\w]+$", var.basename)) + error_message = "The name must be between 1 and 52 characters, must end with a letter, number or underscore, and may contain only letters, numbers, underscores, periods, or hyphens." + } +} + +variable "rg_name" { + type = string + description = "Resource group name." + validation { + condition = can(regex("^[-\\w\\.\\(\\)]{1,90}$", var.rg_name)) && can(regex("[-\\w\\(\\)]+$", var.rg_name)) + error_message = "Resource group names must be between 1 and 90 characters and can only include alphanumeric, underscore, parentheses, hyphen, period (except at end)." + } +} + +variable "location" { + type = string + description = "Location of the resource group." +} + +variable "tags" { + type = map(string) + default = {} + description = "A mapping of tags which should be assigned to the deployed resource." +} + +variable "module_enabled" { + type = bool + description = "Variable to enable or disable the module." + default = true +} + +variable "sku_name" { + type = string + description = "(Required) SKU name of the Firewall. Possible values are AZFW_Hub and AZFW_VNet. Changing this forces a new resource to be created." + validation { + condition = contains(["azfw_hub", "azfw_vnet"], lower(var.sku_name)) + error_message = "Valid values for sku_name are \"AZFW_Hub\" or \"AZFW_VNet\"." + } + default = "AZFW_VNet" +} + +variable "sku_tier" { + type = string + description = "(Required) SKU tier of the Firewall. Possible values are Premium, Standard and Basic." + validation { + condition = contains(["premium", "basic", "standard"], lower(var.sku_tier)) + error_message = "Valid values for sku_tier are \"Premium\", \"Standard\" or \"Basic\"." + } + default = "Standard" +} + +variable "zones" { + type = list(string) + description = "(Optional) Specifies a list of Availability Zones in which this Azure Firewall should be located. Changing this forces a new Azure Firewall to be created." + default = [] +} + +variable "threat_intel_mode" { + type = string + description = "(Optional) The operation mode for threat intelligence-based filtering. Possible values are: Off, Alert and Deny. Defaults to Alert." + validation { + condition = contains(["off", "alert", "deny"], lower(var.threat_intel_mode)) + error_message = "Valid values for sku_tier are \"Off\", \"Alert\" or \"Deny\"." + } + default = "Alert" +} + +variable "subnet_id" { + type = string + description = "(Optional) Reference to the subnet associated with the IP Configuration." + default = null +} + +variable "management_subnet_id" { + type = string + description = "(Required) Reference to the subnet associated with the IP Configuration. Changing this forces a new resource to be created. The Management Subnet used for the Firewall must have the name AzureFirewallManagementSubnet and the subnet mask must be at least a /26." + default = null +} + +variable "public_ip_count" { + type = number + description = "(Optional) Specifies the number of public IPs to assign to the Firewall. Defaults to 1." + default = 1 +} + +variable "pip_allocation_method" { + type = string + description = "(Required) Defines the allocation method for this IP address. Possible values are Static or Dynamic." + validation { + condition = contains(["static", "dynamic"], lower(var.pip_allocation_method)) + error_message = "Valid values for pip_allocation_method are \"Static\" or \"Dynamic\"." + } + default = "Static" +} + +variable "pip_sku" { + type = string + description = "(Optional) The SKU of the Public IP. Accepted values are Basic and Standard. Defaults to Basic. Changing this forces a new resource to be created." + validation { + condition = contains(["basic", "standard"], lower(var.pip_sku)) + error_message = "Valid values for pip_sku are \"Basic\" or \"Standard\"." + } + default = "Standard" +} + +variable "sku_policy" { + type = string + description = "(Optional) The SKU Tier of the Firewall Policy. Possible values are Standard, Premium. Changing this forces a new Firewall Policy to be created." + validation { + condition = contains(["Premium", "Standard"], var.sku_policy) + error_message = "Valid values for sku_policy are \"Premium\" or \"Standard\"." + } + default = "Standard" +} + +variable "proxy_enabled" { + type = bool + description = "(Optional) Whether to enable DNS proxy on Firewalls attached to this Firewall Policy? Defaults to false." + default = false +} + +variable "dns_servers" { + type = list(string) + description = "(Optional) A list of custom DNS servers' IP addresses." + default = null +} \ No newline at end of file