├── assets ├── architecture-with-vnet.png └── architecture-without-vnet.png ├── .github ├── linters │ └── .hadolint.yaml ├── CODE_OF_CONDUCT.md ├── workflows │ ├── main.yml │ └── super-linter.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── CHANGELOG.md ├── scripts └── acrbuild.sh ├── modules ├── keyvaultsecret.bicep ├── loganalytics.bicep ├── filestorageAcl.bicep ├── privateendpoint.bicep ├── filestorage.bicep ├── vnet.bicep ├── redis.bicep ├── acr.bicep ├── keyvault.bicep ├── mysql.bicep └── webapp.bicep ├── Dockerfile ├── LICENSE.md ├── CONTRIBUTING.md ├── README.md ├── .gitignore ├── ctfd.bicep └── azuredeploy.json /assets/architecture-with-vnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ctfd-azure-paas/HEAD/assets/architecture-with-vnet.png -------------------------------------------------------------------------------- /assets/architecture-without-vnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ctfd-azure-paas/HEAD/assets/architecture-without-vnet.png -------------------------------------------------------------------------------- /.github/linters/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################## 3 | ## Hadolint config file ## 4 | ########################## 5 | 6 | failure-threshold: error 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /scripts/acrbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Waiting on RBAC replication" 5 | sleep $initialDelay 6 | 7 | echo "$CONTENT" > Dockerfile 8 | 9 | az acr build \ 10 | --registry $acrName \ 11 | --image $taggedImageName \ 12 | --platform $platform \ 13 | . 14 | -------------------------------------------------------------------------------- /modules/keyvaultsecret.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of Azure Key Vault') 2 | param keyVaultName string 3 | 4 | @description('Name of the secret') 5 | param secretName string 6 | 7 | @description('Value of the secret') 8 | @secure() 9 | param secretValue string 10 | 11 | resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { 12 | name: '${keyVaultName}/${secretName}' 13 | properties: { 14 | value: secretValue 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build ARM JSON from Bicep 2 | 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build-bicep: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Bicep Build 18 | uses: Azure/bicep-build-action@v1.0.0 19 | with: 20 | bicepFilePath: ctfd.bicep 21 | outputFilePath: azuredeploy.json 22 | - uses: stefanzweifel/git-auto-commit-action@v5 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile builds a CTFd (https://github.com/CTFd/CTFd) image that 2 | # enables TLS connectivity to Azure Database for MySQL. 3 | # More info: https://learn.microsoft.com/en-gb/azure/postgresql/flexible-server/concepts-networking-ssl-tls#downloading-root-ca-certificates-and-updating-application-clients-in-certificate-pinning-scenarios 4 | FROM ctfd/ctfd:3.7.0 5 | 6 | USER root 7 | RUN apt-get update && apt-get install -y wget --no-install-recommends \ 8 | && apt-get clean \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | RUN wget --user-agent="Mozilla" --progress=dot:giga https://cacerts.digicert.com/DigiCertGlobalRootCA.crt -P /opt/certificates/ 12 | RUN openssl x509 -in /opt/certificates/DigiCertGlobalRootCA.crt -out /opt/certificates/DigiCertGlobalRootCA.crt.pem -outform PEM 13 | 14 | USER 1001 15 | EXPOSE 8000 16 | 17 | ENTRYPOINT ["/opt/CTFd/docker-entrypoint.sh"] 18 | -------------------------------------------------------------------------------- /modules/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | @description('Location for all resources.') 2 | param location string 3 | 4 | @description('Name of WebApp to monitor') 5 | var appName = 'CTFd' 6 | 7 | @description('Name for Log Analytics Workspace') 8 | var logAnalyticsName = 'ctfd-log-analytics-${uniqueString(resourceGroup().id)}' 9 | 10 | @description('Log Retention in days') 11 | var retentionInDays = 30 12 | 13 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { 14 | name: logAnalyticsName 15 | location: location 16 | tags: { 17 | displayName: 'Log Analytics' 18 | ProjectName: appName 19 | } 20 | properties: { 21 | sku: { 22 | name: 'PerGB2018' 23 | } 24 | retentionInDays: retentionInDays 25 | features: { 26 | searchVersion: 1 27 | legacy: 0 28 | enableLogAccessUsingOnlyResourcePermissions: true 29 | } 30 | } 31 | } 32 | 33 | output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.id 34 | -------------------------------------------------------------------------------- /.github/workflows/super-linter.yml: -------------------------------------------------------------------------------- 1 | # This workflow executes several linters on changed files based on languages used in your code base whenever 2 | # you push a code or open a pull request. 3 | # 4 | # You can adjust the behavior by modifying this file. 5 | # For more information, see: 6 | # https://github.com/github/super-linter 7 | name: Lint Code Base 8 | 9 | on: 10 | push: 11 | branches: [ "main" ] 12 | pull_request: 13 | branches: [ "main" ] 14 | jobs: 15 | run-lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | with: 21 | # Full git history is needed to get a proper list of changed files within `super-linter` 22 | fetch-depth: 0 23 | 24 | - name: Lint Code Base 25 | uses: github/super-linter@v4 26 | env: 27 | VALIDATE_ALL_CODEBASE: false 28 | DEFAULT_BRANCH: "main" 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | FILTER_REGEX_EXCLUDE: .*assets/.* 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /modules/filestorageAcl.bicep: -------------------------------------------------------------------------------- 1 | @description('Deploy in VNet') 2 | param vnet bool 3 | 4 | @description('SKU Name for the Azure Storage Account') 5 | param storageSkuName string 6 | 7 | @description('Location for all resources.') 8 | param location string 9 | 10 | @description('Outbound IP adresses of CTF Web App. Required for the non-vnet scenario') 11 | param webAppOutboundIpAdresses string 12 | 13 | @description('Account Name for the Azure Storage Account') 14 | param storageAccountName string 15 | 16 | // map the comma-separated string into a json 17 | var networkAcls = vnet ? { defaultAction: 'Deny', bypass: 'AzureServices' } : { defaultAction: 'Allow', ipRules: map(split(webAppOutboundIpAdresses, ','), ip => { value: ip }) } 18 | 19 | resource existingStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { 20 | name: storageAccountName 21 | } 22 | 23 | resource updateNetworkAcls 'Microsoft.Storage/storageAccounts@2023-05-01' = { 24 | name: existingStorageAccount.name 25 | location: location 26 | sku: { 27 | name: storageSkuName 28 | } 29 | kind: 'StorageV2' 30 | properties: { 31 | networkAcls: networkAcls 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /modules/privateendpoint.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the VNet') 2 | param virtualNetworkName string 3 | 4 | @description('Name of the subnet') 5 | param subnetName string 6 | 7 | @description('Id of the resource') 8 | param resuorceId string 9 | 10 | @description('Group Id of the resource') 11 | param resuorceGroupId string 12 | 13 | @description('Name of dns zone') 14 | param privateDnsZoneName string 15 | 16 | @description('Name of private endpoint') 17 | param privateEndpointName string 18 | 19 | @description('Location for all resources.') 20 | param location string 21 | 22 | resource privateEndpoint 'Microsoft.Network/privateEndpoints@2024-03-01' = { 23 | name: privateEndpointName 24 | location: location 25 | properties: { 26 | subnet: { 27 | id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName) 28 | } 29 | privateLinkServiceConnections: [ 30 | { 31 | name: privateEndpointName 32 | properties: { 33 | privateLinkServiceId: resuorceId 34 | groupIds: [ 35 | resuorceGroupId 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | 43 | resource privateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = { 44 | name: privateDnsZoneName 45 | location: 'global' 46 | properties: {} 47 | } 48 | 49 | resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { 50 | parent: privateDnsZone 51 | name: '${privateDnsZoneName}-link' 52 | location: 'global' 53 | properties: { 54 | registrationEnabled: false 55 | virtualNetwork: { 56 | id: resourceId('Microsoft.Network/virtualNetworks', virtualNetworkName) 57 | } 58 | } 59 | } 60 | 61 | resource pvtEndpointDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-03-01' = { 62 | name: 'default' 63 | parent: privateEndpoint 64 | properties: { 65 | privateDnsZoneConfigs: [ 66 | { 67 | name: 'config1' 68 | properties: { 69 | privateDnsZoneId: privateDnsZone.id 70 | } 71 | } 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /modules/filestorage.bicep: -------------------------------------------------------------------------------- 1 | @description('Deploy in VNet') 2 | param vnet bool 3 | 4 | @description('SKU Name for the Azure Storage Account') 5 | param storageSkuName string 6 | 7 | @description('Name of the VNet') 8 | param virtualNetworkName string 9 | 10 | @description('Name of the internal resources subnet') 11 | param internalResourcesSubnetName string 12 | 13 | @description('Location for all resources.') 14 | param location string 15 | 16 | @description('Log Anaytics Workspace Id') 17 | param logAnalyticsWorkspaceId string 18 | 19 | @description('Account Name for the Azure Storage Account') 20 | param storageAccountName string 21 | 22 | module privateEndpointModule 'privateendpoint.bicep' = if (vnet) { 23 | name: 'storagePrivateEndpointDeploy' 24 | params: { 25 | virtualNetworkName: virtualNetworkName 26 | subnetName: internalResourcesSubnetName 27 | resuorceId: storageAccount.id 28 | resuorceGroupId: 'file' 29 | privateDnsZoneName: 'privatelink.file.${environment().suffixes.storage}' 30 | privateEndpointName: 'storage_private_endpoint' 31 | location: location 32 | } 33 | } 34 | 35 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { 36 | name: storageAccountName 37 | location: location 38 | sku: { 39 | name: storageSkuName 40 | } 41 | kind: 'StorageV2' 42 | properties: { 43 | publicNetworkAccess: (vnet ? 'Disabled' : 'Enabled') 44 | accessTier: 'Hot' 45 | } 46 | } 47 | 48 | resource fileServices 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = { 49 | parent: storageAccount 50 | name: 'default' 51 | properties: {} 52 | } 53 | 54 | resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = { 55 | parent: fileServices 56 | name: 'uploads' 57 | properties: {} 58 | } 59 | 60 | resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 61 | name: '${storageAccountName}-diagnostics' 62 | scope: fileServices 63 | properties: { 64 | workspaceId: logAnalyticsWorkspaceId 65 | logs: [ 66 | { 67 | category: 'StorageRead' 68 | enabled: true 69 | } 70 | { 71 | category: 'StorageWrite' 72 | enabled: true 73 | } 74 | { 75 | category: 'StorageDelete' 76 | enabled: true 77 | } 78 | ] 79 | metrics: [ 80 | { 81 | category: 'Transaction' 82 | enabled: true 83 | } 84 | ] 85 | } 86 | } 87 | 88 | output storageAccountName string = storageAccountName 89 | -------------------------------------------------------------------------------- /modules/vnet.bicep: -------------------------------------------------------------------------------- 1 | @description('Location for all resources.') 2 | param location string 3 | 4 | @description('Name of the VNet') 5 | param virtualNetworkName string 6 | 7 | @description('Name of the internal resources subnet') 8 | param internalResourcesSubnetName string 9 | 10 | @description('Name of the public resources subnet') 11 | param publicResourcesSubnetName string 12 | 13 | @description('Name of the database resources subnet') 14 | param databaseResourcesSubnetName string 15 | 16 | @description('CIDR of the virtual network') 17 | var virtualNetworkCIDR = '10.200.0.0/16' 18 | 19 | @description('CIDR of the public resources subnet') 20 | var publicResourcesSubnetCIDR = '10.200.1.0/26' 21 | 22 | @description('CIDR of the internal resources subnet') 23 | var internalResourcesSubnetCIDR = '10.200.2.0/28' 24 | 25 | @description('CIDR of the databse resources subnet') 26 | var databaseResourcesSubnetCIDR = '10.200.3.0/28' 27 | 28 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-03-01' = { 29 | name: virtualNetworkName 30 | location: location 31 | properties: { 32 | addressSpace: { 33 | addressPrefixes: [ 34 | virtualNetworkCIDR 35 | ] 36 | } 37 | subnets: [ 38 | { 39 | name: internalResourcesSubnetName 40 | properties: { 41 | addressPrefix: internalResourcesSubnetCIDR 42 | privateEndpointNetworkPolicies: 'Disabled' 43 | } 44 | } 45 | { 46 | name: publicResourcesSubnetName 47 | properties: { 48 | addressPrefix: publicResourcesSubnetCIDR 49 | delegations: [ 50 | { 51 | name: 'dlg-Microsoft.Web-serverfarms' 52 | properties: { 53 | serviceName: 'Microsoft.Web/serverfarms' 54 | } 55 | } 56 | ] 57 | privateEndpointNetworkPolicies: 'Enabled' 58 | } 59 | } 60 | { 61 | name: databaseResourcesSubnetName 62 | properties: { 63 | addressPrefix: databaseResourcesSubnetCIDR 64 | delegations: [ 65 | { 66 | name: 'dlg-Microsoft.DBforMySQL-flexibleServers' 67 | properties: { 68 | serviceName: 'Microsoft.DBforMySQL/flexibleServers' 69 | } 70 | } 71 | ] 72 | privateEndpointNetworkPolicies: 'Enabled' 73 | privateLinkServiceNetworkPolicies: 'Enabled' 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | 80 | output virtualNetworkId string = virtualNetwork.id 81 | output databaseResourcesSubnetId string = virtualNetwork.properties.subnets[2].id 82 | -------------------------------------------------------------------------------- /modules/redis.bicep: -------------------------------------------------------------------------------- 1 | @description('Deploy in VNet') 2 | param vnet bool 3 | 4 | @description('SKU Name for Azure cache for Redis') 5 | param redisSkuName string 6 | 7 | @description('The size of the Redis cache') 8 | param redisSkuSize int 9 | 10 | @description('Name of the VNet') 11 | param virtualNetworkName string 12 | 13 | @description('Name of the internal resources subnet') 14 | param internalResourcesSubnetName string 15 | 16 | @description('Name of the key vault') 17 | param keyVaultName string 18 | 19 | @description('Name of the connection string secret') 20 | param ctfCacheSecretName string 21 | 22 | @description('Location for all resources.') 23 | param location string 24 | 25 | @description('Log Anaytics Workspace Id') 26 | param logAnalyticsWorkspaceId string 27 | 28 | @description('Server Name for Azure cache for Redis') 29 | var redisServerName = 'ctfd-redis-${uniqueString(resourceGroup().id)}' 30 | 31 | var family = redisSkuName == 'Basic' || redisSkuName == 'Standard' ? 'C' : 'P' 32 | 33 | resource redisCache 'Microsoft.Cache/redis@2023-08-01' = { 34 | name: redisServerName 35 | location: location 36 | properties: { 37 | publicNetworkAccess: (vnet ? 'Disabled' : 'Enabled') 38 | sku: { 39 | capacity: redisSkuSize 40 | family: family 41 | name: redisSkuName 42 | } 43 | } 44 | } 45 | 46 | module privateEndpointModule 'privateendpoint.bicep' = if (vnet) { 47 | name: 'redisPrivateEndpointDeploy' 48 | params: { 49 | virtualNetworkName: virtualNetworkName 50 | subnetName: internalResourcesSubnetName 51 | resuorceId: redisCache.id 52 | resuorceGroupId: 'redisCache' 53 | privateDnsZoneName: 'privatelink.redis.cache.windows.net' 54 | privateEndpointName: 'redis_private_endpoint' 55 | location: location 56 | } 57 | } 58 | 59 | module cacheSecret 'keyvaultsecret.bicep' = { 60 | name: 'redisKeyDeploy' 61 | params: { 62 | keyVaultName: keyVaultName 63 | secretName: ctfCacheSecretName 64 | secretValue: 'rediss://:${redisCache.listKeys().primaryKey}@${redisCache.name}.redis.cache.windows.net:6380' 65 | } 66 | } 67 | 68 | resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 69 | name: '${redisServerName}-diagnostics' 70 | scope: redisCache 71 | properties: { 72 | logs: [ 73 | { 74 | category: null 75 | categoryGroup: 'audit' 76 | enabled: true 77 | retentionPolicy: { 78 | days: 5 79 | enabled: false 80 | } 81 | } 82 | { 83 | category: null 84 | categoryGroup: 'allLogs' 85 | enabled: true 86 | retentionPolicy: { 87 | days:5 88 | enabled: false 89 | } 90 | } 91 | ] 92 | workspaceId: logAnalyticsWorkspaceId 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /modules/acr.bicep: -------------------------------------------------------------------------------- 1 | @description('Location for all resources.') 2 | param location string 3 | 4 | @description('Tier of Azure Container Registry.') 5 | param containerRegistrySku string 6 | 7 | @description('Managed Identity Principal Id.') 8 | param managedIdentityPrincipalId string 9 | 10 | @description('Managed Identity Id.') 11 | param managedIdentityId string 12 | 13 | @description('Log Anaytics Workspace Id') 14 | param logAnalyticsWorkspaceId string 15 | 16 | @description('Name for Azure Container Registry') 17 | var containerRegistryName = 'ctfdacr${uniqueString(resourceGroup().id)}' 18 | 19 | @description('Name and tag of the custom docker image') 20 | var ctfdImageName = 'ctfd-azure-cert:latest' 21 | 22 | resource acrResource 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { 23 | name: containerRegistryName 24 | location: location 25 | sku: { 26 | name: containerRegistrySku 27 | } 28 | properties: { 29 | adminUserEnabled: false 30 | } 31 | } 32 | 33 | resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 34 | name: '${containerRegistryName}-diagnostics' 35 | scope: acrResource 36 | properties: { 37 | logs: [ 38 | { 39 | category: null 40 | categoryGroup: 'audit' 41 | enabled: true 42 | retentionPolicy: { 43 | days: 5 44 | enabled: false 45 | } 46 | } 47 | { 48 | category: null 49 | categoryGroup: 'allLogs' 50 | enabled: true 51 | retentionPolicy: { 52 | days:5 53 | enabled: false 54 | } 55 | } 56 | ] 57 | workspaceId: logAnalyticsWorkspaceId 58 | } 59 | } 60 | 61 | resource contributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { 62 | scope: acrResource 63 | name: 'b24988ac-6180-42a0-ab88-20f7382dd24c' 64 | } 65 | 66 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 67 | name: guid(resourceGroup().id, 'b24988ac-6180-42a0-ab88-20f7382dd24c') 68 | scope: acrResource 69 | properties: { 70 | principalId: managedIdentityPrincipalId 71 | roleDefinitionId: contributorRoleDefinition.id 72 | principalType: 'ServicePrincipal' 73 | } 74 | } 75 | 76 | resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = { 77 | name: 'buildAndPush' 78 | location: location 79 | kind: 'AzureCLI' 80 | identity: { 81 | type: 'UserAssigned' 82 | userAssignedIdentities: { 83 | '${managedIdentityId}': {} 84 | } 85 | } 86 | properties: { 87 | timeout: 'PT30M' 88 | azCliVersion: '2.40.0' 89 | environmentVariables: [ 90 | { 91 | name: 'acrName' 92 | value: containerRegistryName 93 | } 94 | { 95 | name: 'acrResourceGroup' 96 | secureValue: resourceGroup().name 97 | } 98 | { 99 | name: 'taggedImageName' 100 | value: ctfdImageName 101 | } 102 | { 103 | name: 'CONTENT' 104 | value: loadTextContent('../Dockerfile') 105 | } 106 | { 107 | name: 'platform' 108 | value: 'Linux' 109 | } 110 | { 111 | name: 'initialDelay' 112 | secureValue: '30s' 113 | } 114 | ] 115 | scriptContent: loadTextContent('../scripts/acrbuild.sh') 116 | retentionInterval: 'P1D' 117 | } 118 | } 119 | 120 | output acrImage string = '${acrResource.properties.loginServer}/${ctfdImageName}' 121 | 122 | output registryName string = containerRegistryName 123 | -------------------------------------------------------------------------------- /modules/keyvault.bicep: -------------------------------------------------------------------------------- 1 | @description('Deploy in VNet') 2 | param vnet bool 3 | 4 | @description('Location for all resources.') 5 | param location string 6 | 7 | @description('Specifies the object ID of a user, service principal or security group in the Azure Active Directory tenant for the vault. The object ID must be unique for the list of access policies. Get it by using Get-AzADUser or Get-AzADServicePrincipal cmdlets.') 8 | param readerPrincipalId string 9 | 10 | @description('Specifies whether the key vault is a standard vault or a premium vault.') 11 | @allowed([ 12 | 'standard' 13 | 'premium' 14 | ]) 15 | param skuName string = 'standard' 16 | 17 | @description('Name of the VNet') 18 | param virtualNetworkName string 19 | 20 | @description('Name of the internal resources subnet') 21 | param internalResourcesSubnetName string 22 | 23 | @description('Name of Azure Key Vault') 24 | param keyVaultName string 25 | 26 | @description('Log Anaytics Workspace Id') 27 | param logAnalyticsWorkspaceId string 28 | 29 | @description('Outbound IP adresses of CTF Web App. Required for the non-vnet scenario') 30 | param webAppOutboundIpAdresses string 31 | 32 | @description('Specifies the Azure Active Directory tenant ID that should be used for authenticating requests to the key vault. Get it by using Get-AzSubscription cmdlet.') 33 | var tenantId = subscription().tenantId 34 | 35 | // map the comma-separated string into a json 36 | var networkAcls = vnet ? { defaultAction: 'Deny', bypass: 'AzureServices' } : { defaultAction: 'Allow', ipRules: map(split(webAppOutboundIpAdresses, ','), ip => { value: ip }) } 37 | 38 | resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { 39 | name: keyVaultName 40 | location: location 41 | properties: { 42 | tenantId: tenantId 43 | publicNetworkAccess: (vnet ? 'Disabled' : 'Enabled') 44 | enableRbacAuthorization: true 45 | sku: { 46 | name: skuName 47 | family: 'A' 48 | } 49 | networkAcls: networkAcls 50 | } 51 | } 52 | 53 | resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 54 | name: guid('4633458b-17de-408a-b874-0445c86b69e6', readerPrincipalId, keyVault.id) 55 | scope: keyVault 56 | properties: { 57 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') 58 | principalId: readerPrincipalId 59 | principalType: 'ServicePrincipal' 60 | } 61 | } 62 | 63 | module privateEndpointModule 'privateendpoint.bicep' = if (vnet) { 64 | name: 'keyVaultPrivateEndpointDeploy' 65 | params: { 66 | virtualNetworkName: virtualNetworkName 67 | subnetName: internalResourcesSubnetName 68 | resuorceId: keyVault.id 69 | resuorceGroupId: 'vault' 70 | privateDnsZoneName: 'privatelink.vaultcore.azure.net' 71 | privateEndpointName: 'keyvault_private_endpoint' 72 | location: location 73 | } 74 | } 75 | 76 | resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 77 | name: '${keyVaultName}-diagnostics' 78 | scope: keyVault 79 | properties: { 80 | logs: [ 81 | { 82 | category: null 83 | categoryGroup: 'audit' 84 | enabled: true 85 | retentionPolicy: { 86 | days: 5 87 | enabled: false 88 | } 89 | } 90 | { 91 | category: null 92 | categoryGroup: 'allLogs' 93 | enabled: true 94 | retentionPolicy: { 95 | days: 5 96 | enabled: false 97 | } 98 | } 99 | ] 100 | workspaceId: logAnalyticsWorkspaceId 101 | } 102 | } 103 | 104 | output keyVaultName string = keyVaultName 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CTFd on Azure PaaS 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit the [CLA page](https://cla.opensource.microsoft.com). 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's [issues link](https://github.com/Azure-Samples/ctfd-azure-paas/issues/new]). 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * The file [azuredeploy.json](./azuredeploy.json) is auto-generated by the GitHub Action [main.yml](./.github/workflows/main.yml) to support single-click Deploy-to-Azure functionality. Do not modify this file manually. 60 | * Search the [repository](https://github.com/Azure-Samples/ctfd-azure-paas/pulls) for an open or closed PR 61 | that relates to your submission. You don't want to duplicate effort. 62 | 63 | * Make your changes in a new git fork: 64 | 65 | * Commit your changes using a descriptive commit message 66 | * Push your fork to GitHub: 67 | * In GitHub, create a pull request 68 | * If we suggest changes then: 69 | * Make the required updates. 70 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 71 | 72 | ```shell 73 | git rebase master -i 74 | git push -f 75 | ``` 76 | 77 | That's it! Thank you for your contribution! 78 | -------------------------------------------------------------------------------- /modules/mysql.bicep: -------------------------------------------------------------------------------- 1 | @description('Deploy in VNet') 2 | param vnet bool 3 | 4 | @description('Database administrator login name') 5 | @minLength(1) 6 | param administratorLogin string 7 | 8 | @description('Database administrator password') 9 | @minLength(8) 10 | @secure() 11 | param administratorLoginPassword string 12 | 13 | @description('Name of the VNet') 14 | param virtualNetworkName string 15 | 16 | @description('ID of the vnet') 17 | param vnetId string 18 | 19 | @description('ID of the subnet') 20 | param databaseSubnetId string 21 | 22 | @description('Name of the key vault') 23 | param keyVaultName string 24 | 25 | @description('Name of the connection string secret') 26 | param ctfDbSecretName string 27 | 28 | @description('Location for all resources.') 29 | param location string 30 | 31 | @description('Log Anaytics Workspace Id') 32 | param logAnalyticsWorkspaceId string 33 | 34 | @description('MySql Workload Type') 35 | @allowed([ 36 | 'Development' 37 | 'SmallMedium' 38 | 'BusinessCritical' 39 | ]) 40 | param mysqlWorkloadType string 41 | 42 | @description('Server Name for Azure database for MySql') 43 | var mysqlServerName = 'ctfd-mysql-${uniqueString(resourceGroup().id)}' 44 | 45 | var tier = mysqlWorkloadType == 'Development' 46 | ? 'Burstable' 47 | : mysqlWorkloadType == 'SmallMedium' ? 'GeneralPurpose' : 'MemoryOptimized' 48 | var skuName = mysqlWorkloadType == 'Development' 49 | ? 'Standard_B1ms' 50 | : mysqlWorkloadType == 'SmallMedium' ? 'Standard_E2ads_v5' : 'Standard_E2ads_v5' 51 | var storageSizeGB = mysqlWorkloadType == 'Development' ? 20 : 128 52 | var iops = mysqlWorkloadType == 'Development' ? 360 : 2000 53 | 54 | resource mysqlDbServer 'Microsoft.DBforMySQL/flexibleServers@2023-10-01-preview' = { 55 | name: mysqlServerName 56 | dependsOn: [vnetLink] 57 | location: location 58 | sku: { 59 | name: skuName 60 | tier: tier 61 | } 62 | properties: { 63 | administratorLogin: administratorLogin 64 | administratorLoginPassword: administratorLoginPassword 65 | storage: { 66 | autoGrow: 'Enabled' 67 | iops: iops 68 | storageSizeGB: storageSizeGB 69 | } 70 | network: vnet 71 | ? { 72 | delegatedSubnetResourceId: databaseSubnetId 73 | privateDnsZoneResourceId: dnszone.id 74 | publicNetworkAccess: 'Disabled' 75 | } 76 | : { 77 | publicNetworkAccess: 'Enabled' 78 | } 79 | 80 | createMode: 'Default' 81 | version: '8.0.21' 82 | backup: { 83 | backupRetentionDays: 7 84 | geoRedundantBackup: 'Disabled' 85 | } 86 | highAvailability: { 87 | mode: 'Disabled' 88 | } 89 | } 90 | } 91 | 92 | resource collationConfiguration 'Microsoft.DBforMySQL/flexibleServers/configurations@2023-06-30' = { 93 | name: 'collation_server' 94 | parent: mysqlDbServer 95 | properties: { 96 | source: 'user-override' 97 | value: 'UTF8MB4_UNICODE_CI' 98 | } 99 | } 100 | 101 | resource ctfdDatabase 'Microsoft.DBforMySQL/flexibleServers/databases@2023-06-30' = { 102 | name: 'ctfd' 103 | parent: mysqlDbServer 104 | properties: { 105 | charset: 'utf8mb4' 106 | collation: 'utf8mb4_unicode_ci' 107 | } 108 | } 109 | 110 | resource ctdFirewallRule 'Microsoft.DBforMySQL/flexibleServers/firewallRules@2023-06-30' = if (!vnet) { 111 | name: 'AllowAllAzureServicesAndResourcesWithinAzureIps_2024-5-24_16-27-0' 112 | parent: mysqlDbServer 113 | properties: { 114 | startIpAddress: '0.0.0.0' 115 | endIpAddress: '0.0.0.0' 116 | } 117 | } 118 | 119 | resource dnszone 'Microsoft.Network/privateDnsZones@2024-06-01' = if (vnet) { 120 | name: '${mysqlServerName}.private.mysql.database.azure.com' 121 | location: 'global' 122 | } 123 | 124 | resource vnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (vnet) { 125 | name: virtualNetworkName 126 | parent: dnszone 127 | location: 'global' 128 | properties: { 129 | registrationEnabled: false 130 | virtualNetwork: { 131 | id: vnetId 132 | } 133 | } 134 | } 135 | 136 | module sqlSecret 'keyvaultsecret.bicep' = { 137 | name: 'sqlDbKeyDeploy' 138 | params: { 139 | keyVaultName: keyVaultName 140 | secretName: ctfDbSecretName 141 | secretValue: 'mysql+pymysql://${administratorLogin}:${administratorLoginPassword}@${mysqlServerName}.mysql.database.azure.com/ctfd?ssl_ca=/opt/certificates/DigiCertGlobalRootCA.crt.pem' 142 | } 143 | } 144 | 145 | resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 146 | name: '${mysqlServerName}-diagnostics' 147 | scope: mysqlDbServer 148 | properties: { 149 | logs: [ 150 | { 151 | category: null 152 | categoryGroup: 'allLogs' 153 | enabled: true 154 | retentionPolicy: { 155 | days: 5 156 | enabled: false 157 | } 158 | } 159 | ] 160 | workspaceId: logAnalyticsWorkspaceId 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /modules/webapp.bicep: -------------------------------------------------------------------------------- 1 | @description('Deploy in VNet') 2 | param vnet bool 3 | 4 | @description('Name for Azure Web app') 5 | param webAppName string 6 | 7 | @description('Location for all resources.') 8 | param location string 9 | 10 | @description('Name of the VNet') 11 | param virtualNetworkName string 12 | 13 | @description('Name of the public subnet') 14 | param publicResourcesSubnetName string 15 | 16 | @description('Name of azure key vault') 17 | param keyVaultName string 18 | 19 | @description('Log Anaytics Workspace Id') 20 | param logAnalyticsWorkspaceId string 21 | 22 | @description('App Service Plan SKU name') 23 | param appServicePlanSkuName string 24 | 25 | @description('Azure Container Registry Image name') 26 | param acrImageName string 27 | 28 | @description('Azure Container Registry name') 29 | param registryName string 30 | 31 | @description('Name of the key vault secret holding the cache connection string') 32 | param ctfCacheSecretName string 33 | 34 | @description('Name of the key vault secret holding the database connection string') 35 | param ctfDatabaseSecretName string 36 | 37 | @description('CTF managed identity client ID') 38 | param managedIdentityClientId string 39 | 40 | @description('CTF managed identity ID') 41 | param managedIdentityId string 42 | 43 | @description('Storage Account Name') 44 | param storageAccountName string 45 | 46 | @description('Storage Account File Share Name') 47 | param shareName string = 'uploads' 48 | 49 | @description('Storage Account File Share Name') 50 | param storageMountPath string = '/opt/CTFd/CTFd/uploads' 51 | 52 | @description('Server Name for Azure app service') 53 | var appServicePlanName = 'ctfd-server-${uniqueString(resourceGroup().id)}' 54 | 55 | // Get a reference to the existing storage 56 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { 57 | name: storageAccountName 58 | } 59 | 60 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { 61 | name: appServicePlanName 62 | location: location 63 | kind: 'linux' 64 | properties: { 65 | reserved: true 66 | } 67 | sku: { 68 | name: appServicePlanSkuName 69 | } 70 | } 71 | 72 | resource webApp 'Microsoft.Web/sites@2022-09-01' = { 73 | name: webAppName 74 | location: location 75 | tags: {} 76 | identity: { 77 | type: 'UserAssigned' 78 | userAssignedIdentities: { 79 | '${managedIdentityId}': { } 80 | } 81 | } 82 | properties: { 83 | virtualNetworkSubnetId: (vnet ? resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, publicResourcesSubnetName) : null) 84 | keyVaultReferenceIdentity: managedIdentityId 85 | vnetRouteAllEnabled: (vnet ? true : false) 86 | siteConfig: { 87 | acrUseManagedIdentityCreds: true 88 | acrUserManagedIdentityID: managedIdentityClientId 89 | appSettings: [ 90 | { 91 | name: 'DATABASE_URL' 92 | value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/${ctfDatabaseSecretName}/)' 93 | } 94 | { 95 | name: 'REDIS_URL' 96 | value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/${ctfCacheSecretName}/)' 97 | } 98 | { 99 | name: 'REVERSE_PROXY' 100 | value: 'False' 101 | } 102 | { 103 | name: 'WEBSITES_PORT' 104 | value: '8000' 105 | } 106 | { 107 | name: 'DOCKER_REGISTRY_SERVER_URL' 108 | value: '${registryName}.azurecr.io' 109 | } 110 | ] 111 | linuxFxVersion: 'DOCKER|${acrImageName}' 112 | azureStorageAccounts: { 113 | '${shareName}': { 114 | type: 'AzureFiles' 115 | shareName: shareName 116 | mountPath: storageMountPath 117 | accountName: storageAccountName 118 | accessKey: storageAccount.listKeys().keys[0].value 119 | } 120 | } 121 | } 122 | serverFarmId: appServicePlan.id 123 | } 124 | } 125 | 126 | resource appServiceAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { 127 | parent: webApp 128 | name: 'logs' 129 | properties: { 130 | applicationLogs: { 131 | fileSystem: { 132 | level: 'Warning' 133 | } 134 | } 135 | httpLogs: { 136 | fileSystem: { 137 | retentionInMb: 40 138 | retentionInDays: 5 139 | enabled: true 140 | } 141 | } 142 | failedRequestsTracing: { 143 | enabled: true 144 | } 145 | detailedErrorMessages: { 146 | enabled: true 147 | } 148 | } 149 | } 150 | 151 | resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 152 | name: '${webAppName}-diagnostics' 153 | scope: webApp 154 | properties: { 155 | logs: [ 156 | { 157 | category: 'AppServiceHTTPLogs' 158 | categoryGroup: null 159 | enabled: true 160 | retentionPolicy: { 161 | days: 5 162 | enabled: false 163 | } 164 | } 165 | { 166 | category: 'AppServiceConsoleLogs' 167 | categoryGroup: null 168 | enabled: true 169 | retentionPolicy: { 170 | days:5 171 | enabled: false 172 | } 173 | } 174 | { 175 | category: 'AppServiceAppLogs' 176 | categoryGroup: null 177 | enabled: true 178 | retentionPolicy: { 179 | days: 5 180 | enabled: false 181 | } 182 | } 183 | { 184 | category: 'AppServiceAuditLogs' 185 | categoryGroup: null 186 | enabled: true 187 | retentionPolicy: { 188 | days: 5 189 | enabled: false 190 | } 191 | } 192 | { 193 | category: 'AppServicePlatformLogs' 194 | categoryGroup: null 195 | enabled: true 196 | retentionPolicy: { 197 | days: 5 198 | enabled: false 199 | } 200 | } 201 | ] 202 | workspaceId: logAnalyticsWorkspaceId 203 | } 204 | } 205 | 206 | output outboundIpAdresses string = webApp.properties.outboundIpAddresses 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTFd on Azure PaaS 2 | 3 | This project sets up a self-hosted, secured [CTFd][ctfd] environment, using Azure PaaS, that is easy to maintain. 4 | It supports the *Capture-the-Flag with CTFd on Azure PaaS* content on the [Azure Architecture Center][azure-arch-ctfd-paas]. 5 | 6 | ## Features 7 | 8 | ![CTFd architecture](/assets/architecture-with-vnet.png) 9 | 10 | This project provides the following features: 11 | 12 | * Infrastructure as Code with [Azure Bicep][bicep]. 13 | * High scale that meets different team sizes with [Azure App Service Web App for Containers][app-service]. 14 | * Backend database and cache provided with Azure PaaS [Database for MySQL][mysql] and [Cache for Redis][redis]. 15 | * Persistent file storage provided with [Azure Files][azure-files] using a [mounted SMB share][app-service-connect-storage] 16 | * Secrets management using [Azure Key Vault][keyvault]. 17 | * Log Management with [Azure Log Analytics][log-analytics]. 18 | * Adjustable level of network isolation: The solution can be provisioned either with or without virtual network. Private networking is provided using [Private Endpoints][private-endpoint] and [App Service VNet Integration][vnet-integration]. 19 | * Custom CTFd container image built and hosted on [Azure Container Registry][container-registry] with certificates to allow TLS connectivity to [Azure Database for MySQL][mysql]. 20 | * The image is based off the community CTFd image layered with the certificate required to communicate with Azure [Database for MySQL over TLS](https://learn.microsoft.com/en-us/azure/mysql/single-server/how-to-configure-ssl). 21 | 22 | ## Getting Started 23 | 24 | ### Prerequisites 25 | 26 | * [Azure CLI][az-cli-installation] 27 | * Azure Subscription with at least a Resource-Group's Contributor access 28 | 29 | ### Quickstart 30 | 31 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fctfd-azure-paas%2Fmain%2Fazuredeploy.json) 32 | 33 | ```bash 34 | git clone https://github.com/Azure-Samples/ctfd-azure-paas.git 35 | cd ctfd-azure-paas 36 | 37 | # This is bash syntax. if using Powershell, add $ sign before the assignments (i.e. $DB_PASSWORD='YOUR PASSWORD') 38 | DB_PASSWORD='YOUR PASSWORD' 39 | RESOURCE_GROUP_NAME='RESOURCE GROUP NAME' 40 | 41 | az deployment group create --resource-group $RESOURCE_GROUP_NAME --template-file ctfd.bicep --parameters administratorLoginPassword=$DB_PASSWORD 42 | ``` 43 | 44 | ### Access and Configure CTFd 45 | 46 | * Navigate your browser to the App Service URL, in the form of `*https://[YOUR APP SERVICE NAME].azurewebsites.net*` 47 | * Configure your Capture the Flag event using the administrator dashboard. more info [here](https://docs.ctfd.io/tutorials/getting-started) 48 | 49 | ### Troubleshooting and debugging 50 | 51 | * Navigate to the Log Analytics workspace in the resource group. 52 | * Check logs from CTFd container(s) using the table AppServiceConsoleLogs 53 | 54 | ### Adjustable Network Isolation 55 | 56 | By default the solution isolates network traffic from the CTFd App Service to the internal services (database, cache and key management) using a virtual network. 57 | You may reduce the solution complexity and potentially optimize cost by provisioning it without network isolation using the following command: 58 | 59 | ```bash 60 | az deployment group create --resource-group $RESOURCE_GROUP_NAME --template-file ctfd.bicep --parameters administratorLoginPassword=$DB_PASSWORD --parameters vnet=False 61 | ``` 62 | 63 | When provisioning the solution without a virtual network, the architecture diagram should look like this: 64 | 65 | ![CTFd architecture without vnet](/assets/architecture-without-vnet.png) 66 | 67 | ### Cleanup 68 | 69 | Delete the resource group using the following command 70 | 71 | ```bash 72 | az group delete -n $RESOURCE_GROUP_NAME 73 | ``` 74 | 75 | ### Additional Configuration Options 76 | 77 | The template deployment can be further configured using the following parameters: 78 | 79 | * **resourcesLocation** - Location for all resources. Defaults to the resource group location. 80 | * **vnet** - Deploy the solution with VNet. Defaults to True 81 | * **redisSkuName** - Azure Cache for Redis SKU Name. More info at [Azure Cache for Redis Pricing][redis-pricing] 82 | * **redisSkuSize** - Azure Cache for Redis SKU Size. More info at [Azure Cache for Redis Pricing][redis-pricing] 83 | * **administratorLogin** - Admin Login of Azure Database for MySQL 84 | * **administratorLoginPassword** - Admin Password of Azure Database for MySQL 85 | * **mysqlType** - Azure Database for MySQL Workload Type. Can be either Development, SmallMedium or BusinessCritical. This affects the underlying virtual machine size as well as the storage capacity. More info at [Azure Database for MySQL Pricing][mysql-pricing] 86 | * **appServicePlanSkuName** - Azure App Service Plan SKU Name. More info at [Azure App Service Pricing][app-service-pricing] 87 | * **webAppName** - Azure App Service Name. Controls the DNS name of the CTF site. 88 | 89 | ## Contribute to this project 90 | 91 | Follow the [Contribution Guide](./CONTRIBUTING.md) 92 | 93 | ## Resources 94 | 95 | * [App Services - Web App for container][app-service] 96 | * [Azure Database for MySQL][mysql] 97 | * [Azure Cache for Redis][redis] 98 | * [Azure Key Vault][keyvault] 99 | * [Azure Log Analytics][log-analytics] 100 | * [Azure Networking][azure-networking] 101 | * [Azure Container Registry][container-registry] 102 | * [Azure Files][azure-files] 103 | 104 | 105 | [ctfd]: https://github.com/CTFd/CTFd 106 | [bicep]: https://learn.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=bicep 107 | [app-service]: https://azure.microsoft.com/products/app-service/containers/ 108 | [mysql]: https://azure.microsoft.com/services/mysql/ 109 | [redis]: https://www.microsoft.com/azure/redis-cache/cache-overview 110 | [keyvault]: https://azure.microsoft.com/services/key-vault 111 | [log-analytics]: https://learn.microsoft.com/azure/azure-monitor/log-query/log-analytics-overview 112 | [private-endpoint]: https://learn.microsoft.com/azure/private-link/private-endpoint-overview 113 | [vnet-integration]: https://learn.microsoft.com/azure/app-service/overview-vnet-integration 114 | [az-cli-installation]: https://learn.microsoft.com/cli/azure/install-azure-cli 115 | [azure-networking]: https://learn.microsoft.com/azure/virtual-network/virtual-networks-overview 116 | [container-registry]: https://learn.microsoft.com/azure/container-registry/ 117 | [redis-pricing]: https://azure.microsoft.com/pricing/details/cache/ 118 | [mysql-pricing]: https://learn.microsoft.com/en-gb/azure/mysql/single-server/concepts-pricing-tiers 119 | [app-service-pricing]: https://azure.microsoft.com/pricing/details/app-service/linux/ 120 | [azure-files]: https://learn.microsoft.com/en-us/azure/storage/files/storage-files-introduction 121 | [app-service-connect-storage]: https://learn.microsoft.com/en-us/azure/app-service/configure-connect-to-azure-storage 122 | [azure-arch-ctfd-paas]: https://learn.microsoft.com/en-us/azure/architecture/example-scenario/apps/capture-the-flag-platform-on-azure-paas -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /ctfd.bicep: -------------------------------------------------------------------------------- 1 | @description('Location for all resources.') 2 | param resourcesLocation string = resourceGroup().location 3 | 4 | @description('Deploy with VNet') 5 | param vnet bool = true 6 | 7 | @description('SKU Name for Azure cache for Redis') 8 | @allowed([ 9 | 'Basic' 10 | 'Premium' 11 | 'Standard' 12 | ]) 13 | param redisSkuName string = 'Standard' 14 | 15 | @allowed([ 16 | 0 17 | 1 18 | 2 19 | 3 20 | 4 21 | 5 22 | 6 23 | ]) 24 | @description('The size of the Redis cache') 25 | param redisSkuSize int = 0 26 | 27 | @description('Database administrator login name') 28 | @minLength(1) 29 | param administratorLogin string = 'ctfd' 30 | 31 | @description('Database administrator password. Minimum 8 characters and maximum 128 characters. Password must contain characters from three of the following categories: English uppercase letters, English lowercase letters, numbers, and non-alphanumeric characters.') 32 | @minLength(8) 33 | @secure() 34 | param administratorLoginPassword string 35 | 36 | @description('MySQL Type') 37 | @allowed([ 38 | 'Development' 39 | 'SmallMedium' 40 | 'BusinessCritical' 41 | ]) 42 | param mysqlType string = 'Development' 43 | 44 | @description('App Service Plan SKU name') 45 | @allowed([ 46 | 'B1' 47 | 'B2' 48 | 'B3' 49 | 'S1' 50 | 'S2' 51 | 'S3' 52 | 'P1' 53 | 'P2' 54 | 'P3' 55 | 'P4' 56 | ]) 57 | param appServicePlanSkuName string = 'B1' 58 | 59 | @description('Name for Azure Web app. Controls the DNS name of the CTF website') 60 | param webAppName string = 'ctfd-app-${uniqueString(resourceGroup().id)}' 61 | 62 | @description('SKU for Azure Container Registry') 63 | var containerRegistrySku = 'Basic' 64 | 65 | @description('SKU for Azure Storage Account') 66 | var storageSkuName = 'Standard_LRS' 67 | 68 | @description('Account Name for the Azure Storage Account') 69 | var storageAccountName = 'ctfd${uniqueString(resourceGroup().id)}' 70 | 71 | @description('Name of Azure Key Vault') 72 | var keyVaultName = 'ctfd-kv-${uniqueString(resourceGroup().id)}' 73 | 74 | @description('Name of the key vault secret holding the cache connection string') 75 | var ctfCacheSecretName = 'ctfd-cache-url' 76 | 77 | @description('Name of the key vault secret holding the database connection string') 78 | var ctfDatabaseSecretName = 'ctfd-db-url' 79 | 80 | @description('Name of the VNet') 81 | var virtualNetworkName = 'ctf-vnet' 82 | 83 | @description('Name of the internal resources subnet') 84 | var internalResourcesSubnetName = 'internal_resources_subnet' 85 | 86 | @description('Name of the public resources subnet') 87 | var publicResourcesSubnetName = 'public_resources_subnet' 88 | 89 | @description('Name of the database resources subnet') 90 | var databaseResourcesSubnetName = 'database_resources_subnet' 91 | 92 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 93 | name: 'ctf-mi-${uniqueString(resourceGroup().id)}' 94 | location: resourcesLocation 95 | } 96 | 97 | @description('Deploys Azure Log Analytics workspace') 98 | module logAnalyticsModule 'modules/loganalytics.bicep' = { 99 | name: 'logAnalyticsDeploy' 100 | params: { 101 | location: resourcesLocation 102 | } 103 | } 104 | 105 | @description('Deploys Azure Container Registry and build a custom CTFd docker image') 106 | module acrModule 'modules/acr.bicep' = { 107 | name: 'acrDeploy' 108 | params: { 109 | logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId 110 | location: resourcesLocation 111 | containerRegistrySku: containerRegistrySku 112 | managedIdentityId: managedIdentity.id 113 | managedIdentityPrincipalId: managedIdentity.properties.principalId 114 | } 115 | } 116 | 117 | @description('Deploys Virtual Network with two subnets') 118 | module vnetModule 'modules/vnet.bicep' = if (vnet) { 119 | name: 'vnetDeploy' 120 | params: { 121 | location: resourcesLocation 122 | virtualNetworkName: virtualNetworkName 123 | internalResourcesSubnetName: internalResourcesSubnetName 124 | publicResourcesSubnetName: publicResourcesSubnetName 125 | databaseResourcesSubnetName: databaseResourcesSubnetName 126 | } 127 | } 128 | 129 | module fileStorage 'modules/filestorage.bicep' = { 130 | name: 'ctfdFileStorage' 131 | params: { 132 | internalResourcesSubnetName: internalResourcesSubnetName 133 | location: resourcesLocation 134 | logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId 135 | storageSkuName: storageSkuName 136 | storageAccountName: storageAccountName 137 | virtualNetworkName: virtualNetworkName 138 | vnet: vnet 139 | } 140 | } 141 | 142 | module fileStorageAcl 'modules/filestorageAcl.bicep' = { 143 | name: 'ctfdFileStorageAcl' 144 | params: { 145 | location: resourcesLocation 146 | storageSkuName: storageSkuName 147 | storageAccountName: storageAccountName 148 | vnet: vnet 149 | webAppOutboundIpAdresses: ctfWebAppModule.outputs.outboundIpAdresses 150 | } 151 | } 152 | 153 | 154 | @description('Deploys Azure App Service for containers') 155 | module ctfWebAppModule 'modules/webapp.bicep' = { 156 | name: 'ctfDeploy' 157 | params: { 158 | virtualNetworkName: virtualNetworkName 159 | location: resourcesLocation 160 | appServicePlanSkuName: appServicePlanSkuName 161 | keyVaultName: keyVaultName 162 | ctfCacheSecretName: ctfCacheSecretName 163 | ctfDatabaseSecretName: ctfDatabaseSecretName 164 | publicResourcesSubnetName: publicResourcesSubnetName 165 | webAppName: webAppName 166 | logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId 167 | acrImageName: acrModule.outputs.acrImage 168 | registryName: acrModule.outputs.registryName 169 | managedIdentityClientId: managedIdentity.properties.clientId 170 | managedIdentityId: managedIdentity.id 171 | storageAccountName: fileStorage.outputs.storageAccountName 172 | vnet: vnet 173 | } 174 | } 175 | 176 | @description('Deploys Azure Key Vault') 177 | module akvModule 'modules/keyvault.bicep' = { 178 | name: 'keyVaultDeploy' 179 | dependsOn: [ ctfWebAppModule ] 180 | params: { 181 | location: resourcesLocation 182 | readerPrincipalId: managedIdentity.properties.principalId 183 | internalResourcesSubnetName: internalResourcesSubnetName 184 | virtualNetworkName: virtualNetworkName 185 | logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId 186 | vnet: vnet 187 | keyVaultName: keyVaultName 188 | webAppOutboundIpAdresses: ctfWebAppModule.outputs.outboundIpAdresses 189 | } 190 | } 191 | 192 | @description('Deploys Azure Cache for Redis and a Key Vault secret with its connection string') 193 | module redisModule 'modules/redis.bicep' = { 194 | name: 'redisDeploy' 195 | params: { 196 | internalResourcesSubnetName: internalResourcesSubnetName 197 | virtualNetworkName: virtualNetworkName 198 | location: resourcesLocation 199 | vnet: vnet 200 | ctfCacheSecretName: ctfCacheSecretName 201 | keyVaultName: akvModule.outputs.keyVaultName 202 | redisSkuName: redisSkuName 203 | redisSkuSize: redisSkuSize 204 | logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId 205 | } 206 | } 207 | 208 | @description('Deploys Azure Database for MySql and a Key Vault secret with its connection string') 209 | module mySqlDbModule 'modules/mysql.bicep' = { 210 | name: 'mysqlDbDeploy' 211 | params: { 212 | administratorLogin: administratorLogin 213 | administratorLoginPassword: administratorLoginPassword 214 | vnetId: (vnet) ? vnetModule.outputs.virtualNetworkId : '' 215 | databaseSubnetId: (vnet) ? vnetModule.outputs.databaseResourcesSubnetId : '' 216 | virtualNetworkName: virtualNetworkName 217 | location: resourcesLocation 218 | vnet: vnet 219 | ctfDbSecretName: ctfDatabaseSecretName 220 | keyVaultName: akvModule.outputs.keyVaultName 221 | mysqlWorkloadType: mysqlType 222 | logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.30.23.60470", 8 | "templateHash": "15371799224721930237" 9 | } 10 | }, 11 | "parameters": { 12 | "resourcesLocation": { 13 | "type": "string", 14 | "defaultValue": "[resourceGroup().location]", 15 | "metadata": { 16 | "description": "Location for all resources." 17 | } 18 | }, 19 | "vnet": { 20 | "type": "bool", 21 | "defaultValue": true, 22 | "metadata": { 23 | "description": "Deploy with VNet" 24 | } 25 | }, 26 | "redisSkuName": { 27 | "type": "string", 28 | "defaultValue": "Standard", 29 | "allowedValues": [ 30 | "Basic", 31 | "Premium", 32 | "Standard" 33 | ], 34 | "metadata": { 35 | "description": "SKU Name for Azure cache for Redis" 36 | } 37 | }, 38 | "redisSkuSize": { 39 | "type": "int", 40 | "defaultValue": 0, 41 | "allowedValues": [ 42 | 0, 43 | 1, 44 | 2, 45 | 3, 46 | 4, 47 | 5, 48 | 6 49 | ], 50 | "metadata": { 51 | "description": "The size of the Redis cache" 52 | } 53 | }, 54 | "administratorLogin": { 55 | "type": "string", 56 | "defaultValue": "ctfd", 57 | "minLength": 1, 58 | "metadata": { 59 | "description": "Database administrator login name" 60 | } 61 | }, 62 | "administratorLoginPassword": { 63 | "type": "securestring", 64 | "minLength": 8, 65 | "metadata": { 66 | "description": "Database administrator password. Minimum 8 characters and maximum 128 characters. Password must contain characters from three of the following categories: English uppercase letters, English lowercase letters, numbers, and non-alphanumeric characters." 67 | } 68 | }, 69 | "mysqlType": { 70 | "type": "string", 71 | "defaultValue": "Development", 72 | "allowedValues": [ 73 | "Development", 74 | "SmallMedium", 75 | "BusinessCritical" 76 | ], 77 | "metadata": { 78 | "description": "MySQL Type" 79 | } 80 | }, 81 | "appServicePlanSkuName": { 82 | "type": "string", 83 | "defaultValue": "B1", 84 | "allowedValues": [ 85 | "B1", 86 | "B2", 87 | "B3", 88 | "S1", 89 | "S2", 90 | "S3", 91 | "P1", 92 | "P2", 93 | "P3", 94 | "P4" 95 | ], 96 | "metadata": { 97 | "description": "App Service Plan SKU name" 98 | } 99 | }, 100 | "webAppName": { 101 | "type": "string", 102 | "defaultValue": "[format('ctfd-app-{0}', uniqueString(resourceGroup().id))]", 103 | "metadata": { 104 | "description": "Name for Azure Web app. Controls the DNS name of the CTF website" 105 | } 106 | } 107 | }, 108 | "variables": { 109 | "containerRegistrySku": "Basic", 110 | "storageSkuName": "Standard_LRS", 111 | "storageAccountName": "[format('ctfd{0}', uniqueString(resourceGroup().id))]", 112 | "keyVaultName": "[format('ctfd-kv-{0}', uniqueString(resourceGroup().id))]", 113 | "ctfCacheSecretName": "ctfd-cache-url", 114 | "ctfDatabaseSecretName": "ctfd-db-url", 115 | "virtualNetworkName": "ctf-vnet", 116 | "internalResourcesSubnetName": "internal_resources_subnet", 117 | "publicResourcesSubnetName": "public_resources_subnet", 118 | "databaseResourcesSubnetName": "database_resources_subnet" 119 | }, 120 | "resources": [ 121 | { 122 | "type": "Microsoft.ManagedIdentity/userAssignedIdentities", 123 | "apiVersion": "2023-01-31", 124 | "name": "[format('ctf-mi-{0}', uniqueString(resourceGroup().id))]", 125 | "location": "[parameters('resourcesLocation')]" 126 | }, 127 | { 128 | "type": "Microsoft.Resources/deployments", 129 | "apiVersion": "2022-09-01", 130 | "name": "logAnalyticsDeploy", 131 | "properties": { 132 | "expressionEvaluationOptions": { 133 | "scope": "inner" 134 | }, 135 | "mode": "Incremental", 136 | "parameters": { 137 | "location": { 138 | "value": "[parameters('resourcesLocation')]" 139 | } 140 | }, 141 | "template": { 142 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 143 | "contentVersion": "1.0.0.0", 144 | "metadata": { 145 | "_generator": { 146 | "name": "bicep", 147 | "version": "0.30.23.60470", 148 | "templateHash": "13905535415559483716" 149 | } 150 | }, 151 | "parameters": { 152 | "location": { 153 | "type": "string", 154 | "metadata": { 155 | "description": "Location for all resources." 156 | } 157 | } 158 | }, 159 | "variables": { 160 | "appName": "CTFd", 161 | "logAnalyticsName": "[format('ctfd-log-analytics-{0}', uniqueString(resourceGroup().id))]", 162 | "retentionInDays": 30 163 | }, 164 | "resources": [ 165 | { 166 | "type": "Microsoft.OperationalInsights/workspaces", 167 | "apiVersion": "2023-09-01", 168 | "name": "[variables('logAnalyticsName')]", 169 | "location": "[parameters('location')]", 170 | "tags": { 171 | "displayName": "Log Analytics", 172 | "ProjectName": "[variables('appName')]" 173 | }, 174 | "properties": { 175 | "sku": { 176 | "name": "PerGB2018" 177 | }, 178 | "retentionInDays": "[variables('retentionInDays')]", 179 | "features": { 180 | "searchVersion": 1, 181 | "legacy": 0, 182 | "enableLogAccessUsingOnlyResourcePermissions": true 183 | } 184 | } 185 | } 186 | ], 187 | "outputs": { 188 | "logAnalyticsWorkspaceId": { 189 | "type": "string", 190 | "value": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" 191 | } 192 | } 193 | } 194 | }, 195 | "metadata": { 196 | "description": "Deploys Azure Log Analytics workspace" 197 | } 198 | }, 199 | { 200 | "type": "Microsoft.Resources/deployments", 201 | "apiVersion": "2022-09-01", 202 | "name": "acrDeploy", 203 | "properties": { 204 | "expressionEvaluationOptions": { 205 | "scope": "inner" 206 | }, 207 | "mode": "Incremental", 208 | "parameters": { 209 | "logAnalyticsWorkspaceId": { 210 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy'), '2022-09-01').outputs.logAnalyticsWorkspaceId.value]" 211 | }, 212 | "location": { 213 | "value": "[parameters('resourcesLocation')]" 214 | }, 215 | "containerRegistrySku": { 216 | "value": "[variables('containerRegistrySku')]" 217 | }, 218 | "managedIdentityId": { 219 | "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id)))]" 220 | }, 221 | "managedIdentityPrincipalId": { 222 | "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id))), '2023-01-31').principalId]" 223 | } 224 | }, 225 | "template": { 226 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 227 | "contentVersion": "1.0.0.0", 228 | "metadata": { 229 | "_generator": { 230 | "name": "bicep", 231 | "version": "0.30.23.60470", 232 | "templateHash": "11953261869424114237" 233 | } 234 | }, 235 | "parameters": { 236 | "location": { 237 | "type": "string", 238 | "metadata": { 239 | "description": "Location for all resources." 240 | } 241 | }, 242 | "containerRegistrySku": { 243 | "type": "string", 244 | "metadata": { 245 | "description": "Tier of Azure Container Registry." 246 | } 247 | }, 248 | "managedIdentityPrincipalId": { 249 | "type": "string", 250 | "metadata": { 251 | "description": "Managed Identity Principal Id." 252 | } 253 | }, 254 | "managedIdentityId": { 255 | "type": "string", 256 | "metadata": { 257 | "description": "Managed Identity Id." 258 | } 259 | }, 260 | "logAnalyticsWorkspaceId": { 261 | "type": "string", 262 | "metadata": { 263 | "description": "Log Anaytics Workspace Id" 264 | } 265 | } 266 | }, 267 | "variables": { 268 | "$fxv#0": "# This Dockerfile builds a CTFd (https://github.com/CTFd/CTFd) image that\n# enables TLS connectivity to Azure Database for MySQL.\n# More info: https://learn.microsoft.com/en-gb/azure/postgresql/flexible-server/concepts-networking-ssl-tls#downloading-root-ca-certificates-and-updating-application-clients-in-certificate-pinning-scenarios\nFROM ctfd/ctfd:3.7.0\n\nUSER root\nRUN apt-get update && apt-get install -y wget --no-install-recommends \\\n && apt-get clean \\\n && rm -rf /var/lib/apt/lists/*\n\nRUN wget --user-agent=\"Mozilla\" --progress=dot:giga https://cacerts.digicert.com/DigiCertGlobalRootCA.crt -P /opt/certificates/\nRUN openssl x509 -in /opt/certificates/DigiCertGlobalRootCA.crt -out /opt/certificates/DigiCertGlobalRootCA.crt.pem -outform PEM\n\nUSER 1001\nEXPOSE 8000\n\nENTRYPOINT [\"/opt/CTFd/docker-entrypoint.sh\"]\n", 269 | "$fxv#1": "#!/bin/bash\nset -e\n\necho \"Waiting on RBAC replication\"\nsleep $initialDelay\n\necho \"$CONTENT\" > Dockerfile\n\naz acr build \\\n --registry $acrName \\\n --image $taggedImageName \\\n --platform $platform \\\n .\n", 270 | "containerRegistryName": "[format('ctfdacr{0}', uniqueString(resourceGroup().id))]", 271 | "ctfdImageName": "ctfd-azure-cert:latest" 272 | }, 273 | "resources": [ 274 | { 275 | "type": "Microsoft.ContainerRegistry/registries", 276 | "apiVersion": "2023-01-01-preview", 277 | "name": "[variables('containerRegistryName')]", 278 | "location": "[parameters('location')]", 279 | "sku": { 280 | "name": "[parameters('containerRegistrySku')]" 281 | }, 282 | "properties": { 283 | "adminUserEnabled": false 284 | } 285 | }, 286 | { 287 | "type": "Microsoft.Insights/diagnosticSettings", 288 | "apiVersion": "2021-05-01-preview", 289 | "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', variables('containerRegistryName'))]", 290 | "name": "[format('{0}-diagnostics', variables('containerRegistryName'))]", 291 | "properties": { 292 | "logs": [ 293 | { 294 | "category": null, 295 | "categoryGroup": "audit", 296 | "enabled": true, 297 | "retentionPolicy": { 298 | "days": 5, 299 | "enabled": false 300 | } 301 | }, 302 | { 303 | "category": null, 304 | "categoryGroup": "allLogs", 305 | "enabled": true, 306 | "retentionPolicy": { 307 | "days": 5, 308 | "enabled": false 309 | } 310 | } 311 | ], 312 | "workspaceId": "[parameters('logAnalyticsWorkspaceId')]" 313 | }, 314 | "dependsOn": [ 315 | "[resourceId('Microsoft.ContainerRegistry/registries', variables('containerRegistryName'))]" 316 | ] 317 | }, 318 | { 319 | "type": "Microsoft.Authorization/roleAssignments", 320 | "apiVersion": "2022-04-01", 321 | "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', variables('containerRegistryName'))]", 322 | "name": "[guid(resourceGroup().id, 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", 323 | "properties": { 324 | "principalId": "[parameters('managedIdentityPrincipalId')]", 325 | "roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.ContainerRegistry/registries', variables('containerRegistryName')), 'Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", 326 | "principalType": "ServicePrincipal" 327 | }, 328 | "dependsOn": [ 329 | "[resourceId('Microsoft.ContainerRegistry/registries', variables('containerRegistryName'))]" 330 | ] 331 | }, 332 | { 333 | "type": "Microsoft.Resources/deploymentScripts", 334 | "apiVersion": "2023-08-01", 335 | "name": "buildAndPush", 336 | "location": "[parameters('location')]", 337 | "kind": "AzureCLI", 338 | "identity": { 339 | "type": "UserAssigned", 340 | "userAssignedIdentities": { 341 | "[format('{0}', parameters('managedIdentityId'))]": {} 342 | } 343 | }, 344 | "properties": { 345 | "timeout": "PT30M", 346 | "azCliVersion": "2.40.0", 347 | "environmentVariables": [ 348 | { 349 | "name": "acrName", 350 | "value": "[variables('containerRegistryName')]" 351 | }, 352 | { 353 | "name": "acrResourceGroup", 354 | "secureValue": "[resourceGroup().name]" 355 | }, 356 | { 357 | "name": "taggedImageName", 358 | "value": "[variables('ctfdImageName')]" 359 | }, 360 | { 361 | "name": "CONTENT", 362 | "value": "[variables('$fxv#0')]" 363 | }, 364 | { 365 | "name": "platform", 366 | "value": "Linux" 367 | }, 368 | { 369 | "name": "initialDelay", 370 | "secureValue": "30s" 371 | } 372 | ], 373 | "scriptContent": "[variables('$fxv#1')]", 374 | "retentionInterval": "P1D" 375 | } 376 | } 377 | ], 378 | "outputs": { 379 | "acrImage": { 380 | "type": "string", 381 | "value": "[format('{0}/{1}', reference(resourceId('Microsoft.ContainerRegistry/registries', variables('containerRegistryName')), '2023-01-01-preview').loginServer, variables('ctfdImageName'))]" 382 | }, 383 | "registryName": { 384 | "type": "string", 385 | "value": "[variables('containerRegistryName')]" 386 | } 387 | } 388 | } 389 | }, 390 | "dependsOn": [ 391 | "[resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy')]", 392 | "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id)))]" 393 | ], 394 | "metadata": { 395 | "description": "Deploys Azure Container Registry and build a custom CTFd docker image" 396 | } 397 | }, 398 | { 399 | "condition": "[parameters('vnet')]", 400 | "type": "Microsoft.Resources/deployments", 401 | "apiVersion": "2022-09-01", 402 | "name": "vnetDeploy", 403 | "properties": { 404 | "expressionEvaluationOptions": { 405 | "scope": "inner" 406 | }, 407 | "mode": "Incremental", 408 | "parameters": { 409 | "location": { 410 | "value": "[parameters('resourcesLocation')]" 411 | }, 412 | "virtualNetworkName": { 413 | "value": "[variables('virtualNetworkName')]" 414 | }, 415 | "internalResourcesSubnetName": { 416 | "value": "[variables('internalResourcesSubnetName')]" 417 | }, 418 | "publicResourcesSubnetName": { 419 | "value": "[variables('publicResourcesSubnetName')]" 420 | }, 421 | "databaseResourcesSubnetName": { 422 | "value": "[variables('databaseResourcesSubnetName')]" 423 | } 424 | }, 425 | "template": { 426 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 427 | "contentVersion": "1.0.0.0", 428 | "metadata": { 429 | "_generator": { 430 | "name": "bicep", 431 | "version": "0.30.23.60470", 432 | "templateHash": "14327329091686549038" 433 | } 434 | }, 435 | "parameters": { 436 | "location": { 437 | "type": "string", 438 | "metadata": { 439 | "description": "Location for all resources." 440 | } 441 | }, 442 | "virtualNetworkName": { 443 | "type": "string", 444 | "metadata": { 445 | "description": "Name of the VNet" 446 | } 447 | }, 448 | "internalResourcesSubnetName": { 449 | "type": "string", 450 | "metadata": { 451 | "description": "Name of the internal resources subnet" 452 | } 453 | }, 454 | "publicResourcesSubnetName": { 455 | "type": "string", 456 | "metadata": { 457 | "description": "Name of the public resources subnet" 458 | } 459 | }, 460 | "databaseResourcesSubnetName": { 461 | "type": "string", 462 | "metadata": { 463 | "description": "Name of the database resources subnet" 464 | } 465 | } 466 | }, 467 | "variables": { 468 | "virtualNetworkCIDR": "10.200.0.0/16", 469 | "publicResourcesSubnetCIDR": "10.200.1.0/26", 470 | "internalResourcesSubnetCIDR": "10.200.2.0/28", 471 | "databaseResourcesSubnetCIDR": "10.200.3.0/28" 472 | }, 473 | "resources": [ 474 | { 475 | "type": "Microsoft.Network/virtualNetworks", 476 | "apiVersion": "2024-03-01", 477 | "name": "[parameters('virtualNetworkName')]", 478 | "location": "[parameters('location')]", 479 | "properties": { 480 | "addressSpace": { 481 | "addressPrefixes": [ 482 | "[variables('virtualNetworkCIDR')]" 483 | ] 484 | }, 485 | "subnets": [ 486 | { 487 | "name": "[parameters('internalResourcesSubnetName')]", 488 | "properties": { 489 | "addressPrefix": "[variables('internalResourcesSubnetCIDR')]", 490 | "privateEndpointNetworkPolicies": "Disabled" 491 | } 492 | }, 493 | { 494 | "name": "[parameters('publicResourcesSubnetName')]", 495 | "properties": { 496 | "addressPrefix": "[variables('publicResourcesSubnetCIDR')]", 497 | "delegations": [ 498 | { 499 | "name": "dlg-Microsoft.Web-serverfarms", 500 | "properties": { 501 | "serviceName": "Microsoft.Web/serverfarms" 502 | } 503 | } 504 | ], 505 | "privateEndpointNetworkPolicies": "Enabled" 506 | } 507 | }, 508 | { 509 | "name": "[parameters('databaseResourcesSubnetName')]", 510 | "properties": { 511 | "addressPrefix": "[variables('databaseResourcesSubnetCIDR')]", 512 | "delegations": [ 513 | { 514 | "name": "dlg-Microsoft.DBforMySQL-flexibleServers", 515 | "properties": { 516 | "serviceName": "Microsoft.DBforMySQL/flexibleServers" 517 | } 518 | } 519 | ], 520 | "privateEndpointNetworkPolicies": "Enabled", 521 | "privateLinkServiceNetworkPolicies": "Enabled" 522 | } 523 | } 524 | ] 525 | } 526 | } 527 | ], 528 | "outputs": { 529 | "virtualNetworkId": { 530 | "type": "string", 531 | "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" 532 | }, 533 | "databaseResourcesSubnetId": { 534 | "type": "string", 535 | "value": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName')), '2024-03-01').subnets[2].id]" 536 | } 537 | } 538 | } 539 | }, 540 | "metadata": { 541 | "description": "Deploys Virtual Network with two subnets" 542 | } 543 | }, 544 | { 545 | "type": "Microsoft.Resources/deployments", 546 | "apiVersion": "2022-09-01", 547 | "name": "ctfdFileStorage", 548 | "properties": { 549 | "expressionEvaluationOptions": { 550 | "scope": "inner" 551 | }, 552 | "mode": "Incremental", 553 | "parameters": { 554 | "internalResourcesSubnetName": { 555 | "value": "[variables('internalResourcesSubnetName')]" 556 | }, 557 | "location": { 558 | "value": "[parameters('resourcesLocation')]" 559 | }, 560 | "logAnalyticsWorkspaceId": { 561 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy'), '2022-09-01').outputs.logAnalyticsWorkspaceId.value]" 562 | }, 563 | "storageSkuName": { 564 | "value": "[variables('storageSkuName')]" 565 | }, 566 | "storageAccountName": { 567 | "value": "[variables('storageAccountName')]" 568 | }, 569 | "virtualNetworkName": { 570 | "value": "[variables('virtualNetworkName')]" 571 | }, 572 | "vnet": { 573 | "value": "[parameters('vnet')]" 574 | } 575 | }, 576 | "template": { 577 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 578 | "contentVersion": "1.0.0.0", 579 | "metadata": { 580 | "_generator": { 581 | "name": "bicep", 582 | "version": "0.30.23.60470", 583 | "templateHash": "15189742434822588328" 584 | } 585 | }, 586 | "parameters": { 587 | "vnet": { 588 | "type": "bool", 589 | "metadata": { 590 | "description": "Deploy in VNet" 591 | } 592 | }, 593 | "storageSkuName": { 594 | "type": "string", 595 | "metadata": { 596 | "description": "SKU Name for the Azure Storage Account" 597 | } 598 | }, 599 | "virtualNetworkName": { 600 | "type": "string", 601 | "metadata": { 602 | "description": "Name of the VNet" 603 | } 604 | }, 605 | "internalResourcesSubnetName": { 606 | "type": "string", 607 | "metadata": { 608 | "description": "Name of the internal resources subnet" 609 | } 610 | }, 611 | "location": { 612 | "type": "string", 613 | "metadata": { 614 | "description": "Location for all resources." 615 | } 616 | }, 617 | "logAnalyticsWorkspaceId": { 618 | "type": "string", 619 | "metadata": { 620 | "description": "Log Anaytics Workspace Id" 621 | } 622 | }, 623 | "storageAccountName": { 624 | "type": "string", 625 | "metadata": { 626 | "description": "Account Name for the Azure Storage Account" 627 | } 628 | } 629 | }, 630 | "resources": [ 631 | { 632 | "type": "Microsoft.Storage/storageAccounts", 633 | "apiVersion": "2023-05-01", 634 | "name": "[parameters('storageAccountName')]", 635 | "location": "[parameters('location')]", 636 | "sku": { 637 | "name": "[parameters('storageSkuName')]" 638 | }, 639 | "kind": "StorageV2", 640 | "properties": { 641 | "publicNetworkAccess": "[if(parameters('vnet'), 'Disabled', 'Enabled')]", 642 | "accessTier": "Hot" 643 | } 644 | }, 645 | { 646 | "type": "Microsoft.Storage/storageAccounts/fileServices", 647 | "apiVersion": "2023-05-01", 648 | "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]", 649 | "properties": {}, 650 | "dependsOn": [ 651 | "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" 652 | ] 653 | }, 654 | { 655 | "type": "Microsoft.Storage/storageAccounts/fileServices/shares", 656 | "apiVersion": "2023-05-01", 657 | "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', 'uploads')]", 658 | "properties": {}, 659 | "dependsOn": [ 660 | "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), 'default')]" 661 | ] 662 | }, 663 | { 664 | "type": "Microsoft.Insights/diagnosticSettings", 665 | "apiVersion": "2021-05-01-preview", 666 | "scope": "[format('Microsoft.Storage/storageAccounts/{0}/fileServices/{1}', parameters('storageAccountName'), 'default')]", 667 | "name": "[format('{0}-diagnostics', parameters('storageAccountName'))]", 668 | "properties": { 669 | "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", 670 | "logs": [ 671 | { 672 | "category": "StorageRead", 673 | "enabled": true 674 | }, 675 | { 676 | "category": "StorageWrite", 677 | "enabled": true 678 | }, 679 | { 680 | "category": "StorageDelete", 681 | "enabled": true 682 | } 683 | ], 684 | "metrics": [ 685 | { 686 | "category": "Transaction", 687 | "enabled": true 688 | } 689 | ] 690 | }, 691 | "dependsOn": [ 692 | "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), 'default')]" 693 | ] 694 | }, 695 | { 696 | "condition": "[parameters('vnet')]", 697 | "type": "Microsoft.Resources/deployments", 698 | "apiVersion": "2022-09-01", 699 | "name": "storagePrivateEndpointDeploy", 700 | "properties": { 701 | "expressionEvaluationOptions": { 702 | "scope": "inner" 703 | }, 704 | "mode": "Incremental", 705 | "parameters": { 706 | "virtualNetworkName": { 707 | "value": "[parameters('virtualNetworkName')]" 708 | }, 709 | "subnetName": { 710 | "value": "[parameters('internalResourcesSubnetName')]" 711 | }, 712 | "resuorceId": { 713 | "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" 714 | }, 715 | "resuorceGroupId": { 716 | "value": "file" 717 | }, 718 | "privateDnsZoneName": { 719 | "value": "[format('privatelink.file.{0}', environment().suffixes.storage)]" 720 | }, 721 | "privateEndpointName": { 722 | "value": "storage_private_endpoint" 723 | }, 724 | "location": { 725 | "value": "[parameters('location')]" 726 | } 727 | }, 728 | "template": { 729 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 730 | "contentVersion": "1.0.0.0", 731 | "metadata": { 732 | "_generator": { 733 | "name": "bicep", 734 | "version": "0.30.23.60470", 735 | "templateHash": "8939390758381320725" 736 | } 737 | }, 738 | "parameters": { 739 | "virtualNetworkName": { 740 | "type": "string", 741 | "metadata": { 742 | "description": "Name of the VNet" 743 | } 744 | }, 745 | "subnetName": { 746 | "type": "string", 747 | "metadata": { 748 | "description": "Name of the subnet" 749 | } 750 | }, 751 | "resuorceId": { 752 | "type": "string", 753 | "metadata": { 754 | "description": "Id of the resource" 755 | } 756 | }, 757 | "resuorceGroupId": { 758 | "type": "string", 759 | "metadata": { 760 | "description": "Group Id of the resource" 761 | } 762 | }, 763 | "privateDnsZoneName": { 764 | "type": "string", 765 | "metadata": { 766 | "description": "Name of dns zone" 767 | } 768 | }, 769 | "privateEndpointName": { 770 | "type": "string", 771 | "metadata": { 772 | "description": "Name of private endpoint" 773 | } 774 | }, 775 | "location": { 776 | "type": "string", 777 | "metadata": { 778 | "description": "Location for all resources." 779 | } 780 | } 781 | }, 782 | "resources": [ 783 | { 784 | "type": "Microsoft.Network/privateEndpoints", 785 | "apiVersion": "2024-03-01", 786 | "name": "[parameters('privateEndpointName')]", 787 | "location": "[parameters('location')]", 788 | "properties": { 789 | "subnet": { 790 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" 791 | }, 792 | "privateLinkServiceConnections": [ 793 | { 794 | "name": "[parameters('privateEndpointName')]", 795 | "properties": { 796 | "privateLinkServiceId": "[parameters('resuorceId')]", 797 | "groupIds": [ 798 | "[parameters('resuorceGroupId')]" 799 | ] 800 | } 801 | } 802 | ] 803 | } 804 | }, 805 | { 806 | "type": "Microsoft.Network/privateDnsZones", 807 | "apiVersion": "2024-06-01", 808 | "name": "[parameters('privateDnsZoneName')]", 809 | "location": "global", 810 | "properties": {} 811 | }, 812 | { 813 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 814 | "apiVersion": "2024-06-01", 815 | "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), format('{0}-link', parameters('privateDnsZoneName')))]", 816 | "location": "global", 817 | "properties": { 818 | "registrationEnabled": false, 819 | "virtualNetwork": { 820 | "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" 821 | } 822 | }, 823 | "dependsOn": [ 824 | "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]" 825 | ] 826 | }, 827 | { 828 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 829 | "apiVersion": "2024-03-01", 830 | "name": "[format('{0}/{1}', parameters('privateEndpointName'), 'default')]", 831 | "properties": { 832 | "privateDnsZoneConfigs": [ 833 | { 834 | "name": "config1", 835 | "properties": { 836 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]" 837 | } 838 | } 839 | ] 840 | }, 841 | "dependsOn": [ 842 | "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]", 843 | "[resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpointName'))]" 844 | ] 845 | } 846 | ] 847 | } 848 | }, 849 | "dependsOn": [ 850 | "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" 851 | ] 852 | } 853 | ], 854 | "outputs": { 855 | "storageAccountName": { 856 | "type": "string", 857 | "value": "[parameters('storageAccountName')]" 858 | } 859 | } 860 | } 861 | }, 862 | "dependsOn": [ 863 | "[resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy')]" 864 | ] 865 | }, 866 | { 867 | "type": "Microsoft.Resources/deployments", 868 | "apiVersion": "2022-09-01", 869 | "name": "ctfdFileStorageAcl", 870 | "properties": { 871 | "expressionEvaluationOptions": { 872 | "scope": "inner" 873 | }, 874 | "mode": "Incremental", 875 | "parameters": { 876 | "location": { 877 | "value": "[parameters('resourcesLocation')]" 878 | }, 879 | "storageSkuName": { 880 | "value": "[variables('storageSkuName')]" 881 | }, 882 | "storageAccountName": { 883 | "value": "[variables('storageAccountName')]" 884 | }, 885 | "vnet": { 886 | "value": "[parameters('vnet')]" 887 | }, 888 | "webAppOutboundIpAdresses": { 889 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'ctfDeploy'), '2022-09-01').outputs.outboundIpAdresses.value]" 890 | } 891 | }, 892 | "template": { 893 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 894 | "contentVersion": "1.0.0.0", 895 | "metadata": { 896 | "_generator": { 897 | "name": "bicep", 898 | "version": "0.30.23.60470", 899 | "templateHash": "9147010146567357457" 900 | } 901 | }, 902 | "parameters": { 903 | "vnet": { 904 | "type": "bool", 905 | "metadata": { 906 | "description": "Deploy in VNet" 907 | } 908 | }, 909 | "storageSkuName": { 910 | "type": "string", 911 | "metadata": { 912 | "description": "SKU Name for the Azure Storage Account" 913 | } 914 | }, 915 | "location": { 916 | "type": "string", 917 | "metadata": { 918 | "description": "Location for all resources." 919 | } 920 | }, 921 | "webAppOutboundIpAdresses": { 922 | "type": "string", 923 | "metadata": { 924 | "description": "Outbound IP adresses of CTF Web App. Required for the non-vnet scenario" 925 | } 926 | }, 927 | "storageAccountName": { 928 | "type": "string", 929 | "metadata": { 930 | "description": "Account Name for the Azure Storage Account" 931 | } 932 | } 933 | }, 934 | "variables": { 935 | "networkAcls": "[if(parameters('vnet'), createObject('defaultAction', 'Deny', 'bypass', 'AzureServices'), createObject('defaultAction', 'Allow', 'ipRules', map(split(parameters('webAppOutboundIpAdresses'), ','), lambda('ip', createObject('value', lambdaVariables('ip'))))))]" 936 | }, 937 | "resources": [ 938 | { 939 | "type": "Microsoft.Storage/storageAccounts", 940 | "apiVersion": "2023-05-01", 941 | "name": "[parameters('storageAccountName')]", 942 | "location": "[parameters('location')]", 943 | "sku": { 944 | "name": "[parameters('storageSkuName')]" 945 | }, 946 | "kind": "StorageV2", 947 | "properties": { 948 | "networkAcls": "[variables('networkAcls')]" 949 | } 950 | } 951 | ] 952 | } 953 | }, 954 | "dependsOn": [ 955 | "[resourceId('Microsoft.Resources/deployments', 'ctfDeploy')]" 956 | ] 957 | }, 958 | { 959 | "type": "Microsoft.Resources/deployments", 960 | "apiVersion": "2022-09-01", 961 | "name": "ctfDeploy", 962 | "properties": { 963 | "expressionEvaluationOptions": { 964 | "scope": "inner" 965 | }, 966 | "mode": "Incremental", 967 | "parameters": { 968 | "virtualNetworkName": { 969 | "value": "[variables('virtualNetworkName')]" 970 | }, 971 | "location": { 972 | "value": "[parameters('resourcesLocation')]" 973 | }, 974 | "appServicePlanSkuName": { 975 | "value": "[parameters('appServicePlanSkuName')]" 976 | }, 977 | "keyVaultName": { 978 | "value": "[variables('keyVaultName')]" 979 | }, 980 | "ctfCacheSecretName": { 981 | "value": "[variables('ctfCacheSecretName')]" 982 | }, 983 | "ctfDatabaseSecretName": { 984 | "value": "[variables('ctfDatabaseSecretName')]" 985 | }, 986 | "publicResourcesSubnetName": { 987 | "value": "[variables('publicResourcesSubnetName')]" 988 | }, 989 | "webAppName": { 990 | "value": "[parameters('webAppName')]" 991 | }, 992 | "logAnalyticsWorkspaceId": { 993 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy'), '2022-09-01').outputs.logAnalyticsWorkspaceId.value]" 994 | }, 995 | "acrImageName": { 996 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acrDeploy'), '2022-09-01').outputs.acrImage.value]" 997 | }, 998 | "registryName": { 999 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acrDeploy'), '2022-09-01').outputs.registryName.value]" 1000 | }, 1001 | "managedIdentityClientId": { 1002 | "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id))), '2023-01-31').clientId]" 1003 | }, 1004 | "managedIdentityId": { 1005 | "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id)))]" 1006 | }, 1007 | "storageAccountName": { 1008 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'ctfdFileStorage'), '2022-09-01').outputs.storageAccountName.value]" 1009 | }, 1010 | "vnet": { 1011 | "value": "[parameters('vnet')]" 1012 | } 1013 | }, 1014 | "template": { 1015 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1016 | "contentVersion": "1.0.0.0", 1017 | "metadata": { 1018 | "_generator": { 1019 | "name": "bicep", 1020 | "version": "0.30.23.60470", 1021 | "templateHash": "3807116930661632967" 1022 | } 1023 | }, 1024 | "parameters": { 1025 | "vnet": { 1026 | "type": "bool", 1027 | "metadata": { 1028 | "description": "Deploy in VNet" 1029 | } 1030 | }, 1031 | "webAppName": { 1032 | "type": "string", 1033 | "metadata": { 1034 | "description": "Name for Azure Web app" 1035 | } 1036 | }, 1037 | "location": { 1038 | "type": "string", 1039 | "metadata": { 1040 | "description": "Location for all resources." 1041 | } 1042 | }, 1043 | "virtualNetworkName": { 1044 | "type": "string", 1045 | "metadata": { 1046 | "description": "Name of the VNet" 1047 | } 1048 | }, 1049 | "publicResourcesSubnetName": { 1050 | "type": "string", 1051 | "metadata": { 1052 | "description": "Name of the public subnet" 1053 | } 1054 | }, 1055 | "keyVaultName": { 1056 | "type": "string", 1057 | "metadata": { 1058 | "description": "Name of azure key vault" 1059 | } 1060 | }, 1061 | "logAnalyticsWorkspaceId": { 1062 | "type": "string", 1063 | "metadata": { 1064 | "description": "Log Anaytics Workspace Id" 1065 | } 1066 | }, 1067 | "appServicePlanSkuName": { 1068 | "type": "string", 1069 | "metadata": { 1070 | "description": "App Service Plan SKU name" 1071 | } 1072 | }, 1073 | "acrImageName": { 1074 | "type": "string", 1075 | "metadata": { 1076 | "description": "Azure Container Registry Image name" 1077 | } 1078 | }, 1079 | "registryName": { 1080 | "type": "string", 1081 | "metadata": { 1082 | "description": "Azure Container Registry name" 1083 | } 1084 | }, 1085 | "ctfCacheSecretName": { 1086 | "type": "string", 1087 | "metadata": { 1088 | "description": "Name of the key vault secret holding the cache connection string" 1089 | } 1090 | }, 1091 | "ctfDatabaseSecretName": { 1092 | "type": "string", 1093 | "metadata": { 1094 | "description": "Name of the key vault secret holding the database connection string" 1095 | } 1096 | }, 1097 | "managedIdentityClientId": { 1098 | "type": "string", 1099 | "metadata": { 1100 | "description": "CTF managed identity client ID" 1101 | } 1102 | }, 1103 | "managedIdentityId": { 1104 | "type": "string", 1105 | "metadata": { 1106 | "description": "CTF managed identity ID" 1107 | } 1108 | }, 1109 | "storageAccountName": { 1110 | "type": "string", 1111 | "metadata": { 1112 | "description": "Storage Account Name" 1113 | } 1114 | }, 1115 | "shareName": { 1116 | "type": "string", 1117 | "defaultValue": "uploads", 1118 | "metadata": { 1119 | "description": "Storage Account File Share Name" 1120 | } 1121 | }, 1122 | "storageMountPath": { 1123 | "type": "string", 1124 | "defaultValue": "/opt/CTFd/CTFd/uploads", 1125 | "metadata": { 1126 | "description": "Storage Account File Share Name" 1127 | } 1128 | } 1129 | }, 1130 | "variables": { 1131 | "appServicePlanName": "[format('ctfd-server-{0}', uniqueString(resourceGroup().id))]" 1132 | }, 1133 | "resources": [ 1134 | { 1135 | "type": "Microsoft.Web/serverfarms", 1136 | "apiVersion": "2022-09-01", 1137 | "name": "[variables('appServicePlanName')]", 1138 | "location": "[parameters('location')]", 1139 | "kind": "linux", 1140 | "properties": { 1141 | "reserved": true 1142 | }, 1143 | "sku": { 1144 | "name": "[parameters('appServicePlanSkuName')]" 1145 | } 1146 | }, 1147 | { 1148 | "type": "Microsoft.Web/sites", 1149 | "apiVersion": "2022-09-01", 1150 | "name": "[parameters('webAppName')]", 1151 | "location": "[parameters('location')]", 1152 | "tags": {}, 1153 | "identity": { 1154 | "type": "UserAssigned", 1155 | "userAssignedIdentities": { 1156 | "[format('{0}', parameters('managedIdentityId'))]": {} 1157 | } 1158 | }, 1159 | "properties": { 1160 | "virtualNetworkSubnetId": "[if(parameters('vnet'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('publicResourcesSubnetName')), null())]", 1161 | "keyVaultReferenceIdentity": "[parameters('managedIdentityId')]", 1162 | "vnetRouteAllEnabled": "[if(parameters('vnet'), true(), false())]", 1163 | "siteConfig": { 1164 | "acrUseManagedIdentityCreds": true, 1165 | "acrUserManagedIdentityID": "[parameters('managedIdentityClientId')]", 1166 | "appSettings": [ 1167 | { 1168 | "name": "DATABASE_URL", 1169 | "value": "[format('@Microsoft.KeyVault(SecretUri=https://{0}.vault.azure.net/secrets/{1}/)', parameters('keyVaultName'), parameters('ctfDatabaseSecretName'))]" 1170 | }, 1171 | { 1172 | "name": "REDIS_URL", 1173 | "value": "[format('@Microsoft.KeyVault(SecretUri=https://{0}.vault.azure.net/secrets/{1}/)', parameters('keyVaultName'), parameters('ctfCacheSecretName'))]" 1174 | }, 1175 | { 1176 | "name": "REVERSE_PROXY", 1177 | "value": "False" 1178 | }, 1179 | { 1180 | "name": "WEBSITES_PORT", 1181 | "value": "8000" 1182 | }, 1183 | { 1184 | "name": "DOCKER_REGISTRY_SERVER_URL", 1185 | "value": "[format('{0}.azurecr.io', parameters('registryName'))]" 1186 | } 1187 | ], 1188 | "linuxFxVersion": "[format('DOCKER|{0}', parameters('acrImageName'))]", 1189 | "azureStorageAccounts": { 1190 | "[format('{0}', parameters('shareName'))]": { 1191 | "type": "AzureFiles", 1192 | "shareName": "[parameters('shareName')]", 1193 | "mountPath": "[parameters('storageMountPath')]", 1194 | "accountName": "[parameters('storageAccountName')]", 1195 | "accessKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2023-05-01').keys[0].value]" 1196 | } 1197 | } 1198 | }, 1199 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" 1200 | }, 1201 | "dependsOn": [ 1202 | "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" 1203 | ] 1204 | }, 1205 | { 1206 | "type": "Microsoft.Web/sites/config", 1207 | "apiVersion": "2022-09-01", 1208 | "name": "[format('{0}/{1}', parameters('webAppName'), 'logs')]", 1209 | "properties": { 1210 | "applicationLogs": { 1211 | "fileSystem": { 1212 | "level": "Warning" 1213 | } 1214 | }, 1215 | "httpLogs": { 1216 | "fileSystem": { 1217 | "retentionInMb": 40, 1218 | "retentionInDays": 5, 1219 | "enabled": true 1220 | } 1221 | }, 1222 | "failedRequestsTracing": { 1223 | "enabled": true 1224 | }, 1225 | "detailedErrorMessages": { 1226 | "enabled": true 1227 | } 1228 | }, 1229 | "dependsOn": [ 1230 | "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" 1231 | ] 1232 | }, 1233 | { 1234 | "type": "Microsoft.Insights/diagnosticSettings", 1235 | "apiVersion": "2021-05-01-preview", 1236 | "scope": "[format('Microsoft.Web/sites/{0}', parameters('webAppName'))]", 1237 | "name": "[format('{0}-diagnostics', parameters('webAppName'))]", 1238 | "properties": { 1239 | "logs": [ 1240 | { 1241 | "category": "AppServiceHTTPLogs", 1242 | "categoryGroup": null, 1243 | "enabled": true, 1244 | "retentionPolicy": { 1245 | "days": 5, 1246 | "enabled": false 1247 | } 1248 | }, 1249 | { 1250 | "category": "AppServiceConsoleLogs", 1251 | "categoryGroup": null, 1252 | "enabled": true, 1253 | "retentionPolicy": { 1254 | "days": 5, 1255 | "enabled": false 1256 | } 1257 | }, 1258 | { 1259 | "category": "AppServiceAppLogs", 1260 | "categoryGroup": null, 1261 | "enabled": true, 1262 | "retentionPolicy": { 1263 | "days": 5, 1264 | "enabled": false 1265 | } 1266 | }, 1267 | { 1268 | "category": "AppServiceAuditLogs", 1269 | "categoryGroup": null, 1270 | "enabled": true, 1271 | "retentionPolicy": { 1272 | "days": 5, 1273 | "enabled": false 1274 | } 1275 | }, 1276 | { 1277 | "category": "AppServicePlatformLogs", 1278 | "categoryGroup": null, 1279 | "enabled": true, 1280 | "retentionPolicy": { 1281 | "days": 5, 1282 | "enabled": false 1283 | } 1284 | } 1285 | ], 1286 | "workspaceId": "[parameters('logAnalyticsWorkspaceId')]" 1287 | }, 1288 | "dependsOn": [ 1289 | "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" 1290 | ] 1291 | } 1292 | ], 1293 | "outputs": { 1294 | "outboundIpAdresses": { 1295 | "type": "string", 1296 | "value": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-09-01').outboundIpAddresses]" 1297 | } 1298 | } 1299 | } 1300 | }, 1301 | "dependsOn": [ 1302 | "[resourceId('Microsoft.Resources/deployments', 'acrDeploy')]", 1303 | "[resourceId('Microsoft.Resources/deployments', 'ctfdFileStorage')]", 1304 | "[resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy')]", 1305 | "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id)))]" 1306 | ], 1307 | "metadata": { 1308 | "description": "Deploys Azure App Service for containers" 1309 | } 1310 | }, 1311 | { 1312 | "type": "Microsoft.Resources/deployments", 1313 | "apiVersion": "2022-09-01", 1314 | "name": "keyVaultDeploy", 1315 | "properties": { 1316 | "expressionEvaluationOptions": { 1317 | "scope": "inner" 1318 | }, 1319 | "mode": "Incremental", 1320 | "parameters": { 1321 | "location": { 1322 | "value": "[parameters('resourcesLocation')]" 1323 | }, 1324 | "readerPrincipalId": { 1325 | "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id))), '2023-01-31').principalId]" 1326 | }, 1327 | "internalResourcesSubnetName": { 1328 | "value": "[variables('internalResourcesSubnetName')]" 1329 | }, 1330 | "virtualNetworkName": { 1331 | "value": "[variables('virtualNetworkName')]" 1332 | }, 1333 | "logAnalyticsWorkspaceId": { 1334 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy'), '2022-09-01').outputs.logAnalyticsWorkspaceId.value]" 1335 | }, 1336 | "vnet": { 1337 | "value": "[parameters('vnet')]" 1338 | }, 1339 | "keyVaultName": { 1340 | "value": "[variables('keyVaultName')]" 1341 | }, 1342 | "webAppOutboundIpAdresses": { 1343 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'ctfDeploy'), '2022-09-01').outputs.outboundIpAdresses.value]" 1344 | } 1345 | }, 1346 | "template": { 1347 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1348 | "contentVersion": "1.0.0.0", 1349 | "metadata": { 1350 | "_generator": { 1351 | "name": "bicep", 1352 | "version": "0.30.23.60470", 1353 | "templateHash": "1933883236150725549" 1354 | } 1355 | }, 1356 | "parameters": { 1357 | "vnet": { 1358 | "type": "bool", 1359 | "metadata": { 1360 | "description": "Deploy in VNet" 1361 | } 1362 | }, 1363 | "location": { 1364 | "type": "string", 1365 | "metadata": { 1366 | "description": "Location for all resources." 1367 | } 1368 | }, 1369 | "readerPrincipalId": { 1370 | "type": "string", 1371 | "metadata": { 1372 | "description": "Specifies the object ID of a user, service principal or security group in the Azure Active Directory tenant for the vault. The object ID must be unique for the list of access policies. Get it by using Get-AzADUser or Get-AzADServicePrincipal cmdlets." 1373 | } 1374 | }, 1375 | "skuName": { 1376 | "type": "string", 1377 | "defaultValue": "standard", 1378 | "allowedValues": [ 1379 | "standard", 1380 | "premium" 1381 | ], 1382 | "metadata": { 1383 | "description": "Specifies whether the key vault is a standard vault or a premium vault." 1384 | } 1385 | }, 1386 | "virtualNetworkName": { 1387 | "type": "string", 1388 | "metadata": { 1389 | "description": "Name of the VNet" 1390 | } 1391 | }, 1392 | "internalResourcesSubnetName": { 1393 | "type": "string", 1394 | "metadata": { 1395 | "description": "Name of the internal resources subnet" 1396 | } 1397 | }, 1398 | "keyVaultName": { 1399 | "type": "string", 1400 | "metadata": { 1401 | "description": "Name of Azure Key Vault" 1402 | } 1403 | }, 1404 | "logAnalyticsWorkspaceId": { 1405 | "type": "string", 1406 | "metadata": { 1407 | "description": "Log Anaytics Workspace Id" 1408 | } 1409 | }, 1410 | "webAppOutboundIpAdresses": { 1411 | "type": "string", 1412 | "metadata": { 1413 | "description": "Outbound IP adresses of CTF Web App. Required for the non-vnet scenario" 1414 | } 1415 | } 1416 | }, 1417 | "variables": { 1418 | "tenantId": "[subscription().tenantId]", 1419 | "networkAcls": "[if(parameters('vnet'), createObject('defaultAction', 'Deny', 'bypass', 'AzureServices'), createObject('defaultAction', 'Allow', 'ipRules', map(split(parameters('webAppOutboundIpAdresses'), ','), lambda('ip', createObject('value', lambdaVariables('ip'))))))]" 1420 | }, 1421 | "resources": [ 1422 | { 1423 | "type": "Microsoft.KeyVault/vaults", 1424 | "apiVersion": "2023-07-01", 1425 | "name": "[parameters('keyVaultName')]", 1426 | "location": "[parameters('location')]", 1427 | "properties": { 1428 | "tenantId": "[variables('tenantId')]", 1429 | "publicNetworkAccess": "[if(parameters('vnet'), 'Disabled', 'Enabled')]", 1430 | "enableRbacAuthorization": true, 1431 | "sku": { 1432 | "name": "[parameters('skuName')]", 1433 | "family": "A" 1434 | }, 1435 | "networkAcls": "[variables('networkAcls')]" 1436 | } 1437 | }, 1438 | { 1439 | "type": "Microsoft.Authorization/roleAssignments", 1440 | "apiVersion": "2020-04-01-preview", 1441 | "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('keyVaultName'))]", 1442 | "name": "[guid('4633458b-17de-408a-b874-0445c86b69e6', parameters('readerPrincipalId'), resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')))]", 1443 | "properties": { 1444 | "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", 1445 | "principalId": "[parameters('readerPrincipalId')]", 1446 | "principalType": "ServicePrincipal" 1447 | }, 1448 | "dependsOn": [ 1449 | "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" 1450 | ] 1451 | }, 1452 | { 1453 | "type": "Microsoft.Insights/diagnosticSettings", 1454 | "apiVersion": "2021-05-01-preview", 1455 | "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('keyVaultName'))]", 1456 | "name": "[format('{0}-diagnostics', parameters('keyVaultName'))]", 1457 | "properties": { 1458 | "logs": [ 1459 | { 1460 | "category": null, 1461 | "categoryGroup": "audit", 1462 | "enabled": true, 1463 | "retentionPolicy": { 1464 | "days": 5, 1465 | "enabled": false 1466 | } 1467 | }, 1468 | { 1469 | "category": null, 1470 | "categoryGroup": "allLogs", 1471 | "enabled": true, 1472 | "retentionPolicy": { 1473 | "days": 5, 1474 | "enabled": false 1475 | } 1476 | } 1477 | ], 1478 | "workspaceId": "[parameters('logAnalyticsWorkspaceId')]" 1479 | }, 1480 | "dependsOn": [ 1481 | "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" 1482 | ] 1483 | }, 1484 | { 1485 | "condition": "[parameters('vnet')]", 1486 | "type": "Microsoft.Resources/deployments", 1487 | "apiVersion": "2022-09-01", 1488 | "name": "keyVaultPrivateEndpointDeploy", 1489 | "properties": { 1490 | "expressionEvaluationOptions": { 1491 | "scope": "inner" 1492 | }, 1493 | "mode": "Incremental", 1494 | "parameters": { 1495 | "virtualNetworkName": { 1496 | "value": "[parameters('virtualNetworkName')]" 1497 | }, 1498 | "subnetName": { 1499 | "value": "[parameters('internalResourcesSubnetName')]" 1500 | }, 1501 | "resuorceId": { 1502 | "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" 1503 | }, 1504 | "resuorceGroupId": { 1505 | "value": "vault" 1506 | }, 1507 | "privateDnsZoneName": { 1508 | "value": "privatelink.vaultcore.azure.net" 1509 | }, 1510 | "privateEndpointName": { 1511 | "value": "keyvault_private_endpoint" 1512 | }, 1513 | "location": { 1514 | "value": "[parameters('location')]" 1515 | } 1516 | }, 1517 | "template": { 1518 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1519 | "contentVersion": "1.0.0.0", 1520 | "metadata": { 1521 | "_generator": { 1522 | "name": "bicep", 1523 | "version": "0.30.23.60470", 1524 | "templateHash": "8939390758381320725" 1525 | } 1526 | }, 1527 | "parameters": { 1528 | "virtualNetworkName": { 1529 | "type": "string", 1530 | "metadata": { 1531 | "description": "Name of the VNet" 1532 | } 1533 | }, 1534 | "subnetName": { 1535 | "type": "string", 1536 | "metadata": { 1537 | "description": "Name of the subnet" 1538 | } 1539 | }, 1540 | "resuorceId": { 1541 | "type": "string", 1542 | "metadata": { 1543 | "description": "Id of the resource" 1544 | } 1545 | }, 1546 | "resuorceGroupId": { 1547 | "type": "string", 1548 | "metadata": { 1549 | "description": "Group Id of the resource" 1550 | } 1551 | }, 1552 | "privateDnsZoneName": { 1553 | "type": "string", 1554 | "metadata": { 1555 | "description": "Name of dns zone" 1556 | } 1557 | }, 1558 | "privateEndpointName": { 1559 | "type": "string", 1560 | "metadata": { 1561 | "description": "Name of private endpoint" 1562 | } 1563 | }, 1564 | "location": { 1565 | "type": "string", 1566 | "metadata": { 1567 | "description": "Location for all resources." 1568 | } 1569 | } 1570 | }, 1571 | "resources": [ 1572 | { 1573 | "type": "Microsoft.Network/privateEndpoints", 1574 | "apiVersion": "2024-03-01", 1575 | "name": "[parameters('privateEndpointName')]", 1576 | "location": "[parameters('location')]", 1577 | "properties": { 1578 | "subnet": { 1579 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" 1580 | }, 1581 | "privateLinkServiceConnections": [ 1582 | { 1583 | "name": "[parameters('privateEndpointName')]", 1584 | "properties": { 1585 | "privateLinkServiceId": "[parameters('resuorceId')]", 1586 | "groupIds": [ 1587 | "[parameters('resuorceGroupId')]" 1588 | ] 1589 | } 1590 | } 1591 | ] 1592 | } 1593 | }, 1594 | { 1595 | "type": "Microsoft.Network/privateDnsZones", 1596 | "apiVersion": "2024-06-01", 1597 | "name": "[parameters('privateDnsZoneName')]", 1598 | "location": "global", 1599 | "properties": {} 1600 | }, 1601 | { 1602 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 1603 | "apiVersion": "2024-06-01", 1604 | "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), format('{0}-link', parameters('privateDnsZoneName')))]", 1605 | "location": "global", 1606 | "properties": { 1607 | "registrationEnabled": false, 1608 | "virtualNetwork": { 1609 | "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" 1610 | } 1611 | }, 1612 | "dependsOn": [ 1613 | "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]" 1614 | ] 1615 | }, 1616 | { 1617 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 1618 | "apiVersion": "2024-03-01", 1619 | "name": "[format('{0}/{1}', parameters('privateEndpointName'), 'default')]", 1620 | "properties": { 1621 | "privateDnsZoneConfigs": [ 1622 | { 1623 | "name": "config1", 1624 | "properties": { 1625 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]" 1626 | } 1627 | } 1628 | ] 1629 | }, 1630 | "dependsOn": [ 1631 | "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]", 1632 | "[resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpointName'))]" 1633 | ] 1634 | } 1635 | ] 1636 | } 1637 | }, 1638 | "dependsOn": [ 1639 | "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" 1640 | ] 1641 | } 1642 | ], 1643 | "outputs": { 1644 | "keyVaultName": { 1645 | "type": "string", 1646 | "value": "[parameters('keyVaultName')]" 1647 | } 1648 | } 1649 | } 1650 | }, 1651 | "dependsOn": [ 1652 | "[resourceId('Microsoft.Resources/deployments', 'ctfDeploy')]", 1653 | "[resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy')]", 1654 | "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('ctf-mi-{0}', uniqueString(resourceGroup().id)))]" 1655 | ], 1656 | "metadata": { 1657 | "description": "Deploys Azure Key Vault" 1658 | } 1659 | }, 1660 | { 1661 | "type": "Microsoft.Resources/deployments", 1662 | "apiVersion": "2022-09-01", 1663 | "name": "redisDeploy", 1664 | "properties": { 1665 | "expressionEvaluationOptions": { 1666 | "scope": "inner" 1667 | }, 1668 | "mode": "Incremental", 1669 | "parameters": { 1670 | "internalResourcesSubnetName": { 1671 | "value": "[variables('internalResourcesSubnetName')]" 1672 | }, 1673 | "virtualNetworkName": { 1674 | "value": "[variables('virtualNetworkName')]" 1675 | }, 1676 | "location": { 1677 | "value": "[parameters('resourcesLocation')]" 1678 | }, 1679 | "vnet": { 1680 | "value": "[parameters('vnet')]" 1681 | }, 1682 | "ctfCacheSecretName": { 1683 | "value": "[variables('ctfCacheSecretName')]" 1684 | }, 1685 | "keyVaultName": { 1686 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keyVaultDeploy'), '2022-09-01').outputs.keyVaultName.value]" 1687 | }, 1688 | "redisSkuName": { 1689 | "value": "[parameters('redisSkuName')]" 1690 | }, 1691 | "redisSkuSize": { 1692 | "value": "[parameters('redisSkuSize')]" 1693 | }, 1694 | "logAnalyticsWorkspaceId": { 1695 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy'), '2022-09-01').outputs.logAnalyticsWorkspaceId.value]" 1696 | } 1697 | }, 1698 | "template": { 1699 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1700 | "contentVersion": "1.0.0.0", 1701 | "metadata": { 1702 | "_generator": { 1703 | "name": "bicep", 1704 | "version": "0.30.23.60470", 1705 | "templateHash": "11875769902277335199" 1706 | } 1707 | }, 1708 | "parameters": { 1709 | "vnet": { 1710 | "type": "bool", 1711 | "metadata": { 1712 | "description": "Deploy in VNet" 1713 | } 1714 | }, 1715 | "redisSkuName": { 1716 | "type": "string", 1717 | "metadata": { 1718 | "description": "SKU Name for Azure cache for Redis" 1719 | } 1720 | }, 1721 | "redisSkuSize": { 1722 | "type": "int", 1723 | "metadata": { 1724 | "description": "The size of the Redis cache" 1725 | } 1726 | }, 1727 | "virtualNetworkName": { 1728 | "type": "string", 1729 | "metadata": { 1730 | "description": "Name of the VNet" 1731 | } 1732 | }, 1733 | "internalResourcesSubnetName": { 1734 | "type": "string", 1735 | "metadata": { 1736 | "description": "Name of the internal resources subnet" 1737 | } 1738 | }, 1739 | "keyVaultName": { 1740 | "type": "string", 1741 | "metadata": { 1742 | "description": "Name of the key vault" 1743 | } 1744 | }, 1745 | "ctfCacheSecretName": { 1746 | "type": "string", 1747 | "metadata": { 1748 | "description": "Name of the connection string secret" 1749 | } 1750 | }, 1751 | "location": { 1752 | "type": "string", 1753 | "metadata": { 1754 | "description": "Location for all resources." 1755 | } 1756 | }, 1757 | "logAnalyticsWorkspaceId": { 1758 | "type": "string", 1759 | "metadata": { 1760 | "description": "Log Anaytics Workspace Id" 1761 | } 1762 | } 1763 | }, 1764 | "variables": { 1765 | "redisServerName": "[format('ctfd-redis-{0}', uniqueString(resourceGroup().id))]", 1766 | "family": "[if(or(equals(parameters('redisSkuName'), 'Basic'), equals(parameters('redisSkuName'), 'Standard')), 'C', 'P')]" 1767 | }, 1768 | "resources": [ 1769 | { 1770 | "type": "Microsoft.Cache/redis", 1771 | "apiVersion": "2023-08-01", 1772 | "name": "[variables('redisServerName')]", 1773 | "location": "[parameters('location')]", 1774 | "properties": { 1775 | "publicNetworkAccess": "[if(parameters('vnet'), 'Disabled', 'Enabled')]", 1776 | "sku": { 1777 | "capacity": "[parameters('redisSkuSize')]", 1778 | "family": "[variables('family')]", 1779 | "name": "[parameters('redisSkuName')]" 1780 | } 1781 | } 1782 | }, 1783 | { 1784 | "type": "Microsoft.Insights/diagnosticSettings", 1785 | "apiVersion": "2021-05-01-preview", 1786 | "scope": "[format('Microsoft.Cache/redis/{0}', variables('redisServerName'))]", 1787 | "name": "[format('{0}-diagnostics', variables('redisServerName'))]", 1788 | "properties": { 1789 | "logs": [ 1790 | { 1791 | "category": null, 1792 | "categoryGroup": "audit", 1793 | "enabled": true, 1794 | "retentionPolicy": { 1795 | "days": 5, 1796 | "enabled": false 1797 | } 1798 | }, 1799 | { 1800 | "category": null, 1801 | "categoryGroup": "allLogs", 1802 | "enabled": true, 1803 | "retentionPolicy": { 1804 | "days": 5, 1805 | "enabled": false 1806 | } 1807 | } 1808 | ], 1809 | "workspaceId": "[parameters('logAnalyticsWorkspaceId')]" 1810 | }, 1811 | "dependsOn": [ 1812 | "[resourceId('Microsoft.Cache/redis', variables('redisServerName'))]" 1813 | ] 1814 | }, 1815 | { 1816 | "condition": "[parameters('vnet')]", 1817 | "type": "Microsoft.Resources/deployments", 1818 | "apiVersion": "2022-09-01", 1819 | "name": "redisPrivateEndpointDeploy", 1820 | "properties": { 1821 | "expressionEvaluationOptions": { 1822 | "scope": "inner" 1823 | }, 1824 | "mode": "Incremental", 1825 | "parameters": { 1826 | "virtualNetworkName": { 1827 | "value": "[parameters('virtualNetworkName')]" 1828 | }, 1829 | "subnetName": { 1830 | "value": "[parameters('internalResourcesSubnetName')]" 1831 | }, 1832 | "resuorceId": { 1833 | "value": "[resourceId('Microsoft.Cache/redis', variables('redisServerName'))]" 1834 | }, 1835 | "resuorceGroupId": { 1836 | "value": "redisCache" 1837 | }, 1838 | "privateDnsZoneName": { 1839 | "value": "privatelink.redis.cache.windows.net" 1840 | }, 1841 | "privateEndpointName": { 1842 | "value": "redis_private_endpoint" 1843 | }, 1844 | "location": { 1845 | "value": "[parameters('location')]" 1846 | } 1847 | }, 1848 | "template": { 1849 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1850 | "contentVersion": "1.0.0.0", 1851 | "metadata": { 1852 | "_generator": { 1853 | "name": "bicep", 1854 | "version": "0.30.23.60470", 1855 | "templateHash": "8939390758381320725" 1856 | } 1857 | }, 1858 | "parameters": { 1859 | "virtualNetworkName": { 1860 | "type": "string", 1861 | "metadata": { 1862 | "description": "Name of the VNet" 1863 | } 1864 | }, 1865 | "subnetName": { 1866 | "type": "string", 1867 | "metadata": { 1868 | "description": "Name of the subnet" 1869 | } 1870 | }, 1871 | "resuorceId": { 1872 | "type": "string", 1873 | "metadata": { 1874 | "description": "Id of the resource" 1875 | } 1876 | }, 1877 | "resuorceGroupId": { 1878 | "type": "string", 1879 | "metadata": { 1880 | "description": "Group Id of the resource" 1881 | } 1882 | }, 1883 | "privateDnsZoneName": { 1884 | "type": "string", 1885 | "metadata": { 1886 | "description": "Name of dns zone" 1887 | } 1888 | }, 1889 | "privateEndpointName": { 1890 | "type": "string", 1891 | "metadata": { 1892 | "description": "Name of private endpoint" 1893 | } 1894 | }, 1895 | "location": { 1896 | "type": "string", 1897 | "metadata": { 1898 | "description": "Location for all resources." 1899 | } 1900 | } 1901 | }, 1902 | "resources": [ 1903 | { 1904 | "type": "Microsoft.Network/privateEndpoints", 1905 | "apiVersion": "2024-03-01", 1906 | "name": "[parameters('privateEndpointName')]", 1907 | "location": "[parameters('location')]", 1908 | "properties": { 1909 | "subnet": { 1910 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" 1911 | }, 1912 | "privateLinkServiceConnections": [ 1913 | { 1914 | "name": "[parameters('privateEndpointName')]", 1915 | "properties": { 1916 | "privateLinkServiceId": "[parameters('resuorceId')]", 1917 | "groupIds": [ 1918 | "[parameters('resuorceGroupId')]" 1919 | ] 1920 | } 1921 | } 1922 | ] 1923 | } 1924 | }, 1925 | { 1926 | "type": "Microsoft.Network/privateDnsZones", 1927 | "apiVersion": "2024-06-01", 1928 | "name": "[parameters('privateDnsZoneName')]", 1929 | "location": "global", 1930 | "properties": {} 1931 | }, 1932 | { 1933 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 1934 | "apiVersion": "2024-06-01", 1935 | "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), format('{0}-link', parameters('privateDnsZoneName')))]", 1936 | "location": "global", 1937 | "properties": { 1938 | "registrationEnabled": false, 1939 | "virtualNetwork": { 1940 | "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" 1941 | } 1942 | }, 1943 | "dependsOn": [ 1944 | "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]" 1945 | ] 1946 | }, 1947 | { 1948 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 1949 | "apiVersion": "2024-03-01", 1950 | "name": "[format('{0}/{1}', parameters('privateEndpointName'), 'default')]", 1951 | "properties": { 1952 | "privateDnsZoneConfigs": [ 1953 | { 1954 | "name": "config1", 1955 | "properties": { 1956 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]" 1957 | } 1958 | } 1959 | ] 1960 | }, 1961 | "dependsOn": [ 1962 | "[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZoneName'))]", 1963 | "[resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpointName'))]" 1964 | ] 1965 | } 1966 | ] 1967 | } 1968 | }, 1969 | "dependsOn": [ 1970 | "[resourceId('Microsoft.Cache/redis', variables('redisServerName'))]" 1971 | ] 1972 | }, 1973 | { 1974 | "type": "Microsoft.Resources/deployments", 1975 | "apiVersion": "2022-09-01", 1976 | "name": "redisKeyDeploy", 1977 | "properties": { 1978 | "expressionEvaluationOptions": { 1979 | "scope": "inner" 1980 | }, 1981 | "mode": "Incremental", 1982 | "parameters": { 1983 | "keyVaultName": { 1984 | "value": "[parameters('keyVaultName')]" 1985 | }, 1986 | "secretName": { 1987 | "value": "[parameters('ctfCacheSecretName')]" 1988 | }, 1989 | "secretValue": { 1990 | "value": "[format('rediss://:{0}@{1}.redis.cache.windows.net:6380', listKeys(resourceId('Microsoft.Cache/redis', variables('redisServerName')), '2023-08-01').primaryKey, variables('redisServerName'))]" 1991 | } 1992 | }, 1993 | "template": { 1994 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 1995 | "contentVersion": "1.0.0.0", 1996 | "metadata": { 1997 | "_generator": { 1998 | "name": "bicep", 1999 | "version": "0.30.23.60470", 2000 | "templateHash": "3629446120964483422" 2001 | } 2002 | }, 2003 | "parameters": { 2004 | "keyVaultName": { 2005 | "type": "string", 2006 | "metadata": { 2007 | "description": "Name of Azure Key Vault" 2008 | } 2009 | }, 2010 | "secretName": { 2011 | "type": "string", 2012 | "metadata": { 2013 | "description": "Name of the secret" 2014 | } 2015 | }, 2016 | "secretValue": { 2017 | "type": "securestring", 2018 | "metadata": { 2019 | "description": "Value of the secret" 2020 | } 2021 | } 2022 | }, 2023 | "resources": [ 2024 | { 2025 | "type": "Microsoft.KeyVault/vaults/secrets", 2026 | "apiVersion": "2023-07-01", 2027 | "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretName'))]", 2028 | "properties": { 2029 | "value": "[parameters('secretValue')]" 2030 | } 2031 | } 2032 | ] 2033 | } 2034 | }, 2035 | "dependsOn": [ 2036 | "[resourceId('Microsoft.Cache/redis', variables('redisServerName'))]" 2037 | ] 2038 | } 2039 | ] 2040 | } 2041 | }, 2042 | "dependsOn": [ 2043 | "[resourceId('Microsoft.Resources/deployments', 'keyVaultDeploy')]", 2044 | "[resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy')]" 2045 | ], 2046 | "metadata": { 2047 | "description": "Deploys Azure Cache for Redis and a Key Vault secret with its connection string" 2048 | } 2049 | }, 2050 | { 2051 | "type": "Microsoft.Resources/deployments", 2052 | "apiVersion": "2022-09-01", 2053 | "name": "mysqlDbDeploy", 2054 | "properties": { 2055 | "expressionEvaluationOptions": { 2056 | "scope": "inner" 2057 | }, 2058 | "mode": "Incremental", 2059 | "parameters": { 2060 | "administratorLogin": { 2061 | "value": "[parameters('administratorLogin')]" 2062 | }, 2063 | "administratorLoginPassword": { 2064 | "value": "[parameters('administratorLoginPassword')]" 2065 | }, 2066 | "vnetId": "[if(parameters('vnet'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'vnetDeploy'), '2022-09-01').outputs.virtualNetworkId.value), createObject('value', ''))]", 2067 | "databaseSubnetId": "[if(parameters('vnet'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'vnetDeploy'), '2022-09-01').outputs.databaseResourcesSubnetId.value), createObject('value', ''))]", 2068 | "virtualNetworkName": { 2069 | "value": "[variables('virtualNetworkName')]" 2070 | }, 2071 | "location": { 2072 | "value": "[parameters('resourcesLocation')]" 2073 | }, 2074 | "vnet": { 2075 | "value": "[parameters('vnet')]" 2076 | }, 2077 | "ctfDbSecretName": { 2078 | "value": "[variables('ctfDatabaseSecretName')]" 2079 | }, 2080 | "keyVaultName": { 2081 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keyVaultDeploy'), '2022-09-01').outputs.keyVaultName.value]" 2082 | }, 2083 | "mysqlWorkloadType": { 2084 | "value": "[parameters('mysqlType')]" 2085 | }, 2086 | "logAnalyticsWorkspaceId": { 2087 | "value": "[reference(resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy'), '2022-09-01').outputs.logAnalyticsWorkspaceId.value]" 2088 | } 2089 | }, 2090 | "template": { 2091 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 2092 | "contentVersion": "1.0.0.0", 2093 | "metadata": { 2094 | "_generator": { 2095 | "name": "bicep", 2096 | "version": "0.30.23.60470", 2097 | "templateHash": "2504651493707401756" 2098 | } 2099 | }, 2100 | "parameters": { 2101 | "vnet": { 2102 | "type": "bool", 2103 | "metadata": { 2104 | "description": "Deploy in VNet" 2105 | } 2106 | }, 2107 | "administratorLogin": { 2108 | "type": "string", 2109 | "minLength": 1, 2110 | "metadata": { 2111 | "description": "Database administrator login name" 2112 | } 2113 | }, 2114 | "administratorLoginPassword": { 2115 | "type": "securestring", 2116 | "minLength": 8, 2117 | "metadata": { 2118 | "description": "Database administrator password" 2119 | } 2120 | }, 2121 | "virtualNetworkName": { 2122 | "type": "string", 2123 | "metadata": { 2124 | "description": "Name of the VNet" 2125 | } 2126 | }, 2127 | "vnetId": { 2128 | "type": "string", 2129 | "metadata": { 2130 | "description": "ID of the vnet" 2131 | } 2132 | }, 2133 | "databaseSubnetId": { 2134 | "type": "string", 2135 | "metadata": { 2136 | "description": "ID of the subnet" 2137 | } 2138 | }, 2139 | "keyVaultName": { 2140 | "type": "string", 2141 | "metadata": { 2142 | "description": "Name of the key vault" 2143 | } 2144 | }, 2145 | "ctfDbSecretName": { 2146 | "type": "string", 2147 | "metadata": { 2148 | "description": "Name of the connection string secret" 2149 | } 2150 | }, 2151 | "location": { 2152 | "type": "string", 2153 | "metadata": { 2154 | "description": "Location for all resources." 2155 | } 2156 | }, 2157 | "logAnalyticsWorkspaceId": { 2158 | "type": "string", 2159 | "metadata": { 2160 | "description": "Log Anaytics Workspace Id" 2161 | } 2162 | }, 2163 | "mysqlWorkloadType": { 2164 | "type": "string", 2165 | "allowedValues": [ 2166 | "Development", 2167 | "SmallMedium", 2168 | "BusinessCritical" 2169 | ], 2170 | "metadata": { 2171 | "description": "MySql Workload Type" 2172 | } 2173 | } 2174 | }, 2175 | "variables": { 2176 | "mysqlServerName": "[format('ctfd-mysql-{0}', uniqueString(resourceGroup().id))]", 2177 | "tier": "[if(equals(parameters('mysqlWorkloadType'), 'Development'), 'Burstable', if(equals(parameters('mysqlWorkloadType'), 'SmallMedium'), 'GeneralPurpose', 'MemoryOptimized'))]", 2178 | "skuName": "[if(equals(parameters('mysqlWorkloadType'), 'Development'), 'Standard_B1ms', if(equals(parameters('mysqlWorkloadType'), 'SmallMedium'), 'Standard_E2ads_v5', 'Standard_E2ads_v5'))]", 2179 | "storageSizeGB": "[if(equals(parameters('mysqlWorkloadType'), 'Development'), 20, 128)]", 2180 | "iops": "[if(equals(parameters('mysqlWorkloadType'), 'Development'), 360, 2000)]" 2181 | }, 2182 | "resources": [ 2183 | { 2184 | "type": "Microsoft.DBforMySQL/flexibleServers", 2185 | "apiVersion": "2023-10-01-preview", 2186 | "name": "[variables('mysqlServerName')]", 2187 | "location": "[parameters('location')]", 2188 | "sku": { 2189 | "name": "[variables('skuName')]", 2190 | "tier": "[variables('tier')]" 2191 | }, 2192 | "properties": { 2193 | "administratorLogin": "[parameters('administratorLogin')]", 2194 | "administratorLoginPassword": "[parameters('administratorLoginPassword')]", 2195 | "storage": { 2196 | "autoGrow": "Enabled", 2197 | "iops": "[variables('iops')]", 2198 | "storageSizeGB": "[variables('storageSizeGB')]" 2199 | }, 2200 | "network": "[if(parameters('vnet'), createObject('delegatedSubnetResourceId', parameters('databaseSubnetId'), 'privateDnsZoneResourceId', resourceId('Microsoft.Network/privateDnsZones', format('{0}.private.mysql.database.azure.com', variables('mysqlServerName'))), 'publicNetworkAccess', 'Disabled'), createObject('publicNetworkAccess', 'Enabled'))]", 2201 | "createMode": "Default", 2202 | "version": "8.0.21", 2203 | "backup": { 2204 | "backupRetentionDays": 7, 2205 | "geoRedundantBackup": "Disabled" 2206 | }, 2207 | "highAvailability": { 2208 | "mode": "Disabled" 2209 | } 2210 | }, 2211 | "dependsOn": [ 2212 | "[resourceId('Microsoft.Network/privateDnsZones', format('{0}.private.mysql.database.azure.com', variables('mysqlServerName')))]", 2213 | "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', format('{0}.private.mysql.database.azure.com', variables('mysqlServerName')), parameters('virtualNetworkName'))]" 2214 | ] 2215 | }, 2216 | { 2217 | "type": "Microsoft.DBforMySQL/flexibleServers/configurations", 2218 | "apiVersion": "2023-06-30", 2219 | "name": "[format('{0}/{1}', variables('mysqlServerName'), 'collation_server')]", 2220 | "properties": { 2221 | "source": "user-override", 2222 | "value": "UTF8MB4_UNICODE_CI" 2223 | }, 2224 | "dependsOn": [ 2225 | "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mysqlServerName'))]" 2226 | ] 2227 | }, 2228 | { 2229 | "type": "Microsoft.DBforMySQL/flexibleServers/databases", 2230 | "apiVersion": "2023-06-30", 2231 | "name": "[format('{0}/{1}', variables('mysqlServerName'), 'ctfd')]", 2232 | "properties": { 2233 | "charset": "utf8mb4", 2234 | "collation": "utf8mb4_unicode_ci" 2235 | }, 2236 | "dependsOn": [ 2237 | "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mysqlServerName'))]" 2238 | ] 2239 | }, 2240 | { 2241 | "condition": "[not(parameters('vnet'))]", 2242 | "type": "Microsoft.DBforMySQL/flexibleServers/firewallRules", 2243 | "apiVersion": "2023-06-30", 2244 | "name": "[format('{0}/{1}', variables('mysqlServerName'), 'AllowAllAzureServicesAndResourcesWithinAzureIps_2024-5-24_16-27-0')]", 2245 | "properties": { 2246 | "startIpAddress": "0.0.0.0", 2247 | "endIpAddress": "0.0.0.0" 2248 | }, 2249 | "dependsOn": [ 2250 | "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mysqlServerName'))]" 2251 | ] 2252 | }, 2253 | { 2254 | "condition": "[parameters('vnet')]", 2255 | "type": "Microsoft.Network/privateDnsZones", 2256 | "apiVersion": "2024-06-01", 2257 | "name": "[format('{0}.private.mysql.database.azure.com', variables('mysqlServerName'))]", 2258 | "location": "global" 2259 | }, 2260 | { 2261 | "condition": "[parameters('vnet')]", 2262 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 2263 | "apiVersion": "2024-06-01", 2264 | "name": "[format('{0}/{1}', format('{0}.private.mysql.database.azure.com', variables('mysqlServerName')), parameters('virtualNetworkName'))]", 2265 | "location": "global", 2266 | "properties": { 2267 | "registrationEnabled": false, 2268 | "virtualNetwork": { 2269 | "id": "[parameters('vnetId')]" 2270 | } 2271 | }, 2272 | "dependsOn": [ 2273 | "[resourceId('Microsoft.Network/privateDnsZones', format('{0}.private.mysql.database.azure.com', variables('mysqlServerName')))]" 2274 | ] 2275 | }, 2276 | { 2277 | "type": "Microsoft.Insights/diagnosticSettings", 2278 | "apiVersion": "2021-05-01-preview", 2279 | "scope": "[format('Microsoft.DBforMySQL/flexibleServers/{0}', variables('mysqlServerName'))]", 2280 | "name": "[format('{0}-diagnostics', variables('mysqlServerName'))]", 2281 | "properties": { 2282 | "logs": [ 2283 | { 2284 | "category": null, 2285 | "categoryGroup": "allLogs", 2286 | "enabled": true, 2287 | "retentionPolicy": { 2288 | "days": 5, 2289 | "enabled": false 2290 | } 2291 | } 2292 | ], 2293 | "workspaceId": "[parameters('logAnalyticsWorkspaceId')]" 2294 | }, 2295 | "dependsOn": [ 2296 | "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mysqlServerName'))]" 2297 | ] 2298 | }, 2299 | { 2300 | "type": "Microsoft.Resources/deployments", 2301 | "apiVersion": "2022-09-01", 2302 | "name": "sqlDbKeyDeploy", 2303 | "properties": { 2304 | "expressionEvaluationOptions": { 2305 | "scope": "inner" 2306 | }, 2307 | "mode": "Incremental", 2308 | "parameters": { 2309 | "keyVaultName": { 2310 | "value": "[parameters('keyVaultName')]" 2311 | }, 2312 | "secretName": { 2313 | "value": "[parameters('ctfDbSecretName')]" 2314 | }, 2315 | "secretValue": { 2316 | "value": "[format('mysql+pymysql://{0}:{1}@{2}.mysql.database.azure.com/ctfd?ssl_ca=/opt/certificates/DigiCertGlobalRootCA.crt.pem', parameters('administratorLogin'), parameters('administratorLoginPassword'), variables('mysqlServerName'))]" 2317 | } 2318 | }, 2319 | "template": { 2320 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 2321 | "contentVersion": "1.0.0.0", 2322 | "metadata": { 2323 | "_generator": { 2324 | "name": "bicep", 2325 | "version": "0.30.23.60470", 2326 | "templateHash": "3629446120964483422" 2327 | } 2328 | }, 2329 | "parameters": { 2330 | "keyVaultName": { 2331 | "type": "string", 2332 | "metadata": { 2333 | "description": "Name of Azure Key Vault" 2334 | } 2335 | }, 2336 | "secretName": { 2337 | "type": "string", 2338 | "metadata": { 2339 | "description": "Name of the secret" 2340 | } 2341 | }, 2342 | "secretValue": { 2343 | "type": "securestring", 2344 | "metadata": { 2345 | "description": "Value of the secret" 2346 | } 2347 | } 2348 | }, 2349 | "resources": [ 2350 | { 2351 | "type": "Microsoft.KeyVault/vaults/secrets", 2352 | "apiVersion": "2023-07-01", 2353 | "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretName'))]", 2354 | "properties": { 2355 | "value": "[parameters('secretValue')]" 2356 | } 2357 | } 2358 | ] 2359 | } 2360 | } 2361 | } 2362 | ] 2363 | } 2364 | }, 2365 | "dependsOn": [ 2366 | "[resourceId('Microsoft.Resources/deployments', 'keyVaultDeploy')]", 2367 | "[resourceId('Microsoft.Resources/deployments', 'logAnalyticsDeploy')]", 2368 | "[resourceId('Microsoft.Resources/deployments', 'vnetDeploy')]" 2369 | ], 2370 | "metadata": { 2371 | "description": "Deploys Azure Database for MySql and a Key Vault secret with its connection string" 2372 | } 2373 | } 2374 | ] 2375 | } --------------------------------------------------------------------------------