Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x509pop server plugin support for servers trust bundle #5572

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0a9b0d4
x509pop server plugin support for servers trust bundle
kfox1111 Oct 13, 2024
38e9850
Fix some lint things
kfox1111 Oct 13, 2024
b03cd79
Incorperate feedback
kfox1111 Oct 15, 2024
483d2de
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 15, 2024
dd8f394
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 19, 2024
0c9aa54
Try to fix test
kfox1111 Oct 19, 2024
040e70d
Merge branch 'x509pop-trustbundle' of https://github.com/kfox1111/spi…
kfox1111 Oct 19, 2024
6ea18ad
Try to fix test
kfox1111 Oct 19, 2024
01e9c3f
Try to fix test
kfox1111 Oct 19, 2024
2cb1529
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 23, 2024
44e1cbf
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 23, 2024
6359989
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 23, 2024
a881a47
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 24, 2024
fe5bb17
Add some better defaults and doc updates
kfox1111 Oct 25, 2024
487100d
Incorperate feedback
kfox1111 Oct 25, 2024
4b06fab
Fix test
kfox1111 Oct 26, 2024
d55f40d
Fix lint
kfox1111 Oct 26, 2024
c12860b
Fix lint issue
kfox1111 Oct 26, 2024
6ecc0b8
Fix lint issue
kfox1111 Oct 26, 2024
bf95562
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 28, 2024
cf1cb52
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 29, 2024
b7f77d0
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 30, 2024
289ae88
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 30, 2024
66c21d3
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 31, 2024
d34d912
Trim prefix off automatically
kfox1111 Oct 31, 2024
4d94054
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 31, 2024
4c6ee68
Merge branch 'main' into x509pop-trustbundle
kfox1111 Oct 31, 2024
21f2075
Merge branch 'main' into x509pop-trustbundle
kfox1111 Nov 1, 2024
e476ccf
Merge branch 'main' into x509pop-trustbundle
kfox1111 Nov 2, 2024
dbb120d
Merge branch 'main' into x509pop-trustbundle
kfox1111 Nov 4, 2024
7a8b778
Apply suggestions from code review
kfox1111 Nov 5, 2024
de1611a
Update doc/plugin_server_nodeattestor_x509pop.md
kfox1111 Nov 5, 2024
331f003
Incorperate feedback
kfox1111 Nov 6, 2024
bed2c85
Fix merge conflict
kfox1111 Nov 6, 2024
941236b
Incorperate feedback
kfox1111 Nov 6, 2024
bdf284a
Incorperate feedback
kfox1111 Nov 6, 2024
037ac50
Merge branch 'main' into x509pop-trustbundle
kfox1111 Nov 6, 2024
2f1166d
Incorperate feedback
kfox1111 Nov 6, 2024
aab491a
fmt
kfox1111 Nov 6, 2024
378bdda
Merge branch 'main' into x509pop-trustbundle
kfox1111 Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion doc/plugin_server_nodeattestor_x509pop.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ spiffe://<trust_domain>/spire/agent/x509pop/<fingerprint>

| Configuration | Description | Default |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------|
| `spire_trust_bundle` | If true, use the spire servers own trust bundle to use for validation. | |
| `svid_prefix` | The prefix of the SVID to use for matching vaid SVIDS and exchanging them for Node SVIDs | /spire-exchange |
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
| `ca_bundle_path` | The path to the trusted CA bundle on disk. The file must contain one or more PEM blocks forming the set of trusted root CA's for chain-of-trust verification. If the CA certificates are in more than one file, use `ca_bundle_paths` instead. | |
| `ca_bundle_paths` | A list of paths to trusted CA bundles on disk. The files must contain one or more PEM blocks forming the set of trusted root CA's for chain-of-trust verification. | |
| `agent_path_template` | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `"{{ .PluginName}}/{{ .Fingerprint }}"` |
| `agent_path_template` | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `See Agent Path Template for details` |
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved

A sample configuration:

Expand All @@ -43,9 +45,27 @@ A sample configuration:
| SHA1 Fingerprint | `x509pop:ca:fingerprint:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33` | The SHA1 fingerprint as a hex string for each cert in the PoP chain, excluding the leaf. |
| SerialNumber | `x509pop:serialnumber:0a1b2c3d4e5f` | The leaf certificate serial number as a lowercase hexadecimal string |

## SVID Prefix
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved

When spire_trust_bundle is used, the SPIFFE ID being exchanged must be prefixed by the specified svid_prefix. The prefix will be removed from the .SVIDPath before sending to the
agent path template.

For example, if your trust domain is example.com and svid_prefix = the default of /spire-exchange, and agent path template is the default,

