This post describes a complete method for storing settings in Teamcity to avoid any committing them to Github. Settings will be merged into a separate settings file, referenced from web.config, both during Teamcity builds AND, through a Powershell script, also for developing locally.
Don’t store your passwords, keys, connectionstrings etc. in public
By now, everyone should be aware that it is a terrible idea to store secrets in cloud services, such as Github etc.. As descibed by many articles, a simple search on Github, reveals many secrets that should not be available publicly. And you never know when your private repo might be made public or when a colleague makes a mistake and pushes your private RSA key in a web.config in your code.
Find a private place for storage
You need to find a private place, preferable hosted on-site, behind thick firewalls and layers of security, to store your secrets. We were already using Teamcity to build, test and deploy a .net framework solution. So we want to utilize this to store our secret settings and merge them into the config during the build process. Teamcity also has a REST API that makes it possible to fetch the settings to use outside Teamcity, which we will need.
A solution for both Teamcity builds and local development
We need the settings to be available both during the Teamcity build for merging into the settings file, but also locally where we need to run the code during development. And during local development we want to be able to test out settings for different environments also.
Locally we typically build the code through Visual Studio, which through nugets such as SlowCheetah, has support for merging configs from different environments on build. But there are a couple of issues, such as the configs needs to be committed to Github, which we don’t want, and for web projects, such as mine, merging only happens on deploy or package, not on build. So we need a different approach.
Using the ‘file’ reference in appsettings
For developing locally we need to remember that we cannot store the settings in web.config, as this file is checked into Github. So instead we are making use of the “file” reference feature in .net config files. This enables us to store the secret settings in a different file, and NOT check that file into Github. Note that the ‘file’ reference differs from the ‘configsource’ reference as described here. This is important, because when using the ‘configsource’ reference, ALL of your settings must be in the referenced file, which is not the case with the ‘file’ reference. We only want to store secret settings in the referenced file, to not make life too difficult for developers.
Using Powershell to fetch settings and creating the settings file for local development
We will create a powershell script that takes an environment argument, and based on that, calls the Teamcity REST API, to fetch the settings. Then the script will create the secret settings file, referenced from the solution web.config. Every developer can then use that Powershell script to create settings for whatever environment he needs to test.
Using Teamcity Powershell buildstep to creating the settings file for Teamcity build
For the Teamcity build process, we will create a Powershellscript to be run in a Teamcity build step. This script will take the settings a environment parameters from teamcity and create the secret settings file so it is available for deployment with the rest of the build code.
The finished solution
Here is how I implemented the solution.
Teamcity configuration
We need to do 2 things in Teamcity.
1: Add the secret settings
Navigate to your build confguration and add your secret settings as ‘Configuration Parameters’. I am are prefixing the settings with “SecretAppSetting”, for later use.
2: Setup a build step to create the settings file
Next, add a build step to run the Powershell script, you will create for this purpose below. Note that I am adding the Teamcity parameter “teamcity.build.checkoutDir” which I will need later in the script, to determine where to save the settings file.
Powershell script for Teamcity
Next I create the Powershell script to run in Teamcity to create the secret settings file. Note that the “Param” section in the first line, takes the arguments the we passed in the Teamcity buildstep, including the path for where Teamcity has checked out the files from Github. Using this information the scripts creates the settings file on disk, so that it will be available for the building and packaging in a later buildstep.
Param($checkoutDir,$SecretSetting1,$SecretSetting2) [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 Function CreateAppSetting { param([string]$key,[string]$value ) $addElement = $doc.CreateElement("add"); $addElement.SetAttribute("key", $key); $addElement.SetAttribute("value", $value); $doc.SelectSingleNode("//appSettings").AppendChild($addElement); } $secretAppSettingsConfig = $checkoutDir + "\src\SecretAppSettings.config" # Create the secret settings file "<appSettings/>" | Out-File $secretAppSettingsConfig $doc = [Xml](Get-Content $secretAppSettingsConfig) CreateAppSetting 'MySecretSettingName1' $SecretSetting1 CreateAppSetting 'MySecretSettingName2' $SecretSetting2 $doc.Save($secretAppSettingsConfig)
Powershell script for developing locally
Next we need to create a different Powershell script to use locally on the developers computers to create local settings for development.
There is a couple of things to note. The line “$ParentFolder = Split-Path -Path $PSScriptRoot -Parent” is there to find out where to generate the settings file. This is a way to do that, which works in both Powershell editors and commandline (as opposed to dotting your way throughm that does note work the same in all places).
My Teamcity server is login protected, so there needs to be a “httpAuth” in the url and note that Teamcity is case-sensitive toward that argument… (that took some time to figure out!).
The line “$teamcityCredentials = Get-Credential -Message “Enter your TEAMCITY credentials (not your AD login)” prompts the user for credentials with a windows box that masks the input, which is better than writing it in clear text in a commandline.
And in the end, the scripts finds all Teamcity settings that prefixed with “SecretAppSetting.” and adds them as app settings.
So the idea is that the developers calls the Powershell script with an environment argument to generate settings for that environment.
Param($buildconfigurationName) [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # Find the path where the script is executed to figure out where to generate the settings file $ParentFolder = Split-Path -Path $PSScriptRoot -Parent $secretAppSettingsConfig = $ParentFolder + "\SecretAppSettings.config" switch($buildconfigurationName) { "debug" { Write-Host "debug" -ForegroundColor "Yellow" $url = 'https://teamcityServer/httpAuth/app/rest/buildTypes/id:DebugStoresSettingsForLocalBuild/parameters' Break } "test" { Write-Host "test" -ForegroundColor "Yellow" $url = 'https://teamcityServer/httpAuth/app/rest/buildTypes/id:BuildAndDeployTestBranch/parameters' Break } "prod" { Write-Host "prod" -ForegroundColor "Yellow" $url = 'https://teamcityServer/httpAuth/app/rest/buildTypes/id:BuildProd/parameters' Break } default { Write-Host "Invalid or no environment in args. Usage: CreateLocalSettings <environment>" -ForegroundColor "Red" Write-Host "Valid environments: debug, test, vendor, preprod, prod" -ForegroundColor "Red" Break Script } } Write-Host "Enter your TEAMCITY login (not your AD login)..." -ForegroundColor "Yellow" $teamcityCredentials = Get-Credential -Message "Enter your TEAMCITY credentials (not your AD login)" Write-Host "Calling teamcity to get settings..." try { $response = Invoke-RestMethod -Method GET -Credential $teamcityCredentials -Uri $url } catch { Write-Host $_ -fore green echo "Check teamcity connection or your credentials" Break Script } # clean out old values "<appSettings/>" | Out-File $secretAppSettingsConfig # read the cleaned xml doc $doc = [Xml](Get-Content $secretAppSettingsConfig) [bool]$settingsFound = $false; echo "Setting values..." # This loop finds all setting prefixed with "SecretAppSetting." and adds them as app settings foreach ($setting in $response.properties.ChildNodes) { if($setting.name -like "SecretAppSetting.*"){ $settingName = ($setting.name).Remove(0,17); Write-Host "Found setting:" $settingName; $addElement = $doc.CreateElement("add"); $addElement.SetAttribute("key", $settingName); $addElement.SetAttribute("value", $setting.value); $temp = $doc.SelectSingleNode("//appSettings").AppendChild($addElement); $settingsFound = $true; } } if($settingsFound) { Write-Host "saving file at:" $secretAppSettingsConfig -ForegroundColor "Green" $doc.Save($secretAppSettingsConfig) Write-Host "Done!" -ForegroundColor "Green" } else { Write-Host "No settings found!" -ForegroundColor "Red" }
Changes in the .net solution
I need to make sure that the secret settings file is included in the solution as “content” so that it is included in the packaging a deployment in Teamcity, as I am using a Visuai Studio buildstep in Teamcity. However it should NEVER be comitted to Github (More on that below).
Then I need to reference the file in web.config
Changes in Git/Github
Lastly, I need to make sure that the secret settings file is added to the .gitignore file, so that the file is never added to the Github repository.
Change passwords
If you already have had your connectionstrings, keys or any other secret setting added to a web.config or any other file in your Git repo, you need to remove all traces of it or change your passwords/keys etc.
Note that it is the nature of Git to keep all old commits so it is NOT a trivial task to get rid of all the old commits. It is easier to change your passwords for fresh ones.