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

[Security] Add support for SPIFFE Bundle Maps in XDS bundles #8180

Merged
merged 12 commits into from
Apr 4, 2025

Conversation

gtcooke94
Copy link
Contributor

This adds support for configuring SPIFFE Bundle Maps inside of credentials via xds bundles.

See the gRFC for more detail grpc/proposal#462

RELEASE NOTES: N/A

@gtcooke94 gtcooke94 added Type: Security A bug or other problem affecting security Area: xDS Includes everything xDS related, including LB policies used with xDS. Area: Auth Includes regular credentials API and implementation. Also includes advancedtls, authz, rbac etc. labels Mar 18, 2025
@gtcooke94 gtcooke94 added this to the 1.72 Release milestone Mar 18, 2025
Copy link

codecov bot commented Mar 18, 2025

Codecov Report

Attention: Patch coverage is 79.86577% with 30 lines in your changes missing coverage. Please review.

Project coverage is 82.12%. Comparing base (1f6b0cf) to head (327a534).
Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
internal/testutils/tls_creds.go 55.00% 18 Missing and 9 partials ⚠️
internal/xds/bootstrap/tlscreds/bundle.go 93.87% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8180      +/-   ##
==========================================
- Coverage   82.42%   82.12%   -0.31%     
==========================================
  Files         393      410      +17     
  Lines       39162    40371    +1209     
==========================================
+ Hits        32280    33154     +874     
- Misses       5571     5855     +284     
- Partials     1311     1362      +51     
Files with missing lines Coverage Δ
credentials/tls/certprovider/pemfile/builder.go 100.00% <100.00%> (ø)
credentials/tls/certprovider/pemfile/watcher.go 86.56% <100.00%> (-2.99%) ⬇️
internal/credentials/spiffe/spiffe.go 100.00% <100.00%> (ø)
internal/xds/bootstrap/tlscreds/bundle.go 86.86% <93.87%> (+4.41%) ⬆️
internal/testutils/tls_creds.go 55.00% <55.00%> (ø)

... and 45 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@@ -96,3 +102,79 @@ func (s) TestFailingProvider(t *testing.T) {
t.Errorf("EmptyCall() got err: %s, want err to contain: %s", err, wantErr)
}
}

func (s) TestSPIFFEVerifyFuncMismatchedCert(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can/Should these tests be e2e style as well? That way, we can actually verify that the error returned to the user (as part of the RPC) has enough details in it for the user to take meaningful action.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean should we replicate these tests in bundle_ext_test.go and actually do the client/server call rather than just this inner call? We have one failure case test where there is no bundle for the peer certificates trust domain, I can expand and add these cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went on and expanded the end2end tests, PTAL

Copy link
Contributor

@easwars easwars left a comment

Choose a reason for hiding this comment

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

Mostly LGTM. Just minor comments this time around.

Comment on lines 50 to 68
const wantBundleSize = 2
if len(bundles) != wantBundleSize {
t.Fatalf("LoadSPIFFEBundleMap(%v) did not parse correct bundle length. got %v want %v", spiffeBundleFile, len(bundles), wantBundleSize)
t.Fatalf("BundleMapFromBytes(%v) did not parse correct bundle length. got %v want %v", spiffeBundleFile, len(bundles), wantBundleSize)
}
if bundles["example.com"] == nil {
t.Fatalf("LoadSPIFFEBundleMap(%v) got no bundle for example.com", spiffeBundleFile)
t.Fatalf("BundleMapFromBytes(%v) got no bundle for example.com", spiffeBundleFile)
}
if bundles["test.example.com"] == nil {
t.Fatalf("LoadSPIFFEBundleMap(%v) got no bundle for test.example.com", spiffeBundleFile)
t.Fatalf("BundleMapFromBytes(%v) got no bundle for test.example.com", spiffeBundleFile)
}

expectedExampleComCert := loadX509Cert(t, testdata.Path("spiffe/spiffe_cert.pem"))
expectedTestExampleComCert := loadX509Cert(t, testdata.Path("spiffe/server1_spiffe.pem"))
if !bundles["example.com"].X509Authorities()[0].Equal(expectedExampleComCert) {
t.Fatalf("LoadSPIFFEBundleMap(%v) parsed wrong cert for example.com.", spiffeBundleFile)
wantExampleComCert := loadX509Cert(t, testdata.Path("spiffe/spiffe_cert.pem"))
wantTestExampleComCert := loadX509Cert(t, testdata.Path("spiffe/server1_spiffe.pem"))
if !bundles["example.com"].X509Authorities()[0].Equal(wantExampleComCert) {
t.Fatalf("BundleMapFromBytes(%v) parsed wrong cert for example.com.", spiffeBundleFile)
}
if !bundles["test.example.com"].X509Authorities()[0].Equal(expectedTestExampleComCert) {
t.Fatalf("LoadSPIFFEBundleMap(%v) parsed wrong cert for test.example.com", spiffeBundleFile)
if !bundles["test.example.com"].X509Authorities()[0].Equal(wantTestExampleComCert) {
t.Fatalf("BundleMapFromBytes(%v) parsed wrong cert for test.example.com", spiffeBundleFile)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of individual comparison of fields, would you be able to construct a single wantBundles of type map[string]*spiffebundle.Bundle and directly compare it with the result from BundleMapFromBytes?

See: go/go-style/decisions#compare-full-structures

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main way of creating a spiffebundle.Bundle is loading it from the bytes/file, which is largely what we are wrapping/testing. I could create one manually using the spiffebundle APIs in the test, but I think the readability would suffer significantly.
I could also manually craft more spiffebundle files that are bundles and not a larger bundle map as well, then load those using the spiffebundle library, but I think that also suffers from more confusion, readability issues, and bloat. In addition, that's more files that would need to be manually created and kept in sync with an update.

I'm happy to make it work if you feel very strongly, but I think this is a case where creating a full known struct to compare to is quite a bit more complex than comparing the fields that we care about. Let me know

CertFile: cfg.CertificateFile,
KeyFile: cfg.PrivateKeyFile,
RootFile: cfg.CACertificateFile,
CertFile: cfg.CertificateFile,
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great if you could file an issue to track this. Otherwise, we will definitely forget about it. Thanks. We could even get someone from BLR to work on it if we have the it described in detail in an issue.

Comment on lines 289 to 293
ss := stubserver.StubServer{
Address: server.Address,
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) { return &testpb.Empty{}, nil },
}
server = stubserver.StartTestService(t, &ss, serverCredentials)
Copy link
Contributor

Choose a reason for hiding this comment

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

The restartable listener type provides a Stop and Restart method. When Stop is called, existing connections are closed and new connections are not accepted, and when Restart is called, new connections are accepted. Therefore, once Restart is called, we should see another handshake.

@easwars easwars removed their assignment Apr 3, 2025
@easwars easwars assigned gtcooke94 and unassigned gtcooke94 Apr 3, 2025
@gtcooke94 gtcooke94 assigned easwars and unassigned gtcooke94 Apr 4, 2025
@easwars easwars assigned gtcooke94 and unassigned easwars Apr 4, 2025
@gtcooke94 gtcooke94 merged commit 4b5505d into grpc:master Apr 4, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Auth Includes regular credentials API and implementation. Also includes advancedtls, authz, rbac etc. Area: xDS Includes everything xDS related, including LB policies used with xDS. Type: Security A bug or other problem affecting security
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants