diff --git a/.github/workflows/uptime.yml b/.github/workflows/uptime.yml index aff8e69..bf5a46e 100644 --- a/.github/workflows/uptime.yml +++ b/.github/workflows/uptime.yml @@ -1,6 +1,9 @@ name: Uptime Monitor on: + push: + branches: + - master workflow_dispatch: permissions: @@ -18,9 +21,12 @@ jobs: env: SITE_URL: https://petabit-production.up.railway.app - MAX_ATTEMPTS: 3 + MAX_ATTEMPTS: ${{ github.event_name == 'push' && '8' || '3' }} steps: + - name: Allow Railway deployment to start + if: github.event_name == 'push' + run: sleep 90 - name: Wait for production liveness run: | diff --git a/Petabit.Tests/ApplicationTests.cs b/Petabit.Tests/ApplicationTests.cs index b1d75d4..e8d4639 100644 --- a/Petabit.Tests/ApplicationTests.cs +++ b/Petabit.Tests/ApplicationTests.cs @@ -395,6 +395,26 @@ public async Task IssRateLimitRejectsEleventhRequestFromSameClient() Assert.Equal(HttpStatusCode.TooManyRequests, rejectedResponse.StatusCode); } + [Fact] + public async Task GlobalRateLimitProtectsDynamicPagesButNotLiveness() + { + using var client = CreateClientWithIssResponse( + HttpStatusCode.OK, + """{"latitude":45.81,"longitude":15.98,"velocity":27600}"""); + + for (var requestNumber = 1; requestNumber <= 120; requestNumber++) + { + using var response = await client.GetAsync("/Home/Privacy"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + using var rejectedResponse = await client.GetAsync("/Home/Privacy"); + Assert.Equal(HttpStatusCode.TooManyRequests, rejectedResponse.StatusCode); + + using var healthResponse = await client.GetAsync("/health/live"); + Assert.Equal(HttpStatusCode.OK, healthResponse.StatusCode); + } + private HttpClient CreateClientWithIssResponse(HttpStatusCode statusCode, string content) => CreateClientWithHandler(new StubHttpMessageHandler(statusCode, content)); diff --git a/Petabit/Program.cs b/Petabit/Program.cs index 657b1d7..2853441 100644 --- a/Petabit/Program.cs +++ b/Petabit/Program.cs @@ -126,6 +126,16 @@ public static void Main(string[] args) builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + options.GlobalLimiter = PartitionedRateLimiter.Create(context => + RateLimitPartition.GetFixedWindowLimiter( + context.Connection.RemoteIpAddress?.MapToIPv6().ToString() ?? "unknown-client", + _ => new FixedWindowRateLimiterOptions + { + PermitLimit = 120, + Window = TimeSpan.FromMinutes(1), + QueueLimit = 0, + AutoReplenishment = true + })); options.AddPolicy("iss", context => RateLimitPartition.GetFixedWindowLimiter( context.Connection.RemoteIpAddress?.MapToIPv6().ToString() ?? "unknown-client", @@ -212,11 +222,11 @@ public static void Main(string[] args) app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false - }); + }).DisableRateLimiting(); app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = registration => registration.Tags.Contains("ready") - }); + }).DisableRateLimiting(); app.MapControllerRoute( name: "default", diff --git a/README.md b/README.md index c53fbd7..43e434a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![CI](https://github.com/ChevCellios/Petabit/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ChevCellios/Petabit/actions/workflows/ci.yml) [![Security](https://github.com/ChevCellios/Petabit/actions/workflows/security.yml/badge.svg?branch=master)](https://github.com/ChevCellios/Petabit/actions/workflows/security.yml) +[![Production smoke test](https://github.com/ChevCellios/Petabit/actions/workflows/uptime.yml/badge.svg?branch=master)](https://github.com/ChevCellios/Petabit/actions/workflows/uptime.yml) [![Station status freshness](https://github.com/ChevCellios/Petabit/actions/workflows/station-status-freshness.yml/badge.svg?branch=master)](https://github.com/ChevCellios/Petabit/actions/workflows/station-status-freshness.yml) [![Monitored by Better Stack](https://img.shields.io/badge/uptime-Better%20Stack-5B45FF)](https://betterstack.com/uptime) ![.NET](https://img.shields.io/badge/.NET-10.0-blueviolet) @@ -52,7 +53,8 @@ Izvori podataka: - Content Security Policy s jednokratnim nonceom za inline skripte i stilove - sigurnosna zaglavlja `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` i `Permissions-Policy` - antiforgery validacija za POST zahtjeve -- rate limiting ISS endpointa na 10 zahtjeva u minuti po IP adresi klijenta +- globalni rate limiting dinamičkih ruta na 120 zahtjeva u minuti po IP adresi klijenta +- stroži rate limiting ISS endpointa na 10 zahtjeva u minuti po IP adresi klijenta - sigurno DOM renderiranje API podataka bez umetanja preko `innerHTML` - culture cookie s atributima `HttpOnly`, `Secure` i `SameSite=Lax` - analitika tek nakon izričite korisničke privole @@ -63,11 +65,11 @@ Izvori podataka: ## 🧯 Pouzdanost i nadzor - timeout, retry i circuit breaker za vanjski ISS servis -- `/health/live` provjera procesa i `/health/ready` provjera dostupnosti ISS servisa -- ručno pokretljiv GitHub produkcijski smoke test za ciljanu dijagnostiku +- `/health/live` provjera procesa i `/health/ready` provjera dostupnosti ISS servisa, izuzete iz rate limitinga +- automatski GitHub produkcijski smoke test nakon svakog pusha u `master`, uz ručno pokretanje za ciljanu dijagnostiku - Better Stack provjera produkcijskog liveness endpointa svakih 10 minuta - tjedna provjera starosti kuriranih podataka o ISS posadi i letjelicama -- ručni smoke test početne stranice, readiness endpointa i ISS trackera +- smoke test početne stranice, readiness endpointa i ISS trackera - strukturirani JSON logovi u produkciji - validirani `X-Correlation-ID` za povezivanje korisničkog zahtjeva s Railway logovima - sigurna produkcijska error stranica bez izlaganja detalja iznimke @@ -90,7 +92,7 @@ Grana `master` zaštićena je pull request pravilima i zahtijeva prolazak sljede - `Audit NuGet dependencies` - `Analyze C# with CodeQL` -Nakon mergea Railway automatski deploya novu verziju. Better Stack nadzire liveness svakih 10 minuta, a puni produkcijski smoke test pokreće se ručno kada je potrebna ciljana provjera. +Nakon mergea Railway automatski deploya novu verziju, a produkcijski smoke test čeka pokretanje deploya i potvrđuje dostupnost aplikacije. Better Stack dodatno nadzire liveness svakih 10 minuta. ## ✅ Testiranje