Managing Secrets and Secure Access in Azure Applications
ADD
azure-ts-msi-keyvault-rbac/Pulumi.yaml
+name: azure-msi-keyvault-rbac
+runtime: nodejs
+description: Example of managing the secrets and permissions via services and features like KeyVault, AD Managed Identity, AD RBAC
+template:
+ config:
+ azure:environment:
+ description: The Azure environment to use (`public`, `usgovernment`, `german`, `china`)
+ default: public

ADD
azure-ts-msi-keyvault-rbac/README.md
+[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new)
+
+# Managing Secrets and Secure Access in Azure Applications
+
+[Managed identities](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/) for Azure resources provides Azure services with an automatically managed identity in Azure Active Directory (Azure AD).
+
+This example demostrates using a managed identity with Azure App Service to access Azure KeyVault, Azure Storage, and Azure SQL Database without passwords or secrets.
+
+The application consists of several parts:
+
+- An ASP.NET Application which reads data from a SQL Database and from a file in Blob Storage
+- App Service which host the application. The application binaries are placed in Blob Storage, with Blob Url placed as a secret in Azure Key Vault
+- App Service has a Managed Identity enabled
+- The identify is granted access to the SQL Server, Blob Storage, and Key Vault
+- No secret information is placed in App Service configuration: all access rights are derived from Active Directory
+
+## Running the App
+
+1. Create a new stack:
+
+ ```
+ $ pulumi stack init dev
+ ```
+
+1. Login to Azure CLI (you will be prompted to do this during deployment if you forget this step):
+
+ ```
+ $ az login
+ ```
+
+1. Restore NPM dependencies:
+
+ ```
+ $ npm install
+ ```
+
+1. Build and publish the ASP.NET Core project:
+
+ ```
+ $ dotnet publish webapp
+ ```
+
+1. Run `pulumi up` to preview and deploy changes:
+
+ ```
+ $ pulumi up
+ Previewing changes:
+ ...
+
+ Performing changes:
+ ...
+ info: 15 changes performed:
+ + 15 resources created
+ Update duration: 4m16s
+ ```
+
+1. Check the deployed website endpoint:
+
+ ```
+ $ pulumi stack output endpoint
+ https://app129968b8.azurewebsites.net/
+ $ curl "$(pulumi stack output endpoint)"
+ Hello 311378b3-16b7-4889-a8d7-2eb77478beba@50f73f6a-e8e3-46b6-969c-bf026712a650! Here is your...
+ ```

ADD
azure-ts-msi-keyvault-rbac/index.ts
+import * as pulumi from "@pulumi/pulumi";
+import * as azure from "@pulumi/azure";
+import { signedBlobReadUrl } from "./sas";
+
+// Create a resource group
+const resourceGroup = new azure.core.ResourceGroup("resourceGroup", {
+ location: "West US",
+});
+
+// Create a storage account for Blobs
+const storageAccount = new azure.storage.Account("storage", {
+ resourceGroupName: resourceGroup.name,
+ accountReplicationType: "LRS",
+ accountTier: "Standard",
+});
+
+// The container to put our files into
+const storageContainer = new azure.storage.Container("files", {
+ resourceGroupName: resourceGroup.name,
+ storageAccountName: storageAccount.name,
+ containerAccessType: "private",
+});
+
+// Azure SQL Server that we want to access from the application
+const sqlServer = new azure.sql.SqlServer("sqlserver", {
+ resourceGroupName: resourceGroup.name,
+
+ administratorLogin: "manualadmin",
+ administratorLoginPassword: "WeWontUseTh1sPwd",
+ version: "12.0",
+});
+
+// Azure SQL Database that we want to access from the application
+const database = new azure.sql.Database("sqldb", {
+ resourceGroupName: resourceGroup.name,
+ serverName: sqlServer.name,
+ requestedServiceObjectiveName: "S0"
+});
+
+// The connection string that has no credentials in it: authertication will come through MSI
+const connectionString = pulumi.all([sqlServer.name, database.name]).apply(([server, db]) =>
+ `Server=tcp:${server}.database.windows.net;Database=${db};`);
+
+// A file in Blob Storage that we want to access from the application
+const textBlob = new azure.storage.Blob("text", {
+ resourceGroupName: resourceGroup.name,
+ storageAccountName: storageAccount.name,
+ storageContainerName: storageContainer.name,
+ type: "block",
+ source: "./README.md",
+});
+
+// A plan to host the App Service
+const appServicePlan = new azure.appservice.Plan("asp", {
+ resourceGroupName: resourceGroup.name,
+ kind: "App",
+ sku: {
+ tier: "Basic",
+ size: "B1",
+ },
+});
+
+// ASP.NET deployment package
+const blob = new azure.storage.ZipBlob("zip", {
+ resourceGroupName: resourceGroup.name,
+ storageAccountName: storageAccount.name,
+ storageContainerName: storageContainer.name,
+ type: "block",
+
+ content: new pulumi.asset.FileArchive("./webapp/bin/Debug/netcoreapp2.2/publish")
+});
+
+const clientConfig = pulumi.output(azure.core.getClientConfig({}));
+export const currentPricipal = clientConfig.apply(c => c.servicePrincipalObjectId);
+const tenantId = clientConfig.apply(c => c.tenantId);
+
+// Key Vault to store secrets (e.g. Blob URL with SAS)
+const vault = new azure.keyvault.KeyVault("vault", {
+ resourceGroupName: resourceGroup.name,
+ sku: {
+ name: "standard",
+ },
+ tenantId: tenantId,
+ // It won't setup any access if pulumi is executed under user account
+ accessPolicies: currentPricipal.apply(objectId =>
+ objectId !== ""
+ ? [{
+ tenantId,
+ objectId,
+ secretPermissions: ["delete", "get", "list", "set"],
+ }]
+ : []
+ ),
+});
+
+// Put the URL of the zip Blob to KV
+const secret = new azure.keyvault.Secret("deployment-zip", {
+ keyVaultId: vault.id,
+ value: signedBlobReadUrl(blob, storageAccount, storageContainer),
+});
+const secretUri = pulumi.interpolate`${secret.vaultUri}secrets/${secret.name}/${secret.version}`;
+
+// The application hosted in App Service
+const app = new azure.appservice.AppService("app", {
+ resourceGroupName: resourceGroup.name,
+ appServicePlanId: appServicePlan.id,
+
+ // A system-assigned managed service identity to be used for authentication and authorization to the SQL Database and the Blob Storage
+ identity: {
+ type: "SystemAssigned"
+ },
+
+ appSettings: {
+ // Website is deployed from a URL read from the Key Vault
+ "WEBSITE_RUN_FROM_ZIP": pulumi.interpolate`@Microsoft.KeyVault(SecretUri=${secretUri})`,
+
+ // Note that we simply provide the URL without SAS or keys
+ "StorageBlobUrl": textBlob.url,
+ },
+
+ // A SQL connection string, still without secrets in it
+ connectionStrings: [{
+ name: "db",
+ value: connectionString,
+ type: "SQLAzure"
+ }]
+});
+
+// Grant App Service access to KV secrets
+new azure.keyvault.AccessPolicy("app-policy", {
+ keyVaultId: vault.id,
+ tenantId: tenantId,
+ objectId: app.identity.principalId,
+ secretPermissions: ["get"],
+});
+
+// Make the App Service the admin of the SQL Server (double check if you want a more fine-grained security model in your real app)
+const sqlAdmin = new azure.sql.ActiveDirectoryAdministrator("adadmin", {
+ resourceGroupName: resourceGroup.name,
+ tenantId: tenantId,
+ objectId: app.identity.principalId,
+ login: "adadmin",
+ serverName: sqlServer.name,
+});
+
+// Grant access from App Service to the container in the storage
+const blobPermission = new azure.role.Assignment("readblob", {
+ principalId: app.identity.principalId,
+ scope: pulumi.interpolate`${storageAccount.id}/blobServices/default/containers/${storageContainer.name}`,
+ roleDefinitionName: "Storage Blob Data Reader",
+});
+
+// Add SQL firewall exceptions
+const firewallRules = app.outboundIpAddresses.apply(
+ ips => ips.split(',').map(
+ ip => new azure.sql.FirewallRule(`FR${ip}`, {
+ resourceGroupName: resourceGroup.name,
+ startIpAddress: ip,
+ endIpAddress: ip,
+ serverName: sqlServer.name,
+ })
+ ));
+
+export const endpoint = pulumi.interpolate `https://${app.defaultSiteHostname}`;

