Skip to content

Commit

Permalink
Add documentation, make templates for styling responses
Browse files Browse the repository at this point in the history
  • Loading branch information
cskarby committed Jan 10, 2025
1 parent 61e7b52 commit cce2d51
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 57 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/build-release-binaries.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 ./...
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 45 additions & 0 deletions example/.kube/config
Original file line number Diff line number Diff line change
@@ -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
136 changes: 84 additions & 52 deletions kubekey.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,11 +23,12 @@ import (
"context"
"crypto/rand"
"crypto/sha256"
"embed"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"html"
"html/template"
"log"
"net"
"net/http"
Expand All @@ -43,41 +44,22 @@ import (
"golang.org/x/oauth2"
)

const Version = "1.0.0"
const HTML_OK string = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OK</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://parkert.met.no/.parkert/parkert.css">
</head>
<body>
<img src="%s" alt="" style="float: left; margin-right: 2em;">
<h1>You're identified as %s</h1>
<p>Please close this window and return to kubectl</p>
<div><a href="https://www.met.no/"><img src="https://parkert.met.no/.parkert/met.png" alt="Meteorologisk institutt" class="logo"></a></div>
<div class="photoby"></div>
</body>
</html>
`
const HTML_FAIL string = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://parkert.met.no/.parkert/parkert.css">
</head>
<body style="background-color: rgba(162, 78, 117, 0.8)">
<h1>Authentication error</h1>
<p>%s</p>
<div><a href="https://www.met.no/"><img src="https://parkert.met.no/.parkert/met.png" alt="Meteorologisk institutt" class="logo"></a></div>
<div class="photoby"></div>
</body>
</script>
</html>
`
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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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"`
Expand All @@ -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)
})

Expand Down Expand Up @@ -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, ".")
}
Expand Down Expand Up @@ -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"),
Expand Down
13 changes: 13 additions & 0 deletions templates/html_fail.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Authentication error</h1>
<p>{{ .Msg }}</p>
</body>
</script>
</html>
13 changes: 13 additions & 0 deletions templates/html_ok.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OK</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<img src="{{ .Picture }}" alt="An image of you">
<h1>You're identified as {{ .Name }}</h1>
<p>Please close this window and return to kubectl</p>
</body>
</html>

0 comments on commit cce2d51

Please sign in to comment.