diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index ed856da3f..4b2eed840 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -194,12 +194,10 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string, glog.V(0).Infof("Credential store %s does not support filer address function", store.GetName()) } - // The filer's IAM gRPC service rejects every RPC without an - // admin-signed Bearer token (PR #9442). Mirror the filer's - // jwt.filer_signing.key here so the admin UI's Users/Groups - // pages can talk to it; without this they fail with either - // Unimplemented (filer refuses to register the service) or - // Unauthenticated (missing authorization metadata). + // Mirror the filer's jwt.filer_signing.key so the admin UI's + // Users/Groups pages can present a valid Bearer token when the + // filer enforces IAM gRPC auth. When the key is empty, both + // sides run unauthenticated and no token is sent. if signer, ok := store.(interface { SetAdminSigning(security.SigningKey, int) }); ok { @@ -207,9 +205,7 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string, key := security.SigningKey(viper.GetString("jwt.filer_signing.key")) expires := viper.GetInt("jwt.filer_signing.expires_after_seconds") signer.SetAdminSigning(key, expires) - if len(key) == 0 { - glog.Warningf("jwt.filer_signing.key is empty in security.toml; the admin UI Users/Groups pages will fail until this is set on both the filer and the admin server") - } else { + if len(key) > 0 { glog.V(0).Infof("Credential store configured with admin Bearer token signing") } } diff --git a/weed/command/filer.go b/weed/command/filer.go index c1ec5a424..877302065 100644 --- a/weed/command/filer.go +++ b/weed/command/filer.go @@ -431,20 +431,19 @@ func (fo *FilerOptions) startFiler() { grpcS := pb.NewGrpcServer(security.LoadServerTLS(util.GetViper(), "grpc.filer")) filer_pb.RegisterSeaweedFilerServer(grpcS, fs) - // Register IAM gRPC service only when both a credential manager and an - // admin signing key are configured. The IAM RPCs can create users and - // mint access keys; mounting them on an unauthenticated listener would - // hand any caller that can reach the gRPC port S3-admin equivalent power. - // Operators who relied on the unauthenticated path must now set - // jwt.filer_signing.key in security.toml and attach a Bearer token signed - // with that key on every IAM call. + // Register the IAM gRPC service. Auth is opt-in: when + // jwt.filer_signing.key is configured the service requires a Bearer token + // signed with that key; otherwise it runs unauthenticated, matching the + // rest of the filer's gRPC surface. Operators who expose the filer gRPC + // port beyond a trusted network should set jwt.filer_signing.key on both + // the filer and the admin server. if credentialManager != nil { adminSigningKey := security.SigningKey(util.GetViper().GetString("jwt.filer_signing.key")) + iamGrpcServer := weed_server.NewIamGrpcServer(credentialManager, adminSigningKey) + iam_pb.RegisterSeaweedIdentityAccessManagementServer(grpcS, iamGrpcServer) if len(adminSigningKey) == 0 { - glog.Warningf("IAM gRPC service NOT registered on filer: jwt.filer_signing.key is empty in security.toml; configure it to enable IAM administration") + glog.V(0).Info("Registered IAM gRPC service on filer (unauthenticated; set jwt.filer_signing.key in security.toml to require admin Bearer token)") } else { - iamGrpcServer := weed_server.NewIamGrpcServer(credentialManager, adminSigningKey) - iam_pb.RegisterSeaweedIdentityAccessManagementServer(grpcS, iamGrpcServer) glog.V(0).Info("Registered IAM gRPC service on filer (admin Bearer token required)") } } diff --git a/weed/command/scaffold/security.toml b/weed/command/scaffold/security.toml index b99c2fd8c..860412dcd 100644 --- a/weed/command/scaffold/security.toml +++ b/weed/command/scaffold/security.toml @@ -44,11 +44,13 @@ expires_after_seconds = 10 # seconds # - f.e. the S3 API Shim generates the JWT # - the Filer server validates the JWT on writing # NOTE: This key is ALSO used as a fallback signing key for S3 STS if s3.iam.config does not specify a signingKey. -# NOTE: This key is ALSO required to mount the IAM gRPC service (CreateUser, -# PutPolicy, CreateAccessKey, ...) on the filer. The filer refuses to -# register that service when the key is empty, and every IAM RPC must -# carry a Bearer token signed with this key in its "authorization" -# gRPC metadata. Mint such a token with security.GenJwtForFilerAdmin. +# NOTE: This key also gates the filer IAM gRPC service (CreateUser, PutPolicy, +# CreateAccessKey, ...). When set, every IAM RPC must carry a Bearer +# token signed with this key in its "authorization" gRPC metadata; mint +# such a token with security.GenJwtForFilerAdmin. When empty, the IAM +# gRPC service runs unauthenticated, like the rest of the filer's gRPC +# surface — set the key on both filer and admin if the gRPC port is +# reachable beyond a trusted network. # the jwt defaults to expire after 10 seconds. [jwt.filer_signing] key = "" diff --git a/weed/server/filer_server_handlers_iam_grpc.go b/weed/server/filer_server_handlers_iam_grpc.go index 5ee7230c3..175ce9ade 100644 --- a/weed/server/filer_server_handlers_iam_grpc.go +++ b/weed/server/filer_server_handlers_iam_grpc.go @@ -16,18 +16,20 @@ import ( ) // IamGrpcServer implements the IAM gRPC service on the filer. -// Every RPC requires a Bearer token in the "authorization" metadata, signed -// with the filer write-signing key (jwt.filer_signing.key in security.toml). -// If no signing key is configured the service refuses to register at all; the -// adminSigningKey check below is defensive in case that wiring is bypassed. +// Auth is opt-in: when jwt.filer_signing.key is set in security.toml the +// service requires a Bearer token in the "authorization" metadata signed with +// that key; when it is empty every RPC is accepted unauthenticated, matching +// the rest of SeaweedFS's gRPC surface. Operators who expose the filer gRPC +// port beyond a trusted network should configure the key. type IamGrpcServer struct { iam_pb.UnimplementedSeaweedIdentityAccessManagementServer credentialManager *credential.CredentialManager adminSigningKey security.SigningKey } -// NewIamGrpcServer creates a new IAM gRPC server. adminSigningKey is required: -// callers without a Bearer token signed by this key are rejected. +// NewIamGrpcServer creates a new IAM gRPC server. If adminSigningKey is empty +// the service runs unauthenticated; otherwise every RPC requires a Bearer +// token signed with the key. func NewIamGrpcServer(credentialManager *credential.CredentialManager, adminSigningKey security.SigningKey) *IamGrpcServer { return &IamGrpcServer{ credentialManager: credentialManager, @@ -37,10 +39,11 @@ func NewIamGrpcServer(credentialManager *credential.CredentialManager, adminSign // checkAdminAuth verifies the caller presented a Bearer token signed by the // filer's write-signing key. It is invoked at the top of every IAM RPC. +// When no signing key is configured the service runs unauthenticated and this +// check is a no-op. func (s *IamGrpcServer) checkAdminAuth(ctx context.Context) error { if len(s.adminSigningKey) == 0 { - // Service should not be registered without a key; fail closed. - return status.Error(codes.PermissionDenied, "iam admin auth not configured") + return nil } md, ok := metadata.FromIncomingContext(ctx) if !ok { diff --git a/weed/server/filer_server_handlers_iam_grpc_test.go b/weed/server/filer_server_handlers_iam_grpc_test.go index ff1f0f5aa..b6964c58b 100644 --- a/weed/server/filer_server_handlers_iam_grpc_test.go +++ b/weed/server/filer_server_handlers_iam_grpc_test.go @@ -117,18 +117,31 @@ func TestIamGrpc_ValidToken_ReachesHandler(t *testing.T) { } } -func TestIamGrpc_NoSigningKey_PermissionDenied(t *testing.T) { - // Defensive path: even if the service is somehow registered without a - // key, every RPC must refuse. +func TestIamGrpc_NoSigningKey_Unauthenticated_Allowed(t *testing.T) { + // Auth is opt-in: when the server is built without a signing key, every + // RPC is accepted regardless of (or in the absence of) metadata so the + // admin UI works against a filer that has no jwt.filer_signing.key set. cm, err := credential.NewCredentialManager(credential.StoreTypeMemory, nil, "") if err != nil { t.Fatalf("NewCredentialManager: %v", err) } s := NewIamGrpcServer(cm, nil) + resp, err := s.ListUsers(context.Background(), &iam_pb.ListUsersRequest{}) + if err != nil { + t.Fatalf("ListUsers without key: unexpected error %v", err) + } + if resp == nil { + t.Fatal("ListUsers without key: nil response") + } + if len(resp.Usernames) != 0 { + t.Fatalf("ListUsers: expected empty user list from fresh memory store, got %v", resp.Usernames) + } + + // A token sent by a client that does configure a key is also accepted — + // the server just ignores it rather than rejecting on signature mismatch. good := security.GenJwtForFilerAdmin(security.SigningKey(testIamSigningKey), 60) - _, err = s.ListUsers(ctxWithBearer(string(good)), &iam_pb.ListUsersRequest{}) - if got, want := status.Code(err), codes.PermissionDenied; got != want { - t.Fatalf("ListUsers with no signing key: got code %v, want %v (err=%v)", got, want, err) + if _, err := s.ListUsers(ctxWithBearer(string(good)), &iam_pb.ListUsersRequest{}); err != nil { + t.Fatalf("ListUsers with stray token but no server key: unexpected error %v", err) } }