mirror of
https://github.com/int128/kubelogin.git
synced 2026-08-23 21:06:15 +00:00
Merge pull request #9 from int128/oidc-ca
Add support of OIDC CA certificate
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -9,3 +9,7 @@ indent_size = 2
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
|
||||
+38
-6
@@ -3,7 +3,10 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
@@ -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",
|
||||
@@ -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())
|
||||
|
||||
@@ -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{
|
||||
Issuer: "http://localhost:9000",
|
||||
}
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(conf.Issuer),
|
||||
Handler: NewAuthHandler(t, "http://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(context.Background())
|
||||
kubeconfig := createKubeconfig(t, conf.Issuer)
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "http://localhost:9000",
|
||||
})
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(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(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
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))
|
||||
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,
|
||||
}
|
||||
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
|
||||
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/CA
|
||||
*.key
|
||||
*.csr
|
||||
*.crt
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: authserver.crt authserver-ca.crt
|
||||
|
||||
clean:
|
||||
rm -v authserver*
|
||||
|
||||
authserver-ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
authserver-ca.csr: openssl.cnf authserver-ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key authserver-ca.key \
|
||||
-subj "/CN=Hello CA" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
authserver-ca.crt: authserver-ca.csr authserver-ca.key
|
||||
openssl x509 -req \
|
||||
-signkey authserver-ca.key \
|
||||
-in authserver-ca.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
authserver.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
authserver.csr: openssl.cnf authserver.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key authserver.key \
|
||||
-subj "/CN=localhost" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
|
||||
rm -fr ./CA
|
||||
mkdir -p ./CA
|
||||
touch CA/index.txt
|
||||
touch CA/index.txt.attr
|
||||
echo 00 > CA/serial
|
||||
openssl ca -config openssl.cnf \
|
||||
-extensions v3_req \
|
||||
-batch \
|
||||
-cert authserver-ca.crt \
|
||||
-keyfile authserver-ca.key \
|
||||
-in authserver.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
+6
@@ -19,4 +19,10 @@ users:
|
||||
client-id: kubernetes
|
||||
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
|
||||
idp-issuer-url: {{ .Issuer }}
|
||||
#{{ if .IDPCertificateAuthority }}
|
||||
idp-certificate-authority: {{ .IDPCertificateAuthority }}
|
||||
#{{ end }}
|
||||
#{{ if .IDPCertificateAuthorityData }}
|
||||
idp-certificate-authority-data: {{ .IDPCertificateAuthorityData }}
|
||||
#{{ end }}
|
||||
name: oidc
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = ./CA
|
||||
certs = $dir
|
||||
crl_dir = $dir
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
default_days = 365
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[ req_distinguished_name ]
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = DNS:localhost
|
||||
|
||||
[ v3_ca ]
|
||||
basicConstraints = CA:true
|
||||
@@ -16,6 +16,7 @@ func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
|
||||
return config.AuthInfos[context.AuthInfo]
|
||||
}
|
||||
|
||||
// ToOIDCAuthProviderConfig converts from api.AuthInfo to OIDCAuthProviderConfig.
|
||||
func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig, 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#3-setup-kubectl")
|
||||
@@ -26,6 +27,7 @@ func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig,
|
||||
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
|
||||
}
|
||||
|
||||
// OIDCAuthProviderConfig represents OIDC configuration in the kubeconfig.
|
||||
type OIDCAuthProviderConfig api.AuthProviderConfig
|
||||
|
||||
// IDPIssuerURL returns the idp-issuer-url.
|
||||
@@ -43,10 +45,22 @@ func (c *OIDCAuthProviderConfig) ClientSecret() string {
|
||||
return c.Config["client-secret"]
|
||||
}
|
||||
|
||||
// IDPCertificateAuthority returns the idp-certificate-authority.
|
||||
func (c *OIDCAuthProviderConfig) IDPCertificateAuthority() string {
|
||||
return c.Config["idp-certificate-authority"]
|
||||
}
|
||||
|
||||
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
|
||||
func (c *OIDCAuthProviderConfig) IDPCertificateAuthorityData() string {
|
||||
return c.Config["idp-certificate-authority-data"]
|
||||
}
|
||||
|
||||
// SetIDToken replaces the id-token.
|
||||
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
|
||||
c.Config["id-token"] = idToken
|
||||
}
|
||||
|
||||
// SetRefreshToken replaces the refresh-token.
|
||||
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
|
||||
c.Config["refresh-token"] = refreshToken
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user