fix: audit trustd code for security

There are no security issues fixed.

Drop username/password creds - they were not used.

Improve security of token interceptor.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2026-04-16 20:05:10 +04:00
parent 986e97fc75
commit 9fbb7c95df
No known key found for this signature in database
GPG Key ID: 322C6F63F594CE7C
6 changed files with 114 additions and 103 deletions

View File

@ -104,6 +104,7 @@ func trustdMain() error {
&reg.Registrator{Resources: resources},
factory.WithDefaultLog(),
factory.WithUnaryInterceptor(creds.UnaryInterceptor()),
factory.WithStreamInterceptor(creds.StreamInterceptor()),
factory.ServerOptions(
grpc.Creds(
credentials.NewTLS(serverTLSConfig),

View File

@ -23,6 +23,7 @@ type Credentials interface {
credentials.PerRPCCredentials
UnaryInterceptor() grpc.UnaryServerInterceptor
StreamInterceptor() grpc.StreamServerInterceptor
}
// NewConnection initializes a grpc.ClientConn configured for basic

View File

@ -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")
}
}

View File

@ -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)
}
})
}
}

View File

@ -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
}
}

View File

@ -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
}