mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-29 19:25:35 +00:00
* util: serialize every ViperProxy method; stop promoting unlocked viper calls ViperProxy embedded *viper.Viper, so only the five declared methods took the mutex while every promoted call — GetStringMap in backend.LoadConfiguration was the reported crash — touched viper's maps unsynchronized. `weed server` starts the volume server (SetDefault writer) and the master (GetStringMap reader) back to back, and a race build reports the pair on a plain start. The wrapped viper is now a named field: a method must be declared here to exist on the proxy, so unsynchronized access fails at compile time rather than at runtime. Every promoted use in the tree (GetStringMap, GetUint32, GetFloat64, GetDuration, IsSet, AllKeys, Set) gets a locked wrapper; NewViperProxy replaces struct literals for local vipers. GetStringMap deep-copies its result — viper hands back the internal subtree, so iterating it after the lock is released would race the next writer. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * util: take the shared lock while LoadConfiguration merges a config file viper.MergeInConfig rewrites the same maps the proxy serializes; without the lock a merge can race a concurrent SetDefault or reader exactly like the reported startup crash. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * util: deep-copy slice elements in the GetStringMap snapshot A slice of maps inside the returned subtree still shared the inner maps — copy elements recursively so nothing the caller mutates is viper's internal state. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * util: add the missing AutomaticEnv wrapper used by tests sse_reader_test reaches it through GetViper(); without the wrapper the call no longer exists once the viper field stopped being embedded. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * util: return a fresh slice from GetStringSlice A stored []string comes back uncast from viper — the backing array is shared internal state like the GetStringMap subtree, so copy it while holding the lock. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
340 lines
11 KiB
Go
340 lines
11 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/security/certreload"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
util_http_client "github.com/seaweedfs/seaweedfs/weed/util/http/client"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/credentials/tls/certprovider/pemfile"
|
|
"google.golang.org/grpc/security/advancedtls"
|
|
)
|
|
|
|
// CredRefreshingInterval is the refresh cadence for gRPC mTLS certs.
|
|
// Shares its source of truth with certreload.DefaultRefreshInterval so
|
|
// a single WEED_TLS_CERT_REFRESH_INTERVAL env var tunes both gRPC and
|
|
// HTTPS cert reload.
|
|
var CredRefreshingInterval = certreload.DefaultRefreshInterval
|
|
|
|
type Authenticator struct {
|
|
AllowedWildcardDomain string
|
|
AllowedCommonNames map[string]bool
|
|
}
|
|
|
|
// SNIStrippingTransportCredentials wraps another TransportCredentials
|
|
// and strips the port from the authority in ClientHandshake to prevent
|
|
// advancedtls from using the full "host:port" as ServerName in SNI.
|
|
type SNIStrippingTransportCredentials struct {
|
|
creds credentials.TransportCredentials
|
|
}
|
|
|
|
func (s *SNIStrippingTransportCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
|
host, _, err := net.SplitHostPort(authority)
|
|
if err == nil {
|
|
authority = host
|
|
}
|
|
return s.creds.ClientHandshake(ctx, authority, rawConn)
|
|
}
|
|
|
|
func (s *SNIStrippingTransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
|
return s.creds.ServerHandshake(rawConn)
|
|
}
|
|
|
|
func (s *SNIStrippingTransportCredentials) Info() credentials.ProtocolInfo {
|
|
return s.creds.Info()
|
|
}
|
|
|
|
func (s *SNIStrippingTransportCredentials) Clone() credentials.TransportCredentials {
|
|
return &SNIStrippingTransportCredentials{creds: s.creds.Clone()}
|
|
}
|
|
|
|
func (s *SNIStrippingTransportCredentials) OverrideServerName(serverNameOverride string) error {
|
|
return s.creds.OverrideServerName(serverNameOverride)
|
|
}
|
|
|
|
func LoadServerTLS(config *util.ViperProxy, component string) (grpc.ServerOption, grpc.ServerOption) {
|
|
if config == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
serverOptions := pemfile.Options{
|
|
CertFile: config.GetString(component + ".cert"),
|
|
KeyFile: config.GetString(component + ".key"),
|
|
RefreshDuration: CredRefreshingInterval,
|
|
}
|
|
if serverOptions.CertFile == "" || serverOptions.KeyFile == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
serverIdentityProvider, err := pemfile.NewProvider(serverOptions)
|
|
if err != nil {
|
|
glog.Warningf("pemfile.NewProvider(%v) %v failed: %v", serverOptions, component, err)
|
|
return nil, nil
|
|
}
|
|
|
|
serverRootOptions := pemfile.Options{
|
|
RootFile: config.GetString("grpc.ca"),
|
|
RefreshDuration: CredRefreshingInterval,
|
|
}
|
|
serverRootProvider, err := pemfile.NewProvider(serverRootOptions)
|
|
if err != nil {
|
|
glog.Warningf("pemfile.NewProvider(%v) failed: %v", serverRootOptions, err)
|
|
return nil, nil
|
|
}
|
|
|
|
// Start a server and create a client using advancedtls API with Provider.
|
|
options := &advancedtls.Options{
|
|
IdentityOptions: advancedtls.IdentityCertificateOptions{
|
|
IdentityProvider: serverIdentityProvider,
|
|
},
|
|
RootOptions: advancedtls.RootCertificateOptions{
|
|
RootProvider: serverRootProvider,
|
|
},
|
|
RequireClientCert: true,
|
|
VerificationType: advancedtls.CertVerification,
|
|
}
|
|
options.MinTLSVersion, err = TlsVersionByName(config.GetString("tls.min_version"))
|
|
if err != nil {
|
|
glog.Warningf("tls min version parse failed, %v", err)
|
|
return nil, nil
|
|
}
|
|
options.MaxTLSVersion, err = TlsVersionByName(config.GetString("tls.max_version"))
|
|
if err != nil {
|
|
glog.Warningf("tls max version parse failed, %v", err)
|
|
return nil, nil
|
|
}
|
|
options.CipherSuites, err = TlsCipherSuiteByNames(config.GetString("tls.cipher_suites"))
|
|
if err != nil {
|
|
glog.Warningf("tls cipher suite parse failed, %v", err)
|
|
return nil, nil
|
|
}
|
|
allowedCommonNames := config.GetString(component + ".allowed_commonNames")
|
|
allowedWildcardDomain := config.GetString("grpc.allowed_wildcard_domain")
|
|
if allowedCommonNames != "" || allowedWildcardDomain != "" {
|
|
allowedCommonNamesMap := make(map[string]bool)
|
|
for _, s := range strings.Split(allowedCommonNames, ",") {
|
|
allowedCommonNamesMap[s] = true
|
|
}
|
|
auther := Authenticator{
|
|
AllowedCommonNames: allowedCommonNamesMap,
|
|
AllowedWildcardDomain: allowedWildcardDomain,
|
|
}
|
|
options.AdditionalPeerVerification = auther.Authenticate
|
|
} else {
|
|
options.AdditionalPeerVerification = func(params *advancedtls.HandshakeVerificationInfo) (*advancedtls.PostHandshakeVerificationResults, error) {
|
|
return &advancedtls.PostHandshakeVerificationResults{}, nil
|
|
}
|
|
}
|
|
ta, err := advancedtls.NewServerCreds(options)
|
|
if err != nil {
|
|
glog.Warningf("advancedtls.NewServerCreds(%v) failed: %v", options, err)
|
|
return nil, nil
|
|
}
|
|
return grpc.Creds(ta), nil
|
|
}
|
|
|
|
func LoadClientTLSFromFile(configFile string, component string) (grpc.DialOption, error) {
|
|
v := viper.New()
|
|
v.SetConfigFile(configFile)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
return nil, fmt.Errorf("failed to read security config %s: %v", configFile, err)
|
|
}
|
|
// Resolve relative PEM paths against the config file's directory.
|
|
configDir := filepath.Dir(configFile)
|
|
for _, key := range []string{"grpc.ca", component + ".cert", component + ".key", component + ".client_cert", component + ".client_key"} {
|
|
p := v.GetString(key)
|
|
if p != "" && !filepath.IsAbs(p) {
|
|
v.Set(key, filepath.Join(configDir, p))
|
|
}
|
|
}
|
|
return LoadClientTLS(util.NewViperProxy(v), component), nil
|
|
}
|
|
|
|
func LoadClientTLS(config *util.ViperProxy, component string) grpc.DialOption {
|
|
if config == nil {
|
|
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
|
|
// prefer a dedicated client certificate: CAs may issue certs with only one of the serverAuth/clientAuth EKUs
|
|
certFileName, keyFileName := config.GetString(component+".client_cert"), config.GetString(component+".client_key")
|
|
if certFileName == "" || keyFileName == "" {
|
|
if certFileName != "" || keyFileName != "" {
|
|
glog.Warningf("%s.client_cert and %s.client_key must both be set, falling back to %s.cert and %s.key", component, component, component, component)
|
|
}
|
|
certFileName, keyFileName = config.GetString(component+".cert"), config.GetString(component+".key")
|
|
}
|
|
caFileName := config.GetString("grpc.ca")
|
|
if certFileName == "" || keyFileName == "" || caFileName == "" {
|
|
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
|
|
clientOptions := pemfile.Options{
|
|
CertFile: certFileName,
|
|
KeyFile: keyFileName,
|
|
RefreshDuration: CredRefreshingInterval,
|
|
}
|
|
clientProvider, err := pemfile.NewProvider(clientOptions)
|
|
if err != nil {
|
|
glog.Warningf("pemfile.NewProvider(%v) failed %v", clientOptions, err)
|
|
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
clientRootOptions := pemfile.Options{
|
|
RootFile: config.GetString("grpc.ca"),
|
|
RefreshDuration: CredRefreshingInterval,
|
|
}
|
|
clientRootProvider, err := pemfile.NewProvider(clientRootOptions)
|
|
if err != nil {
|
|
glog.Warningf("pemfile.NewProvider(%v) failed: %v", clientRootOptions, err)
|
|
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
options := &advancedtls.Options{
|
|
IdentityOptions: advancedtls.IdentityCertificateOptions{
|
|
IdentityProvider: clientProvider,
|
|
},
|
|
AdditionalPeerVerification: func(params *advancedtls.HandshakeVerificationInfo) (*advancedtls.PostHandshakeVerificationResults, error) {
|
|
return &advancedtls.PostHandshakeVerificationResults{}, nil
|
|
},
|
|
RootOptions: advancedtls.RootCertificateOptions{
|
|
RootProvider: clientRootProvider,
|
|
},
|
|
VerificationType: advancedtls.CertVerification,
|
|
}
|
|
ta, err := advancedtls.NewClientCreds(options)
|
|
if err != nil {
|
|
glog.Warningf("advancedtls.NewClientCreds(%v) failed: %v", options, err)
|
|
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
wrapped := &SNIStrippingTransportCredentials{creds: ta}
|
|
return grpc.WithTransportCredentials(wrapped)
|
|
}
|
|
|
|
// LoadHTTPClientFromFile creates an HTTP client using the https.client TLS
|
|
// settings from the given security config file. Returns nil if HTTPS is not
|
|
// enabled in the config. This is used by filer.sync to create per-cluster
|
|
// HTTP clients when clusters use different certificates.
|
|
func LoadHTTPClientFromFile(configFile string) (*util_http_client.HTTPClient, error) {
|
|
v := viper.New()
|
|
v.SetConfigFile(configFile)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
return nil, fmt.Errorf("failed to read security config %s: %v", configFile, err)
|
|
}
|
|
|
|
if !v.GetBool("https.client.enabled") {
|
|
return nil, nil
|
|
}
|
|
|
|
configDir := filepath.Dir(configFile)
|
|
resolvePath := func(key string) string {
|
|
p := v.GetString(key)
|
|
if p != "" && !filepath.IsAbs(p) {
|
|
return filepath.Join(configDir, p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
return util_http_client.NewHttpClientWithTLS(
|
|
resolvePath("https.client.cert"),
|
|
resolvePath("https.client.key"),
|
|
resolvePath("https.client.ca"),
|
|
v.GetBool("https.client.insecure_skip_verify"),
|
|
util_http_client.AddDialContext,
|
|
)
|
|
}
|
|
|
|
func LoadClientTLSHTTP(clientCertFile string) *tls.Config {
|
|
clientCerts, err := os.ReadFile(clientCertFile)
|
|
if err != nil {
|
|
glog.Fatal(err)
|
|
}
|
|
certPool := x509.NewCertPool()
|
|
ok := certPool.AppendCertsFromPEM(clientCerts)
|
|
if !ok {
|
|
glog.Fatalf("Error processing client certificate in %s\n", clientCertFile)
|
|
}
|
|
|
|
return &tls.Config{
|
|
ClientCAs: certPool,
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
}
|
|
}
|
|
|
|
func (a Authenticator) Authenticate(params *advancedtls.HandshakeVerificationInfo) (*advancedtls.PostHandshakeVerificationResults, error) {
|
|
if a.AllowedWildcardDomain != "" && strings.HasSuffix(params.Leaf.Subject.CommonName, a.AllowedWildcardDomain) {
|
|
return &advancedtls.PostHandshakeVerificationResults{}, nil
|
|
}
|
|
if _, ok := a.AllowedCommonNames[params.Leaf.Subject.CommonName]; ok {
|
|
return &advancedtls.PostHandshakeVerificationResults{}, nil
|
|
}
|
|
err := fmt.Errorf("Authenticate: invalid subject client common name: %s", params.Leaf.Subject.CommonName)
|
|
glog.Error(err)
|
|
return nil, err
|
|
}
|
|
|
|
func FixTlsConfig(viper *util.ViperProxy, config *tls.Config) error {
|
|
var err error
|
|
config.MinVersion, err = TlsVersionByName(viper.GetString("tls.min_version"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.MaxVersion, err = TlsVersionByName(viper.GetString("tls.max_version"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.CipherSuites, err = TlsCipherSuiteByNames(viper.GetString("tls.cipher_suites"))
|
|
return err
|
|
}
|
|
|
|
func TlsVersionByName(name string) (uint16, error) {
|
|
switch name {
|
|
case "":
|
|
return 0, nil
|
|
case "SSLv3":
|
|
return tls.VersionSSL30, nil
|
|
case "TLS 1.0":
|
|
return tls.VersionTLS10, nil
|
|
case "TLS 1.1":
|
|
return tls.VersionTLS11, nil
|
|
case "TLS 1.2":
|
|
return tls.VersionTLS12, nil
|
|
case "TLS 1.3":
|
|
return tls.VersionTLS13, nil
|
|
default:
|
|
return 0, fmt.Errorf("invalid tls version %s", name)
|
|
}
|
|
}
|
|
|
|
func TlsCipherSuiteByNames(cipherSuiteNames string) ([]uint16, error) {
|
|
cipherSuiteNames = strings.TrimSpace(cipherSuiteNames)
|
|
if cipherSuiteNames == "" {
|
|
return nil, nil
|
|
}
|
|
names := strings.Split(cipherSuiteNames, ",")
|
|
cipherSuites := tls.CipherSuites()
|
|
cipherIds := make([]uint16, 0, len(names))
|
|
for _, name := range names {
|
|
name = strings.TrimSpace(name)
|
|
index := slices.IndexFunc(cipherSuites, func(suite *tls.CipherSuite) bool {
|
|
return name == suite.Name
|
|
})
|
|
if index == -1 {
|
|
return nil, fmt.Errorf("invalid tls cipher suite name %s", name)
|
|
}
|
|
cipherIds = append(cipherIds, cipherSuites[index].ID)
|
|
}
|
|
return cipherIds, nil
|
|
}
|