From f13f6ec24f5b8c0f318cf66f6cd1a2f3c7a01534 Mon Sep 17 00:00:00 2001 From: Barrington Willis <51492255+tredell@users.noreply.github.com> Date: Fri, 3 Mar 2023 07:00:06 -0800 Subject: [PATCH] Identity Archetype (#359) * Squashed commit of the following: commit 6d6b3e49855c365f49a4674534b985bacf9cd74c Author: Barry Willis Date: Mon Feb 27 08:07:45 2023 -0800 changed the areacode on the logging service health alerts architype commit 86b4505c2ffd5127978883c0bc6a1f9b0e7d3268 Author: Barry Willis Date: Fri Feb 24 16:39:08 2023 -0800 prepping for testing in ESLZ test environment commit 0f92b6bf70aee1377b4d49db436fa7024f1bfd25 Merge: 2a3584a 7749e7b Author: Barry Willis Date: Fri Feb 24 16:10:37 2023 -0800 Merge remote-tracking branch 'origin/main' into IdentityLZ commit 7749e7bf7a8756e3b2ffd09016e3e9d9954407db Merge: f6555a4 5337654 Author: Barry Willis Date: Fri Feb 24 16:08:54 2023 -0800 Merge remote-tracking branch 'github-CanadaPubSecALZ/main' commit f6555a41227fdbe47a6981798e2cb2bb97bd7cd6 Author: Barry Willis Date: Mon Feb 13 12:30:20 2023 -0800 Added the patch version to the AKS versions in the Data Archetypes commit 8edcb63d833fd177ede60c9a51b6228f448c0c33 Author: Barry Willis Date: Mon Feb 13 11:32:54 2023 -0800 Changed hte AKS version to only have the Major.Minor commit 37123d71623b7c6ed288a5ba32c7cab5f8e75e6f Author: Barry Willis Date: Mon Feb 13 11:17:38 2023 -0800 updated AKS version in the Data Archetypes commit 459b3c62751cb6bfedf2ddc5800ad39137417d38 Author: Barry Willis Date: Mon Feb 13 08:55:13 2023 -0800 changed the servcie health number prefix to 604 commit cccf88662c3a0e0d7b2f625a13ec191053017985 Author: Barry Willis Date: Mon Feb 13 07:42:52 2023 -0800 changed the invalid dummy service alert phone number to a valid phone number commit 8e9628d26e1285c437a6ec8a3ebd479299f3cb5f Author: Barry Willis Date: Mon Feb 13 07:01:36 2023 -0800 fixed linter warnings in policy files commit 6c2b2f7d2d53b97d0014306656406cf564189779 Author: Barry Willis Date: Sat Feb 11 15:36:36 2023 -0800 Commit 95556ddd: changed the extensionResourceId function to tenantResourceId for all built-in polify definitions commit c58ba48f5073c0b86b41c54fddca9cab0368b59a Author: Barry Willis Date: Sat Feb 11 15:09:56 2023 -0800 Fixed the AKS policy deployment commit f9e8418b7e1faf8cc8122acc9414e12c5bfbd22e Author: Barry Willis Date: Sat Feb 11 14:04:22 2023 -0800 Fixed Bug on policy defnition commit 1a3c82e446072db49d927343a4792e30bdb31f05 Author: Barry Willis Date: Fri Feb 10 19:09:02 2023 -0800 updated the linter rules commit 20e188051a8999d7a5e6ee925ec193f6e1d2dea6 Author: Barry Willis Date: Fri Feb 10 18:52:18 2023 -0800 fixed the remaining linter errors in the policy definitions commit 1610a28e355af15a86d8a555a97ca9912cc11aeb Author: Barry Willis Date: Fri Feb 10 18:27:14 2023 -0800 fixed the remaining linter warnings commit 9f0e049fa09e19f0cf312f4826520e1005e58434 Author: Barry Willis Date: Fri Feb 10 17:31:21 2023 -0800 fixed BCP321 warning commit 466d7b0c070f4bb4fef94b1fb9bac2f3da754c4a Author: Barry Willis Date: Fri Feb 10 17:22:46 2023 -0800 changed the pOlicyScopedId var to be set by using the MGResourceID Function commit 9362967e5006d9ec3882cdc5bec5aae5b872bf29 Author: Barry Willis Date: Fri Feb 10 16:48:26 2023 -0800 Fixed Role Definition Id References to use the ResourceId function commit 4bcbc28212ecac9bff2a8e3c720a9a364479733c Author: Barry Willis Date: Fri Feb 10 16:07:33 2023 -0800 Fixed BCP321 Linter warning in networking files commit 2a3584a7cac9c5822c7a226bc8a5d44f52d69a65 Author: Barry Willis Date: Fri Feb 10 15:07:43 2023 -0800 Removed Linter exception BCP321 - will fix in the linter PR commit a0b48ec7710a5ee8023a066e4cb5394074002c1e Author: Barry Willis Date: Fri Feb 10 10:39:36 2023 -0800 Fixed the bugs with conditionally deploying DNS Resolver commit 4f24be78f48465b404c529b276db66496c9958db Author: Barry Willis Date: Wed Feb 8 15:29:38 2023 -0800 Updated documentation and made the DNS Resolver subnets optional commit 03fcb5e50b0670c67d1850063dd828ffa6945cf8 Merge: dfe0d9a 0fa01e8 Author: Barry Willis Date: Mon Feb 6 16:58:41 2023 -0800 Merge remote-tracking branch 'origin/main' into IdentityLZ commit dfe0d9acab086df1d9dfbfbdae5770fbf5da999a Author: Barry Willis Date: Wed Jan 11 15:52:06 2023 -0800 added Schema validation to the identity config file commit fb88630b5d707db6b7f4ab1aa2455ff79920d5b3 Author: Barry Willis Date: Mon Jan 9 10:28:13 2023 -0800 changed the DNS Resolver ruleset to be an object-array commit 78aaf4d6cdeff8d9832d8a309f26c10cefe97a22 Author: Barry Willis Date: Sat Jan 7 13:57:37 2023 -0800 first pass at creating conditional forwarding rulesets in the Identity LZ commit e7b554d04daee83a55a985073ec0c59084c7f3c2 Author: Barry Willis Date: Fri Jan 6 08:54:27 2023 -0800 Configured Subnet Delegation for Az DNS Resolver commit 978ab9925f876945ba02280493f7deba1c07e7ee Author: Barry Willis Date: Thu Jan 5 19:52:24 2023 -0800 added Private DNS Resolver to the Identity LZ commit 9735d58fc04d7a587a76a5387deb112c466390fe Author: Barry Willis Date: Thu Jan 5 13:19:05 2023 -0800 Removed the optional Subnet commit 4cd57ed41a09672b3cfbc1792c2edbdc3569a060 Author: Barry Willis Date: Thu Jan 5 13:09:36 2023 -0800 first cut at the identity LZ framework commit a119eea02fca28a2028362f484aa2835c9313c1d Author: Barry Willis Date: Wed Dec 21 11:54:58 2022 -0800 added identitypathfromroot in the branch config file commit 75b6ccc2ab6efd55037e0a5a938d49f2eef32de4 Author: Barry Willis Date: Wed Dec 21 11:35:12 2022 -0800 Added: identity vars display Changed: location reference to identity param file commit e0cfc41b5a83c4c331689fcafa5edc9928e93d39 Author: Barry Willis Date: Wed Dec 21 11:22:35 2022 -0800 fixed misconfigured working directory commit fb58b16999aeb9cc6b6b81647c76e95024e1267c Author: Barry Willis Date: Wed Dec 21 11:18:46 2022 -0800 removed schema validation to test deployment commit 240189de7e30fa57654c3ec76ec37c762ff80133 Author: Barry Willis Date: Wed Dec 21 11:15:43 2022 -0800 fixed bug - neworking region is now identity region commit 89e63b5976cb5cdc4e85d0b25c01234ffe4853d7 Author: Barry Willis Date: Wed Dec 21 11:11:48 2022 -0800 initial identity lz deployment commit d4b40b26b893b78d7a9250dffe24c3e9ce06d690 Author: Barry Willis Date: Wed Dec 21 11:03:29 2022 -0800 Added default region for Identity Subscription commit 41e611818d09181b1a455f612425cae20f0683f7 Author: Barry Willis Date: Wed Dec 21 08:29:33 2022 -0800 Changed bastion subnet range in identity subnet commit f5a43f2d44803e80db8a043d31e5c9f72fc51675 Author: Barry Willis Date: Wed Dec 21 07:33:03 2022 -0800 Param file for Identity LZ commit 13d084b0fe74f39ca1423b2eb9f333a2b760b1f2 Author: Barry Willis Date: Tue Dec 20 15:19:23 2022 +0000 Deleted identity.parameteres.json commit 5ba9a12fa8e8e02f60f3f2afea43681cc84d7446 Merge: 002b2be e395307 Author: Barry Willis Date: Tue Dec 20 07:18:40 2022 -0800 Merge branch 'IdentityLZ' of https://dev.azure.com/Tredell/CanadaALZ/_git/CanadaALZ into IdentityLZ commit 002b2be1bb5b555a334f35cbb505e7a68f321649 Author: Barry Willis Date: Tue Dec 20 07:18:32 2022 -0800 id-lz - created param section for id lz commit e395307b1c12786cc28cf3d4b00586dde69739d5 Author: Barry Willis Date: Tue Dec 20 07:13:54 2022 -0800 id-lz - created param section for id lz commit 7f4a43eb4fdc7f6f37ebab8e661981cccbee9f50 Author: Barry Willis Date: Mon Dec 19 14:54:57 2022 -0800 disabled privatelink infrastructure to be deployed in hub lz commit db85049ac94b5c394d586b6960343bc1286997f1 Author: Barry Willis Date: Mon Dec 19 14:46:36 2022 -0800 Configured hub networking parameter files commit 8d772e868803d1b712013f7db21044d48ab730d2 Author: Barry Willis Date: Mon Dec 19 14:07:43 2022 -0800 removed comment from json - not supported commit 89cde8d92704f1a41a123af46da6dd90568d99cb Author: Barry Willis Date: Mon Dec 19 12:56:47 2022 -0800 Configuring Policies for deployment to Test enviornment commit ba781ee844a4abd403071e072645988b63ada494 Author: Barry Willis Date: Mon Dec 19 12:40:53 2022 -0800 added a default security Group commit 1269da21e08fdf4c29a53b38a4d18722c64461e0 Author: Barry Willis Date: Mon Dec 19 12:26:14 2022 -0800 setting up logging for my test environment commit 4d6a41f4133380223f5895dba270cbce4ae5a39b Author: Barry Willis Date: Mon Dec 19 12:13:08 2022 -0800 testing the path to the logging configuraiton file commit 75d0b99caf6aed5f809c28566cad35569d78be58 Author: Barry Willis Date: Mon Dec 19 12:00:14 2022 -0800 added the full path to the logging parameters file commit 32e8382bcb8deaaaab0c7bc1c2791483ef439971 Author: Barry Willis Date: Mon Dec 19 11:55:00 2022 -0800 path to logging parameters file was incorrect commit 5757d36a486e7f3b707f00848d19cfe64de83358 Author: Barry Willis Date: Mon Dec 19 11:37:20 2022 -0800 Changed MG Root to match test enviornment commit 1fdd02db1638420decf5ab021fb617b95920aada Author: Barry Willis Date: Mon Dec 19 11:09:46 2022 -0800 Adding config file for IdentityLZ branch * PowerShell Deployment Files created * GitHub Action Pipelines modified to add the Identity Archetype * made the Identity GitHub Action optional * put the boolean option in single quotes * fixed a few bugs (BCP321 & references to the wrong tenant) * changed the sub id for the logging subscription * Removed the hardcoded reference to the LAW in the identity param file * updated the param file with the LAW ID * disabled private dns zone deployment in the identity sub * removed the config files from my custom branch * uncommented the validation in the Identity ADO Pipeline * removed commented trigger code from ADO Identity Pipeline * renenabled the dployment of the DNSPrivateEndPoints policyset * removed the provider registration for containerservices in the deploy-identity-pipeline yaml * added an explanation comment to the dnsforwardingruleset file * Added telemetry tracking for the identity subscription * fixed cut and paste errors * Updated test cases & documentation * added the consistency check & pull request checks for github actions * fixed spelling error --- .github/workflows/0-everything.yml | 33 ++ .github/workflows/6-identity.yml | 46 ++ ...-subscriptions.yml => 7-subscriptions.yml} | 0 .github/workflows/README.md | 3 +- .github/workflows/consistency-check.yml | 9 + .github/workflows/pull-request-check.yml | 9 + .pipelines/platform-identity.yml | 64 +++ .pipelines/policy.yml | 2 +- .../steps/deploy-platform-identity.yml | 83 ++++ .pipelines/templates/steps/show-variables.yml | 6 + .../network/dns-forwarding-ruleset.bicep | 57 +++ .../network/dnsresolver-vnet-link.bicep | 26 + azresources/network/dnsresolver.bicep | 73 +++ .../CanadaESLZ-main/identity.parameters.json | 170 +++++++ config/telemetry.json | 1 + config/variables/CanadaESLZ-main.yml | 6 + docs/archetypes/identity.md | 342 +++++++++++++ docs/architecture.md | 16 +- .../media/architecture/archetype-identity.jpg | Bin 0 -> 140799 bytes docs/onboarding/azure-devops-pipelines.md | 185 ++++++- docs/onboarding/azure-devops-scripts.md | 4 +- .../lz-platform-identity/dnsResolver.bicep | 112 +++++ landingzones/lz-platform-identity/main.bicep | 340 +++++++++++++ .../lz-platform-identity/networking.bicep | 404 ++++++++++++++++ .../landingzones/lz-platform-identity.json | 455 ++++++++++++++++++ .../Functions/EnvironmentContext.ps1 | 1 + scripts/deployments/Functions/Identity.ps1 | 81 ++++ scripts/deployments/RunWorkflows.ps1 | 24 + .../lz-Identity-With-DNS-Resolver.json | 170 +++++++ ...th-Private-DNS-Zones-And-DNS-Resolver.json | 170 +++++++ .../lz-Identity-With-Private-DNS-Zones.json | 170 +++++++ .../lz-Identity-Without-DNS-Resolver.json | 170 +++++++ tests/schemas/run-tests.sh | 2 + 33 files changed, 3224 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/6-identity.yml rename .github/workflows/{6-subscriptions.yml => 7-subscriptions.yml} (100%) create mode 100644 .pipelines/platform-identity.yml create mode 100644 .pipelines/templates/steps/deploy-platform-identity.yml create mode 100644 azresources/network/dns-forwarding-ruleset.bicep create mode 100644 azresources/network/dnsresolver-vnet-link.bicep create mode 100644 azresources/network/dnsresolver.bicep create mode 100644 config/identity/CanadaESLZ-main/identity.parameters.json create mode 100644 docs/archetypes/identity.md create mode 100644 docs/media/architecture/archetype-identity.jpg create mode 100644 landingzones/lz-platform-identity/dnsResolver.bicep create mode 100644 landingzones/lz-platform-identity/main.bicep create mode 100644 landingzones/lz-platform-identity/networking.bicep create mode 100644 schemas/latest/landingzones/lz-platform-identity.json create mode 100644 scripts/deployments/Functions/Identity.ps1 create mode 100644 tests/schemas/lz-platform-identity/lz-Identity-With-DNS-Resolver.json create mode 100644 tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones-And-DNS-Resolver.json create mode 100644 tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones.json create mode 100644 tests/schemas/lz-platform-identity/lz-Identity-Without-DNS-Resolver.json diff --git a/.github/workflows/0-everything.yml b/.github/workflows/0-everything.yml index 8130c32a..e2f7b01a 100644 --- a/.github/workflows/0-everything.yml +++ b/.github/workflows/0-everything.yml @@ -20,6 +20,11 @@ on: - "HubNetworkWithNVA" - "HubNetworkWithAzureFirewall" default: "HubNetworkWithAzureFirewall" + deployIdentity: + type: boolean + description: "Deploy Identity Subscription" + required: true + default: false subscriptionIds: type: string description: Subscription ID(s) (optional), e.g. "abcd", "1234" @@ -306,6 +311,34 @@ jobs: -NvaUsername (ConvertTo-SecureString -String '${{secrets.NVA_USERNAME}}' -AsPlainText -Force) ` -NvaPassword (ConvertTo-SecureString -String '${{secrets.NVA_PASSWORD}} '-AsPlainText -Force) + identity: + name: Identity + if: github.event.inputs.deployIdentity == 'true' + + needs: + - Logging + - HubNetworking + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure PowerShell modules + run: | + Install-Module Az -Force + Install-Module powershell-yaml -Force + + - name: Deploy Identity + run: | + ./RunWorkflows.ps1 ` + -DeployIdentity ` + -EnvironmentName '${{github.event.inputs.environmentName}}' ` + -LoginServicePrincipalJson (ConvertTo-SecureString -String '${{secrets.ALZ_CREDENTIALS}}' -AsPlainText -Force) ` + -GitHubRepo ${env:GITHUB_REPOSITORY} ` + -GitHubRef ${env:GITHUB_REF} + SubscriptionMatrix: if: github.event.inputs.subscriptionIds != '' diff --git a/.github/workflows/6-identity.yml b/.github/workflows/6-identity.yml new file mode 100644 index 00000000..0804b1ab --- /dev/null +++ b/.github/workflows/6-identity.yml @@ -0,0 +1,46 @@ +# ---------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# +# THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +# ---------------------------------------------------------------------------------- + +name: 6 - Identity + +on: + workflow_dispatch: + inputs: + environmentName: + type: string + description: Environment name (optional), e.g. CanadaESLZ-main + required: false + +defaults: + run: + shell: pwsh + working-directory: scripts/deployments + +jobs: + identity: + name: Identity + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure PowerShell modules + run: | + Install-Module Az -Force + Install-Module powershell-yaml -Force + + - name: Deploy Identity + run: | + ./RunWorkflows.ps1 ` + -DeployIdentity ` + -EnvironmentName '${{github.event.inputs.environmentName}}' ` + -LoginServicePrincipalJson (ConvertTo-SecureString -String '${{secrets.ALZ_CREDENTIALS}}' -AsPlainText -Force) ` + -GitHubRepo ${env:GITHUB_REPOSITORY} ` + -GitHubRef ${env:GITHUB_REF} diff --git a/.github/workflows/6-subscriptions.yml b/.github/workflows/7-subscriptions.yml similarity index 100% rename from .github/workflows/6-subscriptions.yml rename to .github/workflows/7-subscriptions.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 800218a8..72cc59ac 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -22,7 +22,8 @@ The following workflows are present in the `.github/workflows` repository folder | 5 | Azure Firewall Policy (required for Hub Networking with Azure Firewall) | `5-azure-firewall-policy.yml` | 5 | Hub Networking with Azure Firewall | `5-hub-network-with-azure-firewall.yml` | 5 | Hub Networking with NVA | `5-hub-network-with-nva.yml` -| 6 | Subscriptions | `6-subscriptions.yml` +| 6 | Identity | `6-identity.yml` +| 7 | Subscriptions | `7-subscriptions.yml` With the exception of the `Everything` workflow, all other workflows need to be run in the order specified. For example, the `Policy` workflow is dependent on resources deployed by the `Logging` workflow. Think of it as a layered approach; once the layer is deployed, it only requires re-running if some configuration at that layer changes. diff --git a/.github/workflows/consistency-check.yml b/.github/workflows/consistency-check.yml index 6b6367f5..81b8d980 100644 --- a/.github/workflows/consistency-check.yml +++ b/.github/workflows/consistency-check.yml @@ -9,6 +9,7 @@ env: SCHEMA_FOLDER: schemas/latest/landingzones LOGGING_PATH_FROM_ROOT: config/logging NETWORKING_PATH_FROM_ROOT: config/networking + IDENTITY_PATH_FROM_ROOT: config/identity SUBSCRIPTIONS_PATH_FROM_ROOT: config/subscriptions jobs: @@ -82,6 +83,14 @@ jobs: Get-Content -Raw $_ | Test-Json -SchemaFile $HubNetworkWithNVASchemaFile } + $IdentityFileFilter="*.json" + $IdentitySchemaFile="${{env.SCHEMA_FOLDER}}/lz-platform-identity.json" + + Get-ChildItem -Recurse -Filter $IdentityFileFilter -Path "${{env.IDENTITY_PATH_FROM_ROOT}}" | ForEach-Object { + Write-Host "Validating: $_ with $IdentitySchemaFile" + Get-Content -Raw $_ | Test-Json -SchemaFile $IdentitySchemaFile + } + $GenericSubscriptionFileFilter="*generic-subscription*.json" $GenericSubscriptionSchemaFile="${{env.SCHEMA_FOLDER}}/lz-generic-subscription.json" diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml index b4adfe81..67eb8afd 100644 --- a/.github/workflows/pull-request-check.yml +++ b/.github/workflows/pull-request-check.yml @@ -12,6 +12,7 @@ env: SCHEMA_FOLDER: schemas/latest/landingzones LOGGING_PATH_FROM_ROOT: config/logging NETWORKING_PATH_FROM_ROOT: config/networking + IDENTITY_PATH_FROM_ROOT: config/identity SUBSCRIPTIONS_PATH_FROM_ROOT: config/subscriptions jobs: @@ -84,6 +85,14 @@ jobs: Write-Host "Validating: $_ with $HubNetworkWithNVASchemaFile" Get-Content -Raw $_ | Test-Json -SchemaFile $HubNetworkWithNVASchemaFile } + + $IdentityFileFilter="*.json" + $IdentitySchemaFile="${{env.SCHEMA_FOLDER}}/lz-platform-identity.json" + + Get-ChildItem -Recurse -Filter $IdentityFileFilter -Path "${{env.IDENTITY_PATH_FROM_ROOT}}" | ForEach-Object { + Write-Host "Validating: $_ with $IdentitySchemaFile" + Get-Content -Raw $_ | Test-Json -SchemaFile $IdentitySchemaFile + } $GenericSubscriptionFileFilter="*generic-subscription*.json" $GenericSubscriptionSchemaFile="${{env.SCHEMA_FOLDER}}/lz-generic-subscription.json" diff --git a/.pipelines/platform-identity.yml b/.pipelines/platform-identity.yml new file mode 100644 index 00000000..09e53144 --- /dev/null +++ b/.pipelines/platform-identity.yml @@ -0,0 +1,64 @@ +# ---------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# +# THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +# ---------------------------------------------------------------------------------- + +trigger: none + +pr: none + +variables: +- name: devops-org-name + value: ${{ replace(replace(variables['System.CollectionUri'], 'https://dev.azure.com/' , ''), '/', '') }} +- name: logging-config-directory + value: $(System.DefaultWorkingDirectory)/$(loggingPathFromRoot)/${{ variables['devops-org-name'] }}-${{ variables['Build.SourceBranchName'] }} +- name: identity-config-directory + value: $(System.DefaultWorkingDirectory)/$(identityPathFromRoot)/${{ variables['devops-org-name'] }}-${{ variables['Build.SourceBranchName'] }} +- name: variable-template-file + value: ${{ variables['devops-org-name'] }}-${{ variables['Build.SourceBranchName'] }}.yml +- template: ../config/variables/common.yml +- template: ../config/variables/${{ variables['variable-template-file'] }} + + +pool: + vmImage: $[ variables.vmImage ] + +stages: + +- stage: DeployNetworkingStage + displayName: Deploy Networking Stage + + jobs: + + - deployment: DeployIdentityJob + displayName: Deploy Identity Job + environment: ${{ variables['Build.SourceBranchName'] }} + strategy: + runOnce: + deploy: + steps: + - checkout: self + + - template: templates/steps/load-variables.yml + + - template: templates/steps/load-log-analytics-vars.yml + parameters: + logAnalyticsSubscriptionId: $(var-logging-subscriptionId) + logAnalyticsConfigurationFile: ${{ variables['logging-config-directory'] }}/$(var-logging-configurationFileName) + + - template: templates/steps/show-variables.yml + parameters: + json: ${{ convertToJson(variables) }} + + - template: templates/steps/deploy-platform-identity.yml + parameters: + workingDir: $(System.DefaultWorkingDirectory)/landingzones + deployOperation: ${{ variables['deployOperation'] }} + identityManagementGroupId: $(var-identity-managementGroupId) + identitySubscriptionId: $(var-identity-subscriptionId) + identityRegion: $(var-identity-region) + identityConfigurationPath: ${{ variables['identity-config-directory'] }}/$(var-identity-configurationFileName) diff --git a/.pipelines/policy.yml b/.pipelines/policy.yml index 17dad0a2..495f3469 100644 --- a/.pipelines/policy.yml +++ b/.pipelines/policy.yml @@ -96,7 +96,7 @@ stages: - template: templates/steps/define-policyset.yml parameters: description: 'Define Policy Set' - deployTemplates: [AKS, DefenderForCloud, LogAnalytics, Network, DNSPrivateEndpoints, Tags] + deployTemplates: [AKS, DefenderForCloud, DNSPrivateEndpoints, LogAnalytics, Network, Tags] deployOperation: ${{ variables['deployOperation'] }} workingDir: $(System.DefaultWorkingDirectory)/policy/custom/definitions/policyset diff --git a/.pipelines/templates/steps/deploy-platform-identity.yml b/.pipelines/templates/steps/deploy-platform-identity.yml new file mode 100644 index 00000000..358fb190 --- /dev/null +++ b/.pipelines/templates/steps/deploy-platform-identity.yml @@ -0,0 +1,83 @@ +# ---------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# +# THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +# ---------------------------------------------------------------------------------- + +parameters: + - name: workingDir + type: string + - name: deployOperation + type: string + default: create + values: + - create + - what-if + - name: identityManagementGroupId + type: string + - name: identitySubscriptionId + type: string + - name: identityRegion + type: string + - name: identityConfigurationPath + type: string + +steps: + +- task: PowerShell@2 + displayName: Validate identity Parameters + inputs: + targetType: 'inline' + script: | + $schemaFile="$(Build.SourcesDirectory)/schemas/latest/landingzones/lz-platform-identity.json" + + Write-Host "Parameters File: ${{ parameters.identityConfigurationPath }}" + Write-Host "Schema File: ${schemaFile}" + + Get-Content -Raw "${{ parameters.identityConfigurationPath }}" | Test-Json -SchemaFile "${schemaFile}" + +- template: ./move-subscription.yml + parameters: + managementGroup: ${{ parameters.identityManagementGroupId }} + subscriptionGuid: ${{ parameters.identitySubscriptionId }} + subscriptionLocation: ${{ parameters.identityRegion }} + templateDirectory: $(Build.SourcesDirectory)/landingzones/utils/mg-move + templateFile: move-subscription.bicep + workingDir: ${{ parameters.workingDir }}/utils/mg-move + +- task: AzureCLI@2 + displayName: Configure Identity LZ + inputs: + azureSubscription: $(serviceConnection) + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + $(var-bashPreInjectScript) + + # Check if the log analytics workspace id is provided in the parameters json. + # If present, then do no change it. Otherwise add it to the json parameter file. + LOG_ANALYTICS_WORKSPACE_RESOURCE_ID_IN_PARAMETERS=`jq -r .parameters.logAnalyticsWorkspaceResourceId.value ${{ parameters.identityConfigurationPath }}` + + if [[ $LOG_ANALYTICS_WORKSPACE_RESOURCE_ID_IN_PARAMETERS != null && "$LOG_ANALYTICS_WORKSPACE_RESOURCE_ID_IN_PARAMETERS" != "" ]]; + then + echo "Log Analytics Workspace Resource ID is set in ${{ parameters.identityConfigurationPath }} to $LOG_ANALYTICS_WORKSPACE_RESOURCE_ID_IN_PARAMETERS" + else + echo "Log Analytics Workspace Resource ID is not set in ${{ parameters.identityConfigurationPath }}. Updating ${{ parameters.identityConfigurationPath }} with $(var-logging-logAnalyticsWorkspaceResourceId)" + + # use jq to update the json parameter file + echo "$( jq '.parameters.logAnalyticsWorkspaceResourceId.value = "$(var-logging-logAnalyticsWorkspaceResourceId)"' ${{ parameters.identityConfigurationPath }} )" > ${{ parameters.identityConfigurationPath }} + fi + + echo "Deploying main.bicep using ${{ parameters.deployOperation}} operation using ${{ parameters.identityConfigurationPath }}..." + + az deployment sub ${{ parameters.deployOperation }} \ + --location ${{ parameters.identityRegion }} \ + --subscription ${{ parameters.identitySubscriptionId }} \ + --template-file main.bicep \ + --parameters @${{ parameters.identityConfigurationPath }} + + $(var-bashPostInjectScript) + workingDirectory: '${{ parameters.workingDir }}/lz-platform-identity' diff --git a/.pipelines/templates/steps/show-variables.yml b/.pipelines/templates/steps/show-variables.yml index 10f8c424..5f750473 100644 --- a/.pipelines/templates/steps/show-variables.yml +++ b/.pipelines/templates/steps/show-variables.yml @@ -70,4 +70,10 @@ steps: echo printenv -0 | grep -zi '^var-hubnetwork-nva-' | xargs -0 -L 1 echo + echo + echo + echo "IDENTITY" + echo + printenv -0 | grep -zi '^var-identity-' | xargs -0 -L 1 echo + $(var-bashPostInjectScript) diff --git a/azresources/network/dns-forwarding-ruleset.bicep b/azresources/network/dns-forwarding-ruleset.bicep new file mode 100644 index 00000000..8293ef32 --- /dev/null +++ b/azresources/network/dns-forwarding-ruleset.bicep @@ -0,0 +1,57 @@ +// ---------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// ---------------------------------------------------------------------------------- + +param name string +param location string = resourceGroup().location + +@description('Outbound endpoint id') +param outEndpointId string + +param forwardingRuleSet array + +param linkRuleSetToVnet bool = false +param linkName string = '' +param vnetId string = '' + + + +resource ruleset 'Microsoft.Network/dnsForwardingRulesets@2022-07-01' = { + name: name + location: location + properties: { + dnsResolverOutboundEndpoints: [ + { + id: outEndpointId + } + ] + } +} + +resource fwRule 'Microsoft.Network/dnsForwardingRulesets/forwardingRules@2022-07-01' = [for rule in forwardingRuleSet: { + name: rule.name + parent: ruleset + properties: { + forwardingRuleState: rule.state + domainName: endsWith(rule.domain, '.') ? rule.domain : '${rule.domain}.' //Adding a '.' at the end of the domain name if it is not present + targetDnsServers: rule.targetDnsServers + } +}] + + +module dnsResolverLinkVnet 'dnsresolver-vnet-link.bicep'= if(linkRuleSetToVnet){ + name:'deploy-private-dns-resolver-vnet-link' + params:{ + forwardingRulesetName: ruleset.name + linkName: linkName + vnetId: vnetId + } +} + +output ruleSetName string = ruleset.name + diff --git a/azresources/network/dnsresolver-vnet-link.bicep b/azresources/network/dnsresolver-vnet-link.bicep new file mode 100644 index 00000000..aba56f55 --- /dev/null +++ b/azresources/network/dnsresolver-vnet-link.bicep @@ -0,0 +1,26 @@ +// ---------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// ---------------------------------------------------------------------------------- + +param linkName string +param vnetId string +param forwardingRulesetName string + +resource dnsResolver 'Microsoft.Network/dnsForwardingRulesets@2022-07-01' existing = { + name: forwardingRulesetName +} + +resource resolverLink 'Microsoft.Network/dnsForwardingRulesets/virtualNetworkLinks@2022-07-01' = { + name: linkName + parent: dnsResolver + properties: { + virtualNetwork: { + id: vnetId + } + } +} diff --git a/azresources/network/dnsresolver.bicep b/azresources/network/dnsresolver.bicep new file mode 100644 index 00000000..a7a5806f --- /dev/null +++ b/azresources/network/dnsresolver.bicep @@ -0,0 +1,73 @@ +// ---------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// ---------------------------------------------------------------------------------- + +param name string +param location string = resourceGroup().location +param vnetId string + +@description('Name of the private dns resolver outbound endpoint') +param inboundEndpointName string + +@description('Name of the private dns resolver outbound endpoint') +param outboundEndpointName string + +@description('name of the subnet that will be used for private resolver inbound endpoint') +param inboundSubnetName string + +@description('name of the subnet that will be used for private resolver outbound endpoint') +param outboundSubnetName string + +param vnetResourceGroupName string +param vnetName string + +var subscriptionId = subscription().subscriptionId + +var inboundSubnetId = resourceId(subscriptionId, vnetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', vnetName, inboundSubnetName) +var outboundSubnetId = resourceId(subscriptionId, vnetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', vnetName, outboundSubnetName) + +resource resolver 'Microsoft.Network/dnsResolvers@2022-07-01' = { + name: name + location: location + properties: { + virtualNetwork: { + id: vnetId + } + } +} + +resource inEndPoint 'Microsoft.Network/dnsResolvers/inboundEndpoints@2022-07-01' = { + parent: resolver + name: inboundEndpointName + location: location + properties: { + ipConfigurations: [ + { + privateIpAllocationMethod: 'dynamic' + subnet: { + id: inboundSubnetId + } + } + ] + } +} + + +resource outEndpoint 'Microsoft.Network/dnsResolvers/outboundEndpoints@2022-07-01' = { + parent: resolver + name: outboundEndpointName + location: location + properties: { + subnet: { + id: outboundSubnetId + } + } +} + +output inboundDnsIp string = inEndPoint.properties.ipConfigurations[0].privateIpAddress +output outboundEndpointId string = outEndpoint.id diff --git a/config/identity/CanadaESLZ-main/identity.parameters.json b/config/identity/CanadaESLZ-main/identity.parameters.json new file mode 100644 index 00000000..8c759443 --- /dev/null +++ b/config/identity/CanadaESLZ-main/identity.parameters.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "serviceHealthAlerts": { + "value": { + "resourceGroupName": "service-health", + "incidentTypes": [ "Incident", "Security" ], + "regions": [ "Global", "Canada East", "Canada Central" ], + "receivers": { + "app": [ "alzcanadapubsec@microsoft.com" ], + "email": [ "alzcanadapubsec@microsoft.com" ], + "sms": [ { "countryCode": "1", "phoneNumber": "6045555555" } ], + "voice": [ { "countryCode": "1", "phoneNumber": "6045555555" } ] + }, + "actionGroupName": "Service health action group", + "actionGroupShortName": "health-alert", + "alertRuleName": "Incidents and Security", + "alertRuleDescription": "Service Health: Incidents and Security" + } + }, + "securityCenter": { + "value": { + "email": "alzcanadapubsec@microsoft.com", + "phone": "6045555555" + } + }, + "subscriptionRoleAssignments": { + "value": [ + { + "comments": "Built-in Owner Role", + "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "securityGroupObjectIds": [ + "3a4fa072-cc14-471d-aeac-49afdbde9f7a" + ] + } + ] + }, + "subscriptionBudget": { + "value": { + "createBudget": false + } + }, + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + "automation": { + "value": { + "name": "automation" + } + }, + "backupRecoveryVault": { + "value": { + "enabled": true, + "name": "backup-vault" + } + }, + "privateDnsZones": { + "value": { + "enabled": false, + "resourceGroupName": "pubsec-dns" + } + }, + + "privateDnsResolver": { + "value": { + "enabled": true, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, + + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + } + } \ No newline at end of file diff --git a/config/telemetry.json b/config/telemetry.json index fd7db1c1..b9e91337 100644 --- a/config/telemetry.json +++ b/config/telemetry.json @@ -10,6 +10,7 @@ "nvaFortinet": "a83f6385-f514-415f-991b-2d9bd7aed658", "azureFirewall": "a83f6385-f514-415f-991b-2d9bd7aed658" }, + "identity": "a83f6385-f514-415f-991b-2d9bd7aed658", "archetypes": { "genericSubscription": "a83f6385-f514-415f-991b-2d9bd7aed658", "machineLearning": "a83f6385-f514-415f-991b-2d9bd7aed658", diff --git a/config/variables/CanadaESLZ-main.yml b/config/variables/CanadaESLZ-main.yml index 56d8f1a8..2e020d24 100644 --- a/config/variables/CanadaESLZ-main.yml +++ b/config/variables/CanadaESLZ-main.yml @@ -67,6 +67,12 @@ variables: ## This parameter is only used for HIPAA/HITRUST Policy Assignment var-logging-diagnosticSettingsforNetworkSecurityGroupsStoragePrefix: pubsecnsg + # Platform Identity + var-identity-region: canadacentral + var-identity-managementGroupId: pubsecPlatformIdentity + var-identity-subscriptionId: b357bf7b-3328-4d21-b94b-4bfa84af97b1 + var-identity-configurationFileName: identity.parameters.json + # Hub Networking var-hubnetwork-region: canadacentral var-hubnetwork-managementGroupId: pubsecPlatformConnectivity diff --git a/docs/archetypes/identity.md b/docs/archetypes/identity.md new file mode 100644 index 00000000..f993f0d8 --- /dev/null +++ b/docs/archetypes/identity.md @@ -0,0 +1,342 @@ +# Archetype: Generic Subscription + +## Table of Contents + +- [Archetype: Generic Subscription](#archetype-generic-subscription) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Azure Deployment](#azure-deployment) + - [Schema Definition](#schema-definition) + - [Delete Locks](#delete-locks) + - [Service Health](#service-health) + - [Deployment Scenarios](#deployment-scenarios) + - [Example Deployment Parameters](#example-deployment-parameters) + - [Recommended Parameter Property Updates](#recommended-parameter-property-updates) + - [Service Health Alerts](#service-health-alerts) + - [Security Center](#security-center) + - [Subscription Role Assignments](#subscription-role-assignments) + - [Resource Tags and Preferred Naming Convention](#resource-tags-and-preferred-naming-convention) + - [Hub Virtual Network ID](#hub-virtual-network-id) + - [Deployment Instructions](#deployment-instructions) + +## Overview + +Identity and access management are core features of the Azure landing zone implementation. The deployment includes a subscription dedicated to identity, where customers can deploy the Active Directory domain controllers their environments require. This landing zone will be in the `pubsecPlatformIdentity` management group. + +![Archetype: Generic Subscription](../media/architecture/archetype-identity.jpg) + + +**Workflow** + +* A new subscription is created through existing process (either via ea.azure.com or Azure Portal). +* The subscription will automatically be assigned to the **pubsecSandbox** management group. +* Update configuration in Azure DevOps Git repo +* Execute the Platofrm - Identity azure DevOps Pipeline. The pipeline will: + * Move it to the target management group + * Scaffold the subscription with baseline configuration. + +**Subscription Move** + +Subscription can be moved to a target Management Group through Azure ARM Templates/Bicep. Move has been incorporated into the landing zone Azure DevOps Pipeline automation. + +**Capabilities** + +| Capability | Description | +| --- | --- | +| Service Health Alerts | Configures Service Health alerts such as Security, Incident, Maintenance. Alerts are configured with email, sms and voice notifications. | +| Microsoft Defender for Cloud | Configures security contact information (email and phone). | +| Subscription Role Assignments | Configures subscription scoped role assignments. Roles can be built-in or custom. | +| Subscription Budget | Configures monthly subscription budget with email notification. Budget is configured by default for 10 years and the amount. | +| Subscription Tags | A set of tags that are assigned to the subscription. | +| Resource Tags | A set of tags that are assigned to the resource group and resources. These tags must include all required tags as defined the Tag Governance policy. | +| Automation | Deploys an Azure Automation Account in each subscription. | +| Backup Recovery Vault | Configures a backup recovery vault . | +| Hub Networking | Configures virtual network peering to Hub Network which is required for egress traffic flow and hub-managed DNS resolution (on-premises or other spokes, private endpoints). +| Networking | A spoke virtual network with 1 to 3 subnets: Domain Controllers, DNS Resolver Inbound, DNS Resolver Outbound. Additional subnets can be configured at deployment time using configuration (see below). +| DNS Resolver | Configures DNS Resolver with inbound and outbound forwarding. Inbound forwarding is configured to forward DNS queries to the Hub Network DNS Resolver. Outbound forwarding is configured to forward DNS queries to the Hub Network DNS Resolver. | +| DNS Forwarding Ruleset | Configures DNS Forwarding Ruleset to forward DNS queries to the Hub Network DNS Resolver. +| Private DNS Zones | Optionally configures the PrivateLink DNS zones to be linked to the Identity vNET + +## Azure Deployment + +### Schema Definition + +Reference implementation uses parameter files with `object` parameters to consolidate parameters based on their context. The schemas types are: + +* Schema (version: `latest`) + + * [Identity deployment parameters definition](../../schemas/latest/landingzones/lz-platform-identity.json) + + * Common types + * [Location](../../schemas/latest/landingzones/types/location.json) + * [Service Health Alerts](../../schemas/latest/landingzones/types/serviceHealthAlerts.json) + * [Microsoft Defender for Cloud](../../schemas/latest/landingzones/types/securityCenter.json) + * [Subscription Role Assignments](../../schemas/latest/landingzones/types/subscriptionRoleAssignments.json) + * [Subscription Budget](../../schemas/latest/landingzones/types/subscriptionBudget.json) + * [Subscription Tags](../../schemas/latest/landingzones/types/subscriptionTags.json) + * [Resource Tags](../../schemas/latest/landingzones/types/resourceTags.json) + * [Log Analytics Workspace](../../schemas/latest/landingzones/types/logAnalyticsWorkspaceId.json) + * [Automation](../../schemas/latest/landingzones/types/automation.json) + * [Backup Recovery Vault](../../schemas/latest/landingzones/types/backupRecoveryVault.json) + + +### Service Health + +[Service health notifications](https://learn.microsoft.com/azure/service-health/service-health-notifications-properties) are published by Azure, and contain information about the resources under your subscription. Service health notifications can be informational or actionable, depending on the category. + +Our examples configure service health alerts for `Security` and `Incident`. However, these categories can be customized based on your need. Please review the possible options in [Azure Docs](https://learn.microsoft.com/azure/service-health/service-health-notifications-properties#details-on-service-health-level-information). + +### Deployment Scenarios + +> Sample deployment scenarios are based on the latest JSON parameters file schema definition. If you have an older version of this repository, please use the examples from your repository. + +| Scenario | Example JSON Parameters | Notes | +|:-------- |:----------------------- |:----- | +| Deployment with DNS Resolver | [tests/schemas/lz-platform-identity/lz-Identity-With-DNS-Resolver.json](../../tests/schemas/lz-platform-identity/lz-Identity-With-DNS-Resolver.json) | Does not deploy Private DNS Zones | +| Deployment without DNS Resolver| [tests/schemas/lz-platform-identity/lz-Identity-Without-DNS-Resolver.json](../../tests/schemas/lz-platform-identity/lz-Identity-Without-DNS-Resolver.json) | `parameters.privateDnsResolver.value.enabled` is `false`. If this setting is false the DNS Conditional Setting ruleset will not be deployed even if it's set to true | +| Deployment with DNS Resolver and Private DNS Zones | [tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones-And-DNS-Resolver.json](../../tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones-And-DNS-Resolver.json) | `parameters.privatednszones.value.enabled` is set to `true` && `parameters.privateDnsResolver.value.enabled` is `true` | +| Deployment with Private DNS Zones | [tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones.json](../../tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones.json) | `parameters.privatednszones.value.enabled` is set to `true`.| + +### Example Deployment Parameters + +This example configures: + +1. Service Health Alerts +2. Microsoft Defender for Cloud +3. Subscription Role Assignments using built-in and custom roles +4. Subscription Budget with $1000 +5. Subscription Tags +6. Resource Tags (aligned to the default tags defined in [Policies](../../policy/custom/definitions/policyset/Tags.parameters.json)) +7. Log Analytics Workspace integration through Azure Defender for Cloud +8. Automation Account +9. Backup Recovery Vault +10. Spoke Virtual Network with Hub-managed DNS, Virtual Network Peering and 3 subnets. + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "logAnalyticsWorkspaceResourceId": { + "value": "/subscriptions/46e3c98c-7c51-4634-be53-df9916714a46/resourceGroups/pubsec-central-logging/providers/Microsoft.OperationalInsights/workspaces/bwill-log-analytics-workspace" + }, + "serviceHealthAlerts": { + "value": { + "resourceGroupName": "service-health", + "incidentTypes": [ "Incident", "Security" ], + "regions": [ "Global", "Canada East", "Canada Central" ], + "receivers": { + "app": [ "alzcanadapubsec@microsoft.com" ], + "email": [ "alzcanadapubsec@microsoft.com" ], + "sms": [ { "countryCode": "1", "phoneNumber": "5555555555" } ], + "voice": [ { "countryCode": "1", "phoneNumber": "5555555555" } ] + }, + "actionGroupName": "Service health action group", + "actionGroupShortName": "health-alert", + "alertRuleName": "Incidents and Security", + "alertRuleDescription": "Service Health: Incidents and Security" + } + }, + "securityCenter": { + "value": { + "email": "alzcanadapubsec@microsoft.com", + "phone": "5555555555" + } + }, + "subscriptionRoleAssignments": { + "value": [ + { + "comments": "Built-in Owner Role", + "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "securityGroupObjectIds": [ + "3af30d9f-04aa-4d64-b4d5-407d7ff64ff8" + ] + } + ] + }, + "subscriptionBudget": { + "value": { + "createBudget": false + } + }, + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + "automation": { + "value": { + "name": "automation" + } + }, + "backupRecoveryVault": { + "value": { + "enabled": true, + "name": "backup-vault" + } + }, + "privateDnsZones": { + "value": { + "enabled": false, + "resourceGroupName": "pubsec-dns" + } + }, + + "privateDnsResolver": { + "value": { + "enabled": true, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/db8a3c31-7dbb-4368-8883-f9e6333ff23a/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, + + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + } + } +``` + +## Recommended Parameter Property Updates + +### Service Health Alerts + +Update the **serviceHealthAlerts** properties with specific email addresses and phone numbers as required. + +![Generic Subscription: Service Health Alerts](../../docs/media/archetypes/service-health-alerts-receivers.jpg) + +### Security Center + +Change the **securityCenter** properties with specific email and address values to reflect your actual point of contact. + +![Generic Subscription: Security Center](../../docs/media/archetypes/security-center-contact-info.jpg) + +### Subscription Role Assignments + +Modify the two **subscriptionRoleAssignments** properties with your specific unique object ids of the respective groups for the **Contributor** built-in +and **Custom Role: Landing Zone Application Owner** roles for this landing zone subscription. These assignments are optional and can be 0 or more role assignments using either Built-In or Custom roles and security groups. + +![Generic Subscription: Subscription Role Assignments](../../docs/media/archetypes/subscription-role-assignments.jpg) + +### Resource Tags and Preferred Naming Convention + +1. Specify the desired custom values for the **resourceTags** properties. +You may also include any additional name value pairs of tags required. Generally, these tags can be modified and even replaced as required, and should also align to the Tagging policy set paramters at: [Tag Policy](https://github.com/Azure/CanadaPubSecALZ/blob/main/policy/custom/definitions/policyset/Tags.parameters.json). + +2. Addtionally, you can customize default resources and resource group names with any specific preferred naming convention, as indicated by the item **2** circles shown below. + + +![Generic Subscription: Tags and Naming Conventions](../../docs/media/archetypes/resource-tags-and-naming-conventions.jpg) + +### Hub Virtual Network ID + +**IMPORTANT** + +To avoid a failure when running any of the connectivity pipelines, the subscriptionId segment value of the **hubNetwork** string (item **1**), must be updated from it's default value to the specific hubNetwork subscriptionId that was actually deployed previously, so that the virtual network in this spoke subscription can be VNET Peered to the Hub Network. + +![Generic Subscription: Hub Virtual Network ID](../../docs/media/archetypes/virtual-network-id.jpg) + +The rest of the segments for the **virtualNetworkId** string must also match the actual resources that were deployed from the connectivity pipeline, such as the name of the resource group, +in case a different prefix besides **pubsec** was used to conform to a specific and preferred naming convention or organization prefix (item **2**), or the default VNET name of hub-vnet was also changed to something else, +(**item 3**) - again based on a specific and preferred naming convention that may have been used before when the actual hub VNET was deployed. + +### Deployment Instructions + +### Virtual Appliance IP +To ensure traffic is routed/filtered via the firewall, please validate or update the "egressVirtualApplianceIp" value to the firewall IP in your environment: + - For Azure Firewall, use the firewall IP address + - For Network Virtual Appliances (i.e. Fortigate firewalls), use the internal load-balancer IP (item **1**) +![Generic Subscription:Egress Virtual Appliance IP](../../docs/media/archetypes/egressvirtualApplianceIP.jpg) + +Please see [archetype authoring guide for deployment instructions](authoring-guide.md#deployment-instructions). diff --git a/docs/architecture.md b/docs/architecture.md index 36ee398f..adf26610 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -293,19 +293,28 @@ Azure PaaS services use Private DNS Zones to map their fully qualified domain na * Private DNS Zones from being created in the spoke subscriptions. These can only be created in the designated resource group in the Hub Subscription. * Ensure private endpoints can be automatically mapped to the centrally managed Private DNS Zones. -The following diagram shows a typical high-level architecture for enterprise environments with central DNS resolution and name resolution for Private Link resources via Azure Private DNS. This topology provides: +The following diagrams show a typical high-level architecture for enterprise environments with central DNS resolution and name resolution for Private Link resources via Azure Private DNS. This topology provides: * Name resolution from hub to spoke * Name resolution from spoke to spoke * Name resolution from on-premises to Azure (Hub & Spoke resources). Additional configuration is required to deploy DNS resolvers in the Hub Network & provide DNS forwarding from on-premises to Azure. +**`DNS Resolution using Azure DNS Resolver`** +![DNS using Azure DNS Resolver](https://learn.microsoft.com/en-us/azure/dns/media/dns-resolver-overview/resolver-architecture.png) +**Reference:** [What is Azure DNS Private Resolver?](https://learn.microsoft.com/en-us/azure/dns/dns-resolver-overview) + + + +**`DNS using Virtual Machines managed by IT`** + ![Hub Managed DNS](media/architecture/hubnetwork-private-link-central-dns.png) **Reference:** [Private Link and DNS integration at scale](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/private-link-and-dns-integration-at-scale) Reference implementation provides the following capabilities: -* Deploy Private DNS Zones to the Hub Networking subscription. Enable/disable via configuration. +* Deploy Private DNS Zones to the Hub Networking or Identity subscription. Enable/disable via configuration. +* Deploy Azure DNS Private Resolver to the Hub or Identity Subscription. Enable/disable via configuration. * Azure Policy to block private zones from being created outside of the designated resource group in the Hub networking subscription. * Azure Policy to automatically detect new private endpoints and add their A records to their respective Private DNS Zone. * Support to ensure Hub managed Private DNS Zones are used when deploying archetypes. @@ -316,7 +325,7 @@ The reference implementation does not deploy DNS Servers (as Virtual Machines) i * Link Private DNS Zones directly to the spoke virtual networks and use the [built-in DNS resolver in each virtual network](https://learn.microsoft.com/azure/virtual-network/virtual-networks-name-resolution-for-vms-and-role-instances). Virtual network(s) in spoke subscriptions be configured through Virtual Network Link for name resolution. DNS resolution is automatic once the Private DNS Zone is linked to the virtual network. -* Leverage DNS Servers on virtual machines that are managed by department's IT. +* Leverage DNS Services from either Azure DNS Private Resolver or on virtual machines that are managed by department's IT. ### Spoke Landing Zone Networks @@ -556,6 +565,7 @@ Use the [Azure DevOps Pipelines](onboarding/azure-devops-pipelines.md) onboardin | Platform – Hub Networking using NVAs | platform-connectivity-hub-nva.yml | platform-connectivity-hub-nva-ci | Configures Hub Networking with Fortigate Firewalls. | spn-azure-platform-ops | None | | Platform – Hub Networking with Azure Firewall - Firewall Policy | platform-connectivity-hub-azfw-policy.yml | platform-connectivity-hub-azfw-policy-ci | Configures Azure Firewall Policy. A policy contains firewall rules and firewall configuration such as enabling DNS Proxy. Firewall policies can be updated independently of Azure Firewall. | spn-azure-platform-ops | None | | Platform – Hub Networking with Azure Firewall | platform-connectivity-hub-azfw.yml | platform-connectivity-hub-azfw-ci | Configures Hub Networking with Azure Firewall. | spn-azure-platform-ops | None | +| Identity | platform-identity.yml | platform-identity-ci | Configures a Identity Landing Zone that will be used by all landing zones for managing identities services (i.e. Domain Controllers). | spn-azure-platform-ops | None | | Subscriptions | subscriptions.yml | subscriptions-ci | Configures a new subscription based on the archetype defined in the configuration file name. | spn-azure-platform-ops | None | | Pull Request Validation | pull-request-check.yml | pull-request-validation-ci | Checks for breaking changes to Bicep templates & parameter schemas prior to merging the change to main branch. This pipeline must be configured as a check for the `main` branch. | spn-azure-platform-ops | None | diff --git a/docs/media/architecture/archetype-identity.jpg b/docs/media/architecture/archetype-identity.jpg new file mode 100644 index 0000000000000000000000000000000000000000..56d1745fe0f2ceb93392b5e8ced0687be25e4a1a GIT binary patch literal 140799 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 literal 0 HcmV?d00001 diff --git a/docs/onboarding/azure-devops-pipelines.md b/docs/onboarding/azure-devops-pipelines.md index 1deed16f..4f71e426 100644 --- a/docs/onboarding/azure-devops-pipelines.md +++ b/docs/onboarding/azure-devops-pipelines.md @@ -63,6 +63,7 @@ This deployment diagram describes the steps for deploying one, many or all modul Logging: Logging Policy: Azure Policy HubNetworking: Hub Networking (NVAs or Azure Firewall) + Identity: Identity Archetypes: Archetypes (Spokes) [*] --> ManagementGroups @@ -73,10 +74,13 @@ This deployment diagram describes the steps for deploying one, many or all modul Policy --> HubNetworking Policy --> Archetypes + HubNetworking --> Identity + HubNetworking --> Archetypes Policy --> [*] + Identity --> [*] HubNetworking --> [*] Archetypes --> [*] ``` @@ -103,6 +107,10 @@ This deployment diagram describes the steps for deploying one, many or all modul AssignDDOSPolicy: [Optional] Assign Azure Policy for linking DDoS Standard Plan to virtual network AssignPrivateDNSZonesPolicy: [Optional] Assign Azure Policies for centrally managing private DNS zones + Identity: Identity + DeployVirtualNetwork: Deploy Virtual Network + DeployDNSResolver: Deploy DNS Resolver (optional) + Archetypes: Archetypes (Spokes) DeployGenericSubscriptionArchetype: Generic Subscription DeployMachineLearningArchetype: Machine Learning @@ -126,6 +134,7 @@ This deployment diagram describes the steps for deploying one, many or all modul } Policy --> HubNetworking: When Hub Networking is required + HubNetworking --> Archetypes: When archetypes are deployed in spoke subscriptions Policy --> Archetypes: When existing Hub Networking is in place state HubNetworking { @@ -150,7 +159,13 @@ This deployment diagram describes the steps for deploying one, many or all modul AssignPrivateDNSZonesPolicy --> [*] } - HubNetworking --> Archetypes: When archetypes are deployed in spoke subscriptions + HubNetworking --> Identity: When Identity Sub is required + + state Identity { + DeployVirtualNetwork --> DeployDNSResolver + DeployDNSResolver --> [*] + } + state Archetypes { state ArchetypeChoice <> @@ -163,7 +178,8 @@ This deployment diagram describes the steps for deploying one, many or all modul } Policy --> [*]: MVP deployment and enables Microsoft Sentinel & Log Analytics - HubNetworking --> [*] + HubNetworking --> [*]: Identity Sub is NOT required + Identity --> [*] Archetypes --> [*] ``` @@ -178,7 +194,8 @@ This deployment diagram describes the steps for deploying one, many or all modul * [Step 5 - Configure Logging](#step-5---configure-logging) * [Step 6 - Configure Azure Policies](#step-6---configure-azure-policies) * [Step 7 - Configure Hub Networking](#step-7---configure-hub-networking) -* [Step 8 - Configure Subscription Archetypes](#step-8---configure-subscription-archetypes) +* [Step 8 - Configure Identity subscription](#step-8---configure-identity-subscription) +* [Step 9 - Configure Subscription Archetypes](#step-9---configure-subscription-archetypes) * [Appendix](#appendix) * [Populate management group hierarchy from your environment](#populate-management-group-hierarchy-from-your-environment) * [Migrate Logging configuration from Azure DevOps variables to JSON parameters file](#migrate-logging-configuration-from-azure-devops-variables-to-json-parameters-file) @@ -1479,8 +1496,168 @@ In order to configure audit stream for Azure Monitor, identify the following inf * When using Hub Networking with Azure Firewall, run `platform-connectivity-hub-azfw-policy-ci` pipeline first. This ensures that the Azure Firewall Policy is deployed and can be used as a reference for Azure Firewall. This approach allows for Azure Firewall Policies (such as allow/deny rules) to be managed independently from the Hub Networking components. --- +## Step 8 - Configure Identity Subscription + +1. Configure Pipeline definition for Identity + + > Pipelines are stored as YAML definitions in Git and imported into Azure DevOps Pipelines. This approach allows for portability and change tracking. + + 1. Go to Pipelines + 1. New Pipeline + 1. Choose Azure Repos Git + 1. Select Repository + 1. Select Existing Azure Pipeline YAML file + 1. Identify the pipeline in `.pipelines/platform-identity.yml`. + 1. Save the pipeline (don't run it yet) + 1. Rename the pipeline to `identity-ci` + +1. Create a subscription configuration file (JSON) + + 1. Create directory ./config/identity. + + 1. Create subdirectory based on the syntax: `-` (i.e. `CanadaESLZ-main` to create path `./config/identity/CanadaESLZ-main/`). + + 1. Make a copy of an existing subscription configuration file under `config/identity/CanadaESLZ-main` as a starting point + + 1. Define deployment parameters based on example below. + + * Set the values for the Azure tags that would be applied to the identity subscription and the Identity resources. + * Set resource group names for the Identity resources. + > **Note:** Each resource group is created if the associated resource's Enabled flag is set to `true`. + + * Example deployment parameters file: + + ```json + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + ``` + * Configure Azure DNS Private Resolver + + ```json + "privateDnsResolver": { + "value": { + "enabled": true, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + ``` + * Configure the Identity network & it's peering connection to the hub network + > **Note:** If the PrivateDnsResolver Enabled setting is set to `false` the associated DNS Resolver subnets will not be deployed. + + ```json + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/db8a3c31-7dbb-4368-8883-f9e6333ff23a/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, -## Step 8 - Configure Subscription Archetypes + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + ``` + +1. Configure the Azure DevOps pipeline for Identity + + 1. In Azrue DevOps, go to Pipelines + 1. New Pipeline + 1. Select Existing Azrue Pipline YAML file + 1. Identify the pipeline in `.pipelines/identity.yml` + 1. save the pipeline (don't run it yet) + 1. Rename the pipeline to `identity-ci` + +1. Run pipeline and wait for completion + +--- +## Step 9 - Configure Subscription Archetypes 1. Configure Pipeline definition for subscription archetypes diff --git a/docs/onboarding/azure-devops-scripts.md b/docs/onboarding/azure-devops-scripts.md index df1015a7..e00d1470 100644 --- a/docs/onboarding/azure-devops-scripts.md +++ b/docs/onboarding/azure-devops-scripts.md @@ -260,6 +260,7 @@ Run the `create-pipelines.bat` script to create the landing zone pipelines: - platform-connectivity-hub-nva-ci - platform-connectivity-hub-azfw-ci - platform-connectivity-hub-azfw-policy-ci +- platform-identity-ci - subscriptions-ci If you would rather perform these steps manually, detailed guidance is available in the following sections of the [Azure DevOps Pipelines Onboarding Guide](./azure-devops-pipelines.md): @@ -269,7 +270,8 @@ If you would rather perform these steps manually, detailed guidance is available - [Step 5 - Configure Logging](./azure-devops-pipelines.md#step-5--configure-logging) - [Step 6 - Configure Azure Policies](./azure-devops-pipelines.md#step-6---configure-azure-policies) - [Step 7 - Configure Hub Networking](./azure-devops-pipelines.md#step-7---configure-hub-networking) -- [Step 8 - Configure Subscription Archetypes](./azure-devops-pipelines.md#step-8---configure-subscription-archetypes) +- [Step 8 - Configure Identity Subscription](./azure-devops-pipelines.md#step-8---configure-identity-subscription) +- [Step 9 - Configure Subscription Archetypes](./azure-devops-pipelines.md#step-9---configure-subscription-archetypes) ### Give pipelines access to service endpoint diff --git a/landingzones/lz-platform-identity/dnsResolver.bicep b/landingzones/lz-platform-identity/dnsResolver.bicep new file mode 100644 index 00000000..a1a16eae --- /dev/null +++ b/landingzones/lz-platform-identity/dnsResolver.bicep @@ -0,0 +1,112 @@ +// ---------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// ---------------------------------------------------------------------------------- + +targetScope = 'subscription' + + +@description('Location for the deployment.') +param location string = deployment().location + + +// Example (JSON) +// ----------------------------- +// "resourceTags": { +// "value": { +// "ClientOrganization": "client-organization-tag", +// "CostCenter": "cost-center-tag", +// "DataSensitivity": "data-sensitivity-tag", +// "ProjectContact": "project-contact-tag", +// "ProjectName": "project-name-tag", +// "TechnicalContact": "technical-contact-tag" +// } +// } + +// Example (Bicep) +// ----------------------------- +// { +// ClientOrganization: 'client-organization-tag' +// CostCenter: 'cost-center-tag' +// DataSensitivity: 'data-sensitivity-tag' +// ProjectContact: 'project-contact-tag' +// ProjectName: 'project-name-tag' +// TechnicalContact: 'technical-contact-tag' +// } +@description('A set of key/value pairs of tags assigned to the resource group and resources.') +param resourceTags object + +@description('Network configuration for the spoke virtual network. It includes name, dnsServers, address spaces, vnet peering and subnets.') +param network object + +// Private DNS Resolver +@description('Private DNS Resolver configuration for Inbound connections.') +param privateDnsResolver object + +// Private DNS Resolver Ruleset +@description('Private DNS Resolver Default Ruleset Configuration') +param privateDnsResolverRuleset object + +// Private DNS Resolver Ruleset +@description('Private DNS Resolver Default Ruleset Configuration') +param dnsResolverRG string + +// vnet resource group +@description('virtual network resource group name') +param rgVnet string + +// vnet +@description('virtual network ID') +param vnetId string + +// vnet +@description('virtual network Name') +param vnetName string + + + + +// Create Private DNS Resolver Resource Group +resource rgPrivateDnsResolver 'Microsoft.Resources/resourceGroups@2020-06-01' = { + name: dnsResolverRG + location: location + tags: resourceTags +} + +//create Private DNS Resolver +module dnsResolver '../../azresources/network/dnsresolver.bicep' ={ + name:'deploy-private-dns-resolver' + scope: rgPrivateDnsResolver + params:{ + name: privateDnsResolver.name + location: location + inboundEndpointName: privateDnsResolver.inboundEndpointName + inboundSubnetName: network.subnets.dnsResolverInbound.name + outboundEndpointName: privateDnsResolver.outboundEndpointName + outboundSubnetName: network.subnets.dnsResolverOutbound.name + vnetResourceGroupName: rgVnet + vnetId: vnetId + vnetName: vnetName + } +} + +module dnsResolverFwRuleset '../../azresources/network/dns-forwarding-ruleset.bicep' = if (privateDnsResolverRuleset.enabled) { + name:'deploy-private-dns-resolver-fw-ruleset' + scope: rgPrivateDnsResolver + + params:{ + name: privateDnsResolverRuleset.name + location: location + outEndpointId: dnsResolver.outputs.outboundEndpointId + + forwardingRuleSet: privateDnsResolverRuleset.forwardingRules + + linkRuleSetToVnet: privateDnsResolverRuleset.linkRuleSetToVnet + linkName: privateDnsResolverRuleset.linkRuleSetToVnetName + vnetId: vnetId + } +} diff --git a/landingzones/lz-platform-identity/main.bicep b/landingzones/lz-platform-identity/main.bicep new file mode 100644 index 00000000..1631cc5d --- /dev/null +++ b/landingzones/lz-platform-identity/main.bicep @@ -0,0 +1,340 @@ +// ---------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// ---------------------------------------------------------------------------------- + + +/* + +Identity Landing Zone to support ESLZ topology. This architeype will provide: + +* Azure Automation Account +* Azure Virtual Network +* Azure Recovery Services Vault +* Role-based access control for Owner, Contributor & Reader +* Integration with Azure Cost Management for Subscription-scoped budget +* Integration with Microsoft Defender for Cloud +* Azure Private DNS Resolver & Conditional Forwarder Zone (optional). +* Enables Azure Private DNS Zones (optional). + +*/ + +targetScope = 'subscription' + +@description('Location for the deployment.') +param location string = deployment().location + +// Service Health +// Example (JSON) +// ----------------------------- +// "serviceHealthAlerts": { +// "value": { +// "incidentTypes": [ "Incident", "Security", "Maintenance", "Information", "ActionRequired" ], +// "regions": [ "Global", "Canada East", "Canada Central" ], +// "receivers": { +// "app": [ "email-1@company.com", "email-2@company.com" ], +// "email": [ "email-1@company.com", "email-3@company.com", "email-4@company.com" ], +// "sms": [ { "countryCode": "1", "phoneNumber": "1234567890" }, { "countryCode": "1", "phoneNumber": "0987654321" } ], +// "voice": [ { "countryCode": "1", "phoneNumber": "1234567890" } ] +// }, +// "actionGroupName": "ALZ action group", +// "actionGroupShortName": "alz-alert", +// "alertRuleName": "ALZ alert rule", +// "alertRuleDescription": "Alert rule for Azure Landing Zone" +// } +// } +@description('Service Health alerts') +param serviceHealthAlerts object = {} + +// Log Analytics +@description('Log Analytics Resource Id to integrate Microsoft Defender for Cloud.') +param logAnalyticsWorkspaceResourceId string + +// Microsoft Defender for Cloud +// Example (JSON) +// ----------------------------- +// "securityCenter": { +// "value": { +// "email": "alzcanadapubsec@microsoft.com", +// "phone": "5555555555" +// } +// } + +// Example (Bicep) +// ----------------------------- +// { +// email: 'alzcanadapubsec@microsoft.com' +// phone: '5555555555' +// } +@description('Microsoft Defender for Cloud configuration. It includes email and phone.') +param securityCenter object + +// Subscription Role Assignments +// Example (JSON) +// ----------------------------- +// [ +// { +// "comments": "Built-in Contributor Role", +// "roleDefinitionId": "b24988ac-6180-42a0-ab88-20f7382dd24c", +// "securityGroupObjectIds": [ +// "38f33f7e-a471-4630-8ce9-c6653495a2ee" +// ] +// } +// ] + +// Example (Bicep) +// ----------------------------- +// [ +// { +// comments: 'Built-In Contributor Role' +// roleDefinitionId: 'b24988ac-6180-42a0-ab88-20f7382dd24c' +// securityGroupObjectIds: [ +// '38f33f7e-a471-4630-8ce9-c6653495a2ee' +// ] +// } +// ] +@description('Array of role assignments at subscription scope. The array will contain an object with comments, roleDefinitionId and array of securityGroupObjectIds.') +param subscriptionRoleAssignments array = [] + +// Subscription Budget +// Example (JSON) +// --------------------------- +// "subscriptionBudget": { +// "value": { +// "createBudget": false, +// "name": "MonthlySubscriptionBudget", +// "amount": 1000, +// "timeGrain": "Monthly", +// "contactEmails": [ "alzcanadapubsec@microsoft.com" ] +// } +// } + +// Example (Bicep) +// --------------------------- +// { +// createBudget: true +// name: 'MonthlySubscriptionBudget' +// amount: 1000 +// timeGrain: 'Monthly' +// contactEmails: [ +// 'alzcanadapubsec@microsoft.com' +// ] +// } +@description('Subscription budget configuration containing createBudget flag, name, amount, timeGrain and array of contactEmails') +param subscriptionBudget object + +// Tags +// Example (JSON) +// ----------------------------- +// "subscriptionTags": { +// "value": { +// "ISSO": "isso-tag" +// } +// } + +// Example (Bicep) +// --------------------------- +// { +// ISSO: 'isso-tag' +// } +@description('A set of key/value pairs of tags assigned to the subscription.') +param subscriptionTags object + +// Example (JSON) +// ----------------------------- +// "resourceTags": { +// "value": { +// "ClientOrganization": "client-organization-tag", +// "CostCenter": "cost-center-tag", +// "DataSensitivity": "data-sensitivity-tag", +// "ProjectContact": "project-contact-tag", +// "ProjectName": "project-name-tag", +// "TechnicalContact": "technical-contact-tag" +// } +// } + +// Example (Bicep) +// ----------------------------- +// { +// ClientOrganization: 'client-organization-tag' +// CostCenter: 'cost-center-tag' +// DataSensitivity: 'data-sensitivity-tag' +// ProjectContact: 'project-contact-tag' +// ProjectName: 'project-name-tag' +// TechnicalContact: 'technical-contact-tag' +// } +@description('A set of key/value pairs of tags assigned to the resource group and resources.') +param resourceTags object + +// Resource Groups +@description('Resource groups required for the archetype. It includes automation, networking and networkWatcher.') +param resourceGroups object + +// RecoveryVault +@description('Azure recovery vault configuration containing enabled flag, and name') +param backupRecoveryVault object + +// Azure Automation Account +@description('Azure Automation Account configuration. Includes name.') +param automation object + +// Networking +@description('Hub Network configuration that includes virtualNetworkId, rfc1918IPRange, rfc6598IPRange and egressVirtualApplianceIp.') +param hubNetwork object + +@description('Network configuration for the spoke virtual network. It includes name, dns services, address spaces, vnet peering and subnets.') +param network object + +// Private Dns Zones +@description('Private DNS Zones configuration. See docs/archetypes/identity.md for configuration settings.') +param privateDnsZones object + +// Private DNS Resolver +@description('Private DNS Resolver configuration for Inbound connections.') +param privateDnsResolver object + +// Private DNS Resolver Ruleset +@description('Private DNS Resolver Default Ruleset Configuration') +param privateDnsResolverRuleset object + + +// Telemetry - Azure customer usage attribution +// Reference: https://docs.microsoft.com/azure/marketplace/azure-partner-customer-usage-attribution +var telemetry = json(loadTextContent('../../config/telemetry.json')) +module telemetryCustomerUsageAttribution '../../azresources/telemetry/customer-usage-attribution-subscription.bicep' = if (telemetry.customerUsageAttribution.enabled) { + name: 'pid-${telemetry.customerUsageAttribution.modules.identity}' +} + +/* + Scaffold the subscription which includes: + * Microsoft Defender for Cloud - Enable Azure Defender (all available options) + * Microsoft Defender for Cloud - Configure Log Analytics Workspace + * Microsoft Defender for Cloud - Configure Security Alert Contact + * Service Health Alerts + * Role Assignments to Security Groups + * Subscription Budget + * Subscription Tags +*/ +module subScaffold '../scaffold-subscription.bicep' = { + name: 'configure-subscription' + scope: subscription() + params: { + location: location + + serviceHealthAlerts: serviceHealthAlerts + + logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId + securityCenter: securityCenter + + subscriptionBudget: subscriptionBudget + + subscriptionTags: subscriptionTags + resourceTags: resourceTags + + subscriptionRoleAssignments: subscriptionRoleAssignments + } +} + +// Create Network Watcher Resource Group +resource rgNetworkWatcher 'Microsoft.Resources/resourceGroups@2020-06-01' = { + name: resourceGroups.networkWatcher + location: location + tags: resourceTags +} + +// Create Virtual Network Resource Group - only if Virtual Network is being deployed +resource rgVnet 'Microsoft.Resources/resourceGroups@2020-06-01' = if (network.deployVnet) { + name: network.deployVnet ? resourceGroups.networking : 'placeholder' + location: location + tags: resourceTags +} + +// Create Azure Automation Resource Group +resource rgAutomation 'Microsoft.Resources/resourceGroups@2020-06-01' = { + name: resourceGroups.automation + location: location + tags: resourceTags +} + +// Create Azure backup RecoveryVault Resource Group +resource rgBackupVault 'Microsoft.Resources/resourceGroups@2020-06-01' =if (backupRecoveryVault.enabled) { + name: resourceGroups.backupRecoveryVault + location: location + tags: resourceTags +} + +// Create Private DNS Zones Resource Group +resource rgPrivateDnsZones 'Microsoft.Resources/resourceGroups@2020-06-01' =if (privateDnsZones.enabled) { + name: resourceGroups.privateDnsZones + location: location + tags: resourceTags +} + +// Create automation account +module automationAccount '../../azresources/automation/automation-account.bicep' = { + name: 'deploy-automation-account' + scope: rgAutomation + params: { + automationAccountName: automation.name + tags: resourceTags + location: location + } +} + +//create recovery vault for backup of vms +module backupVault '../../azresources/management/backup-recovery-vault.bicep'= if(backupRecoveryVault.enabled){ + name:'deploy-backup-recoveryvault' + scope: rgBackupVault + params:{ + vaultName: backupRecoveryVault.name + tags: resourceTags + location: location + } +} + +// Create & configure virtaual network - only if Virtual Network is being deployed +module vnet 'networking.bicep' = if (network.deployVnet) { + name: 'deploy-networking' + scope: resourceGroup(rgVnet.name) + params: { + hubNetwork: hubNetwork + network: network + location: location + deployDNSResolver: privateDnsResolver + } +} + +module dnsResolver 'dnsResolver.bicep' = if (privateDnsResolver.enabled) { + name: 'deploy-dns-resolver' + scope: subscription() + params: { + privateDnsResolver: privateDnsResolver + location: location + rgVnet: rgVnet.name + vnetId: vnet.outputs.vnetId + vnetName: vnet.outputs.vnetName + network: network + resourceTags: resourceTags + privateDnsResolverRuleset: privateDnsResolverRuleset + dnsResolverRG: resourceGroups.dnsResolver + } +} + +// Private DNS Zones +module privatelinkDnsZones '../../azresources/network/private-dns-zone-privatelinks.bicep' = if (privateDnsZones.enabled) { + name: 'deploy-privatelink-private-dns-zones' + scope: resourceGroup(resourceGroups.privateDnsZones) + params: { + vnetId: vnet.outputs.vnetId + dnsCreateNewZone: true + dnsLinkToVirtualNetwork: true + + // Not required since the private dns zones will be created and linked to hub virtual network. + dnsExistingZoneSubscriptionId: '' + dnsExistingZoneResourceGroupName: '' + } +} diff --git a/landingzones/lz-platform-identity/networking.bicep b/landingzones/lz-platform-identity/networking.bicep new file mode 100644 index 00000000..6e08bfca --- /dev/null +++ b/landingzones/lz-platform-identity/networking.bicep @@ -0,0 +1,404 @@ +// ---------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// ---------------------------------------------------------------------------------- + +@description('Location for the deployment.') +param location string = resourceGroup().location + +// Networking +// Example (JSON) +// ----------------------------- +// "hubNetwork": { +// "value": { +// "virtualNetworkId": "/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", +// "rfc1918IPRange": "10.18.0.0/22", +// "rfc6598IPRange": "100.60.0.0/16", +// "egressVirtualApplianceIp": "10.18.0.36", +// "privateDnsManagedByHub": true, +// "privateDnsManagedByHubSubscriptionId": "ed7f4eed-9010-4227-b115-2a5e37728f27", +// "privateDnsManagedByHubResourceGroupName": "pubsec-dns" +// } +// } + +// Example (Bicep) +// ----------------------------- +// { +// virtualNetworkId: '/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet' +// rfc1918IPRange: '10.18.0.0/22' +// rfc6598IPRange: '100.60.0.0/16' +// egressVirtualApplianceIp: '10.18.0.36' +// privateDnsManagedByHub: true, +// privateDnsManagedByHubSubscriptionId: 'ed7f4eed-9010-4227-b115-2a5e37728f27', +// privateDnsManagedByHubResourceGroupName: 'pubsec-dns' +// } +@description('Hub Network configuration that includes virtualNetworkId, rfc1918IPRange, rfc6598IPRange, egressVirtualApplianceIp, privateDnsManagedByHub flag, privateDnsManagedByHubSubscriptionId and privateDnsManagedByHubResourceGroupName.') +param hubNetwork object + +// Example (JSON) +// ----------------------------- +// "network": { +// "value": { +// "peerToHubVirtualNetwork": true, +// "useRemoteGateway": false, +// "name": "vnet", +// "dnsServers": [ +// "10.18.1.4" +// ], +// "addressPrefixes": [ +// "10.2.0.0/16" +// ], +// "subnets": { +// "privateEndpoints": { +// "comments": "Private Endpoints Subnet", +// "name": "privateendpoints", +// "addressPrefix": "10.2.5.0/25" +// }, +// "sqlmi": { +// "comments": "SQL Managed Instances Delegated Subnet", +// "name": "sqlmi", +// "addressPrefix": "10.2.6.0/25" +// }, +// "databricksPublic": { +// "comments": "Databricks Public Delegated Subnet", +// "name": "databrickspublic", +// "addressPrefix": "10.2.7.0/25" +// }, +// "databricksPrivate": { +// "comments": "Databricks Private Delegated Subnet", +// "name": "databricksprivate", +// "addressPrefix": "10.2.8.0/25" +// }, +// "aks": { +// "comments": "AKS Subnet", +// "name": "aks", +// "addressPrefix": "10.2.9.0/25" +// }, +// "appService": { +// "comments": "App Service Subnet", +// "name": "appService", +// "addressPrefix": "10.2.10.0/25" +// } +// "optional": [ +// { +// "comments": "Optional Subnet 1", +// "name": "virtualMachines", +// "addressPrefix": "10.6.11.0/25", +// "nsg": { +// "enabled": true +// }, +// "udr": { +// "enabled": true +// } +// }, +// { +// "comments": "Optional Subnet 2 with delegation for NetApp Volumes", +// "name": "NetappVolumes", +// "addressPrefix": "10.6.12.0/25", +// "nsg": { +// "enabled": false +// }, +// "udr": { +// "enabled": false +// }, +// "delegations": { +// "serviceName": "Microsoft.NetApp/volumes" +// } +// } +// ] +// } +// } +// } + +// Example (Bicep) +// ----------------------------- +// { +// peerToHubVirtualNetwork: true +// useRemoteGateway: false +// name: 'vnet' +// dnsServers: [ +// '10.18.1.4' +// ] +// addressPrefixes: [ +// '10.2.0.0/16' +// ] +// subnets: { +// privateEndpoints: { +// comments: 'Private Endpoints Subnet' +// name: 'privateendpoints' +// addressPrefix: '10.2.5.0/25' +// } +// sqlmi: { +// comments: 'SQL Managed Instances Delegated Subnet' +// name: 'sqlmi' +// addressPrefix: '10.2.6.0/25' +// } +// databricksPublic: { +// comments: 'Databricks Public Delegated Subnet' +// name: 'databrickspublic' +// addressPrefix: '10.2.7.0/25' +// } +// databricksPrivate: { +// comments: 'Databricks Private Delegated Subnet' +// name: 'databricksprivate' +// addressPrefix: '10.2.8.0/25' +// } +// aks: { +// comments: 'AKS Subnet' +// name: 'aks' +// addressPrefix: '10.2.9.0/25' +// } +// appService: { +// comments: 'App Service Subnet' +// name: 'appService' +// addressPrefix: '10.2.10.0/25' +// } +// optional: [ +// { +// comments: 'Optional Subnet 1' +// name: 'virtualMachines' +// addressPrefix: '10.6.11.0/25' +// nsg: { +// enabled: true +// }, +// udr: { +// enabled: true +// } +// }, +// { +// comments: 'Optional Subnet 2 with delegation for NetApp Volumes', +// name: 'NetappVolumes' +// addressPrefix: '10.6.12.0/25' +// nsg: { +// enabled: false +// }, +// udr: { +// enabled: false +// }, +// delegations: { +// serviceName: 'Microsoft.NetApp/volumes' +// } +// } +// ] +// } +// } +@description('Network configuration for the spoke virtual network. It includes name, dnsServers, address spaces, vnet peering and subnets.') +param network object + +@description('Get the DNS Private Resolver enabled/disabled setting so the associated subnets can be optionally deployed based on the value.') +param deployDNSResolver object + +var hubVnetIdSplit = split(hubNetwork.virtualNetworkId, '/') + + +var routesToHub = [ + // Force Routes to Hub IPs (RFC1918 range) via FW despite knowing that route via peering + { + name: 'SpokeUdrHubRFC1918FWRoute' + properties: { + addressPrefix: hubNetwork.rfc1918IPRange + nextHopType: 'VirtualAppliance' + nextHopIpAddress: hubNetwork.egressVirtualApplianceIp + } + } + // Force Routes to Hub IPs (CGNAT range) via FW despite knowing that route via peering + { + name: 'SpokeUdrHubRFC6598FWRoute' + properties: { + addressPrefix: hubNetwork.rfc6598IPRange + nextHopType: 'VirtualAppliance' + nextHopIpAddress: hubNetwork.egressVirtualApplianceIp + } + } + { + name: 'RouteToEgressFirewall' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: hubNetwork.egressVirtualApplianceIp + } + } +] + +// Network Security Groups +resource nsg 'Microsoft.Network/networkSecurityGroups@2021-02-01' = [for subnet in network.subnets.optional: if (subnet.nsg.enabled) { + name: '${subnet.name}Nsg' + location: location + properties: { + securityRules: [] + } +}] + +module nsgDomainControllers '../../azresources/network/nsg/nsg-empty.bicep' = { + name: 'deploy-nsg-DomainControllers' + params: { + name: '${network.subnets.domainControllers.name}Nsg' + location: location + } +} + +module nsgDnsResolverInbound '../../azresources/network/nsg/nsg-empty.bicep' = if (deployDNSResolver.enabled) { + name: 'deploy-nsg-dnsResolverInbound' + params: { + name: '${network.subnets.dnsResolverInbound.name}Nsg' + location: location + } +} + +module nsgDnsResolverOutbound '../../azresources/network/nsg/nsg-empty.bicep' = if (deployDNSResolver.enabled) { + name: 'deploy-nsg-dnsResolverOutbound' + params: { + name: '${network.subnets.dnsResolverOutbound.name}Nsg' + location: location + } +} + +// Route Tables +resource udr 'Microsoft.Network/routeTables@2021-02-01' = { + name: 'RouteTable' + location: location + properties: { + routes: network.peerToHubVirtualNetwork ? routesToHub : null + } +} + +// Virtual Network +var requiredSubnets = [ + { + name: network.subnets.domainControllers.name + properties: { + addressPrefix: network.subnets.domainControllers.addressPrefix + privateEndpointNetworkPolicies: 'Enabled' + routeTable: { + id: udr.id + } + networkSecurityGroup: { + id: nsgDomainControllers.outputs.nsgId + } + } + } +] + +var dnsResolverSubnets = [ + { + name: network.subnets.dnsResolverInbound.name + properties: { + addressPrefix: network.subnets.dnsResolverInbound.addressPrefix + privateEndpointNetworkPolicies: 'Enabled' + routeTable: { + id: udr.id + } + networkSecurityGroup: { + id: nsgDnsResolverInbound.outputs.nsgId + } + delegations: [ + { + name: 'delAzureDNSResolverInbound' + properties: { + serviceName: 'Microsoft.Network/dnsResolvers' + } + } + ] + } + } + + { + name: network.subnets.dnsResolverOutbound.name + properties: { + addressPrefix: network.subnets.dnsResolverOutbound.addressPrefix + privateEndpointNetworkPolicies: 'Enabled' + routeTable: { + id: udr.id + } + networkSecurityGroup: { + id: nsgDnsResolverOutbound.outputs.nsgId + } + delegations: [ + { + name: 'delAzureDNSResolverOutbound' + properties: { + serviceName: 'Microsoft.Network/dnsResolvers' + } + } + ] + } + } +] + +var optionalSubnets = [for (subnet, i) in network.subnets.optional: { + name: subnet.name + properties: { + addressPrefix: subnet.addressPrefix + networkSecurityGroup: (subnet.nsg.enabled) ? { + id: nsg[i].id + } : null + routeTable: (subnet.udr.enabled) ? { + id: udr.id + } : null + delegations: contains(subnet, 'delegations') ? [ + { + name: replace(subnet.delegations.serviceName, '/', '.') + properties: { + serviceName: subnet.delegations.serviceName + } + } + ] : null + } +}] + +//Optionally add DNS Resolver subnets based on if the deployDNSResolver parameter is set to true +var allSubnets = deployDNSResolver.enabled ? union(requiredSubnets, optionalSubnets, dnsResolverSubnets) : union(requiredSubnets, optionalSubnets) + + +resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = { + name: network.name + location: location + properties: { + dhcpOptions: { + dnsServers: network.dnsServers + } + addressSpace: { + addressPrefixes: network.addressPrefixes + } + subnets: allSubnets + } +} + +module vnetPeeringSpokeToHub '../../azresources/network/vnet-peering.bicep' = if (network.peerToHubVirtualNetwork) { + name: 'deploy-vnet-peering-spoke-to-hub' + scope: resourceGroup() + params: { + peeringName: 'Hub-${vnet.name}-to-${last(hubVnetIdSplit)}' + allowForwardedTraffic: true + allowVirtualNetworkAccess: true + sourceVnetName: vnet.name + targetVnetId: hubNetwork.virtualNetworkId + useRemoteGateways: network.useRemoteGateway + } +} + +// For Hub to Spoke vnet peering, we must rescope the deployment to the subscription id & resource group of where the Hub VNET is located. +module vnetPeeringHubToSpoke '../../azresources/network/vnet-peering.bicep' = if (network.peerToHubVirtualNetwork) { + name: 'deploy-vnet-peering-${subscription().subscriptionId}' + // vnet id = /subscriptions/<>/resourceGroups/<>/providers/Microsoft.Network/virtualNetworks/<> + scope: resourceGroup(network.peerToHubVirtualNetwork ? hubVnetIdSplit[2] : '', network.peerToHubVirtualNetwork ? hubVnetIdSplit[4] : '') + params: { + peeringName: 'Spoke-${last(hubVnetIdSplit)}-to-${vnet.name}-${uniqueString(vnet.id)}' + allowForwardedTraffic: true + allowVirtualNetworkAccess: true + sourceVnetName: last(hubVnetIdSplit)! + targetVnetId: vnet.id + useRemoteGateways: false + } +} + + + + + +output vnetId string = vnet.id +output vnetName string = vnet.name diff --git a/schemas/latest/landingzones/lz-platform-identity.json b/schemas/latest/landingzones/lz-platform-identity.json new file mode 100644 index 00000000..b18e318d --- /dev/null +++ b/schemas/latest/landingzones/lz-platform-identity.json @@ -0,0 +1,455 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/PlatformIdentityArchetypeDefinition", + "definitions": { + "PlatformIdentityArchetypeDefinition": { + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "format": "uri", + "qt-uri-protocols": [ + "https" + ], + "qt-uri-extensions": [ + ".json" + ] + }, + "contentVersion": { + "type": "string" + }, + "parameters": { + "$ref": "#/definitions/Parameters" + } + }, + "required": [ + "$schema", + "contentVersion", + "parameters" + ], + "title": "PlatformIdentityArchetypeDefinition" + }, + "Parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "location": { + "$ref": "types/location.json#/definitions/Location" + }, + "logAnalyticsWorkspaceResourceId": { + "$ref": "types/logAnalyticsWorkspaceId.json#/definitions/LogAnalyticsWorkspaceId" + }, + "serviceHealthAlerts": { + "$ref": "types/serviceHealthAlerts.json#/definitions/ServiceHealthAlerts" + }, + "securityCenter": { + "$ref": "types/securityCenter.json#/definitions/SecurityCenter" + }, + "subscriptionRoleAssignments": { + "$ref": "types/subscriptionRoleAssignments.json#/definitions/SubscriptionRoleAssignments" + }, + "subscriptionBudget": { + "$ref": "types/subscriptionBudget.json#/definitions/SubscriptionBudget" + }, + "subscriptionTags": { + "$ref": "types/subscriptionTags.json#/definitions/SubscriptionTags" + }, + "resourceTags": { + "$ref": "types/resourceTags.json#/definitions/ResourceTags" + }, + "resourceGroups": { + "$ref": "#/definitions/ResourceGroups" + }, + "automation": { + "$ref": "types/automation.json#/definitions/Automation" + }, + "backupRecoveryVault": { + "$ref": "types/backupRecoveryVault.json#/definitions/RecoveryVault" + }, + "privateDnsZones": { + "$ref": "#/definitions/PrivateDNSZones" + }, + "privateDnsResolver": { + "$ref": "#/definitions/PrivateDNSResolver" + }, + "privateDnsResolverRuleset": { + "$ref": "#/definitions/PrivateDNSResolverRuleset" + }, + "hubNetwork": { + "$ref": "#/definitions/HubNetwork" + }, + "network": { + "$ref": "#/definitions/Network" + } + }, + "required": [ + "automation", + "backupRecoveryVault", + "hubNetwork", + "network", + "privateDnsResolver", + "privateDnsResolverRuleset", + "privateDnsZones", + "resourceGroups", + "resourceTags", + "securityCenter", + "serviceHealthAlerts", + "subscriptionBudget", + "subscriptionRoleAssignments", + "subscriptionTags" + ], + "title": "Parameters" + }, + "HubNetwork": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "$ref": "#/definitions/HubNetworkValue" + } + }, + "required": [ + "value" + ], + "title": "HubNetwork" + }, + "HubNetworkValue": { + "type": "object", + "additionalProperties": false, + "properties": { + "virtualNetworkId": { + "type": "string" + }, + "rfc1918IPRange": { + "type": "string" + }, + "rfc6598IPRange": { + "type": "string" + }, + "egressVirtualApplianceIp": { + "type": "string" + } + }, + "required": [ + "egressVirtualApplianceIp", + "rfc1918IPRange", + "rfc6598IPRange", + "virtualNetworkId" + ], + "title": "HubNetworkValue" + }, + "DNSResolverInbound": { + "type": "object", + "additionalProperties": false, + "properties": { + "comments": { + "type": "string" + }, + "name": { + "type": "string" + }, + "addressPrefix": { + "type": "string" + } + }, + "required": [ + "addressPrefix", + "comments", + "name" + ], + "title": "DNSResolverInbound" + }, + "PrivateDNSResolver": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "$ref": "#/definitions/PrivateDNSResolverValue" + } + }, + "required": [ + "value" + ], + "title": "PrivateDNSResolver" + }, + "PrivateDNSResolverValue": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "inboundEndpointName": { + "type": "string" + }, + "outboundEndpointName": { + "type": "string" + } + }, + "required": [ + "enabled", + "inboundEndpointName", + "name", + "outboundEndpointName" + ], + "title": "PrivateDNSResolverValue" + }, + "PrivateDNSResolverRuleset": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "$ref": "#/definitions/PrivateDNSResolverRulesetValue" + } + }, + "required": [ + "value" + ], + "title": "PrivateDNSResolverRuleset" + }, + "PrivateDNSResolverRulesetValue": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "linkRuleSetToVnet": { + "type": "boolean" + }, + "linkRuleSetToVnetName": { + "type": "string" + }, + "forwardingRules": { + "type": "array", + "items": { + "$ref": "#/definitions/ForwardingRule" + } + } + }, + "required": [ + "enabled", + "forwardingRules", + "linkRuleSetToVnet", + "linkRuleSetToVnetName", + "name" + ], + "title": "PrivateDNSResolverRulesetValue" + }, + "ForwardingRule": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "state": { + "type": "string" + }, + "targetDnsServers": { + "type": "array", + "items": { + "$ref": "#/definitions/TargetDNSServer" + } + } + }, + "required": [ + "domain", + "name", + "state", + "targetDnsServers" + ], + "title": "ForwardingRule" + }, + "TargetDNSServer": { + "type": "object", + "additionalProperties": false, + "properties": { + "ipAddress": { + "type": "string" + } + }, + "required": [ + "ipAddress" + ], + "title": "TargetDNSServer" + }, + "PrivateDNSZones": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "$ref": "#/definitions/PrivateDNSZonesValue" + } + }, + "required": [ + "value" + ], + "title": "PrivateDNSZones" + }, + "PrivateDNSZonesValue": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "resourceGroupName": { + "type": "string" + } + }, + "required": [ + "enabled", + "resourceGroupName" + ], + "title": "PrivateDNSZonesValue" + }, + "ResourceGroups": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "$ref": "#/definitions/ResourceGroupsValue" + } + }, + "required": [ + "value" + ], + "title": "ResourceGroups" + }, + "ResourceGroupsValue": { + "type": "object", + "additionalProperties": false, + "properties": { + "automation": { + "type": "string" + }, + "networking": { + "type": "string" + }, + "networkWatcher": { + "type": "string" + }, + "backupRecoveryVault": { + "type": "string" + }, + "domainControllers": { + "type": "string" + }, + "dnsResolver": { + "type": "string" + }, + "dnsCondionalForwarders": { + "type": "string" + }, + "privateDnsZones": { + "type": "string" + } + }, + "required": [ + "automation", + "backupRecoveryVault", + "dnsCondionalForwarders", + "dnsResolver", + "domainControllers", + "networkWatcher", + "networking", + "privateDnsZones" + ], + "title": "ResourceGroupsValue" + }, + + "Network": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "$ref": "#/definitions/NetworkValue" + } + }, + "required": [ + "value" + ], + "title": "Network" + }, + "NetworkValue": { + "type": "object", + "additionalProperties": false, + "properties": { + "deployVnet": { + "type": "boolean" + }, + "peerToHubVirtualNetwork": { + "type": "boolean" + }, + "useRemoteGateway": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + } + }, + "addressPrefixes": { + "type": "array", + "items": { + "type": "string" + } + }, + "subnets": { + "$ref": "#/definitions/Subnets" + } + }, + "required": [ + "addressPrefixes", + "deployVnet", + "dnsServers", + "name", + "peerToHubVirtualNetwork", + "subnets", + "useRemoteGateway" + ], + "title": "NetworkValue" + }, + "Subnets": { + "type": "object", + "additionalProperties": false, + "properties": { + "domainControllers": { + "$ref": "#/definitions/DNSResolverInbound" + }, + "dnsResolverInbound": { + "$ref": "#/definitions/DNSResolverInbound" + }, + "dnsResolverOutbound": { + "$ref": "#/definitions/DNSResolverInbound" + }, + "optional": { + "type": "array", + "items": {} + } + }, + "required": [ + "dnsResolverInbound", + "dnsResolverOutbound", + "domainControllers", + "optional" + ], + "title": "Subnets" + } + } +} \ No newline at end of file diff --git a/scripts/deployments/Functions/EnvironmentContext.ps1 b/scripts/deployments/Functions/EnvironmentContext.ps1 index 347af7d7..f3087232 100644 --- a/scripts/deployments/Functions/EnvironmentContext.ps1 +++ b/scripts/deployments/Functions/EnvironmentContext.ps1 @@ -49,6 +49,7 @@ function New-EnvironmentContext { LoggingDirectory = "$WorkingDirectory/config/logging/$Environment" NetworkingDirectory = "$WorkingDirectory/config/networking/$Environment" SubscriptionsDirectory = "$WorkingDirectory/config/subscriptions/$Environment" + IdentityDirectory = "$WorkingDirectory/config/identity/$Environment" Variables = $Variables ManagementGroupHierarchy = $ManagementGroupHierarchy diff --git a/scripts/deployments/Functions/Identity.ps1 b/scripts/deployments/Functions/Identity.ps1 new file mode 100644 index 00000000..cbb57012 --- /dev/null +++ b/scripts/deployments/Functions/Identity.ps1 @@ -0,0 +1,81 @@ +<# +---------------------------------------------------------------------------------- +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +---------------------------------------------------------------------------------- +#> + +function Set-Identity { + param ( + [Parameter(Mandatory = $true)] + $Context, + + [Parameter(Mandatory = $true)] + [String]$Region, + + [Parameter(Mandatory = $true)] + [String]$ManagementGroupId, + + [Parameter(Mandatory = $true)] + [String]$SubscriptionId, + + [Parameter(Mandatory = $true)] + [String]$ConfigurationFilePath, + + [Parameter(Mandatory = $true)] + [String]$LogAnalyticsWorkspaceResourceId + ) + + Set-AzContext -Subscription $SubscriptionId + + $SchemaFilePath = "$($Context.SchemaDirectory)/landingzones/lz-platform-identity.json" + + Write-Output "Validation JSON parameter configuration using $SchemaFilePath" + Get-Content -Raw $ConfigurationFilePath | Test-Json -SchemaFile $SchemaFilePath + + # Load networking configuration + $Configuration = Get-Content $ConfigurationFilePath | ConvertFrom-Json -Depth 100 + + #region Check if Log Analytics Workspace Id is provided. Otherwise set it. + $LogAnalyticsWorkspaceResourceIdInFile = $Configuration.parameters | Get-Member -Name logAnalyticsWorkspaceResourceId + + if ($null -eq $LogAnalyticsWorkspaceResourceIdInFile -or $Configuration.parameters.logAnalyticsWorkspaceResourceId.value -eq "") { + $LogAnalyticsWorkspaceIdElement = @{ + logAnalyticsWorkspaceResourceId = @{ + value = $LogAnalyticsWorkspaceResourceId + } + } + + $Configuration.parameters | Add-Member $LogAnalyticsWorkspaceIdElement -Force + } + #endregion + + $PopulatedParametersFilePath = $ConfigurationFilePath.Split('.')[0] + '-populated.json' + + Write-Output "Creating new file with runtime populated parameters: $PopulatedParametersFilePath" + $Configuration | ConvertTo-Json -Depth 100 | Set-Content $PopulatedParametersFilePath + + Write-Output "Moving Subscription ($SubscriptionId) to Management Group ($ManagementGroupId)" + New-AzManagementGroupDeployment ` + -ManagementGroupId $ManagementGroupId ` + -Location $Context.DeploymentRegion ` + -TemplateFile "$($Context.WorkingDirectory)/landingzones/utils/mg-move/move-subscription.bicep" ` + -TemplateParameterObject @{ + managementGroupId = $ManagementGroupId + subscriptionId = $SubscriptionId + } ` + -Verbose + + Write-Output "Deploying Identity to $SubscriptionId in $Region with $ConfigurationFilePath" + New-AzSubscriptionDeployment ` + -Name "main-$Region" ` + -Location $Region ` + -TemplateFile "$($Context.WorkingDirectory)/landingzones/lz-platform-identity/main.bicep" ` + -TemplateParameterFile $PopulatedParametersFilePath ` + -Verbose + +} \ No newline at end of file diff --git a/scripts/deployments/RunWorkflows.ps1 b/scripts/deployments/RunWorkflows.ps1 index ce9a35e3..bacaa54a 100644 --- a/scripts/deployments/RunWorkflows.ps1 +++ b/scripts/deployments/RunWorkflows.ps1 @@ -61,6 +61,9 @@ OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. .PARAMETER DeployHubNetworkWithNVA If true, run the NVA hub network workflow. + + .PARAMETER DeployIdentity + If true, run the Identity workflow. .PARAMETER DeploySubscriptionIds Comma separated list of quoted subscription ids to run the subscription workflow against. @@ -173,6 +176,8 @@ Param( [switch]$DeployHubNetworkWithNVA, [switch]$DeployHubNetworkWithAzureFirewall, + [switch]$DeployIdentity, + [string[]]$DeploySubscriptionIds=@(), # How to deploy @@ -220,6 +225,7 @@ Write-Host "Loading functions..." . ".\Functions\Policy.ps1" . ".\Functions\HubNetworkWithNVA.ps1" . ".\Functions\HubNetworkWithAzureFirewall.ps1" +. ".\Functions\Identity.ps1" . ".\Functions\Subscriptions.ps1" # Az Login interactively @@ -393,6 +399,24 @@ if ($DeployHubNetworkWithAzureFirewall) { -LogAnalyticsWorkspaceResourceId $LoggingConfiguration.LogAnalyticsWorkspaceResourceId } +# Deploy Identity Subscription +if ($DeployIdentity) { + Write-Host "Deploying Identity Subscription..." + # Get Logging information using logging config file + $LoggingConfiguration = Get-LoggingConfiguration ` + -ConfigurationFilePath "$($Context.LoggingDirectory)/$($Context.Variables['var-logging-configurationFileName'])" ` + -SubscriptionId $Context.Variables['var-logging-subscriptionId'] + + #Create Identity Subscription + Set-Identity ` + -Context $Context ` + -Region $Context.Variables['var-identity-region'] ` + -ManagementGroupId $Context.Variables['var-identity-managementGroupId'] ` + -SubscriptionId $Context.Variables['var-identity-subscriptionId'] ` + -ConfigurationFilePath "$($Context.IdentityDirectory)/$($Context.Variables['var-identity-configurationFileName'])" ` + -LogAnalyticsWorkspaceResourceId $LoggingConfiguration.LogAnalyticsWorkspaceResourceId +} + # Deploy Subscription archetypes if (($null -ne $DeploySubscriptionIds) -and ($DeploySubscriptionIds.Count -gt 0)) { Write-Host "Deploying Subscriptions..." diff --git a/tests/schemas/lz-platform-identity/lz-Identity-With-DNS-Resolver.json b/tests/schemas/lz-platform-identity/lz-Identity-With-DNS-Resolver.json new file mode 100644 index 00000000..8c759443 --- /dev/null +++ b/tests/schemas/lz-platform-identity/lz-Identity-With-DNS-Resolver.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "serviceHealthAlerts": { + "value": { + "resourceGroupName": "service-health", + "incidentTypes": [ "Incident", "Security" ], + "regions": [ "Global", "Canada East", "Canada Central" ], + "receivers": { + "app": [ "alzcanadapubsec@microsoft.com" ], + "email": [ "alzcanadapubsec@microsoft.com" ], + "sms": [ { "countryCode": "1", "phoneNumber": "6045555555" } ], + "voice": [ { "countryCode": "1", "phoneNumber": "6045555555" } ] + }, + "actionGroupName": "Service health action group", + "actionGroupShortName": "health-alert", + "alertRuleName": "Incidents and Security", + "alertRuleDescription": "Service Health: Incidents and Security" + } + }, + "securityCenter": { + "value": { + "email": "alzcanadapubsec@microsoft.com", + "phone": "6045555555" + } + }, + "subscriptionRoleAssignments": { + "value": [ + { + "comments": "Built-in Owner Role", + "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "securityGroupObjectIds": [ + "3a4fa072-cc14-471d-aeac-49afdbde9f7a" + ] + } + ] + }, + "subscriptionBudget": { + "value": { + "createBudget": false + } + }, + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + "automation": { + "value": { + "name": "automation" + } + }, + "backupRecoveryVault": { + "value": { + "enabled": true, + "name": "backup-vault" + } + }, + "privateDnsZones": { + "value": { + "enabled": false, + "resourceGroupName": "pubsec-dns" + } + }, + + "privateDnsResolver": { + "value": { + "enabled": true, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, + + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + } + } \ No newline at end of file diff --git a/tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones-And-DNS-Resolver.json b/tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones-And-DNS-Resolver.json new file mode 100644 index 00000000..bb65f4a8 --- /dev/null +++ b/tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones-And-DNS-Resolver.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "serviceHealthAlerts": { + "value": { + "resourceGroupName": "service-health", + "incidentTypes": [ "Incident", "Security" ], + "regions": [ "Global", "Canada East", "Canada Central" ], + "receivers": { + "app": [ "alzcanadapubsec@microsoft.com" ], + "email": [ "alzcanadapubsec@microsoft.com" ], + "sms": [ { "countryCode": "1", "phoneNumber": "6045555555" } ], + "voice": [ { "countryCode": "1", "phoneNumber": "6045555555" } ] + }, + "actionGroupName": "Service health action group", + "actionGroupShortName": "health-alert", + "alertRuleName": "Incidents and Security", + "alertRuleDescription": "Service Health: Incidents and Security" + } + }, + "securityCenter": { + "value": { + "email": "alzcanadapubsec@microsoft.com", + "phone": "6045555555" + } + }, + "subscriptionRoleAssignments": { + "value": [ + { + "comments": "Built-in Owner Role", + "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "securityGroupObjectIds": [ + "3a4fa072-cc14-471d-aeac-49afdbde9f7a" + ] + } + ] + }, + "subscriptionBudget": { + "value": { + "createBudget": false + } + }, + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + "automation": { + "value": { + "name": "automation" + } + }, + "backupRecoveryVault": { + "value": { + "enabled": true, + "name": "backup-vault" + } + }, + "privateDnsZones": { + "value": { + "enabled": true, + "resourceGroupName": "pubsec-dns" + } + }, + + "privateDnsResolver": { + "value": { + "enabled": true, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, + + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + } + } \ No newline at end of file diff --git a/tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones.json b/tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones.json new file mode 100644 index 00000000..806fd142 --- /dev/null +++ b/tests/schemas/lz-platform-identity/lz-Identity-With-Private-DNS-Zones.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "serviceHealthAlerts": { + "value": { + "resourceGroupName": "service-health", + "incidentTypes": [ "Incident", "Security" ], + "regions": [ "Global", "Canada East", "Canada Central" ], + "receivers": { + "app": [ "alzcanadapubsec@microsoft.com" ], + "email": [ "alzcanadapubsec@microsoft.com" ], + "sms": [ { "countryCode": "1", "phoneNumber": "6045555555" } ], + "voice": [ { "countryCode": "1", "phoneNumber": "6045555555" } ] + }, + "actionGroupName": "Service health action group", + "actionGroupShortName": "health-alert", + "alertRuleName": "Incidents and Security", + "alertRuleDescription": "Service Health: Incidents and Security" + } + }, + "securityCenter": { + "value": { + "email": "alzcanadapubsec@microsoft.com", + "phone": "6045555555" + } + }, + "subscriptionRoleAssignments": { + "value": [ + { + "comments": "Built-in Owner Role", + "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "securityGroupObjectIds": [ + "3a4fa072-cc14-471d-aeac-49afdbde9f7a" + ] + } + ] + }, + "subscriptionBudget": { + "value": { + "createBudget": false + } + }, + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + "automation": { + "value": { + "name": "automation" + } + }, + "backupRecoveryVault": { + "value": { + "enabled": true, + "name": "backup-vault" + } + }, + "privateDnsZones": { + "value": { + "enabled": true, + "resourceGroupName": "pubsec-dns" + } + }, + + "privateDnsResolver": { + "value": { + "enabled": false, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, + + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + } + } \ No newline at end of file diff --git a/tests/schemas/lz-platform-identity/lz-Identity-Without-DNS-Resolver.json b/tests/schemas/lz-platform-identity/lz-Identity-Without-DNS-Resolver.json new file mode 100644 index 00000000..59508d0f --- /dev/null +++ b/tests/schemas/lz-platform-identity/lz-Identity-Without-DNS-Resolver.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "serviceHealthAlerts": { + "value": { + "resourceGroupName": "service-health", + "incidentTypes": [ "Incident", "Security" ], + "regions": [ "Global", "Canada East", "Canada Central" ], + "receivers": { + "app": [ "alzcanadapubsec@microsoft.com" ], + "email": [ "alzcanadapubsec@microsoft.com" ], + "sms": [ { "countryCode": "1", "phoneNumber": "6045555555" } ], + "voice": [ { "countryCode": "1", "phoneNumber": "6045555555" } ] + }, + "actionGroupName": "Service health action group", + "actionGroupShortName": "health-alert", + "alertRuleName": "Incidents and Security", + "alertRuleDescription": "Service Health: Incidents and Security" + } + }, + "securityCenter": { + "value": { + "email": "alzcanadapubsec@microsoft.com", + "phone": "6045555555" + } + }, + "subscriptionRoleAssignments": { + "value": [ + { + "comments": "Built-in Owner Role", + "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "securityGroupObjectIds": [ + "3a4fa072-cc14-471d-aeac-49afdbde9f7a" + ] + } + ] + }, + "subscriptionBudget": { + "value": { + "createBudget": false + } + }, + "subscriptionTags": { + "value": { + "ISSO": "isso-tbd", + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceTags": { + "value": { + "ClientOrganization": "client-organization-tag", + "CostCenter": "cost-center-tag", + "DataSensitivity": "data-sensitivity-tag", + "ProjectContact": "project-contact-tag", + "ProjectName": "project-name-tag", + "TechnicalContact": "technical-contact-tag" + } + }, + "resourceGroups": { + "value": { + "automation": "automation", + "networking": "networking", + "networkWatcher": "NetworkWatcherRG", + "backupRecoveryVault": "backup", + "domainControllers": "DomainControllersRG", + "dnsResolver": "dns-resolverRG", + "dnsCondionalForwarders": "dns-CondionalForwardersRG", + "privateDnsZones": "pubsec-dns" + } + }, + "automation": { + "value": { + "name": "automation" + } + }, + "backupRecoveryVault": { + "value": { + "enabled": true, + "name": "backup-vault" + } + }, + "privateDnsZones": { + "value": { + "enabled": false, + "resourceGroupName": "pubsec-dns" + } + }, + + "privateDnsResolver": { + "value": { + "enabled": false, + "name": "dns-resolver", + "inboundEndpointName": "dns-resolver-Inbound", + "outboundEndpointName": "dns-resolver-Outbound" + } + }, + + "privateDnsResolverRuleset": { + "value": { + "enabled": true, + "name": "dns-resolver-ruleset", + "linkRuleSetToVnet": true, + "linkRuleSetToVnetName": "dns-resolver-vnet-link", + "forwardingRules": [ + { + "name": "default", + "domain": "dontMakeMeThink.local", + "state": "Enabled", + "targetDnsServers": [ + { + "ipAddress": "10.99.99.100" + }, + { + "ipAddress": "10.99.99.99" + } + ] + } + ] + } + }, + + "hubNetwork": { + "value": { + "virtualNetworkId": "/subscriptions/ed7f4eed-9010-4227-b115-2a5e37728f27/resourceGroups/pubsec-hub-networking/providers/Microsoft.Network/virtualNetworks/hub-vnet", + "rfc1918IPRange": "10.18.0.0/22", + "rfc6598IPRange": "100.60.0.0/16", + "egressVirtualApplianceIp": "10.18.1.4" + } + }, + + "network": { + "value": { + "deployVnet": true, + "peerToHubVirtualNetwork": true, + "useRemoteGateway": false, + "name": "id-vnet", + "dnsServers": [ + "10.18.1.4" + ], + "addressPrefixes": [ + "10.15.0.0/24" + ], + "subnets": { + "domainControllers": { + "comments": "Identity Subnet for Domain Controllers and VM-Based DNS Servers", + "name": "DomainControllers", + "addressPrefix": "10.15.0.0/27" + }, + "dnsResolverInbound": { + "comments": "Azure DNS Resolver Inbound Requests subnet", + "name": "AzureDNSResolver-Inbound", + "addressPrefix": "10.15.0.32/27" + }, + "dnsResolverOutbound": { + "comments": "Azure DNS Resolver Outbound Requests subnet", + "name": "AzureDNSResolver-Outbound", + "addressPrefix": "10.15.0.64/27" + }, + "optional": [] + } + } + } + } + } \ No newline at end of file diff --git a/tests/schemas/run-tests.sh b/tests/schemas/run-tests.sh index 2dfab965..f18ae56b 100644 --- a/tests/schemas/run-tests.sh +++ b/tests/schemas/run-tests.sh @@ -12,6 +12,8 @@ pwsh -File ./validate-deployment-config.ps1 -SchemaFile '../../schemas/latest/l pwsh -File ./validate-deployment-config.ps1 -SchemaFile '../../schemas/latest/landingzones/lz-platform-connectivity-hub-nva.json' -TestFolder '../../config/networking/*/hub-nva/' -FileFilter '*.json' +pwsh -File ./validate-deployment-config.ps1 -SchemaFile '../../schemas/latest/landingzones/lz-platform-identity.json' -TestFolder '../../config/identity/' -FileFilter '*.json' + pwsh -File ./validate-deployment-config.ps1 -SchemaFile '../../schemas/latest/landingzones/lz-generic-subscription.json' -TestFolder '../../config/subscriptions/' -FileFilter '*generic-subscription*.json' pwsh -File ./validate-deployment-config.ps1 -SchemaFile '../../schemas/latest/landingzones/lz-machinelearning.json' -TestFolder '../../config/subscriptions/' -FileFilter '*machinelearning*.json'