feat: Initialize Zastava Webhook service with TLS and Authority authentication
- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
		
							
								
								
									
										234
									
								
								src/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								src/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,234 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using System.Security.Cryptography.X509Certificates;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using Serilog.Events;
 | 
			
		||||
using StellaOps.Attestor.Core.Options;
 | 
			
		||||
using StellaOps.Attestor.Core.Submission;
 | 
			
		||||
using StellaOps.Attestor.Infrastructure;
 | 
			
		||||
using StellaOps.Configuration;
 | 
			
		||||
using StellaOps.Auth.ServerIntegration;
 | 
			
		||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
 | 
			
		||||
using OpenTelemetry.Metrics;
 | 
			
		||||
using StellaOps.Attestor.Core.Observability;
 | 
			
		||||
using StellaOps.Attestor.Core.Verification;
 | 
			
		||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
 | 
			
		||||
 | 
			
		||||
const string ConfigurationSection = "attestor";
 | 
			
		||||
 | 
			
		||||
var builder = WebApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
builder.Configuration.AddStellaOpsDefaults(options =>
 | 
			
		||||
{
 | 
			
		||||
    options.BasePath = builder.Environment.ContentRootPath;
 | 
			
		||||
    options.EnvironmentPrefix = "ATTESTOR_";
 | 
			
		||||
    options.BindingSection = ConfigurationSection;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
 | 
			
		||||
{
 | 
			
		||||
    loggerConfiguration
 | 
			
		||||
        .MinimumLevel.Information()
 | 
			
		||||
        .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
 | 
			
		||||
        .Enrich.FromLogContext()
 | 
			
		||||
        .WriteTo.Console();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
var attestorOptions = builder.Configuration.BindOptions<AttestorOptions>(ConfigurationSection);
 | 
			
		||||
 | 
			
		||||
builder.Services.AddSingleton(TimeProvider.System);
 | 
			
		||||
builder.Services.AddSingleton(attestorOptions);
 | 
			
		||||
 | 
			
		||||
builder.Services.AddOptions<AttestorOptions>()
 | 
			
		||||
    .Bind(builder.Configuration.GetSection(ConfigurationSection))
 | 
			
		||||
    .ValidateOnStart();
 | 
			
		||||
 | 
			
		||||
builder.Services.AddProblemDetails();
 | 
			
		||||
builder.Services.AddEndpointsApiExplorer();
 | 
			
		||||
builder.Services.AddAttestorInfrastructure();
 | 
			
		||||
builder.Services.AddHttpContextAccessor();
 | 
			
		||||
builder.Services.AddHealthChecks()
 | 
			
		||||
    .AddCheck("self", () => HealthCheckResult.Healthy());
 | 
			
		||||
 | 
			
		||||
builder.Services.AddOpenTelemetry()
 | 
			
		||||
    .WithMetrics(metricsBuilder =>
 | 
			
		||||
    {
 | 
			
		||||
        metricsBuilder.AddMeter(AttestorMetrics.MeterName);
 | 
			
		||||
        metricsBuilder.AddAspNetCoreInstrumentation();
 | 
			
		||||
        metricsBuilder.AddRuntimeInstrumentation();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
if (attestorOptions.Security.Authority is { Issuer: not null } authority)
 | 
			
		||||
{
 | 
			
		||||
    builder.Services.AddStellaOpsResourceServerAuthentication(
 | 
			
		||||
        builder.Configuration,
 | 
			
		||||
        configurationSection: null,
 | 
			
		||||
        configure: resourceOptions =>
 | 
			
		||||
        {
 | 
			
		||||
            resourceOptions.Authority = authority.Issuer!;
 | 
			
		||||
            resourceOptions.RequireHttpsMetadata = authority.RequireHttpsMetadata;
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(authority.JwksUrl))
 | 
			
		||||
            {
 | 
			
		||||
                resourceOptions.MetadataAddress = authority.JwksUrl;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var audience in authority.Audiences)
 | 
			
		||||
            {
 | 
			
		||||
                resourceOptions.Audiences.Add(audience);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var scope in authority.RequiredScopes)
 | 
			
		||||
            {
 | 
			
		||||
                resourceOptions.RequiredScopes.Add(scope);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    builder.Services.AddAuthorization(options =>
 | 
			
		||||
    {
 | 
			
		||||
        options.AddPolicy("attestor:write", policy =>
 | 
			
		||||
        {
 | 
			
		||||
            policy.RequireAuthenticatedUser();
 | 
			
		||||
            policy.RequireClaim("scope", authority.RequiredScopes);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
else
 | 
			
		||||
{
 | 
			
		||||
    builder.Services.AddAuthorization();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
builder.WebHost.ConfigureKestrel(kestrel =>
 | 
			
		||||
{
 | 
			
		||||
    kestrel.ConfigureHttpsDefaults(https =>
 | 
			
		||||
    {
 | 
			
		||||
        if (attestorOptions.Security.Mtls.RequireClientCertificate)
 | 
			
		||||
        {
 | 
			
		||||
            https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
var app = builder.Build();
 | 
			
		||||
 | 
			
		||||
app.UseSerilogRequestLogging();
 | 
			
		||||
 | 
			
		||||
app.UseExceptionHandler(static handler =>
 | 
			
		||||
{
 | 
			
		||||
    handler.Run(async context =>
 | 
			
		||||
    {
 | 
			
		||||
        var result = Results.Problem(statusCode: StatusCodes.Status500InternalServerError);
 | 
			
		||||
        await result.ExecuteAsync(context);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.UseAuthentication();
 | 
			
		||||
app.UseAuthorization();
 | 
			
		||||
 | 
			
		||||
app.MapHealthChecks("/health/ready");
 | 
			
		||||
app.MapHealthChecks("/health/live");
 | 
			
		||||
 | 
			
		||||
app.MapPost("/api/v1/rekor/entries", async (AttestorSubmissionRequest request, HttpContext httpContext, IAttestorSubmissionService submissionService, CancellationToken cancellationToken) =>
 | 
			
		||||
{
 | 
			
		||||
    var certificate = httpContext.Connection.ClientCertificate;
 | 
			
		||||
    if (certificate is null)
 | 
			
		||||
    {
 | 
			
		||||
        return Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Client certificate required");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var user = httpContext.User;
 | 
			
		||||
    if (user?.Identity is not { IsAuthenticated: true })
 | 
			
		||||
    {
 | 
			
		||||
        return Results.Problem(statusCode: StatusCodes.Status401Unauthorized, title: "Authentication required");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var submissionContext = BuildSubmissionContext(user, certificate);
 | 
			
		||||
 | 
			
		||||
    try
 | 
			
		||||
    {
 | 
			
		||||
        var result = await submissionService.SubmitAsync(request, submissionContext, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        return Results.Ok(result);
 | 
			
		||||
    }
 | 
			
		||||
    catch (AttestorValidationException validationEx)
 | 
			
		||||
    {
 | 
			
		||||
        return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: validationEx.Message, extensions: new Dictionary<string, object?>
 | 
			
		||||
        {
 | 
			
		||||
            ["code"] = validationEx.Code
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
.RequireAuthorization("attestor:write");
 | 
			
		||||
 | 
			
		||||
app.MapGet("/api/v1/rekor/entries/{uuid}", async (string uuid, bool? refresh, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
 | 
			
		||||
{
 | 
			
		||||
    var entry = await verificationService.GetEntryAsync(uuid, refresh is true, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    if (entry is null)
 | 
			
		||||
    {
 | 
			
		||||
        return Results.NotFound();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Results.Ok(new
 | 
			
		||||
    {
 | 
			
		||||
        uuid = entry.RekorUuid,
 | 
			
		||||
        index = entry.Index,
 | 
			
		||||
        proof = entry.Proof is null ? null : new
 | 
			
		||||
        {
 | 
			
		||||
            checkpoint = entry.Proof.Checkpoint is null ? null : new
 | 
			
		||||
            {
 | 
			
		||||
                origin = entry.Proof.Checkpoint.Origin,
 | 
			
		||||
                size = entry.Proof.Checkpoint.Size,
 | 
			
		||||
                rootHash = entry.Proof.Checkpoint.RootHash,
 | 
			
		||||
                timestamp = entry.Proof.Checkpoint.Timestamp?.ToString("O")
 | 
			
		||||
            },
 | 
			
		||||
            inclusion = entry.Proof.Inclusion is null ? null : new
 | 
			
		||||
            {
 | 
			
		||||
                leafHash = entry.Proof.Inclusion.LeafHash,
 | 
			
		||||
                path = entry.Proof.Inclusion.Path
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        logURL = entry.Log.Url,
 | 
			
		||||
        status = entry.Status,
 | 
			
		||||
        artifact = new
 | 
			
		||||
        {
 | 
			
		||||
            sha256 = entry.Artifact.Sha256,
 | 
			
		||||
            kind = entry.Artifact.Kind,
 | 
			
		||||
            imageDigest = entry.Artifact.ImageDigest,
 | 
			
		||||
            subjectUri = entry.Artifact.SubjectUri
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}).RequireAuthorization("attestor:write");
 | 
			
		||||
 | 
			
		||||
app.MapPost("/api/v1/rekor/verify", async (AttestorVerificationRequest request, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
 | 
			
		||||
{
 | 
			
		||||
    try
 | 
			
		||||
    {
 | 
			
		||||
        var result = await verificationService.VerifyAsync(request, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        return Results.Ok(result);
 | 
			
		||||
    }
 | 
			
		||||
    catch (AttestorVerificationException ex)
 | 
			
		||||
    {
 | 
			
		||||
        return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, extensions: new Dictionary<string, object?>
 | 
			
		||||
        {
 | 
			
		||||
            ["code"] = ex.Code
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}).RequireAuthorization("attestor:write");
 | 
			
		||||
 | 
			
		||||
app.Run();
 | 
			
		||||
 | 
			
		||||
static SubmissionContext BuildSubmissionContext(ClaimsPrincipal user, X509Certificate2 certificate)
 | 
			
		||||
{
 | 
			
		||||
    var subject = user.FindFirst("sub")?.Value ?? certificate.Subject;
 | 
			
		||||
    var audience = user.FindFirst("aud")?.Value ?? string.Empty;
 | 
			
		||||
    var clientId = user.FindFirst("client_id")?.Value;
 | 
			
		||||
    var tenant = user.FindFirst("tenant")?.Value;
 | 
			
		||||
 | 
			
		||||
    return new SubmissionContext
 | 
			
		||||
    {
 | 
			
		||||
        CallerSubject = subject,
 | 
			
		||||
        CallerAudience = audience,
 | 
			
		||||
        CallerClientId = clientId,
 | 
			
		||||
        CallerTenant = tenant,
 | 
			
		||||
        ClientCertificate = certificate,
 | 
			
		||||
        MtlsThumbprint = certificate.Thumbprint
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user