diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml new file mode 100644 index 0000000..2d05747 --- /dev/null +++ b/.github/workflows/build-release-binaries.yml @@ -0,0 +1,31 @@ +name: Build Release Binaries + +on: + release: + types: + - created + +jobs: + build: + name: Build Release Assets + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.23.4 + + - name: Build kubekey for MacOS(darwin), linux and windows + run: make + + - name: Upload the Rodeo binaries + uses: actions/svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref }} + file: ./kubekey-*-amd64* + file_glob: true + body: "Release of kubekey v${{ github.ref }}" diff --git a/Makefile b/Makefile index ce838a3..c331a10 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,16 @@ .PHONY: all clean update -all: kubekey +all: kubekey-darwin-amd64 kubekey-linux-amd64 kubekey-windows-amd64.exe clean: - rm -f kubekey + rm -f kubekey-*-amd64* -kubekey: kubekey.go +kubekey-%-amd64: kubekey.go go mod download - CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' kubekey.go + GOOS=$(patsubst kubekey-%-amd64,%,$@) GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o $@ kubekey.go + +kubekey-%-amd64.exe: kubekey-%-amd64 + mv $^ $@ update: go get -u ./... diff --git a/README.md b/README.md index 755f5b7..6936819 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ kubekey is a [client-go credentials plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins) for kubectl +* Compile with make +* Optionally make html templates for OK and failure available, e.g. in /etc/kubekey/html_fail.tmpl and /etc/kubekey/html_ok.tmpl +* Configure your OIDC issuer - you need to get + * `CLIENT_ID`: A client id that all tokens must be issued for. + * `CLIENT_SECRET`: Empty if supported by your issuer, or if needed just set this to what you receive when configuring the issuer. + * `IDP_ISSUER_URL`: If the issuer's OIDC discovery URL is https://accounts.provider.example/.well-known/openid-configuration, the value should be https://accounts.provider.example +* Configure your kubernetes cluster to trust an OIDC issuer, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuring-the-api-server +* See [example configuration](./examples/.kube/config) for more instruction on how to configure kubekey for usage with kubectl for your users + ## Copyright and license -Copyright (C) 2019 MET Norway. kubekey is licensed under [GPL version 2](https://github.com/metno/kubekey/blob/master/LICENSE) or (at your option) any later version. +Copyright (C) 2019 - 2025 MET Norway. kubekey is licensed under [GPL version 2](https://github.com/metno/kubekey/blob/master/LICENSE) or (at your option) any later version. diff --git a/example/.kube/config b/example/.kube/config new file mode 100644 index 0000000..ff0f278 --- /dev/null +++ b/example/.kube/config @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: Config +preferences: {} + +# +# Add any number of kubernetes clusters here +# - Extract your Certificate Authority Data from the relevant cluster +# on a kubeadm created cluster you could find it in /etc/kubernetes/admin.conf +# on the first control-plane node +# - Expose your API so that your users can access it via a jumphost, VPN or other aproperiate mechanism - update the server URL accordingly +# +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJWlpKTFE0UFBBbVl3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBeE1Ea3hNekF5TkRKYUZ3MHpOVEF4TURjeE16QTNOREphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURDSlZYUXpaVmJFWS90Yk10WGU1Qis4S2JTM1d2TjdLbmU2Y0lkMDBHcG1zcTE4dVRUQnJnRDBPam8Kb3JvUXk5NFNuSjAwamFqUzdDaElsaUR1bHREd2pjU1FkZFUrWlR0dCt6eDlvV24zTlVFc1lrSUR4a3VsREx6QgpxNVc2YnpWY1lHU2JmejRCQkFJbmxlcmJkb2hRdVozYno1SXJHOXUxZkFlMjNZQ2NsZnFvbU40c0V2Yi9aL1JMClY3Tm5oSzdIak1wU0drZzcvRk1Gd0VOSFZJK2N4djZlWnUrdDVnV1Bkc3ZtVjAzNXJIR2xoVHFydXVGaHFaR2EKd3J1N1hhb2JtVHdMRlVpQ3FPK0pOZ25JTElNdXc5MU1PZDlnNXNVS3pPdTlkdlo0RGd2emtCbWFpRXFEUFljRAp5eGNvN0ZpRkQ1UjZDM3ZvYm9mbXByK3lDZjd4QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSdlVzM0JGVEJvM1UvUGQrWm8rZmdrbjQ5US9qQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVRJbExXUTVyMAozYkhVS1NaOURoRmJFMTRNSFFGbTVZRlZsTGNoUFVkMlJhQ1ZnWVVuc0VFaW53TUdMSnpJck5jL3ppbkU4QllwCms2cGlIckticjU0dnU0LzhYL1hjM09CaGs3eWFjSDlaaUZYU0ZMa2lEN2k5dnZLNXI5QU9sRWVYY3ppNUZYZWYKa2VRQTZUSmxKZFpoL0NIdXJyeTUvTk1wa3E4blN4Z0p5cTg4T2tCd3pQSTNKU0gwcy9CZW1jeTNNV1A0dEREOApIaFc4TkRubUREcTVRSHExYytSUHY2eTgxTzR5T3NNYm9HUmpUQnVMYWgzdkxzRjQveWx0cm5FY0RMV3JTUDlMClE3VVRPYnVrNy94YXdGejQvVTBuTlVjS08zQ0JDQldtNWx4ZDZvZHVwVmJEVFA0NHlNT2t1ZU5HTDB5TXRpakQKRzl6Vm4rNDhaQndnCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://cluster.example.com:6443 + name: k8s.met.no + +# +# Any OpenIDConnect provider should do - here is an example using google login +# Update CLIENT_ID and CLIENT_SECRET for use with google (and also IDP_ISSUER_URL if you are using another provider) +# +users: +- name: google-login + user: + exec: + apiVersion: "client.authentication.k8s.io/v1beta1" + command: /usr/local/bin/kubekey + env: + - name: CLIENT_ID + value: 605222161680-h15ydfp5zhxp1cjzlzazphq1kptyejam.apps.googleusercontent.com + - name: CLIENT_SECRET + value: SVLHacFXLeuJxlIPNciOeFzl + - name: IDP_ISSUER_URL + value: https://accounts.google.com + +# +# Contexts just refences other sections in this configuration file, update names to match +# what you have used above. Notice that you could also set a current-context by name +# +contexts: +- context: + cluster: cluster.example.com + user: google-login + name: cluster.example.com +current-context: cluster.example.com diff --git a/kubekey.go b/kubekey.go index d1bb5b6..c5b7601 100644 --- a/kubekey.go +++ b/kubekey.go @@ -1,6 +1,6 @@ /* kubekey is a client-go credentials plugin for kubectl -Copyright (C) 2019 Meteorologisk Institutt (MET Norway) +Copyright (C) 2019 - 2025 Meteorologisk Institutt (MET Norway) Postboks 43 Blindern, 0313 OSLO, Norway - www.met.no This program is free software; you can redistribute it and/or @@ -23,11 +23,12 @@ import ( "context" "crypto/rand" "crypto/sha256" + "embed" "encoding/base64" "encoding/json" "flag" "fmt" - "html" + "html/template" "log" "net" "net/http" @@ -43,41 +44,22 @@ import ( "golang.org/x/oauth2" ) -const Version = "1.0.0" -const HTML_OK string = ` - -
- -Please close this window and return to kubectl
- - - - -` -const HTML_FAIL string = ` - - - -%s
- - - - - -` +const Version = "1.0.20250110" + +//go:embed templates/* +var embeddedTemplates embed.FS +var useEmbeddedTemplates bool + +func ParseFiles(filename string) (*template.Template, error) { + if useEmbeddedTemplates { + return template.ParseFS(embeddedTemplates, fmt.Sprintf("templates/%v", filename)) + } + return template.ParseFiles(filename) +} + +type FailMsg struct { + Msg string +} func newState() string { random := make([]byte, 32) @@ -125,7 +107,7 @@ func (cfg *OIDC) Authenticate(tokenChan chan<- string) { time.Sleep(2 * time.Second) srv.Close() } - + // Listen to random port on localhost listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -161,12 +143,17 @@ func (cfg *OIDC) Authenticate(tokenChan chan<- string) { }) mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { - var token string - var err error + var token string + var err error fail := func(msg string) { + failMsg := FailMsg{Msg: msg} w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, HTML_FAIL, html.EscapeString(msg)) + tmpl, err := ParseFiles("html_fail.tmpl") + if err != nil { + log.Fatal(err) + } + tmpl.Execute(w, failMsg) go closeSrv(token, err) } @@ -190,7 +177,7 @@ func (cfg *OIDC) Authenticate(tokenChan chan<- string) { fail("Failed to verify ID Token: " + err.Error()) return } - token = fmt.Sprintf("%s.%d", rawIDToken, idToken.Expiry.Unix()) + token = fmt.Sprintf("%s.%d", rawIDToken, idToken.Expiry.Unix()) var claims struct { Name string `json:"name"` @@ -201,7 +188,11 @@ func (cfg *OIDC) Authenticate(tokenChan chan<- string) { fail("Obtained IDToken, but some info seems to be missing. (Might still be working.)" + err.Error()) return } - fmt.Fprintf(w, HTML_OK, claims.Picture, claims.Name) + tmpl, err := ParseFiles("html_ok.tmpl") + if err != nil { + log.Fatal(err) + } + tmpl.Execute(w, claims) go closeSrv(token, nil) }) @@ -233,9 +224,9 @@ func (cfg *OIDC) GetToken() (string, time.Time) { go cfg.Authenticate(tokenCh) token = <-tokenCh if strings.Count(token, ".") < 3 { - fmt.Fprintf(os.Stderr, "Failed to aquire vaild credentials (waiting 20s to allow you to read error message in browser)") - time.Sleep(20 * time.Second) // Allow user to read error message in browser - os.Exit(1) + log.Println("Failed to aquire vaild credentials") + time.Sleep(1 * time.Second) // Allow user to read error message in browser + os.Exit(1) } parts = strings.Split(token, ".") } @@ -265,13 +256,54 @@ func ExecCredential(tk string, expire time.Time) *execCredential { } } +func changeToTemplateDirectory() { + var err error + templateDir := make([]string, 0, 5) + + templateDirectoryFromEnv := os.Getenv("KUBEKEY_TEMPLATEDIR") + if templateDirectoryFromEnv != "" { + templateDir = append(templateDir, templateDirectoryFromEnv) + } + + templateDir = append(templateDir, "/etc/kubekey") + templateDir = append(templateDir, "/usr/local/share/kubekey") + templateDir = append(templateDir, "/usr/share/kubekey") + templateDir = append(templateDir, "templates") + + // Try to change to directories in the order defined above + // Return from this function on success + for len(templateDir) > 0 { + tryDirectory := templateDir[0] + templateDir = templateDir[1:] // remove the first index from array + err = os.Chdir(tryDirectory) + if err == nil { + useEmbeddedTemplates = false + return + } else if tryDirectory == templateDirectoryFromEnv { + log.Println("Environment variable KUBEKEY_TEMPLATEDIR is set, but couldn't change to that directory") + log.Fatal(err) + } + } + + useEmbeddedTemplates = true + return +} + func main() { - getVersionPtr := flag.Bool("v", false, "version") - flag.Parse() - if *getVersionPtr { - fmt.Printf("kubekey v%s\n", Version) - os.Exit(0) - } + getVersionPtr := flag.Bool("v", false, "version") + flag.Parse() + if *getVersionPtr { + fmt.Printf("kubekey v%s\n", Version) + os.Exit(0) + } + + changeToTemplateDirectory() + log.Println(os.Getwd()) + if useEmbeddedTemplates { + log.Println("Use embedded templates") + } else { + log.Println("Read template files from directory") + } oidc := &OIDC{ ClientID: os.Getenv("CLIENT_ID"), diff --git a/templates/html_fail.tmpl b/templates/html_fail.tmpl new file mode 100644 index 0000000..b36b457 --- /dev/null +++ b/templates/html_fail.tmpl @@ -0,0 +1,13 @@ + + + + +{{ .Msg }}
+ + + diff --git a/templates/html_ok.tmpl b/templates/html_ok.tmpl new file mode 100644 index 0000000..28f4727 --- /dev/null +++ b/templates/html_ok.tmpl @@ -0,0 +1,13 @@ + + + + +Please close this window and return to kubectl
+ +