ADD
azure-ts-msi-keyvault-rbac/package.json
+{
+ "name": "azure-msi-keyvault-rbac",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@types/node": "^10.3.3"
+ },
+ "dependencies": {
+ "@pulumi/azure": "latest",
+ "@pulumi/pulumi": "latest"
+ }
+}

ADD
azure-ts-msi-keyvault-rbac/sas.ts
+import * as pulumi from "@pulumi/pulumi";
+import * as azure from "@pulumi/azure";
+
+import * as azurestorage from "azure-storage";
+
+// Given an Azure blob, create a SAS URL that can read it.
+export function signedBlobReadUrl(
+ blob: azure.storage.Blob | azure.storage.ZipBlob,
+ account: azure.storage.Account,
+ container: azure.storage.Container,
+): pulumi.Output<string> {
+ // Choose a fixed, far-future expiration date for signed blob URLs.
+ // The shared access signature (SAS) we generate for the Azure storage blob must remain valid for as long as the
+ // Function App is deployed, since new instances will download the code on startup. By using a fixed date, rather
+ // than (e.g.) "today plus ten years", the signing operation is idempotent.
+ const signatureExpiration = new Date(2100, 1);
+
+ return pulumi.all([
+ account.primaryConnectionString,
+ container.name,
+ blob.name,
+ ]).apply(([connectionString, containerName, blobName]) => {
+ let blobService = new azurestorage.BlobService(connectionString);
+ let signature = blobService.generateSharedAccessSignature(
+ containerName,
+ blobName,
+ {
+ AccessPolicy: {
+ Expiry: signatureExpiration,
+ Permissions: azurestorage.BlobUtilities.SharedAccessPermissions.READ,
+ },
+ }
+ );
+
+ return blobService.getUrl(containerName, blobName, signature);
+ });
+}

ADD
azure-ts-msi-keyvault-rbac/tsconfig.json
+{
+ "compilerOptions": {
+ "outDir": "bin",
+ "target": "es6",
+ "lib": [
+ "es6"
+ ],
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "experimentalDecorators": true,
+ "pretty": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "forceConsistentCasingInFileNames": true,
+ "strictNullChecks": true
+ },
+ "files": [
+ "index.ts",
+ "sas.ts"
+ ]
+}

ADD
azure-ts-msi-keyvault-rbac/webapp/webapp.csproj
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.2</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore" Version="2.1.7" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
+ <PackageReference Include="System.Data.SqlClient" Version="4.6.0" />
+ <PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.0.3" />
+ <PackageReference Include="Microsoft.Azure.Storage.Blob" Version="10.0.3" />
+ </ItemGroup>
+
+</Project>

Managing Secrets and Secure Access in Azure Applications