Committing the secrets along with application codes to a repository is one of the most commonly made mistakes by many developers. This can get nasty when an application is developed for Cloud deployment. You probably have read the story of checking in AWS S3 secrets to GitHub. The developer corrected the mistake in 5 mins, but still received a hefty invoice because of bots that crawl open source sites, looking for secrets. There are many tools that can scan codes for potential secret leakages, they can be embedded in CI/CD pipeline. These tools do a great job in finding out deliberate or unintentional commits that contains secrets before they get merged to a release/master branch. However, they are not absolutely protecting all potential secrets leaks. Developers still need to be carefully review their codes on every commits.
Azure Managed Service Instance (MSI) can address this problem in a very neat way. MSI has the potential to design application that are secret-less. There is no need to have any secrets (specially secrets for database connection strings, storage keys etc.) at all application codes.
Secret management in application
Let’s recall how we were doing secret management yesterday. Simplicity’s sake, we have a web application that is backed by a SQL server. This means, we almost certainly have a configuration key (SQL Connection String) in our configuration file. If we have storage accounts, we might have the Shared Access Signature (aka SAS token) in our config file.
As we see, we’re adding secrets one after another in our configuration file – in plain text format. We need now, credential scanner tasks in our pipelines, having some local configuration files in place (for local developments) and we need to mitigate the mistakes of checking in secrets to repository.
Azure Key Vault as secret store
Azure Key Vault can simplify these above a lot, and make things much cleaner. We can store the secrets in a Key Vault and in CI/CD pipeline, we can get them from vault and write them in configuration files, just before we publish the application code into the cloud infrastructure. VSTS build and release pipeline have a concept of Library, that can be linked with Key vault secrets, designed just to do that. The configuration file in this case should have some sort of String Placeholders that will be replaced with secrets during CD execution.
The above works great, but you still have a configuration file with all the placeholders for secrets (when you have multiple services that has secrets) – which makes it difficult to manage for local development and cloud developments. An improvement can be keep all the secrets in Key Vault, and let the application load those secrets runtime (during the startup event) directly from the Key vault. This is way easier to manage and also pretty clean solution. The local environment can use a different key vault than production, the configuration logic becomes extremely simpler and the configuration file now have only one secret. That’s a Service Principal secret – which can be used to talk to the key vault during startup.
So we get all the secrets stored in a vault and exactly one secret in our configuration file – nice! But if we accidentally commit this very single secret, all other secrets in vault are also compromised. What we can do to make this more secure? Let’s recap our knowledge about service principals before we draw the solution.
What is Service Principal?
A resource that is secured by Azure AD tenant, can only be accessed by a security principal. A user is granted access to a AD resource on his security principal, known as User Principal. When a service (a piece of software code) wants to access a secure resource, it needs to use a security principal of a Azure AD Application Object. We call them Service Principal. You can think of Service Principals as an instance of an Azure AD Application.A service principal has a secret, often referred as Client Secret. This can be analogous to the password of a user principal. The Service Principal ID (often known as Application ID or Client ID) and Client Secret together can authenticate an application to Azure AD for a secure resource access. In our earlier example, we needed to keep this client secret (the only secret) in our configuration file, to gain access to the Key vault. Client secrets have expiration period that up to the application developers to renew to keep things more secure. In a large solution this can easily turn into a difficult job to keep all the service principal secrets renewed with short expiration time.
Managed Service Identity
Managed Service Identity is explained in Microsoft Documents in details. In layman’s term, MSI literally is a Service Principal, created directly by Azure and it’s client secret is stored and rotated by Azure as well. Therefore it is “managed”. If we create a Azure web app and turn on Manage Service Identity on it (which is just a toggle switch) – Azure will provision an Application Object in AD (Azure Active Directory for the tenant) and create a Service Principal for it and store the client secret somewhere – that we don’t care. This MSI now represents the web application identity in Azure AD.
Managed Service Identity can be provisioned in Azure Portal, Azure Power-Shell or Azure CLI as below:
az login az group create --name myResourceGroup --location westus az appservice plan create --name myPlan --resource-group myResourceGroup --sku S1 az webapp create --name myApp --plan myPlan --resource-group myResourceGroup az webapp identity assign --name myApp --resource-group myResourceGroup
Or via Azure Resource Manager Template:
{ "apiVersion": "2016-08-01", "type": "Microsoft.Web/sites", "name": "[variables('appName')]", "location": "[resourceGroup().location]", "identity": { "type": "SystemAssigned" }, "properties": { "name": "[variables('appName')]", "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", "hostingEnvironment": "", "clientAffinityEnabled": false, "alwaysOn": true }, "dependsOn": [ "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]" ]}
Going back to our key vault example, with MSI we can now eliminate the client secret of Service Principal from our application code.
But wait! We used to read keys/secrets from Key vault during the application startup, and we needed that client secret for that. How we are going to talk to Key vault now without the secret?
Using MSI from App service
Azure provides couple of environment variables for app services that has MSI enabled.
- MSI_ENDPOINT
- MSI_SECRET
The first one is a URL that our application can make a request to, with the MSI_SECRET as parameter and the response will be a access token that will let us talk to the key vault. This sounds a bit complex, but fortunately we don’t need to do that by hand.
Microsoft.Azure.Services.AppAuthentication library for .NET wraps these complexities for us and provides an easy API to get the access token returned.
We need to add references to the Microsoft.Azure.Services.AppAuthentication and Microsoft.Azure.KeyVault NuGet packages to our application.
Now we can get the access token to communicate to the key vault in our startup like following:
using Microsoft.Azure.Services.AppAuthentication; using Microsoft.Azure.KeyVault; // ... var azureServiceTokenProvider = new AzureServiceTokenProvider(); string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://management.azure.com/"); // OR var kv = new KeyVaultClient(new KeyVaultClient .AuthenticationCallback (azureServiceTokenProvider.KeyVaultTokenCallback));
This is neat, agree? We now have our application configuration file that has no secrets or keys whatsoever. Isn’t it cool?
Step up – activating zero-secret mode
We have managed deploying our web application with zero secret above. However, we still have secrets for SQL database, storage accounts etc. in our key vault, we just don’t have to put them in our configuration files. But they are still there and loaded in startup event of our web application. This is a great improvement, of course. But MSI allows us to take this even better stage.
Azure AD Authentication for Azure Services
To leverage MSI’s full potentials we should use Azure AD authentication (RBAC controls). For instance, we have been using Shared Access Signatures or SQL connection strings to communicate Azure Storage/Service Bus and SQL servers. With AD authentication, we will use a security principal that has a role assignment with Azure RBAC.
Azure gradually enabling AD authentication for resources. As of today (time of writing this blog) the following services/resources supports AD authentication with Managed Service Identity.
Service | Resource ID | Status | Date | Assign access |
Azure Resource Manager | https://management.azure.com/ | Available | September 2017 | Azure portal PowerShell Azure CLI |
Azure Key Vault | https://vault.azure.net | Available | September 2017 | |
Azure Data Lake | https://datalake.azure.net/ | Available | September 2017 | |
Azure SQL | https://database.windows.net/ | Available | October 2017 | |
Azure Event Hubs | https://eventhubs.azure.net | Available | December 2017 | |
Azure Service Bus | https://servicebus.azure.net | Available | December 2017 | |
Azure Storage | https://storage.azure.com/ | Preview | May 2018 |
Read more updated info here.
AD authentication finally allows us to completely remove those secrets from Key vaults and directly access to the storage account, Data lake stores, SQL servers with MSI tokens. Let’s see some examples to understand this.
Example: Accessing Storage Queues with MSI
In our earlier example, we talked about the Azure web app, for which we have enabled Managed Service Identity. In this example we will see how we can put a message in Azure Storage Queue using MSI. Assuming our web application name is:
contoso-msi-web-app
Once we have enabled the managed service identity for this web app, Azure provisioned an identity (an AD Application object and a Service Principal for it) with the same name as the web application, i.e. contoso-msi-web-app.
Now we need to set role assignment for this Service Principal so that it can access to the storage account. We can do that in Azure Portal. Go to the Azure Portal IAM blade (the access control page) and add a role for this principal to the storage account. Of course, you can also do that with Power-Shell.
If you are not doing it in Portal, you need to know the ID of the MSI. Here’s how you get that: (in Azure CLI console)
az resource show -n $webApp -g $resourceGroup --resource-type Microsoft.Web/sites --query identity
You should see an output like following:
{ "principalId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "tenantId": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx", "type": null }
The Principal ID is what you are after. We can now assign roles for this principal as follows:
$exitingRoleDef = Get-AzureRmRoleAssignment ` -ObjectId ` -RoleDefinitionName "Contributor" ` -ResourceGroupName "RGP NAME" If ($exitingRoleDef -eq $null) { New-AzureRmRoleAssignment ` -ObjectId ` -RoleDefinitionName "Contributor" ` -ResourceGroupName "RGP NAME" }
You can run these commands in CD pipeline with Azure Inline Power Shell tasks in VSTS release pipelines.
Let’s write a MSI token helper class.
internal class TokenHelper | |
{ | |
internal async Task<string> GetManagementApiAccessTokenAsync() | |
{ | |
var astp = new AzureServiceTokenProvider(); | |
var accessToken = await astp | |
.GetAccessTokenAsync(Constants.AzureManagementAPI); | |
return accessToken; | |
} | |
} |
We will use the Token Helper in a Storage Account helper class.
internal class StorageAccountHelper | |
{ | |
internal async Task<StorageKeys> GetStorageKeysAsync() | |
{ | |
var token = await new TokenHelper().GetManagementApiAccessTokenAsync(); | |
return await GetStorageKeysAsync(token); | |
} | |
internal async Task<StorageKeys> GetStorageKeysAsync(string token) | |
{ | |
var uri = new Uri($"{Constants.AzureManagementAPI}subscriptions/{Constants.SubscriptionID}/resourceGroups/{Constants.ResourceGroupName}/providers/Microsoft.Storage/storageAccounts/{Constants.StorageAccountName}/listKeys?api-version=2016-01-01"); | |
var content = new StringContent(string.Empty, Encoding.UTF8, "text/html"); | |
using (var httpClient = new HttpClient()) | |
{ | |
httpClient.DefaultRequestHeaders.Authorization | |
= new AuthenticationHeaderValue("Bearer", token); | |
using (var response = await httpClient.PostAsync(uri, content)) | |
{ | |
var responseText = await response.Content.ReadAsStringAsync(); | |
var keys = JsonConvert.DeserializeObject<StorageKeys>(responseText); | |
return keys; | |
} | |
} | |
} | |
} |
Now, let’s write a message into the Storage Queue.
public class MessageClient | |
{ | |
public MessageClient() | |
{ | |
} | |
public virtual async Task SendAsync(string message) | |
{ | |
var cq = await GetQueueClient(); | |
await cq.AddMessageAsync(new CloudQueueMessage(message)); | |
} | |
private static async Task<CloudQueue> GetQueueClient( ) | |
{ | |
var keys = await new StorageAccountHelper().GetStorageKeysAsync(); | |
var storageCredentials = | |
new StorageCredentials(Constants.StorageAccountName, keys.Keys.First().Value); | |
var csa = new CloudStorageAccount(storageCredentials, true); | |
var cqc = csa.CreateCloudQueueClient(); | |
var cq = cqc.GetQueueReference(Constants.QueueName); | |
await cq.CreateIfNotExistsAsync(); | |
return cq; | |
} | |
} |
Isn’t it awesome?
Another example, this time SQL server
As of now, Azure SQL Database does not support creating logins or users from service principals created from Managed Service Identity. Fortunately, we have workaround. We can add the MSI principal an AAD group as member, and then grant access to the group to the database.
We can use the Azure CLI to create the group and add our MSI to it:
az ad group create --display-name sqlusers --mail-nickname 'NotNeeded'az ad group member add -g sqlusers --member-id xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
Again, we are using the MSI id as member id parameter here.
Next step, we need to allow this group to access SQL database. PowerShell rescues again:
$query = @"CREATE USER [$adGroupName] FROM EXTERNAL PROVIDER GO ALTER ROLE db_owner ADD MEMBER [$adGroupName] "@ sqlcmd.exe -S "tcp:$sqlServer,1433" ` -N -C -d $database -G -U $sqlAdmin.UserName ` -P $sqlAdmin.GetNetworkCredential().Password ` -Q $query
Let’s write a token helper class for SQL as we did before for storage queue.
public static class TokenHelper | |
{ | |
public static Task<String> GetTokenAsync() | |
{ | |
var provider = new AzureServiceTokenProvider(); | |
return provider.GetAccessTokenAsync("https://database.windows.net/"); | |
} | |
} | |
public static class SqlConnectionExtensions | |
{ | |
public async static Task<TPayload> ExecuteScalar<TPayload>(string commandText) | |
where TPayload: class | |
{ | |
var connectionString = "connection string without credentails"; | |
var token = await ADAuthentication.GetSqlTokenAsync(); | |
using (var conn = new SqlConnection(connectionString)) | |
{ | |
conn.AccessToken = token; | |
await conn.OpenAsync(); | |
using (var cmd = new SqlCommand(commandText, conn)) | |
{ | |
var result = await cmd.ExecuteScalarAsync(); | |
return result as TPayload; | |
} | |
} | |
} | |
} |
We are almost done, now we can run SQL commands from web app like this:
public class WebApp | |
{ | |
public async static void StartUp() | |
{ | |
var userName = await SqlConnectionExtensions | |
.ExecuteScalar<string>("SELECT SUSER_SNAME()"); | |
} | |
} |
Voila!
Conclusion
Managed Service Identity is awesome and powerful, it really drives application where security of the application are easy to manage over longer period. Specially when you have lots of applications you end up with huge number of service principals. Managing their secrets over time, keeping track of their expiration is a nightmare. Managed Service makes it so beautiful!
Thanks for reading!
Hi, i read your blog from time to time and i own a similar one and i was just curious if you get a lot of spam feedback? If so how do you protect against it, any plugin or anything you can recommend? I get so much lately it’s driving me mad so any support is very much appreciated.|
LikeLike
Thats what an angel is. Dust pressed into a diamond by the weight of this world. #TheOA needs your help. #SaveTheOA
LikeLike
Somebody essentially assist to make significantly posts I might state. This is the first time I frequented your web page and thus far? I amazed with the analysis you made to create this actual submit incredible. Wonderful task!|
LikeLike
Hey There. I found your weblog the usage of msn. That is an extremely neatly written article. I’ll be sure to bookmark it and return to read more of your helpful information. Thanks for the post. I will certainly comeback.|
LikeLike
Hi this is somewhat of off topic but I was wanting to know if blogs use WYSIWYG editors or if you have to manually code with HTML. I’m starting a blog soon but have no coding experience so I wanted to get advice from someone with experience. Any help would be enormously appreciated!|
LikeLike
Hi there! I know this is kinda off topic but I was wondering if you knew where I could find a captcha plugin for my comment form? I’m using the same blog platform as yours and I’m having problems finding one? Thanks a lot!|
LikeLike