diff --git a/doc/plugin_server_nodeattestor_x509pop.md b/doc/plugin_server_nodeattestor_x509pop.md index 121f0ff970..a74479cf3d 100644 --- a/doc/plugin_server_nodeattestor_x509pop.md +++ b/doc/plugin_server_nodeattestor_x509pop.md @@ -17,10 +17,12 @@ spiffe:///spire/agent/x509pop/ ``` | Configuration | Description | Default | -|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------| -| `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 }}"` | +|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| +| `mode` | If `spiffe`, use the spire servers own trust bundle to use for validation. If `external_pki`, use the specified CA(s). | external_pki | +| `svid_prefix` | The prefix of the SVID to use for matching valid SVIDS and exchanging them for Node SVIDs | /spire-exchange | +| `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. | `See [Agent Path Template](#agent-path-template) for details` | A sample configuration: @@ -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 Path Prefix + +When mode="spiffe", the SPIFFE ID being exchanged must be prefixed by the specified svid_prefix. The prefix will be removed from the .SVIDPathTrimmed property before sending to the +agent path template. If set to "", all prefixes are allowed and you will want to do limiting logic in in the agent_path_template. + +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}}/{{ .SVIDPathTrimmed }}" + 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: @@ -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 | +| .SVIDPathTrimmed | The SVID Path after trimming off the SVID prefix | diff --git a/pkg/common/plugin/x509pop/x509pop.go b/pkg/common/plugin/x509pop/x509pop.go index 51c5e18c8f..9432374258 100644 --- a/pkg/common/plugin/x509pop/x509pop.go +++ b/pkg/common/plugin/x509pop/x509pop.go @@ -26,7 +26,8 @@ const ( ) // DefaultAgentPathTemplate is the default template -var DefaultAgentPathTemplate = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .Fingerprint }}") +var DefaultAgentPathTemplateCN = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .Fingerprint }}") +var DefaultAgentPathTemplateSVID = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .SVIDPathTrimmed }}") type agentPathTemplateData struct { *x509.Certificate @@ -34,6 +35,7 @@ type agentPathTemplateData struct { Fingerprint string PluginName string TrustDomain string + SVIDPathTrimmed string } type AttestationData struct { @@ -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, svidPathTrimmed string) (spiffeid.ID, error) { agentPath, err := agentPathTemplate.Execute(agentPathTemplateData{ TrustDomain: td.Name(), Certificate: cert, PluginName: PluginName, SerialNumberHex: SerialNumberHex(cert.SerialNumber), Fingerprint: Fingerprint(cert), + SVIDPathTrimmed: svidPathTrimmed, }) if err != nil { return spiffeid.ID{}, err diff --git a/pkg/common/plugin/x509pop/x509pop_test.go b/pkg/common/plugin/x509pop/x509pop_test.go index 4262fc78ba..23ecc43bc1 100644 --- a/pkg/common/plugin/x509pop/x509pop_test.go +++ b/pkg/common/plugin/x509pop/x509pop_test.go @@ -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", }, { @@ -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) diff --git a/pkg/server/plugin/nodeattestor/x509pop/x509pop.go b/pkg/server/plugin/nodeattestor/x509pop/x509pop.go index d8ba2d7fae..1e63d04443 100644 --- a/pkg/server/plugin/nodeattestor/x509pop/x509pop.go +++ b/pkg/server/plugin/nodeattestor/x509pop/x509pop.go @@ -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" @@ -35,12 +38,16 @@ func builtin(p *Plugin) catalog.BuiltIn { } type Config struct { + Mode string `hcl:"mode"` + SVIDPrefix *string `hcl:"spiffe_prefix"` CABundlePath string `hcl:"ca_bundle_path"` CABundlePaths []string `hcl:"ca_bundle_paths"` AgentPathTemplate string `hcl:"agent_path_template"` } type configuration struct { + mode string + svidPrefix string trustDomain spiffeid.TrustDomain trustBundle *x509.CertPool pathTemplate *agentpathtemplate.Template @@ -53,29 +60,44 @@ 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.Mode == "" { + hclConfig.Mode = "external_pki" } - 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") + if hclConfig.Mode != "external_pki" && hclConfig.Mode != "spiffe" { + status.ReportError("mode can only be either spiffe or external_pki") } - 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.Mode == "external_pki" { + 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 + if hclConfig.Mode == "spiffe" && (hclConfig.CABundlePath != "" || len(hclConfig.CABundlePaths) > 0) { + status.ReportError("you can not use ca_bundle_path or ca_bundle_paths in spiffe mode") + } + + pathTemplate := x509pop.DefaultAgentPathTemplateCN + if hclConfig.Mode == "spiffe" { + pathTemplate = x509pop.DefaultAgentPathTemplateSVID + } if len(hclConfig.AgentPathTemplate) > 0 { tmpl, err := agentpathtemplate.Parse(hclConfig.AgentPathTemplate) if err != nil { @@ -84,10 +106,22 @@ func buildConfig(coreConfig catalog.CoreConfig, hclText string, status *pluginco pathTemplate = tmpl } + var svidPrefix string + if hclConfig.SVIDPrefix == nil { + svidPrefix = "/spire-exchange/" + } else { + svidPrefix = *hclConfig.SVIDPrefix + if !strings.HasSuffix(svidPrefix, "/") { + svidPrefix += "/" + } + } + newConfig := &configuration{ trustDomain: coreConfig.TrustDomain, trustBundle: util.NewCertPool(trustBundles...), pathTemplate: pathTemplate, + mode: hclConfig.Mode, + svidPrefix: svidPrefix, } return newConfig @@ -97,14 +131,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 { @@ -143,10 +185,18 @@ func (p *Plugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) error { intermediates.AddCert(intermediate) } + trustBundle := config.trustBundle + if config.mode == "spiffe" { + 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 { @@ -188,7 +238,19 @@ 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 := "" + if config.mode == "spiffe" { + if len(leaf.URIs) == 0 { + return status.Errorf(codes.PermissionDenied, "valid SVID x509 cert not found") + } + svidPath = leaf.URIs[0].EscapedPath() + if !strings.HasPrefix(svidPath, config.svidPrefix) { + return status.Errorf(codes.PermissionDenied, "x509 cert doesnt match SVID prefix") + } + svidPath = strings.TrimPrefix(svidPath, config.svidPrefix) + } + + 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) } @@ -226,6 +288,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() diff --git a/pkg/server/plugin/nodeattestor/x509pop/x509pop_test.go b/pkg/server/plugin/nodeattestor/x509pop/x509pop_test.go index 59bf072cbb..3c3926a376 100644 --- a/pkg/server/plugin/nodeattestor/x509pop/x509pop_test.go +++ b/pkg/server/plugin/nodeattestor/x509pop/x509pop_test.go @@ -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" @@ -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") }) @@ -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), @@ -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"), }),