spiffe://example.com/spire-exchange/testhost will render out to spiffe://example.com/spire/agent/x509pop/testhost

If spiffe://example.com/other/testhost is given, it wont match the svid_prefix and it will be rejected.

## Agent Path Template

The agent path template is a way of customizing the format of generated SPIFFE IDs for agents.

If using ca_bundle_path(s), the default is:
"{{ .PluginName}}/{{ .Fingerprint }}"

If using spire_trust_bundle, the default exchanges an SVID under `/spire-exchange/*` for `/spire/agent/x509pop/*`, via:
"{{ .PluginName}}/{{ .SVIDPath }}"

The template formatter is using Golang text/template conventions, it can reference values provided by the plugin or in a [golang x509.Certificate](https://pkg.go.dev/crypto/x509#Certificate)

Some useful values are:
Expand All @@ -57,3 +77,4 @@ Some useful values are:
| .TrustDomain | The configured trust domain |
| .Subject.CommonName | The common name field of the agent's x509 certificate |
| .SerialNumberHex | The serial number field of the agent's x509 certificate represented as lowercase hexadecimal |
| .SVIDPath | The SVID Path after removing the SVID Prefix |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe SVIDPathSuffix better implies that a prefix has been stripped?

7 changes: 5 additions & 2 deletions pkg/common/plugin/x509pop/x509pop.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ const (
)

// DefaultAgentPathTemplate is the default template
var DefaultAgentPathTemplate = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .Fingerprint }}")
var DefaultAgentPathTemplateCN = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .Fingerprint }}")
var DefaultAgentPathTemplateSVID = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .SVIDPath }}")

type agentPathTemplateData struct {
*x509.Certificate
SerialNumberHex string
Fingerprint string
PluginName string
TrustDomain string
SVIDPath string
}

