This repo shows how to retrieve secrets stored in an Azure Key Vault using a .NET Framework & .NET Core application. It demonstrates both pulling the secrets via middleware at startup time and dynamically when a page is loaded.
Onprem, you need to provide a way for the running application to access the Key Vault securly. This is accomplished via service principal provisioned in Azure Active Directory. This service principal is then granted access to the Key Vault. The application authenticates to Azure Active Directory using a X.509 certificate so that it can use the service principal to access the Key Vault.
Note: The application is written to pull the certificate from the cert:\LocalMachine\My
certificate store on the onprem server. If this is not the right location to get your certificate from, you will need to modify the code to pull the certificate from the correct location.
You will need to modify code similar to the below code to pull from the right store for your deployment.
var x509Store = new X509Store(StoreName.My,
StoreLocation.LocalMachine);
In Azure, the process can be simplified by using a Managed Identity. The Managed Identity is granted access to the Key Vault & is assigned to the App Service so code running in the App Service can use it. The deployment script will set the Managed Identity client ID for you as part of deployment. This will override the values specified in the configuration files.
Note: This repo intentionally doesn't access the secrets through the Azure App Service configuration so that it is portable between onprem & Azure. If you are only targeting Azure, you can store the secrets in the App Service configuration.
Note: You could still use the certificate-based authentication method to allow your application to authenticate with Azure AD instead of using Managed Identity. Managed Identity makes the process simpler, but is not required.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The .NET Framework version of this application pulls the secrets from Key Vault when the web page is loaded. It does not have a default inversion of control container that pulls the secrets at startup time. Instead, it uses a custom implementation of the ConfigBuilder
to pull the certificate from the Windows certificate store to authenticate to Azure with & pull all the secrets from Key Vault.
The following NuGet packages are required to access Key Vault using .NET Framework (these will install some additional dependencies):
- Azure.Identity
- Azure.Security.KeyVault.Secrets
- Microsoft.Configuration.ConfigurationBuilders.Azure
If you look at the ./web-net-framework/Web.config
file, you can see the following configBuilder
section which tells teh custom CertificateAuthenticationAzureKeyVaultConfigBuilder
class how to authenticate with Azure Active Directory so it can use the service principal to access the Key Vault.
<configBuilders>
<builders>
<add name="KeyVault" mode="Greedy" vaultName="kv-keyvault-web-ussc-dev" enabled="true" certificateStoreName="My" certificateStoreLocation="LocalMachine" certificateThumbprint="a17d4362fbf40049bb4aa7eb465d082358c7878a" tenantId="72f988bf-86f1-41af-91ab-2d7cd011db47" clientId="9bfd1049-3cfe-4466-a684-2b5fb636b03e" type="web_net_framework.Services.CertificateAuthenticationAzureKeyVaultConfigBuilder, web-net-framework" />
</builders>
</configBuilders>
<appSettings configBuilders="KeyVault">
<add key="the-king-of-england" value="Elizabeth II" />
...
</appSettings>
You will notice that the appSettings
section of the Web.config
file has a value already for the-king-of-england
. Naturally, this value is wrong and we want to override it with the value from Key Vault. The Greedy
flag will override the value from the Web.config
withe the one from Key Vault if it was able to successfully authenticate & pull secrets.
Here is what the running application should look like if it was able to successfully authenticate with Azure Active Directory & pull secrets from Key Vault.
In the ./web-net-framework/Services/CertificateAuthenticationAzureKeyVaultConfigBuilder.cs
, we pull the certificate from the local store, authenticate with Azure AD, then pull the secrets that are needed. This custom class is needed because the default implementation of the AzureKeyVaultConfigBuilder
will try to use the DefaultAzureCredentials
class which will not work on the onprem server since it doesn't have a managed identity nor does the service account running the app pool have access to Azure. Instead, we want to pull a certificate from the local store and authenticate with Azure AD.
protected override TokenCredential GetCredential()
{
StoreName storeName = (StoreName)Enum.Parse(typeof(StoreName), CertificateStoreName);
StoreLocation storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), CertificateStoreLocation);
var x509Store = new X509Store(storeName,
storeLocation);
x509Store.Open(OpenFlags.ReadOnly);
X509Certificate2 x509Certificate;
try
{
x509Certificate = x509Store.Certificates.Find(X509FindType.FindByThumbprint,
CertificateThumbprint,
validOnly: false)
.OfType<X509Certificate2>()
.Single();
}
catch (Exception ex)
{
throw new ArgumentException($"Unable to find certificate in cert:\\{CertificateStoreLocation}\\{CertificateStoreName} with thumbprint: {CertificateThumbprint}", ex);
}
var tokenCredential = new ClientCertificateCredential(TenantId,
ClientId,
x509Certificate);
return tokenCredential;
}
Using the Managed Identity associated with the App Service, it much simpler to authenticate with Azure AD.
client = new SecretClient(new Uri(kvUri),
new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = ConfigurationManager.AppSettings["Authentication:ManagedIdentityClientId"]
}));
Similarly, the .NET Core version of this application pulls the Key Vault secrets when the web page is loaded. However, because .NET Core already has a middleware installed, most of the secrets are pulled at startup time. Only 1 secret is pulled at page load time to demonstrate how to pull secrets dynamically.
If you look at the ./web-net-core/appsettings.json
file, you can see the following app settings which will allow the application to authenticate with Azure Active Directory so it can use the service principal to access the Key Vault.
"KeyVaultName": "kv-keyvault-web-ussc-dev",
"Authentication": {
"AzureADApplicationId": "9bfd1049-3cfe-4466-a684-2b5fb636b03e",
"AzureADCertificateThumbprint": "539cd5afadb7b25b85cf90a78c261074a6db6445",
"AzureADDirectoryId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
"ManagedIdentityClientId": ""
},
"IsHostedOnPrem": "true"
Here is what the running application should look like if it was able to successfully authenticate with Azure Active Directory & pull secrets from Key Vault.
The middleware allows us to pull all secrets from Key Vault at startup time and store them as configuration values that can be used throughout the application (look at the ./web-net-core/Program.cs
file).
builder.Configuration.AddAzureKeyVault(new Uri(kvUri), new ClientCertificateCredential(
builder.Configuration["Authentication:AzureADDirectoryId"],
builder.Configuration["Authentication:AzureADApplicationId"],
x509Certificate));
- Azure CLI
- Dotnet CLI
- .NET Framework 4.7.2
- Azure subscription & resource group
- On-prem server
It is recommeded that you get a signed certificate from your company's certificate authority. However, if you cannot, you can generate a self-signed certificate locally.
Run this script as Administrator on the onprem server where you are running the application.
./create-certificate.ps1
This script will generate a self-signed certificate, install it in the cert:\LocalMachine\My
certificate store and write the public key .cer
file to the current user's Desktop. You will need to upload this file to the Azure AD service principal so the local application can use it to authenticate.
You will need a service principal in Azure Active Directory to be the identity that your application uses to access the Key Vault.
-
Navigate to the Azure portal and sign in.
-
Click on the
Azure Active Directory
link in the left-hand navigation menu. -
Click on the
App registrations
blade. -
Click on the
New registration
button. -
Give it a name & a redirect uri where your application will be listening (http://localhost as an example for running locally).
-
Click on the
Certificates & secrets
blade. -
Upload the
.cer
certificate file that was created in the previous step. Save theThumbprint
value for configuring the application.
- On the
Overview
blade, copy theApplication ID (client ID)
and theDirectory (tenant) ID
values for configuring the application.
- Modify the
./infra/env/dev.parameters.json
file as needed. Make sure and update theazureADApplicationId
with theApplication ID (client ID)
value from the previous step.
az deployment group create -g rg-keyvault-web-ussc-dev --template-file ./infra/main.bicep --parameters ./infra/env/dev.parameters.json --parameters theKingOfAustriaSecretValue="Joseph the 2nd" theKingOfPrussiaSecretValue="Fredrick Wilhelm the 3rd" theKingOfEnglandSecretValue="Why the tyrant King George, of course!"
The script will output the name of the Key Vault & the URLs to the web apps.
-
Navigate to the Azure portal and sign in.
-
Select the
Key Vault
created in the previous step. -
Click on the
Access policies
blade. -
Click on the
Add Access Policy
button. -
Add
Get
andList
Secret permissions
.
-
Click on the
Select principal
button. -
Search for your newly created service principal (from the previous step), select it and click the
Select
button. Click on theAdd
button.
-
Click on the
Save
button. -
Repeat these steps for the Managed Identity created in the previous step.
You will likely need to configure the onprem server to be able to use the certificate from the local certificate store.
-
Login to the onprem server as
Administrator
. -
Open the
Computer certificate store
and select the store where you have provisioned your certificate (Personal
in this example). -
Right-click on the provisioned certificate and select
All Tasks->Manage Private Keys
.
-
Click on the
Add
button and select the account that the IIS app pool is running under (IIS_ISRS
in this example). -
Select
Full control
as the permissions and clickOK
.
-
Open the
./web-net-framework/web-net-framework.sln
file in Visual Studio. -
Update the
./web-net-framework/Web.config
file with the your values in theconfigBuilders
section.vaultName
- the name of your Key VaultclientId
- the application ID (client ID) of your service principalcertificateThumbprint
- the thumbprint of the certificate installed on the machine that will be used to authenticate with Azure Active DirectorycertificateStoreName
- the name of the certificate store you want to pull fromcertificateStoreLocation
- the location of the certificate store you want to pull fromtenantId
- the tenant ID where your service principal is instantiated
-
Right-click on the project and select Build.
-
Right-click on the project and select Publish.
Note: The following instructions assume you are using Web Deploy for the onprem IIS server. You could also manually copy your application to the onprem server.
-
Click the New button.
-
Select
Web Server (IIS)
as the publish type. ClickNext
. -
Choose
Web Deploy
as the specific target. ClickNext
. -
Enter the credentials for the onprem server. Click
Next
. andFinish
. -
Click
Publish
to push your code to the onprem server.
-
Open the
./web-net-core/web-net-core.csproj
file in Visual Studio. -
Update the
./web-net-core/appsettings.json
file with the your values.KeyVaultName
- the name of your Key VaultAuthentication:AzureADApplicationId
- the application ID (client ID) of your service principalAuthentication:AzureADCertificateThumbprint
- the thumbprint of the certificate installed on the machine that will be used to authenticate with Azure Active DirectoryAuthentication:AzureADDirectoryId
- the tenant ID where your service principal is instantiatedAuthentication:ManagedIdentityClientId
- this value doesn't need to be set when running onprem (since there is no managed identity to use)IsHostedOnPrem
- set this value totrue
Note: You can also set these values in IIS and override the values in the ./web-net-core/appsettings.json
file.
-
Right-click on the project and select Build.
-
Right-click on the project and select Publish.
Note: The following instructions assume you are using Web Deploy for the onprem IIS server. You could also manually copy your application to the onprem server.
-
Click the New button.
-
Select
Web Server (IIS)
as the publish type. ClickNext
. -
Choose
Web Deploy
as the specific target. ClickNext
. -
Enter the credentials for the onprem server. Click
Next
. andFinish
. -
Click
Publish
to push your code to the onprem server.
-
Open the
./web-net-framework/web-net-framework.sln
file in Visual Studio. -
You don't need to modify the values of the
./web-net-framework/Web.config
file since the values will be set in the App Service Configuration settings automatically by the Infrastructure as Code Bicep scripts. -
Right-click on the project and select Build.
-
Right-click on the project and select Publish.
-
Click the New button.
-
Select Azure, then Next. Select Azure App Service (Windows), then Next.
-
Select your Azure subscription, Resource Group and App Service instance (make sure and select the wa-net-framework App Service), then Finish and Close.
-
Click Publish to push your app to the App Service.
-
Navigate to the
./web-net-core
directory on the command line (or use Visual Studio). -
Build the application & create a publish package.
dotnet publish --configuration Release
- Zip up the publish package.
Compress-Archive -DestinationPath ./app.zip -Update ./bin/Release/net6.0/publish
- Deploy your zip package to Azure.
az webapp deployment source config-zip --resource-group rg-keyvault-web-ussc-dev --name wa-keyvault-web-ussc-dev --src ./app.zip