mirror of
https://github.com/int128/kubelogin.git
synced 2026-05-22 15:52:46 +00:00
Initial commit
This commit is contained in:
36
.circleci/config.yml
Normal file
36
.circleci/config.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/int128/kubelogin
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d
|
||||
- run: go get github.com/golang/lint/golint
|
||||
- run: golint
|
||||
- run: go build -v
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/int128/kubelogin
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d
|
||||
- run: go get github.com/mitchellh/gox
|
||||
- run: go get github.com/tcnksm/ghr
|
||||
- run: gox --osarch 'darwin/amd64 linux/amd64 windows/amd64' -output 'dist/{{.Dir}}_{{.OS}}_{{.Arch}}'
|
||||
- run: ghr -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" "$CIRCLE_TAG" dist
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build
|
||||
- release:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /.*/
|
||||
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/kubelogin
|
||||
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)
|
||||
|
||||
`kubelogin` is a command tool to setup authentication for `kubectl`.
|
||||
|
||||
Currently OpenID Connect (OIDC) is supported.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Setup OIDC Identity Provider
|
||||
|
||||
This article assumes you have created an OIDC client with the following:
|
||||
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/hello`
|
||||
- Redirect URL: `https://kubernetes-dashboard.example.com/*`
|
||||
- Client ID: `kubernetes`
|
||||
- Client Secret: `YOUR_CLIENT_SECRET`
|
||||
- Groups claim: `groups` (optional for group based access controll)
|
||||
|
||||
### 2. Setup Kubernetes API Server
|
||||
|
||||
Setup the Kubernetes API server allows your identity provider.
|
||||
|
||||
If you are using kops, `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcClientID: kubernetes
|
||||
oidcGroupsClaim: groups
|
||||
oidcIssuerURL: https://keycloak.example.com/auth/realms/hello
|
||||
```
|
||||
|
||||
### 3. Setup kubectl
|
||||
|
||||
```sh
|
||||
CLUSTER_NAME=hello.k8s.local
|
||||
|
||||
kubectl config set-cluster $CLUSTER_NAME \
|
||||
--server https://api.example.com \
|
||||
--certificate-authority ~/.kube/$CLUSTER_NAME.crt
|
||||
|
||||
kubectl config set-credentials $CLUSTER_NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
|
||||
--auth-provider-arg client-id=kubernetes \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
|
||||
kubectl config set-context $CLUSTER_NAME --cluster $CLUSTER_NAME --user $CLUSTER_NAME
|
||||
```
|
||||
|
||||
### 4. Use kubelogin and kubectl
|
||||
|
||||
Refresh the ID token:
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/03/21 17:13:20 Reading config from ~/.kube/config
|
||||
---- Authentication ----
|
||||
1. Open the following URL:
|
||||
|
||||
https://keycloak.example.com/auth/realms/hello/protocol/openid-connect/auth?client_id=...
|
||||
|
||||
2. Enter the code: ey...
|
||||
|
||||
2018/03/21 17:13:32 Updated ~/.kube/config
|
||||
```
|
||||
|
||||
Make sure you can access to the cluster:
|
||||
|
||||
```
|
||||
% kubectl version
|
||||
Client Version: version.Info{...}
|
||||
Server Version: version.Info{...}
|
||||
```
|
||||
46
config.go
Normal file
46
config.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// GetCurrentAuthInfo returns the current authInfo
|
||||
func GetCurrentAuthInfo(config api.Config) *api.AuthInfo {
|
||||
context := config.Contexts[config.CurrentContext]
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
authInfo := config.AuthInfos[context.AuthInfo]
|
||||
return authInfo
|
||||
}
|
||||
|
||||
// ReadKubeConfig returns the current config
|
||||
func ReadKubeConfig(path string) (*api.Config, error) {
|
||||
config, err := clientcmd.LoadFromFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// WriteKubeConfig writes the config
|
||||
func WriteKubeConfig(config api.Config, path string) error {
|
||||
return clientcmd.WriteToFile(config, path)
|
||||
}
|
||||
|
||||
// FindKubeConfig returns env:KUBECONFIG or ~/.kube/config
|
||||
func FindKubeConfig() (string, error) {
|
||||
env := os.Getenv("KUBECONFIG")
|
||||
if env != "" {
|
||||
return env, nil
|
||||
}
|
||||
path, err := homedir.Expand("~/.kube/config")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
54
main.go
Normal file
54
main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
kubeConfigPath, err := FindKubeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Reading config from %s", kubeConfigPath)
|
||||
kubeConfig, err := ReadKubeConfig(kubeConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
authInfo := GetCurrentAuthInfo(*kubeConfig)
|
||||
if authInfo == nil {
|
||||
log.Fatal("Could not find the current user")
|
||||
}
|
||||
authProvider := authInfo.AuthProvider
|
||||
if authInfo == nil {
|
||||
log.Fatal("auth-provider is not set in the config")
|
||||
}
|
||||
|
||||
switch authProvider.Name {
|
||||
case "oidc":
|
||||
if err := mutateConfigWithOIDC(authProvider); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Updating %s", kubeConfigPath)
|
||||
WriteKubeConfig(*kubeConfig, kubeConfigPath)
|
||||
|
||||
default:
|
||||
log.Fatalf("Currently auth-provider `%s` is not supported", authProvider.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func mutateConfigWithOIDC(authProvider *api.AuthProviderConfig) error {
|
||||
issuer := authProvider.Config["idp-issuer-url"]
|
||||
clientID := authProvider.Config["client-id"]
|
||||
clientSecret := authProvider.Config["client-secret"]
|
||||
oidcToken, err := GetOIDCTokenByAuthCode(issuer, clientID, clientSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authProvider.Config["id-token"] = oidcToken.IDToken
|
||||
authProvider.Config["refresh-token"] = oidcToken.RefreshToken
|
||||
return nil
|
||||
}
|
||||
91
oidc.go
Normal file
91
oidc.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// OIDCToken is a token set
|
||||
type OIDCToken struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenClaim *IDTokenClaim
|
||||
}
|
||||
|
||||
// IDTokenClaim represents an ID token decoded
|
||||
type IDTokenClaim struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// GetOIDCTokenByAuthCode returns a token retrieved by auth code grant
|
||||
func GetOIDCTokenByAuthCode(issuer string, clientID string, clientSecret string) (*OIDCToken, error) {
|
||||
provider, err := oidc.NewProvider(oauth2.NoContext, issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "email"},
|
||||
}
|
||||
|
||||
state, err := generateState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authCodeURL := config.AuthCodeURL(state)
|
||||
fmt.Printf(`---- Authentication ----
|
||||
1. Open the following URL:
|
||||
|
||||
%s
|
||||
|
||||
2. Enter the code: `, authCodeURL)
|
||||
var code string
|
||||
if _, err := fmt.Scanln(&code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
token, err := config.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: config.ClientID})
|
||||
idToken, err := verifier.Verify(oauth2.NoContext, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idTokenClaim := IDTokenClaim{}
|
||||
if err := idToken.Claims(&idTokenClaim); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &OIDCToken{
|
||||
IDToken: rawIDToken,
|
||||
IDTokenClaim: &idTokenClaim,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateState() (string, error) {
|
||||
var n uint64
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", n), nil
|
||||
}
|
||||
Reference in New Issue
Block a user