Secure your SMTP configuration - Configuration Builders

Posted on January 21, 2024

In .NET Framework, we are all aware of the famous <network> node under the Web.config file. This has been the facto way to configure your SMTP settings since, well, a long time. In .NET, the story is quite different, as the approach is way cleaner as it previously was, there is no enforced structure or way to configure your settings, you decide, by yourself, how you would like these settings to exist within, either your appsetting.json file or any configuration provider you might have added to your solution.

For example, under a Optimizely CMS 12 and higher solution, you would have to add configuration under the "EPiServer.Cms.Smtp.Network" section. But in the end, this structure is something established by Optimizely, nothing prevents you to add them elsewhere (even though I would not recommend to duplicate configuration under multiple different nodes).

In previous version of Optimizely (lower than version 12), since we're using .NET Framework, we must use the Web.config structure to be able to have our SMTP settings properly setup. The thing though is that we had to put the password cleartext right inside this section. There was also no way to use configuration transformation as it usually works with the appSettings and connectionStrings sections. Until we found a way to move those settings inside the appSettings section.

We deploy our solutions to DXP. So naturally speaking, we have no control over the infrastructure where the application is hosted, but we have a way, just before sending the package to Optimizely, to transform the configuration. Under Azure DevOps, there's a task, File Transform v2, which does exactly what we want. We store our secrests inside variable groups and let the configuration transformation task to do its magic (variable substitution). So, this step covers like 90% of our cases, we can safely store secrets, but unfortunately, like I said, this only works for the appSettings section. What about the smtp section then? I'd tell you "Configuration builders". There are already existing config builders in the wild, the one we use the most is the AzureKeyVaultConfigBuilder, but mainly for development purpose, as we still don't want to expose secrets, even for that.

But I can hear ya, like at a 100 miles, with your question: "But Dave, what does this change for the SMTP configuration?" I'm glad you're asking! See, you can add existing config builder, but you can also create a new one. So, since I want the SMTP configuration to be loaded from the appSettings, I'll create a class that will do the following:

public class SmtpConfigurationBuilder : ConfigurationBuilder
    {
        public override ConfigurationSection ProcessConfigurationSection(ConfigurationSection configSection)
        {
            if (!(configSection is SmtpSection smtpSection) || smtpSection.Network == null) 
                return base.ProcessConfigurationSection(configSection);

            smtpSection.Network.Host = ConfigurationManager.AppSettings["smtp-host"];
            smtpSection.Network.UserName = ConfigurationManager.AppSettings["smtp-userName"];
            smtpSection.Network.Password = ConfigurationManager.AppSettings["smtp-password"];
            smtpSection.Network.Port = ConfigurationManager.AppSettings["smtp-port"].ToInt(defaultValue: 587);
            smtpSection.Network.EnableSsl = ConfigurationManager.AppSettings["smtp-enableSsl"].ToBoolean();

            return base.ProcessConfigurationSection(configSection);
        }
    }

This will instruct whatever that uses my config builder to execute only when the current section is a SMTP section and that the Network node isn't null. To have that work under your Web.config, you'll have to add three additional items:

  • Look for the "configSections" node and add the following at the end:
      <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />
    
  • Just after the "configSections" node, add the following:
      <configBuilders>
      	<builders>
      		<add name="SmtpConfigurationBuilder" type="Full.Namespace.Of.My.Class.SmtpConfigurationBuilder, Assembly.Name" />
      	</builders>
      </configBuilders>
    
    If you have multiple config builder, take a good attention of the ordering, as I think it has importance when they are loaded.
  • And finally, replace your SMTP node with something like so:
      <system.net>
      	<mailSettings>
      		<smtp from="[email protected]" configBuilders="SmtpConfigurationBuilder" />
      	</mailSettings>
      </system.net>
    

So, with that, you have a way to load all you SMTP configuration directly from your appSettings. Isn't that great? Assuming you're using Azure DevOps to build and deploy your solutions, you can now easily leverage the variable substitution to change your SMTP settings just before sending the package to Optimizely for a deployment.

Another thing to point out, as you can see, you can add multiple config builders. We do that on our side, only for our development environments, to protect any sensitive information, by using the AzureKeyVaultConfigBuilder. This setup can work in pair, meaning that, say, you have both AzureKeyVaultConfigBuilder and the SmtpConfigurationBuilder, you can use the first config builder to load the secrets from your key vault, then ask your SMTP builder to load these for your SMTP configuration. It can be extremely useful for a lot of different applications.