Skip to content

Commit

Permalink
Add HTTP health server (closes #115)
Browse files Browse the repository at this point in the history
Signed-off-by: Keegan Witt <[email protected]>
  • Loading branch information
keeganwitt committed Dec 7, 2024
1 parent 2f033c2 commit fb86fa4
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 27 deletions.
40 changes: 21 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
7 changes: 7 additions & 0 deletions cmd/spiffe-helper/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}

Expand Down
41 changes: 40 additions & 1 deletion cmd/spiffe-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"

"github.com/sirupsen/logrus"
Expand All @@ -28,10 +31,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()
Expand All @@ -48,7 +58,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")
Expand All @@ -58,3 +68,32 @@ 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, r *http.Request) {

Check failure on line 83 in cmd/spiffe-helper/main.go

View workflow job for this annotation

GitHub Actions / lint (linux)

unused-parameter: parameter 'r' seems to be unused, consider removing or renaming it as _ (revive)
healthy := spiffeSidecar.CheckHealth()
if healthy {
w.Write([]byte(http.StatusText(http.StatusOK)))

Check failure on line 86 in cmd/spiffe-helper/main.go

View workflow job for this annotation

GitHub Actions / lint (linux)

Error return value of `w.Write` is not checked (errcheck)
} else {
statusText := http.StatusText(http.StatusServiceUnavailable)
b, err := json.Marshal(spiffeSidecar.GetFileWritesSuccess())
if err != nil {
statusText = string(b)
}
http.Error(w, statusText, http.StatusServiceUnavailable)
}
})
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(hclConfig.HealthCheckPort), nil))

Check failure on line 96 in cmd/spiffe-helper/main.go

View workflow job for this annotation

GitHub Actions / lint (linux)

G114: Use of net/http serve function that has no support for setting timeouts (gosec)
}
return nil
}
46 changes: 39 additions & 7 deletions pkg/sidecar/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
Expand All @@ -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
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand All @@ -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
}

0 comments on commit fb86fa4

Please sign in to comment.