diff --git a/terraform/README.md b/terraform/README.md index cf1c045b..401950e8 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -28,6 +28,12 @@ All resources in these Resource Groups should be reflected in Terraform in this For browsing the [Azure portal](https://portal.azure.com), you can [switch your `Default subscription filter`](https://docs.microsoft.com/en-us/azure/azure-portal/set-preferences). +## Access restrictions + +We restrict which IP addresses that can access the app service by using a Web Application Firewall (WAF) configured on a Front Door. There is an exception for the `/healthcheck` path, which can be accessed by any IP address. + +The app service itself gives access only to our Front Door and to Azure availability tests. + ## Monitoring We have [ping tests](https://docs.microsoft.com/en-us/azure/azure-monitor/app/monitor-web-app-availability) set up to notify about availability of each environment. Alerts go to [#benefits-notify](https://cal-itp.slack.com/archives/C022HHSEE3F). diff --git a/terraform/app_service.tf b/terraform/app_service.tf index b82f56cb..5fce0c67 100644 --- a/terraform/app_service.tf +++ b/terraform/app_service.tf @@ -22,13 +22,25 @@ resource "azurerm_linux_web_app" "main" { ftps_state = "Disabled" http2_enabled = true - dynamic "ip_restriction" { - for_each = var.IP_ADDRESS_WHITELIST - content { - ip_address = ip_restriction.value + vnet_route_all_enabled = true + + ip_restriction { + name = "Front Door" + priority = 100 + action = "Allow" + service_tag = "AzureFrontDoor.Backend" + headers { + x_azure_fdid = [azurerm_cdn_frontdoor_profile.main.resource_guid] } } - vnet_route_all_enabled = true + + ip_restriction { + name = "Availability Test" + priority = 200 + action = "Allow" + service_tag = "ApplicationInsightsAvailability" + } + application_stack { docker_image = "ghcr.io/cal-itp/eligibility-server" docker_image_tag = local.env_name diff --git a/terraform/environment.tf b/terraform/environment.tf index f3264b27..6daa1a61 100644 --- a/terraform/environment.tf +++ b/terraform/environment.tf @@ -1,5 +1,6 @@ locals { is_prod = terraform.workspace == "default" + is_test = terraform.workspace == "test" env_name = local.is_prod ? "prod" : terraform.workspace } diff --git a/terraform/front_door.tf b/terraform/front_door.tf new file mode 100644 index 00000000..f47431b6 --- /dev/null +++ b/terraform/front_door.tf @@ -0,0 +1,104 @@ +locals { + front_door_name = "eligibility-server-${local.env_name}" +} + +resource "azurerm_cdn_frontdoor_profile" "main" { + name = local.front_door_name + resource_group_name = data.azurerm_resource_group.main.name + sku_name = "Standard_AzureFrontDoor" +} + +resource "azurerm_cdn_frontdoor_endpoint" "main" { + # used in the front door URL + name = "mst-courtesy-cards-eligibility-server-${local.env_name}" + cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id +} + +resource "azurerm_cdn_frontdoor_origin_group" "main" { + name = local.front_door_name + cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id + + # this block is required, and it's empty because we are fine with using the default values + # https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/cdn_frontdoor_origin_group#load_balancing + load_balancing {} +} + +resource "azurerm_cdn_frontdoor_origin" "main" { + name = local.front_door_name + cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.main.id + + enabled = true + host_name = azurerm_linux_web_app.main.default_hostname + origin_host_header = azurerm_linux_web_app.main.default_hostname + certificate_name_check_enabled = true + weight = 1000 +} + +resource "azurerm_cdn_frontdoor_route" "main" { + name = local.front_door_name + cdn_frontdoor_endpoint_id = azurerm_cdn_frontdoor_endpoint.main.id + cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.main.id + cdn_frontdoor_origin_ids = [azurerm_cdn_frontdoor_origin.main.id] + + https_redirect_enabled = true + supported_protocols = ["Http", "Https"] + patterns_to_match = ["/*"] + forwarding_protocol = "HttpsOnly" + link_to_default_domain = true +} + +resource "azurerm_cdn_frontdoor_security_policy" "main" { + name = local.front_door_name + cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id + + security_policies { + firewall { + cdn_frontdoor_firewall_policy_id = azurerm_cdn_frontdoor_firewall_policy.main.id + association { + patterns_to_match = ["/*"] + domain { + cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_endpoint.main.host_name + } + } + } + } +} + +resource "azurerm_cdn_frontdoor_firewall_policy" "main" { + name = "${local.env_name}waf" + resource_group_name = data.azurerm_resource_group.main.name + sku_name = azurerm_cdn_frontdoor_profile.main.sku_name + enabled = true + mode = "Prevention" + custom_block_response_status_code = 403 + custom_block_response_body = base64encode("Forbidden") + + custom_rule { + name = "healthcheck" + enabled = true + type = "MatchRule" + priority = 1 + action = "Allow" + + match_condition { + match_variable = "RequestUri" + operator = "Equals" + match_values = ["https://${azurerm_cdn_frontdoor_endpoint.main.host_name}:443/healthcheck"] + } + } + + custom_rule { + name = "iprestriction${local.env_name}" + enabled = true + type = "MatchRule" + priority = 2 + action = "Block" + + match_condition { + match_variable = "SocketAddr" + operator = "Contains" + negation_condition = true + match_values = local.is_prod ? var.IP_ADDRESS_WHITELIST_PROD : local.is_test ? var.IP_ADDRESS_WHITELIST_TEST : var.IP_ADDRESS_WHITELIST_DEV + } + } +} diff --git a/terraform/uptime.tf b/terraform/uptime.tf index 703901cb..b613e9c3 100644 --- a/terraform/uptime.tf +++ b/terraform/uptime.tf @@ -1,3 +1,4 @@ +# when setting up access restrictions, make sure to allow the ApplicationInsightsAvailability service tag module "healthcheck" { source = "./uptime" diff --git a/terraform/variables.tf b/terraform/variables.tf index d3332c98..f253713c 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -16,7 +16,19 @@ variable "VELOCITY_ETL_APP_OBJECT_ID" { type = string } -variable "IP_ADDRESS_WHITELIST" { +variable "IP_ADDRESS_WHITELIST_DEV" { + description = "List of IP addresses allowed to connect to the app service, in CIDR notation: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app#ip_address. By default, all IP addresses are allowed." + type = list(string) + default = [] +} + +variable "IP_ADDRESS_WHITELIST_TEST" { + description = "List of IP addresses allowed to connect to the app service, in CIDR notation: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app#ip_address. By default, all IP addresses are allowed." + type = list(string) + default = [] +} + +variable "IP_ADDRESS_WHITELIST_PROD" { description = "List of IP addresses allowed to connect to the app service, in CIDR notation: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app#ip_address. By default, all IP addresses are allowed." type = list(string) default = []