@@ -38,6 +38,7 @@ import (
3838 "github.com/fluxcd/pkg/runtime/conditions"
3939 "github.com/go-logr/logr"
4040 "github.com/google/go-github/v64/github"
41+ "google.golang.org/api/idtoken"
4142 corev1 "k8s.io/api/core/v1"
4243 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4344 "k8s.io/apimachinery/pkg/runtime/schema"
@@ -224,12 +225,23 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver apiv1.Receiver,
224225 }
225226 r .Body = io .NopCloser (bytes .NewReader (b ))
226227
227- // Fetch the token .
228- token , err := s .token (ctx , receiver )
228+ // Fetch the secret .
229+ secret , err := s .secret (ctx , receiver )
229230 if err != nil {
230- return fmt .Errorf ("unable to read token , error: %w" , err )
231+ return fmt .Errorf ("unable to read secret , error: %w" , err )
231232 }
232233
234+ // Extract the token from the secret.
235+ secretName := types.NamespacedName {
236+ Namespace : receiver .GetNamespace (),
237+ Name : receiver .Spec .SecretRef .Name ,
238+ }
239+ tokenBytes , ok := secret .Data ["token" ]
240+ if ! ok {
241+ return fmt .Errorf ("invalid %q secret data: required field 'token'" , secretName )
242+ }
243+ token := string (tokenBytes )
244+
233245 logger := s .logger .WithValues (
234246 "reconciler kind" , apiv1 .ReceiverKind ,
235247 "name" , receiver .Name ,
@@ -399,8 +411,6 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver apiv1.Receiver,
399411 r .Body = io .NopCloser (bytes .NewReader (b ))
400412 return nil
401413 case apiv1 .GCRReceiver :
402- const tokenIndex = len ("Bearer " )
403-
404414 type data struct {
405415 Action string `json:"action"`
406416 Digest string `json:"digest"`
@@ -416,8 +426,34 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver apiv1.Receiver,
416426 } `json:"message"`
417427 }
418428
419- err := authenticateGCRRequest (& http.Client {}, r .Header .Get ("Authorization" ), tokenIndex )
420- if err != nil {
429+ expectedEmail , ok := secret .Data ["email" ]
430+ _ = ok
431+ // TODO: in Flux 2.9, require the email. this will be a breaking change.
432+ // if !ok {
433+ // return fmt.Errorf("invalid secret data: required field 'email' for GCR receiver")
434+ // }
435+
436+ // Determine the expected audience. If explicitly set in the secret, use
437+ // that. Otherwise, reconstruct the webhook URL from the request, which is
438+ // the default audience used by GCR when it sends the webhook.
439+ audience := string (secret .Data ["audience" ])
440+ if audience == "" {
441+ scheme := "https"
442+ if r .TLS == nil {
443+ if proto := r .Header .Get ("X-Forwarded-Proto" ); proto != "" {
444+ scheme = proto
445+ } else {
446+ scheme = "http"
447+ }
448+ }
449+ audience = scheme + "://" + r .Host + r .URL .Path
450+ }
451+
452+ authenticate := authenticateGCRRequest
453+ if s .gcrTokenValidator != nil {
454+ authenticate = s .gcrTokenValidator
455+ }
456+ if err := authenticate (ctx , r .Header .Get ("Authorization" ), string (expectedEmail ), audience ); err != nil {
421457 return fmt .Errorf ("cannot authenticate GCR request: %w" , err )
422458 }
423459
@@ -499,26 +535,18 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver apiv1.Receiver,
499535 return fmt .Errorf ("recevier type %q not supported" , receiver .Spec .Type )
500536}
501537
502- func (s * ReceiverServer ) token (ctx context.Context , receiver apiv1.Receiver ) (string , error ) {
503- token := ""
538+ func (s * ReceiverServer ) secret (ctx context.Context , receiver apiv1.Receiver ) (* corev1.Secret , error ) {
504539 secretName := types.NamespacedName {
505540 Namespace : receiver .GetNamespace (),
506541 Name : receiver .Spec .SecretRef .Name ,
507542 }
508543
509544 var secret corev1.Secret
510- err := s .kubeClient .Get (ctx , secretName , & secret )
511- if err != nil {
512- return "" , fmt .Errorf ("unable to read token from secret %q error: %w" , secretName , err )
545+ if err := s .kubeClient .Get (ctx , secretName , & secret ); err != nil {
546+ return nil , fmt .Errorf ("unable to read secret %q: %w" , secretName , err )
513547 }
514548
515- if val , ok := secret .Data ["token" ]; ok {
516- token = string (val )
517- } else {
518- return "" , fmt .Errorf ("invalid %q secret data: required field 'token'" , secretName )
519- }
520-
521- return token , nil
549+ return & secret , nil
522550}
523551
524552// requestReconciliation requests reconciliation of all the resources matching the given CrossNamespaceObjectReference by annotating them accordingly.
@@ -578,26 +606,53 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource *metav1.PartialO
578606 return nil
579607}
580608
581- func authenticateGCRRequest (c * http.Client , bearer string , tokenIndex int ) (err error ) {
582- type auth struct {
583- Aud string `json:"aud"`
609+ // authenticateGCRRequest validates the OIDC ID token according to
610+ // https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions#go.
611+ func authenticateGCRRequest (ctx context.Context , bearer string , expectedEmail string , expectedAudience string ) error {
612+ const bearerPrefix = "Bearer "
613+ if ! strings .HasPrefix (bearer , bearerPrefix ) {
614+ return fmt .Errorf ("the Authorization header is missing or malformed" )
584615 }
585616
586- if len (bearer ) < tokenIndex {
587- return fmt .Errorf ("the Authorization header is missing or malformed: %v" , bearer )
588- }
617+ token := bearer [len (bearerPrefix ):]
589618
590- token := bearer [tokenIndex :]
591- url := fmt .Sprintf ("https://oauth2.googleapis.com/tokeninfo?id_token=%s" , token )
592-
593- resp , err := c .Get (url )
619+ // Validate the OIDC ID token signature and claims using Google's public keys.
620+ v , err := idtoken .NewValidator (ctx )
621+ if err != nil {
622+ return fmt .Errorf ("cannot create ID token validator: %w" , err )
623+ }
624+ payload , err := v .Validate (ctx , token , expectedAudience )
594625 if err != nil {
595- return fmt .Errorf ("cannot verify authenticity of payload: %w" , err )
626+ // Extract the actual audience from the token for logging.
627+ gotAudience := "<unknown>"
628+ if parts := strings .Split (token , "." ); len (parts ) == 3 {
629+ if claimsJSON , decErr := base64 .RawURLEncoding .DecodeString (parts [1 ]); decErr == nil {
630+ var claims struct {
631+ Aud string `json:"aud"`
632+ }
633+ if json .Unmarshal (claimsJSON , & claims ) == nil && claims .Aud != "" {
634+ gotAudience = claims .Aud
635+ }
636+ }
637+ }
638+ return fmt .Errorf ("invalid ID token: audience is '%s', want '%s': %w" , gotAudience , expectedAudience , err )
596639 }
597640
598- var p auth
599- if err := json .NewDecoder (resp .Body ).Decode (& p ); err != nil {
600- return fmt .Errorf ("cannot decode auth payload: %w" , err )
641+ // Verify the token issuer.
642+ issuer , _ := payload .Claims ["iss" ].(string )
643+ if issuer != "accounts.google.com" && issuer != "https://accounts.google.com" {
644+ return fmt .Errorf ("token issuer is '%s', want 'accounts.google.com' or 'https://accounts.google.com'" , issuer )
645+ }
646+
647+ // Verify the token was issued for the expected service account.
648+ email , _ := payload .Claims ["email" ].(string )
649+ emailVerified , _ := payload .Claims ["email_verified" ].(bool )
650+ // TODO: in Flux 2.9, require the email (remove `expectedEmail != "" &&`). this will be a breaking change.
651+ if expectedEmail != "" && email != expectedEmail {
652+ return fmt .Errorf ("token email is '%s', want '%s'" , email , expectedEmail )
653+ }
654+ if ! emailVerified {
655+ return fmt .Errorf ("token email '%s' is not verified" , email )
601656 }
602657
603658 return nil
0 commit comments