Baustein 10: Deployment-Haertung (Kapitel 10)

- Rate Limiting ueber ASP.NET Cores eingebautes
  Microsoft.AspNetCore.RateLimiting (keine neue Paketabhaengigkeit):
  /api/auth/login auf 5 Versuche/Minute begrenzt (Brute-Force-Schutz),
  alle Endpunkte zusaetzlich global auf 120 Anfragen/Minute je IP
- Kestrel MaxRequestBodySize auf 15 MB gedeckelt (Anhaenge sind
  clientseitig ohnehin auf 10 MB begrenzt)
- Neu docker/backup.sh: Tar-Archiv von ./data (Ereignis-Logs,
  Snapshots, Anhaenge, Nutzer), raeumt Archive aelter als 30 Tage auf,
  laeuft direkt auf dem Host
- docker/README.md um Backup- und Rate-Limit-Dokumentation ergaenzt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:38:42 +02:00
co-authored by Claude Sonnet 5
parent fc2d7aea3e
commit 6774123270
5 changed files with 98 additions and 3 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ using System.Text;
using LehrerApp.Core.Models;
using LehrerApp.Sync.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.IdentityModel.Tokens;
namespace LehrerApp.Api;
@@ -21,7 +22,7 @@ public static class Endpoints
if (!store.VerifyPassword(req.Username, req.Password))
return Results.Unauthorized();
return Results.Ok(new { token = Jwt(req.Username, secret), userId = req.Username });
});
}).RequireRateLimiting("login");
}
// ── Sync ──────────────────────────────────────────────────────────────────
+30
View File
@@ -1,6 +1,8 @@
using System.Text;
using System.Threading.RateLimiting;
using LehrerApp.Api;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
@@ -9,6 +11,9 @@ builder.WebHost.UseKestrel(o =>
{
var port = builder.Configuration.GetValue<int>("Api:Port", 5000);
o.ListenAnyIP(port);
// Anhänge sind clientseitig auf 10 MB begrenzt (IAttachmentStorage.MaxSizeBytes) - etwas
// Puffer für Verschlüsselungs-Overhead/JSON-Ereignisse, aber trotzdem eine harte Obergrenze.
o.Limits.MaxRequestBodySize = 15 * 1024 * 1024;
});
var data = builder.Configuration["Api:DataPath"] ?? "./data";
@@ -29,6 +34,30 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
});
builder.Services.AddAuthorization();
// Rate Limiting (10.2.2): striktes Limit gezielt gegen Brute-Force auf /api/auth/login, plus ein
// grobes globales Limit pro IP als einfacher Schutz vor Überlastung der übrigen Endpunkte.
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("login", o =>
{
o.PermitLimit = 5;
o.Window = TimeSpan.FromMinutes(1);
o.QueueLimit = 0;
});
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 120,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0,
}));
});
builder.Services.AddSingleton<UserStore>(_ => new UserStore(data));
builder.Services.AddSingleton<AttachmentStore>(_ => new AttachmentStore(data));
builder.Services.AddSingleton<EventStore>(_ => new EventStore(data));
@@ -38,6 +67,7 @@ builder.Services.AddSingleton<PlainEventStore>(sp =>
new PlainEventStore(sp.GetRequiredService<EventStore>()));
var app = builder.Build();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapAuthEndpoints(secret);