Skip to content

Commit

Permalink
Merge pull request #9 from int128/oidc-ca
Browse files Browse the repository at this point in the history
Add support of OIDC CA certificate
  • Loading branch information
int128 authored Aug 24, 2018
2 parents adaeba4 + 7726ac6 commit b7bbcd4
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 49 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -v
- run: make -C integration-test/testdata
- run: go test -v ./...

release:
Expand Down
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ indent_size = 2
[*.go]
indent_style = tab
indent_size = 4

[Makefile]
indent_style = tab
indent_size = 4
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ Run `kubelogin` and make sure you can access to the cluster.
See the previous section for details.


## Tips
## Configuration

### Config file
### Kubeconfig

You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
Expand All @@ -184,6 +184,15 @@ Default to `~/.kube/config`.
export KUBECONFIG="$PWD/.kubeconfig"
```

### OpenID Connect Provider CA Certificate

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).

```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
```

### Setup script

In actual team operation, you can share the following script to your team members for easy setup.
Expand Down
44 changes: 38 additions & 6 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package cli
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"

Expand Down Expand Up @@ -45,7 +48,7 @@ func (c *CLI) ExpandKubeConfig() (string, error) {
}

// Run performs this command.
func (c *CLI) Run() error {
func (c *CLI) Run(ctx context.Context) error {
path, err := c.ExpandKubeConfig()
if err != nil {
return err
Expand All @@ -64,11 +67,11 @@ func (c *CLI) Run() error {
if err != nil {
return fmt.Errorf("Could not find auth-provider: %s", err)
}

client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}}
ctx := context.Background()
tlsConfig, err := c.tlsConfig(authProvider)
if err != nil {
return fmt.Errorf("Could not configure TLS: %s", err)
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
token, err := auth.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
Expand All @@ -81,3 +84,32 @@ func (c *CLI) Run() error {
log.Printf("Updated %s", path)
return nil
}

func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProviderConfig) (*tls.Config, error) {
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 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 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
}
7 changes: 4 additions & 3 deletions integration-test/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"math/big"
"net/http"
"testing"
"time"

jwt "github.com/dgrijalva/jwt-go"
Expand All @@ -29,7 +30,7 @@ type AuthHandler struct {
}

// NewAuthHandler returns a new AuthHandler.
func NewAuthHandler(issuer string) *AuthHandler {
func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
h := &AuthHandler{
Issuer: issuer,
AuthCode: "0b70006b-f62a-4438-aba5-c0b96775d8e5",
Expand All @@ -44,11 +45,11 @@ func NewAuthHandler(issuer string) *AuthHandler {
})
k, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
log.Fatal(err)
t.Fatalf("Could not generate a key pair: %s", err)
}
h.IDToken, err = token.SignedString(k)
if err != nil {
log.Fatal(err)
t.Fatalf("Could not generate an ID token: %s", err)
}
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
Expand Down
165 changes: 132 additions & 33 deletions integration-test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,159 @@ package integration

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"time"

"github.com/int128/kubelogin/cli"
)

type configuration struct {
Issuer string
}
const caCert = "testdata/authserver-ca.crt"
const tlsCert = "testdata/authserver.crt"
const tlsKey = "testdata/authserver.key"

func Test(t *testing.T) {
conf := configuration{
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,
}
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,
}
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,
}
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(conf.Issuer),
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
b, err := ioutil.ReadFile(caCert)
if err != nil {
t.Fatal(err)
}
defer authServer.Shutdown(context.Background())
kubeconfig := createKubeconfig(t, conf.Issuer)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(b),
})
defer os.Remove(kubeconfig)

go func() {
if err := authServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
go func() {
time.Sleep(100 * time.Millisecond)
res, err := http.Get("http://localhost:8000/")
if err != nil {
t.Error(err)
}
if res.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d: res=%+v", res.StatusCode, res)
}
}()
c := cli.CLI{KubeConfig: kubeconfig}
if err := c.Run(); err != nil {
t.Fatal(err)
}

b, err := ioutil.ReadFile(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})

c := cli.CLI{
KubeConfig: kubeconfig,
}
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 strings.Index(string(b), "id-token: ey") == -1 {
t.Errorf("kubeconfig wants id-token but %s", string(b))
if !p.AppendCertsFromPEM(b) {
t.Fatalf("Could not AppendCertsFromPEM")
}
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
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)
}
}
26 changes: 22 additions & 4 deletions integration-test/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ package integration
import (
"html/template"
"io/ioutil"
"log"
"strings"
"testing"
)

func createKubeconfig(t *testing.T, issuer string) string {
type kubeconfigValues struct {
Issuer string
IDPCertificateAuthority string
IDPCertificateAuthorityData string
}

func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
Expand All @@ -18,9 +24,21 @@ func createKubeconfig(t *testing.T, issuer string) string {
if err != nil {
t.Fatal(err)
}
if err := tpl.Execute(f, struct{ Issuer string }{issuer}); err != nil {
if err := tpl.Execute(f, v); err != nil {
t.Fatal(err)
}
log.Printf("Created %s", f.Name())
return f.Name()
}

func verifyKubeconfig(t *testing.T, kubeconfig string) {
b, err := ioutil.ReadFile(kubeconfig)
if err != nil {
t.Fatal(err)
}
if strings.Index(string(b), "id-token: ey") == -1 {
t.Errorf("kubeconfig wants id-token but %s", string(b))
}
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
}
}
4 changes: 4 additions & 0 deletions integration-test/testdata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/CA
*.key
*.csr
*.crt
Loading

0 comments on commit b7bbcd4

Please sign in to comment.