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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Infrastructure\StellaOps.Attestor.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user