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

Merged
merged 56 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 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
b7eea56
Merge branch 'main' into x509pop-trustbundle
kfox1111 Dec 5, 2024
9f6b9f7
Apply suggestions from code review
kfox1111 Dec 10, 2024
f1385d4
Merge branch 'main' into x509pop-trustbundle
kfox1111 Dec 10, 2024
e4313e3
Add logging
kfox1111 Dec 12, 2024
ee6e679
Merge branch 'main' into x509pop-trustbundle
kfox1111 Dec 12, 2024
c599b12
Add some tests
kfox1111 Dec 12, 2024
7820d1a
Merge branch 'x509pop-trustbundle' of https://github.com/kfox1111/spi…
kfox1111 Dec 12, 2024
fd6a320
Merge branch 'main' into x509pop-trustbundle
kfox1111 Dec 12, 2024
b2c3cdf
Add some new test certs
kfox1111 Dec 13, 2024
78e61eb
Initial x509 spiffe test
kfox1111 Dec 13, 2024
40e94ac
Merge branch 'x509pop-trustbundle' of https://github.com/kfox1111/spi…
kfox1111 Dec 13, 2024
1c75f36
Add some failure testing too
kfox1111 Dec 13, 2024
238edcb
Add no bundle test. Cleanup
kfox1111 Dec 13, 2024
2b5c884
Fix lint
kfox1111 Dec 13, 2024
021a90b
More lint
kfox1111 Dec 13, 2024
50ebc2b
Merge branch 'main' into x509pop-trustbundle
kfox1111 Dec 13, 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
29 changes: 25 additions & 4 deletions doc/plugin_server_nodeattestor_x509pop.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ spiffe://<trust_domain>/spire/agent/x509pop/<fingerprint>
```

| 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:

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 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)
Details about the template engine are available [here](template_engine.md).

Expand All @@ -58,3 +78,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 |
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 }}/{{ .SVIDPathTrimmed }}")

type agentPathTemplateData struct {
*x509.Certificate
SerialNumberHex string
Fingerprint string
PluginName string
TrustDomain string
SVIDPathTrimmed 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, 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
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: 109 additions & 21 deletions pkg/server/plugin/nodeattestor/x509pop/x509pop.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import (
"context"
"crypto/x509"
"encoding/json"
"strings"
"sync"

"github.com/hashicorp/go-hclog"
"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,12 +39,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
Expand All @@ -53,29 +61,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 hclConfig.Mode != "external_pki" && hclConfig.Mode != "spiffe" {
status.ReportError("mode can only be either spiffe or external_pki")
}
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.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")
Comment on lines +73 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

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

is not the same error happens in line 80?
may we return here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm.... The check here checks if both are configured. The check at 80 ensures at least one is configured. Both are needed I think.

I was thinking status.ReportError was fatal. Is it not? Maybe all the ReportError's here then need retruns?

Copy link
Collaborator

Choose a reason for hiding this comment

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

mmm, it is like if both are configured, you must never get the second one right?
status.Report is not a fatal, but just adding a list of errors, than then are used in configure or verify,
but this approach has a good win than you can get the complete list of errors to resolve all together,
but it makes very hard to follow and see dependency errors...
In any case we are fine for now

}
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")
}
trustBundles = append(trustBundles, certs...)

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...)
}
}

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.DefaultAgentPathTemplate
pathTemplate := x509pop.DefaultAgentPathTemplateCN
if hclConfig.Mode == "spiffe" {
pathTemplate = x509pop.DefaultAgentPathTemplateSVID
}
if len(hclConfig.AgentPathTemplate) > 0 {
tmpl, err := agentpathtemplate.Parse(hclConfig.AgentPathTemplate)
if err != nil {
Expand All @@ -84,10 +107,20 @@ func buildConfig(coreConfig catalog.CoreConfig, hclText string, status *pluginco
pathTemplate = tmpl
}

svidPrefix := "/spire-exchange/"
if hclConfig.SVIDPrefix != nil {
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
Expand All @@ -97,14 +130,24 @@ type Plugin struct {
nodeattestorv1.UnsafeNodeAttestorServer
configv1.UnsafeConfigServer

m sync.Mutex
config *configuration
log hclog.Logger

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 +186,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 {
Expand Down Expand Up @@ -188,7 +239,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)
}
Expand Down Expand Up @@ -226,6 +289,31 @@ func (p *Plugin) Validate(_ context.Context, req *configv1.ValidateRequest) (*co
}, err
}

// SetLogger sets this plugin's logger
func (p *Plugin) SetLogger(log hclog.Logger) {
p.log = log
}

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
}
p.log.Warn("No trust bundle retrieved from SPIRE")
return nil, nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

if this happens we'll reject attestations, but it is going to be hard to understand why it is happening (basically we are not going to have a trusted bundle)
May we at least return wartning or error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we log a warning about not currently finding a trust bundle? Its nor fatal as it might show up at some point later?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeap, I agree but this attestation will fail and mention a verification error
so a warning may help users to understand than that the that attestation failure is cuased by the attestor being unabled to get the trust bundle.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a warning.

}

func (p *Plugin) getConfig() (*configuration, error) {
p.m.Lock()
defer p.m.Unlock()
Expand Down
Loading
Loading