diff --git a/README.md b/README.md index ca3db016..6c675f3e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted f | `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"` | - | `exit_when_ready` | Fetch x509 certificate and then exit(0) | `true` | + | `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"` | diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index 21c0aa77..fbc3c3bd 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -2,6 +2,7 @@ package config import ( "errors" + "flag" "os" "github.com/hashicorp/hcl" @@ -23,10 +24,10 @@ type Config struct { CmdArgsDeprecated string `hcl:"cmdArgs"` CertDir string `hcl:"cert_dir"` CertDirDeprecated string `hcl:"certDir"` - ExitWhenReady bool `hcl:"exit_when_ready"` IncludeFederatedDomains bool `hcl:"include_federated_domains"` RenewSignal string `hcl:"renew_signal"` RenewSignalDeprecated string `hcl:"renewSignal"` + DaemonMode *bool `hcl:"daemon_mode"` // x509 configuration SVIDFileName string `hcl:"svid_file_name"` @@ -48,8 +49,6 @@ type JWTConfig struct { // ParseConfig parses the given HCL file into a Config struct func ParseConfig(file string) (*Config, error) { - sidecarConfig := new(Config) - // Read HCL file dat, err := os.ReadFile(file) if err != nil { @@ -57,17 +56,31 @@ func ParseConfig(file string) (*Config, error) { } // Parse HCL - if err := hcl.Decode(sidecarConfig, string(dat)); err != nil { + config := new(Config) + if err := hcl.Decode(config, string(dat)); err != nil { return nil, err } - return sidecarConfig, nil + return config, nil +} + +// ParseConfigFlagOverrides handles command line arguments that override config file settings +func (c *Config) ParseConfigFlagOverrides(daemonModeFlag bool, daemonModeFlagName string) { + if isFlagPassed(daemonModeFlagName) { + // If daemon mode is set by CLI this takes precedence + c.DaemonMode = &daemonModeFlag + } else if c.DaemonMode == nil { + // If daemon mode is not set, then default to true + daemonMode := true + c.DaemonMode = &daemonMode + } } -func ValidateConfig(c *Config, exitWhenReady bool, log logrus.FieldLogger) error { +func (c *Config) ValidateConfig(log logrus.FieldLogger) error { if err := validateOSConfig(c); err != nil { return err } + if c.AgentAddressDeprecated != "" { if c.AgentAddress != "" { return errors.New("use of agent_address and agentAddress found, use only agent_address") @@ -140,16 +153,15 @@ func ValidateConfig(c *Config, exitWhenReady bool, log logrus.FieldLogger) error } } - c.ExitWhenReady = c.ExitWhenReady || exitWhenReady - - x509EmptyCount := countEmpty(c.SVIDFileName, c.SVIDBundleFileName, c.SVIDKeyFileName) - jwtBundleEmptyCount := countEmpty(c.SVIDBundleFileName) - if x509EmptyCount == 3 && len(c.JWTSVIDs) == 0 && jwtBundleEmptyCount == 1 { - return errors.New("at least one of the sets ('svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name'), 'jwt_svids', or 'jwt_bundle_file_name' must be fully specified") + x509Enabled, err := validateX509Config(c) + if err != nil { + return err } - if x509EmptyCount != 0 && x509EmptyCount != 3 { - return errors.New("all or none of 'svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name' must be specified") + jwtBundleEnabled, jwtSVIDsEnabled := validateJWTConfig(c) + + if !x509Enabled && !jwtBundleEnabled && !jwtSVIDsEnabled { + return errors.New("at least one of the sets ('svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name'), 'jwt_svids', or 'jwt_bundle_file_name' must be fully specified") } return nil @@ -162,7 +174,6 @@ func NewSidecarConfig(config *Config, log logrus.FieldLogger) *sidecar.Config { Cmd: config.Cmd, CmdArgs: config.CmdArgs, CertDir: config.CertDir, - ExitWhenReady: config.ExitWhenReady, IncludeFederatedDomains: config.IncludeFederatedDomains, JWTBundleFilename: config.JWTBundleFilename, Log: log, @@ -182,6 +193,21 @@ func NewSidecarConfig(config *Config, log logrus.FieldLogger) *sidecar.Config { return sidecarConfig } +func validateX509Config(c *Config) (bool, error) { + x509EmptyCount := countEmpty(c.SVIDFileName, c.SVIDBundleFileName, c.SVIDKeyFileName) + if x509EmptyCount != 0 && x509EmptyCount != 3 { + return false, errors.New("all or none of 'svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name' must be specified") + } + + return x509EmptyCount == 0, nil +} + +func validateJWTConfig(c *Config) (bool, bool) { + jwtBundleEmptyCount := countEmpty(c.SVIDBundleFileName) + + return jwtBundleEmptyCount == 0, len(c.JWTSVIDs) > 0 +} + func getWarning(s1 string, s2 string) string { return s1 + " will be deprecated, should be used as " + s2 } @@ -193,5 +219,18 @@ func countEmpty(configs ...string) int { cnt++ } } + return cnt } + +// isFlagPassed tests to see if a command line argument was set at all or left empty +func isFlagPassed(name string) bool { + var found bool + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + + return found +} diff --git a/cmd/spiffe-helper/config/config_test.go b/cmd/spiffe-helper/config/config_test.go index 65371de0..73950a8f 100644 --- a/cmd/spiffe-helper/config/config_test.go +++ b/cmd/spiffe-helper/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "flag" "os" "testing" @@ -10,6 +11,10 @@ import ( "github.com/stretchr/testify/require" ) +const ( + daemonModeFlagName = "daemon-mode" +) + func TestParseConfig(t *testing.T) { c, err := ParseConfig("testdata/helper.conf") @@ -294,7 +299,7 @@ func TestValidateConfig(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { log, hook := test.NewNullLogger() - err := ValidateConfig(tt.config, false, log) + err := tt.config.ValidateConfig(log) require.ElementsMatch(t, tt.expectLogs, getShortEntries(hook.AllEntries())) @@ -345,7 +350,7 @@ func TestDefaultAgentAddress(t *testing.T) { SVIDBundleFileName: "bundle.pem", } log, _ := test.NewNullLogger() - err := ValidateConfig(config, false, log) + err := config.ValidateConfig(log) require.NoError(t, err) assert.Equal(t, config.AgentAddress, tt.expectedAgentAddress) }) @@ -388,16 +393,22 @@ func TestNewSidecarConfig(t *testing.T) { assert.Equal(t, "", sidecarConfig.RenewSignal) } -func TestExitOnWaitFlag(t *testing.T) { +func TestDaemonModeFlag(t *testing.T) { config := &Config{ SVIDFileName: "cert.pem", SVIDKeyFileName: "key.pem", SVIDBundleFileName: "bundle.pem", } - log, _ := test.NewNullLogger() - err := ValidateConfig(config, true, log) + + 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") require.NoError(t, err) - assert.Equal(t, config.ExitWhenReady, true) + + config.ParseConfigFlagOverrides(*daemonModeFlag, daemonModeFlagName) + require.NotNil(t, config.DaemonMode) + assert.Equal(t, false, *config.DaemonMode) } type shortEntry struct { diff --git a/cmd/spiffe-helper/main.go b/cmd/spiffe-helper/main.go index e0c441b6..6ca529cf 100644 --- a/cmd/spiffe-helper/main.go +++ b/cmd/spiffe-helper/main.go @@ -12,40 +12,48 @@ import ( "github.com/spiffe/spiffe-helper/pkg/sidecar" ) -func main() { - // 0. Load configuration - // 1. Create Sidecar - // 2. Run Sidecar's Daemon +const ( + daemonModeFlagName = "daemon-mode" +) +func main() { configFile := flag.String("config", "helper.conf", " Configuration file path") - exitWhenReady := flag.Bool("exitWhenReady", false, "Exit once the requested objects are retrieved") + 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") - log.Infof("Using configuration file: %q\n", *configFile) - if err := startSidecar(*configFile, *exitWhenReady, log); err != nil { - log.WithError(err).Error("Exiting due this error") + if err := startSidecar(*configFile, *daemonModeFlag, log); err != nil { + log.WithError(err).Errorf("Error starting spiffe-helper") os.Exit(1) } log.Infof("Exiting") + os.Exit(0) } -func startSidecar(configPath string, exitWhenReady bool, log logrus.FieldLogger) error { +func startSidecar(configFile string, daemonModeFlag bool, log logrus.FieldLogger) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() - hclConfig, err := config.ParseConfig(configPath) + log.Infof("Using configuration file: %q", configFile) + hclConfig, err := config.ParseConfig(configFile) if err != nil { - return fmt.Errorf("failed to parse %q: %w", configPath, err) + return fmt.Errorf("failed to parse %q: %w", configFile, err) } - if err := config.ValidateConfig(hclConfig, exitWhenReady, log); err != nil { + hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName) + + if err := hclConfig.ValidateConfig(log); err != nil { return fmt.Errorf("invalid configuration: %w", err) } sidecarConfig := config.NewSidecarConfig(hclConfig, log) spiffeSidecar := sidecar.New(sidecarConfig) + if !*hclConfig.DaemonMode { + log.Info("Daemon mode disabled") + return spiffeSidecar.Run(ctx) + } + + log.Info("Launching daemon") return spiffeSidecar.RunDaemon(ctx) } diff --git a/examples/k8s/helper-no-daemon.yaml b/examples/k8s/helper-no-daemon.yaml new file mode 100644 index 00000000..578247af --- /dev/null +++ b/examples/k8s/helper-no-daemon.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: spiffe-helper +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spiffe-helper +data: + helper.conf: | + cmd = "" + cmd_args = "" + cert_dir = "" + renew_signal = "" + svid_file_name = "tls.crt" + svid_key_file_name = "tls.key" + svid_bundle_file_name = "ca.pem" + jwt_bundle_file_name = "cert.jwt" + jwt_svids = [{jwt_audience="test", jwt_svid_file_name="jwt_svid.token"}] + daemon_mode = false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spiffe-helper + labels: + app: spiffe-helper +spec: + selector: + matchLabels: + app: spiffe-helper + template: + metadata: + labels: + app: spiffe-helper + spec: + serviceAccountName: spiffe-helper + containers: + - name: spiffe-helper + image: ghcr.io/spiffe/spiffe-helper:devel + args: ["-config", "config/helper.conf"] + volumeMounts: + - name: spire-agent-socket + mountPath: /tmp/spire-agent/public/ + readOnly: true + - name: helper-config + mountPath: /config + volumes: + - name: spire-agent-socket + hostPath: + path: /run/spire/agent-sockets + type: Directory + - name: helper-config + configMap: + name: spiffe-helper diff --git a/go.mod b/go.mod index 2e4736db..ef4b6abb 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,25 @@ require ( github.com/stretchr/testify v1.9.0 golang.org/x/sys v0.24.0 google.golang.org/grpc v1.65.0 + k8s.io/apimachinery v0.30.1 + k8s.io/client-go v0.30.1 ) require ( github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) require ( diff --git a/go.sum b/go.sum index 36fbe8d9..8962755f 100644 --- a/go.sum +++ b/go.sum @@ -7,37 +7,67 @@ github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -45,9 +75,13 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -73,9 +107,14 @@ golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= @@ -85,6 +124,25 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= +k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/helper.conf b/helper.conf index 380c1fc5..ce937af2 100644 --- a/helper.conf +++ b/helper.conf @@ -9,4 +9,4 @@ svid_bundle_file_name = "svid_bundle.pem" # Add CA with intermediates into Bundle file instead of SVID file, # it is the expected behavior in some scenarios like MySQL. # Default: false -# add_intermediates_to_bundle = false \ No newline at end of file +# add_intermediates_to_bundle = false diff --git a/pkg/sidecar/sidecar.go b/pkg/sidecar/sidecar.go index fa70e5d7..c1799ec3 100644 --- a/pkg/sidecar/sidecar.go +++ b/pkg/sidecar/sidecar.go @@ -23,6 +23,7 @@ import ( // implements the interface Sidecar type Sidecar struct { config *Config + client *workloadapi.Client jwtSource *workloadapi.JWTSource processRunning int32 process *os.Process @@ -44,38 +45,41 @@ func New(config *Config) *Sidecar { func (s *Sidecar) RunDaemon(ctx context.Context) error { var wg sync.WaitGroup - s.config.Log.WithField("agent_address", s.config.AgentAddress).Info("Connecting to agent") + if err := s.setupClients(ctx); err != nil { + return err + } + if s.client != nil { + defer s.client.Close() + } + if s.jwtSource != nil { + defer s.jwtSource.Close() + } - if s.config.SVIDFileName != "" && s.config.SVIDKeyFileName != "" && s.config.SVIDBundleFileName != "" { + if s.x509Enabled() { + s.config.Log.Info("Watching for X509 Context") wg.Add(1) go func() { defer wg.Done() - err := workloadapi.WatchX509Context(ctx, &x509Watcher{sidecar: s}, s.getWorkloadAPIAdress()) + err := s.client.WatchX509Context(ctx, &x509Watcher{sidecar: s}) if err != nil && status.Code(err) != codes.Canceled { s.config.Log.Fatalf("Error watching X.509 context: %v", err) } }() } - if s.config.JWTBundleFilename != "" { + if s.jwtBundleEnabled() { + s.config.Log.Info("Watching for JWT Bundles") wg.Add(1) go func() { defer wg.Done() - err := workloadapi.WatchJWTBundles(ctx, &JWTBundlesWatcher{sidecar: s}, s.getWorkloadAPIAdress()) + err := s.client.WatchJWTBundles(ctx, &JWTBundlesWatcher{sidecar: s}) if err != nil && status.Code(err) != codes.Canceled { s.config.Log.Fatalf("Error watching JWT bundle updates: %v", err) } }() } - if len(s.config.JWTSVIDs) > 0 { - jwtSource, err := workloadapi.NewJWTSource(ctx, workloadapi.WithClientOptions(s.getWorkloadAPIAdress())) - if err != nil { - s.config.Log.Fatalf("Error watching JWT svid updates: %v", err) - } - s.jwtSource = jwtSource - defer s.jwtSource.Close() - + if s.jwtSVIDsEnabled() { for _, jwtConfig := range s.config.JWTSVIDs { jwtConfig := jwtConfig wg.Add(1) @@ -91,11 +95,72 @@ func (s *Sidecar) RunDaemon(ctx context.Context) error { return nil } +func (s *Sidecar) Run(ctx context.Context) error { + if err := s.setupClients(ctx); err != nil { + return err + } + if s.client != nil { + defer s.client.Close() + } + if s.jwtSource != nil { + defer s.jwtSource.Close() + } + + if s.x509Enabled() { + s.config.Log.Debug("Fetching x509 certificates") + if err := s.fetchAndWriteX509Context(ctx); err != nil { + s.config.Log.WithError(err).Error("Error fetching x509 certificates") + return err + } + s.config.Log.Info("Successfully fetched x509 certificates") + } + + if s.jwtBundleEnabled() { + s.config.Log.Debug("Fetching JWT Bundle") + if err := s.fetchAndWriteJWTBundle(ctx); err != nil { + s.config.Log.WithError(err).Error("Error fetching JWT bundle") + return err + } + s.config.Log.Info("Successfully fetched JWT bundle") + } + + if s.jwtSVIDsEnabled() { + s.config.Log.Debug("Fetching JWT SVIDs") + if err := s.fetchAndWriteJWTSVIDs(ctx); err != nil { + s.config.Log.WithError(err).Error("Error fetching JWT SVIDs") + return err + } + s.config.Log.Info("Successfully fetched JWT SVIDs") + } + + return nil +} + // CertReadyChan returns a channel to know when the certificates are ready func (s *Sidecar) CertReadyChan() <-chan struct{} { return s.certReadyChan } +func (s *Sidecar) setupClients(ctx context.Context) error { + if s.x509Enabled() || s.jwtBundleEnabled() { + client, err := workloadapi.New(ctx, s.getWorkloadAPIAdress()) + if err != nil { + return err + } + s.client = client + } + + if s.jwtSVIDsEnabled() { + jwtSource, err := workloadapi.NewJWTSource(ctx, workloadapi.WithClientOptions(s.getWorkloadAPIAdress())) + if err != nil { + return err + } + s.jwtSource = jwtSource + } + + return nil +} + // 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") @@ -111,10 +176,6 @@ func (s *Sidecar) updateCertificates(svidResponse *workloadapi.X509Context) { } } - if s.config.ExitWhenReady { - os.Exit(0) - } - select { case s.certReadyChan <- struct{}{}: default: @@ -253,6 +314,18 @@ func (s *Sidecar) updateJWTSVID(ctx context.Context, jwtAudience string, jwtSVID } } +func (s *Sidecar) x509Enabled() bool { + return s.config.SVIDFileName != "" && s.config.SVIDKeyFileName != "" && s.config.SVIDBundleFileName != "" +} + +func (s *Sidecar) jwtBundleEnabled() bool { + return s.config.JWTBundleFilename != "" +} + +func (s *Sidecar) jwtSVIDsEnabled() bool { + return len(s.config.JWTSVIDs) > 0 +} + // x509Watcher is a sample implementation of the workload.X509SVIDWatcher interface type x509Watcher struct { sidecar *Sidecar diff --git a/pkg/sidecar/workloadapi.go b/pkg/sidecar/workloadapi.go new file mode 100644 index 00000000..bef682ef --- /dev/null +++ b/pkg/sidecar/workloadapi.go @@ -0,0 +1,88 @@ +package sidecar + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/spiffe/go-spiffe/v2/workloadapi" + "github.com/spiffe/spiffe-helper/pkg/disk" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" +) + +var ( + backoff = wait.Backoff{ + Steps: 10, + Duration: 10 * time.Millisecond, + Factor: 5.0, + Jitter: 0.1, + } +) + +func (s *Sidecar) fetchAndWriteX509Context(ctx context.Context) error { + var x509Context *workloadapi.X509Context + + // Retry PermissionDenied errors. We may get a few of these before the cert is minted + err := retry.OnError(backoff, func(err error) bool { + return status.Code(err) == codes.PermissionDenied + }, func() (err error) { + x509Context, err = s.client.FetchX509Context(ctx) + return err + }) + if err != nil { + return err + } + + return disk.WriteX509Context(x509Context, s.config.AddIntermediatesToBundle, s.config.IncludeFederatedDomains, s.config.CertDir, s.config.SVIDFileName, s.config.SVIDKeyFileName, s.config.SVIDBundleFileName) +} + +func (s *Sidecar) fetchAndWriteJWTBundle(ctx context.Context) error { + var jwtBundleSet *jwtbundle.Set + + // Retry PermissionDenied errors. We may get a few of these before the cert is minted + err := retry.OnError(backoff, func(err error) bool { + return status.Code(err) == codes.PermissionDenied + }, func() (err error) { + jwtBundleSet, err = s.client.FetchJWTBundles(ctx) + return err + }) + if err != nil { + return err + } + + return disk.WriteJWTBundleSet(jwtBundleSet, s.config.CertDir, s.config.JWTBundleFilename) +} + +func (s *Sidecar) fetchAndWriteJWTSVIDs(ctx context.Context) error { + var errs []error + for _, jwtConfig := range s.config.JWTSVIDs { + if err := s.fetchAndWriteJWTSVID(ctx, jwtConfig.JWTAudience, jwtConfig.JWTSVIDFilename); err != nil { + errs = append(errs, fmt.Errorf("unable to fetch JWT SVID for audience %q: %w", jwtConfig.JWTAudience, err)) + } + } + + return errors.Join(errs...) +} + +func (s *Sidecar) fetchAndWriteJWTSVID(ctx context.Context, audience, jwtSVIDFilename string) error { + var jwtSVID *jwtsvid.SVID + + // Retry PermissionDenied errors. We may get a few of these before the cert is minted + err := retry.OnError(backoff, func(err error) bool { + return status.Code(err) == codes.PermissionDenied + }, func() (err error) { + jwtSVID, err = s.jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience}) + return err + }) + if err != nil { + return err + } + + return disk.WriteJWTSVID(jwtSVID, s.config.CertDir, jwtSVIDFilename) +}