I'm using docker (windows nanoserver), traefik, asp.net core 3.1 and windows authentication/negotiate. If a user connects to the container, he or she logs in and their user is bound to the httpcontext. If another user connects afte that, the first user is still bound to the context.
This is my setup in code:
docker-compose.yml
version: "3.7"
services:
webapplication:
image: <repository>/webapplication:8-1
networks:
webapplication-network:
aliases:
- webapplication
traefik:
aliases:
- traefik-webapplication
credential_spec:
file: webapp({GMSA_ACCOUNT})_credential.json
secrets:
- source: config_secrets
target: C:/app/appsettings.json
deploy:
mode: replicated
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 10
window: 30s
labels:
- applicationname=({APPLICATIONNAME})
- "traefik.enable=true"
- "traefik.http.routers.({APPLICATIONNAME})-webapplication.rule=Host(`({APPLICATIONNAME})-webapplication.({DOMAIN})`)"
- "traefik.http.routers.({APPLICATIONNAME})-webapplication.service=({APPLICATIONNAME})-webapplication"
- "traefik.http.routers.({APPLICATIONNAME})-webapplication.entrypoints=https"
- "traefik.http.routers.({APPLICATIONNAME})-webapplication.tls=true"
- "traefik.http.services.({APPLICATIONNAME})-webapplication.loadbalancer.server.scheme=http"
- "traefik.http.services.({APPLICATIONNAME})-webapplication.loadbalancer.server.port=80"
# redirect http to https
- traefik.http.middlewares.({APPLICATIONNAME})-webapplication-unsecure-redirect-secure.redirectscheme.scheme=https
- traefik.http.routers.({APPLICATIONNAME})-webapplication-unsecure.middlewares=({APPLICATIONNAME})-webapplication-unsecure-redirect-secure
- traefik.http.routers.({APPLICATIONNAME})-webapplication-unsecure.rule=Host(`({APPLICATIONNAME})-webapplication.({DOMAIN})`)
- traefik.http.routers.({APPLICATIONNAME})-webapplication-unsecure.entrypoints=http
# Attempt to make Windows authentication work through TCP
- "traefik.tcp.routers.({APPLICATIONNAME})-webapplication.rule=HostSNI(`({APPLICATIONNAME})-webapplication.({DOMAIN})`)"
- "traefik.tcp.routers.({APPLICATIONNAME})-webapplication.service=({APPLICATIONNAME})-webapplication"
- "traefik.tcp.routers.({APPLICATIONNAME})-webapplication.tls=true"
- "traefik.tcp.services.({APPLICATIONNAME})-webapplication.loadbalancer.server.port=80"
- "traefik.http.services.({APPLICATIONNAME})-webapplication.loadbalancer.sticky=true"
Dockerfile webapplication-windows-netcore-base
# escape=`
FROM mcr.microsoft.com/windows/servercore:ltsc2019 AS installer
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
# Retrieve .NET Core Runtime
RUN $dotnet_version = '3.1.3'; `
Invoke-WebRequest -OutFile dotnet.zip https://dotnetcli.azureedge.net/dotnet/Runtime/$dotnet_version/dotnet-runtime-$dotnet_version-win-x64.zip; `
$dotnet_sha512 = '62a18838664afd6f08cdb9a90a96a67626743aab1f0de0065eadfd7d1df31681c90f96744ccb5b7e40834c9e3c4952125c8c83625867c500692050cdd113ff50'; `
if ((Get-FileHash dotnet.zip -Algorithm sha512).Hash -ne $dotnet_sha512) { `
Write-Host 'CHECKSUM VERIFICATION FAILED!'; `
exit 1; `
}; `
`
Expand-Archive dotnet.zip -DestinationPath dotnet; `
Remove-Item -Force dotnet.zip; `
# Install ASP.NET Core Runtime
$aspnetcore_version = '3.1.3'; `
Invoke-WebRequest -OutFile aspnetcore.zip https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/$aspnetcore_version/aspnetcore-runtime-$aspnetcore_version-win-x64.zip; `
$aspnetcore_sha512 = '6d8a21a7420db9091fc05613ef0a923c7b86cc995360c9f0133d632020e042db0efac0343ee6516a28feb2c1dd004b33b74bfdcc1a687efdefa9db1a486c1ca2'; `
if ((Get-FileHash aspnetcore.zip -Algorithm sha512).Hash -ne $aspnetcore_sha512) { `
Write-Host 'CHECKSUM VERIFICATION FAILED!'; `
exit 1; `
}; `
`
Expand-Archive aspnetcore.zip -DestinationPath dotnet -Force; `
Remove-Item -Force aspnetcore.zip
# Runtime image
FROM mcr.microsoft.com/windows/servercore:ltsc2019
ENV `
# Configure web servers to bind to port 80 when present
ASPNETCORE_URLS=http://+:80 `
# Enable detection of running in a container
DOTNET_RUNNING_IN_CONTAINER=true
WORKDIR C:\app
USER ContainerAdministrator
RUN setx /M PATH "%PATH%;C:\Program Files\dotnet" && `
REG ADD HKLM\SYSTEM\CurrentControlSet\Control\FileSystem /v LongPathsEnabled /t REG_DWORD /d 1 -f
COPY --from=installer ["/dotnet", "/Program Files/dotnet"]
Dockerfile webapplication
# escape=`
##### Runtime #####
ARG baseImageVersie
ARG buildNumber
ARG release
FROM <repository>/webapplication-build:$release-$buildNumber AS buildtools
FROM webapplication-windows-netcore-base:$baseImageVersie
ENV DOTNET_RUNNING_IN_CONTAINER=true
ENTRYPOINT ["dotnet", "webapplication.dll"]
COPY --from=buildtools C:/publish .
Startup.cs looks like this
public class Startup
{
private readonly IConfiguration configuration;
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(o =>
{
o.Filters.Add(typeof(GlobalExceptionFilter));
});
services
.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate(o => o.Validate());
var appsettings = configuration.Get<Appsettings>();
services.AddCors(options =>
{
options.AddPolicy("cors",
builder =>
{
builder.WithOrigins(appsettings.Origins)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "Webapplication",
Version = "v1" ,
});
c.ExampleFilters();
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
});
services.AddSwaggerExamplesFromAssemblyOf<AuthorizeModelExample>();
}
// ReSharper disable once UnusedMember.Global
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseCors("cors");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints
.MapControllers()
.RequireAuthorization()
.RequireCors("cors");
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Webapplication V1");
});
}
}
UserController
[Produces("application/json")]
[ApiController]
[Route("api/connect/[controller]")]
public class UserController : ControllerBase
{
[HttpGet]
public ActionResult Get()
{
return Ok(User?.Identity?.Name)
}
}
What happens - User 1 logs in (using incognito mode) by filling in his credentials - User 1 calls /api/connect/user and it returns "user 1" - User 2 opens webapplication and does not have to provide any credentials - User 2 calls /api/connect/user and it still returns "user 1"
Question: How do i prevent my asp.net core application returning the wrong windows authentication user?
I managed to make it work using traefik TLS passthrough. So I had to change the application to serve https itself instead of having traefik do SSL termination. My application's compose file now looks like this:
docker-compose.yml
version: "3.7"
services:
webapplication:
image: <repository>/webapplication:8-1
environment:
- ASPNETCORE_Kestrel__Certificates__Default__Path=wildcard_certificate.pfx
networks:
webapplication-network:
aliases:
- webapplication
traefik:
aliases:
- traefik-webapplication
credential_spec:
file: webapp({GMSA_ACCOUNT})_credential.json
secrets:
- source: config_secrets
target: C:/app/appsettings.json
- source: wildcard_certificate_pfx
target: c:\certificates\wildcard_certificate.pfx
deploy:
mode: replicated
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 10
window: 30s
labels:
- applicatienaam=({APPLICATIENAAM})
- "traefik.enable=true"
- "traefik.http.routers.({APPLICATIENAAM})-webapplication.rule=Host(`({APPLICATIENAAM})-webapplication.({DOMAIN})`)"
- "traefik.http.routers.({APPLICATIENAAM})-webapplication.entrypoints=https"
- "traefik.http.routers.({APPLICATIENAAM})-webapplication.tls=true"
- "traefik.http.services.({APPLICATIENAAM})-webapplication.loadbalancer.server.scheme=https"
- "traefik.http.services.({APPLICATIENAAM})-webapplication.loadbalancer.server.port=443"
# Windows authentication works through TCP
# TLS passtrough, because otherwise windows authentication won't support multiple users
- "traefik.tcp.routers.({APPLICATIENAAM})-webapplication.tls=true"
- "traefik.tcp.routers.({APPLICATIENAAM})-webapplication.tls.options=default"
- "traefik.tcp.routers.({APPLICATIENAAM})-webapplication.tls.passthrough=true"
- "traefik.tcp.routers.({APPLICATIENAAM})-webapplication.rule=HostSNI(`({APPLICATIENAAM})-webapplication.({DOMAIN})`)"
- "traefik.tcp.routers.({APPLICATIENAAM})-webapplication.entrypoints=https"
- "traefik.tcp.services.({APPLICATIENAAM})-webapplication.loadbalancer.server.port=443"
I think the first user that logs in is bound to the traefik connection, so all next users will use the first users session, but I don't know for sure. All I know is that the solution above prevents mixing up user sessions.
I also had to make a change to my dockerfile to make serving https from the container work, because of a WindowsCryptographicException bug.
# escape=`
##### Runtime #####
ARG baseImageVersie
ARG buildNumber
ARG release
FROM <repository>/webapplication-build:$release-$buildNumber AS buildtools
FROM webapplication-windows-netcore-base:$baseImageVersie
ENV DOTNET_RUNNING_IN_CONTAINER=true
# The copy is done, because wildcard_certificate.pfx is put into the container using docker secrets, which makes it a symlink.
# Reading a certificate as a symlink is not supported at this moment: https://stackoverflow.com/q/43955181/1608705
# After doing a copy, the copied version is not a symlink anymore.
ENTRYPOINT (IF EXIST "c:\certificates\wildcard_certificate.pfx" (copy c:\certificates\wildcard_certificate.pfx c:\app\wildcard_certificate.pfx)) && dotnet webapplication.dll
COPY --from=buildtools C:/publish .