This is an automated email from the ASF dual-hosted git repository.

github-actions[bot] pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new 299a8c77 Fixed grpc inbound potential issues (#987)
299a8c77 is described below

commit 299a8c7738c854bea618bc3ee1f78a2cfc18829f
Author: mfordjody <[email protected]>
AuthorDate: Thu Aug 6 17:28:26 2026 +0800

    Fixed grpc inbound potential issues (#987)
---
 dubbod/discovery/cmd/app/grpc_inbound.go      | 359 +++++++++++++++++++++++---
 dubbod/discovery/cmd/app/grpc_inbound_test.go | 274 +++++++++++++++++++-
 2 files changed, 584 insertions(+), 49 deletions(-)

diff --git a/dubbod/discovery/cmd/app/grpc_inbound.go 
b/dubbod/discovery/cmd/app/grpc_inbound.go
index f31736b9..17c86954 100644
--- a/dubbod/discovery/cmd/app/grpc_inbound.go
+++ b/dubbod/discovery/cmd/app/grpc_inbound.go
@@ -24,6 +24,7 @@ import (
        "fmt"
        "io"
        "net"
+       neturl "net/url"
        "os"
        "strconv"
        "strings"
@@ -37,13 +38,16 @@ import (
 )
 
 type grpcInboundOptions struct {
-       listen         string
-       upstream       string
-       bootstrapPath  string
-       runtimeConfig  string
-       mtlsMode       string
-       acceptTimeout  time.Duration
-       connectTimeout time.Duration
+       listen            string
+       upstream          string
+       bootstrapPath     string
+       runtimeConfig     string
+       mtlsMode          string
+       trustDomain       string
+       allowedPrincipals string
+       acceptTimeout     time.Duration
+       connectTimeout    time.Duration
+       reloadInterval    time.Duration
 }
 
 type grpcInboundMTLSMode string
@@ -54,15 +58,30 @@ const (
        grpcInboundMTLSModeStrict     grpcInboundMTLSMode = "STRICT"
 )
 
+// grpcInboundReloadInterval bounds how stale the workload certificate and the
+// runtime config may be. Certificates are rotated well ahead of expiry by the
+// control plane, so polling is enough and avoids the inotify blind spot around
+// kubelet's atomic symlink swap of the mounted secret.
+const grpcInboundReloadInterval = 30 * time.Second
+
+// grpcInboundAcceptTimeout caps how long a peer may take to get through the
+// first read and the TLS handshake. Every mesh pod can reach this port, so
+// without a deadline a peer that connects and never writes pins a goroutine
+// and a file descriptor indefinitely.
+const grpcInboundAcceptTimeout = 10 * time.Second
+
 func newGRPCInboundCommand() *cobra.Command {
        opts := &grpcInboundOptions{
-               listen:         
firstNonEmpty(os.Getenv("DUBBO_GRPC_INBOUND_LISTEN"), fmt.Sprintf(":%d", 
inject.ProxylessGRPCInboundPort)),
-               upstream:       
firstNonEmpty(os.Getenv("DUBBO_GRPC_INBOUND_UPSTREAM"), "127.0.0.1:80"),
-               bootstrapPath:  os.Getenv("GRPC_XDS_BOOTSTRAP"),
-               runtimeConfig:  
firstNonEmpty(os.Getenv(inject.ProxylessGRPCConfigEnvName), 
inject.ProxylessGRPCConfigPath),
-               mtlsMode:       os.Getenv("DUBBO_GRPC_INBOUND_MTLS_MODE"),
-               acceptTimeout:  
durationSecondsFromEnv("DUBBO_GRPC_INBOUND_ACCEPT_TIMEOUT", 0),
-               connectTimeout: 
durationSecondsFromEnv("DUBBO_GRPC_INBOUND_CONNECT_TIMEOUT", 5*time.Second),
+               listen:            
firstNonEmpty(os.Getenv("DUBBO_GRPC_INBOUND_LISTEN"), fmt.Sprintf(":%d", 
inject.ProxylessGRPCInboundPort)),
+               upstream:          
firstNonEmpty(os.Getenv("DUBBO_GRPC_INBOUND_UPSTREAM"), "127.0.0.1:80"),
+               bootstrapPath:     os.Getenv("GRPC_XDS_BOOTSTRAP"),
+               runtimeConfig:     
firstNonEmpty(os.Getenv(inject.ProxylessGRPCConfigEnvName), 
inject.ProxylessGRPCConfigPath),
+               mtlsMode:          os.Getenv("DUBBO_GRPC_INBOUND_MTLS_MODE"),
+               trustDomain:       
firstNonEmpty(os.Getenv("DUBBO_GRPC_INBOUND_TRUST_DOMAIN"), 
os.Getenv("TRUST_DOMAIN")),
+               allowedPrincipals: 
os.Getenv("DUBBO_GRPC_INBOUND_ALLOWED_PRINCIPALS"),
+               acceptTimeout:     
durationSecondsFromEnv("DUBBO_GRPC_INBOUND_ACCEPT_TIMEOUT", 
grpcInboundAcceptTimeout),
+               connectTimeout:    
durationSecondsFromEnv("DUBBO_GRPC_INBOUND_CONNECT_TIMEOUT", 5*time.Second),
+               reloadInterval:    
durationSecondsFromEnv("DUBBO_GRPC_INBOUND_RELOAD_INTERVAL", 
grpcInboundReloadInterval),
        }
        c := &cobra.Command{
                Use:   "grpc-inbound",
@@ -81,8 +100,12 @@ func newGRPCInboundCommand() *cobra.Command {
        c.Flags().StringVar(&opts.bootstrapPath, "bootstrap", 
opts.bootstrapPath, "gRPC xDS bootstrap file")
        c.Flags().StringVar(&opts.runtimeConfig, "runtime-config", 
opts.runtimeConfig, "proxyless runtime config file")
        c.Flags().StringVar(&opts.mtlsMode, "mtls-mode", opts.mtlsMode, 
"override inbound mTLS mode: DISABLE, PERMISSIVE, or STRICT")
-       c.Flags().DurationVar(&opts.acceptTimeout, "accept-timeout", 
opts.acceptTimeout, "optional TLS handshake timeout")
+       c.Flags().StringVar(&opts.trustDomain, "trust-domain", 
opts.trustDomain, "trust domain peers must belong to; defaults to the trust 
domain of the workload certificate")
+       c.Flags().StringVar(&opts.allowedPrincipals, "allowed-principals", 
opts.allowedPrincipals,
+               "comma-separated peer identities allowed to connect, as 
spiffe:// URIs or ns/<namespace>/sa/<serviceaccount>; empty allows any peer in 
the trust domain")
+       c.Flags().DurationVar(&opts.acceptTimeout, "accept-timeout", 
opts.acceptTimeout, "deadline for the first read and the TLS handshake; 0 
disables it")
        c.Flags().DurationVar(&opts.connectTimeout, "connect-timeout", 
opts.connectTimeout, "timeout for connecting to the local upstream")
+       c.Flags().DurationVar(&opts.reloadInterval, "reload-interval", 
opts.reloadInterval, "how often to reload the workload certificate and runtime 
config")
        return c
 }
 
@@ -100,7 +123,18 @@ func (o *grpcInboundOptions) run(ctx context.Context) 
error {
        if err != nil {
                return err
        }
-       tlsConfig, err := grpcInboundTLSConfigFromBootstrap(bootstrap)
+       certs, err := newGRPCInboundCertStore(bootstrap)
+       if err != nil {
+               return err
+       }
+       modes := newGRPCInboundModeStore(o.runtimeConfig, 
upstreamPort(o.upstream))
+       if err := modes.reload(); err != nil {
+               // A missing or unreadable runtime config is not fatal: the 
store falls
+               // back to STRICT until a successful load, and an explicit 
--mtls-mode
+               // still overrides it.
+               log.Warnf("grpc-inbound: initial runtime config load failed: 
%v", err)
+       }
+       peers, err := o.peerPolicy(certs)
        if err != nil {
                return err
        }
@@ -109,10 +143,61 @@ func (o *grpcInboundOptions) run(ctx context.Context) 
error {
                return fmt.Errorf("listen grpc-inbound %s: %w", o.listen, err)
        }
        defer lis.Close()
-       return serveGRPCInbound(ctx, lis, tlsConfig, o.upstream, 
o.effectiveMTLSMode, o.acceptTimeout, o.connectTimeout)
+       go grpcInboundReloadLoop(ctx, o.reloadInterval, certs, modes)
+       return serveGRPCInbound(ctx, lis, certs.tlsConfig(peers), o.upstream, 
o.effectiveMTLSMode(modes), o.acceptTimeout, o.connectTimeout)
+}
+
+func (o *grpcInboundOptions) peerPolicy(certs *grpcInboundCertStore) 
(*grpcInboundPeerPolicy, error) {
+       trustDomain := firstNonEmpty(strings.TrimSpace(o.trustDomain), 
certs.trustDomain())
+       allowed, err := parseGRPCInboundPrincipals(o.allowedPrincipals, 
trustDomain)
+       if err != nil {
+               return nil, err
+       }
+       if trustDomain == "" {
+               log.Warnf("grpc-inbound: no trust domain configured and the 
workload certificate carries no SPIFFE identity; peer identity is not checked")
+       }
+       return &grpcInboundPeerPolicy{trustDomain: trustDomain, allowed: 
allowed}, nil
 }
 
-func grpcInboundTLSConfigFromBootstrap(bootstrap *xdsresolver.BootstrapConfig) 
(*tls.Config, error) {
+// grpcInboundReloadLoop keeps the workload certificate and the runtime config
+// in sync with the mounted secret. Without it the process would serve the
+// key pair it loaded at startup until the pod is restarted, so inbound mTLS
+// would break as soon as the control plane rotates the certificate.
+func grpcInboundReloadLoop(ctx context.Context, interval time.Duration, certs 
*grpcInboundCertStore, modes *grpcInboundModeStore) {
+       if interval <= 0 {
+               interval = grpcInboundReloadInterval
+       }
+       ticker := time.NewTicker(interval)
+       defer ticker.Stop()
+       for {
+               select {
+               case <-ctx.Done():
+                       return
+               case <-ticker.C:
+                       if err := certs.reload(); err != nil {
+                               log.Warnf("grpc-inbound: certificate reload 
failed, keeping previous material: %v", err)
+                       }
+                       if err := modes.reload(); err != nil {
+                               log.Warnf("grpc-inbound: runtime config reload 
failed, keeping previous mode: %v", err)
+                       }
+               }
+       }
+}
+
+// grpcInboundCertStore holds the currently loaded workload key pair and trust
+// bundle. Handshakes read through it, so a reload takes effect on the next
+// connection without dropping established ones.
+type grpcInboundCertStore struct {
+       certFile string
+       keyFile  string
+       caFile   string
+
+       mu        sync.RWMutex
+       cert      *tls.Certificate
+       clientCAs *x509.CertPool
+}
+
+func newGRPCInboundCertStore(bootstrap *xdsresolver.BootstrapConfig) 
(*grpcInboundCertStore, error) {
        if bootstrap == nil {
                return nil, fmt.Errorf("bootstrap config is nil")
        }
@@ -126,24 +211,168 @@ func grpcInboundTLSConfigFromBootstrap(bootstrap 
*xdsresolver.BootstrapConfig) (
        if cfg.CACertificateFile == "" {
                return nil, fmt.Errorf("grpc-inbound mTLS requires 
ca_certificate_file")
        }
-       cert, err := tls.LoadX509KeyPair(cfg.CertificateFile, 
cfg.PrivateKeyFile)
+       store := &grpcInboundCertStore{
+               certFile: cfg.CertificateFile,
+               keyFile:  cfg.PrivateKeyFile,
+               caFile:   cfg.CACertificateFile,
+       }
+       if err := store.reload(); err != nil {
+               return nil, err
+       }
+       return store, nil
+}
+
+func (s *grpcInboundCertStore) reload() error {
+       cert, err := tls.LoadX509KeyPair(s.certFile, s.keyFile)
        if err != nil {
-               return nil, fmt.Errorf("load grpc-inbound certificate/key: %w", 
err)
+               return fmt.Errorf("load grpc-inbound certificate/key: %w", err)
        }
-       rootPEM, err := os.ReadFile(cfg.CACertificateFile)
+       // Leaf is needed to read the workload's own SPIFFE identity; older Go
+       // releases leave it nil after LoadX509KeyPair.
+       if cert.Leaf == nil && len(cert.Certificate) > 0 {
+               leaf, err := x509.ParseCertificate(cert.Certificate[0])
+               if err != nil {
+                       return fmt.Errorf("parse grpc-inbound certificate %s: 
%w", s.certFile, err)
+               }
+               cert.Leaf = leaf
+       }
+       rootPEM, err := os.ReadFile(s.caFile)
        if err != nil {
-               return nil, fmt.Errorf("read grpc-inbound CA certificate %s: 
%w", cfg.CACertificateFile, err)
+               return fmt.Errorf("read grpc-inbound CA certificate %s: %w", 
s.caFile, err)
        }
        clientCAs := x509.NewCertPool()
        if !clientCAs.AppendCertsFromPEM(rootPEM) {
-               return nil, fmt.Errorf("parse grpc-inbound CA certificate %s: 
no certificates found", cfg.CACertificateFile)
+               return fmt.Errorf("parse grpc-inbound CA certificate %s: no 
certificates found", s.caFile)
        }
+       s.mu.Lock()
+       defer s.mu.Unlock()
+       s.cert = &cert
+       s.clientCAs = clientCAs
+       return nil
+}
+
+func (s *grpcInboundCertStore) current() (*tls.Certificate, *x509.CertPool) {
+       s.mu.RLock()
+       defer s.mu.RUnlock()
+       return s.cert, s.clientCAs
+}
+
+// tlsConfig returns a config whose material is resolved per handshake.
+// GetConfigForClient is used rather than GetCertificate because the trust
+// bundle rotates alongside the key pair and ClientCAs cannot be swapped from
+// the certificate callback.
+func (s *grpcInboundCertStore) tlsConfig(peers *grpcInboundPeerPolicy) 
*tls.Config {
        return &tls.Config{
-               MinVersion:   tls.VersionTLS12,
-               Certificates: []tls.Certificate{cert},
-               ClientCAs:    clientCAs,
-               ClientAuth:   tls.RequireAndVerifyClientCert,
-       }, nil
+               MinVersion: tls.VersionTLS12,
+               ClientAuth: tls.RequireAndVerifyClientCert,
+               GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, 
error) {
+                       cert, clientCAs := s.current()
+                       if cert == nil || clientCAs == nil {
+                               return nil, fmt.Errorf("grpc-inbound 
certificate material is not loaded")
+                       }
+                       return &tls.Config{
+                               MinVersion:            tls.VersionTLS12,
+                               Certificates:          []tls.Certificate{*cert},
+                               ClientCAs:             clientCAs,
+                               ClientAuth:            
tls.RequireAndVerifyClientCert,
+                               VerifyPeerCertificate: 
peers.verifyPeerCertificate,
+                       }, nil
+               },
+       }
+}
+
+// trustDomain reports the trust domain of the workload's own SPIFFE identity,
+// used as the default peer trust domain when none is configured explicitly.
+func (s *grpcInboundCertStore) trustDomain() string {
+       cert, _ := s.current()
+       if cert == nil || cert.Leaf == nil {
+               return ""
+       }
+       for _, id := range spiffeIdentities(cert.Leaf) {
+               return id.Host
+       }
+       return ""
+}
+
+// grpcInboundPeerPolicy authorizes an authenticated peer. Chain verification
+// alone only proves the peer holds a certificate signed by the mesh CA, which
+// makes every workload in the mesh a valid caller for every other workload.
+// This narrows that to a trust domain and, when configured, to an explicit set
+// of SPIFFE identities.
+type grpcInboundPeerPolicy struct {
+       trustDomain string
+       allowed     map[string]struct{}
+}
+
+// verifyPeerCertificate runs after chain verification, so verifiedChains is
+// non-empty and its leaf is already trusted. It only decides whether that
+// proven identity may talk to this workload.
+func (p *grpcInboundPeerPolicy) verifyPeerCertificate(_ [][]byte, 
verifiedChains [][]*x509.Certificate) error {
+       if p == nil || (p.trustDomain == "" && len(p.allowed) == 0) {
+               return nil
+       }
+       if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
+               return fmt.Errorf("grpc-inbound: peer presented no verified 
certificate chain")
+       }
+       identities := spiffeIdentities(verifiedChains[0][0])
+       if len(identities) == 0 {
+               return fmt.Errorf("grpc-inbound: peer certificate carries no 
SPIFFE identity")
+       }
+       for _, id := range identities {
+               if p.trustDomain != "" && id.Host != p.trustDomain {
+                       continue
+               }
+               if len(p.allowed) == 0 {
+                       return nil
+               }
+               if _, ok := p.allowed[id.String()]; ok {
+                       return nil
+               }
+       }
+       return fmt.Errorf("grpc-inbound: peer identity %s is not authorized", 
identities[0])
+}
+
+// spiffeIdentities returns the SPIFFE URI SANs of a certificate. A workload
+// certificate normally carries exactly one.
+func spiffeIdentities(cert *x509.Certificate) []*neturl.URL {
+       if cert == nil {
+               return nil
+       }
+       out := make([]*neturl.URL, 0, len(cert.URIs))
+       for _, uri := range cert.URIs {
+               if uri != nil && uri.Scheme == "spiffe" {
+                       out = append(out, uri)
+               }
+       }
+       return out
+}
+
+// parseGRPCInboundPrincipals accepts full spiffe:// URIs or the
+// ns/<namespace>/sa/<serviceaccount> shorthand, which is expanded against the
+// trust domain.
+func parseGRPCInboundPrincipals(list, trustDomain string) 
(map[string]struct{}, error) {
+       out := map[string]struct{}{}
+       for _, raw := range strings.Split(list, ",") {
+               entry := strings.TrimSpace(raw)
+               if entry == "" {
+                       continue
+               }
+               if !strings.HasPrefix(entry, "spiffe://") {
+                       if trustDomain == "" {
+                               return nil, fmt.Errorf("principal %q needs a 
trust domain: set --trust-domain or use a full spiffe:// URI", entry)
+                       }
+                       entry = "spiffe://" + trustDomain + "/" + 
strings.TrimPrefix(entry, "/")
+               }
+               parsed, err := neturl.Parse(entry)
+               if err != nil {
+                       return nil, fmt.Errorf("parse principal %q: %w", raw, 
err)
+               }
+               out[parsed.String()] = struct{}{}
+       }
+       if len(out) == 0 {
+               return nil, nil
+       }
+       return out, nil
 }
 
 func serveGRPCInbound(ctx context.Context, lis net.Listener, tlsConfig 
*tls.Config, upstream string, mode func() grpcInboundMTLSMode, acceptTimeout, 
connectTimeout time.Duration) error {
@@ -216,14 +445,54 @@ func isTLSClientHello(first byte) bool {
        return first == 0x16
 }
 
-func (o *grpcInboundOptions) effectiveMTLSMode() grpcInboundMTLSMode {
+func (o *grpcInboundOptions) effectiveMTLSMode(modes *grpcInboundModeStore) 
func() grpcInboundMTLSMode {
        if mode, ok := parseGRPCInboundMTLSMode(o.mtlsMode); ok {
-               return mode
+               return func() grpcInboundMTLSMode { return mode }
        }
-       if mode, ok := grpcInboundMTLSModeFromRuntimeConfig(o.runtimeConfig, 
upstreamPort(o.upstream)); ok {
-               return mode
+       return modes.current
+}
+
+// grpcInboundModeStore caches the inbound mTLS mode read from the runtime
+// config. Reading it per connection would put a file read and a full JSON
+// parse on the accept path, and would let a transient read error silently
+// downgrade a STRICT port to PERMISSIVE.
+type grpcInboundModeStore struct {
+       path string
+       port int
+
+       mu     sync.RWMutex
+       mode   grpcInboundMTLSMode
+       loaded bool
+}
+
+func newGRPCInboundModeStore(path string, port int) *grpcInboundModeStore {
+       return &grpcInboundModeStore{path: path, port: port}
+}
+
+// current fails closed: until the runtime config has been read successfully
+// at least once, inbound traffic must present a client certificate.
+func (s *grpcInboundModeStore) current() grpcInboundMTLSMode {
+       s.mu.RLock()
+       defer s.mu.RUnlock()
+       if !s.loaded {
+               return grpcInboundMTLSModeStrict
+       }
+       return s.mode
+}
+
+// reload replaces the cached mode only on a successful read and parse. Any
+// failure leaves the last known good mode in place, so a remount race or a
+// truncated write cannot relax the policy.
+func (s *grpcInboundModeStore) reload() error {
+       mode, err := loadGRPCInboundMTLSMode(s.path, s.port)
+       if err != nil {
+               return err
        }
-       return grpcInboundMTLSModePermissive
+       s.mu.Lock()
+       defer s.mu.Unlock()
+       s.mode = mode
+       s.loaded = true
+       return nil
 }
 
 func parseGRPCInboundMTLSMode(mode string) (grpcInboundMTLSMode, bool) {
@@ -251,13 +520,21 @@ func upstreamPort(upstream string) int {
        return out
 }
 
-func grpcInboundMTLSModeFromRuntimeConfig(path string, port int) 
(grpcInboundMTLSMode, bool) {
+// loadGRPCInboundMTLSMode reads the inbound mTLS mode for port from the 
runtime
+// config. An absent config means the workload is unconfigured and yields
+// PERMISSIVE, matching a standalone run with no mounted secret. A config that
+// exists but cannot be read or parsed returns an error so the caller can keep
+// the last known mode instead of relaxing the policy.
+func loadGRPCInboundMTLSMode(path string, port int) (grpcInboundMTLSMode, 
error) {
        if path == "" {
-               return "", false
+               return grpcInboundMTLSModePermissive, nil
        }
        data, err := os.ReadFile(path)
        if err != nil {
-               return "", false
+               if os.IsNotExist(err) {
+                       return grpcInboundMTLSModePermissive, nil
+               }
+               return "", fmt.Errorf("read runtime config %s: %w", path, err)
        }
        var cfg struct {
                Services []struct {
@@ -268,7 +545,7 @@ func grpcInboundMTLSModeFromRuntimeConfig(path string, port 
int) (grpcInboundMTL
                } `json:"services"`
        }
        if err := json.Unmarshal(data, &cfg); err != nil {
-               return "", false
+               return "", fmt.Errorf("parse runtime config %s: %w", path, err)
        }
 
        foundDisable := false
@@ -283,19 +560,19 @@ func grpcInboundMTLSModeFromRuntimeConfig(path string, 
port int) (grpcInboundMTL
                                continue
                        }
                        if mode == grpcInboundMTLSModeStrict {
-                               return grpcInboundMTLSModeStrict, true
+                               return grpcInboundMTLSModeStrict, nil
                        }
                        foundPermissive = foundPermissive || mode == 
grpcInboundMTLSModePermissive
                        foundDisable = foundDisable || mode == 
grpcInboundMTLSModeDisable
                }
        }
        if foundPermissive {
-               return grpcInboundMTLSModePermissive, true
+               return grpcInboundMTLSModePermissive, nil
        }
        if foundDisable {
-               return grpcInboundMTLSModeDisable, true
+               return grpcInboundMTLSModeDisable, nil
        }
-       return "", false
+       return grpcInboundMTLSModePermissive, nil
 }
 
 func copyBothDirections(a, b net.Conn) {
diff --git a/dubbod/discovery/cmd/app/grpc_inbound_test.go 
b/dubbod/discovery/cmd/app/grpc_inbound_test.go
index adfc6433..25aac4b0 100644
--- a/dubbod/discovery/cmd/app/grpc_inbound_test.go
+++ b/dubbod/discovery/cmd/app/grpc_inbound_test.go
@@ -29,6 +29,7 @@ import (
        "net"
        "net/http"
        "net/http/httptest"
+       neturl "net/url"
        "os"
        "path/filepath"
        "testing"
@@ -48,7 +49,7 @@ func TestGRPCInboundRequiresClientCertificateAndProxiesHTTP(t 
*testing.T) {
        writePEM(t, filepath.Join(dir, "client-cert.pem"), "CERTIFICATE", 
clientCert.Raw)
        writePEM(t, filepath.Join(dir, "client-key.pem"), "RSA PRIVATE KEY", 
x509.MarshalPKCS1PrivateKey(clientKey))
 
-       tlsConfig, err := 
grpcInboundTLSConfigFromBootstrap(&xdsresolver.BootstrapConfig{
+       certs, err := newGRPCInboundCertStore(&xdsresolver.BootstrapConfig{
                CertProviders: map[string]xdsresolver.FileWatcherCertConfig{
                        "default": {
                                CertificateFile:   filepath.Join(dir, 
"cert-chain.pem"),
@@ -58,8 +59,9 @@ func TestGRPCInboundRequiresClientCertificateAndProxiesHTTP(t 
*testing.T) {
                },
        })
        if err != nil {
-               t.Fatalf("grpcInboundTLSConfigFromBootstrap() failed: %v", err)
+               t.Fatalf("newGRPCInboundCertStore() failed: %v", err)
        }
+       tlsConfig := certs.tlsConfig(nil)
 
        upstream := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, _ *http.Request) {
                _, _ = fmt.Fprintln(w, "nginx v1")
@@ -136,7 +138,7 @@ func TestGRPCInboundPermissiveAcceptsPlaintextAndMTLS(t 
*testing.T) {
        writePEM(t, filepath.Join(dir, "client-cert.pem"), "CERTIFICATE", 
clientCert.Raw)
        writePEM(t, filepath.Join(dir, "client-key.pem"), "RSA PRIVATE KEY", 
x509.MarshalPKCS1PrivateKey(clientKey))
 
-       tlsConfig, err := 
grpcInboundTLSConfigFromBootstrap(&xdsresolver.BootstrapConfig{
+       certs, err := newGRPCInboundCertStore(&xdsresolver.BootstrapConfig{
                CertProviders: map[string]xdsresolver.FileWatcherCertConfig{
                        "default": {
                                CertificateFile:   filepath.Join(dir, 
"cert-chain.pem"),
@@ -146,8 +148,9 @@ func TestGRPCInboundPermissiveAcceptsPlaintextAndMTLS(t 
*testing.T) {
                },
        })
        if err != nil {
-               t.Fatalf("grpcInboundTLSConfigFromBootstrap() failed: %v", err)
+               t.Fatalf("newGRPCInboundCertStore() failed: %v", err)
        }
+       tlsConfig := certs.tlsConfig(nil)
 
        upstream := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, _ *http.Request) {
                _, _ = fmt.Fprintln(w, "nginx v1")
@@ -223,11 +226,88 @@ func TestGRPCInboundMTLSModeFromRuntimeConfig(t 
*testing.T) {
                t.Fatalf("os.WriteFile() failed: %v", err)
        }
 
-       if got, ok := grpcInboundMTLSModeFromRuntimeConfig(path, 80); !ok || 
got != grpcInboundMTLSModePermissive {
-               t.Fatalf("mode for 80 = %q, %v; want PERMISSIVE, true", got, ok)
+       if got, err := loadGRPCInboundMTLSMode(path, 80); err != nil || got != 
grpcInboundMTLSModePermissive {
+               t.Fatalf("mode for 80 = %q, %v; want PERMISSIVE, nil", got, err)
        }
-       if got, ok := grpcInboundMTLSModeFromRuntimeConfig(path, 8080); !ok || 
got != grpcInboundMTLSModeStrict {
-               t.Fatalf("mode for 8080 = %q, %v; want STRICT, true", got, ok)
+       if got, err := loadGRPCInboundMTLSMode(path, 8080); err != nil || got 
!= grpcInboundMTLSModeStrict {
+               t.Fatalf("mode for 8080 = %q, %v; want STRICT, nil", got, err)
+       }
+}
+
+func TestGRPCInboundModeStoreDoesNotDowngradeOnReadFailure(t *testing.T) {
+       path := filepath.Join(t.TempDir(), "dubbo-grpc-xds.json")
+       if err := os.WriteFile(path, 
[]byte(`{"services":[{"ports":[{"port":8080,"mtlsMode":"STRICT"}]}]}`), 0o600); 
err != nil {
+               t.Fatalf("os.WriteFile() failed: %v", err)
+       }
+       store := newGRPCInboundModeStore(path, 8080)
+
+       if got := store.current(); got != grpcInboundMTLSModeStrict {
+               t.Fatalf("mode before first load = %q, want STRICT", got)
+       }
+       if err := store.reload(); err != nil {
+               t.Fatalf("reload() failed: %v", err)
+       }
+       if got := store.current(); got != grpcInboundMTLSModeStrict {
+               t.Fatalf("mode after load = %q, want STRICT", got)
+       }
+
+       // A truncated write must not relax the policy to PERMISSIVE.
+       if err := os.WriteFile(path, []byte(`{"services":`), 0o600); err != nil 
{
+               t.Fatalf("os.WriteFile(truncated) failed: %v", err)
+       }
+       if err := store.reload(); err == nil {
+               t.Fatalf("reload() on malformed config returned nil error")
+       }
+       if got := store.current(); got != grpcInboundMTLSModeStrict {
+               t.Fatalf("mode after failed reload = %q, want STRICT", got)
+       }
+}
+
+func TestGRPCInboundModeStoreMissingConfigIsPermissive(t *testing.T) {
+       store := newGRPCInboundModeStore(filepath.Join(t.TempDir(), 
"absent.json"), 8080)
+       if err := store.reload(); err != nil {
+               t.Fatalf("reload() on absent config failed: %v", err)
+       }
+       if got := store.current(); got != grpcInboundMTLSModePermissive {
+               t.Fatalf("mode for absent config = %q, want PERMISSIVE", got)
+       }
+}
+
+func TestGRPCInboundCertStoreReloadsRotatedCertificate(t *testing.T) {
+       caCert, caKey := newTestCA(t)
+       firstCert, firstKey := newSignedCert(t, caCert, caKey, "grpc-inbound")
+       dir := t.TempDir()
+       writePEM(t, filepath.Join(dir, "root-cert.pem"), "CERTIFICATE", 
caCert.Raw)
+       writePEM(t, filepath.Join(dir, "cert-chain.pem"), "CERTIFICATE", 
firstCert.Raw)
+       writePEM(t, filepath.Join(dir, "key.pem"), "RSA PRIVATE KEY", 
x509.MarshalPKCS1PrivateKey(firstKey))
+
+       store, err := newGRPCInboundCertStore(&xdsresolver.BootstrapConfig{
+               CertProviders: map[string]xdsresolver.FileWatcherCertConfig{
+                       "default": {
+                               CertificateFile:   filepath.Join(dir, 
"cert-chain.pem"),
+                               PrivateKeyFile:    filepath.Join(dir, 
"key.pem"),
+                               CACertificateFile: filepath.Join(dir, 
"root-cert.pem"),
+                       },
+               },
+       })
+       if err != nil {
+               t.Fatalf("newGRPCInboundCertStore() failed: %v", err)
+       }
+       before, _ := store.current()
+
+       rotatedCert, rotatedKey := newSignedCert(t, caCert, caKey, 
"grpc-inbound")
+       writePEM(t, filepath.Join(dir, "cert-chain.pem"), "CERTIFICATE", 
rotatedCert.Raw)
+       writePEM(t, filepath.Join(dir, "key.pem"), "RSA PRIVATE KEY", 
x509.MarshalPKCS1PrivateKey(rotatedKey))
+       if err := store.reload(); err != nil {
+               t.Fatalf("reload() failed: %v", err)
+       }
+
+       after, _ := store.current()
+       if string(before.Certificate[0]) == string(after.Certificate[0]) {
+               t.Fatalf("certificate was not reloaded after rotation")
+       }
+       if string(after.Certificate[0]) != string(rotatedCert.Raw) {
+               t.Fatalf("reloaded certificate does not match the rotated one")
        }
 }
 
@@ -298,3 +378,181 @@ func writePEM(t *testing.T, path, typ string, der []byte) 
{
                t.Fatalf("pem.Encode(%s) failed: %v", path, err)
        }
 }
+
+func TestGRPCInboundDefaultAcceptTimeoutIsSet(t *testing.T) {
+       t.Setenv("DUBBO_GRPC_INBOUND_ACCEPT_TIMEOUT", "")
+       cmd := newGRPCInboundCommand()
+       flag := cmd.Flags().Lookup("accept-timeout")
+       if flag == nil {
+               t.Fatalf("accept-timeout flag is missing")
+       }
+       if got, want := flag.DefValue, grpcInboundAcceptTimeout.String(); got 
!= want {
+               t.Fatalf("accept-timeout default = %q, want %q", got, want)
+       }
+}
+
+func TestGRPCInboundPeerPolicy(t *testing.T) {
+       caCert, caKey := newTestCA(t)
+       local := newSPIFFECert(t, caCert, caKey, 
"spiffe://cluster.local/ns/default/sa/reviews")
+       foreign := newSPIFFECert(t, caCert, caKey, 
"spiffe://evil.example/ns/default/sa/reviews")
+       other := newSPIFFECert(t, caCert, caKey, 
"spiffe://cluster.local/ns/default/sa/ratings")
+       noIdentity, _ := newSignedCert(t, caCert, caKey, "no-spiffe")
+
+       cases := []struct {
+               name    string
+               policy  *grpcInboundPeerPolicy
+               peer    *x509.Certificate
+               wantErr bool
+       }{
+               {
+                       name:   "nil policy allows any peer",
+                       policy: nil,
+                       peer:   foreign,
+               },
+               {
+                       name:   "empty policy allows any peer",
+                       policy: &grpcInboundPeerPolicy{},
+                       peer:   foreign,
+               },
+               {
+                       name:   "matching trust domain is allowed",
+                       policy: &grpcInboundPeerPolicy{trustDomain: 
"cluster.local"},
+                       peer:   local,
+               },
+               {
+                       name:    "foreign trust domain is rejected",
+                       policy:  &grpcInboundPeerPolicy{trustDomain: 
"cluster.local"},
+                       peer:    foreign,
+                       wantErr: true,
+               },
+               {
+                       name:    "certificate without SPIFFE identity is 
rejected",
+                       policy:  &grpcInboundPeerPolicy{trustDomain: 
"cluster.local"},
+                       peer:    noIdentity,
+                       wantErr: true,
+               },
+               {
+                       name: "listed principal is allowed",
+                       policy: &grpcInboundPeerPolicy{
+                               trustDomain: "cluster.local",
+                               allowed:     
map[string]struct{}{"spiffe://cluster.local/ns/default/sa/reviews": {}},
+                       },
+                       peer: local,
+               },
+               {
+                       name: "unlisted principal is rejected",
+                       policy: &grpcInboundPeerPolicy{
+                               trustDomain: "cluster.local",
+                               allowed:     
map[string]struct{}{"spiffe://cluster.local/ns/default/sa/reviews": {}},
+                       },
+                       peer:    other,
+                       wantErr: true,
+               },
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       err := tc.policy.verifyPeerCertificate(nil, 
[][]*x509.Certificate{{tc.peer}})
+                       if tc.wantErr && err == nil {
+                               t.Fatalf("verifyPeerCertificate() = nil, want 
error")
+                       }
+                       if !tc.wantErr && err != nil {
+                               t.Fatalf("verifyPeerCertificate() = %v, want 
nil", err)
+                       }
+               })
+       }
+}
+
+func TestGRPCInboundPeerPolicyRejectsEmptyChain(t *testing.T) {
+       policy := &grpcInboundPeerPolicy{trustDomain: "cluster.local"}
+       if err := policy.verifyPeerCertificate(nil, nil); err == nil {
+               t.Fatalf("verifyPeerCertificate() with no chain = nil, want 
error")
+       }
+}
+
+func TestParseGRPCInboundPrincipals(t *testing.T) {
+       allowed, err := parseGRPCInboundPrincipals("ns/default/sa/reviews, 
spiffe://other.mesh/ns/x/sa/y", "cluster.local")
+       if err != nil {
+               t.Fatalf("parseGRPCInboundPrincipals() failed: %v", err)
+       }
+       for _, want := range []string{
+               "spiffe://cluster.local/ns/default/sa/reviews",
+               "spiffe://other.mesh/ns/x/sa/y",
+       } {
+               if _, ok := allowed[want]; !ok {
+                       t.Fatalf("principal %q missing from %v", want, allowed)
+               }
+       }
+
+       if got, err := parseGRPCInboundPrincipals("", ""); err != nil || got != 
nil {
+               t.Fatalf("parseGRPCInboundPrincipals(empty) = %v, %v; want nil, 
nil", got, err)
+       }
+       if _, err := parseGRPCInboundPrincipals("ns/default/sa/reviews", ""); 
err == nil {
+               t.Fatalf("shorthand principal without trust domain returned nil 
error")
+       }
+}
+
+func TestGRPCInboundCertStoreTrustDomain(t *testing.T) {
+       caCert, caKey := newTestCA(t)
+       leaf, key := newSPIFFECertWithKey(t, caCert, caKey, 
"spiffe://cluster.local/ns/default/sa/reviews")
+       dir := t.TempDir()
+       writePEM(t, filepath.Join(dir, "root-cert.pem"), "CERTIFICATE", 
caCert.Raw)
+       writePEM(t, filepath.Join(dir, "cert-chain.pem"), "CERTIFICATE", 
leaf.Raw)
+       writePEM(t, filepath.Join(dir, "key.pem"), "RSA PRIVATE KEY", 
x509.MarshalPKCS1PrivateKey(key))
+
+       store, err := newGRPCInboundCertStore(&xdsresolver.BootstrapConfig{
+               CertProviders: map[string]xdsresolver.FileWatcherCertConfig{
+                       "default": {
+                               CertificateFile:   filepath.Join(dir, 
"cert-chain.pem"),
+                               PrivateKeyFile:    filepath.Join(dir, 
"key.pem"),
+                               CACertificateFile: filepath.Join(dir, 
"root-cert.pem"),
+                       },
+               },
+       })
+       if err != nil {
+               t.Fatalf("newGRPCInboundCertStore() failed: %v", err)
+       }
+       if got, want := store.trustDomain(), "cluster.local"; got != want {
+               t.Fatalf("trustDomain() = %q, want %q", got, want)
+       }
+}
+
+func newSPIFFECert(t *testing.T, caCert *x509.Certificate, caKey 
*rsa.PrivateKey, id string) *x509.Certificate {
+       t.Helper()
+       cert, _ := newSPIFFECertWithKey(t, caCert, caKey, id)
+       return cert
+}
+
+func newSPIFFECertWithKey(t *testing.T, caCert *x509.Certificate, caKey 
*rsa.PrivateKey, id string) (*x509.Certificate, *rsa.PrivateKey) {
+       t.Helper()
+       uri, err := neturl.Parse(id)
+       if err != nil {
+               t.Fatalf("neturl.Parse(%s) failed: %v", id, err)
+       }
+       key, err := rsa.GenerateKey(rand.Reader, 2048)
+       if err != nil {
+               t.Fatalf("rsa.GenerateKey() failed: %v", err)
+       }
+       serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
+       if err != nil {
+               t.Fatalf("rand.Int() failed: %v", err)
+       }
+       tmpl := &x509.Certificate{
+               SerialNumber: serial,
+               Subject:      pkix.Name{CommonName: id},
+               NotBefore:    time.Now().Add(-time.Minute),
+               NotAfter:     time.Now().Add(time.Hour),
+               KeyUsage:     x509.KeyUsageDigitalSignature | 
x509.KeyUsageKeyEncipherment,
+               ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, 
x509.ExtKeyUsageClientAuth},
+               URIs:         []*neturl.URL{uri},
+       }
+       der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, 
&key.PublicKey, caKey)
+       if err != nil {
+               t.Fatalf("x509.CreateCertificate(%s) failed: %v", id, err)
+       }
+       cert, err := x509.ParseCertificate(der)
+       if err != nil {
+               t.Fatalf("x509.ParseCertificate(%s) failed: %v", id, err)
+       }
+       return cert, key
+}

Reply via email to