diff --git a/src/docs/command-line-deployment.md b/src/docs/command-line-deployment.md index 4e4f9d7bf..0aa3d5f0e 100644 --- a/src/docs/command-line-deployment.md +++ b/src/docs/command-line-deployment.md @@ -101,6 +101,7 @@ deploy.sh: create all the configuration and deploy Terraform resources with mini --write-output -w [OPTIONAL] Tier 3 Deployment requires Terraform output, use this flag to write terraform output --no-bastion [OPTIONAL] when present, do not create a Bastion Host and Jumpbox VM --no-sentinel [OPTIONAL] when present, do not create an Azure Sentinel solution + --policy [OPTIONAL] when present, create Policy Assignments for built-in NIST initiative --no-service-principal [OPTIONAL] when present, do not create an Azure Service Principal, instead use the credentials in the environment variables '$ARM_CLIENT_ID' and '$ARM_CLIENT_SECRET' --help -h Print this message ``` diff --git a/src/docs/images/20210419_missionlz_as_of_Aug2021_Policy.png b/src/docs/images/20210419_missionlz_as_of_Aug2021_Policy.png new file mode 100644 index 000000000..cb1a816a2 Binary files /dev/null and b/src/docs/images/20210419_missionlz_as_of_Aug2021_Policy.png differ diff --git a/src/docs/policies.md b/src/docs/policies.md new file mode 100644 index 000000000..fa9741a9f --- /dev/null +++ b/src/docs/policies.md @@ -0,0 +1,59 @@ +# Mission Landing Zone Regulatory Compliance - NIST Policies + +As part of Mission Landing Zone (MLZ) it's been a goal to ensure deployments have the tools and resources available that allow it to be compliant with most regulations across industries. This does not mean that workloads are compliant, but it does mean that the technologies in use can be compliant. This is caused by not only the varying number of compliance bodies involved and and the regulations they mandate but also caused by the decisions required by how and what controls are followed. + +For the purposes of this documentation we created an example method in which the MLZ deployment can be audited for current National Institute of Standards and Technology (NIST) controls and requirements using [Azure Policies built in initiative](https://docs.microsoft.com/en-us/azure/governance/policy/samples/nist-sp-800-53-r4) for NIST 800-53. _Note: this is focused on NIST controls that have built in policies in Azure clouds._ + +By adding the `--policy` switch to the deployment command the script will multiple assignments to the deployment final architecture. The result is for each Tier (Hub, Tier0, Tier1, and Tier2) there will be an additional policy/initiative assigned scoped to those recourse groups. This will not impact other policies/initiatives assigned that are deployed at different scopes either prior to deploying MLZ or post deployment. + +![](images/20210419_missionlz_as_of_Aug2021_Policy.png) + +## Known Issues + +Currently there are a set of known issues with this approach. The first and somewhat important detail is that these policies are based on built in policies available in the different Azure environments. There are some variances currently between clouds. This will always happen when separate isolated environments have different deployment cycles but also can be based on preview testing versus generally available components in one cloud environment versus another. + +A secondary issue comes from the method in which the assignment is deployed. This results in 'out of band' requirements for customers. In particular, the current built-in NIST initiative has a couple policies attached that modify and/or deploy if a resource doesn't exist. Example, VM extensions for guest policy configuration would be deployed if they don't exist in the VM. These types of policies require a managed identity be created that the Policy engine can use to take these actions. This managed identity must have contributor access to the resources but deploying as a contributor and not owner limits the ability. The terraform MLZ deployment as it is today using service principles with contributor rights cannot make this role assignment but the managed identity is created. This is by design for security purposes. + +The final note is that these are audits based on NIST controls and recommendations that will require out of band work. As an example, storage account redundancy and encryption will require a decision process on what MLZ is using as temporary storage for logs versus requirements for the workloads. For example, encryption can be accomplished with multiple key models, which one is required for what category of data? + +## Deploying + +Deploying policy assignments for NIST along with a standard deployment of MLZ is as simple as adding the –policy switch to the deployment script command. This will add a separate assignment of the built in NIST initiative per resource group in the deployment, excluding the resource groups used as deployment artifacts like state and config. + +Example: + `src/scripts/deploy.sh -s -l usgovvirginia --tf-environment usgovernment –policy` + +After the resources are deployed, you will need to go into go into each assignment and retrieve the managed identity and modify its role access to contributor scoped to the associated resource group. This is due to the initiative including modify and deploy policies that act on resources, like deploying the require policy guest configuration extensions to VMs. + +Modifying + +This model uses an additional custom terraform module called 'policy-assignments'. This can be modified for adding additional initiatives if desired. The module deployments retrieve their parameter values from a local json file stored in the module directory named 'nist-parameter-values' and named after the cloud environment they are deploying to, public or usgovernment. + +Example parameters file snippet: +``` +{ + "listOfMembersToExcludeFromWindowsVMAdministratorsGroup": + { + "value": "admin" + }, + "listOfMembersToIncludeInWindowsVMAdministratorsGroup": + { + "value": "azureuser" + }, + "logAnalyticsWorkspaceIdforVMReporting": + { + "value": ${jsonencode(laws_instance_id)} + }, + "IncludeArcMachines": + { + "value": "true" + } +``` + +In the above example the 'logAnalyticsWorkspaceIdforVMReporting' is retrieved from the running terraform deployment variables. This could be modified to use a central logging workspace if desired. + +What's Next + +While this is only a start, the NIST controls included in the built-in initiatives are a good start to understanding requirements on top of MLZ for compliance. In the near future the hopes are for this to be expanded with additional built-in initiatives as well as offering an option to create your own initiative and custom policies. Potential additions will be server baselines, IL compliances, and custom policies. + +Also scripts to assist in these out-of-band processes will be added. \ No newline at end of file diff --git a/src/scripts/config/create_mlz_config_resources.sh b/src/scripts/config/create_mlz_config_resources.sh index d169a5ce1..67d731c47 100755 --- a/src/scripts/config/create_mlz_config_resources.sh +++ b/src/scripts/config/create_mlz_config_resources.sh @@ -167,13 +167,19 @@ else # Assign Contributor Role to Subscriptions for sub in "${subs[@]}" do - echo "INFO: setting Contributor role assignment for ${sp_client_id} on subscription ${sub}..." + echo "INFO: setting Contributor and Policy Contributor role assignments for ${sp_client_id} on subscription ${sub}..." az role assignment create \ --role Contributor \ --assignee-object-id "${sp_object_id}" \ --scope "/subscriptions/${sub}" \ --assignee-principal-type ServicePrincipal \ --output none + az role assignment create \ + --role 'Resource Policy Contributor' \ + --assignee-object-id "${sp_object_id}" \ + --scope "/subscriptions/${sub}" \ + --assignee-principal-type ServicePrincipal \ + --output none done else error_log "ERROR: A service principal named ${mlz_sp_name} already exists. This must be a unique service principal for your use only. Try again with a new mlz-env-name. Exiting script." diff --git a/src/scripts/deploy.sh b/src/scripts/deploy.sh index 676d7b7cb..04b1632f8 100755 --- a/src/scripts/deploy.sh +++ b/src/scripts/deploy.sh @@ -36,6 +36,7 @@ show_help() { print_formatted "--no-bastion" "" "[OPTIONAL] when present, do not create a Bastion Host and Jumpbox VM" print_formatted "--no-sentinel" "" "[OPTIONAL] when present, do not create an Azure Sentinel solution" print_formatted "--no-service-principal" "" "[OPTIONAL] when present, do not create an Azure Service Principal, instead use the credentials in the environment variables '\$ARM_CLIENT_ID' and '\$ARM_CLIENT_SECRET'" + print_formatted "--policy" "" "[OPTIONAL] when present, create Policy Assignments for built-in NIST initiative" print_formatted "--help" "-h" "Print this message" } @@ -155,7 +156,7 @@ create_mlz_resources() { create_terraform_variables() { echo "INFO: creating terraform variables at ${tfvars_file_path}..." - "${this_script_path}/terraform/create_tfvars_from_config.sh" "${tfvars_file_path}" "${mlz_config_file_path}" "${create_bastion_jumpbox}" "${create_sentinel}" + "${this_script_path}/terraform/create_tfvars_from_config.sh" "${tfvars_file_path}" "${mlz_config_file_path}" "${create_bastion_jumpbox}" "${create_sentinel}" "${create_assignment}" } apply_terraform() { @@ -194,6 +195,7 @@ default_env_name="mlz${timestamp}" create_bastion_jumpbox=true create_sentinel=true create_service_principal=true +create_assignment=false mlz_config_subid="${default_config_subid}" mlz_config_location="${default_config_location}" @@ -239,6 +241,8 @@ while [ $# -gt 0 ] ; do create_sentinel=false ;; --no-service-principal) create_service_principal=false ;; + --policy) + create_assignment=true ;; -h | --help) show_help exit 0 ;; diff --git a/src/scripts/terraform/create_tfvars_from_config.sh b/src/scripts/terraform/create_tfvars_from_config.sh index 26003a32c..dcae77907 100755 --- a/src/scripts/terraform/create_tfvars_from_config.sh +++ b/src/scripts/terraform/create_tfvars_from_config.sh @@ -17,7 +17,7 @@ error_log() { usage() { echo "create_tfvars_from_config.sh: generate a terraform tfvars file given an MLZ config and a desired tfvars file name" - echo "create_tfvars_from_config.sh: " + echo "create_tfvars_from_config.sh: " show_help } @@ -30,6 +30,7 @@ file_to_create=$1 mlz_config=$2 create_bastion_jumpbox=${3:-true} create_sentinel=${4:-true} +create_assignment=${5:-false} # source config . "${mlz_config}" @@ -55,6 +56,7 @@ append_kvp "mlz_cloud" "${mlz_cloudname}" append_kvp "mlz_tenantid" "${mlz_tenantid}" append_kvp "mlz_location" "${mlz_config_location}" append_kvp "mlz_metadatahost" "${mlz_metadatahost}" +append_kvp "create_assignment" "${create_assignment}" append_kvp "hub_subid" "${mlz_saca_subid}" append_kvp "hub_rgname" "rg-saca-${mlz_env_name}" diff --git a/src/terraform/mlz/main.tf b/src/terraform/mlz/main.tf index 92af9b0da..c9a4305e6 100644 --- a/src/terraform/mlz/main.tf +++ b/src/terraform/mlz/main.tf @@ -513,3 +513,55 @@ module "jumpbox" { DeploymentName = var.deploymentname } } + +##################################### +### STAGE 4: Compliance example ### +##################################### + +module "hub-policy-assignment" { + count = var.create_assignment ? 1 : 0 + + providers = { azurerm = azurerm.hub } + source = "../modules/policy-assignments" + depends_on = [azurerm_resource_group.hub, azurerm_log_analytics_workspace.laws] + resource_group_name = azurerm_resource_group.hub.name + laws_instance_id = azurerm_log_analytics_workspace.laws.workspace_id + environment = var.tf_environment # Example "usgovernment" + log_analytics_workspace_resource_id = azurerm_log_analytics_workspace.laws.id +} + +module "tier0-policy-assignment" { + count = var.create_assignment ? 1 : 0 + + providers = { azurerm = azurerm.tier0 } + source = "../modules/policy-assignments" + depends_on = [azurerm_resource_group.tier0, azurerm_log_analytics_workspace.laws] + resource_group_name = azurerm_resource_group.tier0.name + laws_instance_id = azurerm_log_analytics_workspace.laws.workspace_id + environment = var.tf_environment # Example "usgovernment" + log_analytics_workspace_resource_id = azurerm_log_analytics_workspace.laws.id +} + +module "tier1-policy-assignment" { + count = var.create_assignment ? 1 : 0 + + providers = { azurerm = azurerm.tier1 } + source = "../modules/policy-assignments" + depends_on = [azurerm_resource_group.tier1, azurerm_log_analytics_workspace.laws] + resource_group_name = azurerm_resource_group.tier1.name + laws_instance_id = azurerm_log_analytics_workspace.laws.workspace_id + environment = var.tf_environment # Example "usgovernment" + log_analytics_workspace_resource_id = azurerm_log_analytics_workspace.laws.id +} + +module "tier2-policy-assignment" { + count = var.create_assignment ? 1 : 0 + + providers = { azurerm = azurerm.tier2 } + source = "../modules/policy-assignments" + depends_on = [azurerm_resource_group.tier2, azurerm_log_analytics_workspace.laws] + resource_group_name = azurerm_resource_group.tier2.name + laws_instance_id = azurerm_log_analytics_workspace.laws.workspace_id + environment = var.tf_environment # Example "usgovernment" + log_analytics_workspace_resource_id = azurerm_log_analytics_workspace.laws.id +} \ No newline at end of file diff --git a/src/terraform/mlz/variables.tf b/src/terraform/mlz/variables.tf index 4b73757a7..c158a9751 100644 --- a/src/terraform/mlz/variables.tf +++ b/src/terraform/mlz/variables.tf @@ -53,6 +53,12 @@ variable "mlz_objectid" { sensitive = true } +variable "create_assignment" { + description = "Create an Azure Policy assignement for defaul NIST initiative." + type = bool + default = false +} + ################################# # Hub Configuration ################################# diff --git a/src/terraform/modules/policy-assignments/main.tf b/src/terraform/modules/policy-assignments/main.tf new file mode 100644 index 000000000..ac8f2c3d0 --- /dev/null +++ b/src/terraform/modules/policy-assignments/main.tf @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +resource "azurerm_resource_group_policy_assignment" "policy_assign" { + name = "NIST Assignment - ${data.azurerm_resource_group.rg.name}" + resource_group_id = data.azurerm_resource_group.rg.id + policy_definition_id = var.policy_id + location = data.azurerm_resource_group.rg.location + identity { + type = "SystemAssigned" + } + # Define parameters for value template file directed to environment + parameters = templatefile("${path.module}/nist-parameter-values/${var.environment}.json.tmpl", { + laws_instance_id = var.laws_instance_id + }) +} \ No newline at end of file diff --git a/src/terraform/modules/policy-assignments/nist-parameter-values/public.json.tmpl b/src/terraform/modules/policy-assignments/nist-parameter-values/public.json.tmpl new file mode 100644 index 000000000..df80a22c6 --- /dev/null +++ b/src/terraform/modules/policy-assignments/nist-parameter-values/public.json.tmpl @@ -0,0 +1,34 @@ +{ + "listOfMembersToExcludeFromWindowsVMAdministratorsGroup": + { + "value": "admin" + }, + "listOfMembersToIncludeInWindowsVMAdministratorsGroup": + { + "value": "azureuser" + }, + "logAnalyticsWorkspaceIdforVMReporting": + { + "value": ${jsonencode(laws_instance_id)} + }, + "IncludeArcMachines": + { + "value": "true" + }, + "MinimumTLSVersion-5752e6d6-1206-46d8-8ab1-ecc2f71a8112": + { + "value": "1.2" + }, + "NotAvailableMachineState-bed48b13-6647-468e-aa2f-1af1d3f4dd40": + { + "value": "Compliant" + }, + "requiredRetentionDays": + { + "value": "365" + }, + "resourceGroupName-b6e2945c-0b7b-40f5-9233-7a5323b5cdc6": + { + "value": "NetworkWatcherRG" + } +} \ No newline at end of file diff --git a/src/terraform/modules/policy-assignments/nist-parameter-values/usgovernment.json.tmpl b/src/terraform/modules/policy-assignments/nist-parameter-values/usgovernment.json.tmpl new file mode 100644 index 000000000..957c53ccb --- /dev/null +++ b/src/terraform/modules/policy-assignments/nist-parameter-values/usgovernment.json.tmpl @@ -0,0 +1,34 @@ +{ + "listOfMembersToExcludeFromWindowsVMAdministratorsGroup": + { + "value": "admin" + }, + "listOfMembersToIncludeInWindowsVMAdministratorsGroup": + { + "value": "azureuser" + }, + "logAnalyticsWorkspaceIdforVMReporting": + { + "value": ${jsonencode(laws_instance_id)} + }, + "IncludeArcMachines": + { + "value": "true" + }, + "MinimumTLSVersion-5752e6d6-1206-46d8-8ab1-ecc2f71a8112": + { + "value": "1.2" + }, + "NotAvailableMachineState-bed48b13-6647-468e-aa2f-1af1d3f4dd40": + { + "value": "Compliant" + }, + "requiredRetentionDays": + { + "value": "365" + }, + "resourceGroupName-b6e2945c-0b7b-40f5-9233-7a5323b5cdc6": + { + "value": "NetworkWatcherRG" + } +} \ No newline at end of file diff --git a/src/terraform/modules/policy-assignments/output.tf b/src/terraform/modules/policy-assignments/output.tf new file mode 100644 index 000000000..e69de29bb diff --git a/src/terraform/modules/policy-assignments/variables.tf b/src/terraform/modules/policy-assignments/variables.tf new file mode 100644 index 000000000..d07a9a568 --- /dev/null +++ b/src/terraform/modules/policy-assignments/variables.tf @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +variable "policy_id" { + description = "The Azure policy ID for the NIST 800-53 R4 policy initiative." + type = string + default = "/providers/Microsoft.Authorization/policySetDefinitions/cf25b9c1-bd23-4eb6-bd2c-f4f3ac644a5f" +} + +variable "resource_group_name" { + description = "Resource group name for policy assignment." + type = string +} + +variable "environment" { + description = "The Terraform backend environment e.g. public or usgovernment. It defaults to public." + type = string + default = "public" +} + +variable "laws_instance_id" { + description = "The log analytics workspace ID which will be provided to the underlying policy rules via the policy parameters." + type = string +} + +# Full resource ID used if enabling activity diagnostic logging +variable "log_analytics_workspace_resource_id" { + description = "The resource id of the Log Analytics Workspace" + type = string +} \ No newline at end of file