One of the key challenges we have when working with cloud-based infrastructure is how to secure them effectively, particularly if you're creating multiple environments.
For those with less engineering experience, it's often the case that you'll create resources in Azure manually, and copy-and-paste connection strings from the portal directly into your source code and commit them. Obviously, this is a security risk - if someone gets access to your source control, they can access - and probably delete - your resources and / or data. Or even worse, change the code on that works with the data!
As we gain experience, we might move to taking secrets out of source control and into e.g. Azure App Settings or perhaps a dedicated service such as KeyVault. However, if you're trying to deploy your resources with an infrastructure-as-code approach, it gets more challenging because you ideally want to provide the authorisation token during the deployment process. Otherwise, your deployment process may create the infrastructure that you require, but configuring the infrastructure components to talk to each other will still be a manual task.
Creating zero-configuration deployments
Farmer already has excellent support for working with settings and "auto configure", using features such as ARM template Secure Parameters to avoid ever seeing secrets, and / or using ARM expressions to calculate connection strings at deployment time, so you never have to even see them yourself:
let storage = storageAccount {
name "mystorage"
}
let app = webApp {
name "myApp"
setting "storagekey" storage.Key
}
The storage.Key
member is an ARM expression. It's not a connection string (it can't be, if you think about it - the storage account doesn't yet exist!). Instead, think of it as a code instruction that says "when the storage account is created, only then retrieve the key and set it as this setting".
Farmer also now no longer requires you to explicitly add the Storage Account as a dependency on the Web App - it will automatically infer the dependency and set it for you.
However, the catch here is that we're still ultimately setting a connection string into the web application. Even if we move to using something like Key Vault and setting the secret there, there's still a secret that's required to be sent and stored on the web app. Even though the chance is probably relatively minimal that it could leak, it still exists e.g. imagine a diagnostics page that shows connection strings for development accidentally gets pushed into the live system.
Moving to identity-based trust
An alternative way of working is to flip the responsibilities around: instead of giving a connection string to the web app in order to anonymously connect to the storage account, have the storage account explicitly grant permissions to the web application itself! In this model, there's no key or secret that the web application requires. Instead, the web application connects with a specific identity which the storage account trusts.
Here's how we model managed identities in Farmer:
let app = webApp {
name "myApp"
system_identity
}
let storage = storageAccount {
name "mystorage"
grant_access app.SystemIdentity Roles.StorageBlobDataOwner
}
There are two key keywords here. First is system_identity
, which "activates" the identity of the web app; secondly is the grant_access
keyword on the storage account, which grants the StorageBlobDataOwner role to the web application. Once you've done this, you can use e.g. the Azure.Identity NuGet package in code to connect to the Storage Account without needing a key, at all.
There are many benefits to this approach, including:
- Obviously, no secrets are ever passed around, so there's nothing to leak.
- You can amend permissions outside the lifecycle of the web application.
- You can have different permissions for different applications or users.
An interesting observation here is that doing this also flips the dependencies around - the Storage Account is now dependent on the Web App, rather than the inverse.
Using User Assigned Identities
Farmer also has great support for what are known as user assigned identities. Unlike the "system" identity shown above, these identities live outside the scope of any individual resource and can be shared across multiple resources - so you could have a virtual machine, a container and a web app all connecting to your storage account with the same identity.
let myIdentity = userAssignedIdentity {
name "myidentity"
}
let app = webApp {
name "myApp"
add_identity myIdentity
}
let myOtherApp = webApp {
name "myOtherApp"
add_identity myIdentity
}
let storage = storageAccount {
name "mystorage"
grant_access myIdentity Roles.StorageBlobDataOwner
}
The first value represents the new identity we'll be creating (this is a full Azure Active Directory identity). We then add this identity to both web apps, and finally grant a permission to it in the storage account.
Summary
We're really excited about this. One of the core goals of Farmer is to make it easy for people building infrastructure-as-code to "do the right thing". In terms of secret management, we've achieved this in several ways:
- Providing the ability to use "secret" settings in applications, so that they don't show in your ARM templates.
- Providing ARM expressions to "derive" secrets at deployment time, so that they never hit your code.
- Providing excellent KeyVault integration.
We now are starting the process of offering another option:
- Managed identity support.
At this point, the latest beta of Farmer 1.2 has the capabilities shown above, so you can try it out today - you can read up more on our docs site here. Going forward, we'll be adding identity support to more and more resources, such as databases and other data services, which are prime candidates for identity usage.
We hope you have have (fun _ -> ()) with Farmer!
Isaac