type AttestationData struct {
Expand Down Expand Up @@ -266,13 +268,14 @@ func Fingerprint(cert *x509.Certificate) string {
}

// MakeAgentID creates an agent ID from X.509 certificate data.
func MakeAgentID(td spiffeid.TrustDomain, agentPathTemplate *agentpathtemplate.Template, cert *x509.Certificate) (spiffeid.ID, error) {
func MakeAgentID(td spiffeid.TrustDomain, agentPathTemplate *agentpathtemplate.Template, cert *x509.Certificate, svidPath string) (spiffeid.ID, error) {
agentPath, err := agentPathTemplate.Execute(agentPathTemplateData{
TrustDomain: td.Name(),
Certificate: cert,
PluginName: PluginName,
SerialNumberHex: SerialNumberHex(cert.SerialNumber),
Fingerprint: Fingerprint(cert),
SVIDPath: svidPath,
})
if err != nil {
return spiffeid.ID{}, err
Expand Down
4 changes: 2 additions & 2 deletions pkg/common/plugin/x509pop/x509pop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestMakeAgentID(t *testing.T) {
}{
{
desc: "default template with sha1",
template: DefaultAgentPathTemplate,
template: DefaultAgentPathTemplateCN,
expectID: "spiffe://example.org/spire/agent/x509pop/da39a3ee5e6b4b0d3255bfef95601890afd80709",
},
{
Expand All @@ -161,7 +161,7 @@ func TestMakeAgentID(t *testing.T) {
CommonName: "test-cert",
},
}
id, err := MakeAgentID(spiffeid.RequireTrustDomainFromString("example.org"), tt.template, cert)
id, err := MakeAgentID(spiffeid.RequireTrustDomainFromString("example.org"), tt.template, cert, "")
if tt.expectErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectErr)
Expand Down
130 changes: 101 additions & 29 deletions pkg/server/plugin/nodeattestor/x509pop/x509pop.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"context"
"crypto/x509"
"encoding/json"
"strings"
"sync"

"github.com/hashicorp/hcl"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/spire-plugin-sdk/pluginsdk"
identityproviderv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/hostservice/server/identityprovider/v1"
nodeattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/nodeattestor/v1"
configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1"
"github.com/spiffe/spire/pkg/common/agentpathtemplate"
Expand Down Expand Up @@ -35,15 +38,19 @@ func builtin(p *Plugin) catalog.BuiltIn {
}

type Config struct {
SPIRETrustBundle bool `hcl:"spire_trust_bundle"`
SPIFFEPrefix *string `hcl:"spiffe_prefix"`
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
CABundlePath string `hcl:"ca_bundle_path"`
CABundlePaths []string `hcl:"ca_bundle_paths"`
AgentPathTemplate string `hcl:"agent_path_template"`
}

type configuration struct {
trustDomain spiffeid.TrustDomain
trustBundle *x509.CertPool
pathTemplate *agentpathtemplate.Template
SPIRETrustBundle bool
SPIFFEPrefix string
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
trustDomain spiffeid.TrustDomain
trustBundle *x509.CertPool
pathTemplate *agentpathtemplate.Template
}

func buildConfig(coreConfig catalog.CoreConfig, hclText string, status *pluginconf.Status) *configuration {
Expand All @@ -53,29 +60,34 @@ func buildConfig(coreConfig catalog.CoreConfig, hclText string, status *pluginco
return nil
}

var caPaths []string
if hclConfig.CABundlePath != "" && len(hclConfig.CABundlePaths) > 0 {
status.ReportError("only one of ca_bundle_path or ca_bundle_paths can be configured, not both")
}
if hclConfig.CABundlePath != "" {
caPaths = []string{hclConfig.CABundlePath}
} else {
caPaths = hclConfig.CABundlePaths
}
if len(caPaths) == 0 {
status.ReportError("one of ca_bundle_path or ca_bundle_paths must be configured")
}

var trustBundles []*x509.Certificate
for _, caPath := range caPaths {
certs, err := util.LoadCertificates(caPath)
if err != nil {
status.ReportErrorf("unable to load trust bundle %q: %v", caPath, err)
if !hclConfig.SPIRETrustBundle {
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
var caPaths []string
if hclConfig.CABundlePath != "" && len(hclConfig.CABundlePaths) > 0 {
status.ReportError("only one of ca_bundle_path or ca_bundle_paths can be configured, not both")
}
if hclConfig.CABundlePath != "" {
caPaths = []string{hclConfig.CABundlePath}
} else {
caPaths = hclConfig.CABundlePaths
}
if len(caPaths) == 0 {
status.ReportError("one of ca_bundle_path or ca_bundle_paths must be configured")
}

for _, caPath := range caPaths {
certs, err := util.LoadCertificates(caPath)
if err != nil {
status.ReportErrorf("unable to load trust bundle %q: %v", caPath, err)
}
trustBundles = append(trustBundles, certs...)
}
trustBundles = append(trustBundles, certs...)
}

pathTemplate := x509pop.DefaultAgentPathTemplate
pathTemplate := x509pop.DefaultAgentPathTemplateCN
if hclConfig.SPIRETrustBundle {
pathTemplate = x509pop.DefaultAgentPathTemplateSVID
}
if len(hclConfig.AgentPathTemplate) > 0 {
tmpl, err := agentpathtemplate.Parse(hclConfig.AgentPathTemplate)
if err != nil {
Expand All @@ -84,10 +96,22 @@ func buildConfig(coreConfig catalog.CoreConfig, hclText string, status *pluginco
pathTemplate = tmpl
}

var SPIFFEPrefix string
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
if hclConfig.SPIFFEPrefix == nil {
SPIFFEPrefix = "/spire-exchange/"
} else {
SPIFFEPrefix = *hclConfig.SPIFFEPrefix
if !strings.HasSuffix(SPIFFEPrefix, "/") {
SPIFFEPrefix += "/"
}
}

newConfig := &configuration{
trustDomain: coreConfig.TrustDomain,
trustBundle: util.NewCertPool(trustBundles...),
pathTemplate: pathTemplate,
trustDomain: coreConfig.TrustDomain,
trustBundle: util.NewCertPool(trustBundles...),
pathTemplate: pathTemplate,
SPIRETrustBundle: hclConfig.SPIRETrustBundle,
SPIFFEPrefix: SPIFFEPrefix,
}

return newConfig
Expand All @@ -97,14 +121,22 @@ type Plugin struct {
nodeattestorv1.UnsafeNodeAttestorServer
configv1.UnsafeConfigServer

m sync.Mutex
config *configuration
m sync.Mutex
config *configuration
identityProvider identityproviderv1.IdentityProviderServiceClient
}

func New() *Plugin {
return &Plugin{}
}

func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error {
if !broker.BrokerClient(&p.identityProvider) {
return status.Errorf(codes.FailedPrecondition, "IdentityProvider host service is required")
}
return nil
}

func (p *Plugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) error {
req, err := stream.Recv()
if err != nil {
Expand Down Expand Up @@ -143,10 +175,18 @@ func (p *Plugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) error {
intermediates.AddCert(intermediate)
}

trustBundle := config.trustBundle
if config.SPIRETrustBundle {
trustBundle, err = p.getTrustBundle(stream.Context())
if err != nil {
return status.Errorf(codes.Internal, "failed to get trust bundle: %v", err)
}
}

// verify the chain of trust
chains, err := leaf.Verify(x509.VerifyOptions{
Intermediates: intermediates,
Roots: config.trustBundle,
Roots: trustBundle,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
})
if err != nil {
Expand Down Expand Up @@ -188,7 +228,20 @@ func (p *Plugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) error {
return status.Errorf(codes.PermissionDenied, "challenge response verification failed: %v", err)
}

spiffeid, err := x509pop.MakeAgentID(config.trustDomain, config.pathTemplate, leaf)
SVIDPath := ""
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
if config.SPIRETrustBundle {
if len(leaf.URIs) >= 1 {
kfox1111 marked this conversation as resolved.
Show resolved Hide resolved
SVIDPath = leaf.URIs[0].EscapedPath()
if !strings.HasPrefix(SVIDPath, config.SPIFFEPrefix) {
return status.Errorf(codes.PermissionDenied, "x509 cert doesnt match SPIFFE prefix")
}
SVIDPath = strings.TrimPrefix(SVIDPath, config.SPIFFEPrefix)
} else {
return status.Errorf(codes.PermissionDenied, "valid SVID x509 cert not found")
}
}

spiffeid, err := x509pop.MakeAgentID(config.trustDomain, config.pathTemplate, leaf, SVIDPath)
if err != nil {
return status.Errorf(codes.Internal, "failed to make spiffe id: %v", err)
}
Expand Down Expand Up @@ -226,6 +279,25 @@ func (p *Plugin) Validate(_ context.Context, req *configv1.ValidateRequest) (*co
}, err
}

func (p *Plugin) getTrustBundle(ctx context.Context) (*x509.CertPool, error) {
resp, err := p.identityProvider.FetchX509Identity(ctx, &identityproviderv1.FetchX509IdentityRequest{})
if err != nil {
return nil, err
}
var trustBundles []*x509.Certificate
for _, rawcert := range resp.Bundle.X509Authorities {
certificates, err := x509.ParseCertificates(rawcert.Asn1)
if err != nil {
return nil, err
}
trustBundles = append(trustBundles, certificates...)
}
if len(trustBundles) > 0 {
return util.NewCertPool(trustBundles...), nil
}
return nil, nil
}

func (p *Plugin) getConfig() (*configuration, error) {
p.m.Lock()
defer p.m.Unlock()
Expand Down
8 changes: 7 additions & 1 deletion pkg/server/plugin/nodeattestor/x509pop/x509pop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
"testing"

"github.com/spiffe/go-spiffe/v2/spiffeid"
identityproviderv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/hostservice/server/identityprovider/v1"
"github.com/spiffe/spire/pkg/common/catalog"
"github.com/spiffe/spire/pkg/common/plugin/x509pop"
"github.com/spiffe/spire/pkg/server/plugin/nodeattestor"
"github.com/spiffe/spire/proto/spire/common"
"github.com/spiffe/spire/test/fakes/fakeidentityprovider"
"github.com/spiffe/spire/test/fixture"
"github.com/spiffe/spire/test/plugintest"
"github.com/spiffe/spire/test/spiretest"
Expand Down Expand Up @@ -158,7 +160,9 @@ func (s *Suite) TestAttestFailure() {

s.T().Run("not configured", func(t *testing.T) {
attestor := new(nodeattestor.V1)
plugintest.Load(t, BuiltIn(), attestor)
plugintest.Load(t, BuiltIn(), attestor,
plugintest.HostServices(identityproviderv1.IdentityProviderServiceServer(fakeidentityprovider.New())),
)
attestFails(t, attestor, []byte("payload"), codes.FailedPrecondition,
"nodeattestor(x509pop): not configured")
})
Expand Down Expand Up @@ -216,6 +220,7 @@ func (s *Suite) TestConfigure() {
doConfig := func(t *testing.T, coreConfig catalog.CoreConfig, config string) error {
var err error
plugintest.Load(t, BuiltIn(), nil,
plugintest.HostServices(identityproviderv1.IdentityProviderServiceServer(fakeidentityprovider.New())),
plugintest.CaptureConfigureError(&err),
plugintest.CoreConfig(coreConfig),
plugintest.Configure(config),
Expand Down Expand Up @@ -271,6 +276,7 @@ func (s *Suite) TestConfigure() {
func (s *Suite) loadPlugin(t *testing.T, config string) nodeattestor.NodeAttestor {
v1 := new(nodeattestor.V1)
plugintest.Load(t, BuiltIn(), v1,
plugintest.HostServices(identityproviderv1.IdentityProviderServiceServer(fakeidentityprovider.New())),
plugintest.CoreConfig(catalog.CoreConfig{
TrustDomain: spiffeid.RequireTrustDomainFromString("example.org"),
}),
Expand Down
Loading