From d378b30156e8b69fd932fde9e076a62a90ffc805 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Sat, 7 Dec 2024 16:39:42 -0500 Subject: [PATCH 01/14] Add HTTP health server (closes #115) Signed-off-by: Keegan Witt --- README.md | 40 ++++++++++++----------- cmd/spiffe-helper/config/config.go | 7 ++++ cmd/spiffe-helper/main.go | 51 +++++++++++++++++++++++++++++- pkg/sidecar/sidecar.go | 46 +++++++++++++++++++++++---- pkg/sidecar/sidecar_test.go | 5 +-- 5 files changed, 120 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9e10506a..2fca129f 100644 --- a/README.md +++ b/README.md @@ -18,25 +18,27 @@ The flag `-exitWhenReady` is also supported. ## Configuration The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted file that defines the following configurations: - | Configuration | Description | Example Value | - |-------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `agent_address` | Socket address of SPIRE Agent. | `"/tmp/agent.sock"` | - | `cmd` | The path to the process to launch. | `"ghostunnel"` | - | `cmd_args` | The arguments of the process to launch. | `"server --listen localhost:8002 --target localhost:8001--keystore certs/svid_key.pem --cacert certs/svid_bundle.pem --allow-uri-san spiffe://example.org/Database"` | - | `cert_dir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` | - | `daemon_mode` | Toggle running as a daemon, keeping X.509 and JWT up to date; or just fetch X.509 and JWT and exit 0 | `true` | - | `add_intermediates_to_bundle` | Add intermediate certificates into Bundle file instead of SVID file. | `true` | - | `renew_signal` | The signal that the process to be launched expects to reload the certificates. It is not supported on Windows. | `"SIGUSR1"` | - | `svid_file_name` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` | - | `svid_key_file_name` | File name to be used to store the X.509 SVID private key and public certificate in PEM format. | `"svid_key.pem"` | - | `svid_bundle_file_name` | File name to be used to store the X.509 SVID Bundle in PEM format. | `"svid_bundle.pem"` | - | `jwt_svids` | An array with the audience, optional extra audiences array, and file name to store the JWT SVIDs. File is Base64-encoded string). | `[{jwt_audience="your-audience", jwt_extra_audiences=["your-extra-audience-1", "your-extra-audience-2"], jwt_svid_file_name="jwt_svid.token"}]` | - | `jwt_bundle_file_name` | File name to be used to store JWT Bundle in JSON format. | `"jwt_bundle.json"` | - | `include_federated_domains` | Include trust domains from federated servers in the CA bundle. | `true` | - | `cert_file_mode` | The octal file mode to use when saving the X.509 public certificate file. | `0644` | - | `key_file_mode` | The octal file mode to use when saving the X.509 private key file. | `0600` | - | `jwt_bundle_file_mode` | The octal file mode to use when saving a JWT Bundle file. | `0600` | - | `jwt_svid_file_mode` | The octal file mode to use when saving a JWT SVID file. | `0600` | + | Configuration | Description | Example Value | + |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `agent_address` | Socket address of SPIRE Agent. | `"/tmp/agent.sock"` | + | `cmd` | The path to the process to launch. | `"ghostunnel"` | + | `cmd_args` | The arguments of the process to launch. | `"server --listen localhost:8002 --target localhost:8001--keystore certs/svid_key.pem --cacert certs/svid_bundle.pem --allow-uri-san spiffe://example.org/Database"` | + | `cert_dir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` | + | `daemon_mode` | Toggle running as a daemon, keeping X.509 and JWT up to date; or just fetch X.509 and JWT and exit 0 | `true` | + | `add_intermediates_to_bundle` | Add intermediate certificates into Bundle file instead of SVID file. | `true` | + | `renew_signal` | The signal that the process to be launched expects to reload the certificates. It is not supported on Windows. | `"SIGUSR1"` | + | `svid_file_name` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` | + | `svid_key_file_name` | File name to be used to store the X.509 SVID private key and public certificate in PEM format. | `"svid_key.pem"` | + | `svid_bundle_file_name` | File name to be used to store the X.509 SVID Bundle in PEM format. | `"svid_bundle.pem"` | + | `jwt_svids` | An array with the audience, optional extra audiences array, and file name to store the JWT SVIDs. File is Base64-encoded string). | `[{jwt_audience="your-audience", jwt_extra_audiences=["your-extra-audience-1", "your-extra-audience-2"], jwt_svid_file_name="jwt_svid.token"}]` | + | `jwt_bundle_file_name` | File name to be used to store JWT Bundle in JSON format. | `"jwt_bundle.json"` | + | `include_federated_domains` | Include trust domains from federated servers in the CA bundle. | `true` | + | `cert_file_mode` | The octal file mode to use when saving the X.509 public certificate file. | `0644` | + | `key_file_mode` | The octal file mode to use when saving the X.509 private key file. | `0600` | + | `jwt_bundle_file_mode` | The octal file mode to use when saving a JWT Bundle file. | `0600` | + | `jwt_svid_file_mode` | The octal file mode to use when saving a JWT SVID file. | `0600` | + | enable_health_check | Whether to start an HTTP server at `/healthz` with the daemon health. Doesn't apply for non-daemon mode. | `false` | + | health_check_port | The port to run the HTTP health server. | `8081` | ### Configuration example ``` diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index 784e455e..47b949b4 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -37,6 +37,8 @@ type Config struct { IncludeFederatedDomains bool `hcl:"include_federated_domains"` RenewSignal string `hcl:"renew_signal"` DaemonMode *bool `hcl:"daemon_mode"` + EnableHealthCheck *bool `hcl:"enable_health_check"` + HealthCheckPort int `hcl:"health_check_port"` // x509 configuration SVIDFileName string `hcl:"svid_file_name"` @@ -158,6 +160,11 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { c.JWTSVIDFileMode = defaultJWTSVIDFileMode } + if c.EnableHealthCheck == nil { + defaultEnableHealthCheck := false + c.EnableHealthCheck = &defaultEnableHealthCheck + } + return nil } diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index 295b67b8..0291b171 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -2,11 +2,15 @@ package main import ( "context" + "encoding/json" "flag" "fmt" + "net/http" "os" "os/signal" + "strconv" "syscall" + "time" "github.com/sirupsen/logrus" "github.com/spiffe/spiffe-helper/cmd/spiffe-helper/config" @@ -28,10 +32,17 @@ func main() { os.Exit(1) } + if err := startHealthServer(*configFile, *daemonModeFlag, log); err != nil { + log.WithError(err).Errorf("Error starting spiffe-helper health check server") + os.Exit(1) + } + log.Infof("Exiting") os.Exit(0) } +var spiffeSidecar *sidecar.Sidecar + func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -48,7 +59,7 @@ func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger } sidecarConfig := config.NewSidecarConfig(hclConfig, log) - spiffeSidecar := sidecar.New(sidecarConfig) + spiffeSidecar = sidecar.New(sidecarConfig) if !*hclConfig.DaemonMode { log.Info("Daemon mode disabled") @@ -58,3 +69,41 @@ func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger log.Info("Launching daemon") return spiffeSidecar.RunDaemon(ctx) } + +func startHealthServer(configFile string, daemonModeFlag bool, log logrus.FieldLogger) error { + hclConfig, err := config.ParseConfig(configFile) + if err != nil { + return fmt.Errorf("failed to parse %q: %w", configFile, err) + } + hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName) + if err := hclConfig.ValidateConfig(log); err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + + if *hclConfig.DaemonMode && *hclConfig.EnableHealthCheck { + http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + healthy := spiffeSidecar.CheckHealth() + if healthy { + _, err := w.Write([]byte(http.StatusText(http.StatusOK))) + log.Error(err) + if err != nil { + return + } + } else { + statusText := http.StatusText(http.StatusServiceUnavailable) + b, err := json.Marshal(spiffeSidecar.GetFileWritesSuccess()) + if err != nil { + statusText = string(b) + } + http.Error(w, statusText, http.StatusServiceUnavailable) + } + }) + server := &http.Server{ + Addr: ":" + strconv.Itoa(hclConfig.HealthCheckPort), + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + } + log.Fatal(server.ListenAndServe()) + } + return nil +} diff --git a/pkg/sidecar/sidecar.go b/pkg/sidecar/sidecar.go index 891b79fb..1f8fa5e0 100644 --- a/pkg/sidecar/sidecar.go +++ b/pkg/sidecar/sidecar.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "path" "strconv" "strings" "sync" @@ -24,20 +25,23 @@ import ( // Sidecar is the component that consumes the Workload API and renews certs // implements the interface Sidecar type Sidecar struct { - config *Config - client *workloadapi.Client - jwtSource *workloadapi.JWTSource - processRunning int32 - process *os.Process - certReadyChan chan struct{} + config *Config + client *workloadapi.Client + jwtSource *workloadapi.JWTSource + processRunning int32 + process *os.Process + certReadyChan chan struct{} + fileWritesSuccess map[string]bool } // New creates a new SPIFFE sidecar func New(config *Config) *Sidecar { - return &Sidecar{ + sidecar := &Sidecar{ config: config, certReadyChan: make(chan struct{}, 1), } + sidecar.fileWritesSuccess = make(map[string]bool) + return sidecar } // RunDaemon starts the main loop @@ -167,10 +171,19 @@ func (s *Sidecar) setupClients(ctx context.Context) error { // updateCertificates Updates the certificates stored in disk and signal the Process to restart func (s *Sidecar) updateCertificates(svidResponse *workloadapi.X509Context) { s.config.Log.Debug("Updating X.509 certificates") + svidFile := path.Join(s.config.CertDir, s.config.SVIDFileName) + svidKeyFile := path.Join(s.config.CertDir, s.config.SVIDKeyFileName) + svidBundleFile := path.Join(s.config.CertDir, s.config.SVIDBundleFileName) if err := disk.WriteX509Context(svidResponse, s.config.AddIntermediatesToBundle, s.config.IncludeFederatedDomains, s.config.CertDir, s.config.SVIDFileName, s.config.SVIDKeyFileName, s.config.SVIDBundleFileName, s.config.CertFileMode, s.config.KeyFileMode); err != nil { s.config.Log.WithError(err).Error("Unable to dump bundle") + s.fileWritesSuccess[svidFile] = false + s.fileWritesSuccess[svidKeyFile] = false + s.fileWritesSuccess[svidBundleFile] = false return } + s.fileWritesSuccess[svidFile] = true + s.fileWritesSuccess[svidKeyFile] = true + s.fileWritesSuccess[svidBundleFile] = true s.config.Log.Info("X.509 certificates updated") if s.config.Cmd != "" { @@ -300,10 +313,13 @@ func (s *Sidecar) performJWTSVIDUpdate(ctx context.Context, jwtAudience string, return nil, err } + jwtSVIDPath := path.Join(s.config.CertDir, jwtSVIDFilename) if err = disk.WriteJWTSVID(jwtSVID, s.config.CertDir, jwtSVIDFilename, s.config.JWTSVIDFileMode); err != nil { s.config.Log.Errorf("Unable to update JWT SVID: %v", err) + s.fileWritesSuccess[jwtSVIDPath] = false return nil, err } + s.fileWritesSuccess[jwtSVIDPath] = true s.config.Log.Info("JWT SVID updated") return jwtSVID, nil @@ -397,10 +413,13 @@ type JWTBundlesWatcher struct { // OnJWTBundlesUpdate is run every time a bundle is updated func (w JWTBundlesWatcher) OnJWTBundlesUpdate(jwkSet *jwtbundle.Set) { w.sidecar.config.Log.Debug("Updating JWT bundle") + jwtBundleFilePath := path.Join(w.sidecar.config.CertDir, w.sidecar.config.JWTBundleFilename) if err := disk.WriteJWTBundleSet(jwkSet, w.sidecar.config.CertDir, w.sidecar.config.JWTBundleFilename, w.sidecar.config.JWTBundleFileMode); err != nil { w.sidecar.config.Log.Errorf("Error writing JWT Bundle to disk: %v", err) + w.sidecar.fileWritesSuccess[jwtBundleFilePath] = false return } + w.sidecar.fileWritesSuccess[jwtBundleFilePath] = true w.sidecar.config.Log.Info("JWT bundle updated") } @@ -411,3 +430,16 @@ func (w JWTBundlesWatcher) OnJWTBundlesWatchError(err error) { w.sidecar.config.Log.Errorf("Error while watching JWT bundles: %v", err) } } + +func (s *Sidecar) CheckHealth() bool { + for _, success := range s.fileWritesSuccess { + if !success { + return false + } + } + return true +} + +func (s *Sidecar) GetFileWritesSuccess() map[string]bool { + return s.fileWritesSuccess +} diff --git a/pkg/sidecar/sidecar_test.go b/pkg/sidecar/sidecar_test.go index c462402b..7bee280f 100644 --- a/pkg/sidecar/sidecar_test.go +++ b/pkg/sidecar/sidecar_test.go @@ -91,8 +91,9 @@ func TestSidecar_RunDaemon(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) sidecar := Sidecar{ - config: config, - certReadyChan: make(chan struct{}, 1), + config: config, + certReadyChan: make(chan struct{}, 1), + fileWritesSuccess: make(map[string]bool), } defer close(sidecar.certReadyChan) From 4a0e3b6dc2cae4c0c4e2ca76d2bc8d57d5740925 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Wed, 11 Dec 2024 20:56:41 -0500 Subject: [PATCH 02/14] PR feedback Signed-off-by: Keegan Witt --- README.md | 42 ++++++++++----------- cmd/spiffe-helper/config/config.go | 42 ++++++++++++--------- cmd/spiffe-helper/main.go | 60 ++++-------------------------- pkg/health/health.go | 50 +++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 91 deletions(-) create mode 100644 pkg/health/health.go diff --git a/README.md b/README.md index 2fca129f..bda86d3c 100644 --- a/README.md +++ b/README.md @@ -18,27 +18,27 @@ The flag `-exitWhenReady` is also supported. ## Configuration The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted file that defines the following configurations: - | Configuration | Description | Example Value | - |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `agent_address` | Socket address of SPIRE Agent. | `"/tmp/agent.sock"` | - | `cmd` | The path to the process to launch. | `"ghostunnel"` | - | `cmd_args` | The arguments of the process to launch. | `"server --listen localhost:8002 --target localhost:8001--keystore certs/svid_key.pem --cacert certs/svid_bundle.pem --allow-uri-san spiffe://example.org/Database"` | - | `cert_dir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` | - | `daemon_mode` | Toggle running as a daemon, keeping X.509 and JWT up to date; or just fetch X.509 and JWT and exit 0 | `true` | - | `add_intermediates_to_bundle` | Add intermediate certificates into Bundle file instead of SVID file. | `true` | - | `renew_signal` | The signal that the process to be launched expects to reload the certificates. It is not supported on Windows. | `"SIGUSR1"` | - | `svid_file_name` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` | - | `svid_key_file_name` | File name to be used to store the X.509 SVID private key and public certificate in PEM format. | `"svid_key.pem"` | - | `svid_bundle_file_name` | File name to be used to store the X.509 SVID Bundle in PEM format. | `"svid_bundle.pem"` | - | `jwt_svids` | An array with the audience, optional extra audiences array, and file name to store the JWT SVIDs. File is Base64-encoded string). | `[{jwt_audience="your-audience", jwt_extra_audiences=["your-extra-audience-1", "your-extra-audience-2"], jwt_svid_file_name="jwt_svid.token"}]` | - | `jwt_bundle_file_name` | File name to be used to store JWT Bundle in JSON format. | `"jwt_bundle.json"` | - | `include_federated_domains` | Include trust domains from federated servers in the CA bundle. | `true` | - | `cert_file_mode` | The octal file mode to use when saving the X.509 public certificate file. | `0644` | - | `key_file_mode` | The octal file mode to use when saving the X.509 private key file. | `0600` | - | `jwt_bundle_file_mode` | The octal file mode to use when saving a JWT Bundle file. | `0600` | - | `jwt_svid_file_mode` | The octal file mode to use when saving a JWT SVID file. | `0600` | - | enable_health_check | Whether to start an HTTP server at `/healthz` with the daemon health. Doesn't apply for non-daemon mode. | `false` | - | health_check_port | The port to run the HTTP health server. | `8081` | + | Configuration | Description | Example Value | + |-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `agent_address` | Socket address of SPIRE Agent. | `"/tmp/agent.sock"` | + | `cmd` | The path to the process to launch. | `"ghostunnel"` | + | `cmd_args` | The arguments of the process to launch. | `"server --listen localhost:8002 --target localhost:8001--keystore certs/svid_key.pem --cacert certs/svid_bundle.pem --allow-uri-san spiffe://example.org/Database"` | + | `cert_dir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` | + | `daemon_mode` | Toggle running as a daemon, keeping X.509 and JWT up to date; or just fetch X.509 and JWT and exit 0 | `true` | + | `add_intermediates_to_bundle` | Add intermediate certificates into Bundle file instead of SVID file. | `true` | + | `renew_signal` | The signal that the process to be launched expects to reload the certificates. It is not supported on Windows. | `"SIGUSR1"` | + | `svid_file_name` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` | + | `svid_key_file_name` | File name to be used to store the X.509 SVID private key and public certificate in PEM format. | `"svid_key.pem"` | + | `svid_bundle_file_name` | File name to be used to store the X.509 SVID Bundle in PEM format. | `"svid_bundle.pem"` | + | `jwt_svids` | An array with the audience, optional extra audiences array, and file name to store the JWT SVIDs. File is Base64-encoded string). | `[{jwt_audience="your-audience", jwt_extra_audiences=["your-extra-audience-1", "your-extra-audience-2"], jwt_svid_file_name="jwt_svid.token"}]` | + | `jwt_bundle_file_name` | File name to be used to store JWT Bundle in JSON format. | `"jwt_bundle.json"` | + | `include_federated_domains` | Include trust domains from federated servers in the CA bundle. | `true` | + | `cert_file_mode` | The octal file mode to use when saving the X.509 public certificate file. | `0644` | + | `key_file_mode` | The octal file mode to use when saving the X.509 private key file. | `0600` | + | `jwt_bundle_file_mode` | The octal file mode to use when saving a JWT Bundle file. | `0600` | + | `jwt_svid_file_mode` | The octal file mode to use when saving a JWT SVID file. | `0600` | + | `health_checks.enable_health_check` | Whether to start an HTTP server at `/healthz` with the daemon health. Doesn't apply for non-daemon mode. | `false` | + | `health_checks.health_check_port` | The port to run the HTTP health server. | `8081` | ### Configuration example ``` diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index 47b949b4..b2784b2b 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -15,6 +15,10 @@ import ( "github.com/spiffe/spiffe-helper/pkg/sidecar" ) +const ( + DaemonModeFlagName = "daemon-mode" +) + const ( defaultAgentAddress = "/tmp/spire-agent/public/api.sock" defaultCertFileMode = 0644 @@ -24,21 +28,20 @@ const ( ) type Config struct { - AddIntermediatesToBundle bool `hcl:"add_intermediates_to_bundle"` - AgentAddress string `hcl:"agent_address"` - Cmd string `hcl:"cmd"` - CmdArgs string `hcl:"cmd_args"` - PIDFileName string `hcl:"pid_file_name"` - CertDir string `hcl:"cert_dir"` - CertFileMode int `hcl:"cert_file_mode"` - KeyFileMode int `hcl:"key_file_mode"` - JWTBundleFileMode int `hcl:"jwt_bundle_file_mode"` - JWTSVIDFileMode int `hcl:"jwt_svid_file_mode"` - IncludeFederatedDomains bool `hcl:"include_federated_domains"` - RenewSignal string `hcl:"renew_signal"` - DaemonMode *bool `hcl:"daemon_mode"` - EnableHealthCheck *bool `hcl:"enable_health_check"` - HealthCheckPort int `hcl:"health_check_port"` + AddIntermediatesToBundle bool `hcl:"add_intermediates_to_bundle"` + AgentAddress string `hcl:"agent_address"` + Cmd string `hcl:"cmd"` + CmdArgs string `hcl:"cmd_args"` + PIDFileName string `hcl:"pid_file_name"` + CertDir string `hcl:"cert_dir"` + CertFileMode int `hcl:"cert_file_mode"` + KeyFileMode int `hcl:"key_file_mode"` + JWTBundleFileMode int `hcl:"jwt_bundle_file_mode"` + JWTSVIDFileMode int `hcl:"jwt_svid_file_mode"` + IncludeFederatedDomains bool `hcl:"include_federated_domains"` + RenewSignal string `hcl:"renew_signal"` + DaemonMode *bool `hcl:"daemon_mode"` + HealthCheck HealthCheckConfig `hcl:"health_checks"` // x509 configuration SVIDFileName string `hcl:"svid_file_name"` @@ -52,6 +55,11 @@ type Config struct { UnusedKeyPositions map[string][]token.Pos `hcl:",unusedKeyPositions"` } +type HealthCheckConfig struct { + EnableHealthCheck *bool `hcl:"enable_health_check"` + HealthCheckPort int `hcl:"health_check_port"` +} + type JWTConfig struct { JWTAudience string `hcl:"jwt_audience"` JWTExtraAudiences []string `hcl:"jwt_extra_audiences"` @@ -160,9 +168,9 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { c.JWTSVIDFileMode = defaultJWTSVIDFileMode } - if c.EnableHealthCheck == nil { + if c.HealthCheck.EnableHealthCheck == nil { defaultEnableHealthCheck := false - c.EnableHealthCheck = &defaultEnableHealthCheck + c.HealthCheck.EnableHealthCheck = &defaultEnableHealthCheck } return nil diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index 0291b171..38d9b058 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -2,28 +2,20 @@ package main import ( "context" - "encoding/json" "flag" "fmt" - "net/http" - "os" - "os/signal" - "strconv" - "syscall" - "time" - "github.com/sirupsen/logrus" "github.com/spiffe/spiffe-helper/cmd/spiffe-helper/config" + "github.com/spiffe/spiffe-helper/pkg/health" "github.com/spiffe/spiffe-helper/pkg/sidecar" -) - -const ( - daemonModeFlagName = "daemon-mode" + "os" + "os/signal" + "syscall" ) func main() { configFile := flag.String("config", "helper.conf", " Configuration file path") - daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") + daemonModeFlag := flag.Bool(config.DaemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") flag.Parse() log := logrus.WithField("system", "spiffe-helper") @@ -32,7 +24,7 @@ func main() { os.Exit(1) } - if err := startHealthServer(*configFile, *daemonModeFlag, log); err != nil { + if err := health.StartHealthServer(*configFile, *daemonModeFlag, log, spiffeSidecar); err != nil { log.WithError(err).Errorf("Error starting spiffe-helper health check server") os.Exit(1) } @@ -52,7 +44,7 @@ func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger if err != nil { return fmt.Errorf("failed to parse %q: %w", configFile, err) } - hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName) + hclConfig.ParseConfigFlagOverrides(daemonModeFlag, config.DaemonModeFlagName) if err := hclConfig.ValidateConfig(log); err != nil { return fmt.Errorf("invalid configuration: %w", err) @@ -69,41 +61,3 @@ func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger log.Info("Launching daemon") return spiffeSidecar.RunDaemon(ctx) } - -func startHealthServer(configFile string, daemonModeFlag bool, log logrus.FieldLogger) error { - hclConfig, err := config.ParseConfig(configFile) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", configFile, err) - } - hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName) - if err := hclConfig.ValidateConfig(log); err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - - if *hclConfig.DaemonMode && *hclConfig.EnableHealthCheck { - http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { - healthy := spiffeSidecar.CheckHealth() - if healthy { - _, err := w.Write([]byte(http.StatusText(http.StatusOK))) - log.Error(err) - if err != nil { - return - } - } else { - statusText := http.StatusText(http.StatusServiceUnavailable) - b, err := json.Marshal(spiffeSidecar.GetFileWritesSuccess()) - if err != nil { - statusText = string(b) - } - http.Error(w, statusText, http.StatusServiceUnavailable) - } - }) - server := &http.Server{ - Addr: ":" + strconv.Itoa(hclConfig.HealthCheckPort), - ReadHeaderTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - } - log.Fatal(server.ListenAndServe()) - } - return nil -} diff --git a/pkg/health/health.go b/pkg/health/health.go new file mode 100644 index 00000000..c880c0ea --- /dev/null +++ b/pkg/health/health.go @@ -0,0 +1,50 @@ +package health + +import ( + "encoding/json" + "fmt" + "github.com/sirupsen/logrus" + "github.com/spiffe/spiffe-helper/cmd/spiffe-helper/config" + "github.com/spiffe/spiffe-helper/pkg/sidecar" + "net/http" + "strconv" + "time" +) + +func StartHealthServer(configFile string, daemonModeFlag bool, log logrus.FieldLogger, sidecar *sidecar.Sidecar) error { + hclConfig, err := config.ParseConfig(configFile) + if err != nil { + return fmt.Errorf("failed to parse %q: %w", configFile, err) + } + hclConfig.ParseConfigFlagOverrides(daemonModeFlag, config.DaemonModeFlagName) + if err := hclConfig.ValidateConfig(log); err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + + if *hclConfig.DaemonMode && *hclConfig.HealthCheck.EnableHealthCheck { + http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + healthy := sidecar.CheckHealth() + if healthy { + _, err := w.Write([]byte(http.StatusText(http.StatusOK))) + log.Error(err) + if err != nil { + return + } + } else { + statusText := http.StatusText(http.StatusServiceUnavailable) + b, err := json.Marshal(sidecar.GetFileWritesSuccess()) + if err != nil { + statusText = string(b) + } + http.Error(w, statusText, http.StatusServiceUnavailable) + } + }) + server := &http.Server{ + Addr: ":" + strconv.Itoa(hclConfig.HealthCheck.HealthCheckPort), + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + } + log.Fatal(server.ListenAndServe()) + } + return nil +} From 6ac9f3f29a6d18a2ee6484c327cf16e54e161eb6 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Wed, 11 Dec 2024 22:09:52 -0500 Subject: [PATCH 03/14] More PR feedback Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 15 +++++++++++---- cmd/spiffe-helper/main.go | 13 ++++++------- pkg/health/health.go | 13 +++++++------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index b2784b2b..f7451818 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -16,10 +16,7 @@ import ( ) const ( - DaemonModeFlagName = "daemon-mode" -) - -const ( + daemonModeFlagName = "daemon-mode" defaultAgentAddress = "/tmp/spire-agent/public/api.sock" defaultCertFileMode = 0644 defaultKeyFileMode = 0600 @@ -176,6 +173,16 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { return nil } +func ParseConfigFile(log logrus.FieldLogger, configFile string, daemonModeFlag bool) (*Config, error) { + log.Infof("Using configuration file: %q", configFile) + hclConfig, err := ParseConfig(configFile) + if err != nil { + return nil, fmt.Errorf("failed to parse %q: %w", configFile, err) + } + hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName) + return hclConfig, nil +} + // checkForUnknownConfig looks for any unknown configuration keys and returns an error if one is found func (c *Config) checkForUnknownConfig() error { if len(c.UnusedKeyPositions) != 0 { diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index 38d9b058..92ea557d 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -4,13 +4,14 @@ import ( "context" "flag" "fmt" + "os" + "os/signal" + "syscall" + "github.com/sirupsen/logrus" "github.com/spiffe/spiffe-helper/cmd/spiffe-helper/config" "github.com/spiffe/spiffe-helper/pkg/health" "github.com/spiffe/spiffe-helper/pkg/sidecar" - "os" - "os/signal" - "syscall" ) func main() { @@ -39,12 +40,10 @@ func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - log.Infof("Using configuration file: %q", configFile) - hclConfig, err := config.ParseConfig(configFile) + hclConfig, err := config.ParseConfigFile(log, configFile, daemonModeFlag) if err != nil { - return fmt.Errorf("failed to parse %q: %w", configFile, err) + return err } - hclConfig.ParseConfigFlagOverrides(daemonModeFlag, config.DaemonModeFlagName) if err := hclConfig.ValidateConfig(log); err != nil { return fmt.Errorf("invalid configuration: %w", err) diff --git a/pkg/health/health.go b/pkg/health/health.go index c880c0ea..689d7a8d 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -3,20 +3,21 @@ package health import ( "encoding/json" "fmt" - "github.com/sirupsen/logrus" - "github.com/spiffe/spiffe-helper/cmd/spiffe-helper/config" - "github.com/spiffe/spiffe-helper/pkg/sidecar" "net/http" "strconv" "time" + + "github.com/sirupsen/logrus" + "github.com/spiffe/spiffe-helper/cmd/spiffe-helper/config" + "github.com/spiffe/spiffe-helper/pkg/sidecar" ) func StartHealthServer(configFile string, daemonModeFlag bool, log logrus.FieldLogger, sidecar *sidecar.Sidecar) error { - hclConfig, err := config.ParseConfig(configFile) + hclConfig, err := config.ParseConfigFile(log, configFile, daemonModeFlag) if err != nil { - return fmt.Errorf("failed to parse %q: %w", configFile, err) + return err } - hclConfig.ParseConfigFlagOverrides(daemonModeFlag, config.DaemonModeFlagName) + if err := hclConfig.ValidateConfig(log); err != nil { return fmt.Errorf("invalid configuration: %w", err) } From 6b32baeda464216b9d99d6bcf9beb6153971ab89 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Wed, 11 Dec 2024 23:37:09 -0500 Subject: [PATCH 04/14] Fix DaemonModeFlagName const Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 11 +++++++---- cmd/spiffe-helper/config/config_test.go | 10 +++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index f7451818..9a90b9bc 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -16,7 +16,10 @@ import ( ) const ( - daemonModeFlagName = "daemon-mode" + DaemonModeFlagName = "daemon-mode" +) + +const ( defaultAgentAddress = "/tmp/spire-agent/public/api.sock" defaultCertFileMode = 0644 defaultKeyFileMode = 0600 @@ -83,8 +86,8 @@ func ParseConfig(file string) (*Config, error) { } // ParseConfigFlagOverrides handles command line arguments that override config file settings -func (c *Config) ParseConfigFlagOverrides(daemonModeFlag bool, daemonModeFlagName string) { - if isFlagPassed(daemonModeFlagName) { +func (c *Config) ParseConfigFlagOverrides(daemonModeFlag bool) { + if isFlagPassed(DaemonModeFlagName) { // If daemon mode is set by CLI this takes precedence c.DaemonMode = &daemonModeFlag } else if c.DaemonMode == nil { @@ -179,7 +182,7 @@ func ParseConfigFile(log logrus.FieldLogger, configFile string, daemonModeFlag b if err != nil { return nil, fmt.Errorf("failed to parse %q: %w", configFile, err) } - hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName) + hclConfig.ParseConfigFlagOverrides(daemonModeFlag) return hclConfig, nil } diff --git a/cmd/spiffe-helper/config/config_test.go b/cmd/spiffe-helper/config/config_test.go index 94e71adb..cc2a3f45 100644 --- a/cmd/spiffe-helper/config/config_test.go +++ b/cmd/spiffe-helper/config/config_test.go @@ -10,10 +10,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - daemonModeFlagName = "daemon-mode" -) - func TestParseConfig(t *testing.T) { c, err := ParseConfig("testdata/helper.conf") @@ -308,13 +304,13 @@ func TestDaemonModeFlag(t *testing.T) { SVIDBundleFileName: "bundle.pem", } - daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") + daemonModeFlag := flag.Bool(DaemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") flag.Parse() - err := flag.Set(daemonModeFlagName, "false") + err := flag.Set(DaemonModeFlagName, "false") require.NoError(t, err) - config.ParseConfigFlagOverrides(*daemonModeFlag, daemonModeFlagName) + config.ParseConfigFlagOverrides(*daemonModeFlag) require.NotNil(t, config.DaemonMode) assert.Equal(t, false, *config.DaemonMode) } From b4b21f657cd198401bf6abc07f169ca2eb7ee4a6 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Fri, 13 Dec 2024 10:49:47 -0500 Subject: [PATCH 05/14] Make health check path configurable and implement default health check configs Signed-off-by: Keegan Witt --- README.md | 3 +- cmd/spiffe-helper/config/config.go | 45 +++++++++++++++++++----------- pkg/health/health.go | 2 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index bda86d3c..e6a3fa6f 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted f | `key_file_mode` | The octal file mode to use when saving the X.509 private key file. | `0600` | | `jwt_bundle_file_mode` | The octal file mode to use when saving a JWT Bundle file. | `0600` | | `jwt_svid_file_mode` | The octal file mode to use when saving a JWT SVID file. | `0600` | - | `health_checks.enable_health_check` | Whether to start an HTTP server at `/healthz` with the daemon health. Doesn't apply for non-daemon mode. | `false` | + | `health_checks.enable_health_check` | Whether to start an HTTP server at the configured endpoint for the daemon health. Doesn't apply for non-daemon mode. | `false` | | `health_checks.health_check_port` | The port to run the HTTP health server. | `8081` | +| `health_checks.health_check_path` | The URL path for the health check | `/healthz` | ### Configuration example ``` diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index 9a90b9bc..0feda098 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -25,23 +25,25 @@ const ( defaultKeyFileMode = 0600 defaultJWTBundleFileMode = 0600 defaultJWTSVIDFileMode = 0600 + defaultHealthCheckPort = 8081 + defaultHealthCheckPath = "/healthz" ) type Config struct { - AddIntermediatesToBundle bool `hcl:"add_intermediates_to_bundle"` - AgentAddress string `hcl:"agent_address"` - Cmd string `hcl:"cmd"` - CmdArgs string `hcl:"cmd_args"` - PIDFileName string `hcl:"pid_file_name"` - CertDir string `hcl:"cert_dir"` - CertFileMode int `hcl:"cert_file_mode"` - KeyFileMode int `hcl:"key_file_mode"` - JWTBundleFileMode int `hcl:"jwt_bundle_file_mode"` - JWTSVIDFileMode int `hcl:"jwt_svid_file_mode"` - IncludeFederatedDomains bool `hcl:"include_federated_domains"` - RenewSignal string `hcl:"renew_signal"` - DaemonMode *bool `hcl:"daemon_mode"` - HealthCheck HealthCheckConfig `hcl:"health_checks"` + AddIntermediatesToBundle bool `hcl:"add_intermediates_to_bundle"` + AgentAddress string `hcl:"agent_address"` + Cmd string `hcl:"cmd"` + CmdArgs string `hcl:"cmd_args"` + PIDFileName string `hcl:"pid_file_name"` + CertDir string `hcl:"cert_dir"` + CertFileMode int `hcl:"cert_file_mode"` + KeyFileMode int `hcl:"key_file_mode"` + JWTBundleFileMode int `hcl:"jwt_bundle_file_mode"` + JWTSVIDFileMode int `hcl:"jwt_svid_file_mode"` + IncludeFederatedDomains bool `hcl:"include_federated_domains"` + RenewSignal string `hcl:"renew_signal"` + DaemonMode *bool `hcl:"daemon_mode"` + HealthCheck *HealthCheckConfig `hcl:"health_checks"` // x509 configuration SVIDFileName string `hcl:"svid_file_name"` @@ -56,8 +58,9 @@ type Config struct { } type HealthCheckConfig struct { - EnableHealthCheck *bool `hcl:"enable_health_check"` - HealthCheckPort int `hcl:"health_check_port"` + EnableHealthCheck *bool `hcl:"enable_health_check"` + HealthCheckPort int `hcl:"health_check_port"` + HealthCheckPath string `hcl:"health_check_path"` } type JWTConfig struct { @@ -168,10 +171,18 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { c.JWTSVIDFileMode = defaultJWTSVIDFileMode } - if c.HealthCheck.EnableHealthCheck == nil { + if c.HealthCheck == nil || c.HealthCheck.EnableHealthCheck == nil { defaultEnableHealthCheck := false c.HealthCheck.EnableHealthCheck = &defaultEnableHealthCheck } + if c.HealthCheck.HealthCheckPort < 0 { + return errors.New("health check port must be positive") + } else if c.HealthCheck.HealthCheckPort == 0 { + c.HealthCheck.HealthCheckPort = defaultHealthCheckPort + } + if c.HealthCheck.HealthCheckPath == "" { + c.HealthCheck.HealthCheckPath = defaultHealthCheckPath + } return nil } diff --git a/pkg/health/health.go b/pkg/health/health.go index 689d7a8d..fe0f7060 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -23,7 +23,7 @@ func StartHealthServer(configFile string, daemonModeFlag bool, log logrus.FieldL } if *hclConfig.DaemonMode && *hclConfig.HealthCheck.EnableHealthCheck { - http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + http.HandleFunc(hclConfig.HealthCheck.HealthCheckPath, func(w http.ResponseWriter, _ *http.Request) { healthy := sidecar.CheckHealth() if healthy { _, err := w.Write([]byte(http.StatusText(http.StatusOK))) From 07985d79ae66fc9526bc4ef3d04f2cc0e62719c6 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Fri, 13 Dec 2024 21:18:20 -0500 Subject: [PATCH 06/14] Keep ParseConfigFile in main Signed-off-by: Keegan Witt --- cmd/spiffe-helper/main.go | 26 ++++++++++++++------------ pkg/health/health.go | 12 +----------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index 92ea557d..f3219cab 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -20,12 +20,23 @@ func main() { flag.Parse() log := logrus.WithField("system", "spiffe-helper") - if err := startSidecar(*configFile, *daemonModeFlag, log); err != nil { + hclConfig, err := config.ParseConfigFile(log, *configFile, *daemonModeFlag) + if err != nil { + log.WithError(err).Errorf("failed to parse configuration") + os.Exit(1) + } + + if err := hclConfig.ValidateConfig(log); err != nil { + log.WithError(err).Errorf("invalid configuration") + os.Exit(1) + } + + if err := startSidecar(hclConfig, log); err != nil { log.WithError(err).Errorf("Error starting spiffe-helper") os.Exit(1) } - if err := health.StartHealthServer(*configFile, *daemonModeFlag, log, spiffeSidecar); err != nil { + if err := health.StartHealthServer(hclConfig, log, spiffeSidecar); err != nil { log.WithError(err).Errorf("Error starting spiffe-helper health check server") os.Exit(1) } @@ -36,19 +47,10 @@ func main() { var spiffeSidecar *sidecar.Sidecar -func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger) error { +func startSidecar(hclConfig *config.Config, log logrus.FieldLogger) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - hclConfig, err := config.ParseConfigFile(log, configFile, daemonModeFlag) - if err != nil { - return err - } - - if err := hclConfig.ValidateConfig(log); err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - sidecarConfig := config.NewSidecarConfig(hclConfig, log) spiffeSidecar = sidecar.New(sidecarConfig) diff --git a/pkg/health/health.go b/pkg/health/health.go index fe0f7060..8bacf16a 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -2,7 +2,6 @@ package health import ( "encoding/json" - "fmt" "net/http" "strconv" "time" @@ -12,16 +11,7 @@ import ( "github.com/spiffe/spiffe-helper/pkg/sidecar" ) -func StartHealthServer(configFile string, daemonModeFlag bool, log logrus.FieldLogger, sidecar *sidecar.Sidecar) error { - hclConfig, err := config.ParseConfigFile(log, configFile, daemonModeFlag) - if err != nil { - return err - } - - if err := hclConfig.ValidateConfig(log); err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - +func StartHealthServer(hclConfig *config.Config, log logrus.FieldLogger, sidecar *sidecar.Sidecar) error { if *hclConfig.DaemonMode && *hclConfig.HealthCheck.EnableHealthCheck { http.HandleFunc(hclConfig.HealthCheck.HealthCheckPath, func(w http.ResponseWriter, _ *http.Request) { healthy := sidecar.CheckHealth() From 28885ec9264bb705b6206ffac5c8f2159bcdc46d Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Fri, 13 Dec 2024 21:50:09 -0500 Subject: [PATCH 07/14] Fix tests Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 12 ++++-------- cmd/spiffe-helper/main.go | 1 - pkg/health/health.go | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index 0feda098..39d9d244 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -58,7 +58,7 @@ type Config struct { } type HealthCheckConfig struct { - EnableHealthCheck *bool `hcl:"enable_health_check"` + EnableHealthCheck bool `hcl:"enable_health_check"` HealthCheckPort int `hcl:"health_check_port"` HealthCheckPath string `hcl:"health_check_path"` } @@ -171,16 +171,12 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { c.JWTSVIDFileMode = defaultJWTSVIDFileMode } - if c.HealthCheck == nil || c.HealthCheck.EnableHealthCheck == nil { - defaultEnableHealthCheck := false - c.HealthCheck.EnableHealthCheck = &defaultEnableHealthCheck - } - if c.HealthCheck.HealthCheckPort < 0 { + if c.HealthCheck != nil && c.HealthCheck.HealthCheckPort < 0 { return errors.New("health check port must be positive") - } else if c.HealthCheck.HealthCheckPort == 0 { + } else if c.HealthCheck != nil && c.HealthCheck.HealthCheckPort == 0 { c.HealthCheck.HealthCheckPort = defaultHealthCheckPort } - if c.HealthCheck.HealthCheckPath == "" { + if c.HealthCheck != nil && c.HealthCheck.HealthCheckPath == "" { c.HealthCheck.HealthCheckPath = defaultHealthCheckPath } diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index f3219cab..33805b6c 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -3,7 +3,6 @@ package main import ( "context" "flag" - "fmt" "os" "os/signal" "syscall" diff --git a/pkg/health/health.go b/pkg/health/health.go index 8bacf16a..3bda8d1e 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -12,7 +12,7 @@ import ( ) func StartHealthServer(hclConfig *config.Config, log logrus.FieldLogger, sidecar *sidecar.Sidecar) error { - if *hclConfig.DaemonMode && *hclConfig.HealthCheck.EnableHealthCheck { + if *hclConfig.DaemonMode && hclConfig.HealthCheck.EnableHealthCheck { http.HandleFunc(hclConfig.HealthCheck.HealthCheckPath, func(w http.ResponseWriter, _ *http.Request) { healthy := sidecar.CheckHealth() if healthy { From 59af728f1ac00fa0b6ff493809bd44495be9fb4a Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Fri, 13 Dec 2024 21:58:36 -0500 Subject: [PATCH 08/14] Upgrade golangci-lint to fix error Error: can't load config: the Go language version (go1.22) used to build golangci-lint is lower than the targeted Go version (1.23.3) Signed-off-by: Keegan Witt --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5fd63ab9..fe0e79a0 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,7 @@ else endif go_path := PATH="$(go_bin_dir):$(PATH)" -golangci_lint_version = v1.60.3 +golangci_lint_version = v1.62.2 golangci_lint_dir = $(build_dir)/golangci_lint/$(golangci_lint_version) golangci_lint_bin = $(golangci_lint_dir)/golangci-lint golangci_lint_cache = $(golangci_lint_dir)/cache From 68a27ba4f7f3f3443f4068a02a34b46125c47c2d Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Mon, 16 Dec 2024 16:45:22 -0500 Subject: [PATCH 09/14] Update cmd/spiffe-helper/config/config.go Co-authored-by: Faisal Memon Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index 39d9d244..f38ce750 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -58,9 +58,9 @@ type Config struct { } type HealthCheckConfig struct { - EnableHealthCheck bool `hcl:"enable_health_check"` - HealthCheckPort int `hcl:"health_check_port"` - HealthCheckPath string `hcl:"health_check_path"` + ListenerEnabled bool `hcl:"listener_enabled"` + BindPort int `hcl:"bind_port"` + HealthPath string `hcl:"health_path"` } type JWTConfig struct { From 608468549ed94de04dbbaf3edb02753e4c76c3d2 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Mon, 16 Dec 2024 16:45:33 -0500 Subject: [PATCH 10/14] Update README.md Co-authored-by: Faisal Memon Signed-off-by: Keegan Witt --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index e6a3fa6f..48a0ccb3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,12 @@ The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted f | `key_file_mode` | The octal file mode to use when saving the X.509 private key file. | `0600` | | `jwt_bundle_file_mode` | The octal file mode to use when saving a JWT Bundle file. | `0600` | | `jwt_svid_file_mode` | The octal file mode to use when saving a JWT SVID file. | `0600` | + +### Health Check Configuration +SPIFFE Helper can expose and endpoint that can be used for health checking + +| Configuration | Description | Example Value | + |-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `health_checks.enable_health_check` | Whether to start an HTTP server at the configured endpoint for the daemon health. Doesn't apply for non-daemon mode. | `false` | | `health_checks.health_check_port` | The port to run the HTTP health server. | `8081` | | `health_checks.health_check_path` | The URL path for the health check | `/healthz` | From 944d88072df2aa0d3c13f1dfc7ff355f128dddc0 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Mon, 16 Dec 2024 22:26:03 -0500 Subject: [PATCH 11/14] Fix remaining config renames Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 20 ++++++++++---------- pkg/health/health.go | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index f38ce750..fa9a34cf 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -25,8 +25,8 @@ const ( defaultKeyFileMode = 0600 defaultJWTBundleFileMode = 0600 defaultJWTSVIDFileMode = 0600 - defaultHealthCheckPort = 8081 - defaultHealthCheckPath = "/healthz" + defaultBindPort = 8081 + defaultHealthPath = "/healthz" ) type Config struct { @@ -59,8 +59,8 @@ type Config struct { type HealthCheckConfig struct { ListenerEnabled bool `hcl:"listener_enabled"` - BindPort int `hcl:"bind_port"` - HealthPath string `hcl:"health_path"` + BindPort int `hcl:"bind_port"` + HealthPath string `hcl:"health_path"` } type JWTConfig struct { @@ -171,13 +171,13 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { c.JWTSVIDFileMode = defaultJWTSVIDFileMode } - if c.HealthCheck != nil && c.HealthCheck.HealthCheckPort < 0 { - return errors.New("health check port must be positive") - } else if c.HealthCheck != nil && c.HealthCheck.HealthCheckPort == 0 { - c.HealthCheck.HealthCheckPort = defaultHealthCheckPort + if c.HealthCheck != nil && c.HealthCheck.BindPort < 0 { + return errors.New("bind port must be positive") + } else if c.HealthCheck != nil && c.HealthCheck.BindPort == 0 { + c.HealthCheck.BindPort = defaultBindPort } - if c.HealthCheck != nil && c.HealthCheck.HealthCheckPath == "" { - c.HealthCheck.HealthCheckPath = defaultHealthCheckPath + if c.HealthCheck != nil && c.HealthCheck.HealthPath == "" { + c.HealthCheck.HealthPath = defaultHealthPath } return nil diff --git a/pkg/health/health.go b/pkg/health/health.go index 3bda8d1e..b8fd93b4 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -12,8 +12,8 @@ import ( ) func StartHealthServer(hclConfig *config.Config, log logrus.FieldLogger, sidecar *sidecar.Sidecar) error { - if *hclConfig.DaemonMode && hclConfig.HealthCheck.EnableHealthCheck { - http.HandleFunc(hclConfig.HealthCheck.HealthCheckPath, func(w http.ResponseWriter, _ *http.Request) { + if *hclConfig.DaemonMode && hclConfig.HealthCheck.ListenerEnabled { + http.HandleFunc(hclConfig.HealthCheck.HealthPath, func(w http.ResponseWriter, _ *http.Request) { healthy := sidecar.CheckHealth() if healthy { _, err := w.Write([]byte(http.StatusText(http.StatusOK))) @@ -31,7 +31,7 @@ func StartHealthServer(hclConfig *config.Config, log logrus.FieldLogger, sidecar } }) server := &http.Server{ - Addr: ":" + strconv.Itoa(hclConfig.HealthCheck.HealthCheckPort), + Addr: ":" + strconv.Itoa(hclConfig.HealthCheck.BindPort), ReadHeaderTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, } From d8ff964e1db8e65e3a02dd5173b03a637b4336b4 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Tue, 17 Dec 2024 09:47:01 -0500 Subject: [PATCH 12/14] Add back flag duplication Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 7 ++----- cmd/spiffe-helper/config/config_test.go | 4 ++-- cmd/spiffe-helper/main.go | 6 +++++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index fa9a34cf..b1ca5fe7 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -16,10 +16,7 @@ import ( ) const ( - DaemonModeFlagName = "daemon-mode" -) - -const ( + daemonModeFlagName = "daemon-mode" defaultAgentAddress = "/tmp/spire-agent/public/api.sock" defaultCertFileMode = 0644 defaultKeyFileMode = 0600 @@ -90,7 +87,7 @@ func ParseConfig(file string) (*Config, error) { // ParseConfigFlagOverrides handles command line arguments that override config file settings func (c *Config) ParseConfigFlagOverrides(daemonModeFlag bool) { - if isFlagPassed(DaemonModeFlagName) { + if isFlagPassed(daemonModeFlagName) { // If daemon mode is set by CLI this takes precedence c.DaemonMode = &daemonModeFlag } else if c.DaemonMode == nil { diff --git a/cmd/spiffe-helper/config/config_test.go b/cmd/spiffe-helper/config/config_test.go index cc2a3f45..aa44220a 100644 --- a/cmd/spiffe-helper/config/config_test.go +++ b/cmd/spiffe-helper/config/config_test.go @@ -304,10 +304,10 @@ func TestDaemonModeFlag(t *testing.T) { SVIDBundleFileName: "bundle.pem", } - daemonModeFlag := flag.Bool(DaemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") + daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") flag.Parse() - err := flag.Set(DaemonModeFlagName, "false") + err := flag.Set(daemonModeFlagName, "false") require.NoError(t, err) config.ParseConfigFlagOverrides(*daemonModeFlag) diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index 33805b6c..842adaf3 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -13,9 +13,13 @@ import ( "github.com/spiffe/spiffe-helper/pkg/sidecar" ) +const ( + daemonModeFlagName = "daemon-mode" +) + func main() { configFile := flag.String("config", "helper.conf", " Configuration file path") - daemonModeFlag := flag.Bool(config.DaemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") + daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit") flag.Parse() log := logrus.WithField("system", "spiffe-helper") From 16407611d737e07f30cf517aa0c101e026ec5c1e Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Tue, 17 Dec 2024 09:49:26 -0500 Subject: [PATCH 13/14] Don't use a pointer for HealthCheckConfig Signed-off-by: Keegan Witt --- cmd/spiffe-helper/config/config.go | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index b1ca5fe7..c1cc2b5c 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -27,20 +27,20 @@ const ( ) type Config struct { - AddIntermediatesToBundle bool `hcl:"add_intermediates_to_bundle"` - AgentAddress string `hcl:"agent_address"` - Cmd string `hcl:"cmd"` - CmdArgs string `hcl:"cmd_args"` - PIDFileName string `hcl:"pid_file_name"` - CertDir string `hcl:"cert_dir"` - CertFileMode int `hcl:"cert_file_mode"` - KeyFileMode int `hcl:"key_file_mode"` - JWTBundleFileMode int `hcl:"jwt_bundle_file_mode"` - JWTSVIDFileMode int `hcl:"jwt_svid_file_mode"` - IncludeFederatedDomains bool `hcl:"include_federated_domains"` - RenewSignal string `hcl:"renew_signal"` - DaemonMode *bool `hcl:"daemon_mode"` - HealthCheck *HealthCheckConfig `hcl:"health_checks"` + AddIntermediatesToBundle bool `hcl:"add_intermediates_to_bundle"` + AgentAddress string `hcl:"agent_address"` + Cmd string `hcl:"cmd"` + CmdArgs string `hcl:"cmd_args"` + PIDFileName string `hcl:"pid_file_name"` + CertDir string `hcl:"cert_dir"` + CertFileMode int `hcl:"cert_file_mode"` + KeyFileMode int `hcl:"key_file_mode"` + JWTBundleFileMode int `hcl:"jwt_bundle_file_mode"` + JWTSVIDFileMode int `hcl:"jwt_svid_file_mode"` + IncludeFederatedDomains bool `hcl:"include_federated_domains"` + RenewSignal string `hcl:"renew_signal"` + DaemonMode *bool `hcl:"daemon_mode"` + HealthCheck HealthCheckConfig `hcl:"health_checks"` // x509 configuration SVIDFileName string `hcl:"svid_file_name"` @@ -168,12 +168,12 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { c.JWTSVIDFileMode = defaultJWTSVIDFileMode } - if c.HealthCheck != nil && c.HealthCheck.BindPort < 0 { + if c.HealthCheck.ListenerEnabled && c.HealthCheck.BindPort < 0 { return errors.New("bind port must be positive") - } else if c.HealthCheck != nil && c.HealthCheck.BindPort == 0 { + } else if c.HealthCheck.ListenerEnabled && c.HealthCheck.BindPort == 0 { c.HealthCheck.BindPort = defaultBindPort } - if c.HealthCheck != nil && c.HealthCheck.HealthPath == "" { + if c.HealthCheck.ListenerEnabled && c.HealthCheck.HealthPath == "" { c.HealthCheck.HealthPath = defaultHealthPath } From 97019c67fd58e8a4e174183d762bbe555ced0d01 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Tue, 17 Dec 2024 12:29:42 -0500 Subject: [PATCH 14/14] Remove global variable Signed-off-by: Keegan Witt --- cmd/spiffe-helper/main.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index 842adaf3..3caceb9d 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -34,7 +34,8 @@ func main() { os.Exit(1) } - if err := startSidecar(hclConfig, log); err != nil { + spiffeSidecar, err := startSidecar(hclConfig, log) + if err != nil { log.WithError(err).Errorf("Error starting spiffe-helper") os.Exit(1) } @@ -48,20 +49,18 @@ func main() { os.Exit(0) } -var spiffeSidecar *sidecar.Sidecar - -func startSidecar(hclConfig *config.Config, log logrus.FieldLogger) error { +func startSidecar(hclConfig *config.Config, log logrus.FieldLogger) (*sidecar.Sidecar, error) { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() sidecarConfig := config.NewSidecarConfig(hclConfig, log) - spiffeSidecar = sidecar.New(sidecarConfig) + spiffeSidecar := sidecar.New(sidecarConfig) if !*hclConfig.DaemonMode { log.Info("Daemon mode disabled") - return spiffeSidecar.Run(ctx) + return spiffeSidecar, spiffeSidecar.Run(ctx) } log.Info("Launching daemon") - return spiffeSidecar.RunDaemon(ctx) + return spiffeSidecar, spiffeSidecar.RunDaemon(ctx) }