YARP (Yet Another Reverse Proxy) is a .NET library for running a reverse proxy server under ASP.NET Core. It’s used in Azure to front HTTP requests to Azure App Services, the gateway for Microsoft’s AI services, and is a core part of the strategy for developers migrating ASP.NET web apps to ASP.NET Core. It’s also very easy to create your own reverse proxy server based on it - only a few lines of code are needed, with most functionality being driven through configuration.
// https://dotnet.github.io/yarp/#get-started
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
I’m running a Docker container on my own private server to provide access to some personal web apps. Because it’s a privately hosted server, and not on a cloud provider, SSL/TLS certificates for HTTPS is also something to consider. Let’s Encrypt is a free service that provides free TLS certificates, with automated domain validation and renewal through the ACME protocol, and the LettuceEncrypt library makes all this available for ASP.NET Core projects (so long as you use ASP.NET Core’s Kestrel web server).
Here is the entirety of the code for the reverse proxy I’m running - just over 50 lines even with plenty of formatting. Everything is driven through configuration, so I only need to update the project when upgrading .NET or package versions, or adding a new feature (such as allowing certain sites to require authentication in a future update).
using LettuceEncrypt;
using LettuceEncrypt.Azure;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("data/config/settings.json", optional: true, reloadOnChange: true)
.AddJsonFile("data/config/proxy.json", optional: true, reloadOnChange: true)
.AddJsonFile("data/config/logging.json", optional: true, reloadOnChange: true);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var lettuceBuilder = builder.Services.AddLettuceEncrypt();
var lettuceConfig = builder.Configuration.GetSection(nameof(LettuceEncrypt));
if (lettuceConfig.GetSection("Directory").Get<DirectoryLettuceEncryptOptions>() is var directoryOptions and not null)
{
lettuceBuilder.PersistDataToDirectory(new DirectoryInfo(directoryOptions.DirectoryPath), directoryOptions.PfxPassword);
}
else if (lettuceConfig.GetSection("AzureKeyVault").Get<AzureKeyVaultLettuceEncryptOptions>() is not null)
{
lettuceBuilder.PersistCertificatesToAzureKeyVault();
}
builder.WebHost.UseKestrel(server =>
{
server.ConfigureHttpsDefaults(options =>
{
options.UseLettuceEncrypt(server.ApplicationServices);
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.MapReverseProxy();
app.Run();
public class DirectoryLettuceEncryptOptions
{
public string DirectoryPath { get; set; } = default!;
public string PfxPassword { get; set; } = default!;
}
Configuration#
The .json
files under data/config/
allow me to limit what files/directories are exposed from the container to the host system, and separate out concerns so that, for example, I don’t mistakenly remove LettuceEncrypt’s settings when updating the proxy’s routes or changing what’s logged.
LettuceEncrypt offers three options out of the box for storing the generated certificates, and I can control which used here. (LettuceEncrypt does also expose an API to provide other certificate repositories if you want something else.)
- By default, the machine’s X.509 store is used
- If the configuration contains a
LettuceEncrypt:Directory
section, certificates will be persisted to files inDirectoryPath
, and encrypted withPfxPassword
- With the
LettuceEncrypt:AzureKeyVault
configuration section, certificates will be stored in the Azure Key Vault given byLettuceEncrypt:AzureKeyVault:AzureKeyVaultEndpoint
I’m using Azure Key Vault here, as there’s no risk of exposing the certificate or protecting password, and is extremely cheap (it’s cost me less than $0.01 for the past 12 months). Of course, there’s some extra configuration in Azure that is required to authenticate to the Key Vault.
Authenticating to Azure#
To allow the reverse proxy to access the Key Vault for storing the certificates, I created a service principal identity for it in Entra ID (formerly Azure Active Directory) with an App Registration. No special configuration (authentication flows, API permissions or scopes) is required outside of how the reverse proxy itself will authenticate: either a public certificate (depending on your needs, self-signed might be enough), or a client secret. I’m currently using a client secret, but whichever you choose keep in mind that it will expire at some point in time. You will also need to note the “Application (client) ID” and “Directory (tenant) ID” (which I’ll just refer to as the Client ID and Tenant ID).
With the app identity created, it needs to be given permission to the Key Vault resource. Under “Access control (IAM)”, give both the “Key Vault Certificates Office” and “Key Vault Secrets Officer” roles to the service principal.
As I’m trying to somewhat protect these values (part of the reason I’m using Azure Key Vault instead of just storing the certificates locally), they are passed through to the reverse proxy’s container with environment variables.
AZURE_CLIENT_ID=[Client ID]
AZURE_TENANT_ID=[Tenant ID]
AZURE_CLIENT_SECRET=[Client Secret]
The Azure SDK client libraries automatically try to find suitable credentials through a chain of credential providers, one of which is client secret credentials. The WebApplicationBuilder
also automatically loads configuration from several sources by default, so there’s nothing extra required in the application’s code.