mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-14 10:08:35 +02:00
The local-peer fallback and bearer-token validation used by the filer IAM gRPC path move into weed/security so every admin-plane gRPC service can enforce the same contract. A client certificate verified by the server's TLS config now also counts as authenticated.
114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
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
|
|
}
|