Initial commit

This commit is contained in:
Hidetake Iwata
2018-03-21 10:14:07 +09:00
commit 014f102748
7 changed files with 312 additions and 0 deletions

36
.circleci/config.yml Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
/kubelogin

74
README.md Normal file
View File

@@ -0,0 +1,74 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](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
View 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
View 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
View 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
}