Skip to content

Commit

Permalink
Add ability to configure file permissions of written files (closes #183)
Browse files Browse the repository at this point in the history
Signed-off-by: Keegan Witt <[email protected]>
  • Loading branch information
keeganwitt committed Sep 11, 2024
1 parent 089a13d commit d7036a4
Show file tree
Hide file tree
Showing 12 changed files with 128 additions and 28 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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
Expand Down
65 changes: 64 additions & 1 deletion cmd/spiffe-helper/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ package config
import (
"errors"
"flag"
"math"
"os"
"strconv"

"github.com/hashicorp/hcl"
"github.com/sirupsen/logrus"
"github.com/spiffe/spiffe-helper/pkg/sidecar"
)

const (
defaultAgentAddress = "/tmp/spire-agent/public/api.sock"
defaultAgentAddress = "/tmp/spire-agent/public/api.sock"
defaultCertFileMode = 0644
defaultKeyFileMode = 0600
defaultJwtBundleFileMode = 0600
defaultJwtSvidFileMode = 0600
)

type Config struct {
Expand All @@ -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"`
Expand Down Expand Up @@ -164,16 +174,69 @@ func (c *Config) ValidateConfig(log logrus.FieldLogger) error {
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")
}

if c.CertFileMode == "" {
c.CertFileMode = strconv.FormatUint(uint64(defaultCertFileMode), 8)
}
if c.KeyFileMode == "" {
c.KeyFileMode = strconv.FormatUint(uint64(defaultKeyFileMode), 8)
}
if c.JwtBundleFileMode == "" {
c.JwtBundleFileMode = strconv.FormatUint(uint64(defaultJwtBundleFileMode), 8)
}
if c.JwtSvidFileMode == "" {
c.JwtSvidFileMode = strconv.FormatUint(uint64(defaultJwtSvidFileMode), 8)
}

return nil
}

func NewSidecarConfig(config *Config, log logrus.FieldLogger) *sidecar.Config {
certFileMode := os.FileMode(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 := os.FileMode(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 := os.FileMode(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 := os.FileMode(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,
Expand Down
8 changes: 8 additions & 0 deletions cmd/spiffe-helper/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions cmd/spiffe-helper/config/testdata/helper.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 7 additions & 6 deletions pkg/disk/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path"

Expand All @@ -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() {
Expand All @@ -25,27 +26,27 @@ 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))
}

return errors.Join(errs...)
}

// 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
}

filePath := path.Join(dir, filename)

return os.WriteFile(filePath, file, 0600)
return os.WriteFile(filePath, file, fileMode)
}
7 changes: 5 additions & 2 deletions pkg/disk/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/rand"
"crypto/rsa"
"fmt"
"io/fs"
"os"
"path"
"testing"
Expand All @@ -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) {
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down
18 changes: 7 additions & 11 deletions pkg/disk/x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion pkg/disk/x509_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package disk

import (
"io/fs"
"path"
"testing"

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions pkg/sidecar/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package sidecar

import (
"io/fs"

"github.com/sirupsen/logrus"
)

Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions pkg/sidecar/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/sidecar/sidecar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto"
"crypto/x509"
"os"
"path"
"testing"
"time"
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit d7036a4

Please sign in to comment.