mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 16:28:30 +01:00
security: fail closed on gRPC TLS errors, verify dialed server identity (#12263)
* security: fail closed on misconfigured gRPC TLS LoadServerTLS/LoadClientTLS returned plaintext credentials on every error path once TLS was configured: a typo in tls.min_version or tls.cipher_suites, cert and key in different directories, or cert/key set without grpc.ca all left the component serving or dialing plaintext. A partially configured TLS setup now stops the process at startup instead; leaving cert/key unset still means no TLS, as before. The loaders also stat cert/key/ca files up front since the pemfile providers read lazily and would otherwise only fail at handshake time. Mirrored in the Rust volume server: partial grpc.volume cert/key, a missing grpc.ca, or a rustls build failure now abort startup rather than warn-and-continue on plaintext. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * security: optional server-identity allowlist for gRPC clients The mTLS client accepts any certificate the configured CA signs, so a per-workload CA lets one component stand in for any other: a filer identity can answer as the master. The server side already screens callers via allowed_commonNames and allowed_wildcard_domain; nothing screened the server. LoadClientTLS now wires AdditionalPeerVerification when the caller configures a server allowlist: grpc.<component>.allowed_server_commonNames (falling back to a shared grpc.allowed_server_commonNames) matched against the peer cert's subject CN, DNS SANs, or URI SANs such as a SPIFFE id, plus a grpc.allowed_server_wildcard_domain suffix match. Leaving them unset keeps the previous behavior. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * security: URI SANs are exact-match only in the server identity allowlist A URI identity whose path happens to end in the wildcard domain would otherwise pass the DNS suffix check. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * security: tighten gRPC TLS fail-closed and server-identity paths - URI SANs match the server-identity allowlist exactly; the wildcard domain suffix check applies only to CN and DNS SANs. - A partially set client_cert/client_key pair is an error when the cert/key fallback is empty, instead of dialing plaintext. - Cert/key/ca files are loaded and parsed at startup (LoadX509KeyPair and CA PEM validation) since pemfile providers read lazily. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: error on a partial grpc client_cert/client_key pair Mirror the Go loadClientTLS path: when client_cert or client_key is set without its mate and the cert/key fallback is empty, fail instead of dialing plaintext. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * security: wildcard domain match requires a valid hostname A CA-signed identity like ".example.com" satisfied the bare suffix check for wildcard ".example.com" despite not being a hostname. Require a real subdomain: non-empty label before the suffix, a '.' boundary when the configured wildcard has no leading dot, and DNS-name syntax. A leading "*." label still matches so wildcard certs keep working. Applied to both the Go server/client authenticators and the Rust volume's client-cert verifier. * security: ignore a trailing root dot in wildcard domain matching * tls: reject a partial dedicated client cert/key pair instead of falling back When exactly one of component.client_cert/client_key is set, warn-and- fallback silently dialed with the serving identity. Fail closed like the other TLS misconfigurations in this change; mirrored in the Rust loader. Regression tests cover a partial pair with a valid fallback. * rust volume: harden partial client cert pair test with real pem files * security: verify dialed server identity per target kind on the filer The filer reused one grpc.filer-verifying dial option for master, volume, filer and s3 targets, so a broad allowlist accepted the wrong component at the wrong port and a narrow one rejected legitimate peers. Outbound calls now pick grpc.<target>.allowed_server_commonNames (falling back to the client component's and the global list) while still presenting the grpc.filer client identity. * security: scope per-target server allowlists under the client component The filer read grpc.volume.allowed_server_commonNames when dialing a volume, but that key already denotes the volume's own outbound allowlist (e.g. the master identity). A deployment restricting volume->master calls would make the filer reject real volume servers. Per-target policy now lives under the client component: grpc.filer.volume_* falls back to grpc.filer.* then grpc.*. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7d8b72c2b4
commit
e98adfd3bb
15 files changed
+821
-101
No files matched your search
@@ -123,11 +123,14 @@ type FilerServer struct {
|
||||
inFlightDataLimitCond *sync.Cond
|
||||
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
option *FilerOption
|
||||
filer *filer.Filer
|
||||
filerGuard *security.Guard
|
||||
volumeGuard *security.Guard
|
||||
grpcDialOption grpc.DialOption
|
||||
option *FilerOption
|
||||
filer *filer.Filer
|
||||
filerGuard *security.Guard
|
||||
volumeGuard *security.Guard
|
||||
grpcDialOption grpc.DialOption
|
||||
grpcDialOptionMaster grpc.DialOption
|
||||
grpcDialOptionVolume grpc.DialOption
|
||||
grpcDialOptionS3 grpc.DialOption
|
||||
|
||||
// metrics read from the master
|
||||
metricsAddress string
|
||||
@@ -258,6 +261,9 @@ func NewFilerServer(defaultMux, readonlyMux *http.ServeMux, option *FilerOption)
|
||||
fs = &FilerServer{
|
||||
option: option,
|
||||
grpcDialOption: security.LoadClientTLS(util.GetViper(), "grpc.filer"),
|
||||
grpcDialOptionMaster: security.LoadClientTLSForTarget(util.GetViper(), "grpc.filer", "grpc.master"),
|
||||
grpcDialOptionVolume: security.LoadClientTLSForTarget(util.GetViper(), "grpc.filer", "grpc.volume"),
|
||||
grpcDialOptionS3: security.LoadClientTLSForTarget(util.GetViper(), "grpc.filer", "grpc.s3"),
|
||||
knownListeners: make(map[int32]int32),
|
||||
subscribers: make(map[int32]*metadataSubscriber),
|
||||
inFlightDataLimitCond: sync.NewCond(new(sync.Mutex)),
|
||||
@@ -294,7 +300,11 @@ func NewFilerServer(defaultMux, readonlyMux *http.ServeMux, option *FilerOption)
|
||||
v.SetDefault("filer.options.max_file_name_length", 255)
|
||||
maxFilenameLength := v.GetUint32("filer.options.max_file_name_length")
|
||||
glog.V(0).Infof("max_file_name_length %d", maxFilenameLength)
|
||||
fs.filer = filer.NewFiler(*option.Masters, fs.grpcDialOption, option.Host, option.FilerGroup, option.Collection, option.DefaultReplication, option.DataCenter, maxFilenameLength, nil)
|
||||
fs.filer = filer.NewFiler(*option.Masters, &filer.DialOptions{
|
||||
Filer: fs.grpcDialOption,
|
||||
Master: fs.masterGrpcDialOption(),
|
||||
Volume: fs.volumeGrpcDialOption(),
|
||||
}, option.Host, option.FilerGroup, option.Collection, option.DefaultReplication, option.DataCenter, maxFilenameLength, nil)
|
||||
// Peer metadata replay takes the same per-path locks, so a replayed change
|
||||
// cannot split a local mutation's read-modify-write.
|
||||
fs.entryLockTable = fs.filer.EntryLockTable
|
||||
@@ -423,19 +433,42 @@ func NewFilerServer(defaultMux, readonlyMux *http.ServeMux, option *FilerOption)
|
||||
fs.CredentialManager.SetFilerAddressFunc(func() pb.ServerAddress {
|
||||
return fs.option.Host
|
||||
}, fs.grpcDialOption)
|
||||
fs.CredentialManager.SetMasterClient(fs.filer.MasterClient, fs.grpcDialOption)
|
||||
fs.CredentialManager.SetMasterClient(fs.filer.MasterClient, fs.s3GrpcDialOption())
|
||||
}
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// Target-specific dial options fall back to the shared filer option, which is
|
||||
// what FilerServer literals in tests set up.
|
||||
func (fs *FilerServer) masterGrpcDialOption() grpc.DialOption {
|
||||
if fs.grpcDialOptionMaster != nil {
|
||||
return fs.grpcDialOptionMaster
|
||||
}
|
||||
return fs.grpcDialOption
|
||||
}
|
||||
|
||||
func (fs *FilerServer) volumeGrpcDialOption() grpc.DialOption {
|
||||
if fs.grpcDialOptionVolume != nil {
|
||||
return fs.grpcDialOptionVolume
|
||||
}
|
||||
return fs.grpcDialOption
|
||||
}
|
||||
|
||||
func (fs *FilerServer) s3GrpcDialOption() grpc.DialOption {
|
||||
if fs.grpcDialOptionS3 != nil {
|
||||
return fs.grpcDialOptionS3
|
||||
}
|
||||
return fs.grpcDialOption
|
||||
}
|
||||
|
||||
func (fs *FilerServer) checkWithMaster() {
|
||||
|
||||
isConnected := false
|
||||
for !isConnected {
|
||||
fs.option.Masters.RefreshBySrvIfAvailable()
|
||||
for _, master := range fs.option.Masters.GetInstances() {
|
||||
readErr := operation.WithMasterServerClient(context.Background(), false, master, fs.grpcDialOption, func(masterClient master_pb.SeaweedClient) error {
|
||||
readErr := operation.WithMasterServerClient(context.Background(), false, master, fs.masterGrpcDialOption(), func(masterClient master_pb.SeaweedClient) error {
|
||||
resp, err := masterClient.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get master %s configuration: %v", master, err)
|
||||
@@ -655,7 +688,7 @@ func (fs *FilerServer) waitForClusterRemoval() {
|
||||
// The per-query timeout shares the overall deadline, so a stalled
|
||||
// master cannot stretch the wait past the budget.
|
||||
queryCtx, queryCancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
nodes, err := cluster.ListExistingPeerUpdates(queryCtx, master, fs.grpcDialOption, fs.filer.MasterClient.FilerGroup, cluster.FilerType)
|
||||
nodes, err := cluster.ListExistingPeerUpdates(queryCtx, master, fs.masterGrpcDialOption(), fs.filer.MasterClient.FilerGroup, cluster.FilerType)
|
||||
queryCancel()
|
||||
if err != nil {
|
||||
// A failed query is not confirmed removal; retry until the
|
||||
|
||||
Reference in new issue
Block a user