app: add security headers middleware
X-Frame-Options: DENY and frame-ancestors 'none' stop clickjacking of OIDC, register-confirm, and debug HTML pages. nosniff and no-referrer are cheap defence-in-depth for the same surfaces. Updates #3157
This commit is contained in:
parent
5a7cafdf85
commit
0567cb6da3
@ -488,6 +488,20 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
|
|||||||
return os.Remove(h.cfg.UnixSocket)
|
return os.Remove(h.cfg.UnixSocket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// securityHeaders sets baseline response headers on every HTTP response:
|
||||||
|
// deny framing (clickjacking), forbid MIME-type sniffing, drop the Referer
|
||||||
|
// header on outbound navigation. Cheap defense-in-depth for HTML surfaces.
|
||||||
|
func securityHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("X-Frame-Options", "DENY")
|
||||||
|
h.Set("Content-Security-Policy", "frame-ancestors 'none'")
|
||||||
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
h.Set("Referrer-Policy", "no-referrer")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(metrics.Collector(metrics.CollectorOpts{
|
r.Use(metrics.Collector(metrics.CollectorOpts{
|
||||||
@ -501,6 +515,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
|||||||
r.Use(middleware.RealIP)
|
r.Use(middleware.RealIP)
|
||||||
r.Use(middleware.RequestLogger(&zerologRequestLogger{}))
|
r.Use(middleware.RequestLogger(&zerologRequestLogger{}))
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(securityHeaders)
|
||||||
|
|
||||||
r.Post(ts2021UpgradePath, h.NoiseUpgradeHandler)
|
r.Post(ts2021UpgradePath, h.NoiseUpgradeHandler)
|
||||||
|
|
||||||
|
|||||||
26
hscontrol/app_test.go
Normal file
26
hscontrol/app_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package hscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecurityHeaders(t *testing.T) {
|
||||||
|
handler := securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
h := rec.Result().Header
|
||||||
|
assert.Equal(t, "DENY", h.Get("X-Frame-Options"))
|
||||||
|
assert.Equal(t, "frame-ancestors 'none'", h.Get("Content-Security-Policy"))
|
||||||
|
assert.Equal(t, "nosniff", h.Get("X-Content-Type-Options"))
|
||||||
|
assert.Equal(t, "no-referrer", h.Get("Referrer-Policy"))
|
||||||
|
}
|
||||||
@ -379,7 +379,7 @@ func (h *Headscale) debugHTTPServer() *http.Server {
|
|||||||
|
|
||||||
debugHTTPServer := &http.Server{
|
debugHTTPServer := &http.Server{
|
||||||
Addr: h.cfg.MetricsAddr,
|
Addr: h.cfg.MetricsAddr,
|
||||||
Handler: debugMux,
|
Handler: securityHeaders(debugMux),
|
||||||
ReadTimeout: types.HTTPTimeout,
|
ReadTimeout: types.HTTPTimeout,
|
||||||
WriteTimeout: 0,
|
WriteTimeout: 0,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user