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 d378b30
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 29 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
51 changes: 50 additions & 1 deletion cmd/spiffe-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand All @@ -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")
Expand All @@ -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
}
Loading

0 comments on commit d378b30

Please sign in to comment.