Skip to content

Commit d2393c4

Browse files
committed
fix: add logging to authentication errors in controller and router
Log authentication failures across the controller service (Listen, GetLease, RequestLease, ReleaseLease, ListLeases, Register, Unregister, Status, Dial), the Auth helpers (AuthClient, AuthExporter) which cover all ClientService call sites, the router service, and the Python exporter's PassphraseInterceptor. The peer IP address is extracted from the gRPC context and included in every auth log entry. For ClientService paths the peer is attached via peerAddr(); for ControllerService unary handlers logContext() is called at the start of each method so the peer propagates into all subsequent log entries including the auth failure. Log level: Info (not Error) is used intentionally, following the convention established by kube-apiserver. Authentication rejections are expected adversarial events, not controller bugs. Using Error would conflate "something is broken in the controller" with "a caller sent a bad token", muddying alerts. The controller-runtime/logr interface has no native Warn level (the backend is zapr, which maps logr.Info -> zap INFO and logr.Error -> zap ERROR with no WARN in between), so Info at the default V=0 verbosity is the correct choice for security-relevant but operationally normal events. Also fix the router JWT validation gRPC status code from codes.InvalidArgument to codes.Unauthenticated, which is the semantically correct code for a failed authentication. Fixes #811
1 parent f19e473 commit d2393c4

4 files changed

Lines changed: 57 additions & 8 deletions

File tree

controller/internal/service/auth/auth.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@ package auth
22

33
import (
44
"context"
5+
"net"
56

67
jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
78
"github.com/jumpstarter-dev/jumpstarter-controller/internal/authentication"
89
"github.com/jumpstarter-dev/jumpstarter-controller/internal/authorization"
910
"github.com/jumpstarter-dev/jumpstarter-controller/internal/oidc"
1011
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/peer"
1113
"google.golang.org/grpc/status"
1214
"k8s.io/apiserver/pkg/authorization/authorizer"
1315
kclient "sigs.k8s.io/controller-runtime/pkg/client"
16+
"sigs.k8s.io/controller-runtime/pkg/log"
1417
)
1518

