The 15 Factors of ASP.NET — Configuration
Ever since writing the Beyond the 12 Factor Application eBook for O’Reilly, I have been answering questions about how those factors can be applied specifically to the migration and development of ASP.NET applications.
After answering the same questions enough times, I decided to write it all down. This series of blog posts, The 15 Factors of ASP.NET, should hopefully be more informative (but not nearly as fun) than the 13 Ghosts of Scooby Doo. For each of the applicable factors outlined in my book, I’ll take a look at how that factor specifically applies to ASP.NET Web Applications and .NET microservices.
For this post, I want to talk about configuration. In the eBook, I make the assertion that Configuration, Code, and Credentials should be treated as volatile substances that explode when combined. I still stand by that assertion and it certainly applies to ASP.NET applications.
The most common type of information you’ll find in web.config that changes on a per-environment basis is a connection string. There are hundreds of blog posts and books and references online that all tell you that web.config is a great place to store your connection string as an alternative to hard-coding it. There’s a fundamental problem with this: if you check something into source control, it might as well be hard-coded. In other words, to me, there is no difference between a connection string in web.config and a connection string in your code — both will end up in your source code repository, both expose potentially confidential information to the wrong people, and both violate the “3 Cs” volatile substance rule.
I realize this is a harsh stance, but when building cloud native (12 or 15 factor) applications, there are times when we need to not just draw lines in the sand, but instead dig moats.
This is is a little snippet of configuration that should look pretty familiar to anyone who has built an ASP.NET application in the past:
<connectionStrings>
<add name=”myConnectionString”
connectionString=”server();database=();uid=();password=();” />
</connectionStrings>
This should now be considered a bad smell for modern applications. The possibilities for refactoring away from this are nearly unlimited, but the basic idea behind making an application configured this way cloud native is to get the configuration from the environment.
If I push my application to the “dev” cloud, I should get the connection string for my application that is applicable to the dev environment. If I push it to “stage”, I should get the staging connection string. My CI pipeline should have built an immutable artifact that does not change from one environment to the other. If I’m swapping in and out web.dev.config and web.stage.config files in the artifact itself, I am violating that rule (and likely also storing credentials and code in the same place!).
Mutable build artifacts introduce instability and unpredictability to any application, and unpredictability breeds fear, and fear increases time between releases when we want to be releasing more often.
If we’re pushing our application to Cloud Foundry, we can use SteelToe to convert bound services into raw configuration data that we can then read. For example, we could override the constructor for our Entity Framework entity class to read from a configuration property like vcap.services.mydatabase.*. SteelToe also has the ability to get our configuration data from a Spring Cloud Configuration Server instead of from bound services.
We could also read the VCAP_SERVICES environment variable directly, or if we’re not deploying to a Cloud Foundry cloud, we might just read raw environment variables for a connection string.
In every single case the application code remains the same from one environment to another, and the only thing that changes is the data the application uses for configuration. With this configuration externalized, we can also safely secure and encrypt that data to avoid accidental exposure.
At this point, some of you might be thinking, “But we encrypt our connection strings, so this post doesn’t really apply to us.” Actually, this only provides you with a false sense of security. Firstly, if your connection strings are encrypted in your code or web.config file, and someone compromises your code base, they may also be able to compromise the encryption key. This is essentially the “flower pot” security violation (the key to your house is in a flower pot on your front porch).
Further, encrypted connection strings embedded in code files (remember your web.config file is essentially just code) prevent you from easily rotating encryption keys. To change encryption keys, every team building services has to regenerate and do another check-in — a slow, manual, and error-prone process. If your CI/CD pipeline tool is doing encrypted configuration injection, you’re just shifting the time-consuming burden from developers to operations.
Rotating the encryption keys is a best practice for prevention against Advanced Persistent Threats (APTs), as discussed in Justin Smith’s blog post.
To sum up, this blog post was just a quick, high-level discussion. The devil is always in the details and your mileage may vary depending on how many different types of environment-specific configuration you have in your web.config file(s). [Shameless Plug]Chris Umbel and I are working on a book that you can pre-order that will go into extensive detail on how this all works in ASP.NET Core.
If you just keep in mind that, for this factor, your goal when moving your ASP.NET application to the cloud is that any configuration data that changes from one environment to the other must come from somewhere external to the application, and not from any artifact checked into your application’s source code repository (or injected into your build artifact during a build pipeline), you should be in good shape.