Security & resource hardening: eliminate CPU/disk attack surface
Addresses production CPU spike incident. Key changes: - Guard OTel exporter behind OTEL_EXPORTER_OTLP_ENDPOINT env var; filter tracing to /api paths only — unconditional export was primary suspect - Remove /healthz endpoint entirely (unauthenticated, hit DB on every call) - Replace KnownUserMiddleware with POST /api/users/me called once on login from TokenSync — eliminates unconditional DB write on every request - Add DB indexes: (BudgetId, IsDeleted) on Incomes/Outgos, OwnerUserId on Budgets, SharedWithUserId and (IsPending, SharedWithEmail) on BudgetShares - Move UseRateLimiter() before UseStaticFiles() so all requests are throttled - Replace full-array reorder with move-by-position (id + newIndex) — bounded input, fewer DB writes, better API design - Lock ForwardedHeaders to 172.20.0.0/16 subnet; fixes KnownNetworks deprecation warning (0 warnings in build now) - Add AsNoTracking() to all read-only queries in Summary/Incomes/OutgosController - FrequencyCalculator returns 0 for unknown enum values instead of throwing - Thread.Sleep → await Task.Delay in OIDC startup loop - AllowedHosts locked to budget.stwaddle.com Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-32
@@ -3,11 +3,9 @@ using Budget.Api.Services;
|
||||
using Budget.Infrastructure.Data;
|
||||
using Budget.Infrastructure.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using OpenTelemetry;
|
||||
@@ -16,20 +14,25 @@ using OpenTelemetry.Trace;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
var otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
|
||||
if (!string.IsNullOrEmpty(otlpEndpoint))
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource => resource
|
||||
.AddService(serviceName: "budget", serviceVersion: "1.0.0"))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddEntityFrameworkCoreInstrumentation())
|
||||
.UseOtlpExporter();
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource => resource
|
||||
.AddService(serviceName: "budget", serviceVersion: "1.0.0"))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation(opts =>
|
||||
opts.Filter = ctx => ctx.Request.Path.StartsWithSegments("/api"))
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddEntityFrameworkCoreInstrumentation())
|
||||
.UseOtlpExporter();
|
||||
}
|
||||
|
||||
var connStr = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? $"Host={builder.Configuration["POSTGRES_HOST"] ?? "localhost"};" +
|
||||
@@ -43,8 +46,9 @@ builder.Services.AddDbContext<AppDbContext>(opt => opt.UseNpgsql(connStr));
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
options.KnownIPNetworks.Clear();
|
||||
options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("172.20.0.0/16"));
|
||||
});
|
||||
|
||||
var oidc = builder.Configuration.GetSection("Oidc");
|
||||
@@ -109,9 +113,6 @@ builder.Services.AddControllers()
|
||||
.AddJsonOptions(opts =>
|
||||
opts.JsonSerializerOptions.Converters.Add(
|
||||
new System.Text.Json.Serialization.JsonStringEnumConverter()));
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddDbContextCheck<AppDbContext>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Block startup until OIDC discovery succeeds (handles auth/app container race).
|
||||
@@ -136,7 +137,7 @@ while (true)
|
||||
break;
|
||||
}
|
||||
startupLogger.LogWarning(ex, "OIDC discovery failed; retrying in 5 s");
|
||||
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,26 +151,15 @@ using (var scope = app.Services.CreateScope())
|
||||
app.UseForwardedHeaders();
|
||||
app.UseMiddleware<ErrorHandlingMiddleware>();
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseMiddleware<KnownUserMiddleware>();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/healthz", new HealthCheckOptions
|
||||
{
|
||||
ResultStatusCodes =
|
||||
{
|
||||
[HealthStatus.Healthy] = StatusCodes.Status200OK,
|
||||
[HealthStatus.Degraded] = StatusCodes.Status200OK,
|
||||
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable,
|
||||
}
|
||||
});
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user