diff --git a/weed/s3api/s3api_server_grpc.go b/weed/s3api/s3api_server_grpc.go index 059bb14ba..3613d993d 100644 --- a/weed/s3api/s3api_server_grpc.go +++ b/weed/s3api/s3api_server_grpc.go @@ -2,7 +2,6 @@ package s3api import ( "context" - "net" "strings" "github.com/seaweedfs/seaweedfs/weed/glog" @@ -10,7 +9,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/security" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) @@ -56,46 +54,7 @@ func (s3a *S3ApiServer) checkAdminAuth(ctx context.Context) error { } func checkLocalPeer(ctx context.Context) error { - if ctx == nil { - return status.Error(codes.Unauthenticated, "admin gRPC calls require jwt.filer_signing.key or a local client") - } - pr, ok := peer.FromContext(ctx) - if !ok { - return status.Error(codes.Unauthenticated, "admin gRPC calls require jwt.filer_signing.key or a local client") - } - if _, isUnix := pr.Addr.(*net.UnixAddr); isUnix { - return nil - } - var ip net.IP - if tcpAddr, ok := pr.Addr.(*net.TCPAddr); ok { - ip = tcpAddr.IP - } else if h, _, err := net.SplitHostPort(pr.Addr.String()); err == nil { - ip = net.ParseIP(h) - } else { - ip = net.ParseIP(pr.Addr.String()) - } - if ip != nil && (ip.IsLoopback() || isLocalAddress(ip)) { - return nil - } - glog.V(1).Infof("rejected unauthenticated admin gRPC call from %s: no jwt.filer_signing.key configured", pr.Addr) - return status.Error(codes.Unauthenticated, "admin gRPC calls require jwt.filer_signing.key or a local client") -} - -// isLocalAddress enumerates interfaces per call: only reachable for -// non-loopback TCP peers on the no-key admin path, which is low-volume — -// and a cached set would keep trusting an address after it is removed from -// the host and reassigned. -func isLocalAddress(ip net.IP) bool { - addrs, err := net.InterfaceAddrs() - if err != nil { - return false - } - for _, a := range addrs { - if ipNet, ok := a.(*net.IPNet); ok && ipNet.IP.Equal(ip) { - return true - } - } - return false + return security.CheckLocalGrpcPeer(ctx) } func (s3a *S3ApiServer) PutIdentity(ctx context.Context, req *iam_pb.PutIdentityRequest) (*iam_pb.PutIdentityResponse, error) { diff --git a/weed/security/grpc_peer.go b/weed/security/grpc_peer.go new file mode 100644 index 000000000..a6b1fba95 --- /dev/null +++ b/weed/security/grpc_peer.go @@ -0,0 +1,113 @@ +package security + +import ( + "context" + "net" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +// CheckGrpcAdminPeer authenticates the peer of an admin-plane gRPC handler: +// a client certificate verified by the server's TLS config, or a Bearer token +// signed by signingKey (jwt.filer_signing.key). With no key configured remote +// callers cannot be told apart, so only local clients (unix socket, loopback, +// or an address assigned to this host) are allowed. +func CheckGrpcAdminPeer(ctx context.Context, signingKey SigningKey) error { + if isMtlsVerifiedPeer(ctx) { + return nil + } + if len(signingKey) == 0 { + return CheckLocalGrpcPeer(ctx) + } + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return status.Error(codes.Unauthenticated, "missing metadata") + } + authHeaders := md.Get("authorization") + if len(authHeaders) == 0 { + return status.Error(codes.Unauthenticated, "missing authorization metadata") + } + raw := strings.TrimSpace(authHeaders[0]) + parts := strings.Fields(raw) + if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] == "" { + return status.Error(codes.Unauthenticated, "authorization header must use Bearer scheme") + } + parsed, err := DecodeJwt(signingKey, EncodedJwt(parts[1]), &SeaweedFilerAdminClaims{}) + if err != nil || parsed == nil || !parsed.Valid { + return status.Error(codes.Unauthenticated, "invalid admin token") + } + if claims, ok := parsed.Claims.(*SeaweedFilerAdminClaims); !ok || claims.SessionId != "" { + return status.Error(codes.Unauthenticated, "invalid admin token") + } + return nil +} + +// GrpcAdminPeerCredentials returns outgoing metadata carrying a Bearer token +// for admin-plane gRPC handlers, or nil when no signing key is configured. +func GrpcAdminPeerCredentials(signingKey SigningKey, expiresAfterSec int) metadata.MD { + token := GenJwtForFilerAdmin(signingKey, expiresAfterSec) + if token == "" { + return nil + } + return metadata.Pairs("authorization", "Bearer "+string(token)) +} + +func isMtlsVerifiedPeer(ctx context.Context) bool { + pr, ok := peer.FromContext(ctx) + if !ok { + return false + } + tlsInfo, ok := pr.AuthInfo.(credentials.TLSInfo) + return ok && len(tlsInfo.State.VerifiedChains) > 0 +} + +// CheckLocalGrpcPeer admits only clients on this host: unix sockets, loopback, +// or an address assigned to a local interface. +func CheckLocalGrpcPeer(ctx context.Context) error { + if ctx == nil { + return status.Error(codes.Unauthenticated, "admin gRPC calls require jwt.filer_signing.key or a local client") + } + pr, ok := peer.FromContext(ctx) + if !ok { + return status.Error(codes.Unauthenticated, "admin gRPC calls require jwt.filer_signing.key or a local client") + } + if _, isUnix := pr.Addr.(*net.UnixAddr); isUnix { + return nil + } + var ip net.IP + if tcpAddr, ok := pr.Addr.(*net.TCPAddr); ok { + ip = tcpAddr.IP + } else if h, _, err := net.SplitHostPort(pr.Addr.String()); err == nil { + ip = net.ParseIP(h) + } else { + ip = net.ParseIP(pr.Addr.String()) + } + if ip != nil && (ip.IsLoopback() || isLocalAddress(ip)) { + return nil + } + glog.V(1).Infof("rejected unauthenticated admin gRPC call from %s: no jwt.filer_signing.key configured", pr.Addr) + return status.Error(codes.Unauthenticated, "admin gRPC calls require jwt.filer_signing.key or a local client") +} + +// isLocalAddress enumerates interfaces per call: only reachable for +// non-loopback TCP peers on the no-key admin path, which is low-volume — +// and a cached set would keep trusting an address after it is removed from +// the host and reassigned. +func isLocalAddress(ip net.IP) bool { + addrs, err := net.InterfaceAddrs() + if err != nil { + return false + } + for _, a := range addrs { + if ipNet, ok := a.(*net.IPNet); ok && ipNet.IP.Equal(ip) { + return true + } + } + return false +} diff --git a/weed/security/grpc_peer_test.go b/weed/security/grpc_peer_test.go new file mode 100644 index 000000000..8a6127d07 --- /dev/null +++ b/weed/security/grpc_peer_test.go @@ -0,0 +1,68 @@ +package security + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +func peerCtx(addr net.Addr) context.Context { + return peer.NewContext(context.Background(), &peer.Peer{Addr: addr}) +} + +func bearerCtx(addr net.Addr, signingKey SigningKey) context.Context { + ctx := peerCtx(addr) + token := GenJwtForFilerAdmin(signingKey, 0) + return metadata.NewIncomingContext(ctx, metadata.Pairs("authorization", "Bearer "+string(token))) +} + +func TestCheckGrpcAdminPeer(t *testing.T) { + key := SigningKey("test-signing-key-0123456789abcdef") + loopback := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000} + remote := &net.TCPAddr{IP: net.ParseIP("203.0.113.9"), Port: 5000} + + for name, tc := range map[string]struct { + ctx context.Context + signingKey SigningKey + wantCode codes.Code + }{ + "no peer info, no key": {context.Background(), nil, codes.Unauthenticated}, + "loopback, no key": {peerCtx(loopback), nil, codes.OK}, + "remote, no key": {peerCtx(remote), nil, codes.Unauthenticated}, + "remote + valid token": {bearerCtx(remote, key), key, codes.OK}, + "remote + no metadata, key set": {peerCtx(remote), key, codes.Unauthenticated}, + "remote + bad token": {metadata.NewIncomingContext(peerCtx(remote), metadata.Pairs("authorization", "Bearer wrong")), key, codes.Unauthenticated}, + "unix socket, no key": {peerCtx(&net.UnixAddr{Name: "/tmp/s.sock"}), nil, codes.OK}, + "mtls verified peer": {peer.NewContext(context.Background(), &peer.Peer{ + Addr: remote, + AuthInfo: credentials.TLSInfo{State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{}}}}, + }), nil, codes.OK}, + } { + err := CheckGrpcAdminPeer(tc.ctx, tc.signingKey) + got := codes.OK + if err != nil { + got = status.Code(err) + } + if got != tc.wantCode { + t.Errorf("%s: got %v want %v (err=%v)", name, got, tc.wantCode, err) + } + } +} + +func TestGrpcAdminPeerCredentials(t *testing.T) { + if md := GrpcAdminPeerCredentials(nil, 0); len(md) != 0 { + t.Errorf("expected nil metadata without signing key, got %v", md) + } + md := GrpcAdminPeerCredentials(SigningKey("key"), 60) + if len(md.Get("authorization")) != 1 { + t.Fatalf("expected one authorization entry, got %v", md) + } +}