├── 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 | 
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 | [](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 | 
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 | }
--------------------------------------------------------------------------------