Don't Keep Secrets in Code
Most developers know the rule that you shouldn't check secrets such as database connection strings or API keys into code. And to be fair, it's been a while since I've seen a production secret stored in source code. But often when developers are writing dev/test utilities, they can be tempted to relax the rules. For example, in production we might fetch a connection string from an environment variable, but when developing locally, we use a hard-coded fallback value that points at a shared cloud-hosted resource.
In this post I want to highlight a variety of simple tools and techniques .NET and Azure developers can use to completely eliminate secrets from source code.
Use command line parameters
First off, a nice and simple one. If you're writing a console application, allow secrets to be passed in as command line parameters. Give a nice error message if the secret isn't passed in. And to make life even easier, consider writing a PowerShell script that fetches the secret (more on how to do that later) and passes it in for you.
Use environment variables
Environment variables are arguably an even better way to get secrets into your code. They can be used with automated testing frameworks like NUnit, and are the default way of making secrets available in a variety of environments including containers and Azure App Service. By using environment variables for your secrets in development, you're also minimising the difference between how your dev and production environments work which is always a good thing.
Use .NET user secrets
.NET comes with a nice capability called "user secrets", which is intended for helping you manage development secrets in ASP.NET Core, but is not limited to ASP.NET Core. You initialize it on a project with a call to:
dotnet user-secrets init
This updates your .csproj
file with a UserSecretsId
GUID. Then you can store a secret from the command line like this
dotnet user-secrets set "MyApp:MySecret" "abc123"
Visual Studio has built-in tooling to simplify working with user secrets (right-click the project in Solution Explorer, and select "Manage User Secrets").
In code, by default when you're in a development environment, ASP.NET Core will automatically add the user secrets for your application to IConfiguration
, making them trivial to access.
If you want to use user secrets in an application that doesn't make use of HostBuilder
, you can just reference the Microsoft.Extensions.Configuration.UserSecrets
NuGet package and create an IConfiguration
object to access them. Here's a very simple example:
var config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
var userSecret = config["AppSecrets:MySecret"];
Configuration JSON file
Another option that you can use is to have a configuration JSON file that you enter your secrets into, but you don't check into source control (using a .gitignore
file to exclude it).
Azure Functions takes this approach with its local.settings.json
file. This works OK, but it does require anyone cloning your repo to manually set up their own local.settings.json
file, so unless you're using Azure Functions, I would generally avoid this approach.
Good README and error messages
Of course, one of the reasons that we tend to hard-code secrets is that we want the start-up experience for a new developer to be as simple as possible. We want them to clone the code and get going straight away.
If your app needs secrets to be configured before it can run, make sure you include good instructions in the README to explain how to fetch the values and put them in the right place for the application to access. And provide good error messages that tell you what's wrong if you failed to set the secrets up correctly.
Even better, make developers lives as easy as possible by automating the fetching of secrets...
Fetch secrets with the Azure CLI
If you're needing to fetch secrets like Azure Service Bus or Azure Storage connection strings, then my favourite way to do so is with the Azure CLI. It's usually pretty easy to query resources for the secrets you need. Here's an example from my post on managing blob storage that fetches a Storage Account connection string.
$connectionString=az storage account show-connection-string -n $storageAccount -g $resourceGroup --query connectionString -o tsv
Once you've retrieved it, you can easily set it as an environment variable, or pass it as a command line parameter to your application.
Use Azure Key Vault
Of course, not all the secrets you need can be fetched directly with something like the Azure CLI. For example, maybe you have an API key for a service like SendGrid, or a password for an admin account on a VM. In that case, I'd recommend storing it in Azure Key Vault, but you can use any similar secret store.
Again the Azure CLI makes it really easy to retrieve secret values from the Key Vault:
$mysecret = az keyvault secret show --vault-name mykeyvault --name mysecret --query value -o tsv
Use managed identities
Of course, even better than keeping secret connection strings out of code is to have the connection string not contain secrets at all. And that's what managed identities allow us to do. For example managed identities let you connect to an Azure SQL Database using Active Directory authentication. Your connection string doesn't need to include a password, and therefore is no longer technically a "secret":
Server=my-sql-server.database.windows.net,1433;Database=my-database;Authentication=Active Directory Default
It's a little bit of extra work to set this up and grant the correct AD identities permission to access the resource, but again it's something you can automate, so once you've done it once, it's easy to do in the future.
Many of the new Azure SDKs support this mode of connecting, and for local development you can use DefaultAzureCredential
which uses a variety of techniques to get hold of your identity including using Azure CLI if you've logged in with az login
. Find out more about how it works here (and here's an article I wrote showing this technique in action)
Auto-generate and rotate passwords
Of course, if you have taken the trouble to follow the advice I've given and you automate the lookup of secrets and passwords, then there's no reason for them to be reused "well-known" values. Reusing secrets is something I've unfortunately seen too often in development teams where everything has the same password to make life easier. Once you've automated the process of fetching the password, you are free to use randomly generated strong passwords for everything, and rotate them freely, knowing developers will automatically pick up the latest version next time they run.
Keep hard-coded secrets out of build and deploy pipelines
I've been focusing in this post on the local development environment, but another place hard-coded secrets can sneak in is into build and deploy pipelines as they often need to deal with connections to various online resources to store or retrieve assets like NuGet packages or container images. Whether you're using TeamCity or Azure Pipelines or GitHub Actions, all of these provide a way for you to securely enter secrets that can be made available to the build scripts.
Bonus - LINQPad Password Manager
As a bonus extra, I'm a big fan of LINQPad, which is a great tool for creating simple experimental scripts. Often in a LINQPad script you are connecting out to an external resource, and so again there is a big temptation to just hard-code a password or secret. But there's no need. LINQPad has a "password manager" that can securely store your secrets for you. In the script, just call Util.GetPassword("MySecretName")
. This will return the stored secret with that name, or prompt you to provide one if its not available
Summary
The temptation to hard-code a secret is great, but there are plenty of good alternatives available to you. There really is no excuse to check secrets into source control anymore. Did I miss any useful techniques? Let me know in the comments.
Comments
Regarding JSON configuration files:
This is also true of .NET user secrets, since the file isn't in the repo. However, I think it's a more convenient approach than user secrets, because:
- The file is immediately visible in the solution explorer, so there's no need to use the "Manage user secrets" VS command, or to manually find the file if you're not using VS.
- You can have different config files for different environments. For instance, you can have the following files: localappsettings.json, localappsettings.Development.json, localappsettings.Staging.json. Thomas Levesque
I’d recommend against passing secrets on the command line. On most operating systems, command line arguments of running processes are visible to non-admin users.
jorispa@thomaslevesque:disqus We don't use Azure, but we do use Github. Do you know if Github has a secrets capability?
Orin WalkerAzure Application Config is much much better for storing application configuration than Key Vault. When we can help it, we go there instead and use a connection string instead of keeping secrets anywhere else.
Intv Prime