From cd7fdd11d61b5bc80059e68a9b1278e7bdc58f5f Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Tue, 2 Dec 2025 11:46:50 -0800 Subject: [PATCH] Serve over TLS and switch to stdlib CSRF protection In particular this will allow CLI tools to request JIT access without breaking CSRF mitigations. --- go.mod | 2 -- go.sum | 6 ------ hallpass.go | 51 ++++++++++++++++++++++++++++++++----------------- index.tmpl.html | 1 - 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 329701a..a486829 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/tailscale/hallpass go 1.25.1 require ( - github.com/gorilla/csrf v1.7.3 github.com/tailscale/setec v0.0.0-20250916214228-71d9e0b5aae2 tailscale.com v1.88.3 tailscale.com/client/tailscale/v2 v2.2.0 @@ -42,7 +41,6 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify/v3 v3.0.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index 5b58ced..6bb7802 100644 --- a/go.sum +++ b/go.sum @@ -99,16 +99,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0= -github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk= diff --git a/hallpass.go b/hallpass.go index f636249..3f4bf11 100644 --- a/hallpass.go +++ b/hallpass.go @@ -13,7 +13,6 @@ import ( "bytes" "cmp" "context" - "crypto/rand" _ "embed" "encoding/json" "errors" @@ -29,7 +28,6 @@ import ( "strings" "time" - "github.com/gorilla/csrf" "github.com/tailscale/setec/client/setec" "tailscale.com/client/local" "tailscale.com/client/tailscale/apitype" @@ -44,6 +42,7 @@ var ( oauthSecret = flag.String("oauth-secret", keyPath("hallpass-key"), "name of setec secret containing Tailscale OAuth ClientSecret; if --secret-server is empty, ignored and reads from $HOME/keys/hallpass-key") webhookSecret = flag.String("webhook-secret", keyPath("hallpass-webhook"), "name of setec secret containing the Slack webhook URL; if --secret-server is empty, ignored and reads from $HOME/keys/hallpass-webhook") configDir = flag.String("tsnet-dir", "", "tsnet server directory; if empty, tsnet uses an automatic config directory based on the binary name") + tls = flag.Bool("tls", true, "serve over TLS using Tailscale Serve") ) func main() { @@ -115,21 +114,39 @@ func main() { js.webhookURL = setec.StaticSecret(readFile(*webhookSecret)) } - ln, err := ts.Listen("tcp", ":80") - if err != nil { - log.Fatal(err) + if *tls { + go func() { + lnHTTP, err := ts.Listen("tcp", ":80") + if err != nil { + log.Fatal(err) + } + defer lnHTTP.Close() + log.Printf("Serving at http://%s ...", js.fqdn) + log.Fatal(http.Serve(lnHTTP, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "https://"+js.fqdn, http.StatusPermanentRedirect) + }))) + }() + + lnHTTPS, err := ts.ListenTLS("tcp", ":443") + if err != nil { + log.Fatal(err) + } + defer lnHTTPS.Close() + csrf := http.NewCrossOriginProtection() + csrf.AddTrustedOrigin("https://" + js.fqdn) + log.Printf("Serving at https://%s ...", js.fqdn) + log.Fatal(http.Serve(lnHTTPS, csrf.Handler(js))) + } else { + lnHTTP, err := ts.Listen("tcp", ":80") + if err != nil { + log.Fatal(err) + } + defer lnHTTP.Close() + csrf := http.NewCrossOriginProtection() + csrf.AddTrustedOrigin("http://" + js.fqdn) + log.Printf("Serving at http://%s ...", js.fqdn) + log.Fatal(http.Serve(lnHTTP, csrf.Handler(js))) } - defer ln.Close() - - // CSRF protection - csrfSecret := make([]byte, 32) - rand.Read(csrfSecret) - protect := csrf.Protect(csrfSecret, - csrf.Secure(false), - csrf.TrustedOrigins([]string{"jit.corp.ts.net"})) - - log.Printf("Serving at http://%s ...", js.fqdn) - log.Fatal(http.Serve(ln, protect(js))) } func whitespaceTrimmingSecret(s setec.Secret) setec.Secret { @@ -159,7 +176,6 @@ type rootData struct { NodeName string AccessTypes []accessTypeConfig Durations []durationDropdown - CSRF template.HTML } // durationDropdown is a single option in the duration {{range .AccessTypes}}