Compare commits

..

18 Commits
1.6 ... 1.8

Author SHA1 Message Date
Hidetake Iwata
4b08a49a51 Add workaround for Go Modules issue 2018-10-31 14:21:33 +09:00
Hidetake Iwata
9c74f3748b Merge pull request #23 from int128/oauth2cli
Import github.com/int128/oauth2cli
2018-10-31 14:16:25 +09:00
Hidetake Iwata
17e03f2abc Import github.com/int128/oauth2cli 2018-10-31 14:13:09 +09:00
Hidetake Iwata
ebef81f9d7 Merge pull request #22 from int128/ignore-cert-errors
Skip invalid CA certs instead of raising error
2018-10-30 09:56:22 +09:00
Hidetake Iwata
8d0d82fb71 Skip invalid CA certs instead of raising error 2018-10-30 09:55:12 +09:00
Hidetake Iwata
b600e54a12 Refactor e2e test 2018-10-29 14:04:10 +09:00
Hidetake Iwata
75317f88a1 Merge pull request #21 from int128/go111
Move to Go Modules
2018-10-29 13:50:01 +09:00
Hidetake Iwata
5a794e8ceb Move to Go Modules 2018-10-29 13:45:32 +09:00
Hidetake Iwata
1fe1ec4c20 Update README.md 2018-10-16 09:47:50 +09:00
Hidetake Iwata
7676ffbfab Fix golint 2018-10-16 09:25:38 +09:00
Hidetake Iwata
7e1e6a096b Refactor docs 2018-10-15 15:52:52 +09:00
Hidetake Iwata
4d3d1c3b78 Add explanation about extra scopes 2018-10-04 16:05:36 +09:00
Hidetake Iwata
1ebdfc0e4f Use "Application Type: Other" on Google IdP 2018-10-03 15:33:56 +09:00
Hidetake Iwata
9c67c52b34 Fix test for #17 2018-09-14 15:06:05 +09:00
Hidetake Iwata
550396e1dd Merge pull request #17 from stang/allow-to-change-listening-port
Allow to change listening port
2018-09-14 11:20:08 +09:00
Stephane Tang
34f0578b59 Allow to change listening port
It's using port 8000 by default, which is identical as the original behavior.

Signed-off-by: Stephane Tang <hi@stang.sh>
2018-09-14 00:03:18 +01:00
Hidetake Iwata
604d118b68 Update README.md 2018-09-07 10:56:26 +09:00
Hidetake Iwata
91959e8a56 Update README.md 2018-09-07 10:56:21 +09:00
23 changed files with 480 additions and 395 deletions

View File

@@ -2,24 +2,26 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/int128/kubelogin
- image: circleci/golang:1.11.1
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/golang/lint/golint
- run: go get -u golang.org/x/lint/golint
- run: golint
# Workaround for https://github.com/golang/go/issues/27925
- run: go get -v k8s.io/client-go
- run: go vet
- run: go build -v
- run: make -C e2e/authserver/testdata
- run: go test -v ./...
- run: make -C cli_test/authserver/testdata
- run: go test -v -race ./...
release:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/int128/kubelogin
- image: circleci/golang:1.11.1
steps:
- checkout
- run: go get -v -t -d ./...
# Workaround for https://github.com/golang/go/issues/27925
- run: go get -v k8s.io/client-go
- run: go vet
- run: curl -sL https://git.io/goreleaser | bash
workflows:

258
README.md
View File

