diff --git a/internal/app/trustd/main.go b/internal/app/trustd/main.go index 1ed85d660..07a4f7431 100644 --- a/internal/app/trustd/main.go +++ b/internal/app/trustd/main.go @@ -104,6 +104,7 @@ func trustdMain() error { ®.Registrator{Resources: resources}, factory.WithDefaultLog(), factory.WithUnaryInterceptor(creds.UnaryInterceptor()), + factory.WithStreamInterceptor(creds.StreamInterceptor()), factory.ServerOptions( grpc.Creds( credentials.NewTLS(serverTLSConfig), diff --git a/pkg/grpc/middleware/auth/basic/basic.go b/pkg/grpc/middleware/auth/basic/basic.go index b1f7a98d7..a040356f0 100644 --- a/pkg/grpc/middleware/auth/basic/basic.go +++ b/pkg/grpc/middleware/auth/basic/basic.go @@ -23,6 +23,7 @@ type Credentials interface { credentials.PerRPCCredentials UnaryInterceptor() grpc.UnaryServerInterceptor + StreamInterceptor() grpc.StreamServerInterceptor } // NewConnection initializes a grpc.ClientConn configured for basic diff --git a/pkg/grpc/middleware/auth/basic/token.go b/pkg/grpc/middleware/auth/basic/token.go index 938342dd0..e28502939 100644 --- a/pkg/grpc/middleware/auth/basic/token.go +++ b/pkg/grpc/middleware/auth/basic/token.go @@ -6,11 +6,13 @@ package basic import ( "context" - "fmt" + "crypto/sha256" + "crypto/subtle" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" ) // TokenGetterFunc is the function to dynamically retrieve the token. @@ -66,13 +68,23 @@ func (b *TokenCredentials) authenticate(ctx context.Context) error { return err } - if md, ok := metadata.FromIncomingContext(ctx); ok { - if len(md["token"]) > 0 && md["token"][0] == token { - return nil - } + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return status.Error(codes.Unauthenticated, "missing token") } - return fmt.Errorf("%s", codes.Unauthenticated.String()) + if len(md["token"]) == 0 { + return status.Error(codes.Unauthenticated, "missing token") + } + + incomingTokenHash := sha256.Sum256([]byte(md["token"][0])) + expectedTokenHash := sha256.Sum256([]byte(token)) + + if subtle.ConstantTimeCompare(incomingTokenHash[:], expectedTokenHash[:]) != 1 { + return status.Error(codes.Unauthenticated, "invalid token") + } + + return nil } // UnaryInterceptor sets the UnaryServerInterceptor for the server and enforces @@ -86,3 +98,14 @@ func (b *TokenCredentials) UnaryInterceptor() grpc.UnaryServerInterceptor { return handler(ctx, req) } } + +// StreamInterceptor sets the StreamServerInterceptor for the server and enforces +// basic authentication. +// +// For now, it rejects any API, as we don't have any streaming APIs in trustd component. +// This is to prevent accidentally allowing unauthenticated access to streaming APIs in the future without realizing it. +func (b *TokenCredentials) StreamInterceptor() grpc.StreamServerInterceptor { + return func(any, grpc.ServerStream, *grpc.StreamServerInfo, grpc.StreamHandler) error { + return status.Error(codes.Unimplemented, "streaming APIs are not supported") + } +} diff --git a/pkg/grpc/middleware/auth/basic/token_test.go b/pkg/grpc/middleware/auth/basic/token_test.go new file mode 100644 index 000000000..deb06b757 --- /dev/null +++ b/pkg/grpc/middleware/auth/basic/token_test.go @@ -0,0 +1,83 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package basic_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/siderolabs/talos/pkg/grpc/middleware/auth/basic" +) + +func TestTokenInterceptor(t *testing.T) { + t.Parallel() + + const validToken = "valid-token" + + creds := basic.NewTokenCredentials(validToken) + + interceptor := creds.UnaryInterceptor() + + for _, test := range []struct { + name string + + md metadata.MD + wantErr bool + }{ + { + name: "valid token", + + md: metadata.MD{ + "token": []string{validToken}, + }, + wantErr: false, + }, + { + name: "invalid token", + + md: metadata.MD{ + "token": []string{"invalid-token"}, + }, + wantErr: true, + }, + { + name: "missing token", + md: metadata.MD{}, + wantErr: true, + }, + { + name: "no metadata", + md: nil, + wantErr: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + if test.md != nil { + ctx = metadata.NewIncomingContext(ctx, test.md) + } + + _, err := interceptor(ctx, nil, nil, func(context.Context, any) (any, error) { + return nil, nil + }) + + if test.wantErr { + require.Error(t, err) + assert.Equal(t, codes.Unauthenticated, status.Code(err)) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/grpc/middleware/auth/basic/username_and_password.go b/pkg/grpc/middleware/auth/basic/username_and_password.go deleted file mode 100644 index 9a4a23e25..000000000 --- a/pkg/grpc/middleware/auth/basic/username_and_password.go +++ /dev/null @@ -1,83 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package basic - -import ( - "context" - "fmt" - "log" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" -) - -// UsernameAndPasswordCredentials implements credentials.PerRPCCredentials. It uses a basic -// username and password lookup to authenticate users. -type UsernameAndPasswordCredentials struct { - Username string - Password string -} - -// NewUsernameAndPasswordCredentials initializes username and password -// Credentials. -func NewUsernameAndPasswordCredentials(username, password string) (creds Credentials) { - creds = &UsernameAndPasswordCredentials{ - Username: username, - Password: password, - } - - return creds -} - -// GetRequestMetadata sets the value for the username and password. -func (b *UsernameAndPasswordCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { - return map[string]string{ - "username": b.Username, - "password": b.Password, - }, nil -} - -// RequireTransportSecurity is set to true in order to encrypt the -// communication. -func (b *UsernameAndPasswordCredentials) RequireTransportSecurity() bool { - return true -} - -func (b *UsernameAndPasswordCredentials) authorize(ctx context.Context) error { - if md, ok := metadata.FromIncomingContext(ctx); ok { - if len(md["username"]) > 0 && md["username"][0] == b.Username && - len(md["password"]) > 0 && md["password"][0] == b.Password { - return nil - } - - return fmt.Errorf("%s", codes.Unauthenticated.String()) - } - - return nil -} - -// UnaryInterceptor sets the UnaryServerInterceptor for the server and enforces -// basic authentication. -func (b *UsernameAndPasswordCredentials) UnaryInterceptor() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - start := time.Now() - - if err := b.authorize(ctx); err != nil { - return nil, err - } - - h, err := handler(ctx, req) - - log.Printf("request - Method:%s\tDuration:%s\tError:%v\n", - info.FullMethod, - time.Since(start), - err, - ) - - return h, err - } -} diff --git a/pkg/grpc/middleware/auth/basic/username_and_password_test.go b/pkg/grpc/middleware/auth/basic/username_and_password_test.go deleted file mode 100644 index 6d98e236b..000000000 --- a/pkg/grpc/middleware/auth/basic/username_and_password_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package basic_test - -import "testing" - -func TestEmpty(t *testing.T) { - // added for accurate coverage estimation - // - // please remove it once any unit-test is added - // for this package -}