mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-23 05:23:48 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309e73d8c0 | ||
|
|
857d5dad88 | ||
|
|
455c920b65 | ||
|
|
afad46817a | ||
|
|
4f506b9f62 | ||
|
|
72bc19bc10 | ||
|
|
69bcb16e26 |
@@ -10,7 +10,7 @@ jobs:
|
||||
- run: go get github.com/golang/lint/golint
|
||||
- run: golint
|
||||
- run: go build -v
|
||||
- run: make -C integration-test/testdata
|
||||
- run: make -C e2e/testdata
|
||||
- run: go test -v ./...
|
||||
|
||||
release:
|
||||
|
||||
132
README.md
132
README.md
@@ -1,29 +1,38 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)
|
||||
|
||||
`kubelogin` is a command to get an OpenID Connect (OIDC) token for `kubectl` authentication.
|
||||
This is a helper 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
|
||||
|
||||
1. Setup your OpenID Connect provider, e.g. Google Identity Platform or Keycloak.
|
||||
1. Setup your Kubernetes cluster.
|
||||
1. Setup your `kubectl`.
|
||||
You need to setup the OIDC provider and [Kubernetes OIDC authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
|
||||
After setup or when the token has been expired, just run `kubelogin`:
|
||||
|
||||
```
|
||||
% kubelogin --help
|
||||
2018/08/15 19:08:58 Usage:
|
||||
kubelogin [OPTIONS]
|
||||
|
||||
Application Options:
|
||||
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
|
||||
--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]
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
% 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.
|
||||
|
||||
|
||||
## Getting Started with Google Account
|
||||
|
||||
@@ -176,7 +185,34 @@ See the previous section for details.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Kubeconfig
|
||||
```
|
||||
kubelogin [OPTIONS]
|
||||
|
||||
Application Options:
|
||||
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
|
||||
--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]
|
||||
|
||||
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).
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
`idp-issuer-url` | IN (Required) | Issuer URL of the provider.
|
||||
`client-id` | IN (Required) | Client ID of the provider.
|
||||
`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.
|
||||
`id-token` | OUT | ID token got from the provider.
|
||||
`refresh-token` | OUT | Refresh token got from the provider.
|
||||
|
||||
|
||||
### Kubeconfig path
|
||||
|
||||
You can set the environment variable `KUBECONFIG` to point the config file.
|
||||
Default to `~/.kube/config`.
|
||||
@@ -185,37 +221,41 @@ Default to `~/.kube/config`.
|
||||
export KUBECONFIG="$PWD/.kubeconfig"
|
||||
```
|
||||
|
||||
### OpenID Connect Provider CA Certificate
|
||||
### Team onboarding
|
||||
|
||||
You can specify the CA certificate of your OpenID Connect provider by [`idp-certificate-authority` or `idp-certificate-authority-data` in the kubeconfig](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
|
||||
You can share the kubeconfig to your team members for easy setup.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials CLUSTER_NAME \
|
||||
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
|
||||
```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
|
||||
```
|
||||
|
||||
### Setup script
|
||||
|
||||
In actual team operation, you can share the following script to your team members for easy setup.
|
||||
If you are using kops, export the kubeconfig and edit it.
|
||||
|
||||
```sh
|
||||
#!/bin/sh -xe
|
||||
CLUSTER_NAME="hello.k8s.local"
|
||||
|
||||
export KUBECONFIG="$PWD/.kubeconfig"
|
||||
|
||||
kubectl config set-cluster "$CLUSTER_NAME" \
|
||||
--server https://api-xxx.xxx.elb.amazonaws.com \
|
||||
--certificate-authority "$PWD/cluster.crt"
|
||||
|
||||
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
|
||||
|
||||
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
|
||||
kubectl config use-context "$CLUSTER_NAME"
|
||||
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
|
||||
vim .kubeconfig
|
||||
```
|
||||
|
||||
|
||||
@@ -224,12 +264,18 @@ kubectl config use-context "$CLUSTER_NAME"
|
||||
This is an open source software licensed under Apache License 2.0.
|
||||
Feel free to open issues and pull requests.
|
||||
|
||||
### Build
|
||||
### 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).
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -61,6 +62,7 @@ func (f *authCodeFlow) getAuthCode(ctx context.Context) (string, error) {
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package integration
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
145
e2e/e2e_test.go
Normal file
145
e2e/e2e_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package integration
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
@@ -1,164 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/cli"
|
||||
)
|
||||
|
||||
const caCert = "testdata/authserver-ca.crt"
|
||||
const tlsCert = "testdata/authserver.crt"
|
||||
const tlsKey = "testdata/authserver.key"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "http://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "http://localhost:9000",
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServe(t, authServer)
|
||||
go authenticate(t, &tls.Config{})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
SkipOpenBrowser: true,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func TestWithSkipTLSVerify(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServeTLS(t, authServer)
|
||||
go authenticate(t, &tls.Config{InsecureSkipVerify: true})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
SkipTLSVerify: true,
|
||||
SkipOpenBrowser: true,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func TestWithCACert(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: caCert,
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServeTLS(t, authServer)
|
||||
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
SkipOpenBrowser: true,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func TestWithCACertData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
b, err := ioutil.ReadFile(caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(b),
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServeTLS(t, authServer)
|
||||
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
SkipOpenBrowser: true,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func authenticate(t *testing.T, tlsConfig *tls.Config) {
|
||||
t.Helper()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get("http://localhost:8000/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d: res=%+v", res.StatusCode, res)
|
||||
}
|
||||
}
|
||||
|
||||
func loadCACert(t *testing.T) *x509.CertPool {
|
||||
p := x509.NewCertPool()
|
||||
b, err := ioutil.ReadFile(caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !p.AppendCertsFromPEM(b) {
|
||||
t.Fatalf("Could not AppendCertsFromPEM")
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func listenAndServe(t *testing.T, s *http.Server) {
|
||||
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func listenAndServeTLS(t *testing.T, s *http.Server) {
|
||||
if err := s.ListenAndServeTLS(tlsCert, tlsKey); err != nil && err != http.ErrServerClosed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user