@@ -3,193 +3,56 @@
This is a command for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
It gets a token from the OIDC provider and writes it to the kubeconfig.
This may work with various OIDC providers such as Keycloak, Google Identity Platform and Azure AD.
## TL;DR
You need to setup the OIDC provider and [Kubernetes OIDC authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
You need to setup the following components:
After initial setup or when the token has been expired, just run `kubelogin`:
- OIDC provider
- Kubernetes API server
- Role for your group or user
- kubectl authentication
You can install this by brew tap or from the [releases](https://github.com/int128/kubelogin/releases).
```sh
brew tap int128/kubelogin
brew install kubelogin
```
After initial setup or when the token has been expired, just run `kubelogin`.
```
% kubelogin
2018/08/27 15:03:06 Reading /home/user/.kube/config
2018/08/27 15:03:06 Using current context: hello.k8s.local
2018/08/27 15:03:07 Open http://localhost:8000 for authorization
```
It opens the browser and you can log in to the provider.
After you logged in to the provider, it closes the browser automatically.
Then it writes the ID token and refresh token to the kubeconfig.
```
2018/08/27 15:03:07 GET /
2018/08/27 15:03:08 GET /?state=a51081925f20c043&session_state=5637cbdf-ffdc-4fab-9fc7-68a3e6f2e73f&code=ey...
2018/08/27 15:03:09 Got token for subject=cf228a73-47fe-4986-a2a8-b2ced80a884b
2018/08/27 15:03:09 Updated /home/user/.kube/config
```
Please see the later section for details.
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
For more, see the following documents:
## Getting Started with Google Account
### 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client as follows:
- Application Type: Web application
- Redirect URL: `http://localhost:8000/`
### 2. Setup Kubernetes cluster
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://accounts.google.com
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
```
Here assign the `cluster-admin` role to your user.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: https://accounts.google.com#1234567890
```
### 3. Setup kubectl and kubelogin
Setup `kubectl` to authenticate with your identity provider.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
Run `kubelogin` and open http://localhost:8000 in your browser.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```
## Getting Started with Keycloak
### 1. Setup Keycloak
Create an OIDC client as follows:
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
- Client ID: `kubernetes`
- Groups claim: `groups`
Then create a group `kubernetes:admin` and join to it.
### 2. Setup Kubernetes cluster
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
oidcClientID: kubernetes
oidcGroupsClaim: groups
```
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: /kubernetes:admin
```
### 3. Setup kubectl and kubelogin
Setup `kubectl` to authenticate with your identity provider.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
Run `kubelogin` and make sure you can access to the cluster.
See the previous section for details.
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
## Configuration
This supports the following options.
```
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
@@ -198,8 +61,8 @@ Help Options:
-h, --help Show this help message
```
This supports the following keys of `auth-provider` in kubeconfig.
See also [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
This also supports the following keys of `auth-provider` in kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
Key | Direction | Value
----|-----------|------
@@ -222,62 +85,37 @@ Default to `~/.kube/config`.
export KUBECONFIG="$PWD/.kubeconfig"
```
### Team onboarding
You can share the kubeconfig to your team members for easy setup.
### Extra scopes
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
auth-provider:
name: oidc
config:
client-id: YOUR_CLIEND_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: YOUR_ISSUER
```
If you are using kops, export the kubeconfig and edit it.
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
vim .kubeconfig
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
Note that kubectl does not accept multiple scopes and you need to edit the kubeconfig as like:
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
## CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by `idp-certificate-authority` and `idp-certificate-authority-data` in the kubeconfig.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```
If kubelogin could not parse the certificate, it shows a warning and skips it.
## Contributions
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests.
### Build and Test
```sh
go get github.com/int128/kubelogin
```
```sh
cd $GOPATH/src/github.com/int128/kubelogin
make -C e2e/authserver/testdata
go test -v ./...
```
### Release
CircleCI publishes the build to GitHub.
See [.circleci/config.yml](.circleci/config.yml).
Feel free to open issues and pull requests for improving code and documents.

View File

@@ -1,110 +0,0 @@
package auth
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/pkg/browser"
"golang.org/x/oauth2"
)
type authCodeFlow struct {
Config *oauth2.Config
AuthCodeOptions []oauth2.AuthCodeOption
ServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
func (f *authCodeFlow) getToken(ctx context.Context) (*oauth2.Token, error) {
code, err := f.getAuthCode(ctx)
if err != nil {
return nil, fmt.Errorf("Could not get an auth code: %s", err)
}
token, err := f.Config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("Could not exchange token: %s", err)
}
return token, nil
}
func (f *authCodeFlow) getAuthCode(ctx context.Context) (string, error) {
state, err := generateState()
if err != nil {
return "", fmt.Errorf("Could not generate state parameter: %s", err)
}
codeCh := make(chan string)
defer close(codeCh)
errCh := make(chan error)
defer close(errCh)
server := http.Server{
Addr: fmt.Sprintf("localhost:%d", f.ServerPort),
Handler: &authCodeHandler{
authCodeURL: f.Config.AuthCodeURL(state, f.AuthCodeOptions...),
gotCode: func(code string, gotState string) {
if gotState == state {
codeCh <- code
} else {
errCh <- fmt.Errorf("State does not match, wants %s but %s", state, gotState)
}
},
gotError: func(err error) {
errCh <- err
},
},
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
go func() {
log.Printf("Open http://localhost:%d for authorization", f.ServerPort)
if !f.SkipOpenBrowser {
time.Sleep(500 * time.Millisecond)
browser.OpenURL(fmt.Sprintf("http://localhost:%d/", f.ServerPort))
}
}()
select {
case err := <-errCh:
server.Shutdown(ctx)
return "", err
case code := <-codeCh:
server.Shutdown(ctx)
return code, nil
case <-ctx.Done():
server.Shutdown(ctx)
return "", ctx.Err()
}
}
type authCodeHandler struct {
authCodeURL string
gotCode func(code string, state string)
gotError func(err error)
}
func (h *authCodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.RequestURI)
m := r.Method
p := r.URL.Path
q := r.URL.Query()
switch {
case m == "GET" && p == "/" && q.Get("error") != "":
h.gotError(fmt.Errorf("OAuth Error: %s %s", q.Get("error"), q.Get("error_description")))
http.Error(w, "OAuth Error", 500)
case m == "GET" && p == "/" && q.Get("code") != "":
h.gotCode(q.Get("code"), q.Get("state"))
w.Header().Add("Content-Type", "text/html")
fmt.Fprintf(w, `<html><body>OK<script>window.close()</script></body></html>`)
case m == "GET" && p == "/":
http.Redirect(w, r, h.authCodeURL, 302)
default:
http.Error(w, "Not Found", 404)
}
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
oidc "github.com/coreos/go-oidc"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
)
@@ -23,7 +24,7 @@ type Config struct {
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
ServerPort int // HTTP server port
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
@@ -36,20 +37,18 @@ func (c *Config) GetTokenSet(ctx context.Context) (*TokenSet, error) {
if err != nil {
return nil, fmt.Errorf("Could not discovery the OIDC issuer: %s", err)
}
oauth2Config := &oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
RedirectURL: fmt.Sprintf("http://localhost:%d/", c.ServerPort),
}
flow := &authCodeFlow{
ServerPort: c.ServerPort,
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: c.LocalServerPort,
SkipOpenBrowser: c.SkipOpenBrowser,
Config: oauth2Config,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
}
token, err := flow.getToken(ctx)
token, err := flow.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("Could not get a token: %s", err)
}

View File

@@ -1,15 +0,0 @@
package auth
import (
"crypto/rand"
"encoding/binary"
"fmt"
)
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
}

View File

@@ -32,6 +32,7 @@ func Parse(osArgs []string, version string) (*CLI, error) {
// CLI represents an interface of this command.
type CLI struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
}
@@ -68,17 +69,14 @@ func (c *CLI) Run(ctx context.Context) error {
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`,
err, cfg.CurrentContext)
}
tlsConfig, err := c.tlsConfig(authProvider)
if err != nil {
return fmt.Errorf("Could not configure TLS: %s", err)
}
tlsConfig := c.tlsConfig(authProvider)
authConfig := &auth.Config{
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}},
ServerPort: 8000,
LocalServerPort: c.ListenPort,
SkipOpenBrowser: c.SkipOpenBrowser,
}
token, err := authConfig.GetTokenSet(ctx)

View File

@@ -11,32 +11,47 @@ import (
"github.com/int128/kubelogin/kubeconfig"
)
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) (*tls.Config, error) {
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) *tls.Config {
p := x509.NewCertPool()
if ca := authProvider.IDPCertificateAuthority(); ca != "" {
b, err := ioutil.ReadFile(ca)
if err != nil {
return nil, fmt.Errorf("Could not read %s: %s", ca, err)
if err := appendCertFile(p, ca); err != nil {
log.Printf("Skip CA certificate of idp-certificate-authority: %s", err)
} else {
log.Printf("Using CA certificate: %s", ca)
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not append CA certificate from %s", ca)
}
log.Printf("Using CA certificate: %s", ca)
}
if ca := authProvider.IDPCertificateAuthorityData(); ca != "" {
b, err := base64.StdEncoding.DecodeString(ca)
if err != nil {
return nil, fmt.Errorf("Could not decode idp-certificate-authority-data: %s", err)
if err := appendCertData(p, ca); err != nil {
log.Printf("Skip CA certificate of idp-certificate-authority-data: %s", err)
} else {
log.Printf("Using CA certificate of idp-certificate-authority-data")
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not append CA certificate from idp-certificate-authority-data")
}
log.Printf("Using CA certificate: idp-certificate-authority-data")
}
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}
if len(p.Subjects()) > 0 {
cfg.RootCAs = p
}
return cfg, nil
return cfg
}
func appendCertFile(p *x509.CertPool, name string) error {
b, err := ioutil.ReadFile(name)
if err != nil {
return fmt.Errorf("Could not read %s: %s", name, err)
}
if p.AppendCertsFromPEM(b) != true {
return fmt.Errorf("Could not append certificate from %s", name)
}
return nil
}
func appendCertData(p *x509.CertPool, data string) error {
b, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return fmt.Errorf("Could not decode base64: %s", err)
}
if p.AppendCertsFromPEM(b) != true {
return fmt.Errorf("Could not append certificate")
}
return nil
}

View File

@@ -1,4 +1,4 @@
package e2e
package cli_test
import (
"context"
@@ -13,7 +13,8 @@ import (
"time"
"github.com/int128/kubelogin/cli"
"github.com/int128/kubelogin/e2e/authserver"
"github.com/int128/kubelogin/cli_test/authserver"
"github.com/int128/kubelogin/cli_test/kubeconfig"
"golang.org/x/sync/errgroup"
)
@@ -26,19 +27,19 @@ import (
// 5. Shutdown the auth server.
func TestE2E(t *testing.T) {
data := map[string]struct {
kubeconfigValues kubeconfigValues
kubeconfigValues kubeconfig.Values
cli cli.CLI
serverConfig authserver.Config
clientTLS *tls.Config
}{
"NoTLS": {
kubeconfigValues{Issuer: "http://localhost:9000"},
kubeconfig.Values{Issuer: "http://localhost:9000"},
cli.CLI{},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
"ExtraScope": {
kubeconfigValues{
kubeconfig.Values{
Issuer: "http://localhost:9000",
ExtraScopes: "profile groups",
},
@@ -50,7 +51,7 @@ func TestE2E(t *testing.T) {
&tls.Config{},
},
"SkipTLSVerify": {
kubeconfigValues{Issuer: "https://localhost:9000"},
kubeconfig.Values{Issuer: "https://localhost:9000"},
cli.CLI{SkipTLSVerify: true},
authserver.Config{
Issuer: "https://localhost:9000",
@@ -60,7 +61,7 @@ func TestE2E(t *testing.T) {
&tls.Config{InsecureSkipVerify: true},
},
"CACert": {
kubeconfigValues{
kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: authserver.CACert,
},
@@ -73,7 +74,7 @@ func TestE2E(t *testing.T) {
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
},
"CACertData": {
kubeconfigValues{
kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)),
},
@@ -85,6 +86,24 @@ func TestE2E(t *testing.T) {
},
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
},
"InvalidCACertShouldBeSkipped": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
IDPCertificateAuthority: "e2e_test.go",
},
cli.CLI{},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
"InvalidCACertDataShouldBeSkipped": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")),
},
cli.CLI{},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
}
for name, c := range data {
@@ -93,10 +112,11 @@ func TestE2E(t *testing.T) {
defer cancel()
server := c.serverConfig.Start(t)
defer server.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &c.kubeconfigValues)
defer os.Remove(kubeconfig)
c.cli.KubeConfig = kubeconfig
kcfg := kubeconfig.Create(t, &c.kubeconfigValues)
defer os.Remove(kcfg)
c.cli.KubeConfig = kcfg
c.cli.SkipOpenBrowser = true
c.cli.ListenPort = 8000
var eg errgroup.Group
eg.Go(func() error {
@@ -109,7 +129,7 @@ func TestE2E(t *testing.T) {
if err := eg.Wait(); err != nil {
t.Fatalf("CLI returned error: %s", err)
}
verifyKubeconfig(t, kubeconfig)
kubeconfig.Verify(t, kcfg)
})
}
}

View File

@@ -1,4 +1,4 @@
package e2e
package kubeconfig
import (
"html/template"
@@ -7,21 +7,23 @@ import (
"testing"
)
type kubeconfigValues struct {
// Values represents values in .kubeconfig template.
type Values struct {
Issuer string
ExtraScopes string
IDPCertificateAuthority string
IDPCertificateAuthorityData string
}
func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
// Create creates a kubeconfig file and returns path to it.
func Create(t *testing.T, v *Values) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tpl, err := template.ParseFiles("testdata/kubeconfig.yaml")
tpl, err := template.ParseFiles("kubeconfig/testdata/kubeconfig.yaml")
if err != nil {
t.Fatal(err)
}
@@ -31,7 +33,8 @@ func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
return f.Name()
}
func verifyKubeconfig(t *testing.T, kubeconfig string) {
// Verify returns true if the kubeconfig has valid values.
func Verify(t *testing.T, kubeconfig string) {
b, err := ioutil.ReadFile(kubeconfig)
if err != nil {
t.Fatal(err)

100
docs/google.md Normal file
View File

@@ -0,0 +1,100 @@
# Getting Started with Google Identity Platform
## Prerequisite
- You have a Google account.
- You have the Cluster Admin role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
## 2. Setup Kubernetes API server
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
### kops
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://accounts.google.com
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to you.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: https://accounts.google.com#1234567890
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

107
docs/keycloak.md Normal file
View File

@@ -0,0 +1,107 @@
# Getting Started with Keycloak
## Prerequisite
- You have administrator access to the Keycloak.
- You have the Cluster Admin role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Keycloak
Open the Keycloak and create an OIDC client as follows:
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
- Client ID: `kubernetes`
- Groups claim: `groups`
Create a group `kubernetes:admin` and join to it.
This is used for group based access control.
## 2. Setup Kubernetes API server
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
### kops
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
oidcClientID: kubernetes
oidcGroupsClaim: groups
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: /kubernetes:admin
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://keycloak.example.com/auth/realms/YOUR_REALM
client-id: kubernetes
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

40
docs/team_ops.md Normal file
View File

@@ -0,0 +1,40 @@
# Team Operation
## kops
Export the kubeconfig.
```sh
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
```
Remove the `admin` access from the kubeconfig.
It should look as like:
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
auth-provider:
name: oidc
config:
client-id: YOUR_CLIEND_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: YOUR_ISSUER
```
You can share the kubeconfig to your team members for easy onboarding.

30
go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/int128/kubelogin
require (
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/int128/oauth2cli v1.0.0
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.5 // indirect
github.com/mitchellh/go-homedir v1.0.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/pflag v1.0.3 // indirect
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 // indirect
golang.org/x/text v0.3.0 // indirect
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.1.9 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd // indirect
k8s.io/client-go v9.0.0+incompatible
)

58
go.sum Normal file
View File

@@ -0,0 +1,58 @@
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/int128/oauth2cli v1.0.0 h1:bQcKSgS7lBVIhEJ1IE689oGW6AmTPrh/mQkxqGxX2Z0=
github.com/int128/oauth2cli v1.0.0/go.mod h1:ClkmeKFkDlkVtqncv98+V4Gny/luvARCIoK/+3KlKb8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107 h1:63fpDttzclb8owmRoxSaFNbnT1CG25L0Yvnhh9lU1SE=
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 h1:biUuj9O+0+XckRUCDzjoOGm6yFV5c0IHbm1ODP3e4Zw=
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/square/go-jose.v2 v2.1.9 h1:YCFbL5T2gbmC2sMG12s1x2PAlTK5TZNte3hjZEIcCAg=
gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd h1:rzRDbWmTXaWr6SDV9caWzwAUPSfvgwX6j89LBnq/ycw=
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v9.0.0+incompatible h1:NXWpDuPFeVB5lYP1fTqJUtwigjtmRXJNtndnN53ldGI=
k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=