Resolve Concelier/Excititor merge conflicts

This commit is contained in:
master
2025-10-20 14:19:25 +03:00
2687 changed files with 212646 additions and 85913 deletions

View File

@@ -0,0 +1,93 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using StellaOps.Zastava.Webhook.Configuration;
namespace StellaOps.Zastava.Webhook.Authority;
public interface IAuthorityTokenProvider
{
ValueTask<AuthorityToken> GetTokenAsync(CancellationToken cancellationToken = default);
}
public sealed record AuthorityToken(string Value, DateTimeOffset? ExpiresAtUtc);
public sealed class StaticAuthorityTokenProvider : IAuthorityTokenProvider
{
private readonly ZastavaWebhookAuthorityOptions _options;
private readonly ILogger<StaticAuthorityTokenProvider> _logger;
private AuthorityToken? _cachedToken;
public StaticAuthorityTokenProvider(
IOptionsMonitor<ZastavaWebhookOptions> options,
ILogger<StaticAuthorityTokenProvider> logger)
{
_options = options.CurrentValue.Authority;
_logger = logger;
}
public ValueTask<AuthorityToken> GetTokenAsync(CancellationToken cancellationToken = default)
{
if (_cachedToken is { } token)
{
return ValueTask.FromResult(token);
}
var value = !string.IsNullOrWhiteSpace(_options.StaticTokenValue)
? _options.StaticTokenValue
: LoadTokenFromFile(_options.StaticTokenPath);
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException("No Authority token configured. Provide either 'StaticTokenValue' or 'StaticTokenPath'.");
}
token = new AuthorityToken(value.Trim(), ExpiresAtUtc: null);
_cachedToken = token;
_logger.LogInformation("Loaded static Authority token (length {Length}).", token.Value.Length);
return ValueTask.FromResult(token);
}
private string LoadTokenFromFile(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException("Authority static token path not set.");
}
if (!File.Exists(path))
{
throw new FileNotFoundException("Authority static token file not found.", path);
}
return File.ReadAllText(path);
}
}
public sealed class AuthorityTokenHealthCheck : IHealthCheck
{
private readonly IAuthorityTokenProvider _tokenProvider;
private readonly ILogger<AuthorityTokenHealthCheck> _logger;
public AuthorityTokenHealthCheck(IAuthorityTokenProvider tokenProvider, ILogger<AuthorityTokenHealthCheck> logger)
{
_tokenProvider = tokenProvider;
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var token = await _tokenProvider.GetTokenAsync(cancellationToken);
return HealthCheckResult.Healthy("Authority token acquired.", data: new Dictionary<string, object>
{
["expiresAtUtc"] = token.ExpiresAtUtc?.ToString("O") ?? "static"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to obtain Authority token.");
return HealthCheckResult.Unhealthy("Failed to obtain Authority token.", ex);
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Security.Cryptography.X509Certificates;
using StellaOps.Zastava.Webhook.Configuration;
namespace StellaOps.Zastava.Webhook.Certificates;
/// <summary>
/// Placeholder implementation for CSR-based certificate provisioning.
/// </summary>
public sealed class CsrCertificateSource : IWebhookCertificateSource
{
private readonly ILogger<CsrCertificateSource> _logger;
public CsrCertificateSource(ILogger<CsrCertificateSource> logger)
{
_logger = logger;
}
public bool CanHandle(ZastavaWebhookTlsMode mode) => mode == ZastavaWebhookTlsMode.CertificateSigningRequest;
public X509Certificate2 LoadCertificate(ZastavaWebhookTlsOptions options)
{
_logger.LogError("CSR certificate mode is not implemented yet. Configuration requested CSR mode.");
throw new NotSupportedException("CSR certificate provisioning is not implemented (tracked by ZASTAVA-WEBHOOK-12-101).");
}
}

View File

@@ -0,0 +1,49 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Options;
using StellaOps.Zastava.Webhook.Configuration;
namespace StellaOps.Zastava.Webhook.Certificates;
public interface IWebhookCertificateProvider
{
X509Certificate2 GetCertificate();
}
public sealed class WebhookCertificateProvider : IWebhookCertificateProvider
{
private readonly ILogger<WebhookCertificateProvider> _logger;
private readonly ZastavaWebhookTlsOptions _options;
private readonly Lazy<X509Certificate2> _certificate;
private readonly IWebhookCertificateSource _certificateSource;
public WebhookCertificateProvider(
IOptions<ZastavaWebhookOptions> options,
IEnumerable<IWebhookCertificateSource> certificateSources,
ILogger<WebhookCertificateProvider> logger)
{
_logger = logger;
_options = options.Value.Tls;
_certificateSource = certificateSources.FirstOrDefault(source => source.CanHandle(_options.Mode))
?? throw new InvalidOperationException($"No certificate source registered for mode {_options.Mode}.");
_certificate = new Lazy<X509Certificate2>(LoadCertificate, LazyThreadSafetyMode.ExecutionAndPublication);
}
public X509Certificate2 GetCertificate() => _certificate.Value;
private X509Certificate2 LoadCertificate()
{
_logger.LogInformation("Loading webhook TLS certificate using {Mode} mode.", _options.Mode);
var certificate = _certificateSource.LoadCertificate(_options);
_logger.LogInformation("Loaded webhook TLS certificate with subject {Subject} and thumbprint {Thumbprint}.",
certificate.Subject, certificate.Thumbprint);
return certificate;
}
}
public interface IWebhookCertificateSource
{
bool CanHandle(ZastavaWebhookTlsMode mode);
X509Certificate2 LoadCertificate(ZastavaWebhookTlsOptions options);
}

View File

@@ -0,0 +1,98 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
using StellaOps.Zastava.Webhook.Configuration;
namespace StellaOps.Zastava.Webhook.Certificates;
public sealed class SecretFileCertificateSource : IWebhookCertificateSource
{
private readonly ILogger<SecretFileCertificateSource> _logger;
public SecretFileCertificateSource(ILogger<SecretFileCertificateSource> logger)
{
_logger = logger;
}
public bool CanHandle(ZastavaWebhookTlsMode mode) => mode == ZastavaWebhookTlsMode.Secret;
public X509Certificate2 LoadCertificate(ZastavaWebhookTlsOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
if (!string.IsNullOrWhiteSpace(options.PfxPath))
{
return LoadFromPfx(options.PfxPath, options.PfxPassword);
}
if (string.IsNullOrWhiteSpace(options.CertificatePath) || string.IsNullOrWhiteSpace(options.PrivateKeyPath))
{
throw new InvalidOperationException("TLS mode 'Secret' requires either a PFX bundle or both PEM certificate and private key paths.");
}
if (!File.Exists(options.CertificatePath))
{
throw new FileNotFoundException("Webhook certificate file not found.", options.CertificatePath);
}
if (!File.Exists(options.PrivateKeyPath))
{
throw new FileNotFoundException("Webhook certificate private key file not found.", options.PrivateKeyPath);
}
try
{
var certificate = X509Certificate2.CreateFromPemFile(options.CertificatePath, options.PrivateKeyPath)
.WithExportablePrivateKey();
_logger.LogDebug("Loaded certificate {Subject} from PEM secret files.", certificate.Subject);
return certificate;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load webhook certificate from PEM files {CertPath} / {KeyPath}.",
options.CertificatePath, options.PrivateKeyPath);
throw;
}
}
private X509Certificate2 LoadFromPfx(string pfxPath, string? password)
{
if (!File.Exists(pfxPath))
{
throw new FileNotFoundException("Webhook certificate PFX bundle not found.", pfxPath);
}
try
{
var storageFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet;
var certificate = X509CertificateLoader.LoadPkcs12FromFile(pfxPath, password, storageFlags);
_logger.LogDebug("Loaded certificate {Subject} from PFX bundle.", certificate.Subject);
return certificate;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load webhook certificate from PFX bundle {PfxPath}.", pfxPath);
throw;
}
}
}
internal static class X509Certificate2Extensions
{
public static X509Certificate2 WithExportablePrivateKey(this X509Certificate2 certificate)
{
// Ensure the private key is exportable for Kestrel; CreateFromPemFile returns a temporary key material otherwise.
using var rsa = certificate.GetRSAPrivateKey();
if (rsa is null)
{
return certificate;
}
var certificateWithKey = certificate.CopyWithPrivateKey(rsa);
certificate.Dispose();
return certificateWithKey;
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace StellaOps.Zastava.Webhook.Certificates;
public sealed class WebhookCertificateHealthCheck : IHealthCheck
{
private readonly IWebhookCertificateProvider _certificateProvider;
private readonly ILogger<WebhookCertificateHealthCheck> _logger;
private readonly TimeSpan _expiryThreshold = TimeSpan.FromDays(7);
public WebhookCertificateHealthCheck(
IWebhookCertificateProvider certificateProvider,
ILogger<WebhookCertificateHealthCheck> logger)
{
_certificateProvider = certificateProvider;
_logger = logger;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var certificate = _certificateProvider.GetCertificate();
var expires = certificate.NotAfter.ToUniversalTime();
var remaining = expires - DateTimeOffset.UtcNow;
if (remaining <= TimeSpan.Zero)
{
return Task.FromResult(HealthCheckResult.Unhealthy("Webhook certificate expired.", data: new Dictionary<string, object>
{
["expiresAtUtc"] = expires.ToString("O")
}));
}
if (remaining <= _expiryThreshold)
{
return Task.FromResult(HealthCheckResult.Degraded("Webhook certificate nearing expiry.", data: new Dictionary<string, object>
{
["expiresAtUtc"] = expires.ToString("O"),
["daysRemaining"] = remaining.TotalDays
}));
}
return Task.FromResult(HealthCheckResult.Healthy("Webhook certificate valid.", data: new Dictionary<string, object>
{
["expiresAtUtc"] = expires.ToString("O"),
["daysRemaining"] = remaining.TotalDays
}));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load webhook certificate.");
return Task.FromResult(HealthCheckResult.Unhealthy("Failed to load webhook certificate.", ex));
}
}
}

View File

@@ -0,0 +1,146 @@
using System.ComponentModel.DataAnnotations;
namespace StellaOps.Zastava.Webhook.Configuration;
public sealed class ZastavaWebhookOptions
{
public const string SectionName = "zastava:webhook";
[Required]
public ZastavaWebhookTlsOptions Tls { get; init; } = new();
[Required]
public ZastavaWebhookAuthorityOptions Authority { get; init; } = new();
[Required]
public ZastavaWebhookAdmissionOptions Admission { get; init; } = new();
}
public sealed class ZastavaWebhookAdmissionOptions
{
/// <summary>
/// Namespaces that default to fail-open when backend calls fail.
/// </summary>
public HashSet<string> FailOpenNamespaces { get; init; } = new(StringComparer.Ordinal);
/// <summary>
/// Namespaces that must fail-closed even if the global default is fail-open.
/// </summary>
public HashSet<string> FailClosedNamespaces { get; init; } = new(StringComparer.Ordinal);
/// <summary>
/// Global fail-open toggle. When true, namespaces not in <see cref="FailClosedNamespaces"/> will allow requests on backend failures.
/// </summary>
public bool FailOpenByDefault { get; init; }
/// <summary>
/// Enables tag resolution to immutable digests when set.
/// </summary>
public bool ResolveTags { get; init; } = true;
/// <summary>
/// Optional cache seed path for pre-computed runtime verdicts.
/// </summary>
public string? CacheSeedPath { get; init; }
}
public enum ZastavaWebhookTlsMode
{
Secret = 0,
CertificateSigningRequest = 1
}
public sealed class ZastavaWebhookTlsOptions
{
[Required]
public ZastavaWebhookTlsMode Mode { get; init; } = ZastavaWebhookTlsMode.Secret;
/// <summary>
/// PEM certificate path when using <see cref="ZastavaWebhookTlsMode.Secret"/>.
/// </summary>
public string? CertificatePath { get; init; }
/// <summary>
/// PEM private key path when using <see cref="ZastavaWebhookTlsMode.Secret"/>.
/// </summary>
public string? PrivateKeyPath { get; init; }
/// <summary>
/// Optional PFX bundle path; takes precedence over PEM values when provided.
/// </summary>
public string? PfxPath { get; init; }
/// <summary>
/// Optional password for the PFX bundle.
/// </summary>
public string? PfxPassword { get; init; }
/// <summary>
/// Optional CA bundle path to present to Kubernetes when configuring webhook registration.
/// </summary>
public string? CaBundlePath { get; init; }
/// <summary>
/// CSR related settings when <see cref="Mode"/> equals <see cref="ZastavaWebhookTlsMode.CertificateSigningRequest"/>.
/// </summary>
public ZastavaWebhookTlsCsrOptions Csr { get; init; } = new();
}
public sealed class ZastavaWebhookTlsCsrOptions
{
/// <summary>
/// Kubernetes namespace that owns the <c>CertificateSigningRequest</c> object.
/// </summary>
[Required(AllowEmptyStrings = false)]
public string Namespace { get; init; } = "stellaops";
/// <summary>
/// CSR object name; defaults to <c>zastava-webhook</c>.
/// </summary>
[Required(AllowEmptyStrings = false)]
[MaxLength(253)]
public string Name { get; init; } = "zastava-webhook";
/// <summary>
/// DNS names placed in the CSR <c>subjectAltName</c>.
/// </summary>
[MinLength(1)]
public string[] DnsNames { get; init; } = Array.Empty<string>();
/// <summary>
/// Where the signed certificate is persisted after approval (mounted emptyDir).
/// </summary>
[Required(AllowEmptyStrings = false)]
public string PersistPath { get; init; } = "/var/run/zastava-webhook/certs";
}
public sealed class ZastavaWebhookAuthorityOptions
{
/// <summary>
/// Authority issuer URL for token acquisition.
/// </summary>
[Required(AllowEmptyStrings = false)]
public Uri Issuer { get; init; } = new("https://authority.internal");
/// <summary>
/// Audience that tokens must target.
/// </summary>
[MinLength(1)]
public string[] Audience { get; init; } = new[] { "scanner", "zastava" };
/// <summary>
/// Optional path to static OpTok for bootstrap environments.
/// </summary>
public string? StaticTokenPath { get; init; }
/// <summary>
/// Optional literal token value (test only). Takes precedence over <see cref="StaticTokenPath"/>.
/// </summary>
public string? StaticTokenValue { get; init; }
/// <summary>
/// Interval for refreshing cached tokens before expiry.
/// </summary>
[Range(typeof(double), "1", "3600")]
public double RefreshSkewSeconds { get; init; } = TimeSpan.FromMinutes(5).TotalSeconds;
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Zastava.Webhook.Authority;
using StellaOps.Zastava.Webhook.Certificates;
using StellaOps.Zastava.Webhook.Configuration;
using StellaOps.Zastava.Webhook.Hosting;
namespace Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddZastavaWebhook(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<ZastavaWebhookOptions>()
.Bind(configuration.GetSection(ZastavaWebhookOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IWebhookCertificateSource, SecretFileCertificateSource>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IWebhookCertificateSource, CsrCertificateSource>());
services.TryAddSingleton<IWebhookCertificateProvider, WebhookCertificateProvider>();
services.TryAddSingleton<WebhookCertificateHealthCheck>();
services.TryAddSingleton<IAuthorityTokenProvider, StaticAuthorityTokenProvider>();
services.TryAddSingleton<AuthorityTokenHealthCheck>();
services.AddHostedService<StartupValidationHostedService>();
services.AddHealthChecks()
.AddCheck<WebhookCertificateHealthCheck>("webhook_tls")
.AddCheck<AuthorityTokenHealthCheck>("authority_token");
return services;
}
}

View File

@@ -0,0 +1,31 @@
using StellaOps.Zastava.Webhook.Authority;
using StellaOps.Zastava.Webhook.Certificates;
namespace StellaOps.Zastava.Webhook.Hosting;
public sealed class StartupValidationHostedService : IHostedService
{
private readonly IWebhookCertificateProvider _certificateProvider;
private readonly IAuthorityTokenProvider _authorityTokenProvider;
private readonly ILogger<StartupValidationHostedService> _logger;
public StartupValidationHostedService(
IWebhookCertificateProvider certificateProvider,
IAuthorityTokenProvider authorityTokenProvider,
ILogger<StartupValidationHostedService> logger)
{
_certificateProvider = certificateProvider;
_authorityTokenProvider = authorityTokenProvider;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Running webhook startup validation.");
_certificateProvider.GetCertificate();
await _authorityTokenProvider.GetTokenAsync(cancellationToken);
_logger.LogInformation("Webhook startup validation complete.");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -0,0 +1,105 @@
# Zastava Webhook · Wave 0 Implementation Notes
> Authored 2025-10-19 by Zastava Webhook Guild.
## ZASTAVA-WEBHOOK-12-101 — Admission Controller Host (TLS bootstrap + Authority auth)
**Objectives**
- Provide a deterministic, restart-safe .NET 10 host that exposes a Kubernetes ValidatingAdmissionWebhook endpoint.
- Load serving certificates at start-up only (per restart-time plug-in rule) and surface reload guidance via documentation rather than hot-reload.
- Authenticate outbound calls to Authority/Scanner using OpTok + DPoP as defined in `docs/ARCHITECTURE_ZASTAVA.md`.
**Plan**
1. **Project scaffolding**
- Create `StellaOps.Zastava.Webhook` project with minimal API pipeline (`Program.cs`, `Startup` equivalent via extension methods).
- Reference shared helpers once `ZASTAVA-CORE-12-201/202` land; temporarily stub interfaces behind `IZastavaAdmissionRequest`/`IZastavaAdmissionResult`.
2. **TLS bootstrap**
- Support two certificate sources:
1. Mounted secret path (`/var/run/secrets/zastava-webhook/tls.{crt,key}`) with optional CA bundle.
2. CSR workflow: generate CSR + private key, submit to Kubernetes Certificates API when `admission.tls.autoApprove` enabled; persist signed cert/key to mounted emptyDir for reuse across replicas.
- Validate cert/key pair on boot; abort start-up if invalid to preserve deterministic behavior.
- Configure Kestrel for mutual TLS off (API Server already provides client auth) but enforce minimum TLS 1.3, strong cipher suite list, HTTP/2 disabled (K8s uses HTTP/1.1).
3. **Authority auth**
- Bootstrap Authority client via shared DI extension (`AuthorityClientBuilder` once exposed); until then, placeholder `IAuthorityTokenSource` reading static OpTok from secret for smoke testing.
- Implement DPoP proof generator bound to webhook host keypair (prefer Ed25519) with configurable rotation period (default 24h, triggered at restart).
- Add background health check verifying token freshness and surfacing metrics (`zastava.authority_token_renew_failures_total`).
4. **Hosting concerns**
- Configure structured logging with correlation id from AdmissionReview UID.
- Expose `/healthz` (reads cert expiry, Authority token status) and `/metrics` (Prometheus).
- Add readiness gate that requires initial TLS and Authority bootstrap to succeed.
**Deliverables**
- Compilable host project with integration tests covering TLS load (mounted files + CSR mock) and Authority token acquisition.
- Documentation snippet for deploy charts describing secret/CSR wiring.
**Open Questions**
- Need confirmation from Core guild on DTO naming (`AdmissionReviewEnvelope`, `AdmissionDecision`) to avoid rework.
- Determine whether CSR auto-approval is acceptable for air-gapped clusters without Kubernetes cert-manager; may require fallback manual cert import path.
## ZASTAVA-WEBHOOK-12-102 — Backend policy query & digest resolution
**Objectives**
- Resolve all images within AdmissionReview to immutable digests before policy evaluation.
- Call Scanner WebService `/api/v1/scanner/policy/runtime` with namespace/labels/images payload, enforce verdicts with deterministic error messaging.
**Plan**
1. **Image resolution**
- Implement resolver service with pluggable strategies:
- Use existing digest if present.
- Resolve tags via registry HEAD (respecting `admission.resolveTags` flag); fallback to Observer-provided digest once core DTOs available.
- Cache per-registry auth to minimise latency; adhere to allow/deny lists from configuration.
2. **Scanner client**
- Define typed request/response models mirroring `docs/ARCHITECTURE_ZASTAVA.md` structure (`ttlSeconds`, `results[digest] -> { signed, hasSbom, policyVerdict, reasons, rekor }`).
- Implement retry policy (3 attempts, exponential backoff) and map HTTP errors to webhook fail-open/closed depending on namespace configuration.
- Instrument latency (`zastava.backend_latency_seconds`) and failure counts.
3. **Verdict enforcement**
- Evaluate per-image results: if any `policyVerdict != pass` (or `warn` when `enforceWarnings=false`), deny with aggregated reasons.
- Attach `ttlSeconds` to admission response annotations for auditing.
- Record structured logs with namespace, pod, image digest, decision, reasons, backend latency.
4. **Contract coordination**
- Schedule joint review with Scanner WebService guild once SCANNER-RUNTIME-12-302 schema stabilises; track in TASKS sub-items.
- Provide sample payload fixtures for CLI team (`CLI-RUNTIME-13-005`) to validate table output; ensure field names stay aligned.
**Deliverables**
- Registry resolver unit tests (tag->digest) with deterministic fixtures.
- HTTP client integration tests using Scanner stub returning varied verdict combinations.
- Documentation update summarising contract and failure handling.
**Open Questions**
- Confirm expected policy verdict enumeration (`pass|warn|fail|error`?) and textual reason codes.
- Need TTL behaviour: should webhook reduce TTL when backend returns > configured max?
## ZASTAVA-WEBHOOK-12-103 — Caching, fail-open/closed toggles, metrics/logging
**Objectives**
- Provide deterministic caching layer respecting backend TTL while ensuring eviction on policy mutation.
- Allow namespace-scoped fail-open behaviour with explicit metrics and alerts.
- Surface actionable metrics/logging aligned with Architecture doc.
**Plan**
1. **Cache design**
- In-memory LRU keyed by image digest; value carries verdict payload + expiry timestamp.
- Support optional persistent seed (read-only) to prime hot digests for offline clusters (config: `admission.cache.seedPath`).
- On startup, load seed file and emit metric `zastava.cache_seed_entries_total`.
- Evict entries on TTL or when `policyRevision` annotation in AdmissionReview changes (requires hook from Core DTO).
2. **Fail-open/closed toggles**
- Configuration: global default + namespace overrides through `admission.failOpenNamespaces`, `admission.failClosedNamespaces`.
- Decision matrix:
- Backend success + verdict PASS → allow.
- Backend success + non-pass → deny unless namespace override says warn allowed.
- Backend failure → allow if namespace fail-open, deny otherwise; annotate response with `zastava.ops/fail-open=true`.
- Implement policy change event hook (future) to clear cache if observer signals revocation.
3. **Metrics & logging**
- Counters: `zastava.admission_requests_total{decision}`, `zastava.cache_hits_total{result=hit|miss}`, `zastava.fail_open_total`, `zastava.backend_failures_total{stage}`.
- Histograms: `zastava.admission_latency_seconds` (overall), `zastava.resolve_latency_seconds`.
- Logs: structured JSON with `decision`, `namespace`, `pod`, `imageDigest`, `reasons`, `cacheStatus`, `failMode`.
- Optionally emit OpenTelemetry span for admission path with attributes capturing backend latency + cache path.
4. **Testing & ops hooks**
- Unit tests for cache TTL, namespace override logic, fail-open metric increments.
- Integration test simulating backend outage ensuring fail-open/closed behaviour matches config.
- Document runbook snippet describing interpreting metrics and toggling namespaces.
**Open Questions**
- Confirm whether cache entries should include `policyRevision` to detect backend policy updates; requires coordination with Policy guild.
- Need guidance on maximum cache size (default suggestions: 5k entries per replica?) to avoid memory blow-up.

View File

@@ -0,0 +1,68 @@
using System.Security.Authentication;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Serilog;
using Serilog.Events;
using StellaOps.Zastava.Webhook.Authority;
using StellaOps.Zastava.Webhook.Certificates;
using StellaOps.Zastava.Webhook.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
{
loggerConfiguration
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console();
});
builder.Services.AddRouting();
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddHttpClient();
builder.Services.AddZastavaWebhook(builder.Configuration);
builder.WebHost.ConfigureKestrel((context, options) =>
{
options.AddServerHeader = false;
options.Limits.MinRequestBodyDataRate = null; // Admission payloads are small; relax defaults for determinism.
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certificateProvider = options.ApplicationServices?.GetRequiredService<IWebhookCertificateProvider>()
?? throw new InvalidOperationException("Webhook certificate provider unavailable.");
httpsOptions.SslProtocols = SslProtocols.Tls13;
httpsOptions.ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.NoCertificate;
httpsOptions.CheckCertificateRevocation = false; // Kubernetes API server terminates client auth; revocation handled upstream.
httpsOptions.ServerCertificate = certificateProvider.GetCertificate();
});
});
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseStatusCodePages();
// Health endpoints.
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
AllowCachingResponses = false
});
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
AllowCachingResponses = false,
Predicate = _ => false
});
// Placeholder admission endpoint; will be replaced as tasks 12-102/12-103 land.
app.MapPost("/admission", () => Results.StatusCode(StatusCodes.Status501NotImplemented))
.WithName("AdmissionReview");
app.MapGet("/", () => Results.Ok(new { status = "ok", service = "zastava-webhook" }));
app.Run();

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RootNamespace>StellaOps.Zastava.Webhook</RootNamespace>
<NoWarn>$(NoWarn);CA2254</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0-preview.6.24328.4" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
# Zastava Webhook Task Board
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| ZASTAVA-WEBHOOK-12-101 | DOING | Zastava Webhook Guild | — | Admission controller host with TLS bootstrap and Authority auth. | Webhook host boots with deterministic TLS bootstrap, enforces Authority-issued credentials, e2e smoke proves admission callback lifecycle, structured logs + metrics emit on each decision. |
| ZASTAVA-WEBHOOK-12-102 | DOING | Zastava Webhook Guild | — | Query Scanner `/policy/runtime`, resolve digests, enforce verdicts. | Scanner client resolves image digests + policy verdicts, unit tests cover allow/deny, integration harness rejects/admits workloads per policy with deterministic payloads. |
| ZASTAVA-WEBHOOK-12-103 | DOING | Zastava Webhook Guild | — | Caching, fail-open/closed toggles, metrics/logging for admission decisions. | Configurable cache TTL + seeds survive restart, fail-open/closed toggles verified via tests, metrics/logging exported per decision path, docs note operational knobs. |
> Status update · 2025-10-19: Confirmed no prerequisites for ZASTAVA-WEBHOOK-12-101/102/103; tasks moved to DOING for kickoff. Implementation plan covering TLS bootstrap, backend contract, caching/metrics recorded in `IMPLEMENTATION_PLAN.md`.