From a8296e4ccd355f5fac805828ad8e474381a6c5a2 Mon Sep 17 00:00:00 2001 From: Chris Hager Date: Wed, 27 Nov 2024 09:10:57 +0100 Subject: [PATCH] TLS support (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 Summary Support for TLS encrypted requests. Can create cert+key files, or use existing ones. New configuration options: ```toml [general] # TLS configuration tls_enabled = true tls_create_if_missing = true tls_cert_path = "cert.pem" tls_key_path = "key.pem" tls_hosts = ["localhost", ""] ``` ## ⛱ Motivation and Context Encryption of requests --- ## ✅ I have run these commands * [x] `make lint` * [x] `make test` * [x] `go mod tidy` --- cmd/system-api/main.go | 2 +- go.mod | 1 + go.sum | 2 ++ systemapi-config.toml | 7 +++++ systemapi/config.go | 22 +++++++++------ systemapi/config_test.go | 7 +++++ systemapi/server.go | 22 +++++++++++++-- systemapi/tls.go | 58 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 systemapi/tls.go diff --git a/cmd/system-api/main.go b/cmd/system-api/main.go index db8299f..13a18f6 100644 --- a/cmd/system-api/main.go +++ b/cmd/system-api/main.go @@ -96,7 +96,7 @@ func runCli(cCtx *cli.Context) (err error) { log.Error("Error creating server", "err", err) return err } - go server.Start() + go server.Start() //nolint:errcheck // Wait for signal, then graceful shutdown exit := make(chan os.Signal, 1) diff --git a/go.mod b/go.mod index 3ea901f..783717d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.1 require ( github.com/ethereum/go-ethereum v1.14.9 + github.com/flashbots/go-utils v0.8.2 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/httplog/v2 v2.1.1 github.com/pelletier/go-toml/v2 v2.2.3 diff --git a/go.sum b/go.sum index 3285a41..607d7a2 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etly github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/ethereum/go-ethereum v1.14.9 h1:J7iwXDrtUyE9FUjUYbd4c9tyzwMh6dTJsKzo9i6SrwA= github.com/ethereum/go-ethereum v1.14.9/go.mod h1:QeW+MtTpRdBEm2pUFoonByee8zfHv7kGp0wK0odvU1I= +github.com/flashbots/go-utils v0.8.2 h1:8JUKd9Cv1CTcp63V03ya+47nflTwBEqZq357iwW4fxQ= +github.com/flashbots/go-utils v0.8.2/go.mod h1:Lo/nrlC+q8ANgT3e6MKALIJCU+V9qTSgNtoLk/q1uIw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk= diff --git a/systemapi-config.toml b/systemapi-config.toml index ad2fe65..03c222e 100644 --- a/systemapi-config.toml +++ b/systemapi-config.toml @@ -16,6 +16,13 @@ basic_auth_secret_salt = "D;%yL9TS:5PalS/d" # use a random string for the s # http_read_timeout_ms = 2500 # http_write_timeout_ms = 2500 +# TLS configuration +tls_enabled = true +tls_create_if_missing = true +tls_cert_hosts = ["localhost", ""] +tls_cert_path = "cert.pem" +tls_key_path = "key.pem" + [actions] echo_test = "echo test" # reboot = "reboot" diff --git a/systemapi/config.go b/systemapi/config.go index 7bbb0b9..1bd3321 100644 --- a/systemapi/config.go +++ b/systemapi/config.go @@ -10,18 +10,24 @@ import ( var DefaultLogMaxEntries = common.GetEnvInt("MAX_EVENTS", 1000) type systemAPIConfigGeneral struct { - ListenAddr string `toml:"listen_addr"` - PipeFile string `toml:"pipe_file"` - LogJSON bool `toml:"log_json"` - LogDebug bool `toml:"log_debug"` + ListenAddr string `toml:"listen_addr"` // Address (host and port) for server to listen on + PipeFile string `toml:"pipe_file"` // Path for the named pipe file + LogJSON bool `toml:"log_json"` // Enables JSON logging + LogDebug bool `toml:"log_debug"` // Enables debug logging EnablePprof bool `toml:"pprof"` // Enables pprof endpoints LogMaxEntries int `toml:"log_max_entries"` // Maximum number of log entries - BasicAuthSecretPath string `toml:"basic_auth_secret_path"` - BasicAuthSecretSalt string `toml:"basic_auth_secret_salt"` + BasicAuthSecretPath string `toml:"basic_auth_secret_path"` // Path to the file containing the basic auth secret hash + BasicAuthSecretSalt string `toml:"basic_auth_secret_salt"` // Path to the file containing the basic auth secret salt - HTTPReadTimeoutMillis int `toml:"http_read_timeout_ms"` - HTTPWriteTimeoutMillis int `toml:"http_write_timeout_ms"` + HTTPReadTimeoutMillis int `toml:"http_read_timeout_ms"` // A zero or negative value means there will be no timeout. + HTTPWriteTimeoutMillis int `toml:"http_write_timeout_ms"` // A zero or negative value means there will be no timeout. + + TLSEnabled bool `toml:"tls_enabled"` // Enable TLS + TLSCreateIfMissing bool `toml:"tls_create_if_missing"` // Create TLS cert and key files if they do not exist + TLSCertHosts []string `toml:"tls_cert_hosts"` // Hosts for the TLS cert + TLSCertPath string `toml:"tls_cert_path"` // Path to the TLS cert file + TLSKeyPath string `toml:"tls_key_path"` // Path to the TLS key file } type SystemAPIConfig struct { diff --git a/systemapi/config_test.go b/systemapi/config_test.go index eae44df..002b033 100644 --- a/systemapi/config_test.go +++ b/systemapi/config_test.go @@ -13,6 +13,13 @@ func TestLoadConfig(t *testing.T) { require.NotNil(t, cfg) require.NotEmpty(t, cfg.Actions) require.Equal(t, "echo test", cfg.Actions["echo_test"]) + + // check TLS config + require.True(t, cfg.General.TLSEnabled) + require.True(t, cfg.General.TLSCreateIfMissing) + require.NotEmpty(t, cfg.General.TLSCertHosts) + require.NotEmpty(t, cfg.General.TLSCertPath) + require.NotEmpty(t, cfg.General.TLSKeyPath) } func TestEmptyConfig(t *testing.T) { diff --git a/systemapi/server.go b/systemapi/server.go index c4d5ccf..2e4d11c 100644 --- a/systemapi/server.go +++ b/systemapi/server.go @@ -77,6 +77,15 @@ func NewServer(log *httplog.Logger, cfg *SystemAPIConfig) (server *Server, err e go server.readPipeInBackground() } + // Load or create TLS certificate + if cfg.General.TLSEnabled { + err = server.createTLSCertIfNotExists() + if err != nil { + server.log.Error("Failed to create TLS certificate", "err", err) + return nil, err + } + } + // Create the HTTP server server.srv = &http.Server{ Addr: cfg.General.ListenAddr, @@ -180,11 +189,20 @@ func (s *Server) readPipeInBackground() { } } -func (s *Server) Start() { +func (s *Server) Start() (err error) { s.log.Info("Starting HTTP server", "listenAddress", s.cfg.General.ListenAddr) - if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + + if s.cfg.General.TLSEnabled { + s.log.Info("TLS enabled", "cert", s.cfg.General.TLSCertPath, "key", s.cfg.General.TLSKeyPath) + err = s.srv.ListenAndServeTLS(s.cfg.General.TLSCertPath, s.cfg.General.TLSKeyPath) + } else { + err = s.srv.ListenAndServe() + } + if err != nil && !errors.Is(err, http.ErrServerClosed) { s.log.Error("HTTP server failed", "err", err) + return err } + return nil } func (s *Server) Shutdown(ctx context.Context) error { diff --git a/systemapi/tls.go b/systemapi/tls.go new file mode 100644 index 0000000..05e5023 --- /dev/null +++ b/systemapi/tls.go @@ -0,0 +1,58 @@ +package systemapi + +import ( + "errors" + "os" + "time" + + "github.com/flashbots/go-utils/tls" +) + +// createTLSCertIfNotExists created a cert and key file if it doesn't exist yet +func (s *Server) createTLSCertIfNotExists() error { + log := s.log.With("cert", s.cfg.General.TLSCertPath, "key", s.cfg.General.TLSKeyPath) + _, err1 := os.Stat(s.cfg.General.TLSCertPath) + if err1 != nil && !os.IsNotExist(err1) { + return err1 + } + + _, err2 := os.Stat(s.cfg.General.TLSKeyPath) + if err2 != nil && !os.IsNotExist(err2) { + return err2 + } + + certFileExists := err1 == nil + keyFileExists := err2 == nil + if certFileExists && keyFileExists { + // Files exist, use them + log.Info("TLS cert and key found, using them") + return nil + } else if certFileExists || keyFileExists { + // Only one of the files exist, should not happen + return errors.New("both TLS cert and key files are required, but only one exists") + } + + // Files do not exist, should create them + if !s.cfg.General.TLSCreateIfMissing { + return errors.New("TLS cert and key files do not exist, but config is set to not create them") + } + + // Create them + cert, key, err := tls.GenerateTLS(time.Hour*24*365, s.cfg.General.TLSCertHosts) + if err != nil { + return err + } + + err = os.WriteFile(s.cfg.General.TLSCertPath, cert, 0o600) + if err != nil { + return err + } + + err = os.WriteFile(s.cfg.General.TLSKeyPath, key, 0o600) + if err != nil { + return err + } + + log.With("hosts", s.cfg.General.TLSCertHosts).Info("TLS cert and key files created") + return nil +}