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