It’s considered a bad practice to store any private information in source control, in large part due to a few accidental disclosures on GitHub that cost thousands in cloud bills. So many .NET applications use the Web.config, App.config and appsettings.json to store API keys, Service Bus access keys, third party credentials, and more that I’m surprised this happened more often.
But as it turns out, securing any type of sensitive information from end-to-end isn’t as easy as it sounds. A bad commit here or a forgotten string there could mean the difference between success and failure.
Below I outline a process to keep secrets safe when developing with Azure Functions and Azure DevOps. It uses the Application Settings to store and retrieve secret information from the local machine where the code is written all the way out to the cloud where it runs.
Outlining The Process
The process I’ve adopted to keep secrets safe in Azure Functions looks like this:
- Local Development
- Write and test Azure Functions on a local machine, be it with Visual Studio, the Functions CLI, or VS Code.
- Ensure that local.settings.json is excluded from source control.
- Store keys, credentials and secrets used for local development in local.settings.json.
- Build, Run, and Test the Function locally.
- Build and Release Pipeline
- Store keys, credentials and secrets to be used during deployment in a variable group. Each variable should be tagged as a secret variable to protect it.
- The Build Pipeline is triggered whenever a change is pushed to Git:
- The
dotnet build
command builds the Function App - Unit tests are run against the compiled code
- The
dotnet publish
command packages the Function App into a Zip file for release. - Publish the Zipped artifact for deployment.
- No secrets are needed at any point of the build process.
- The
- The Release Pipeline deploys, configures, and tests the Function App:
- The zipped artifact is deployed to Azure via the Azure App Service Deploy task.
- Sets Application Setting secrets via the Azure CLI task by running a script that calls
az functionapp config appsettings set
for each setting that needs to be set. The variables’ secret values are passed by parameter to the script. - Acceptance tests are run to ensure the Function App is running as expected.
I’ll detail some of the finer points about securing the credentials next.
Securing Local Development
The Azure Functions team has made it easier than ever to code, run and test Azure Functions locally. Functions can be run from a development machine, but only if it has all the credentials it needs to do its work.
In a traditional Web API project, configuration keys were stored in the Web.config. Its equivalent in the Azure Functions world is called Application Settings, and can be accessed through the Portal for the Function App in question.
A file called local.settings.json is placed at the root of a Function App project when developing locally. This file mimics the Application Settings of the deployed Function App. Both the Application Settings and local.settings.json are dictionaries. Here’s what the local.settings.json looks like for a Function App that calls an imaginary third party API with a secret API key:
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "blobstoragekey", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "ImaginaryApiKey": "SecretApiKeyValue" } }
At runtime, the Function retrieves the ImaginaryApiKey with the Environment.GetEnvironmentVariable("ImaginaryApiKey")
command. Any other keys, credentials, and connection strings that are in App Settings can be retrieved at runtime in the same way.
Here’s the really neat thing: the local.settings.json file isn’t needed to deploy to Azure, and so can be excluded from source control. Excluding it from source control means that any sensitive information it contains won’t be visible by wrong-doers, reducing the risk of accidental exposure through a public repository.
The latest templates supplied by the Functions CLI automatically excludes the file already, so in theory there’s nothing to do. It doesn’t hurt to verify that the file is listed in your .gitignore, and remember that the file needs to be deleted if it was previously added to source control, otherwise Git continues to track it.
Developers will have to share the file through some other means if it isn’t in source control. Luckily, it can always be retrieved by downloading the source code for the Function App in question from the Portal. So the information is not lost forever even if the only person with technical knowledge on Azure Functions walks out the door.
Securing Build And Release Pipelines
Deploying Functions to Azure has never been easier with a wide range of products that can create a rock-solid build and deployment strategy. Securing credentials is just as easy to do, but there are a few things to keep in mind.
Avoid using the same credentials and keys across environments. It’s more work to set up but any resources used by a Function App should use different secrets across the local, development, staging, and production environments. Doing so puts the odds in your favor that an accidental disclosure won’t compromise your entire operation.
Use secure environment variables within Azure DevOps for any sensitive values. A variable is marked as secure by clicking on the padlock once its value is set. Doing so encrypts the value, and users can no longer retrieve it. Only the tasks and processes that use the variable can decrypt it.
Another, more scalable way to manage secrets is to use Azure KeyVault to store sensitive information. Key Vault is specialized in keeping information secure, and has the added benefit of centralizing all secrets in a single place. From within Azure DevOps, the secrets can be retrieved in a Build or Release Pipeline with the Azure Key Vault Task.
This approach keeps secrets out of source control, making it hard for a malicious user to get at them. Obviously, someone still needs to know how to get to those secrets. But it’s much easier to restrict access when there is a single source of truth for all sensitive data.
Secured from A to Z
There are certainly other ways to achieve what I just outlined, but I chose this approach since I was familiar with the tooling involved, and feel confident that it be set up with little overhead. Let me know if you’ve found better ways of keeping secrets in Azure Functions in the comments below!
We are developing a CI CD pipeline for Azure functions too.
The question I had was – if a new key is needed by the application, say the function app, would I be manually communicating the details of the key to the Devops team?
Is there a more graceful way of handling it?
LikeLike
Hi Satish, thanks for the question. There is definitely a gray zone with little guidance on that part. The approach I’ve found to work well is for both the developers and DevOps engineers to have access to whatever secure store is being used for the keys. Let’s say you’re keeping the keys in secure variables in Azure DevOps, both teams should be able to read and write to the library that contains those variables. Access should be restricted to only those who need to be able to update keys and credentials, for obvious reasons.
LikeLike