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 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..25481e0d60 --- /dev/null +++ b/pkg/webmiddleware/hsts_headers_test.go @@ -0,0 +1,64 @@ +// 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) { + 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, _ *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) { + 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, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + handler.ServeHTTP(rec, req) + + a.So(rec.Header().Get("Strict-Transport-Security"), assertions.ShouldBeEmpty) + }) +}