Omni is Vulnerable to DoS via Empty Create/Update Resource Requests
Description
Omni manages Kubernetes on bare metal, virtual machines, or in a cloud. Prior to 1.1.5 and 1.0.2, there is a nil pointer dereference vulnerability in the Omni Resource Service allows unauthenticated users to cause a server panic and denial of service by sending empty create/update resource requests through the API endpoints. The vulnerability exists in the isSensitiveSpec function which calls grpcomni.CreateResource without checking if the resource's metadata field is nil. When a resource is created with an empty Metadata field, the CreateResource function attempts to access resource.Metadata.Version causing a segmentation fault. This vulnerability is fixed in 1.1.5 and 1.0.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/siderolabs/omniGo | >= 1.1.0-beta.0, < 1.1.5 | 1.1.5 |
github.com/siderolabs/omniGo | < 1.0.2 | 1.0.2 |
Affected products
1- Range: >= 1.1.0-beta.0, < 1.1.5
Patches
21396083f766afix: fix the order in the grpc interceptor chain
11 files changed · +80 −49
go.mod+1 −1 modified@@ -66,7 +66,7 @@ require ( github.com/siderolabs/discovery-client v0.1.13 github.com/siderolabs/discovery-service v1.0.11 github.com/siderolabs/gen v0.8.5 - github.com/siderolabs/go-api-signature v0.3.7 + github.com/siderolabs/go-api-signature v0.3.8 github.com/siderolabs/go-circular v0.2.3 github.com/siderolabs/go-debug v0.6.0 github.com/siderolabs/go-kubernetes v0.2.25
go.sum+2 −2 modified@@ -409,8 +409,8 @@ github.com/siderolabs/discovery-service v1.0.11 h1:+ymDXKhPL2f1c5MIO559wciA38PcQ github.com/siderolabs/discovery-service v1.0.11/go.mod h1:pUTOYgtYasO/T02zJNNfw0SCP8hDbIgFIvdm+Fn1UKo= github.com/siderolabs/gen v0.8.5 h1:xlWXTynnGD/epaj7uplvKvmAkBH+Fp51bLnw1JC0xME= github.com/siderolabs/gen v0.8.5/go.mod h1:CRrktDXQf3yDJI7xKv+cDYhBbKdfd/YE16OpgcHoT9E= -github.com/siderolabs/go-api-signature v0.3.7 h1:Qx5NH3BrtYucCgiLObAJhx7pouLR4tivr1moOClII3M= -github.com/siderolabs/go-api-signature v0.3.7/go.mod h1:MQy+DcXCQIFFXZr+E4tbMmnQSQs7WpubSpJFRN694mI= +github.com/siderolabs/go-api-signature v0.3.8 h1:0iTcOWIxOAc7M8aB2L+WScUd4BoqdXshvQ4h9tSSeF8= +github.com/siderolabs/go-api-signature v0.3.8/go.mod h1:MQy+DcXCQIFFXZr+E4tbMmnQSQs7WpubSpJFRN694mI= github.com/siderolabs/go-circular v0.2.3 h1:GKkA1Tw79kEFGtWdl7WTxEUTbwtklITeiRT0V1McHrA= github.com/siderolabs/go-circular v0.2.3/go.mod h1:YBN/q9YpQphUYnBtBgPsngauSHj1TEZfgQZWZVjk1WE= github.com/siderolabs/go-debug v0.6.0 h1:wcftcXv3fFeUHwsj4bJpHaXRJ6JJXL+eeaY69fCtHoY=
hack/generate-certs/main.go+1 −1 modified@@ -157,7 +157,7 @@ func generate() (err error) { } func runApp(app string, args ...string) error { - cmd := exec.Command(app, args...) + cmd := exec.Command(app, args...) //nolint:noctx cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin
internal/backend/grpc/resource.go+9 −1 modified@@ -317,7 +317,7 @@ func (s *ResourceServer) Teardown(ctx context.Context, in *resources.DeleteReque func getSource(ctx context.Context) common.Runtime { if md, ok := metadata.FromIncomingContext(ctx); ok { - source := md.Get(message.RuntimeHeaderHey) + source := md.Get(message.RuntimeHeaderKey) if source != nil { if res, ok := common.Runtime_value[source[0]]; ok { return common.Runtime(res) @@ -361,6 +361,14 @@ func withResource(r res) []runtime.QueryOption { // CreateResource creates a resource from a resource proto representation. func CreateResource(resource *resources.Resource) (cosiresource.Resource, error) { //nolint:ireturn + if resource == nil { + return nil, errors.New("resource is nil") + } + + if resource.Metadata == nil { + return nil, errors.New("resource metadata is nil") + } + if resource.Metadata.Version == "" { resource.Metadata.Version = "1" }
internal/backend/grpc/router/router.go+1 −1 modified@@ -154,7 +154,7 @@ func (r *Router) Director(ctx context.Context, fullMethodName string) (proxy.Mod return proxy.One2One, []proxy.Backend{r.omniBackend}, nil } - if runtime := md.Get(message.RuntimeHeaderHey); runtime != nil && runtime[0] == common.Runtime_Talos.String() { + if runtime := md.Get(message.RuntimeHeaderKey); runtime != nil && runtime[0] == common.Runtime_Talos.String() { backends, err := r.getTalosBackend(ctx, md) if err != nil { return proxy.One2One, nil, err
internal/backend/server.go+7 −7 modified@@ -416,6 +416,8 @@ func (s *Server) buildServerOptions() ([]grpc.ServerOption, error) { grpc_ctxtags.UnaryServerInterceptor(), logLevelOverrideUnaryInterceptor, grpc_zap.UnaryServerInterceptor(s.logger, grpc_zap.WithMessageProducer(messageProducer)), + grpc_prometheus.UnaryServerInterceptor, + grpc_recovery.UnaryServerInterceptor(recoveryOpt), grpcutil.SetUserAgent(), grpcutil.SetRealPeerAddress(), grpcutil.SetAuditData(), @@ -428,14 +430,14 @@ func (s *Server) buildServerOptions() ([]grpc.ServerOption, error) { ), 1024, ), - grpc_prometheus.UnaryServerInterceptor, - grpc_recovery.UnaryServerInterceptor(recoveryOpt), } streamInterceptors := []grpc.StreamServerInterceptor{ grpc_ctxtags.StreamServerInterceptor(), logLevelOverrideStreamInterceptor, grpc_zap.StreamServerInterceptor(s.logger, grpc_zap.WithMessageProducer(messageProducer)), + grpc_prometheus.StreamServerInterceptor, + grpc_recovery.StreamServerInterceptor(recoveryOpt), grpcutil.StreamSetUserAgent(), grpcutil.StreamSetRealPeerAddress(), grpcutil.StreamSetAuditData(), @@ -452,8 +454,6 @@ func (s *Server) buildServerOptions() ([]grpc.ServerOption, error) { ), }, ), - grpc_prometheus.StreamServerInterceptor, - grpc_recovery.StreamServerInterceptor(recoveryOpt), } authInterceptors, err := s.getAuthInterceptors() @@ -759,12 +759,12 @@ func resourceServerUpdate(resCopy *resapi.UpdateRequest) (*resapi.UpdateRequest, func isSensitiveResource(res *v1alpha1.Resource) bool { protoR, err := protobuf.Unmarshal(res) if err != nil { - return false + return true } properResource, err := protobuf.UnmarshalResource(protoR) if err != nil { - return false + return true } resDef, ok := properResource.(meta.ResourceDefinitionProvider) @@ -779,7 +779,7 @@ func isSensitiveResource(res *v1alpha1.Resource) bool { func isSensitiveSpec(resource *resapi.Resource) bool { res, err := grpcomni.CreateResource(resource) if err != nil { - return false + return true } resDef, ok := res.(meta.ResourceDefinitionProvider)
internal/integration/auth_test.go+8 −12 modified@@ -945,11 +945,6 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client resource: omni.NewImagePullStatus(resources.DefaultNamespace, uuid.New().String()), allowedVerbSet: readOnlyVerbSet, }, - { - resource: authres.NewAuthConfig(), - allowedVerbSet: readOnlyVerbSet, - isPublic: true, - }, { resource: siderolink.NewConnectionParams(resources.DefaultNamespace, uuid.New().String()), allowedVerbSet: readOnlyVerbSet, @@ -1155,6 +1150,7 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client // delete excluded resources from the untested set delete(untestedResourceTypes, k8s.KubernetesResourceType) delete(untestedResourceTypes, siderolink.DeprecatedLinkCounterType) + delete(untestedResourceTypes, authres.AuthConfigType) for _, tc := range testCases { for _, testVerb := range allVerbs { @@ -1179,19 +1175,19 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client accessErr := accessResource(noSignatureCtx, t, rootCli, scopedCli, tc.resource, testVerb) - if len(tc.allowedVerbSet) == 0 { - assert.ErrorContains(t, accessErr, "no access is permitted") - - return - } - if !tc.isPublic { - assert.ErrorContains(t, accessErr, "missing valid signature") + assert.ErrorContains(t, accessErr, "invalid signature") // refresh the error but with a signature this time accessErr = accessResource(rootCtx, t, rootCli, scopedCli, tc.resource, testVerb) } + if len(tc.allowedVerbSet) == 0 { + assert.ErrorContains(t, accessErr, "no access is permitted") + + return + } + isVerbError := accessErr != nil && strings.Contains(accessErr.Error(), "only") && strings.Contains(accessErr.Error(), "access is permitted") isRoleError := accessErr != nil && strings.Contains(accessErr.Error(), "insufficient role:")
internal/pkg/auth/handler/handler_test.go+16 −6 modified@@ -32,7 +32,7 @@ func testHandler(t *testing.T, authEnabled bool) { logger := zaptest.NewLogger(t) - authenticatorFunc := func(context.Context, string) (*auth.Authenticator, error) { + authenticatorFunc := func(context.Context, string) (*auth.Authenticator, error) { //nolint:unparam return &auth.Authenticator{ Verifier: mockSignerVerifier{}, Identity: "user@example.com", @@ -41,12 +41,15 @@ func testHandler(t *testing.T, authEnabled bool) { }, nil } - wrapWithAuth := func(h http.Handler) http.Handler { - return handler.NewAuthConfig(handler.NewSignature(h, authenticatorFunc, logger), authEnabled, logger) - } + testServer := func(signatureRequired message.SignatureRequiredCheckFunc) *httptest.Server { + wrapWithAuth := func(h http.Handler) http.Handler { + signatureHandler := handler.NewSignature(h, authenticatorFunc, logger, message.WithSignatureRequiredCheck(signatureRequired)) - ts := httptest.NewServer(wrapWithAuth(coreHandler)) - defer ts.Close() + return handler.NewAuthConfig(signatureHandler, authEnabled, logger) + } + + return httptest.NewServer(wrapWithAuth(coreHandler)) + } ctx := t.Context() @@ -59,6 +62,7 @@ func testHandler(t *testing.T, authEnabled bool) { verifyContext bool expectedCode int + public bool } var testCases []testCase @@ -69,6 +73,7 @@ func testHandler(t *testing.T, authEnabled bool) { name: "no signature", expectedCode: http.StatusOK, uri: "/ok", + public: true, }, { name: "correct signature", @@ -101,6 +106,11 @@ func testHandler(t *testing.T, authEnabled bool) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + ts := testServer(func() (bool, error) { + return authEnabled && !tc.public, nil + }) + defer ts.Close() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL+tc.uri, nil) require.NoError(t, err)
internal/pkg/auth/handler/signature.go+4 −2 modified@@ -25,14 +25,16 @@ type Signature struct { authenticatorFunc auth.AuthenticatorFunc next http.Handler logger *zap.Logger + options []message.Option } // NewSignature returns a new signature handler. -func NewSignature(handler http.Handler, authenticatorFunc auth.AuthenticatorFunc, logger *zap.Logger) *Signature { +func NewSignature(handler http.Handler, authenticatorFunc auth.AuthenticatorFunc, logger *zap.Logger, messageOptions ...message.Option) *Signature { return &Signature{ next: handler, authenticatorFunc: authenticatorFunc, logger: logger, + options: messageOptions, } } @@ -57,7 +59,7 @@ func (s *Signature) ServeHTTP(writer http.ResponseWriter, request *http.Request) } func (s *Signature) intercept(request *http.Request) (*http.Request, error) { - msg, err := message.NewHTTP(request) + msg, err := message.NewHTTP(request, s.options...) if err != nil { return nil, err }
internal/pkg/auth/interceptor/auth_config.go+16 −4 modified@@ -14,6 +14,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" + resapi "github.com/siderolabs/omni/client/api/omni/resources" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/ctxstore" @@ -36,7 +38,15 @@ func NewAuthConfig(enabled bool, logger *zap.Logger) *AuthConfig { // Unary returns a new unary GRPC interceptor. func (c *AuthConfig) Unary() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - ctx = c.intercept(ctx, info.FullMethod) + isGetAuthConfigRequest := false + + if req != nil && info != nil && info.FullMethod == resapi.ResourceService_Get_FullMethodName { + if getReq, getReqOk := req.(*resapi.GetRequest); getReqOk && getReq.Type == authres.AuthConfigType { + isGetAuthConfigRequest = true + } + } + + ctx = c.intercept(ctx, isGetAuthConfigRequest, info.FullMethod) return handler(ctx, req) } @@ -45,7 +55,7 @@ func (c *AuthConfig) Unary() grpc.UnaryServerInterceptor { // Stream returns a new streaming GRPC interceptor. func (c *AuthConfig) Stream() grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - ctx := c.intercept(ss.Context(), info.FullMethod) + ctx := c.intercept(ss.Context(), false, info.FullMethod) return handler(srv, &grpc_middleware.WrappedServerStream{ ServerStream: ss, @@ -54,7 +64,7 @@ func (c *AuthConfig) Stream() grpc.StreamServerInterceptor { } } -func (c *AuthConfig) intercept(ctx context.Context, method string) context.Context { +func (c *AuthConfig) intercept(ctx context.Context, isGetAuthConfigRequest bool, method string) context.Context { ctx = ctxstore.WithValue(ctx, auth.EnabledAuthContextKey{Enabled: c.enabled}) if !c.enabled { @@ -66,7 +76,9 @@ func (c *AuthConfig) intercept(ctx context.Context, method string) context.Conte md = metadata.New(nil) } - msg := message.NewGRPC(md, method) + msg := message.NewGRPC(md, method, message.WithSignatureRequiredCheck(func() (bool, error) { + return !isGetAuthConfigRequest, nil + })) auditData, ok := ctxstore.Value[*audit.Data](ctx) if ok {
internal/pkg/auth/interceptor/signature_test.go+15 −12 modified@@ -22,28 +22,29 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "github.com/siderolabs/omni/client/api/omni/resources" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/auth/interceptor" "github.com/siderolabs/omni/internal/pkg/auth/role" "github.com/siderolabs/omni/internal/pkg/test" ) type testServer struct { - grpc_testing.UnimplementedTestServiceServer + resources.UnimplementedResourceServiceServer t *testing.T } -func (s testServer) UnaryCall(_ context.Context, _ *grpc_testing.SimpleRequest) (*grpc_testing.SimpleResponse, error) { - return &grpc_testing.SimpleResponse{}, nil +func (s testServer) Get(context.Context, *resources.GetRequest) (*resources.GetResponse, error) { + return &resources.GetResponse{}, nil } type SignatureTestSuite struct { - testServiceClient grpc_testing.TestServiceClient + testServiceClient resources.ResourceServiceClient clientConn *grpc.ClientConn key *pgp.Key test.GRPCSuite @@ -81,7 +82,7 @@ func (suite *SignatureTestSuite) SetupSuite() { ), ) - grpc_testing.RegisterTestServiceServer(suite.Server, testServer{ + resources.RegisterResourceServiceServer(suite.Server, testServer{ t: suite.T(), }) @@ -94,7 +95,7 @@ func (suite *SignatureTestSuite) SetupSuite() { suite.clientConn, err = grpc.NewClient(suite.Target, dialOptions...) suite.Require().NoError(err) - suite.testServiceClient = grpc_testing.NewTestServiceClient(suite.clientConn) + suite.testServiceClient = resources.NewResourceServiceClient(suite.clientConn) } func (suite *SignatureTestSuite) TearDownSuite() { @@ -103,7 +104,9 @@ func (suite *SignatureTestSuite) TearDownSuite() { } func (suite *SignatureTestSuite) TestMissingSignaturePassthrough() { - _, err := suite.testServiceClient.UnaryCall(suite.T().Context(), &grpc_testing.SimpleRequest{}) + _, err := suite.testServiceClient.Get(suite.T().Context(), &resources.GetRequest{ + Type: authres.AuthConfigType, + }) suite.Assert().NoError(err) } @@ -113,7 +116,7 @@ func (suite *SignatureTestSuite) TestInvalidSignatureVersion() { message.SignatureHeaderKey, "invalid", )) - _, err := suite.testServiceClient.UnaryCall(ctx, &grpc_testing.SimpleRequest{}) + _, err := suite.testServiceClient.Get(ctx, &resources.GetRequest{}) suite.Assert().Error(err) suite.Assert().Equal(codes.Unauthenticated, status.Code(err), "error code should be codes.Unauthenticated") @@ -127,7 +130,7 @@ func (suite *SignatureTestSuite) TestMissingTimestamp() { message.SignatureHeaderKey, fmt.Sprintf("%s test@example.org signer-1 %s", message.SignatureVersionV1, payload), )) - _, err := suite.testServiceClient.UnaryCall(ctx, &grpc_testing.SimpleRequest{}) + _, err := suite.testServiceClient.Get(ctx, &resources.GetRequest{}) suite.Assert().Error(err) suite.Assert().Equal(codes.Unauthenticated, status.Code(err), "error code should be codes.Unauthenticated") @@ -141,7 +144,7 @@ func (suite *SignatureTestSuite) TestValidSignature() { Headers: map[string][]string{ message.TimestampHeaderKey: {epochTimestamp}, }, - Method: "/grpc.testing.TestService/UnaryCall", + Method: resources.ResourceService_Get_FullMethodName, } payloadJSON, err := json.Marshal(payload) @@ -163,7 +166,7 @@ func (suite *SignatureTestSuite) TestValidSignature() { message.PayloadHeaderKey, string(payloadJSON), )) - _, err = suite.testServiceClient.UnaryCall(ctx, &grpc_testing.SimpleRequest{}) + _, err = suite.testServiceClient.Get(ctx, &resources.GetRequest{}) assert.NoError(suite.T(), err) }
1fd954af6498fix: fix the order in the grpc interceptor chain
11 files changed · +80 −49
go.mod+1 −1 modified@@ -66,7 +66,7 @@ require ( github.com/siderolabs/discovery-client v0.1.11 github.com/siderolabs/discovery-service v1.0.10 github.com/siderolabs/gen v0.8.4 - github.com/siderolabs/go-api-signature v0.3.6 + github.com/siderolabs/go-api-signature v0.3.8 github.com/siderolabs/go-circular v0.2.3 github.com/siderolabs/go-debug v0.5.0 github.com/siderolabs/go-kubernetes v0.2.23
go.sum+2 −2 modified@@ -415,8 +415,8 @@ github.com/siderolabs/discovery-service v1.0.10 h1:GSd5p+bC+PJjIpCqiDtVFKKU18Lps github.com/siderolabs/discovery-service v1.0.10/go.mod h1:tzeHcfftQQHZSShuSTcrgIN3BY6fmhlum/Z9yOJ61lk= github.com/siderolabs/gen v0.8.4 h1:1Xj/YvKTXgpnr9ZC7heKcskJo5wHvWOybwjQSCfEmsQ= github.com/siderolabs/gen v0.8.4/go.mod h1:CRrktDXQf3yDJI7xKv+cDYhBbKdfd/YE16OpgcHoT9E= -github.com/siderolabs/go-api-signature v0.3.6 h1:wDIsXbpl7Oa/FXvxB6uz4VL9INA9fmr3EbmjEZYFJrU= -github.com/siderolabs/go-api-signature v0.3.6/go.mod h1:hoH13AfunHflxbXfh+NoploqV13ZTDfQ1mQJWNVSW9U= +github.com/siderolabs/go-api-signature v0.3.8 h1:0iTcOWIxOAc7M8aB2L+WScUd4BoqdXshvQ4h9tSSeF8= +github.com/siderolabs/go-api-signature v0.3.8/go.mod h1:MQy+DcXCQIFFXZr+E4tbMmnQSQs7WpubSpJFRN694mI= github.com/siderolabs/go-circular v0.2.3 h1:GKkA1Tw79kEFGtWdl7WTxEUTbwtklITeiRT0V1McHrA= github.com/siderolabs/go-circular v0.2.3/go.mod h1:YBN/q9YpQphUYnBtBgPsngauSHj1TEZfgQZWZVjk1WE= github.com/siderolabs/go-debug v0.5.0 h1:AQwFtvyFkSYTA1of4/UyDvVu8dVLoQP5sUYgmcp/u+4=
hack/generate-certs/main.go+1 −1 modified@@ -157,7 +157,7 @@ func generate() (err error) { } func runApp(app string, args ...string) error { - cmd := exec.Command(app, args...) + cmd := exec.Command(app, args...) //nolint:noctx cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin
internal/backend/grpc/resource.go+9 −1 modified@@ -317,7 +317,7 @@ func (s *ResourceServer) Teardown(ctx context.Context, in *resources.DeleteReque func getSource(ctx context.Context) common.Runtime { if md, ok := metadata.FromIncomingContext(ctx); ok { - source := md.Get(message.RuntimeHeaderHey) + source := md.Get(message.RuntimeHeaderKey) if source != nil { if res, ok := common.Runtime_value[source[0]]; ok { return common.Runtime(res) @@ -361,6 +361,14 @@ func withResource(r res) []runtime.QueryOption { // CreateResource creates a resource from a resource proto representation. func CreateResource(resource *resources.Resource) (cosiresource.Resource, error) { //nolint:ireturn + if resource == nil { + return nil, errors.New("resource is nil") + } + + if resource.Metadata == nil { + return nil, errors.New("resource metadata is nil") + } + if resource.Metadata.Version == "" { resource.Metadata.Version = "1" }
internal/backend/grpc/router/router.go+1 −1 modified@@ -154,7 +154,7 @@ func (r *Router) Director(ctx context.Context, fullMethodName string) (proxy.Mod return proxy.One2One, []proxy.Backend{r.omniBackend}, nil } - if runtime := md.Get(message.RuntimeHeaderHey); runtime != nil && runtime[0] == common.Runtime_Talos.String() { + if runtime := md.Get(message.RuntimeHeaderKey); runtime != nil && runtime[0] == common.Runtime_Talos.String() { backends, err := r.getTalosBackend(ctx, md) if err != nil { return proxy.One2One, nil, err
internal/backend/server.go+7 −7 modified@@ -416,6 +416,8 @@ func (s *Server) buildServerOptions() ([]grpc.ServerOption, error) { grpc_ctxtags.UnaryServerInterceptor(), logLevelOverrideUnaryInterceptor, grpc_zap.UnaryServerInterceptor(s.logger, grpc_zap.WithMessageProducer(messageProducer)), + grpc_prometheus.UnaryServerInterceptor, + grpc_recovery.UnaryServerInterceptor(recoveryOpt), grpcutil.SetUserAgent(), grpcutil.SetRealPeerAddress(), grpcutil.SetAuditData(), @@ -428,14 +430,14 @@ func (s *Server) buildServerOptions() ([]grpc.ServerOption, error) { ), 1024, ), - grpc_prometheus.UnaryServerInterceptor, - grpc_recovery.UnaryServerInterceptor(recoveryOpt), } streamInterceptors := []grpc.StreamServerInterceptor{ grpc_ctxtags.StreamServerInterceptor(), logLevelOverrideStreamInterceptor, grpc_zap.StreamServerInterceptor(s.logger, grpc_zap.WithMessageProducer(messageProducer)), + grpc_prometheus.StreamServerInterceptor, + grpc_recovery.StreamServerInterceptor(recoveryOpt), grpcutil.StreamSetUserAgent(), grpcutil.StreamSetRealPeerAddress(), grpcutil.StreamSetAuditData(), @@ -452,8 +454,6 @@ func (s *Server) buildServerOptions() ([]grpc.ServerOption, error) { ), }, ), - grpc_prometheus.StreamServerInterceptor, - grpc_recovery.StreamServerInterceptor(recoveryOpt), } authInterceptors, err := s.getAuthInterceptors() @@ -759,12 +759,12 @@ func resourceServerUpdate(resCopy *resapi.UpdateRequest) (*resapi.UpdateRequest, func isSensitiveResource(res *v1alpha1.Resource) bool { protoR, err := protobuf.Unmarshal(res) if err != nil { - return false + return true } properResource, err := protobuf.UnmarshalResource(protoR) if err != nil { - return false + return true } resDef, ok := properResource.(meta.ResourceDefinitionProvider) @@ -779,7 +779,7 @@ func isSensitiveResource(res *v1alpha1.Resource) bool { func isSensitiveSpec(resource *resapi.Resource) bool { res, err := grpcomni.CreateResource(resource) if err != nil { - return false + return true } resDef, ok := res.(meta.ResourceDefinitionProvider)
internal/integration/auth_test.go+8 −12 modified@@ -940,11 +940,6 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client resource: omni.NewImagePullStatus(resources.DefaultNamespace, uuid.New().String()), allowedVerbSet: readOnlyVerbSet, }, - { - resource: authres.NewAuthConfig(), - allowedVerbSet: readOnlyVerbSet, - isPublic: true, - }, { resource: siderolink.NewConnectionParams(resources.DefaultNamespace, uuid.New().String()), allowedVerbSet: readOnlyVerbSet, @@ -1149,6 +1144,7 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client // delete excluded resources from the untested set delete(untestedResourceTypes, k8s.KubernetesResourceType) delete(untestedResourceTypes, siderolink.DeprecatedLinkCounterType) + delete(untestedResourceTypes, authres.AuthConfigType) for _, tc := range testCases { for _, testVerb := range allVerbs { @@ -1173,19 +1169,19 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client accessErr := accessResource(noSignatureCtx, t, rootCli, scopedCli, tc.resource, testVerb) - if len(tc.allowedVerbSet) == 0 { - assert.ErrorContains(t, accessErr, "no access is permitted") - - return - } - if !tc.isPublic { - assert.ErrorContains(t, accessErr, "missing valid signature") + assert.ErrorContains(t, accessErr, "invalid signature") // refresh the error but with a signature this time accessErr = accessResource(rootCtx, t, rootCli, scopedCli, tc.resource, testVerb) } + if len(tc.allowedVerbSet) == 0 { + assert.ErrorContains(t, accessErr, "no access is permitted") + + return + } + isVerbError := accessErr != nil && strings.Contains(accessErr.Error(), "only") && strings.Contains(accessErr.Error(), "access is permitted") isRoleError := accessErr != nil && strings.Contains(accessErr.Error(), "insufficient role:")
internal/pkg/auth/handler/handler_test.go+16 −6 modified@@ -32,7 +32,7 @@ func testHandler(t *testing.T, authEnabled bool) { logger := zaptest.NewLogger(t) - authenticatorFunc := func(context.Context, string) (*auth.Authenticator, error) { + authenticatorFunc := func(context.Context, string) (*auth.Authenticator, error) { //nolint:unparam return &auth.Authenticator{ Verifier: mockSignerVerifier{}, Identity: "user@example.com", @@ -41,12 +41,15 @@ func testHandler(t *testing.T, authEnabled bool) { }, nil } - wrapWithAuth := func(h http.Handler) http.Handler { - return handler.NewAuthConfig(handler.NewSignature(h, authenticatorFunc, logger), authEnabled, logger) - } + testServer := func(signatureRequired message.SignatureRequiredCheckFunc) *httptest.Server { + wrapWithAuth := func(h http.Handler) http.Handler { + signatureHandler := handler.NewSignature(h, authenticatorFunc, logger, message.WithSignatureRequiredCheck(signatureRequired)) - ts := httptest.NewServer(wrapWithAuth(coreHandler)) - defer ts.Close() + return handler.NewAuthConfig(signatureHandler, authEnabled, logger) + } + + return httptest.NewServer(wrapWithAuth(coreHandler)) + } ctx := t.Context() @@ -59,6 +62,7 @@ func testHandler(t *testing.T, authEnabled bool) { verifyContext bool expectedCode int + public bool } var testCases []testCase @@ -69,6 +73,7 @@ func testHandler(t *testing.T, authEnabled bool) { name: "no signature", expectedCode: http.StatusOK, uri: "/ok", + public: true, }, { name: "correct signature", @@ -101,6 +106,11 @@ func testHandler(t *testing.T, authEnabled bool) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + ts := testServer(func() (bool, error) { + return authEnabled && !tc.public, nil + }) + defer ts.Close() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL+tc.uri, nil) require.NoError(t, err)
internal/pkg/auth/handler/signature.go+4 −2 modified@@ -25,14 +25,16 @@ type Signature struct { authenticatorFunc auth.AuthenticatorFunc next http.Handler logger *zap.Logger + options []message.Option } // NewSignature returns a new signature handler. -func NewSignature(handler http.Handler, authenticatorFunc auth.AuthenticatorFunc, logger *zap.Logger) *Signature { +func NewSignature(handler http.Handler, authenticatorFunc auth.AuthenticatorFunc, logger *zap.Logger, messageOptions ...message.Option) *Signature { return &Signature{ next: handler, authenticatorFunc: authenticatorFunc, logger: logger, + options: messageOptions, } } @@ -57,7 +59,7 @@ func (s *Signature) ServeHTTP(writer http.ResponseWriter, request *http.Request) } func (s *Signature) intercept(request *http.Request) (*http.Request, error) { - msg, err := message.NewHTTP(request) + msg, err := message.NewHTTP(request, s.options...) if err != nil { return nil, err }
internal/pkg/auth/interceptor/auth_config.go+16 −4 modified@@ -14,6 +14,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" + resapi "github.com/siderolabs/omni/client/api/omni/resources" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/ctxstore" @@ -36,7 +38,15 @@ func NewAuthConfig(enabled bool, logger *zap.Logger) *AuthConfig { // Unary returns a new unary GRPC interceptor. func (c *AuthConfig) Unary() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - ctx = c.intercept(ctx, info.FullMethod) + isGetAuthConfigRequest := false + + if req != nil && info != nil && info.FullMethod == resapi.ResourceService_Get_FullMethodName { + if getReq, getReqOk := req.(*resapi.GetRequest); getReqOk && getReq.Type == authres.AuthConfigType { + isGetAuthConfigRequest = true + } + } + + ctx = c.intercept(ctx, isGetAuthConfigRequest, info.FullMethod) return handler(ctx, req) } @@ -45,7 +55,7 @@ func (c *AuthConfig) Unary() grpc.UnaryServerInterceptor { // Stream returns a new streaming GRPC interceptor. func (c *AuthConfig) Stream() grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - ctx := c.intercept(ss.Context(), info.FullMethod) + ctx := c.intercept(ss.Context(), false, info.FullMethod) return handler(srv, &grpc_middleware.WrappedServerStream{ ServerStream: ss, @@ -54,7 +64,7 @@ func (c *AuthConfig) Stream() grpc.StreamServerInterceptor { } } -func (c *AuthConfig) intercept(ctx context.Context, method string) context.Context { +func (c *AuthConfig) intercept(ctx context.Context, isGetAuthConfigRequest bool, method string) context.Context { ctx = ctxstore.WithValue(ctx, auth.EnabledAuthContextKey{Enabled: c.enabled}) if !c.enabled { @@ -66,7 +76,9 @@ func (c *AuthConfig) intercept(ctx context.Context, method string) context.Conte md = metadata.New(nil) } - msg := message.NewGRPC(md, method) + msg := message.NewGRPC(md, method, message.WithSignatureRequiredCheck(func() (bool, error) { + return !isGetAuthConfigRequest, nil + })) auditData, ok := ctxstore.Value[*audit.Data](ctx) if ok {
internal/pkg/auth/interceptor/signature_test.go+15 −12 modified@@ -22,28 +22,29 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "github.com/siderolabs/omni/client/api/omni/resources" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/auth/interceptor" "github.com/siderolabs/omni/internal/pkg/auth/role" "github.com/siderolabs/omni/internal/pkg/test" ) type testServer struct { - grpc_testing.UnimplementedTestServiceServer + resources.UnimplementedResourceServiceServer t *testing.T } -func (s testServer) UnaryCall(_ context.Context, _ *grpc_testing.SimpleRequest) (*grpc_testing.SimpleResponse, error) { - return &grpc_testing.SimpleResponse{}, nil +func (s testServer) Get(context.Context, *resources.GetRequest) (*resources.GetResponse, error) { + return &resources.GetResponse{}, nil } type SignatureTestSuite struct { - testServiceClient grpc_testing.TestServiceClient + testServiceClient resources.ResourceServiceClient clientConn *grpc.ClientConn key *pgp.Key test.GRPCSuite @@ -81,7 +82,7 @@ func (suite *SignatureTestSuite) SetupSuite() { ), ) - grpc_testing.RegisterTestServiceServer(suite.Server, testServer{ + resources.RegisterResourceServiceServer(suite.Server, testServer{ t: suite.T(), }) @@ -94,7 +95,7 @@ func (suite *SignatureTestSuite) SetupSuite() { suite.clientConn, err = grpc.NewClient(suite.Target, dialOptions...) suite.Require().NoError(err) - suite.testServiceClient = grpc_testing.NewTestServiceClient(suite.clientConn) + suite.testServiceClient = resources.NewResourceServiceClient(suite.clientConn) } func (suite *SignatureTestSuite) TearDownSuite() { @@ -103,7 +104,9 @@ func (suite *SignatureTestSuite) TearDownSuite() { } func (suite *SignatureTestSuite) TestMissingSignaturePassthrough() { - _, err := suite.testServiceClient.UnaryCall(suite.T().Context(), &grpc_testing.SimpleRequest{}) + _, err := suite.testServiceClient.Get(suite.T().Context(), &resources.GetRequest{ + Type: authres.AuthConfigType, + }) suite.Assert().NoError(err) } @@ -113,7 +116,7 @@ func (suite *SignatureTestSuite) TestInvalidSignatureVersion() { message.SignatureHeaderKey, "invalid", )) - _, err := suite.testServiceClient.UnaryCall(ctx, &grpc_testing.SimpleRequest{}) + _, err := suite.testServiceClient.Get(ctx, &resources.GetRequest{}) suite.Assert().Error(err) suite.Assert().Equal(codes.Unauthenticated, status.Code(err), "error code should be codes.Unauthenticated") @@ -127,7 +130,7 @@ func (suite *SignatureTestSuite) TestMissingTimestamp() { message.SignatureHeaderKey, fmt.Sprintf("%s test@example.org signer-1 %s", message.SignatureVersionV1, payload), )) - _, err := suite.testServiceClient.UnaryCall(ctx, &grpc_testing.SimpleRequest{}) + _, err := suite.testServiceClient.Get(ctx, &resources.GetRequest{}) suite.Assert().Error(err) suite.Assert().Equal(codes.Unauthenticated, status.Code(err), "error code should be codes.Unauthenticated") @@ -141,7 +144,7 @@ func (suite *SignatureTestSuite) TestValidSignature() { Headers: map[string][]string{ message.TimestampHeaderKey: {epochTimestamp}, }, - Method: "/grpc.testing.TestService/UnaryCall", + Method: resources.ResourceService_Get_FullMethodName, } payloadJSON, err := json.Marshal(payload) @@ -163,7 +166,7 @@ func (suite *SignatureTestSuite) TestValidSignature() { message.PayloadHeaderKey, string(payloadJSON), )) - _, err = suite.testServiceClient.UnaryCall(ctx, &grpc_testing.SimpleRequest{}) + _, err = suite.testServiceClient.Get(ctx, &resources.GetRequest{}) assert.NoError(suite.T(), err) }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-4p3p-cr38-v5xpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59836ghsaADVISORY
- github.com/siderolabs/omni/commit/1396083f766a1b0380e9949968d7fc17b7afecaaghsax_refsource_MISCWEB
- github.com/siderolabs/omni/commit/1fd954af64985a8b3dbf5b11deddbf7cd953f5aeghsax_refsource_MISCWEB
- github.com/siderolabs/omni/security/advisories/GHSA-4p3p-cr38-v5xpghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2025-4021ghsaWEB
News mentions
0No linked articles in our index yet.