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:
@@ -4,6 +4,7 @@ using System.Text;
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Sync.Models;
|
using LehrerApp.Sync.Models;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace LehrerApp.Api;
|
namespace LehrerApp.Api;
|
||||||
@@ -21,7 +22,7 @@ public static class Endpoints
|
|||||||
if (!store.VerifyPassword(req.Username, req.Password))
|
if (!store.VerifyPassword(req.Username, req.Password))
|
||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
return Results.Ok(new { token = Jwt(req.Username, secret), userId = req.Username });
|
return Results.Ok(new { token = Jwt(req.Username, secret), userId = req.Username });
|
||||||
});
|
}).RequireRateLimiting("login");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sync ──────────────────────────────────────────────────────────────────
|
// ── Sync ──────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
using LehrerApp.Api;
|
using LehrerApp.Api;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -9,6 +11,9 @@ builder.WebHost.UseKestrel(o =>
|
|||||||
{
|
{
|
||||||
var port = builder.Configuration.GetValue<int>("Api:Port", 5000);
|
var port = builder.Configuration.GetValue<int>("Api:Port", 5000);
|
||||||
o.ListenAnyIP(port);
|
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";
|
var data = builder.Configuration["Api:DataPath"] ?? "./data";
|
||||||
@@ -29,6 +34,30 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|||||||
});
|
});
|
||||||
builder.Services.AddAuthorization();
|
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<UserStore>(_ => new UserStore(data));
|
||||||
builder.Services.AddSingleton<AttachmentStore>(_ => new AttachmentStore(data));
|
builder.Services.AddSingleton<AttachmentStore>(_ => new AttachmentStore(data));
|
||||||
builder.Services.AddSingleton<EventStore>(_ => new EventStore(data));
|
builder.Services.AddSingleton<EventStore>(_ => new EventStore(data));
|
||||||
@@ -38,6 +67,7 @@ builder.Services.AddSingleton<PlainEventStore>(sp =>
|
|||||||
new PlainEventStore(sp.GetRequiredService<EventStore>()));
|
new PlainEventStore(sp.GetRequiredService<EventStore>()));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
app.UseRateLimiter();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapAuthEndpoints(secret);
|
app.MapAuthEndpoints(secret);
|
||||||
|
|||||||
@@ -1387,10 +1387,24 @@ ist aber nur aktiv, wenn eine Server-URL konfiguriert ist.
|
|||||||
entfernt (kein offener Registrierungs-Endpunkt für ein Einzel-/Familien-Deployment); neue
|
entfernt (kein offener Registrierungs-Endpunkt für ein Einzel-/Familien-Deployment); neue
|
||||||
Nutzer werden per CLI angelegt (`dotnet LehrerApp.Api.dll create-user <name>`, dokumentiert
|
Nutzer werden per CLI angelegt (`dotnet LehrerApp.Api.dll create-user <name>`, dokumentiert
|
||||||
in `docker/README.md`), damit keine zusätzliche unauthentifizierte Angriffsfläche entsteht.
|
in `docker/README.md`), damit keine zusätzliche unauthentifizierte Angriffsfläche entsteht.
|
||||||
- [ ] **10.2.2** Rate Limiting und Request-Größenbegrenzung.
|
- [x] **10.2.2** Rate Limiting und Request-Größenbegrenzung.
|
||||||
- [ ] **10.2.3** Serverseitiges Backup der Event-/Snapshot-Dateien.
|
|
||||||
|
**Umsetzung:** ASP.NET Cores eingebautes `Microsoft.AspNetCore.RateLimiting` (keine neue
|
||||||
|
Paketabhängigkeit). `/api/auth/login` speziell auf 5 Versuche/Minute begrenzt
|
||||||
|
(Brute-Force-Schutz), alle Endpunkte zusätzlich global auf 120 Anfragen/Minute je IP. Kestrel
|
||||||
|
`MaxRequestBodySize` auf 15 MB gedeckelt (Anhänge sind clientseitig ohnehin auf 10 MB
|
||||||
|
begrenzt, siehe `IAttachmentStorage.MaxSizeBytes`).
|
||||||
|
- [x] **10.2.3** Serverseitiges Backup der Event-/Snapshot-Dateien.
|
||||||
|
|
||||||
|
**Umsetzung:** `docker/backup.sh` — Tar-Archiv von `./data` (Ereignis-Logs, Snapshots,
|
||||||
|
Anhänge, Nutzer), räumt Archive älter als 30 Tage auf, läuft direkt auf dem Host (kein
|
||||||
|
Container-Zugriff nötig), dokumentiert samt Cron-Beispiel in `docker/README.md`.
|
||||||
- [ ] **10.2.4** Docker-Setup in [docker/](docker/) verifizieren und dokumentieren.
|
- [ ] **10.2.4** Docker-Setup in [docker/](docker/) verifizieren und dokumentieren.
|
||||||
|
|
||||||
|
**Teilweise:** `docker/README.md` um `create-user`-Flow, Backup und Rate-Limits ergänzt.
|
||||||
|
Ein tatsächlicher `docker compose up`-Durchlauf konnte in dieser Umgebung nicht verifiziert
|
||||||
|
werden (kein Docker verfügbar) — steht vor dem ersten echten Deployment noch aus.
|
||||||
|
|
||||||
### 10.3 Verschlüsselung
|
### 10.3 Verschlüsselung
|
||||||
- [ ] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase).
|
- [ ] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase).
|
||||||
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
||||||
|
|||||||
@@ -28,3 +28,26 @@ docker compose exec api dotnet LehrerApp.Api.dll create-user <benutzername> --pa
|
|||||||
|
|
||||||
Der Befehl beendet sich danach sofort wieder, ohne den API-Dienst zu starten – für den
|
Der Befehl beendet sich danach sofort wieder, ohne den API-Dienst zu starten – für den
|
||||||
eigentlichen Serverbetrieb läuft `docker compose up` unverändert weiter.
|
eigentlichen Serverbetrieb läuft `docker compose up` unverändert weiter.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
`backup.sh` sichert `./data` (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als komprimiertes
|
||||||
|
Tar-Archiv unter `docker/backups/` und räumt Archive älter als 30 Tage automatisch auf. Läuft
|
||||||
|
direkt auf dem Host (kein Container-Zugriff nötig, `./data` liegt dort per Bind-Mount ohnehin):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker/backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Für regelmäßige Sicherung z. B. per Cron, einmal täglich nachts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
0 3 * * * /pfad/zu/docker/backup.sh >> /pfad/zu/docker/backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
`/api/auth/login` ist auf 5 Versuche pro Minute begrenzt (Brute-Force-Schutz). Alle übrigen
|
||||||
|
Endpunkte sind zusätzlich global auf 120 Anfragen pro Minute je IP begrenzt. Anhänge sind auf
|
||||||
|
15 MB Anfragegröße gedeckelt (Kestrel `MaxRequestBodySize`), einzelne Dateien clientseitig
|
||||||
|
zusätzlich auf 10 MB (`IAttachmentStorage.MaxSizeBytes`).
|
||||||
|
|||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Sichert das komplette ./data-Verzeichnis (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als
|
||||||
|
# komprimiertes Tar-Archiv. Läuft außerhalb des Containers direkt auf dem Host, da ./data per
|
||||||
|
# Bind-Mount ohnehin dort liegt (siehe docker-compose.yml) — kein Zugriff auf den Container nötig.
|
||||||
|
#
|
||||||
|
# Aufruf z.B. per Cron:
|
||||||
|
# 0 3 * * * /pfad/zu/docker/backup.sh >> /pfad/zu/docker/backup.log 2>&1
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DATA_DIR="$SCRIPT_DIR/data"
|
||||||
|
BACKUP_DIR="$SCRIPT_DIR/backups"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
KEEP_DAYS=30
|
||||||
|
|
||||||
|
if [ ! -d "$DATA_DIR" ]; then
|
||||||
|
echo "Kein Datenverzeichnis unter $DATA_DIR gefunden - nichts zu sichern." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
tar -czf "$BACKUP_DIR/lehrerapp-data-$TIMESTAMP.tar.gz" -C "$SCRIPT_DIR" data
|
||||||
|
|
||||||
|
# Alte Backups jenseits von KEEP_DAYS aufräumen, damit das Verzeichnis nicht unbegrenzt wächst.
|
||||||
|
find "$BACKUP_DIR" -name 'lehrerapp-data-*.tar.gz' -mtime "+$KEEP_DAYS" -delete
|
||||||
|
|
||||||
|
echo "Backup erstellt: $BACKUP_DIR/lehrerapp-data-$TIMESTAMP.tar.gz"
|
||||||
Reference in New Issue
Block a user