From 607bd17320534ee9d55a0a51c9e7233fb1ece5da Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:59:14 +0200 Subject: [PATCH 1/3] all: Implement HSTS headers --- pkg/web/web.go | 1 + pkg/webmiddleware/hsts_headers.go | 33 ++++++++++++++ pkg/webmiddleware/hsts_headers_test.go | 62 ++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 pkg/webmiddleware/hsts_headers.go create mode 100644 pkg/webmiddleware/hsts_headers_test.go diff --git a/pkg/web/web.go b/pkg/web/web.go index 1d32d9c675..3bc0b0b756 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -196,6 +196,7 @@ func New(ctx context.Context, opts ...Option) (*Server, error) { mux.MiddlewareFunc(webmiddleware.Metadata("X-Forwarded-For", "User-Agent")), mux.MiddlewareFunc(webmiddleware.MaxBody(1024*1024*16)), mux.MiddlewareFunc(webmiddleware.SecurityHeaders()), + mux.MiddlewareFunc(webmiddleware.HSTSHeaders()), mux.MiddlewareFunc(webmiddleware.Log(logger, options.logIgnorePaths)), mux.MiddlewareFunc(webmiddleware.Cookies(hashKey, blockKey)), mux.MiddlewareFunc(webmiddleware.NoCache), diff --git a/pkg/webmiddleware/hsts_headers.go b/pkg/webmiddleware/hsts_headers.go new file mode 100644 index 0000000000..248e2a5b8b --- /dev/null +++ b/pkg/webmiddleware/hsts_headers.go @@ -0,0 +1,33 @@ +// Copyright © 2025 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package webmiddleware + +import ( + "net/http" +) + +// HSTSHeaders returns a middleware that adds HTTP Strict Transport Security (HSTS) headers. +// See https://datatracker.ietf.org/doc/html/rfc6797 and https://hstspreload.org/. +func HSTSHeaders() MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only add HSTS when the request is served over HTTPS. + if r.TLS != nil { + w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload") + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/pkg/webmiddleware/hsts_headers_test.go b/pkg/webmiddleware/hsts_headers_test.go new file mode 100644 index 0000000000..ac2a341815 --- /dev/null +++ b/pkg/webmiddleware/hsts_headers_test.go @@ -0,0 +1,62 @@ +// Copyright © 2025 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package webmiddleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/webmiddleware" +) + +func TestHSTSHeaders(t *testing.T) { + a := assertions.New(t) + + m := webmiddleware.HSTSHeaders() + a.So(m, assertions.ShouldNotBeNil) + + t.Run("HTTPS request should add HSTS headers", func(t *testing.T) { + a := assertions.New(t) + + req := httptest.NewRequest("GET", "https://example.com", nil) + rec := httptest.NewRecorder() + + handler := m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + handler.ServeHTTP(rec, req) + + a.So(rec.Header().Get("Strict-Transport-Security"), assertions.ShouldNotBeEmpty) + }) + + t.Run("HTTP request should NOT add HSTS headers", func(t *testing.T) { + a := assertions.New(t) + + req := httptest.NewRequest("GET", "http://example.com", nil) + rec := httptest.NewRecorder() + + handler := m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + handler.ServeHTTP(rec, req) + + a.So(rec.Header().Get("Strict-Transport-Security"), assertions.ShouldBeEmpty) + }) + +} From 7b5b5a1fa8d9f1442d78b325091a12436bde91c3 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:04:54 +0200 Subject: [PATCH 2/3] dev: Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a49e4b02..bc96b54141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ For details about compatibility between different releases, see the **Commitment ### Added +- Add HSTS response headers. + ### Changed ### Deprecated From 0d9c8afa3c792042ec0f0bfdb2862b9d402e1ffa Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Thu, 27 Nov 2025 11:37:36 +0200 Subject: [PATCH 3/3] dev: Linting issue --- pkg/webmiddleware/hsts_headers_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/webmiddleware/hsts_headers_test.go b/pkg/webmiddleware/hsts_headers_test.go index ac2a341815..25481e0d60 100644 --- a/pkg/webmiddleware/hsts_headers_test.go +++ b/pkg/webmiddleware/hsts_headers_test.go @@ -24,18 +24,21 @@ import ( ) func TestHSTSHeaders(t *testing.T) { + t.Parallel() + a := assertions.New(t) m := webmiddleware.HSTSHeaders() a.So(m, assertions.ShouldNotBeNil) t.Run("HTTPS request should add HSTS headers", func(t *testing.T) { + t.Parallel() a := assertions.New(t) req := httptest.NewRequest("GET", "https://example.com", nil) rec := httptest.NewRecorder() - handler := m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := m(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) @@ -45,18 +48,17 @@ func TestHSTSHeaders(t *testing.T) { }) t.Run("HTTP request should NOT add HSTS headers", func(t *testing.T) { + t.Parallel() a := assertions.New(t) req := httptest.NewRequest("GET", "http://example.com", nil) rec := httptest.NewRecorder() - handler := m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := m(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) - handler.ServeHTTP(rec, req) a.So(rec.Header().Get("Strict-Transport-Security"), assertions.ShouldBeEmpty) }) - }