19+
// peerAddr returns the remote IP address from the gRPC peer info stored in ctx,
20+
// or "unknown" if unavailable.
21+
func peerAddr(ctx context.Context) string {
22+
p, ok := peer.FromContext(ctx)
23+
if !ok {
24+
return "unknown"
25+
}
26+
host, _, err := net.SplitHostPort(p.Addr.String())
27+
if err != nil {
28+
return p.Addr.String()
29+
}
30+
return host
31+
}
32+
1633
type Auth struct {
1734
client kclient.Client
1835
authn authentication.ContextAuthenticator
@@ -35,6 +52,8 @@ func NewAuth(
3552
}
3653

3754
func (s *Auth) AuthClient(ctx context.Context, namespace string) (*jumpstarterdevv1alpha1.Client, error) {
55+
logger := log.FromContext(ctx).WithValues("peer", peerAddr(ctx))
56+
3857
jclient, err := oidc.VerifyClientObjectToken(
3958
ctx,
4059
s.authn,
@@ -44,17 +63,22 @@ func (s *Auth) AuthClient(ctx context.Context, namespace string) (*jumpstarterde
4463
)
4564

4665
if err != nil {
66+
logger.Info("client authentication failed", "error", err.Error())
4767
return nil, err
4868
}
4969

5070
if namespace != jclient.Namespace {
51-
return nil, status.Error(codes.PermissionDenied, "namespace mismatch")
71+
err := status.Error(codes.PermissionDenied, "namespace mismatch")
72+
logger.Info("client authentication failed", "client", jclient.Name, "error", err.Error())
73+
return nil, err
5274
}
5375

5476
return jclient, nil
5577
}
5678

5779
func (s *Auth) AuthExporter(ctx context.Context, namespace string) (*jumpstarterdevv1alpha1.Exporter, error) {
80+
logger := log.FromContext(ctx).WithValues("peer", peerAddr(ctx))
81+
5882
jexporter, err := oidc.VerifyExporterObjectToken(
5983
ctx,
6084
s.authn,
@@ -64,11 +88,14 @@ func (s *Auth) AuthExporter(ctx context.Context, namespace string) (*jumpstarter
6488
)
6589

6690
if err != nil {
91+
logger.Info("exporter authentication failed", "error", err.Error())
6792
return nil, err
6893
}
6994

7095
if namespace != jexporter.Namespace {
71-
return nil, status.Error(codes.PermissionDenied, "namespace mismatch")
96+
err := status.Error(codes.PermissionDenied, "namespace mismatch")
97+
logger.Info("exporter authentication failed", "exporter", jexporter.Name, "error", err.Error())
98+
return nil, err
7299
}
73100

74101
return jexporter, nil

controller/internal/service/controller_service.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,12 @@ func (s *ControllerService) authenticateExporter(ctx context.Context) (*jumpstar
240240
}
241241

242242
func (s *ControllerService) Register(ctx context.Context, req *pb.RegisterRequest) (*pb.RegisterResponse, error) {
243+
ctx = logContext(ctx)
243244
logger := log.FromContext(ctx)
244245

245246
exporter, err := s.authenticateExporter(ctx)
246247
if err != nil {
247-
logger.Info("unable to authenticate exporter", "error", err.Error())
248+
logger.Info("exporter authentication failed", "error", err.Error())
248249
return nil, err
249250
}
250251

@@ -311,7 +312,7 @@ func (s *ControllerService) Unregister(
311312

312313
exporter, err := s.authenticateExporter(ctx)
313314
if err != nil {
314-
logger.Error(err, "unable to authenticate exporter")
315+
logger.Info("unable to authenticate exporter", "error", err.Error())
315316
return nil, err
316317
}
317318

@@ -524,6 +525,7 @@ func (s *ControllerService) Listen(req *pb.ListenRequest, stream pb.ControllerSe
524525

525526
exporter, err := s.authenticateExporter(ctx)
526527
if err != nil {
528+
logger.Info("exporter authentication failed", "error", err.Error())
527529
return err
528530
}
529531

@@ -613,7 +615,7 @@ func (s *ControllerService) Status(req *pb.StatusRequest, stream pb.ControllerSe
613615

614616
exporter, err := s.authenticateExporter(ctx)
615617
if err != nil {
616-
logger.Error(err, "unable to authenticate exporter")
618+
logger.Info("unable to authenticate exporter", "error", err.Error())
617619
return err
618620
}
619621

@@ -747,7 +749,7 @@ func (s *ControllerService) Dial(ctx context.Context, req *pb.DialRequest) (*pb.
747749

748750
client, err := s.authenticateClient(ctx)
749751
if err != nil {
750-
logger.Error(err, "unable to authenticate client")
752+
logger.Info("unable to authenticate client", "error", err.Error())
751753
return nil, err
752754
}
753755

@@ -898,8 +900,12 @@ func (s *ControllerService) GetLease(
898900
ctx context.Context,
899901
req *pb.GetLeaseRequest,
900902
) (*pb.GetLeaseResponse, error) {
903+
ctx = logContext(ctx)
904+
logger := log.FromContext(ctx)
905+
901906
client, err := s.authenticateClient(ctx)
902907
if err != nil {
908+
logger.Info("client authentication failed", "error", err.Error())
903909
return nil, err
904910
}
905911

@@ -977,8 +983,12 @@ func (s *ControllerService) RequestLease(
977983
ctx context.Context,
978984
req *pb.RequestLeaseRequest,
979985
) (*pb.RequestLeaseResponse, error) {
986+
ctx = logContext(ctx)
987+
logger := log.FromContext(ctx)
988+
980989
client, err := s.authenticateClient(ctx)
981990
if err != nil {
991+
logger.Info("client authentication failed", "error", err.Error())
982992
return nil, err
983993
}
984994

@@ -1031,8 +1041,12 @@ func (s *ControllerService) ReleaseLease(
10311041
ctx context.Context,
10321042
req *pb.ReleaseLeaseRequest,
10331043
) (*pb.ReleaseLeaseResponse, error) {
1044+
ctx = logContext(ctx)
1045+
logger := log.FromContext(ctx)
1046+
10341047
jclient, err := s.authenticateClient(ctx)
10351048
if err != nil {
1049+
logger.Info("client authentication failed", "error", err.Error())
10361050
return nil, err
10371051
}
10381052

@@ -1062,8 +1076,12 @@ func (s *ControllerService) ListLeases(
10621076
ctx context.Context,
10631077
req *pb.ListLeasesRequest,
10641078
) (*pb.ListLeasesResponse, error) {
1079+
ctx = logContext(ctx)
1080+
logger := log.FromContext(ctx)
1081+
10651082
jclient, err := s.authenticateClient(ctx)
10661083
if err != nil {
1084+
logger.Info("client authentication failed", "error", err.Error())
10671085
return nil, err
10681086
}
10691087

controller/internal/service/router_service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (s *RouterService) authenticate(ctx context.Context) (string, error) {
7171
)
7272

7373
if err != nil || !parsed.Valid {
74-
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token")
74+
return "", status.Errorf(codes.Unauthenticated, "invalid jwt token")
7575
}
7676

7777
return parsed.Claims.GetSubject()
@@ -83,7 +83,7 @@ func (s *RouterService) Stream(stream pb.RouterService_StreamServer) error {
8383

8484
streamName, err := s.authenticate(ctx)
8585
if err != nil {
86-
logger.Error(err, "failed to authenticate")
86+
logger.Info("router authentication failed", "error", err.Error())
8787
return err
8888
}
8989

python/packages/jumpstarter/jumpstarter/exporter/auth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ async def intercept_service(self, continuation, handler_call_details):
2525
provided = metadata.get(PASSPHRASE_METADATA_KEY)
2626

2727
if provided is None or not hmac.compare_digest(provided, self._passphrase):
28+
logger.warning(
29+
"authentication failed: invalid or missing passphrase for method %s",
30+
handler_call_details.method,
31+
)
2832
# Resolve the real handler to preserve the RPC type, then reject
2933
handler = await continuation(handler_call_details)
3034
if handler is None:

0 commit comments

Comments
 (0)