mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-19 19:09:50 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b72cb63826 | ||
|
|
63fda1db0f | ||
|
|
da95fe470f | ||
|
|
4b08a49a51 | ||
|
|
9c74f3748b | ||
|
|
17e03f2abc | ||
|
|
ebef81f9d7 | ||
|
|
8d0d82fb71 | ||
|
|
b600e54a12 | ||
|
|
75317f88a1 | ||
|
|
5a794e8ceb | ||
|
|
1fe1ec4c20 | ||
|
|
7676ffbfab | ||
|
|
7e1e6a096b | ||
|
|
4d3d1c3b78 | ||
|
|
1ebdfc0e4f | ||
|
|
9c67c52b34 | ||
|
|
550396e1dd | ||
|
|
34f0578b59 | ||
|
|
604d118b68 | ||
|
|
91959e8a56 | ||
|
|
9b325a66a9 | ||
|
|
8b6257d60b | ||
|
|
d469df4978 | ||
|
|
3ae68df848 | ||
|
|
e8805f7a94 | ||
|
|
717da9d442 | ||
|
|
de176cfbaa | ||
|
|
9bf8a89577 | ||
|
|
a91c020f46 | ||
|
|
d4fb49613d | ||
|
|
64b1d52208 | ||
|
|
a298058e3f |
@@ -2,37 +2,41 @@ 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/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 ./...
|
||||
- run: go get github.com/mitchellh/gox
|
||||
- run: go get github.com/tcnksm/ghr
|
||||
- run: gox --osarch 'darwin/amd64 linux/amd64 windows/amd64 windows/386' -output 'dist/{{.Dir}}_{{.OS}}_{{.Arch}}'
|
||||
- run: ghr -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" "$CIRCLE_TAG" dist
|
||||
# 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:
|
||||
version: 2
|
||||
build:
|
||||
all:
|
||||
jobs:
|
||||
- build
|
||||
- build:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- release:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- build
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
/kubelogin
|
||||
/dist
|
||||
/.kubeconfig
|
||||
|
||||
23
.goreleaser.yml
Normal file
23
.goreleaser.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
builds:
|
||||
- binary: kubelogin
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
archive:
|
||||
files:
|
||||
- none*
|
||||
brew:
|
||||
github:
|
||||
owner: int128
|
||||
name: homebrew-kubelogin
|
||||
homepage: https://github.com/int128/kubelogin
|
||||
description: "kubectl with OpenID Connect (OIDC) authentication"
|
||||
test: |
|
||||
system "#{bin}/kubelogin --help"
|
||||
system "#{bin}/kubectl-login --help"
|
||||
install: |
|
||||
bin.install "kubelogin"
|
||||
ln_s bin/"kubelogin", bin/"kubectl-login"
|
||||
260
README.md
260
README.md
@@ -1,195 +1,58 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)
|
||||
|
||||
This is a helper command for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
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 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` or `kubectl login`.
|
||||
|
||||
```
|
||||
% 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 CLUSTER_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 CLUSTER_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 `auth-provider` keys 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
|
||||
----|-----------|------
|
||||
@@ -208,6 +71,7 @@ Key | Direction | Value
|
||||
`client-secret` | IN (Required) | Client Secret of the provider.
|
||||
`idp-certificate-authority` | IN (Optional) | CA certificate path of the provider.
|
||||
`idp-certificate-authority-data` | IN (Optional) | Base64 encoded CA certificate of the provider.
|
||||
`extra-scopes` | IN (Optional) | Scopes to request to the provider (comma separated).
|
||||
`id-token` | OUT | ID token got from the provider.
|
||||
`refresh-token` | OUT | Refresh token got from the provider.
|
||||
|
||||
@@ -221,61 +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/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.
|
||||
|
||||
109
auth/authcode.go
109
auth/authcode.go
@@ -1,109 +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
|
||||
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),
|
||||
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)
|
||||
}
|
||||
}
|
||||
24
auth/oidc.go
24
auth/oidc.go
@@ -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,19 +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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
43
cli/cli.go
43
cli/cli.go
@@ -13,10 +13,13 @@ import (
|
||||
)
|
||||
|
||||
// Parse parses command line arguments and returns a CLI instance.
|
||||
func Parse(args []string) (*CLI, error) {
|
||||
func Parse(osArgs []string, version string) (*CLI, error) {
|
||||
var cli CLI
|
||||
parser := flags.NewParser(&cli, flags.HelpFlag)
|
||||
args, err := parser.Parse()
|
||||
parser.LongDescription = fmt.Sprintf(`Version %s
|
||||
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
|
||||
version)
|
||||
args, err := parser.ParseArgs(osArgs[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,6 +32,7 @@ func Parse(args []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."`
|
||||
}
|
||||
@@ -37,51 +41,52 @@ type CLI struct {
|
||||
func (c *CLI) ExpandKubeConfig() (string, error) {
|
||||
d, err := homedir.Expand(c.KubeConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not expand %s", c.KubeConfig)
|
||||
return "", fmt.Errorf("Could not expand %s: %s", c.KubeConfig, err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Run performs this command.
|
||||
func (c *CLI) Run(ctx context.Context) error {
|
||||
log.Printf("Reading %s", c.KubeConfig)
|
||||
path, err := c.ExpandKubeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Reading %s", path)
|
||||
cfg, err := kubeconfig.Read(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not load kubeconfig: %s", err)
|
||||
return fmt.Errorf("Could not read kubeconfig: %s", err)
|
||||
}
|
||||
log.Printf("Using current context: %s", cfg.CurrentContext)
|
||||
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
|
||||
if authInfo == nil {
|
||||
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
|
||||
}
|
||||
authProvider, err := kubeconfig.FindOIDCAuthProvider(authInfo)
|
||||
log.Printf("Using current-context: %s", cfg.CurrentContext)
|
||||
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not find auth-provider: %s", err)
|
||||
}
|
||||
tlsConfig, err := c.tlsConfig(authProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not configure TLS: %s", err)
|
||||
return fmt.Errorf(`Could not find OIDC configuration in kubeconfig: %s
|
||||
Did you setup kubectl for OIDC authentication?
|
||||
kubectl config set-credentials %s \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
|
||||
--auth-provider-arg client-id=YOUR_CLIENT_ID \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`,
|
||||
err, cfg.CurrentContext)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Authentication error: %s", err)
|
||||
return fmt.Errorf("Could not get token from OIDC provider: %s", err)
|
||||
}
|
||||
|
||||
authProvider.SetIDToken(token.IDToken)
|
||||
authProvider.SetRefreshToken(token.RefreshToken)
|
||||
kubeconfig.Write(cfg, path)
|
||||
log.Printf("Updated %s", path)
|
||||
log.Printf("Updated %s", c.KubeConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
35
cli/cli_test.go
Normal file
35
cli/cli_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
c, err := Parse([]string{"kubelogin"}, "version")
|
||||
if err != nil {
|
||||
t.Errorf("Parse returned error: %s", err)
|
||||
}
|
||||
if c == nil {
|
||||
t.Errorf("Parse should return CLI but nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_TooManyArgs(t *testing.T) {
|
||||
c, err := Parse([]string{"kubelogin", "some"}, "version")
|
||||
if err == nil {
|
||||
t.Errorf("Parse should return error but nil")
|
||||
}
|
||||
if c != nil {
|
||||
t.Errorf("Parse should return nil but %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_Help(t *testing.T) {
|
||||
c, err := Parse([]string{"kubelogin", "--help"}, "version")
|
||||
if err == nil {
|
||||
t.Errorf("Parse should return error but nil")
|
||||
}
|
||||
if c != nil {
|
||||
t.Errorf("Parse should return nil but %+v", c)
|
||||
}
|
||||
}
|
||||
53
cli/tls.go
53
cli/tls.go
@@ -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 authProvider.IDPCertificateAuthority() != "" {
|
||||
b, err := ioutil.ReadFile(authProvider.IDPCertificateAuthority())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read idp-certificate-authority: %s", err)
|
||||
if ca := authProvider.IDPCertificateAuthority(); ca != "" {
|
||||
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 load CA certificate from idp-certificate-authority: %s", err)
|
||||
}
|
||||
log.Printf("Using CA certificate: %s", authProvider.IDPCertificateAuthority())
|
||||
}
|
||||
if authProvider.IDPCertificateAuthorityData() != "" {
|
||||
b, err := base64.StdEncoding.DecodeString(authProvider.IDPCertificateAuthorityData())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not decode idp-certificate-authority-data: %s", err)
|
||||
if ca := authProvider.IDPCertificateAuthorityData(); ca != "" {
|
||||
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 load CA certificate from idp-certificate-authority-data: %s", err)
|
||||
}
|
||||
log.Printf("Using CA certificate of 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
|
||||
}
|
||||
|
||||
49
cli_test/authserver/authserver.go
Normal file
49
cli_test/authserver/authserver.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package authserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Addr is address to listen.
|
||||
const Addr = "localhost:9000"
|
||||
|
||||
// CACert is path to the CA certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const CACert = "authserver/testdata/ca.crt"
|
||||
|
||||
// ServerCert is path to the server certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const ServerCert = "authserver/testdata/server.crt"
|
||||
|
||||
// ServerKey is path to the server key.
|
||||
// This should be generated by Makefile before test.
|
||||
const ServerKey = "authserver/testdata/server.key"
|
||||
|
||||
// Config represents server configuration.
|
||||
type Config struct {
|
||||
Issuer string
|
||||
Scope string
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
|
||||
// Start starts a HTTP server.
|
||||
func (c *Config) Start(t *testing.T) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: Addr,
|
||||
Handler: newHandler(t, c),
|
||||
}
|
||||
go func() {
|
||||
var err error
|
||||
if c.Cert != "" && c.Key != "" {
|
||||
err = s.ListenAndServeTLS(c.Cert, c.Key)
|
||||
} else {
|
||||
err = s.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
@@ -1,45 +1,47 @@
|
||||
package e2e
|
||||
package authserver
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// AuthHandler provides the stub handler for OIDC authentication.
|
||||
type AuthHandler struct {
|
||||
// Values in templates
|
||||
type handler struct {
|
||||
discovery *template.Template
|
||||
token *template.Template
|
||||
jwks *template.Template
|
||||
authCode string
|
||||
|
||||
Issuer string
|
||||
AuthCode string
|
||||
Scope string // Default to openid
|
||||
IDToken string
|
||||
PrivateKey struct{ N, E string }
|
||||
|
||||
// Response templates
|
||||
discoveryJSON *template.Template
|
||||
tokenJSON *template.Template
|
||||
jwksJSON *template.Template
|
||||
}
|
||||
|
||||
// NewAuthHandler returns a new AuthHandler.
|
||||
func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Issuer: issuer,
|
||||
AuthCode: "0b70006b-f62a-4438-aba5-c0b96775d8e5",
|
||||
discoveryJSON: template.Must(template.ParseFiles("testdata/oidc-discovery.json")),
|
||||
tokenJSON: template.Must(template.ParseFiles("testdata/oidc-token.json")),
|
||||
jwksJSON: template.Must(template.ParseFiles("testdata/oidc-jwks.json")),
|
||||
func newHandler(t *testing.T, c *Config) *handler {
|
||||
h := handler{
|
||||
discovery: readTemplate(t, "oidc-discovery.json"),
|
||||
token: readTemplate(t, "oidc-token.json"),
|
||||
jwks: readTemplate(t, "oidc-jwks.json"),
|
||||
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
|
||||
Issuer: c.Issuer,
|
||||
Scope: c.Scope,
|
||||
}
|
||||
if h.Scope == "" {
|
||||
h.Scope = "openid"
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
|
||||
Issuer: h.Issuer,
|
||||
Issuer: c.Issuer,
|
||||
Audience: "kubernetes",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
@@ -53,24 +55,43 @@ func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
|
||||
}
|
||||
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
|
||||
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
|
||||
return h
|
||||
return &h
|
||||
}
|
||||
|
||||
func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
func readTemplate(t *testing.T, name string) *template.Template {
|
||||
t.Helper()
|
||||
tpl, err := template.ParseFiles("authserver/testdata/" + name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read template %s: %s", name, err)
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.serveHTTP(w, r); err != nil {
|
||||
log.Printf("[auth-server] Error: %s", err)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
m := r.Method
|
||||
p := r.URL.Path
|
||||
log.Printf("[auth-server] %s %s", m, r.RequestURI)
|
||||
switch {
|
||||
case m == "GET" && p == "/.well-known/openid-configuration":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := s.discoveryJSON.Execute(w, s); err != nil {
|
||||
if err := h.discovery.Execute(w, h); err != nil {
|
||||
return err
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/auth":
|
||||
// Authentication Response
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
|
||||
q := r.URL.Query()
|
||||
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), s.AuthCode)
|
||||
if h.Scope != q.Get("scope") {
|
||||
return fmt.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
|
||||
}
|
||||
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.authCode)
|
||||
http.Redirect(w, r, to, 302)
|
||||
case m == "POST" && p == "/protocol/openid-connect/token":
|
||||
// Token Response
|
||||
@@ -78,16 +99,16 @@ func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.AuthCode != r.Form.Get("code") {
|
||||
return fmt.Errorf("code wants %s but %s", s.AuthCode, r.Form.Get("code"))
|
||||
if h.authCode != r.Form.Get("code") {
|
||||
return fmt.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := s.tokenJSON.Execute(w, s); err != nil {
|
||||
if err := h.token.Execute(w, h); err != nil {
|
||||
return err
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/certs":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := s.jwksJSON.Execute(w, s); err != nil {
|
||||
if err := h.jwks.Execute(w, h); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
@@ -95,10 +116,3 @@ func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.serveHTTP(w, r); err != nil {
|
||||
log.Printf("[auth-server] Error: %s", err)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,40 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: authserver.crt authserver-ca.crt
|
||||
all: server.crt ca.crt
|
||||
|
||||
clean:
|
||||
rm -v authserver*
|
||||
rm -v ca.* server.*
|
||||
|
||||
authserver-ca.key:
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
authserver-ca.csr: openssl.cnf authserver-ca.key
|
||||
ca.csr: openssl.cnf ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key authserver-ca.key \
|
||||
-key ca.key \
|
||||
-subj "/CN=Hello CA" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
authserver-ca.crt: authserver-ca.csr authserver-ca.key
|
||||
ca.crt: ca.csr ca.key
|
||||
openssl x509 -req \
|
||||
-signkey authserver-ca.key \
|
||||
-in authserver-ca.csr \
|
||||
-signkey ca.key \
|
||||
-in ca.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
authserver.key:
|
||||
server.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
authserver.csr: openssl.cnf authserver.key
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key authserver.key \
|
||||
-key server.key \
|
||||
-subj "/CN=localhost" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
|
||||
server.crt: openssl.cnf server.csr ca.key ca.crt
|
||||
rm -fr ./CA
|
||||
mkdir -p ./CA
|
||||
touch CA/index.txt
|
||||
@@ -43,8 +43,8 @@ authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
|
||||
openssl ca -config openssl.cnf \
|
||||
-extensions v3_req \
|
||||
-batch \
|
||||
-cert authserver-ca.crt \
|
||||
-keyfile authserver-ca.key \
|
||||
-in authserver.csr \
|
||||
-cert ca.crt \
|
||||
-keyfile ca.key \
|
||||
-in server.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
167
cli_test/e2e_test.go
Normal file
167
cli_test/e2e_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/cli"
|
||||
"github.com/int128/kubelogin/cli_test/authserver"
|
||||
"github.com/int128/kubelogin/cli_test/kubeconfig"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// End-to-end test.
|
||||
//
|
||||
// 1. Start the auth server at port 9000.
|
||||
// 2. Run the CLI.
|
||||
// 3. Open a request for port 8000.
|
||||
// 4. Wait for the CLI.
|
||||
// 5. Shutdown the auth server.
|
||||
func TestE2E(t *testing.T) {
|
||||
data := map[string]struct {
|
||||
kubeconfigValues kubeconfig.Values
|
||||
cli cli.CLI
|
||||
serverConfig authserver.Config
|
||||
clientTLS *tls.Config
|
||||
}{
|
||||
"NoTLS": {
|
||||
kubeconfig.Values{Issuer: "http://localhost:9000"},
|
||||
cli.CLI{},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
"ExtraScope": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
ExtraScopes: "profile groups",
|
||||
},
|
||||
cli.CLI{},
|
||||
authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
Scope: "profile groups openid",
|
||||
},
|
||||
&tls.Config{},
|
||||
},
|
||||
"SkipTLSVerify": {
|
||||
kubeconfig.Values{Issuer: "https://localhost:9000"},
|
||||
cli.CLI{SkipTLSVerify: true},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
Cert: authserver.ServerCert,
|
||||
Key: authserver.ServerKey,
|
||||
},
|
||||
&tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
"CACert": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: authserver.CACert,
|
||||
},
|
||||
cli.CLI{},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
Cert: authserver.ServerCert,
|
||||
Key: authserver.ServerKey,
|
||||
},
|
||||
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
|
||||
},
|
||||
"CACertData": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)),
|
||||
},
|
||||
cli.CLI{},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
Cert: authserver.ServerCert,
|
||||
Key: authserver.ServerKey,
|
||||
},
|
||||
&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 {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
server := c.serverConfig.Start(t)
|
||||
defer server.Shutdown(ctx)
|
||||
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 {
|
||||
return c.cli.Run(ctx)
|
||||
})
|
||||
if err := openBrowserRequest(c.clientTLS); err != nil {
|
||||
cancel()
|
||||
t.Error(err)
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
t.Fatalf("CLI returned error: %s", err)
|
||||
}
|
||||
kubeconfig.Verify(t, kcfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func openBrowserRequest(tlsConfig *tls.Config) error {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get("http://localhost:8000/")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not send a request: %s", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("StatusCode wants 200 but %d", res.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func read(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read %s: %s", name, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func readCert(t *testing.T, name string) *x509.CertPool {
|
||||
t.Helper()
|
||||
p := x509.NewCertPool()
|
||||
b := read(t, name)
|
||||
if !p.AppendCertsFromPEM(b) {
|
||||
t.Fatalf("Could not append cert from %s", name)
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package e2e
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
@@ -7,20 +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)
|
||||
}
|
||||
@@ -30,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)
|
||||
@@ -19,6 +19,9 @@ users:
|
||||
client-id: kubernetes
|
||||
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
|
||||
idp-issuer-url: {{ .Issuer }}
|
||||
#{{ if .ExtraScopes }}
|
||||
extra-scopes: {{ .ExtraScopes }}
|
||||
#{{ end }}
|
||||
#{{ if .IDPCertificateAuthority }}
|
||||
idp-certificate-authority: {{ .IDPCertificateAuthority }}
|
||||
#{{ end }}
|
||||
100
docs/google.md
Normal file
100
docs/google.md
Normal 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
107
docs/keycloak.md
Normal 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
40
docs/team_ops.md
Normal 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.
|
||||
145
e2e/e2e_test.go
145
e2e/e2e_test.go
@@ -1,145 +0,0 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/cli"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const tlsCACert = "testdata/authserver-ca.crt"
|
||||
const tlsServerCert = "testdata/authserver.crt"
|
||||
const tlsServerKey = "testdata/authserver.key"
|
||||
|
||||
// End-to-end test.
|
||||
//
|
||||
// 1. Start the auth server at port 9000.
|
||||
// 2. Run the CLI.
|
||||
// 3. Open a request for port 8000.
|
||||
// 4. Wait for the CLI.
|
||||
// 5. Shutdown the auth server.
|
||||
func TestE2E(t *testing.T) {
|
||||
data := map[string]struct {
|
||||
kubeconfigValues kubeconfigValues
|
||||
cli cli.CLI
|
||||
startServer func(*testing.T, http.Handler) *http.Server
|
||||
authClientTLS *tls.Config
|
||||
}{
|
||||
"NoTLS": {
|
||||
kubeconfigValues{Issuer: "http://localhost:9000"},
|
||||
cli.CLI{},
|
||||
startServer,
|
||||
&tls.Config{},
|
||||
},
|
||||
"SkipTLSVerify": {
|
||||
kubeconfigValues{Issuer: "https://localhost:9000"},
|
||||
cli.CLI{SkipTLSVerify: true},
|
||||
startServerTLS,
|
||||
&tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
"CACert": {
|
||||
kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: tlsCACert,
|
||||
},
|
||||
cli.CLI{},
|
||||
startServerTLS,
|
||||
&tls.Config{RootCAs: readCert(t, tlsCACert)},
|
||||
},
|
||||
"CACertData": {
|
||||
kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, tlsCACert)),
|
||||
},
|
||||
cli.CLI{},
|
||||
startServerTLS,
|
||||
&tls.Config{RootCAs: readCert(t, tlsCACert)},
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range data {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
authServer := c.startServer(t, NewAuthHandler(t, c.kubeconfigValues.Issuer))
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &c.kubeconfigValues)
|
||||
defer os.Remove(kubeconfig)
|
||||
c.cli.KubeConfig = kubeconfig
|
||||
c.cli.SkipOpenBrowser = true
|
||||
|
||||
var eg errgroup.Group
|
||||
eg.Go(func() error {
|
||||
return c.cli.Run(ctx)
|
||||
})
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.authClientTLS}}
|
||||
res, err := client.Get("http://localhost:8000/")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not send a request: %s", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("StatusCode wants 200 but %d", res.StatusCode)
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
t.Fatalf("CLI returned error: %s", err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, h http.Handler) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: h,
|
||||
}
|
||||
go func() {
|
||||
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
|
||||
func startServerTLS(t *testing.T, h http.Handler) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: h,
|
||||
}
|
||||
go func() {
|
||||
if err := s.ListenAndServeTLS(tlsServerCert, tlsServerKey); err != nil && err != http.ErrServerClosed {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
|
||||
func read(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read %s: %s", name, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func readCert(t *testing.T, name string) *x509.CertPool {
|
||||
t.Helper()
|
||||
p := x509.NewCertPool()
|
||||
b := read(t, name)
|
||||
if !p.AppendCertsFromPEM(b) {
|
||||
t.Fatalf("Could not append cert from %s", name)
|
||||
}
|
||||
return p
|
||||
}
|
||||
30
go.mod
Normal file
30
go.mod
Normal 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
58
go.sum
Normal 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=
|
||||
@@ -2,27 +2,28 @@ package kubeconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// FindCurrentAuthInfo returns the authInfo of current context.
|
||||
// If the current context does not exist, this returns nil.
|
||||
func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
|
||||
// FindOIDCAuthProvider returns the current OIDC authProvider.
|
||||
// If the context, auth-info or auth-provider does not exist, this returns an error.
|
||||
// If auth-provider is not "oidc", this returns an error.
|
||||
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
|
||||
context := config.Contexts[config.CurrentContext]
|
||||
if context == nil {
|
||||
return nil
|
||||
return nil, fmt.Errorf("context %s does not exist", config.CurrentContext)
|
||||
}
|
||||
authInfo := config.AuthInfos[context.AuthInfo]
|
||||
if authInfo == nil {
|
||||
return nil, fmt.Errorf("auth-info %s does not exist", context.AuthInfo)
|
||||
}
|
||||
return config.AuthInfos[context.AuthInfo]
|
||||
}
|
||||
|
||||
// FindOIDCAuthProvider returns the OIDC authProvider.
|
||||
func FindOIDCAuthProvider(authInfo *api.AuthInfo) (*OIDCAuthProvider, error) {
|
||||
if authInfo.AuthProvider == nil {
|
||||
return nil, fmt.Errorf("auth-provider is not set, did you setup kubectl as listed here: https://github.com/int128/kubelogin")
|
||||
return nil, fmt.Errorf("auth-provider is not set")
|
||||
}
|
||||
if authInfo.AuthProvider.Name != "oidc" {
|
||||
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
|
||||
return nil, fmt.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
|
||||
}
|
||||
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
|
||||
}
|
||||
@@ -55,6 +56,14 @@ func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
|
||||
return c.Config["idp-certificate-authority-data"]
|
||||
}
|
||||
|
||||
// ExtraScopes returns the extra-scopes.
|
||||
func (c *OIDCAuthProvider) ExtraScopes() []string {
|
||||
if c.Config["extra-scopes"] == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(c.Config["extra-scopes"], ",")
|
||||
}
|
||||
|
||||
// SetIDToken replaces the id-token.
|
||||
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
|
||||
c.Config["id-token"] = idToken
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// Read parses the file and returns the Config.
|
||||
func Read(path string) (*api.Config, error) {
|
||||
config, err := clientcmd.LoadFromFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not load kubeconfig from %s: %s", path, err)
|
||||
}
|
||||
return config, nil
|
||||
return clientcmd.LoadFromFile(path)
|
||||
}
|
||||
|
||||
// Write writes the config to the file.
|
||||
|
||||
7
main.go
7
main.go
@@ -8,13 +8,16 @@ import (
|
||||
"github.com/int128/kubelogin/cli"
|
||||
)
|
||||
|
||||
// Set by goreleaser, see https://goreleaser.com/environment/
|
||||
var version = "1.x"
|
||||
|
||||
func main() {
|
||||
c, err := cli.Parse(os.Args)
|
||||
c, err := cli.Parse(os.Args, version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := c.Run(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("Error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user