mirror of
https://github.com/imjasonh/gke-auth
synced 2026-07-07 00:23:18 +00:00
The gcloud cred helper [requests these scopes](c19c59784d/cmd/gke-gcloud-auth-plugin/default_credentials_token_provider.go (L17-L19)) when obtaining a token. Without this scope, service accounts authenticate to GKE as their unique ID, which means the RBAC policies must do the same. This is inconvenient when authoring policies since it's hard to reason about which authentication flows will use the email vs unique ID.
211 lines
5.9 KiB
Go
211 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/cli/config"
|
|
"golang.org/x/oauth2/google"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
"k8s.io/client-go/tools/clientcmd/api"
|
|
)
|
|
|
|
var (
|
|
project = flag.String("project", "", "Name of the project")
|
|
location = flag.String("location", "", "Location of the cluster")
|
|
clusterName = flag.String("cluster", "", "Name of the cluster")
|
|
|
|
get = flag.Bool("get", false, "If true, print auth information")
|
|
clear = flag.Bool("clear", false, "If true, clear auth for this cluster")
|
|
configureDocker = flag.Bool("configure-docker", false, "If true, configure docker to use this token")
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
ctx := context.Background()
|
|
|
|
if *get && *clear {
|
|
log.Fatal("cannot pass both --get and --clear")
|
|
}
|
|
if (*get || *clear) && *configureDocker {
|
|
log.Fatal("cannot pass --get or --clear with --configure-docker")
|
|
}
|
|
|
|
if *configureDocker {
|
|
if *location == "" {
|
|
log.Fatal("must pass --location when using --configure-docker")
|
|
}
|
|
cfg, err := config.Load(config.Dir())
|
|
if err != nil {
|
|
log.Fatalf("Loading docker config: %v", err)
|
|
}
|
|
if cfg.CredentialHelpers == nil {
|
|
cfg.CredentialHelpers = map[string]string{}
|
|
}
|
|
host := fmt.Sprintf("%s-docker.pkg.dev", *location)
|
|
cfg.CredentialHelpers[host] = "gke-auth"
|
|
path, err := exec.LookPath(os.Args[0])
|
|
if err != nil {
|
|
log.Fatalf("Looking up path: %v", err)
|
|
}
|
|
sl := filepath.Dir(path) + "/docker-credential-gke-auth"
|
|
if err := os.Remove(sl); err != nil && !os.IsNotExist(err) {
|
|
log.Fatalf("Removing existing symlink: %v", err)
|
|
}
|
|
if err := os.Symlink(path, filepath.Dir(path)+"/docker-credential-gke-auth"); err != nil {
|
|
log.Fatalf("Symlinking: %v", err)
|
|
}
|
|
if err := cfg.Save(); err != nil {
|
|
log.Fatalf("Saving docker config: %v", err)
|
|
}
|
|
log.Printf("Docker configured to use gke-auth for %q in %q", host, *location)
|
|
return
|
|
}
|
|
|
|
// get a token
|
|
scopes := []string{
|
|
// Base scope for GCP auth
|
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
// Needed in order to use service account emails instead of unique IDs in K8S RBAC.
|
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
}
|
|
ts, err := google.DefaultTokenSource(ctx, scopes...)
|
|
if err != nil {
|
|
log.Fatalf("google.DefaultTokenSource: %v", err)
|
|
}
|
|
tok, err := ts.Token()
|
|
if err != nil {
|
|
log.Fatalf("ts.Token: %v", err)
|
|
}
|
|
|
|
if strings.HasSuffix(os.Args[0], "docker-credential-gke-auth") {
|
|
urlb, err := io.ReadAll(io.LimitReader(os.Stdin, 1000))
|
|
if err != nil {
|
|
log.Fatalf("Reading stdin: %v", err)
|
|
}
|
|
|
|
if err := json.NewEncoder(os.Stdout).Encode(&struct {
|
|
ServerURL string `json:"ServerURL"`
|
|
Username string `json:"Username"`
|
|
Secret string `json:"Secret"`
|
|
}{
|
|
ServerURL: string(urlb),
|
|
Username: "oauth2accesstoken",
|
|
Secret: tok.AccessToken,
|
|
}); err != nil {
|
|
log.Fatalf("Encoding JSON: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !*get && (*project == "" || *location == "" || *clusterName == "") {
|
|
log.Fatal("must pass --project and --location and --cluster")
|
|
}
|
|
|
|
if *get {
|
|
// just print the token
|
|
if err := json.NewEncoder(os.Stdout).Encode(&clientauthv1.ExecCredential{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "client.authentication.k8s.io/v1",
|
|
Kind: "ExecCredential",
|
|
},
|
|
Status: &clientauthv1.ExecCredentialStatus{
|
|
ExpirationTimestamp: &metav1.Time{Time: tok.Expiry},
|
|
Token: tok.AccessToken,
|
|
},
|
|
}); err != nil {
|
|
log.Fatalf("Encoding JSON: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
url := fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s", *project, *location, *clusterName)
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
log.Fatalf("http.NewRequest: %v", err)
|
|
}
|
|
tok.SetAuthHeader(req)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Fatalf("http.Do: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
all, _ := io.ReadAll(resp.Body)
|
|
log.Fatalf("http.Do: %d %s %s", resp.StatusCode, resp.Status, string(all))
|
|
}
|
|
var cluster struct {
|
|
Endpoint string
|
|
MasterAuth struct {
|
|
ClusterCaCertificate string
|
|
}
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&cluster); err != nil {
|
|
log.Fatalf("json.Decode: %v", err)
|
|
}
|
|
|
|
cfg, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
|
|
if err != nil {
|
|
log.Fatalf("Loading kubeconfig %v", err)
|
|
}
|
|
|
|
// get kubeconfig location
|
|
key := fmt.Sprintf("gke_%s_%s_%s", *project, *location, *clusterName)
|
|
kcfgPath := clientcmd.RecommendedHomeFile
|
|
if auth := cfg.AuthInfos[key]; auth != nil && auth.LocationOfOrigin != "" {
|
|
kcfgPath = auth.LocationOfOrigin
|
|
}
|
|
|
|
// add user
|
|
if *clear {
|
|
cfg.AuthInfos[key] = &api.AuthInfo{}
|
|
cfg.Clusters[key] = &api.Cluster{}
|
|
cfg.Contexts[key] = &api.Context{}
|
|
cfg.CurrentContext = ""
|
|
log.Println("Auth config cleared")
|
|
} else {
|
|
cfg.AuthInfos[key] = &api.AuthInfo{
|
|
Exec: &api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1",
|
|
Command: os.Args[0],
|
|
Args: []string{"--get"},
|
|
InteractiveMode: api.NeverExecInteractiveMode,
|
|
InstallHint: "go install github.com/imjasonh/gke-auto@latest",
|
|
},
|
|
}
|
|
|
|
// add cluster
|
|
dec, err := base64.StdEncoding.DecodeString(cluster.MasterAuth.ClusterCaCertificate)
|
|
if err != nil {
|
|
log.Fatalf("Decoding CA cert: %v", err)
|
|
}
|
|
cfg.Clusters[key] = &api.Cluster{
|
|
Server: "https://" + cluster.Endpoint,
|
|
CertificateAuthorityData: dec,
|
|
}
|
|
|
|
// update current context
|
|
cfg.Contexts[key] = &api.Context{
|
|
AuthInfo: key,
|
|
Cluster: key,
|
|
}
|
|
cfg.CurrentContext = key
|
|
}
|
|
|
|
// write the file back
|
|
if err := clientcmd.WriteToFile(*cfg, kcfgPath); err != nil {
|
|
log.Fatalf("Writing kubeconfig %q: %v", kcfgPath, err)
|
|
}
|
|
}
|