Add TLS support to exec authenticator plugin

https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md#tls-client-certificate-support

Allows exec plugin to return raw TLS key/cert data. This data populates
transport.Config.TLS fields.
transport.Config.TLS propagates custom credentials using
tls.Config.GetClientCertificate callback.
On key/cert rotation, all connections using old credentials are
closed

Kubernetes-commit: cd89f9473faa60c15b8e9d223e5c4f9dab53627a
This commit is contained in:
Andrew Lytvynov
2018-05-30 14:03:32 -07:00
committed by Kubernetes Publisher
parent 33a8186d0a
commit c669580288
12 changed files with 605 additions and 112 deletions

View File

@@ -18,19 +18,88 @@ package exec
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/transport"
)
var (
certData = []byte(`-----BEGIN CERTIFICATE-----
MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu
MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz
MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v
b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj
lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2
I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb
1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F
kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P
AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ
KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/
p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3
jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq
6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ
HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ
BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0
-----END CERTIFICATE-----`)
keyData = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i
wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc
kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG
0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv
RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi
ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU
FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK
aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm
5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M
ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0
JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr
7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI
cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey
OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/
rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9
8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg
nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k
2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII
NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+
GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S
3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG
77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/
bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/
F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX
stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa
-----END RSA PRIVATE KEY-----`)
validCert *tls.Certificate
)
func init() {
cert, err := tls.X509KeyPair(certData, keyData)
if err != nil {
panic(err)
}
validCert = &cert
}
func TestCacheKey(t *testing.T) {
c1 := &api.ExecConfig{
Command: "foo-bar",
@@ -93,7 +162,7 @@ func compJSON(t *testing.T, got, want []byte) {
}
}
func TestGetToken(t *testing.T) {
func TestRefreshCreds(t *testing.T) {
tests := []struct {
name string
config api.ExecConfig
@@ -101,7 +170,7 @@ func TestGetToken(t *testing.T) {
interactive bool
response *clientauthentication.Response
wantInput string
wantToken string
wantCreds credentials
wantExpiry time.Time
wantErr bool
}{
@@ -122,7 +191,7 @@ func TestGetToken(t *testing.T) {
"token": "foo-bar"
}
}`,
wantToken: "foo-bar",
wantCreds: credentials{token: "foo-bar"},
},
{
name: "interactive",
@@ -144,7 +213,7 @@ func TestGetToken(t *testing.T) {
"token": "foo-bar"
}
}`,
wantToken: "foo-bar",
wantCreds: credentials{token: "foo-bar"},
},
{
name: "response",
@@ -178,7 +247,7 @@ func TestGetToken(t *testing.T) {
"token": "foo-bar"
}
}`,
wantToken: "foo-bar",
wantCreds: credentials{token: "foo-bar"},
},
{
name: "expiry",
@@ -199,7 +268,7 @@ func TestGetToken(t *testing.T) {
}
}`,
wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
wantToken: "foo-bar",
wantCreds: credentials{token: "foo-bar"},
},
{
name: "no-group-version",
@@ -236,7 +305,7 @@ func TestGetToken(t *testing.T) {
wantErr: true,
},
{
name: "no-token",
name: "no-creds",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
@@ -252,6 +321,65 @@ func TestGetToken(t *testing.T) {
}`,
wantErr: true,
},
{
name: "TLS credentials",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: fmt.Sprintf(`{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"clientKeyData": %q,
"clientCertificateData": %q
}
}`, keyData, certData),
wantCreds: credentials{cert: validCert},
},
{
name: "bad TLS credentials",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"clientKeyData": "foo",
"clientCertificateData": "bar"
}
}`,
wantErr: true,
},
{
name: "cert but no key",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: fmt.Sprintf(`{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"clientCertificateData": %q
}
}`, certData),
wantErr: true,
},
}
for _, test := range tests {
@@ -274,8 +402,7 @@ func TestGetToken(t *testing.T) {
a.interactive = test.interactive
a.environ = func() []string { return nil }
token, err := a.getToken(test.response)
if err != nil {
if err := a.refreshCredsLocked(test.response); err != nil {
if !test.wantErr {
t.Errorf("get token %v", err)
}
@@ -285,8 +412,8 @@ func TestGetToken(t *testing.T) {
t.Fatal("expected error getting token")
}
if token != test.wantToken {
t.Errorf("expected token %q got %q", test.wantToken, token)
if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) {
t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds)
}
if !a.exp.Equal(test.wantExpiry) {
@@ -342,8 +469,12 @@ func TestRoundTripper(t *testing.T) {
a.now = now
a.stderr = ioutil.Discard
tc := &transport.Config{}
if err := a.UpdateTransportConfig(tc); err != nil {
t.Fatal(err)
}
client := http.Client{
Transport: a.WrapTransport(http.DefaultTransport),
Transport: tc.WrapTransport(http.DefaultTransport),
}
get := func(t *testing.T, statusCode int) {
@@ -411,3 +542,134 @@ func TestRoundTripper(t *testing.T) {
// Old token is expired, should refresh automatically without hitting a 401.
get(t, http.StatusOK)
}
func TestTLSCredentials(t *testing.T) {
now := time.Now()
certPool := x509.NewCertPool()
cert, key := genClientCert(t)
if !certPool.AppendCertsFromPEM(cert) {
t.Fatal("failed to add client cert to CertPool")
}
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
}))
server.TLS = &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
}
server.StartTLS()
defer server.Close()
a, err := newAuthenticator(newCache(), &api.ExecConfig{
Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1",
})
if err != nil {
t.Fatal(err)
}
var output *clientauthentication.ExecCredential
a.environ = func() []string {
data, err := runtime.Encode(codecs.LegacyCodec(a.group), output)
if err != nil {
t.Fatal(err)
}
return []string{"TEST_OUTPUT=" + string(data)}
}
a.now = func() time.Time { return now }
a.stderr = ioutil.Discard
// We're not interested in server's cert, this test is about client cert.
tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}}
if err := a.UpdateTransportConfig(tc); err != nil {
t.Fatal(err)
}
get := func(t *testing.T, desc string, wantErr bool) {
t.Run(desc, func(t *testing.T) {
tlsCfg, err := transport.TLSConfigFor(tc)
if err != nil {
t.Fatal("TLSConfigFor:", err)
}
client := http.Client{
Transport: &http.Transport{TLSClientConfig: tlsCfg},
}
resp, err := client.Get(server.URL)
switch {
case err != nil && !wantErr:
t.Errorf("got client.Get error: %q, want nil", err)
case err == nil && wantErr:
t.Error("got nil client.Get error, want non-nil")
}
if err == nil {
resp.Body.Close()
}
})
}
output = &clientauthentication.ExecCredential{
Status: &clientauthentication.ExecCredentialStatus{
ClientCertificateData: string(cert),
ClientKeyData: string(key),
ExpirationTimestamp: &v1.Time{now.Add(time.Hour)},
},
}
get(t, "valid TLS cert", false)
// Advance time to force re-exec.
nCert, nKey := genClientCert(t)
now = now.Add(time.Hour * 2)
output = &clientauthentication.ExecCredential{
Status: &clientauthentication.ExecCredentialStatus{
ClientCertificateData: string(nCert),
ClientKeyData: string(nKey),
ExpirationTimestamp: &v1.Time{now.Add(time.Hour)},
},
}
get(t, "untrusted TLS cert", true)
now = now.Add(time.Hour * 2)
output = &clientauthentication.ExecCredential{
Status: &clientauthentication.ExecCredentialStatus{
ClientCertificateData: string(cert),
ClientKeyData: string(key),
ExpirationTimestamp: &v1.Time{now.Add(time.Hour)},
},
}
get(t, "valid TLS cert again", false)
}
// genClientCert generates an x509 certificate for testing. Certificate and key
// are returned in PEM encoding.
func genClientCert(t *testing.T) ([]byte, []byte) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
keyRaw, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
t.Fatal(err)
}
cert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{Organization: []string{"Acme Co"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
}