security: share the admin gRPC peer authentication check

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.
This commit is contained in:
Chris Lu committed 2026-10-12 02:55:50 -07:00
1 parent a4c8d31cb8
commit 3f8035deb7
3 files changed
+182 -42

No files matched your search

+1 -42
View File
@@ -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) {
+113
View File
@@ -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
}
+68
View File
@@ -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)
}
}