From 740f7690ea8f5c078512babca0acc5c0a6778e7c Mon Sep 17 00:00:00 2001
From: Dmitry Savintsev <dsavints@gmail.com>
Date: Mon, 11 Sep 2023 18:33:14 +0200
Subject: [PATCH] Add command-line flag --http-ping-only

New optional command-line flag --http-ping-only allows
to enforce that the http server (which doesn't support mTLS)
allows only to access the /ping entrypoint. This would limit
the risk of the timestamp server being accessed without
mTLS in case of the strict mTLS requirement while still
allowing the mTLS-exempt /ping entrypoint to be called
for example by the heartbeat checkers (load balancers etc.).

Fixes #420.

Signed-off-by: Dmitry Savintsev <dsavints@gmail.com>
---
 cmd/timestamp-server/app/root.go              |  9 ++++----
 cmd/timestamp-server/app/serve.go             |  4 ++--
 .../restapi/configure_timestamp_server.go     | 11 +++++----
 pkg/internal/cmdparams/cmdparams.go           | 23 +++++++++++++++++++
 pkg/internal/cmdparams/doc.go                 | 19 +++++++++++++++
 pkg/ntpmonitor/config.go                      |  1 -
 pkg/server/restapi.go                         |  9 ++++++--
 pkg/tests/server.go                           |  2 +-
 8 files changed, 64 insertions(+), 14 deletions(-)
 create mode 100644 pkg/internal/cmdparams/cmdparams.go
 create mode 100644 pkg/internal/cmdparams/doc.go

diff --git a/cmd/timestamp-server/app/root.go b/cmd/timestamp-server/app/root.go
index 333d0b533..971d5508e 100644
--- a/cmd/timestamp-server/app/root.go
+++ b/cmd/timestamp-server/app/root.go
@@ -26,9 +26,10 @@ import (
 )
 
 var (
-	cfgFile     string
-	logType     string
-	enablePprof bool
+	cfgFile      string
+	logType      string
+	enablePprof  bool
+	httpPingOnly bool
 )
 
 // rootCmd represents the base command when called without any subcommands
@@ -56,7 +57,7 @@ func init() {
 	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.timestamp-server.yaml)")
 	rootCmd.PersistentFlags().StringVar(&logType, "log-type", "dev", "logger type to use (dev/prod)")
 	rootCmd.PersistentFlags().BoolVar(&enablePprof, "enable-pprof", false, "enable pprof for profiling on port 6060")
-
+	rootCmd.PersistentFlags().BoolVar(&httpPingOnly, "http-ping-only", false, "serve only /ping in the http server")
 	rootCmd.PersistentFlags().String("timestamp-signer", "memory", "Timestamping authority signer. Valid options include: [kms, tink, memory, file]. Memory and file-based signers should only be used for testing")
 	// KMS flags
 	rootCmd.PersistentFlags().String("kms-key-resource", "", "KMS key for signing timestamp responses. Valid options include: [gcpkms://resource, azurekms://resource, hashivault://resource, awskms://resource]")
diff --git a/cmd/timestamp-server/app/serve.go b/cmd/timestamp-server/app/serve.go
index 9d432a267..a7807b5e0 100644
--- a/cmd/timestamp-server/app/serve.go
+++ b/cmd/timestamp-server/app/serve.go
@@ -100,8 +100,8 @@ var serveCmd = &cobra.Command{
 		host := viper.GetString("host")
 		port := int(viper.GetUint("port"))
 		scheme := viper.GetStringSlice("scheme")
-
-		server := server.NewRestAPIServer(host, port, scheme, readTimeout, writeTimeout)
+		httpPingOnly := viper.GetBool("http-ping-only")
+		server := server.NewRestAPIServer(host, port, scheme, httpPingOnly, readTimeout, writeTimeout)
 		defer func() {
 			if err := server.Shutdown(); err != nil {
 				log.Logger.Error(err)
diff --git a/pkg/generated/restapi/configure_timestamp_server.go b/pkg/generated/restapi/configure_timestamp_server.go
index f99a659ae..da6474520 100644
--- a/pkg/generated/restapi/configure_timestamp_server.go
+++ b/pkg/generated/restapi/configure_timestamp_server.go
@@ -34,6 +34,7 @@ import (
 	pkgapi "github.com/sigstore/timestamp-authority/pkg/api"
 	"github.com/sigstore/timestamp-authority/pkg/generated/restapi/operations"
 	"github.com/sigstore/timestamp-authority/pkg/generated/restapi/operations/timestamp"
+	"github.com/sigstore/timestamp-authority/pkg/internal/cmdparams"
 	"github.com/sigstore/timestamp-authority/pkg/log"
 )
 
@@ -102,15 +103,15 @@ func (l *logAdapter) Print(v ...interface{}) {
 	log.Logger.Info(v...)
 }
 
-// httpPingOnly custom middleware prohibits all entrypoing except
+// httpPingOnly custom middleware prohibits all entrypoints except
 // "/ping" on the http (non-HTTPS) server.
 func httpPingOnly(endpoint string) func(http.Handler) http.Handler {
 	f := func(h http.Handler) http.Handler {
 		fn := func(w http.ResponseWriter, r *http.Request) {
 			if r.URL.Scheme != "https" && !strings.EqualFold(r.URL.Path, endpoint) {
 				w.Header().Set("Content-Type", "text/plain")
-				w.WriteHeader(http.StatusBadRequest)
-				w.Write([]byte("http server supports only the /ping entrypoint"))
+				w.WriteHeader(http.StatusForbidden)
+				w.Write([]byte("http server supports only the /ping entrypoint")) //nolint:errcheck
 				return
 			}
 			h.ServeHTTP(w, r)
@@ -128,7 +129,9 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler {
 	returnHandler := middleware.Logger(handler)
 	returnHandler = middleware.Recoverer(returnHandler)
 	returnHandler = middleware.Heartbeat("/ping")(returnHandler)
-	returnHandler = httpPingOnly("/ping")(returnHandler)
+	if cmdparams.IsHTTPPingOnly {
+		returnHandler = httpPingOnly("/ping")(returnHandler)
+	}
 
 	handleCORS := cors.Default().Handler
 	returnHandler = handleCORS(returnHandler)
diff --git a/pkg/internal/cmdparams/cmdparams.go b/pkg/internal/cmdparams/cmdparams.go
new file mode 100644
index 000000000..e19b5d394
--- /dev/null
+++ b/pkg/internal/cmdparams/cmdparams.go
@@ -0,0 +1,23 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// 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 cmdparams
+
+// IsHTTPPingOnly is set off the command-line flag to enforce limiting
+// the non-mTLS http server to only serving the /ping entrypoint.
+// It should be set only once when processing command-line flags
+// and then used only in pkg/generated/restapi/configure_timestamp_server.go
+// and as read-only.
+var IsHTTPPingOnly bool
diff --git a/pkg/internal/cmdparams/doc.go b/pkg/internal/cmdparams/doc.go
new file mode 100644
index 000000000..b068e810f
--- /dev/null
+++ b/pkg/internal/cmdparams/doc.go
@@ -0,0 +1,19 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// 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 cmdparams contains variables to propagate from command-line
+// flags to their handling in
+// pkg/generated/restapi/configure_timestamp_server.go.
+package cmdparams
diff --git a/pkg/ntpmonitor/config.go b/pkg/ntpmonitor/config.go
index 67e8b9265..9794b21d2 100644
--- a/pkg/ntpmonitor/config.go
+++ b/pkg/ntpmonitor/config.go
@@ -1,4 +1,3 @@
-//
 // Copyright 2022 The Sigstore Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/pkg/server/restapi.go b/pkg/server/restapi.go
index 0e3131737..e7206f82a 100644
--- a/pkg/server/restapi.go
+++ b/pkg/server/restapi.go
@@ -21,10 +21,15 @@ import (
 	"github.com/sigstore/timestamp-authority/pkg/api"
 	"github.com/sigstore/timestamp-authority/pkg/generated/restapi"
 	"github.com/sigstore/timestamp-authority/pkg/generated/restapi/operations"
+	"github.com/sigstore/timestamp-authority/pkg/internal/cmdparams"
 )
 
 // NewRestAPIServer creates a server for serving the rest API TSA service
-func NewRestAPIServer(host string, port int, scheme []string, readTimeout, writeTimeout time.Duration) *restapi.Server {
+func NewRestAPIServer(host string,
+	port int,
+	scheme []string,
+	httpReadOnly bool,
+	readTimeout, writeTimeout time.Duration) *restapi.Server {
 	doc, _ := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON)
 	server := restapi.NewServer(operations.NewTimestampServerAPI(doc))
 
@@ -33,7 +38,7 @@ func NewRestAPIServer(host string, port int, scheme []string, readTimeout, write
 	server.EnabledListeners = scheme
 	server.ReadTimeout = readTimeout
 	server.WriteTimeout = writeTimeout
-
+	cmdparams.IsHTTPPingOnly = httpReadOnly
 	api.ConfigureAPI()
 	server.ConfigureAPI()
 
diff --git a/pkg/tests/server.go b/pkg/tests/server.go
index d470fad95..9e92302f5 100644
--- a/pkg/tests/server.go
+++ b/pkg/tests/server.go
@@ -29,7 +29,7 @@ import (
 func createServer(t *testing.T) string {
 	viper.Set("timestamp-signer", "memory")
 	// unused port
-	apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, 10*time.Second, 10*time.Second)
+	apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second)
 	server := httptest.NewServer(apiServer.GetHandler())
 	t.Cleanup(server.Close)