From 6228aa2e543bb319ae8d8d0d097b19717b526896 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Mon, 5 Aug 2024 23:08:56 +0200 Subject: [PATCH] feat(azure): scaffold ssh jumper (#958) ## Description A virtual machine that let's us ssh into the `vnet` in order to access resources in subnets. Next step would be to remove the public IP entirely and use `Bastion`. ## Related Issue(s) - #{issue number} ## Verification - [ ] **Your** code builds clean without any errors or warnings - [ ] Manual testing done (required) - [ ] Relevant automated test added (if you find this hard, leave it and we'll help out) ## Documentation - [ ] Documentation is updated (either in `docs`-directory, Altinnpedia or a separate linked PR in [altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if applicable) --- .azure/infrastructure/main.bicep | 21 +++ .azure/infrastructure/production.bicepparam | 1 + .azure/infrastructure/soak.bicepparam | 1 + .azure/infrastructure/staging.bicepparam | 1 + .azure/infrastructure/test.bicepparam | 1 + .azure/modules/ssh-jumper/main.bicep | 146 ++++++++++++++++++ .azure/modules/virtualMachine/main.bicep | 132 ++++++++++++++++ .github/workflows/action-deploy-infra.yml | 3 + .github/workflows/ci-cd-main.yml | 1 + .github/workflows/ci-cd-pull-request.yml | 1 + .github/workflows/ci-cd-staging.yml | 1 + .github/workflows/dispatch-infrastructure.yml | 1 + README.md | 18 +++ 13 files changed, 328 insertions(+) create mode 100644 .azure/modules/ssh-jumper/main.bicep create mode 100644 .azure/modules/virtualMachine/main.bicep diff --git a/.azure/infrastructure/main.bicep b/.azure/infrastructure/main.bicep index d41ef8912..200aee44e 100644 --- a/.azure/infrastructure/main.bicep +++ b/.azure/infrastructure/main.bicep @@ -31,6 +31,11 @@ param sourceKeyVaultResourceGroup string @minLength(3) param sourceKeyVaultName string +@description('SSH secret key for the ssh jumper') +@secure() +@minLength(3) +param sourceKeyVaultSshJumperSshSecretKey string + import { Sku as KeyVaultSku } from '../modules/keyvault/create.bicep' param keyVaultSku KeyVaultSku @@ -59,6 +64,7 @@ var secrets = { sourceKeyVaultSubscriptionId: sourceKeyVaultSubscriptionId sourceKeyVaultResourceGroup: sourceKeyVaultResourceGroup sourceKeyVaultName: sourceKeyVaultName + sourceKeyVaultSshSecretKey: sourceKeyVaultSshJumperSshSecretKey } var namePrefix = 'dp-be-${environment}' @@ -150,6 +156,21 @@ var srcKeyVault = { resourceGroupName: secrets.sourceKeyVaultResourceGroup } +module sshJumper '../modules/ssh-jumper/main.bicep' = { + scope: resourceGroup + name: 'sshJumper' + params: { + namePrefix: namePrefix + location: location + subnetId: vnet.outputs.defaultSubnetId + tags: tags + srcKeyVaultName: secrets.sourceKeyVaultName + srcKeyVaultSubId: secrets.sourceKeyVaultSubscriptionId + srcKeyVaultRGNName: secrets.sourceKeyVaultResourceGroup + srcKeyVaultSshSecretKey: secrets.sourceKeyVaultSshSecretKey + } +} + module postgresql '../modules/postgreSql/create.bicep' = { scope: resourceGroup name: 'postgresql' diff --git a/.azure/infrastructure/production.bicepparam b/.azure/infrastructure/production.bicepparam index 35479f9a9..2203c46f1 100644 --- a/.azure/infrastructure/production.bicepparam +++ b/.azure/infrastructure/production.bicepparam @@ -11,6 +11,7 @@ param dialogportenPgAdminPassword = readEnvironmentVariable('PG_ADMIN_PASSWORD') param sourceKeyVaultSubscriptionId = readEnvironmentVariable('SOURCE_KEY_VAULT_SUBSCRIPTION_ID') param sourceKeyVaultResourceGroup = readEnvironmentVariable('SOURCE_KEY_VAULT_RESOURCE_GROUP') param sourceKeyVaultName = readEnvironmentVariable('SOURCE_KEY_VAULT_NAME') +param sourceKeyVaultSshJumperSshSecretKey = readEnvironmentVariable('SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY') // SKUs param keyVaultSku = { diff --git a/.azure/infrastructure/soak.bicepparam b/.azure/infrastructure/soak.bicepparam index 76a7beae2..86745af63 100644 --- a/.azure/infrastructure/soak.bicepparam +++ b/.azure/infrastructure/soak.bicepparam @@ -11,6 +11,7 @@ param dialogportenPgAdminPassword = readEnvironmentVariable('PG_ADMIN_PASSWORD') param sourceKeyVaultSubscriptionId = readEnvironmentVariable('SOURCE_KEY_VAULT_SUBSCRIPTION_ID') param sourceKeyVaultResourceGroup = readEnvironmentVariable('SOURCE_KEY_VAULT_RESOURCE_GROUP') param sourceKeyVaultName = readEnvironmentVariable('SOURCE_KEY_VAULT_NAME') +param sourceKeyVaultSshJumperSshSecretKey = readEnvironmentVariable('SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY') // SKUs param keyVaultSku = { diff --git a/.azure/infrastructure/staging.bicepparam b/.azure/infrastructure/staging.bicepparam index f00cf24a2..170d3d400 100644 --- a/.azure/infrastructure/staging.bicepparam +++ b/.azure/infrastructure/staging.bicepparam @@ -11,6 +11,7 @@ param dialogportenPgAdminPassword = readEnvironmentVariable('PG_ADMIN_PASSWORD') param sourceKeyVaultSubscriptionId = readEnvironmentVariable('SOURCE_KEY_VAULT_SUBSCRIPTION_ID') param sourceKeyVaultResourceGroup = readEnvironmentVariable('SOURCE_KEY_VAULT_RESOURCE_GROUP') param sourceKeyVaultName = readEnvironmentVariable('SOURCE_KEY_VAULT_NAME') +param sourceKeyVaultSshJumperSshSecretKey = readEnvironmentVariable('SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY') // SKUs param keyVaultSku = { diff --git a/.azure/infrastructure/test.bicepparam b/.azure/infrastructure/test.bicepparam index b1c5de6b3..a823b7823 100644 --- a/.azure/infrastructure/test.bicepparam +++ b/.azure/infrastructure/test.bicepparam @@ -11,6 +11,7 @@ param dialogportenPgAdminPassword = readEnvironmentVariable('PG_ADMIN_PASSWORD') param sourceKeyVaultSubscriptionId = readEnvironmentVariable('SOURCE_KEY_VAULT_SUBSCRIPTION_ID') param sourceKeyVaultResourceGroup = readEnvironmentVariable('SOURCE_KEY_VAULT_RESOURCE_GROUP') param sourceKeyVaultName = readEnvironmentVariable('SOURCE_KEY_VAULT_NAME') +param sourceKeyVaultSshJumperSshSecretKey = readEnvironmentVariable('SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY') // SKUs param keyVaultSku = { diff --git a/.azure/modules/ssh-jumper/main.bicep b/.azure/modules/ssh-jumper/main.bicep new file mode 100644 index 000000000..0362a60d2 --- /dev/null +++ b/.azure/modules/ssh-jumper/main.bicep @@ -0,0 +1,146 @@ +@description('The name prefix to be used for the resource') +param namePrefix string + +@description('The location to deploy the resource to') +param location string + +@description('The subnet to deploy the network interface to') +param subnetId string + +@description('Tags to be applied to the resource') +param tags object + +@description('The name of the source Key Vault') +param srcKeyVaultName string + +@description('The subscription ID of the source Key Vault') +param srcKeyVaultSubId string + +@description('The resource group name of the source Key Vault') +param srcKeyVaultRGNName string + +@description('The SSH secret key to be used to get the ssh key for the virtual machine') +@secure() +param srcKeyVaultSshSecretKey string + +var name = '${namePrefix}-ssh-jumper' + +resource srcKeyVaultResource 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: srcKeyVaultName + scope: resourceGroup(srcKeyVaultSubId, srcKeyVaultRGNName) +} + +resource publicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = { + name: '${name}-ip' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + zones: [ + '1' + ] + properties: { + publicIPAddressVersion: 'IPv4' + publicIPAllocationMethod: 'Static' + idleTimeoutInMinutes: 4 + ipTags: [] + } + tags: tags +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2023-11-01' = { + name: name + location: location + properties: { + ipConfigurations: [ + { + name: '${name}-ipconfig' + type: 'Microsoft.Network/networkInterfaces/ipConfigurations' + properties: { + privateIPAddress: '10.0.0.5' + privateIPAllocationMethod: 'Dynamic' + publicIPAddress: { + id: publicIp.id + properties: { + deleteOption: 'Delete' + } + } + subnet: { + id: subnetId + } + primary: true + privateIPAddressVersion: 'IPv4' + } + } + ] + dnsSettings: { + dnsServers: [] + } + enableAcceleratedNetworking: false + enableIPForwarding: false + disableTcpStateTracking: false + nicType: 'Standard' + auxiliaryMode: 'None' + auxiliarySku: 'None' + } +} + +module virtualMachine '../../modules/virtualMachine/main.bicep' = { + name: name + params: { + name: name + sshKeyData: srcKeyVaultResource.getSecret(srcKeyVaultSshSecretKey) + location: location + tags: tags + hardwareProfile: { + vmSize: 'Standard_B1s' + } + additionalCapabilities: { + hibernationEnabled: false + } + storageProfile: { + imageReference: { + publisher: 'canonical' + offer: '0001-com-ubuntu-server-focal' + sku: '20_04-lts-gen2' + version: 'latest' + } + osDisk: { + osType: 'Linux' + name: '${name}-osdisk' + createOption: 'FromImage' + caching: 'ReadWrite' + managedDisk: { + storageAccountType: 'Premium_LRS' + } + deleteOption: 'Delete' + diskSizeGB: 30 + } + dataDisks: [] + diskControllerType: 'SCSI' + } + securityProfile: { + uefiSettings: { + secureBootEnabled: true + vTpmEnabled: true + } + securityType: 'TrustedLaunch' + } + networkProfile: { + networkInterfaces: [ + { + id: networkInterface.id + properties: { + deleteOption: 'Delete' + } + } + ] + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: true + } + } + } +} diff --git a/.azure/modules/virtualMachine/main.bicep b/.azure/modules/virtualMachine/main.bicep new file mode 100644 index 000000000..b09261c29 --- /dev/null +++ b/.azure/modules/virtualMachine/main.bicep @@ -0,0 +1,132 @@ +param name string +param location string +param tags object + +type HardwareProfile = { + vmSize: string +} +@description('Specifies the hardware profile for the virtual machine') +param hardwareProfile HardwareProfile + +type AdditionalCapabilities = { + hibernationEnabled: bool +} +@description('Specifies the additional capabilities for the virtual machine') +param additionalCapabilities AdditionalCapabilities + +type SecurityProfile = { + uefiSettings: { + secureBootEnabled: bool + vTpmEnabled: bool + } + securityType: string +} +@description('Specifies the security profile for the virtual machine') +param securityProfile SecurityProfile + +type NetworkInterface = { + id: string + properties: { + deleteOption: string + } +} +type NetworkProfile = { + networkInterfaces: NetworkInterface[] +} +@description('Specifies the network profile for the virtual machine') +param networkProfile NetworkProfile + +type DiagnosticsProfile = { + bootDiagnostics: { + enabled: bool + } +} +@description('Specifies the diagnostics profile for the virtual machine') +param diagnosticsProfile DiagnosticsProfile + +type StorageProfile = { + imageReference: { + publisher: string + offer: string + sku: string + version: string + } + osDisk: { + osType: string + name: string + createOption: string + caching: string + managedDisk: { + storageAccountType: string + } + deleteOption: string + diskSizeGB: int + } + dataDisks: array + diskControllerType: string +} +@description('Specifies the storage profile for the virtual machine') +param storageProfile StorageProfile + +@description('Specifies the SSH key data for the virtual machine') +@secure() +param sshKeyData string + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-03-01' = { + name: name + location: location + zones: [ + '1' + ] + properties: { + hardwareProfile: hardwareProfile + additionalCapabilities: additionalCapabilities + storageProfile: storageProfile + osProfile: { + computerName: name + adminUsername: name + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${name}/.ssh/authorized_keys' + keyData: sshKeyData + } + ] + } + provisionVMAgent: true + patchSettings: { + patchMode: 'AutomaticByPlatform' + automaticByPlatformSettings: { + rebootSetting: 'IfRequired' + bypassPlatformSafetyChecksOnUserSchedule: false + } + assessmentMode: 'ImageDefault' + } + } + secrets: [] + allowExtensionOperations: true + requireGuestProvisionSignal: true + } + securityProfile: securityProfile + networkProfile: networkProfile + diagnosticsProfile: diagnosticsProfile + } + identity: { + type: 'SystemAssigned' + } + tags: tags +} + +resource aadLoginExtension 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = { + parent: virtualMachine + name: 'AADSSHLoginForLinux' + location: location + properties: { + publisher: 'Microsoft.Azure.ActiveDirectory' + type: 'AADSSHLoginForLinux' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + } +} diff --git a/.github/workflows/action-deploy-infra.yml b/.github/workflows/action-deploy-infra.yml index a0e42e4ed..5a609c2dd 100644 --- a/.github/workflows/action-deploy-infra.yml +++ b/.github/workflows/action-deploy-infra.yml @@ -18,6 +18,8 @@ on: required: true AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: required: true + AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY: + required: true inputs: region: @@ -98,6 +100,7 @@ jobs: SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} + SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY }} with: scope: subscription template: ./.azure/infrastructure/main.bicep diff --git a/.github/workflows/ci-cd-main.yml b/.github/workflows/ci-cd-main.yml index e9fc9c283..a5405c5d8 100644 --- a/.github/workflows/ci-cd-main.yml +++ b/.github/workflows/ci-cd-main.yml @@ -74,6 +74,7 @@ jobs: AZURE_SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} + AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY }} with: environment: test region: norwayeast diff --git a/.github/workflows/ci-cd-pull-request.yml b/.github/workflows/ci-cd-pull-request.yml index 70a40973f..ad0c5d3e4 100644 --- a/.github/workflows/ci-cd-pull-request.yml +++ b/.github/workflows/ci-cd-pull-request.yml @@ -54,6 +54,7 @@ jobs: AZURE_SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} + AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY }} with: environment: test region: norwayeast diff --git a/.github/workflows/ci-cd-staging.yml b/.github/workflows/ci-cd-staging.yml index e5dc106e2..792e95205 100644 --- a/.github/workflows/ci-cd-staging.yml +++ b/.github/workflows/ci-cd-staging.yml @@ -42,6 +42,7 @@ jobs: AZURE_SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} + AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY }} with: environment: staging region: norwayeast diff --git a/.github/workflows/dispatch-infrastructure.yml b/.github/workflows/dispatch-infrastructure.yml index b8d558c09..5043b1ee8 100644 --- a/.github/workflows/dispatch-infrastructure.yml +++ b/.github/workflows/dispatch-infrastructure.yml @@ -36,6 +36,7 @@ jobs: AZURE_SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} + AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_SECRET_KEY }} with: environment: ${{ inputs.environment }} region: norwayeast diff --git a/README.md b/README.md index 447f0d621..e9e433d6e 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,24 @@ For example, to add a new storage account, you would: Refer to the existing infrastructure definitions as templates for creating new components. +#### Connecting to resources in Azure + +There is a `ssh-jumper` virtual machine deployed with the infrastructure. This can be used to create a `ssh`-tunnel into the `vnet`. Use one of the following methods to gain access to resources within the `vnet`: + +Ensure you log into the azure CLI using the relevant user and subscription using `az login`. + +- Connect to the VNet using the following command: + ``` + az ssh vm --resource-group dp-be--rg --vm-name dp-be--ssh-jumper + ``` + (You may be prompted to install the ssh extension for the azure cli) + +- To create an SSH tunnel for accessing specific resources (e.g., PostgreSQL database), use: + ``` + az ssh vm -g dp-be--rg -n dp-be--ssh-jumper -- -L 5432::5432 + ``` + This example forwards the PostgreSQL default port (5432) to your localhost. Adjust the ports and hostnames as needed for other resources. + ### Applications All application Bicep definitions are located in the `.azure/applications` folder. To add a new application, follow the existing pattern found within this directory. This involves creating a new folder for your application under `.azure/applications` and adding the necessary Bicep files (`main.bicep` and environment-specific parameter files, e.g., `test.bicepparam`, `staging.bicepparam`).