Skip to content

Commit

Permalink
Add merge intermediate certificates mode (#19)
Browse files Browse the repository at this point in the history
* add option to merge intermediate certificates into Bundles file

Signed-off-by: Marcos Yacob <marcos@scytale.io>
  • Loading branch information
MarcosDY authored Mar 2, 2020
1 parent 9618eae commit 71ce388
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 196 deletions.
2 changes: 1 addition & 1 deletion .go-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.13.7
1.13.8
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ If `-config` is not specified, the default value `helper.conf` is assumed.
The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted file that defines the following configurations:

|Configuration | Description | Example Value |
|---------------------|------------------------------------------------------------------------------------------------| ------------- |
|`agentAddress` | Socket address of SPIRE Agent. | `"/tmp/agent.sock"` |
|`cmd` | The path to the process to launch. | `"ghostunnel"` |
|`cmdArgs` | 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"` |
|`certDir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` |
|`renewSignal` | The signal that the process to be launched expects to reload the certificates. | `"SIGUSR1"` |
|`svidFileName` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` |
|`svidKeyFileName` | File name to be used to store the X.509 SVID private key and public certificate in PEM format. | `"svid_key.pem"` |
|`svidBundleFileName` | File name to be used to store the X.509 SVID Bundle in PEM format. | `"svid_bundle.pem"` |
|--------------------------|------------------------------------------------------------------------------------------------| ------------- |
|`agentAddress` | Socket address of SPIRE Agent. | `"/tmp/agent.sock"` |
|`cmd` | The path to the process to launch. | `"ghostunnel"` |
|`cmdArgs` | 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"` |
|`certDir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` |
|`addIntermediatesToBundle`| Add intermediate certificates into Bundle file instead of SVID file. | `true` |
|`renewSignal` | The signal that the process to be launched expects to reload the certificates. | `"SIGUSR1"` |
|`svidFileName` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` |
|`svidKeyFileName` | File name to be used to store the X.509 SVID private key and public certificate in PEM format. | `"svid_key.pem"` |
|`svidBundleFileName` | File name to be used to store the X.509 SVID Bundle in PEM format. | `"svid_bundle.pem"` |

#### Configuration example
```
Expand Down
1 change: 1 addition & 0 deletions cmd/spiffe-helper/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ func TestParseConfig(t *testing.T) {
assert.Equal(t, expectedKeyFileName, c.SvidKeyFileName)
assert.Equal(t, expectedSvidBundleFileName, c.SvidBundleFileName)
assert.Equal(t, expectedTimeOut, c.Timeout)
assert.True(t, c.AddIntermediatesToBundle)
}
4 changes: 4 additions & 0 deletions examples/mysql/helper.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ certDir = "/var/lib/mysql"
svidFileName = "server-cert.pem"
svidKeyFileName = "server-key.pem"
svidBundleFileName = "ca.pem"

# MySQL expect intermediate certificates inside `svidBundleFile` file
# instead of svidFile
addIntermediatesToBundle = true
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ github.com/spiffe/go-spiffe v0.0.0-20190922191205-018e7197ed1c h1:wpwh25WjvKF8/+
github.com/spiffe/go-spiffe v0.0.0-20190922191205-018e7197ed1c/go.mod h1:HyNeJnVYkDyQgB2qcSPxVYkAA2F3lQu51bDxNpFcKxY=
github.com/spiffe/spire v0.0.0-20191007205405-52bcf313f4f6 h1:2lLO9rR2rXlfWjeOC5YR5sHMyw5LGwF/mFJpmiZJdK4=
github.com/spiffe/spire v0.0.0-20191007205405-52bcf313f4f6/go.mod h1:Pnjj4CO6q5pS/vsXyqVHs/HNoDXWmn8+jZekriyWPmM=
github.com/spiffe/spire/proto/spire v0.0.0-20190723205943-8d4a2538e330 h1:n5uIC5TcJhMmGGTPcsV9rvZvuncc49xeWxullDtx4fg=
github.com/spiffe/spire/proto/spire v0.0.0-20190723205943-8d4a2538e330/go.mod h1:qNEFMfiHxU6h7JTH8Op7b5aHGRsSwfQXYnPd/F0Hm8c=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand All @@ -210,6 +211,7 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
github.com/uber-go/tally v3.3.12+incompatible/go.mod h1:YDTIBxdXyOU/sCWilKB4bgyufu1cEi0jdVnRdxvjnmU=
github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/zaffka/mongodb-boltdb-mock v0.0.0-20180816124423-49954d88fa3e/go.mod h1:GsDD1qsG+86MeeCG7ndi6Ei3iGthKL3wQ7PTFigDfNY=
github.com/zeebo/errs v1.2.0 h1:Tk8UszIOLEjtx6DWnvfmMJe6N8q7vu03Bj95HMWDUkc=
github.com/zeebo/errs v1.2.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
Expand Down
4 changes: 4 additions & 0 deletions helper.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ renewSignal = "SIGUSR1"
svidFileName = "svid.pem"
svidKeyFileName = "svid_key.pem"
svidBundleFileName = "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
# addIntermediatesToBundle = false
59 changes: 36 additions & 23 deletions pkg/sidecar/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ import (

// Config contains config variables when creating a SPIFFE Sidecar.
type Config struct {
AgentAddress string `hcl:"agentAddress"`
Cmd string `hcl:"cmd"`
CmdArgs string `hcl:"cmdArgs"`
CertDir string `hcl:"certDir"`
SvidFileName string `hcl:"svidFileName"`
SvidKeyFileName string `hcl:"svidKeyFileName"`
SvidBundleFileName string `hcl:"svidBundleFileName"`
RenewSignal string `hcl:"renewSignal"`
Timeout string `hcl:"timeout"`
ReloadExternalProcess func() error
AgentAddress string `hcl:"agentAddress"`
Cmd string `hcl:"cmd"`
CmdArgs string `hcl:"cmdArgs"`
CertDir string `hcl:"certDir"`
// Merge intermediate certificates into Bundle file instead of SVID file,
// it is useful is some scenarios like MySQL,
// where this is the expected format for presented certificates and bundles
AddIntermediatesToBundle bool `hcl:"addIntermediatesToBundle"`
SvidFileName string `hcl:"svidFileName"`
SvidKeyFileName string `hcl:"svidKeyFileName"`
SvidBundleFileName string `hcl:"svidBundleFileName"`
RenewSignal string `hcl:"renewSignal"`
Timeout string `hcl:"timeout"`
ReloadExternalProcess func() error
}

// Sidecar is the component that consumes the Workload API and renews certs
Expand Down Expand Up @@ -229,7 +233,8 @@ func (s *Sidecar) checkProcessExit() {

// dumpBundles takes a X509SVIDResponse, representing a svid message from
// the Workload API, and calls writeCerts and writeKey to write to disk
// the svid, key and bundle of certificates
// the svid, key and bundle of certificates.
// It is possible to change output setting `addIntermediatesToBundle` as true.
func (s *Sidecar) dumpBundles(svidResponse *proto.X509SVIDResponse) error {
// There may be more than one certificate, but we are interested in the first one only
svid := svidResponse.Svids[0]
Expand All @@ -238,33 +243,41 @@ func (s *Sidecar) dumpBundles(svidResponse *proto.X509SVIDResponse) error {
svidKeyFile := path.Join(s.config.CertDir, s.config.SvidKeyFileName)
svidBundleFile := path.Join(s.config.CertDir, s.config.SvidBundleFileName)

err := s.writeCerts(svidFile, svid.X509Svid)
certs, err := x509.ParseCertificates(svid.X509Svid)
if err != nil {
return err
}

err = s.writeKey(svidKeyFile, svid.X509SvidKey)
bundles, err := x509.ParseCertificates(svid.Bundle)
if err != nil {
return err
}

err = s.writeCerts(svidBundleFile, svid.Bundle)
if err != nil {
// Add intermediates into bundles, and remove them from certs
if s.config.AddIntermediatesToBundle {
bundles = append(bundles, certs[1:]...)
certs = []*x509.Certificate{certs[0]}
}

if err := s.writeCerts(svidFile, certs); err != nil {
return err
}

return nil
}
if err := s.writeKey(svidKeyFile, svid.X509SvidKey); err != nil {
return err
}

// writeCerts takes a slice of bytes, which may contain multiple certificates,
// and encodes them as PEM blocks, writing them to file
func (s *Sidecar) writeCerts(file string, data []byte) error {
certs, err := x509.ParseCertificates(data)
if err != nil {
if err := s.writeCerts(svidBundleFile, bundles); err != nil {
return err
}

pemData := []byte{}
return nil
}

// writeCerts takes an array of certificates,
// and encodes them as PEM blocks, writing them to file
func (s *Sidecar) writeCerts(file string, certs []*x509.Certificate) error {
var pemData []byte
for _, cert := range certs {
b := &pem.Block{
Type: "CERTIFICATE",
Expand Down
174 changes: 126 additions & 48 deletions pkg/sidecar/sidecar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sidecar

import (
"context"
"crypto"
"crypto/x509"
"errors"
"io/ioutil"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/spiffe/spiffe-helper/test/util"

"github.com/spiffe/go-spiffe/proto/spiffe/workload"
"github.com/spiffe/go-spiffe/spiffetest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -22,7 +24,37 @@ import (
//running the Sidecar Daemon, when a SVID Response is sent to the
//UpdateChan on the WorkloadAPI client, the PEM files are stored on disk
func TestSidecar_RunDaemon(t *testing.T) {
var wg sync.WaitGroup
// Create root CA
domain1CA := spiffetest.NewCA(t)
// Create an intermediate certificate
domain1Inter := domain1CA.CreateCA()
domain1Bundle := domain1CA.Roots()

// Svid with intermediate
svidChainWithIntermediate, svidKeyWithIntermediate := domain1Inter.CreateX509SVID("spiffe://example.test/workloadWithIntermediate")
require.Len(t, svidChainWithIntermediate, 2)

// Add cert with intermediate into an svid
svidWithIntermediate := []spiffetest.X509SVID{
{
CertChain: svidChainWithIntermediate,
Key: svidKeyWithIntermediate,
},
}

// Concat bundles with intermediate certificate
bundleWithIntermediate := domain1CA.Roots()
bundleWithIntermediate = append(bundleWithIntermediate, svidChainWithIntermediate[1:]...)

// Create a single svid without intermediate
svidChain, svidKey := domain1CA.CreateX509SVID("spiffe://example.test/workload")
require.Len(t, svidChain, 1)
svid := []spiffetest.X509SVID{
{
CertChain: svidChain,
Key: svidKey,
},
}

tmpdir, err := ioutil.TempDir("", "sidecar-run-daemon")
require.NoError(t, err)
Expand All @@ -46,39 +78,113 @@ func TestSidecar_RunDaemon(t *testing.T) {
sidecar := Sidecar{
config: config,
workloadAPIClient: workloadClient,
certReadyChan: make(chan struct{}, 1),
}
defer close(sidecar.certReadyChan)

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
wg.Add(1)
errCh := make(chan error, 1)
go func() {
defer wg.Done()
err = sidecar.RunDaemon(ctx)
require.NoError(t, err)
errCh <- err
}()

x509SvidTestResponse := x509SvidResponse(t)

//send a X509SVIDResponse to Updates channel
updateMockChan <- x509SvidTestResponse
testCases := []struct {
name string
response *spiffetest.X509SVIDResponse
certs []*x509.Certificate
key crypto.Signer
bundle []*x509.Certificate

//send signal to stop the Daemon
cancel()
wg.Wait()
intermediateInBundle bool
}{
{
name: "svid with intermediate",
response: &spiffetest.X509SVIDResponse{
Bundle: domain1Bundle,
SVIDs: svidWithIntermediate,
},
certs: svidChainWithIntermediate,
key: svidKeyWithIntermediate,
bundle: domain1Bundle,
},
{
name: "intermediate in bundle",
response: &spiffetest.X509SVIDResponse{
Bundle: domain1Bundle,
SVIDs: svidWithIntermediate,
},
// Only first certificate is expected
certs: []*x509.Certificate{svidChainWithIntermediate[0]},
key: svidKeyWithIntermediate,
// A concatenation between bundle and intermediate is expected
bundle: bundleWithIntermediate,

intermediateInBundle: true,
},
{
name: "single svid with intermediate in bundle",
response: &spiffetest.X509SVIDResponse{
Bundle: domain1CA.Roots(),
SVIDs: svid,
},
certs: svidChain,
key: svidKey,
bundle: domain1Bundle,
intermediateInBundle: true,
},
{
name: "single svid",
response: &spiffetest.X509SVIDResponse{
Bundle: domain1CA.Roots(),
SVIDs: svid,
},
certs: svidChain,
key: svidKey,
bundle: domain1Bundle,
},
}

//The expected files
svidFile := path.Join(tmpdir, config.SvidFileName)
svidKeyFile := path.Join(tmpdir, config.SvidKeyFileName)
svidBundleFile := path.Join(tmpdir, config.SvidBundleFileName)

if _, err := os.Stat(svidFile); err != nil {
t.Errorf("error %v with file: %v", err, svidFile)
}
if _, err := os.Stat(svidKeyFile); err != nil {
t.Errorf("error %v with file: %v", err, svidKeyFile)
}
if _, err := os.Stat(svidBundleFile); err != nil {
t.Errorf("error %v with file: %v", err, svidBundleFile)
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
sidecar.config.AddIntermediatesToBundle = testCase.intermediateInBundle

// Push response to start updating process
updateMockChan <- testCase.response.ToProto(t)

// Wait until response is processed
select {
case <-sidecar.CertReadyChan():
//continue
case <-ctx.Done():
require.NoError(t, ctx.Err())
}

// Load certificates from disk and validate it is expected
certs, err := util.LoadCertificates(svidFile)
require.NoError(t, err)
require.Equal(t, testCase.certs, certs)

// Load key from disk and validate it is expected
key, err := util.LoadPrivateKey(svidKeyFile)
require.NoError(t, err)
require.Equal(t, testCase.key, key)

// Load bundle from disk and validate it is expected
bundles, err := util.LoadCertificates(svidBundleFile)
require.NoError(t, err)
require.Equal(t, testCase.bundle, bundles)
})
}

cancel()
err = <-errCh
require.NoError(t, err)
}

//Tests that when there is no defaultTimeout in the config, it uses
Expand Down Expand Up @@ -192,31 +298,3 @@ func (m MockWorkloadClient) CurrentSVID() (*workload.X509SVIDResponse, error) {
}
return m.current, nil
}

// creates a X509SVIDResponse reading test certs from files
func x509SvidResponse(t *testing.T) *workload.X509SVIDResponse {
// TODO: refactor to generate certificates instead reading from disk
svid, key, err := util.LoadSVIDFixture()
if err != nil {
t.Errorf("could not load svid fixture: %v", err)
}
ca, err := util.LoadCA()
if err != nil {
t.Errorf("could not load ca: %v", err)
}

keyData, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Errorf("could not marshal private key: %v", err)
}

svidMsg := &workload.X509SVID{
SpiffeId: "spiffe://example.org/foo",
X509Svid: svid.Raw,
X509SvidKey: keyData,
Bundle: ca.Raw,
}
return &workload.X509SVIDResponse{
Svids: []*workload.X509SVID{svidMsg},
}
}
12 changes: 0 additions & 12 deletions test/fixture/certs/ca.pem

This file was deleted.

Loading

0 comments on commit 71ce388

Please sign in to comment.