-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Adding X.509 SPIFFE auth #14084
Closed
Closed
Adding X.509 SPIFFE auth #14084
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/* | ||
Copyright 2023 The Vitess Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package servenv | ||
|
||
import ( | ||
"context" | ||
"crypto/x509" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/spf13/pflag" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/credentials" | ||
"google.golang.org/grpc/peer" | ||
"google.golang.org/grpc/status" | ||
|
||
"vitess.io/vitess/go/vt/log" | ||
) | ||
|
||
var ( | ||
// spiffeTrustDomains list of allowed SPIFFE Trust Domains for client SVIDs during authorization | ||
spiffeTrustDomains string | ||
// SPIFFEAuthPlugin implements AuthPlugin interface | ||
_ Authenticator = (*SPIFFEAuthPlugin)(nil) | ||
) | ||
|
||
// The datatype for spiffe auth Context keys | ||
type spiffeIdKey int | ||
|
||
const ( | ||
// Internal Context key for the authenticated SPIFFE ID | ||
spiffeId spiffeIdKey = 0 | ||
) | ||
|
||
func registerGRPCServerAuthSPIFFEFlags(fs *pflag.FlagSet) { | ||
fs.StringVar(&spiffeTrustDomains, "grpc_auth_spiffe_allowed_trust_domains", spiffeTrustDomains, "List of allowed SPIFFE Trust Domains for client SVIDs (separated by comma).") | ||
} | ||
|
||
// SPIFFEAuthPlugin implements X.509-based SVID for SPIFFE authentication for grpc. It contains an array of trust domains | ||
// that will be authorized to connect to the grpc server. | ||
type SPIFFEAuthPlugin struct { | ||
spiffeTrustDomains []string | ||
} | ||
|
||
// Authenticate implements Authenticator interface. This method will be used inside a middleware in grpc_server to authenticate | ||
// incoming requests. | ||
func (spa *SPIFFEAuthPlugin) Authenticate(ctx context.Context, fullMethod string) (context.Context, error) { | ||
p, ok := peer.FromContext(ctx) | ||
if !ok { | ||
return nil, status.Errorf(codes.Unauthenticated, "no peer connection info") | ||
} | ||
tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo) | ||
if !ok { | ||
return nil, status.Errorf(codes.Unauthenticated, "not connected via TLS") | ||
} | ||
|
||
cert := tlsInfo.State.PeerCertificates[0] // Only check the leaf certificate | ||
spiffeIdUrl, ok := validateSVIDCert(cert, spa.spiffeTrustDomains) | ||
if !ok { | ||
return nil, status.Errorf(codes.Unauthenticated, "client certificate not authorized") | ||
} | ||
|
||
log.Infof("SPIFFE auth plugin has authenticated client with SPIFFE ID %v", spiffeId) | ||
return newSPIFFEAuthContext(ctx, spiffeIdUrl), nil | ||
} | ||
|
||
// Validates the given certificate as a valid SVID leaf certificate based on | ||
// https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-constraints-and-usage | ||
// and https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#5-validation | ||
func validateSVIDCert(cert *x509.Certificate, trustedDomains []string) (*url.URL, bool) { | ||
issuedForTrustedDomain := false | ||
|
||
// Leaf SVIDs should have exactly one URI SAN | ||
if len(cert.URIs) != 1 { | ||
return nil, false | ||
} | ||
|
||
if cert.URIs[0].Scheme != "spiffe" { | ||
return nil, false | ||
} | ||
|
||
for _, trustDomain := range trustedDomains { | ||
if cert.URIs[0].Hostname() == trustDomain { | ||
issuedForTrustedDomain = true | ||
break | ||
} | ||
} | ||
|
||
if !issuedForTrustedDomain { | ||
return nil, false | ||
} | ||
|
||
// Leaf SVIDs should not be CA certs | ||
if cert.IsCA { | ||
return nil, false | ||
} | ||
|
||
// Leaf SVIDs should not have CA usage | ||
if cert.KeyUsage&x509.KeyUsageCertSign != 0 { | ||
return nil, false | ||
} | ||
|
||
// Leaf SVIDs should not have CRL signing usage | ||
if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { | ||
return nil, false | ||
} | ||
|
||
// Leaf SVIDs must have Digital Signature usage | ||
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { | ||
return nil, false | ||
} | ||
|
||
return cert.URIs[0], true | ||
} | ||
|
||
func newSPIFFEAuthContext(ctx context.Context, foundSPIFFEId *url.URL) context.Context { | ||
return context.WithValue(ctx, spiffeId, foundSPIFFEId) | ||
} | ||
|
||
func spiffeAuthPluginInitializer() (Authenticator, error) { | ||
spiffeAuthPlugin := &SPIFFEAuthPlugin{ | ||
spiffeTrustDomains: strings.Split(spiffeTrustDomains, ","), | ||
} | ||
log.Infof("SPIFFE auth plugin has initialized successfully with allowed trust domains of %v", spiffeTrustDomains) | ||
return spiffeAuthPlugin, nil | ||
} | ||
|
||
// SPIFFEIdFromContext returns the SPIFFE ID authenticated by the spiffe auth plugin and stored in the Context, if any | ||
func SPIFFEIdFromContext(ctx context.Context) *url.URL { | ||
spiffeId, ok := ctx.Value(spiffeId).(*url.URL) | ||
if ok { | ||
return spiffeId | ||
} | ||
return nil | ||
} | ||
|
||
// SPIFFETrustDomains returns the value of the | ||
// `--grpc_auth_spiffe_allowed_trust_domains` flag. | ||
func SPIFFETrustDomains() string { | ||
return spiffeTrustDomains | ||
} | ||
|
||
func init() { | ||
RegisterAuthPlugin("spiffe", spiffeAuthPluginInitializer) | ||
grpcAuthServerFlagHooks = append(grpcAuthServerFlagHooks, registerGRPCServerAuthSPIFFEFlags) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify, this is explicitly only validating the hostnames obviously, but realistically, would you not want to also validate against the path as well?
Typically, this also can be paired with a
(excuse me since I don't fully know the terms here), someSPIRE server to phone home to to ask "is this URI allowed to do this", similar to OAuth.Without that full implementation, is there any merit into only validating the hostname rather than the full path?
I can see a case of:
spiffe://example.com/client1
, andspiffe://example.com/client2
and wanting to allow client1, but not allow client2.I'm not entirely sure of all the use cases, but I believe in a lot of contexts, this translates to a per-user issuing a certificate for auth, or per-app, and you'd want to restrict to some apps/users, and not the others.
I think explicitly declaring we only validate the hostname, but I would assume there'd be a desire to do more and the implementation here would make that a bit harder to shoehorn in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm not an expert on this subject at all, but I tend to refer to Envoy for a golden standard with this configuration, and they seem to cover a lot more options as well as a SPIRE agent. https://spiffe.io/docs/latest/microservices/envoy-x509/readme/
If we're choosing to implement a subset of the feature set, I think that's personally fine, I just worry if we implement a subset, now others might want more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattrobenolt these are great questions/points. For SPIFFE, the entire URI forms the SPIFFE ID, which uniquely identifies a particular user/workload, so things like ACLs could be based on that ID. However, a collection of SPIFFE IDs can belong to a single trust domain (which usually corresponds to a CA, but doesn't necessarily have to). Given this, I think, for accepting connections at all, the thing we should validate is the trust domain, much like we're validating the CA that signed a client cert. This is mostly because we'd accept connections from many individual workloads and users all issued for the same trust domain.
The SPIFFE ID makes more sense to use in ACLs, where we might decide that certain SPIFFE IDs (perhaps some applications or users) should be considered writers, while other IDs are readers.
SPIRE is one implementation of the SPIFFE Workload API which makes it an issuer of SVIDs. It can attest to the identity of workloads and then provide them the tools to access protected resources and validate the IDs of other workloads. You could think of it as something like cert-manager + let's encrypt. SPIRE agents are used to create and automatically rotate workload certs and provide the CA bundle for verifying both sides of an mTLS connection. SPIRE is not used as an API to query information about other workloads. Further, SPIRE is not required to use SPIFFE as long as you have some issuer for SVIDs.