mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-13 09:38:36 +02:00
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:
1 parent
a4c8d31cb8
commit
3f8035deb7
3 files changed
+182
-42
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user