From 465e7c21472aeb3b42762cc6302156e8de473fb9 Mon Sep 17 00:00:00 2001 From: Keegan Witt Date: Tue, 10 Sep 2024 23:00:51 -0400 Subject: [PATCH] Add ability to configure file permissions of written files (closes #183) Signed-off-by: Keegan Witt --- README.md | 9 +++- cmd/spiffe-helper/config/config.go | 52 ++++++++++++++++++- cmd/spiffe-helper/config/config_test.go | 8 +++ cmd/spiffe-helper/config/testdata/helper.conf | 4 ++ pkg/disk/json.go | 13 ++--- pkg/disk/json_test.go | 7 ++- pkg/disk/x509.go | 18 +++---- pkg/disk/x509_test.go | 5 +- pkg/sidecar/config.go | 10 ++++ pkg/sidecar/sidecar.go | 6 +-- pkg/sidecar/sidecar_test.go | 5 ++ pkg/sidecar/workloadapi.go | 6 +-- 12 files changed, 115 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6c675f3e..8affd80c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,10 @@ The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted f | `jwt_svids` | An array with the audience and file name to store the JWT SVIDs. File is Base64-encoded string). | `[{jwt_audience="your-audience", 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 example ``` @@ -47,6 +50,10 @@ svid_key_file_name = "svid_key.pem" svid_bundle_file_name = "svid_bundle.pem" jwt_svids = [{jwt_audience="your-audience", jwt_svid_file_name="jwt_svid.token"}] jwt_bundle_file_name = "bundle.json" +cert_file_mode = "0444" +key_file_mode = "0444" +jwt_bundle_file_mode = "0444" +jwt_svid_file_mode = "0444" ``` ### Windows example diff --git a/cmd/spiffe-helper/config/config.go b/cmd/spiffe-helper/config/config.go index fbc3c3bd..570c8f47 100644 --- a/cmd/spiffe-helper/config/config.go +++ b/cmd/spiffe-helper/config/config.go @@ -3,7 +3,9 @@ package config import ( "errors" "flag" + "math" "os" + "strconv" "github.com/hashicorp/hcl" "github.com/sirupsen/logrus" @@ -11,7 +13,11 @@ import ( ) const ( - defaultAgentAddress = "/tmp/spire-agent/public/api.sock" + defaultAgentAddress = "/tmp/spire-agent/public/api.sock" + defaultCertFileMode = os.FileMode(0644) + defaultKeyFileMode = os.FileMode(0600) + defaultJwtBundleFileMode = os.FileMode(0600) + defaultJwtSvidFileMode = os.FileMode(0600) ) type Config struct { @@ -24,6 +30,10 @@ type Config struct { CmdArgsDeprecated string `hcl:"cmdArgs"` CertDir string `hcl:"cert_dir"` CertDirDeprecated string `hcl:"certDir"` + CertFileMode string `hcl:"cert_file_mode"` + KeyFileMode string `hcl:"key_file_mode"` + JwtBundleFileMode string `hcl:"jwt_bundle_file_mode"` + JwtSvidFileMode string `hcl:"jwt_svid_file_mode"` IncludeFederatedDomains bool `hcl:"include_federated_domains"` RenewSignal string `hcl:"renew_signal"` RenewSignalDeprecated string `hcl:"renewSignal"` @@ -168,12 +178,52 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error { } func NewSidecarConfig(config *Config, log logrus.FieldLogger) *sidecar.Config { + certFileMode := defaultCertFileMode + if config.CertFileMode != "" { + parsedCertFileMode, err := strconv.ParseUint(config.CertFileMode, 8, 32) + if err != nil || parsedCertFileMode > math.MaxUint32 { + log.WithError(err).Error("failed to parse file mode, using default") + } else { + certFileMode = os.FileMode(parsedCertFileMode) //nolint:gosec,G115 + } + } + keyFileMode := defaultKeyFileMode + if config.KeyFileMode != "" { + parsedKeyFileMode, err := strconv.ParseUint(config.KeyFileMode, 8, 32) + if err != nil || parsedKeyFileMode > math.MaxUint32 { + log.WithError(err).Error("failed to parse file mode, using default") + } else { + certFileMode = os.FileMode(parsedKeyFileMode) //nolint:gosec,G115 + } + } + jwtBundleFileMode := defaultJwtBundleFileMode + if config.JwtBundleFileMode != "" { + parsedJwtBundleFileMode, err := strconv.ParseUint(config.JwtBundleFileMode, 8, 32) + if err != nil || parsedJwtBundleFileMode > math.MaxUint32 { + log.WithError(err).Error("failed to parse file mode, using default") + } else { + certFileMode = os.FileMode(parsedJwtBundleFileMode) //nolint:gosec,G115 + } + } + jwtSvidFileMode := defaultJwtSvidFileMode + if config.JwtSvidFileMode != "" { + parsedJwtSvidFileMode, err := strconv.ParseUint(config.JwtSvidFileMode, 8, 32) + if err != nil || parsedJwtSvidFileMode > math.MaxUint32 { + log.WithError(err).Error("failed to parse file mode, using default") + } else { + certFileMode = os.FileMode(parsedJwtSvidFileMode) //nolint:gosec,G115 + } + } sidecarConfig := &sidecar.Config{ AddIntermediatesToBundle: config.AddIntermediatesToBundle, AgentAddress: config.AgentAddress, Cmd: config.Cmd, CmdArgs: config.CmdArgs, CertDir: config.CertDir, + CertFileMode: certFileMode, + KeyFileMode: keyFileMode, + JwtBundleFileMode: jwtBundleFileMode, + JwtSvidFileMode: jwtSvidFileMode, IncludeFederatedDomains: config.IncludeFederatedDomains, JWTBundleFilename: config.JWTBundleFilename, Log: log, diff --git a/cmd/spiffe-helper/config/config_test.go b/cmd/spiffe-helper/config/config_test.go index 73950a8f..f8a5add0 100644 --- a/cmd/spiffe-helper/config/config_test.go +++ b/cmd/spiffe-helper/config/config_test.go @@ -31,6 +31,10 @@ func TestParseConfig(t *testing.T) { expectedJWTSVIDFileName := "jwt_svid.token" expectedJWTBundleFileName := "jwt_bundle.json" expectedJWTAudience := "your-audience" + expectedCertFileMode := "0444" + expectedKeyFileMode := "0444" + expectedJwtBundleFileMode := "0444" + expectedJwtSvidFileMode := "0444" assert.Equal(t, expectedAgentAddress, c.AgentAddress) assert.Equal(t, expectedCmd, c.Cmd) @@ -44,6 +48,10 @@ func TestParseConfig(t *testing.T) { assert.Equal(t, expectedJWTBundleFileName, c.JWTBundleFilename) assert.Equal(t, expectedJWTAudience, c.JWTSVIDs[0].JWTAudience) assert.True(t, c.AddIntermediatesToBundle) + assert.Equal(t, expectedCertFileMode, c.CertFileMode) + assert.Equal(t, expectedKeyFileMode, c.KeyFileMode) + assert.Equal(t, expectedJwtBundleFileMode, c.JwtBundleFileMode) + assert.Equal(t, expectedJwtSvidFileMode, c.JwtSvidFileMode) } func TestValidateConfig(t *testing.T) { diff --git a/cmd/spiffe-helper/config/testdata/helper.conf b/cmd/spiffe-helper/config/testdata/helper.conf index ccd57742..ae881221 100644 --- a/cmd/spiffe-helper/config/testdata/helper.conf +++ b/cmd/spiffe-helper/config/testdata/helper.conf @@ -2,6 +2,10 @@ agent_address = "/tmp/spire-agent/public/api.sock" cmd = "hot-restarter.py" cmd_args = "start_envoy.sh" cert_dir = "certs" +cert_file_mode = "0444" +key_file_mode = "0444" +jwt_bundle_file_mode = "0444" +jwt_svid_file_mode = "0444" renew_signal = "SIGHUP" svid_file_name = "svid.pem" svid_key_file_name = "svid_key.pem" diff --git a/pkg/disk/json.go b/pkg/disk/json.go index d882c7d6..0aca1a59 100644 --- a/pkg/disk/json.go +++ b/pkg/disk/json.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "os" "path" @@ -13,7 +14,7 @@ import ( ) // WriteJWTBundleSet write the given JWT bundles to disk -func WriteJWTBundleSet(jwkSet *jwtbundle.Set, dir string, jwtBundleFilename string) error { +func WriteJWTBundleSet(jwkSet *jwtbundle.Set, dir string, jwtBundleFilename string, jwtBundleFileMode fs.FileMode) error { var errs []error bundles := make(map[string]interface{}) for _, bundle := range jwkSet.Bundles() { @@ -25,7 +26,7 @@ func WriteJWTBundleSet(jwkSet *jwtbundle.Set, dir string, jwtBundleFilename stri bundles[bundle.TrustDomain().Name()] = base64.StdEncoding.EncodeToString(bytes) } - if err := writeJSON(bundles, dir, jwtBundleFilename); err != nil { + if err := writeJSON(bundles, dir, jwtBundleFilename, jwtBundleFileMode); err != nil { errs = append(errs, fmt.Errorf("unable to write JSON file: %w", err)) } @@ -33,13 +34,13 @@ func WriteJWTBundleSet(jwkSet *jwtbundle.Set, dir string, jwtBundleFilename stri } // WriteJWTBundle write the given JWT SVID to disk -func WriteJWTSVID(jwtSVID *jwtsvid.SVID, dir, jwtSVIDFilename string) error { +func WriteJWTSVID(jwtSVID *jwtsvid.SVID, dir, jwtSVIDFilename string, jwtSvidFileMode fs.FileMode) error { filePath := path.Join(dir, jwtSVIDFilename) - return os.WriteFile(filePath, []byte(jwtSVID.Marshal()), 0600) + return os.WriteFile(filePath, []byte(jwtSVID.Marshal()), jwtSvidFileMode) } -func writeJSON(certs map[string]any, dir, filename string) error { +func writeJSON(certs map[string]any, dir, filename string, fileMode fs.FileMode) error { file, err := json.Marshal(certs) if err != nil { return err @@ -47,5 +48,5 @@ func writeJSON(certs map[string]any, dir, filename string) error { filePath := path.Join(dir, filename) - return os.WriteFile(filePath, file, 0600) + return os.WriteFile(filePath, file, fileMode) } diff --git a/pkg/disk/json_test.go b/pkg/disk/json_test.go index 9f58b6a3..19f300b6 100644 --- a/pkg/disk/json_test.go +++ b/pkg/disk/json_test.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "crypto/rsa" "fmt" + "io/fs" "os" "path" "testing" @@ -24,6 +25,8 @@ import ( const ( jwtBundleFilename = "jwt_bundle.json" jwtSVIDFilename = "jwt.json" + jwtBundleFileMode = fs.FileMode(0600) + jwtSvidFileMode = fs.FileMode(0600) ) func TestWriteJWTBundleSet(t *testing.T) { @@ -34,7 +37,7 @@ func TestWriteJWTBundleSet(t *testing.T) { tempDir := t.TempDir() - err := WriteJWTBundleSet(jwtBundleSet, tempDir, jwtBundleFilename) + err := WriteJWTBundleSet(jwtBundleSet, tempDir, jwtBundleFilename, jwtBundleFileMode) require.NoError(t, err) actualJWTBundle, err := jwtbundle.Load(td, path.Join(tempDir, jwtBundleFilename)) @@ -64,7 +67,7 @@ func TestWriteJWTSVID(t *testing.T) { // Write to disk tempDir := t.TempDir() - err = WriteJWTSVID(jwtSVID, tempDir, jwtSVIDFilename) + err = WriteJWTSVID(jwtSVID, tempDir, jwtSVIDFilename, jwtSvidFileMode) require.NoError(t, err) // Read back and check its the same diff --git a/pkg/disk/x509.go b/pkg/disk/x509.go index 013fb810..856221c6 100644 --- a/pkg/disk/x509.go +++ b/pkg/disk/x509.go @@ -4,22 +4,18 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "io/fs" "os" "path" "github.com/spiffe/go-spiffe/v2/workloadapi" ) -const ( - certsFileMode = os.FileMode(0644) - keyFileMode = os.FileMode(0600) -) - // WriteX509Context takes a X509Context, representing a svid message from // the Workload API, and calls writeCerts and writeKey to write to disk // the svid, key and bundle of certificates. // It is possible to change output setting `addIntermediatesToBundle` as true. -func WriteX509Context(x509Context *workloadapi.X509Context, addIntermediatesToBundle, includeFederatedDomains bool, certDir, svidFilename, svidKeyFilename, svidBundleFilename string) error { +func WriteX509Context(x509Context *workloadapi.X509Context, addIntermediatesToBundle, includeFederatedDomains bool, certDir, svidFilename, svidKeyFilename, svidBundleFilename string, certFileMode, keyFileMode fs.FileMode) error { svidFile := path.Join(certDir, svidFilename) svidKeyFile := path.Join(certDir, svidKeyFilename) svidBundleFile := path.Join(certDir, svidBundleFilename) @@ -58,20 +54,20 @@ func WriteX509Context(x509Context *workloadapi.X509Context, addIntermediatesToBu } // Write cert, key, and bundle to disk - if err := writeCerts(svidFile, certs); err != nil { + if err := writeCerts(svidFile, certs, certFileMode); err != nil { return err } - if err := writeKey(svidKeyFile, privateKey); err != nil { + if err := writeKey(svidKeyFile, privateKey, keyFileMode); err != nil { return err } - return writeCerts(svidBundleFile, bundles) + return writeCerts(svidBundleFile, bundles, certFileMode) } // writeCerts takes an array of certificates, // and encodes them as PEM blocks, writing them to file -func writeCerts(file string, certs []*x509.Certificate) error { +func writeCerts(file string, certs []*x509.Certificate, certsFileMode fs.FileMode) error { var pemData []byte for _, cert := range certs { b := &pem.Block{ @@ -86,7 +82,7 @@ func writeCerts(file string, certs []*x509.Certificate) error { // writeKey takes a private key as a slice of bytes, // formats as PEM, and writes it to file -func writeKey(file string, data []byte) error { +func writeKey(file string, data []byte, keyFileMode fs.FileMode) error { b := &pem.Block{ Type: "PRIVATE KEY", Bytes: data, diff --git a/pkg/disk/x509_test.go b/pkg/disk/x509_test.go index ae7bf697..efbaa4e6 100644 --- a/pkg/disk/x509_test.go +++ b/pkg/disk/x509_test.go @@ -1,6 +1,7 @@ package disk import ( + "io/fs" "path" "testing" @@ -17,6 +18,8 @@ const ( svidFilename = "svid.pem" svidKeyFilename = "svid_key.pem" svidBundleFilename = "svid_bundle.pem" + certFileMode = fs.FileMode(0600) + keyFileMode = fs.FileMode(0600) ) func TestWriteX509Context(t *testing.T) { @@ -131,7 +134,7 @@ func TestWriteX509Context(t *testing.T) { } } - err = WriteX509Context(x509Context, test.intermediateInBundle, test.includeFederatedDomains, tempDir, svidFilename, svidKeyFilename, svidBundleFilename) + err = WriteX509Context(x509Context, test.intermediateInBundle, test.includeFederatedDomains, tempDir, svidFilename, svidKeyFilename, svidBundleFilename, certFileMode, keyFileMode) require.NoError(t, err) // Load certificates from disk and validate it is expected diff --git a/pkg/sidecar/config.go b/pkg/sidecar/config.go index 9d08b3b7..66526c99 100644 --- a/pkg/sidecar/config.go +++ b/pkg/sidecar/config.go @@ -1,6 +1,8 @@ package sidecar import ( + "io/fs" + "github.com/sirupsen/logrus" ) @@ -24,6 +26,14 @@ type Config struct { // If true, fetche x509 certificate and then exit(0). ExitWhenReady bool + CertFileMode fs.FileMode + + KeyFileMode fs.FileMode + + JwtBundleFileMode fs.FileMode + + JwtSvidFileMode fs.FileMode + // If true, includes trust domains from federated servers in the CA bundle. IncludeFederatedDomains bool diff --git a/pkg/sidecar/sidecar.go b/pkg/sidecar/sidecar.go index c1799ec3..8d9c4157 100644 --- a/pkg/sidecar/sidecar.go +++ b/pkg/sidecar/sidecar.go @@ -164,7 +164,7 @@ 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") - if err := disk.WriteX509Context(svidResponse, s.config.AddIntermediatesToBundle, s.config.IncludeFederatedDomains, s.config.CertDir, s.config.SVIDFileName, s.config.SVIDKeyFileName, s.config.SVIDBundleFileName); err != nil { + 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") return } @@ -275,7 +275,7 @@ func (s *Sidecar) performJWTSVIDUpdate(ctx context.Context, jwtAudience string, return nil, err } - if err = disk.WriteJWTSVID(jwtSVID, s.config.CertDir, jwtSVIDFilename); err != nil { + 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) return nil, err } @@ -372,7 +372,7 @@ type JWTBundlesWatcher struct { // OnJWTBundlesUpdate is ran every time a bundle is updated func (w JWTBundlesWatcher) OnJWTBundlesUpdate(jwkSet *jwtbundle.Set) { w.sidecar.config.Log.Debug("Updating JWT bundle") - if err := disk.WriteJWTBundleSet(jwkSet, w.sidecar.config.CertDir, w.sidecar.config.JWTBundleFilename); err != nil { + 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) return } diff --git a/pkg/sidecar/sidecar_test.go b/pkg/sidecar/sidecar_test.go index 736ab450..554ef1fb 100644 --- a/pkg/sidecar/sidecar_test.go +++ b/pkg/sidecar/sidecar_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/x509" + "os" "path" "testing" "time" @@ -82,6 +83,10 @@ func TestSidecar_RunDaemon(t *testing.T) { SVIDKeyFileName: "svid_key.pem", SVIDBundleFileName: "svid_bundle.pem", Log: log, + CertFileMode: os.FileMode(0644), + KeyFileMode: os.FileMode(0600), + JwtBundleFileMode: os.FileMode(0600), + JwtSvidFileMode: os.FileMode(0600), } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) diff --git a/pkg/sidecar/workloadapi.go b/pkg/sidecar/workloadapi.go index bef682ef..357f8eb1 100644 --- a/pkg/sidecar/workloadapi.go +++ b/pkg/sidecar/workloadapi.go @@ -39,7 +39,7 @@ func (s *Sidecar) fetchAndWriteX509Context(ctx context.Context) error { return err } - return disk.WriteX509Context(x509Context, s.config.AddIntermediatesToBundle, s.config.IncludeFederatedDomains, s.config.CertDir, s.config.SVIDFileName, s.config.SVIDKeyFileName, s.config.SVIDBundleFileName) + return disk.WriteX509Context(x509Context, s.config.AddIntermediatesToBundle, s.config.IncludeFederatedDomains, s.config.CertDir, s.config.SVIDFileName, s.config.SVIDKeyFileName, s.config.SVIDBundleFileName, s.config.CertFileMode, s.config.KeyFileMode) } func (s *Sidecar) fetchAndWriteJWTBundle(ctx context.Context) error { @@ -56,7 +56,7 @@ func (s *Sidecar) fetchAndWriteJWTBundle(ctx context.Context) error { return err } - return disk.WriteJWTBundleSet(jwtBundleSet, s.config.CertDir, s.config.JWTBundleFilename) + return disk.WriteJWTBundleSet(jwtBundleSet, s.config.CertDir, s.config.JWTBundleFilename, s.config.JwtBundleFileMode) } func (s *Sidecar) fetchAndWriteJWTSVIDs(ctx context.Context) error { @@ -84,5 +84,5 @@ func (s *Sidecar) fetchAndWriteJWTSVID(ctx context.Context, audience, jwtSVIDFil return err } - return disk.WriteJWTSVID(jwtSVID, s.config.CertDir, jwtSVIDFilename) + return disk.WriteJWTSVID(jwtSVID, s.config.CertDir, jwtSVIDFilename, s.config.JwtSvidFileMode) }