Skip to content

Commit fc40cb0

Browse files
committed
Add GCP OAuth authentication support for GKE clusters
This implementation adds GCP OAuth 2.0 authentication to Headlamp for Google Kubernetes Engine (GKE) deployments, replacing the deprecated Identity Service for GKE. Features: - OAuth 2.0 authentication flow with Google - PKCE (Proof Key for Code Exchange) support for enhanced security - Automatic token refresh mechanism - GKE cluster detection and automatic OAuth enablement - "Sign in with Google" button in authentication chooser - Comprehensive GKE deployment documentation with RBAC examples Backend Changes: - New GCP authenticator package (backend/pkg/gcp/auth.go) - OAuth route handlers (/gcp-auth/login, /gcp-auth/callback, /gcp-auth/refresh) - Configuration support via environment variables - Token caching and refresh logic Frontend Changes: - GCPLoginButton component for Google sign-in - Modified auth chooser to show OAuth option - GKE cluster detection utilities Documentation: - Complete GKE setup guide with step-by-step instructions - Architecture overview and authentication flow documentation - RBAC configuration examples - Troubleshooting guide
1 parent 56a1508 commit fc40cb0

File tree

15 files changed

+2113
-27
lines changed

15 files changed

+2113
-27
lines changed

backend/cmd/headlamp.go

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
auth "github.com/kubernetes-sigs/headlamp/backend/pkg/auth"
4848
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
4949
cfg "github.com/kubernetes-sigs/headlamp/backend/pkg/config"
50+
"github.com/kubernetes-sigs/headlamp/backend/pkg/gcp"
5051
"github.com/kubernetes-sigs/headlamp/backend/pkg/serviceproxy"
5152

5253
headlampcfg "github.com/kubernetes-sigs/headlamp/backend/pkg/headlampconfig"
@@ -95,6 +96,11 @@ type HeadlampConfig struct {
9596
meGroupsPaths string
9697
// meUserInfoURL is the URL to fetch additional user info for the /me endpoint. /oauth2/userinfo
9798
meUserInfoURL string
99+
// GCP OAuth fields for GKE authentication
100+
gcpOAuthEnabled bool
101+
gcpClientID string
102+
gcpClientSecret string
103+
gcpRedirectURL string
98104
}
99105

100106
const DrainNodeCacheTTL = 20 // seconds
@@ -457,18 +463,27 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
457463
config.oidcCACert)
458464
if err != nil {
459465
logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context")
460-
}
461-
462-
context.Source = kubeconfig.InCluster
466+
} else {
467+
context.Source = kubeconfig.InCluster
468+
469+
// When GCP OAuth is enabled, we add the cluster context but clear the auth info
470+
// so that users are required to authenticate via GCP OAuth instead of using
471+
// the service account token automatically
472+
if config.gcpOAuthEnabled {
473+
// Clear the auth info so no automatic service account authentication happens
474+
context.AuthInfo = &api.AuthInfo{}
475+
logger.Log(logger.LevelInfo, nil, nil, "Added in-cluster context without service account auth (GCP OAuth enabled)")
476+
}
463477

464-
err = context.SetupProxy()
465-
if err != nil {
466-
logger.Log(logger.LevelError, nil, err, "Failed to setup proxy for in-cluster context")
467-
}
478+
err = context.SetupProxy()
479+
if err != nil {
480+
logger.Log(logger.LevelError, nil, err, "Failed to setup proxy for in-cluster context")
481+
}
468482

469-
err = config.KubeConfigStore.AddContext(context)
470-
if err != nil {
471-
logger.Log(logger.LevelError, nil, err, "Failed to add in-cluster context")
483+
err = config.KubeConfigStore.AddContext(context)
484+
if err != nil {
485+
logger.Log(logger.LevelError, nil, err, "Failed to add in-cluster context")
486+
}
472487
}
473488
}
474489

@@ -884,6 +899,34 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
884899
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
885900
})
886901

902+
// GCP OAuth routes for GKE authentication
903+
if config.gcpOAuthEnabled {
904+
gcpAuth := gcp.NewGCPAuthenticator(
905+
config.gcpClientID,
906+
config.gcpClientSecret,
907+
config.gcpRedirectURL,
908+
config.cache,
909+
)
910+
911+
r.HandleFunc("/gcp-auth/login", auth.HandleGCPAuthLogin(gcpAuth, config.BaseURL)).Methods("GET")
912+
r.HandleFunc("/gcp-auth/callback", auth.HandleGCPAuthCallback(gcpAuth, config.BaseURL)).Methods("GET")
913+
r.HandleFunc("/gcp-auth/refresh", auth.HandleGCPTokenRefresh(gcpAuth, config.BaseURL)).Methods("POST")
914+
915+
logger.Log(logger.LevelInfo, nil, nil, "GCP OAuth authentication enabled for GKE clusters")
916+
}
917+
918+
// Endpoint to check if GCP OAuth is enabled (frontend needs this)
919+
r.HandleFunc("/gcp-auth/enabled", func(w http.ResponseWriter, r *http.Request) {
920+
w.Header().Set("Content-Type", "application/json")
921+
if config.gcpOAuthEnabled {
922+
w.WriteHeader(http.StatusOK)
923+
w.Write([]byte(`{"enabled": true}`))
924+
} else {
925+
w.WriteHeader(http.StatusOK)
926+
w.Write([]byte(`{"enabled": false}`))
927+
}
928+
}).Methods("GET")
929+
887930
// Serve the frontend if needed
888931
if spa.UseEmbeddedFiles {
889932
r.PathPrefix("/").Handler(spa.NewEmbeddedHandler(spa.StaticFilesEmbed, "index.html", config.BaseURL))

backend/cmd/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig {
125125
cache: cache,
126126
multiplexer: multiplexer,
127127
telemetryConfig: buildTelemetryConfig(conf),
128+
// GCP OAuth fields
129+
gcpOAuthEnabled: conf.GCPOAuthEnabled,
130+
gcpClientID: conf.GCPClientID,
131+
gcpClientSecret: conf.GCPClientSecret,
132+
gcpRedirectURL: conf.GCPRedirectURL,
128133
}
129134

130135
if conf.OidcCAFile != "" {

backend/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
)
5050

5151
require (
52+
cloud.google.com/go/compute/metadata v0.9.0 // indirect
5253
dario.cat/mergo v1.0.1 // indirect
5354
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240716105424-66b64c4bb379 // indirect
5455
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
@@ -148,7 +149,7 @@ require (
148149
golang.org/x/crypto v0.40.0 // indirect
149150
golang.org/x/net v0.42.0 // indirect
150151
golang.org/x/sync v0.16.0 // indirect
151-
golang.org/x/sys v0.34.0 // indirect
152+
golang.org/x/sys v0.35.0 // indirect
152153
golang.org/x/text v0.27.0 // indirect
153154
golang.org/x/time v0.12.0 // indirect
154155
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect

backend/go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
22
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3+
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
4+
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
35
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
46
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
57
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -661,8 +663,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
661663
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
662664
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
663665
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
664-
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
665-
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
666+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
667+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
666668
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
667669
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
668670
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=

0 commit comments

Comments
 (0)