diff options
| author | Leitwolf | 2026-10-15 18:44:01 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-15 18:44:01 +0300 |
| commit | 3877e3318e875cdebc3d7b9bc970b4496e5183a9 (patch) | |
| tree | d6390125eb9045ab3496f6cee7544f26a183c4ba | |
| parent | a7604e755e5bb77729a309daea75b14522d22afe (diff) | |
| parent | bd83e9e0fba03a4436e4fcfdde33c1e04d1a9c8f (diff) | |
| download | progmaweb-3877e3318e875cdebc3d7b9bc970b4496e5183a9.tar.gz | |
Merge pull request #37 from Progmasoft/fix/hsts-preload
Treat progmasoft.com as an HSTS-preloaded domain
| -rw-r--r-- | apps/api-tests/SecurityHeaderTests.cs | 18 | ||||
| -rw-r--r-- | apps/api/Program.cs | 8 | ||||
| -rw-r--r-- | ops/nginx/progmaweb.conf | 14 |
3 files changed, 34 insertions, 6 deletions
diff --git a/apps/api-tests/SecurityHeaderTests.cs b/apps/api-tests/SecurityHeaderTests.cs index 2d53ecf..e355035 100644 --- a/apps/api-tests/SecurityHeaderTests.cs +++ b/apps/api-tests/SecurityHeaderTests.cs @@ -4,6 +4,7 @@ using System.Net; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.Testing; using Progmasoft.Progmaweb.Api.Security; namespace Progmasoft.Progmaweb.Api.Tests; @@ -41,4 +42,21 @@ public sealed class SecurityHeaderTests Assert.Contains("frame-ancestors 'none'", Assert.Single(response.Headers.GetValues("Content-Security-Policy")), StringComparison.Ordinal); } + + [Fact] + public async Task HttpsResponsesCarryThePreloadPolicyNginxSends() + { + await using AccountApiFactory factory = new(); + using HttpClient client = factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + BaseAddress = new Uri("https://account.progmasoft.com"), + }); + + HttpResponseMessage response = + await client.GetAsync("/api/v1/status", TestContext.Current.CancellationToken); + + Assert.Equal("max-age=63072000; includeSubDomains; preload", + Assert.Single(response.Headers.GetValues("Strict-Transport-Security"))); + } } diff --git a/apps/api/Program.cs b/apps/api/Program.cs index fd6e97b..3eb96c9 100644 --- a/apps/api/Program.cs +++ b/apps/api/Program.cs @@ -47,6 +47,14 @@ if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(goo }); } builder.Services.AddProblemDetails(); +// progmasoft.com is served as if .com required HSTS: the API answers with the same preload header as Nginx, so a +// proxied response never carries two different policies. +builder.Services.AddHsts(options => +{ + options.MaxAge = TimeSpan.FromDays(730); + options.IncludeSubDomains = true; + options.Preload = true; +}); builder.Services.AddHealthChecks(); builder.Services.AddRateLimiter(options => { diff --git a/ops/nginx/progmaweb.conf b/ops/nginx/progmaweb.conf index ef6d152..b7d5d8a 100644 --- a/ops/nginx/progmaweb.conf +++ b/ops/nginx/progmaweb.conf @@ -12,7 +12,7 @@ server { } location / { - return 301 https://progmasoft.com$request_uri; + return 308 https://progmasoft.com$request_uri; } } @@ -27,7 +27,7 @@ server { } location / { - return 301 https://account.progmasoft.com$request_uri; + return 308 https://account.progmasoft.com$request_uri; } } @@ -42,7 +42,7 @@ server { } location / { - return 301 https://viget.progmasoft.com$request_uri; + return 308 https://viget.progmasoft.com$request_uri; } } @@ -56,6 +56,8 @@ server { ssl_certificate_key /etc/letsencrypt/live/progmasoft.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + return 308 https://progmasoft.com$request_uri; } @@ -71,7 +73,7 @@ server { client_max_body_size 1m; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; @@ -103,7 +105,7 @@ server { client_max_body_size 1m; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; # Account, authentication, recovery, dashboard, and API responses are not @@ -138,7 +140,7 @@ server { client_max_body_size 1m; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; |