add openhack files
This commit is contained in:
@ -0,0 +1,34 @@
|
||||
trigger:
|
||||
- none
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
|
||||
variables:
|
||||
- group: openhack
|
||||
- name: ServiceConnectionName
|
||||
value: AzureServiceConnection
|
||||
- name: RESOURCES_SUFFIX
|
||||
value: sqlsecrot
|
||||
- name: RESOURCE_GROUP_NAME
|
||||
value: $(RESOURCES_PREFIX)$(RESOURCES_SUFFIX)rg
|
||||
- name: workDir
|
||||
value: "$(System.DefaultWorkingDirectory)/support/sqlsecretrotation/iac/bicep"
|
||||
|
||||
steps:
|
||||
- task: AzureCLI@2
|
||||
displayName: "Deploy"
|
||||
inputs:
|
||||
azureSubscription: "$(ServiceConnectionName)"
|
||||
scriptType: "bash"
|
||||
scriptLocation: "inlineScript"
|
||||
inlineScript: |
|
||||
if [ $(az group exists --name $(RESOURCE_GROUP_NAME)) = false ]; then
|
||||
az group create --name $(RESOURCE_GROUP_NAME) --location $(LOCATION)
|
||||
fi
|
||||
az deployment group create \
|
||||
--name $(Build.BuildId) \
|
||||
--resource-group $(RESOURCE_GROUP_NAME) \
|
||||
--template-file main.bicep \
|
||||
--parameters keyVaultRgName='$(RESOURCES_PREFIX)rg' keyVaultName='$(RESOURCES_PREFIX)kv' resourcesPrefix='$(RESOURCES_PREFIX)' resourcesSuffix='$(RESOURCES_SUFFIX)'
|
||||
workingDirectory: $(workDir)
|
@ -0,0 +1,114 @@
|
||||
trigger:
|
||||
- none
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
|
||||
variables:
|
||||
- group: openhack
|
||||
- group: tfstate
|
||||
- name: ServiceConnectionName
|
||||
value: AzureServiceConnection
|
||||
- name: workDir
|
||||
value: "$(System.DefaultWorkingDirectory)/support/sqlsecretrotation"
|
||||
|
||||
stages:
|
||||
- stage: Provision
|
||||
displayName: Provision infrastructure
|
||||
jobs:
|
||||
- deployment: Provision
|
||||
displayName: Provision
|
||||
environment: sqlsecretrotation
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- checkout: self
|
||||
- task: TerraformInstaller@0
|
||||
displayName: Setup Terraform
|
||||
inputs:
|
||||
terraformVersion: "latest"
|
||||
- task: TerraformCLI@0
|
||||
displayName: Terraform Init
|
||||
inputs:
|
||||
command: "init"
|
||||
workingDirectory: "$(workDir)/iac/terraform"
|
||||
backendType: "azurerm"
|
||||
backendServiceArm: "$(ServiceConnectionName)"
|
||||
backendAzureRmResourceGroupName: "$(TFSTATE_RESOURCES_GROUP_NAME)"
|
||||
backendAzureRmStorageAccountName: "$(TFSTATE_STORAGE_ACCOUNT_NAME)"
|
||||
backendAzureRmContainerName: "$(TFSTATE_STORAGE_CONTAINER_NAME)"
|
||||
backendAzureRmKey: "sqlsecrot.tfstate"
|
||||
allowTelemetryCollection: true
|
||||
- task: TerraformCLI@0
|
||||
displayName: Terraform Plan
|
||||
inputs:
|
||||
command: "plan"
|
||||
workingDirectory: "$(workDir)/iac/terraform"
|
||||
environmentServiceName: "$(ServiceConnectionName)"
|
||||
commandOptions: '-detailed-exitcode -var="location=$(LOCATION)" -var="resources_prefix=$(RESOURCES_PREFIX)" -var="secret_name=SQL-PASSWORD" -var="key_vault_name=$(RESOURCES_PREFIX)kv" -var="key_vault_resource_group_name=$(RESOURCES_PREFIX)rg"'
|
||||
publishPlanResults: "tfplan"
|
||||
allowTelemetryCollection: true
|
||||
- task: TerraformCLI@0
|
||||
displayName: Terraform Apply
|
||||
condition: eq(variables['TERRAFORM_PLAN_HAS_CHANGES'], 'true')
|
||||
inputs:
|
||||
command: "apply"
|
||||
workingDirectory: "$(workDir)/iac/terraform"
|
||||
environmentServiceName: "$(ServiceConnectionName)"
|
||||
commandOptions: '-var="location=$(LOCATION)" -var="resources_prefix=$(RESOURCES_PREFIX)" -var="secret_name=SQL-PASSWORD" -var="key_vault_name=$(RESOURCES_PREFIX)kv" -var="key_vault_resource_group_name=$(RESOURCES_PREFIX)rg"'
|
||||
allowTelemetryCollection: true
|
||||
|
||||
- stage: Build
|
||||
displayName: Build function
|
||||
dependsOn: Provision
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
steps:
|
||||
- checkout: self
|
||||
- task: UseDotNet@2
|
||||
displayName: "Setup .NET Core"
|
||||
inputs:
|
||||
packageType: "sdk"
|
||||
version: "3.x"
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build project
|
||||
inputs:
|
||||
command: "build"
|
||||
projects: "$(workDir)/src/*.csproj"
|
||||
arguments: "--output $(System.DefaultWorkingDirectory)/publish_output --configuration Release"
|
||||
workingDirectory: "$(workDir)/src"
|
||||
- task: ArchiveFiles@2
|
||||
displayName: "Archive files"
|
||||
inputs:
|
||||
rootFolderOrFile: "$(System.DefaultWorkingDirectory)/publish_output"
|
||||
includeRootFolder: false
|
||||
archiveType: "zip"
|
||||
archiveFile: "$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip"
|
||||
replaceExistingArchive: true
|
||||
- publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
displayName: "Publish Artifact"
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy function
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: sqlsecretrotation
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureFunctionApp@1
|
||||
displayName: "Azure Functions deploy"
|
||||
inputs:
|
||||
azureSubscription: "AzureServiceConnection"
|
||||
appType: "functionApp"
|
||||
appName: "$(RESOURCES_PREFIX)secrotfunc"
|
||||
package: "$(Pipeline.Workspace)/drop/$(Build.BuildId).zip"
|
||||
deploymentMethod: "auto"
|
66
support/sqlsecretrotation/.github/workflows/workflow.bicep.sqlsecrot.yml
vendored
Normal file
66
support/sqlsecretrotation/.github/workflows/workflow.bicep.sqlsecrot.yml
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
name: "Deploy - sqlsecrot (Bicep)"
|
||||
|
||||
# run manually
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# Set envs
|
||||
env:
|
||||
WORKDIR: "support/sqlsecretrotation/iac/bicep"
|
||||
RESOURCES_SUFFIX: "sqlsecrot"
|
||||
# RESOURCES_PREFIX: "devopsoh44707" # hardcoded or dynamic based on repo name
|
||||
# LOCATION: "westus2" # hardcoded or get from secrets
|
||||
|
||||
# Set defaults for GitHub Actions runner
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "support/sqlsecretrotation/iac/bicep"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: "Deploy"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Get RESOURCES_PREFIX based on the repo name
|
||||
- name: Get repo name
|
||||
uses: actions/github-script@v5
|
||||
id: resources_prefix
|
||||
with:
|
||||
result-encoding: string
|
||||
script: return context.repo.repo.toLowerCase()
|
||||
|
||||
# Concat RG name
|
||||
- name: Get resource group name
|
||||
uses: actions/github-script@v5
|
||||
id: resource_group_name
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { RESOURCES_SUFFIX } = process.env
|
||||
const repo_name = "${{ steps.resources_prefix.outputs.result }}"
|
||||
return `${repo_name}${RESOURCES_SUFFIX}rg`
|
||||
|
||||
# Login to Azure with Service Principal
|
||||
- name: "Azure Login"
|
||||
uses: Azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
# Deploy
|
||||
- name: "Deploy"
|
||||
uses: Azure/cli@1.0.4
|
||||
with:
|
||||
inlineScript: |
|
||||
if [ $(az group exists --name ${{ steps.resource_group_name.outputs.result }}) = false ]; then
|
||||
az group create --name ${{ steps.resource_group_name.outputs.result }} --location ${{ secrets.LOCATION }}
|
||||
fi
|
||||
az deployment group create \
|
||||
--name ${{ github.run_id }} \
|
||||
--resource-group ${{ steps.resource_group_name.outputs.result }} \
|
||||
--template-file ${{ env.WORKDIR }}/main.bicep \
|
||||
--parameters keyVaultRgName='${{ steps.resources_prefix.outputs.result }}rg' keyVaultName='${{ steps.resources_prefix.outputs.result }}kv' resourcesPrefix='${{ steps.resources_prefix.outputs.result }}' resourcesSuffix='${{ env.RESOURCES_SUFFIX }}'
|
35
support/sqlsecretrotation/iac/bicep/deploy.sh
Normal file
35
support/sqlsecretrotation/iac/bicep/deploy.sh
Normal file
@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
declare LOCATION=$1
|
||||
declare RESOURCES_PREFIX=$2
|
||||
declare RESOURCES_SUFFIX=$3
|
||||
declare KEY_VAULT_RESOURCE_GROUP_NAME=$4
|
||||
declare KEY_VAULT_NAME=$5
|
||||
|
||||
declare -r USAGE_HELP="Usage: ./deploy.sh <LOCATION> <RESOURCES_PREFIX> <RESOURCES_SUFFIX> <KEY_VAULT_RESOURCE_GROUP_NAME> <KEY_VAULT_NAME>"
|
||||
|
||||
if [ $# -ne 5 ]; then
|
||||
echo "${USAGE_HELP}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for programs
|
||||
if ! [ -x "$(command -v az)" ]; then
|
||||
echo "az is not installed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "devvars.sh" ]; then
|
||||
. devvars.sh
|
||||
fi
|
||||
|
||||
RESOURCE_GROUP_NAME="${RESOURCES_PREFIX}${RESOURCES_SUFFIX}rg"
|
||||
|
||||
if [ $(az group exists --name "${RESOURCE_GROUP_NAME}") = false ]; then
|
||||
az group create --name "${RESOURCE_GROUP_NAME}" --location "${LOCATION}"
|
||||
fi
|
||||
|
||||
az deployment group create \
|
||||
--resource-group "${RESOURCE_GROUP_NAME}" \
|
||||
--template-file main.bicep \
|
||||
--parameters keyVaultRgName="${KEY_VAULT_RESOURCE_GROUP_NAME}" keyVaultName="${KEY_VAULT_NAME}" resourcesPrefix="${RESOURCES_PREFIX}" resourcesSuffix="${RESOURCES_SUFFIX}"
|
60
support/sqlsecretrotation/iac/bicep/keyVault.bicep
Normal file
60
support/sqlsecretrotation/iac/bicep/keyVault.bicep
Normal file
@ -0,0 +1,60 @@
|
||||
param keyVaultName string
|
||||
param functionAppId string
|
||||
param functionAppPrincipalId string
|
||||
param functionAppTenantId string
|
||||
param eventSubscriptionName string
|
||||
param secretName string
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.keyvault/vaults?tabs=bicep
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
|
||||
name: keyVaultName
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.keyvault/vaults/accesspolicies?tabs=bicep
|
||||
resource keyVaultAccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = {
|
||||
name: 'add'
|
||||
parent: keyVault
|
||||
properties: {
|
||||
accessPolicies: [
|
||||
{
|
||||
tenantId: functionAppTenantId
|
||||
objectId: functionAppPrincipalId
|
||||
permissions: {
|
||||
secrets: [
|
||||
'get'
|
||||
'list'
|
||||
'set'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.eventgrid/eventsubscriptions?tabs=bicep
|
||||
resource keyVaultEventSubscription 'Microsoft.EventGrid/eventSubscriptions@2021-06-01-preview' = {
|
||||
name: eventSubscriptionName
|
||||
scope: keyVault
|
||||
properties: {
|
||||
destination: {
|
||||
endpointType: 'AzureFunction'
|
||||
properties: {
|
||||
maxEventsPerBatch: 1
|
||||
preferredBatchSizeInKilobytes: 64
|
||||
resourceId: '${functionAppId}/functions/AKVSQLRotation'
|
||||
}
|
||||
}
|
||||
filter: {
|
||||
subjectBeginsWith: secretName
|
||||
subjectEndsWith: secretName
|
||||
includedEventTypes: [
|
||||
'Microsoft.KeyVault.SecretNearExpiry'
|
||||
]
|
||||
}
|
||||
eventDeliverySchema: 'EventGridSchema'
|
||||
retryPolicy: {
|
||||
eventTimeToLiveInMinutes: 60
|
||||
maxDeliveryAttempts: 30
|
||||
}
|
||||
}
|
||||
}
|
115
support/sqlsecretrotation/iac/bicep/main.bicep
Normal file
115
support/sqlsecretrotation/iac/bicep/main.bicep
Normal file
@ -0,0 +1,115 @@
|
||||
param keyVaultRgName string = resourceGroup().name
|
||||
param keyVaultName string
|
||||
param resourcesPrefix string
|
||||
param resourcesSuffix string = 'sqlsecrot'
|
||||
param secretName string = 'SQL-PASSWORD'
|
||||
param repoUrl string = 'https://github.com/Azure-Samples/KeyVault-Rotation-SQLPassword-Csharp.git'
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.storage/storageaccounts?tabs=bicep
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' = {
|
||||
name: '${resourcesPrefix}${resourcesSuffix}st'
|
||||
location: resourceGroup().location
|
||||
sku: {
|
||||
name: 'Standard_LRS'
|
||||
}
|
||||
kind: 'StorageV2'
|
||||
properties: {
|
||||
supportsHttpsTrafficOnly: true
|
||||
accessTier: 'Hot'
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.web/serverfarms?tabs=bicep
|
||||
resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
|
||||
name: '${resourcesPrefix}${resourcesSuffix}plan'
|
||||
location: resourceGroup().location
|
||||
sku: {
|
||||
name: 'Y1'
|
||||
tier: 'Dynamic'
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.web/sites?tabs=bicep
|
||||
resource functionApp 'Microsoft.Web/sites@2021-02-01' = {
|
||||
name: '${resourcesPrefix}${resourcesSuffix}func'
|
||||
location: resourceGroup().location
|
||||
kind: 'functionapp'
|
||||
identity: {
|
||||
type: 'SystemAssigned'
|
||||
}
|
||||
properties: {
|
||||
enabled: true
|
||||
serverFarmId: appServicePlan.id
|
||||
httpsOnly: true
|
||||
siteConfig: {
|
||||
appSettings: [
|
||||
{
|
||||
name: 'AzureWebJobsStorage'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTSHARE'
|
||||
value: toLower('${resourcesPrefix}${resourcesSuffix}func')
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_EXTENSION_VERSION'
|
||||
value: '~3'
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_WORKER_RUNTIME'
|
||||
value: 'dotnet'
|
||||
}
|
||||
{
|
||||
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
|
||||
value: applicationInsights.properties.InstrumentationKey
|
||||
}
|
||||
{
|
||||
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
||||
value: applicationInsights.properties.ConnectionString
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.web/sites/sourcecontrols?tabs=bicep
|
||||
resource functionAppSourceControl 'Microsoft.Web/sites/sourcecontrols@2021-02-01' = {
|
||||
name: 'web'
|
||||
parent: functionApp
|
||||
properties: {
|
||||
repoUrl: repoUrl
|
||||
branch: 'main'
|
||||
isManualIntegration: true
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/templates/microsoft.insights/components?tabs=bicep
|
||||
resource applicationInsights 'microsoft.insights/components@2020-02-02' = {
|
||||
name: '${resourcesPrefix}${resourcesSuffix}appi'
|
||||
location: resourceGroup().location
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
}
|
||||
}
|
||||
|
||||
module keyVault './keyVault.bicep' = {
|
||||
name: 'keyVaultDeployment'
|
||||
params: {
|
||||
keyVaultName: keyVaultName
|
||||
functionAppId: functionApp.id
|
||||
functionAppTenantId: functionApp.identity.tenantId
|
||||
functionAppPrincipalId: functionApp.identity.principalId
|
||||
eventSubscriptionName: '${keyVaultName}-${secretName}-${functionApp.name}'
|
||||
secretName: secretName
|
||||
}
|
||||
scope: resourceGroup(keyVaultRgName)
|
||||
dependsOn: [
|
||||
functionApp
|
||||
functionAppSourceControl
|
||||
]
|
||||
}
|
94
support/sqlsecretrotation/iac/terraform/deploy.sh
Normal file
94
support/sqlsecretrotation/iac/terraform/deploy.sh
Normal file
@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
|
||||
declare LOCATION=$1
|
||||
declare RESOURCES_PREFIX=$2
|
||||
declare SECRET_NAME=$3
|
||||
declare KEY_VAULT_RESOURCE_GROUP_NAME=$4
|
||||
declare KEY_VAULT_NAME=$5
|
||||
|
||||
declare -r USAGE_HELP="Usage: ./deploy.sh <LOCATION> <RESOURCES_PREFIX> <SECRET_NAME> <KEY_VAULT_RESOURCE_GROUP_NAME> <KEY_VAULT_NAME>"
|
||||
|
||||
if [ $# -ne 5 ]; then
|
||||
echo "${USAGE_HELP}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for programs
|
||||
if ! [ -x "$(command -v az)" ]; then
|
||||
echo "az is not installed!"
|
||||
exit 1
|
||||
elif ! [ -x "$(command -v terraform)" ]; then
|
||||
echo "terraform is not installed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "devvars.sh" ]; then
|
||||
. devvars.sh
|
||||
fi
|
||||
|
||||
export ARM_THREEPOINTZERO_BETA_RESOURCES=true
|
||||
|
||||
azure_login() {
|
||||
_azuresp_json=$(cat azuresp.json)
|
||||
export ARM_CLIENT_ID=$(echo "${_azuresp_json}" | jq -r ".clientId")
|
||||
export ARM_CLIENT_SECRET=$(echo "${_azuresp_json}" | jq -r ".clientSecret")
|
||||
export ARM_SUBSCRIPTION_ID=$(echo "${_azuresp_json}" | jq -r ".subscriptionId")
|
||||
export ARM_TENANT_ID=$(echo "${_azuresp_json}" | jq -r ".tenantId")
|
||||
az login --service-principal --username "${ARM_CLIENT_ID}" --password "${ARM_CLIENT_SECRET}" --tenant "${ARM_TENANT_ID}"
|
||||
az account set --subscription "${ARM_SUBSCRIPTION_ID}"
|
||||
}
|
||||
|
||||
prepare_tfvars() {
|
||||
echo "Generating tfvars..."
|
||||
echo 'location = "'${LOCATION}'"' > terraform.tfvars
|
||||
echo 'resources_prefix = "'${RESOURCES_PREFIX}'"' >> terraform.tfvars
|
||||
echo 'secret_name = "'${SECRET_NAME}'"' >> terraform.tfvars
|
||||
echo 'key_vault_resource_group_name = "'${KEY_VAULT_RESOURCE_GROUP_NAME}'"' >> terraform.tfvars
|
||||
echo 'key_vault_name = "'${KEY_VAULT_NAME}'"' >> terraform.tfvars
|
||||
terraform fmt
|
||||
}
|
||||
|
||||
lint_terraform(){
|
||||
terraform fmt -check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Terraform files are not properly formatted!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
init_terrafrom() {
|
||||
terraform init -backend-config=storage_account_name="${TFSTATE_STORAGE_ACCOUNT_NAME}" -backend-config=container_name="${TFSTATE_STORAGE_CONTAINER_NAME}" -backend-config=key="${TFSTATE_KEY_SECROT}" -backend-config=resource_group_name="${TFSTATE_RESOURCES_GROUP_NAME}"
|
||||
}
|
||||
|
||||
init_terrafrom_local() {
|
||||
terraform init -backend=false
|
||||
}
|
||||
|
||||
validate_terraform(){
|
||||
terraform validate
|
||||
}
|
||||
|
||||
preview_terraform(){
|
||||
terraform plan --detailed-exitcode
|
||||
return $?
|
||||
}
|
||||
|
||||
deploy_terraform(){
|
||||
local _tfplan_exit_code=${1}
|
||||
|
||||
terraform apply --auto-approve
|
||||
}
|
||||
|
||||
destroy_terraform(){
|
||||
terraform destroy --auto-approve
|
||||
}
|
||||
|
||||
prepare_tfvars
|
||||
azure_login
|
||||
lint_terraform
|
||||
init_terrafrom
|
||||
# init_terrafrom_local
|
||||
validate_terraform
|
||||
preview_terraform
|
||||
deploy_terraform $?
|
||||
# destroy_terraform
|
8
support/sqlsecretrotation/iac/terraform/locals.tf
Normal file
8
support/sqlsecretrotation/iac/terraform/locals.tf
Normal file
@ -0,0 +1,8 @@
|
||||
locals {
|
||||
suffix = "sqlsecrot"
|
||||
resource_group_name = "${var.resources_prefix}${local.suffix}rg"
|
||||
storage_account_name = "${var.resources_prefix}${local.suffix}st"
|
||||
function_app_name = "${var.resources_prefix}${local.suffix}func"
|
||||
app_service_plan_name = "${var.resources_prefix}${local.suffix}plan"
|
||||
application_insights_name = "${var.resources_prefix}${local.suffix}appi"
|
||||
}
|
132
support/sqlsecretrotation/iac/terraform/main.tf
Normal file
132
support/sqlsecretrotation/iac/terraform/main.tf
Normal file
@ -0,0 +1,132 @@
|
||||
data "azurerm_key_vault" "key_vault" {
|
||||
name = var.key_vault_name
|
||||
resource_group_name = var.key_vault_resource_group_name
|
||||
}
|
||||
|
||||
resource "azurerm_resource_group" "resource_group" {
|
||||
name = local.resource_group_name
|
||||
location = var.location
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
tags
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_storage_account" "storage_account" {
|
||||
name = local.storage_account_name
|
||||
location = azurerm_resource_group.resource_group.location
|
||||
resource_group_name = azurerm_resource_group.resource_group.name
|
||||
account_tier = "Standard"
|
||||
account_replication_type = "LRS"
|
||||
min_tls_version = "TLS1_2"
|
||||
enable_https_traffic_only = true
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
tags
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_application_insights" "application_insights" {
|
||||
name = local.application_insights_name
|
||||
location = azurerm_resource_group.resource_group.location
|
||||
resource_group_name = azurerm_resource_group.resource_group.name
|
||||
application_type = "web"
|
||||
}
|
||||
|
||||
resource "azurerm_app_service_plan" "app_service_plan" {
|
||||
name = local.app_service_plan_name
|
||||
location = azurerm_resource_group.resource_group.location
|
||||
resource_group_name = azurerm_resource_group.resource_group.name
|
||||
kind = "FunctionApp"
|
||||
|
||||
sku {
|
||||
tier = "Dynamic"
|
||||
size = "Y1"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
tags,
|
||||
kind
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_function_app" "function_app" {
|
||||
name = local.function_app_name
|
||||
location = azurerm_resource_group.resource_group.location
|
||||
resource_group_name = azurerm_resource_group.resource_group.name
|
||||
app_service_plan_id = azurerm_app_service_plan.app_service_plan.id
|
||||
storage_account_name = azurerm_storage_account.storage_account.name
|
||||
storage_account_access_key = azurerm_storage_account.storage_account.primary_access_key
|
||||
version = "~3"
|
||||
https_only = true
|
||||
|
||||
app_settings = {
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.application_insights.instrumentation_key
|
||||
"FUNCTIONS_WORKER_RUNTIME" = "dotnet"
|
||||
}
|
||||
|
||||
identity {
|
||||
type = "SystemAssigned"
|
||||
}
|
||||
|
||||
site_config {
|
||||
ftps_state = "Disabled"
|
||||
dotnet_framework_version = "v4.0"
|
||||
use_32_bit_worker_process = false
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
tags,
|
||||
# app_settings["WEBSITE_ENABLE_SYNC_UPDATE_SITE"],
|
||||
# app_settings["WEBSITE_RUN_FROM_PACKAGE"],
|
||||
# app_settings["APPINSIGHTS_INSTRUMENTATIONKEY"],
|
||||
# app_settings["FUNCTIONS_WORKER_RUNTIME"]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/app_service_source_control
|
||||
# resource "azurerm_app_service_source_control" "app_service_source_control" {
|
||||
# app_id = azurerm_function_app.function_app.id
|
||||
# repo_url = "https://github.com/Azure-Samples/KeyVault-Rotation-SQLPassword-Csharp.git"
|
||||
# branch = "main"
|
||||
# manual_integration = true
|
||||
# # scm_type = "ExternalGit"
|
||||
# }
|
||||
|
||||
resource "azurerm_key_vault_access_policy" "key_vault_access_policy_function_app" {
|
||||
key_vault_id = data.azurerm_key_vault.key_vault.id
|
||||
tenant_id = azurerm_function_app.function_app.identity[0].tenant_id
|
||||
object_id = azurerm_function_app.function_app.identity[0].principal_id
|
||||
|
||||
secret_permissions = [
|
||||
"Get", "List", "Set"
|
||||
]
|
||||
}
|
||||
|
||||
# resource "azurerm_eventgrid_event_subscription" "eventgrid_event_subscription" {
|
||||
# name = "${data.azurerm_key_vault.key_vault.name}-${var.secret_name}-${azurerm_function_app.function_app.name}"
|
||||
# scope = data.azurerm_key_vault.key_vault.id
|
||||
|
||||
# azure_function_endpoint {
|
||||
# function_id = "${azurerm_function_app.function_app.id}/functions/AKVSQLRotation"
|
||||
# max_events_per_batch = 1
|
||||
# preferred_batch_size_in_kilobytes = 64
|
||||
# }
|
||||
|
||||
# subject_filter {
|
||||
# subject_begins_with = var.secret_name
|
||||
# subject_ends_with = var.secret_name
|
||||
# }
|
||||
|
||||
# included_event_types = [
|
||||
# "Microsoft.KeyVault.SecretNearExpiry"
|
||||
# ]
|
||||
# }
|
16
support/sqlsecretrotation/iac/terraform/providers.tf
Normal file
16
support/sqlsecretrotation/iac/terraform/providers.tf
Normal file
@ -0,0 +1,16 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "2.94.0"
|
||||
}
|
||||
}
|
||||
backend "azurerm" {
|
||||
}
|
||||
}
|
||||
|
||||
data "azurerm_client_config" "current" {}
|
||||
|
||||
provider "azurerm" {
|
||||
features {}
|
||||
}
|
24
support/sqlsecretrotation/iac/terraform/variables.tf
Normal file
24
support/sqlsecretrotation/iac/terraform/variables.tf
Normal file
@ -0,0 +1,24 @@
|
||||
variable "key_vault_resource_group_name" {
|
||||
description = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "key_vault_name" {
|
||||
description = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "resources_prefix" {
|
||||
description = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "location" {
|
||||
description = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "secret_name" {
|
||||
description = ""
|
||||
type = string
|
||||
}
|
1
support/sqlsecretrotation/src/.dockerignore
Normal file
1
support/sqlsecretrotation/src/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
local.settings.json
|
28
support/sqlsecretrotation/src/AKVSQLRotation.cs
Normal file
28
support/sqlsecretrotation/src/AKVSQLRotation.cs
Normal file
@ -0,0 +1,28 @@
|
||||
// Default URL for triggering event grid function in the local environment.
|
||||
// http://localhost:7071/runtime/webhooks/EventGrid?functionName={functionname}
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
|
||||
using Azure.Messaging.EventGrid;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.KeyVault
|
||||
{
|
||||
public static class AKVSQLRotation
|
||||
{
|
||||
|
||||
[FunctionName("AKVSQLRotation")]
|
||||
public static void Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
|
||||
{
|
||||
log.LogInformation("C# Event trigger function processed a request.");
|
||||
var secretName = eventGridEvent.Subject;
|
||||
var secretVersion = Regex.Match(eventGridEvent.Data.ToString(), "Version\":\"([a-z0-9]*)").Groups[1].ToString();
|
||||
var keyVaultName = Regex.Match(eventGridEvent.Topic, ".vaults.(.*)").Groups[1].ToString();
|
||||
log.LogInformation($"Key Vault Name: {keyVaultName}");
|
||||
log.LogInformation($"Secret Name: {secretName}");
|
||||
log.LogInformation($"Secret Version: {secretVersion}");
|
||||
|
||||
SecretRotator.RotateSecret(log, secretName, keyVaultName);
|
||||
}
|
||||
}
|
||||
}
|
31
support/sqlsecretrotation/src/AKVSQLRotationHttp.cs
Normal file
31
support/sqlsecretrotation/src/AKVSQLRotationHttp.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.KeyVault
|
||||
{
|
||||
public static class AKVSQLRotationHttp
|
||||
{
|
||||
[FunctionName("AKVSQLRotationHttp")]
|
||||
public static IActionResult Run(
|
||||
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
|
||||
ILogger log)
|
||||
{
|
||||
string keyVaultName = req.Query["KeyVaultName"];
|
||||
string secretName = req.Query["SecretName"];
|
||||
if (string.IsNullOrEmpty(keyVaultName) || string.IsNullOrEmpty(secretName))
|
||||
{
|
||||
return new BadRequestObjectResult("Please pass a KeyVaultName and SecretName on the query string");
|
||||
}
|
||||
|
||||
log.LogInformation(req.ToString());
|
||||
|
||||
log.LogInformation("C# Http trigger function processed a request.");
|
||||
SecretRotator.RotateSecret(log, secretName, keyVaultName);
|
||||
|
||||
return new OkObjectResult($"Secret Rotated Successfully");
|
||||
}
|
||||
}
|
||||
}
|
14
support/sqlsecretrotation/src/Dockerfile
Normal file
14
support/sqlsecretrotation/src/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS installer-env
|
||||
|
||||
COPY . /src/dotnet-function-app
|
||||
RUN cd /src/dotnet-function-app && \
|
||||
mkdir -p /home/site/wwwroot && \
|
||||
dotnet publish *.csproj --output /home/site/wwwroot
|
||||
|
||||
# To enable ssh & remote debugging on app service change the base image to the one below
|
||||
# FROM mcr.microsoft.com/azure-functions/dotnet:3.0-appservice
|
||||
FROM mcr.microsoft.com/azure-functions/dotnet:3.0-slim
|
||||
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
|
||||
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
|
||||
|
||||
COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]
|
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
|
||||
<RootNamespace>Microsoft.KeyVault</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.EventGrid" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.5.0" />
|
||||
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="3.0.1" />
|
||||
<PackageReference Include="Azure.Core" Version="1.20.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
116
support/sqlsecretrotation/src/SecretRotator.cs
Normal file
116
support/sqlsecretrotation/src/SecretRotator.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using Azure.Security.KeyVault.Secrets;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Azure.Identity;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Microsoft.KeyVault
|
||||
{
|
||||
public class SecretRotator
|
||||
{
|
||||
private const string CredentialIdTag = "CredentialId";
|
||||
private const string ProviderAddressTag = "ProviderAddress";
|
||||
private const string ValidityPeriodDaysTag = "ValidityPeriodDays";
|
||||
|
||||
public static void RotateSecret(ILogger log, string secretName, string keyVaultName)
|
||||
{
|
||||
//Retrieve Current Secret
|
||||
var kvUri = "https://" + keyVaultName + ".vault.azure.net";
|
||||
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
|
||||
KeyVaultSecret secret = client.GetSecret(secretName);
|
||||
log.LogInformation("Secret Info Retrieved");
|
||||
|
||||
//Retrieve Secret Info
|
||||
var credentialId = secret.Properties.Tags.ContainsKey(CredentialIdTag) ? secret.Properties.Tags[CredentialIdTag] : "";
|
||||
var providerAddress = secret.Properties.Tags.ContainsKey(ProviderAddressTag) ? secret.Properties.Tags[ProviderAddressTag] : "";
|
||||
var validityPeriodDays = secret.Properties.Tags.ContainsKey(ValidityPeriodDaysTag) ? secret.Properties.Tags[ValidityPeriodDaysTag] : "";
|
||||
log.LogInformation($"Provider Address: {providerAddress}");
|
||||
log.LogInformation($"Credential Id: {credentialId}");
|
||||
|
||||
//Check Service Provider connection
|
||||
CheckServiceConnection(secret);
|
||||
log.LogInformation("Service Connection Validated");
|
||||
|
||||
//Create new password
|
||||
var randomPassword = CreateRandomPassword();
|
||||
log.LogInformation("New Password Generated");
|
||||
|
||||
//Add secret version with new password to Key Vault
|
||||
CreateNewSecretVersion(client, secret, randomPassword);
|
||||
log.LogInformation("New Secret Version Generated");
|
||||
|
||||
//Update Service Provider with new password
|
||||
UpdateServicePassword(secret, randomPassword);
|
||||
log.LogInformation("Password Changed");
|
||||
log.LogInformation($"Secret Rotated Successfully");
|
||||
}
|
||||
|
||||
private static void CreateNewSecretVersion(SecretClient client, KeyVaultSecret secret, string newSecretValue)
|
||||
{
|
||||
var credentialId = secret.Properties.Tags.ContainsKey(CredentialIdTag) ? secret.Properties.Tags[CredentialIdTag] : "";
|
||||
var providerAddress = secret.Properties.Tags.ContainsKey(ProviderAddressTag) ? secret.Properties.Tags[ProviderAddressTag] : "";
|
||||
var validityPeriodDays = secret.Properties.Tags.ContainsKey(ValidityPeriodDaysTag) ? secret.Properties.Tags[ValidityPeriodDaysTag] : "60";
|
||||
|
||||
//add new secret version to key vault
|
||||
var newSecret = new KeyVaultSecret(secret.Name, newSecretValue);
|
||||
newSecret.Properties.Tags.Add(CredentialIdTag, credentialId);
|
||||
newSecret.Properties.Tags.Add(ProviderAddressTag, providerAddress);
|
||||
newSecret.Properties.Tags.Add(ValidityPeriodDaysTag, validityPeriodDays);
|
||||
newSecret.Properties.ExpiresOn = DateTime.UtcNow.AddDays(Int32.Parse(validityPeriodDays));
|
||||
client.SetSecret(newSecret);
|
||||
}
|
||||
|
||||
private static void UpdateServicePassword(KeyVaultSecret secret, string newpassword)
|
||||
{
|
||||
var userId = secret.Properties.Tags.ContainsKey(CredentialIdTag) ? secret.Properties.Tags[CredentialIdTag] : "";
|
||||
var datasource = secret.Properties.Tags.ContainsKey(ProviderAddressTag) ? secret.Properties.Tags[ProviderAddressTag] : "";
|
||||
var dbResourceId = secret.Properties.Tags.ContainsKey(ProviderAddressTag) ? secret.Properties.Tags[ProviderAddressTag] : "";
|
||||
|
||||
var dbName = dbResourceId.Split('/')[8];
|
||||
var password = secret.Value;
|
||||
|
||||
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
|
||||
builder.DataSource = $"{dbName}.database.windows.net";
|
||||
builder.UserID = userId;
|
||||
builder.Password = password;
|
||||
|
||||
//Update password
|
||||
using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
using (SqlCommand command = new SqlCommand($"ALTER LOGIN {userId} WITH Password='{newpassword}';", connection))
|
||||
{
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateRandomPassword()
|
||||
{
|
||||
const int length = 60;
|
||||
|
||||
byte[] randomBytes = new byte[length];
|
||||
RNGCryptoServiceProvider rngCrypt = new RNGCryptoServiceProvider();
|
||||
rngCrypt.GetBytes(randomBytes);
|
||||
return Convert.ToBase64String(randomBytes);
|
||||
}
|
||||
private static void CheckServiceConnection(KeyVaultSecret secret)
|
||||
{
|
||||
var userId = secret.Properties.Tags.ContainsKey(CredentialIdTag) ? secret.Properties.Tags[CredentialIdTag] : "";
|
||||
var dbResourceId = secret.Properties.Tags.ContainsKey(ProviderAddressTag) ? secret.Properties.Tags[ProviderAddressTag] : "";
|
||||
|
||||
var dbName = dbResourceId.Split('/')[8];
|
||||
var password = secret.Value;
|
||||
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
|
||||
builder.DataSource = $"{dbName}.database.windows.net";
|
||||
builder.UserID = userId;
|
||||
builder.Password = password;
|
||||
using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
support/sqlsecretrotation/src/host.json
Normal file
11
support/sqlsecretrotation/src/host.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingExcludedTypes": "Request",
|
||||
"samplingSettings": {
|
||||
"isEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
support/sqlsecretrotation/src/local.settings.json
Normal file
7
support/sqlsecretrotation/src/local.settings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user