Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf2026-10-15 18:44:01 +0300
committerGitHub2026-10-15 18:44:01 +0300
commit3877e3318e875cdebc3d7b9bc970b4496e5183a9 (patch)
treed6390125eb9045ab3496f6cee7544f26a183c4ba
parenta7604e755e5bb77729a309daea75b14522d22afe (diff)
parentbd83e9e0fba03a4436e4fcfdde33c1e04d1a9c8f (diff)
downloadprogmaweb-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.cs18
-rw-r--r--apps/api/Program.cs8
-rw-r--r--ops/nginx/progmaweb.conf14
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;