diff --git a/go.mod b/go.mod index 90202203..0730b590 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/hashicorp/go-hclog v0.12.0 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-sockaddr v1.0.2 + github.com/hashicorp/go-version v1.2.0 // indirect github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820 github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect diff --git a/go.sum b/go.sum index 85dbddc6..b9307b2a 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/path_config.go b/path_config.go index 8880c401..a32f0cbe 100644 --- a/path_config.go +++ b/path_config.go @@ -57,6 +57,14 @@ extracted. Not every installation of Kuberentes exposes these keys.`, Name: "JWT Issuer", }, }, + "disable_iss_validation": { + Type: framework.TypeBool, + Description: "Disable JWT issuer validation. Allows to skip ISS validation.", + Default: false, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Disable JWT Issuer Validation", + }, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathConfigWrite, @@ -79,10 +87,11 @@ func (b *kubeAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reque // Create a map of data to be returned resp := &logical.Response{ Data: map[string]interface{}{ - "kubernetes_host": config.Host, - "kubernetes_ca_cert": config.CACert, - "pem_keys": config.PEMKeys, - "issuer": config.Issuer, + "kubernetes_host": config.Host, + "kubernetes_ca_cert": config.CACert, + "pem_keys": config.PEMKeys, + "issuer": config.Issuer, + "disable_iss_validation": config.DisableISSValidation, }, } @@ -100,6 +109,7 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ pemList := data.Get("pem_keys").([]string) caCert := data.Get("kubernetes_ca_cert").(string) issuer := data.Get("issuer").(string) + disableIssValidation := data.Get("disable_iss_validation").(bool) if len(pemList) == 0 && len(caCert) == 0 { return logical.ErrorResponse("one of pem_keys or kubernetes_ca_cert must be set"), nil } @@ -114,12 +124,13 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ } config := &kubeConfig{ - PublicKeys: make([]interface{}, len(pemList)), - PEMKeys: pemList, - Host: host, - CACert: caCert, - TokenReviewerJWT: tokenReviewer, - Issuer: issuer, + PublicKeys: make([]interface{}, len(pemList)), + PEMKeys: pemList, + Host: host, + CACert: caCert, + TokenReviewerJWT: tokenReviewer, + Issuer: issuer, + DisableISSValidation: disableIssValidation, } var err error @@ -157,6 +168,8 @@ type kubeConfig struct { TokenReviewerJWT string `json:"token_reviewer_jwt"` // Issuer is the claim that specifies who issued the token Issuer string `json:"issuer"` + // DisableISSValidation is optional parameter to allow to skip ISS validation + DisableISSValidation bool `json:"disable_iss_validation"` } // PasrsePublicKeyPEM is used to parse RSA and ECDSA public keys from PEMs diff --git a/path_config_test.go b/path_config_test.go index 26978d261..90c2e6ce 100644 --- a/path_config_test.go +++ b/path_config_test.go @@ -12,10 +12,11 @@ func TestConfig_Read(t *testing.T) { b, storage := getBackend(t) data := map[string]interface{}{ - "pem_keys": []string{testRSACert, testECCert}, - "kubernetes_host": "host", - "kubernetes_ca_cert": testCACert, - "issuer": "", + "pem_keys": []string{testRSACert, testECCert}, + "kubernetes_host": "host", + "kubernetes_ca_cert": testCACert, + "issuer": "", + "disable_iss_validation": false, } req := &logical.Request{ @@ -135,10 +136,11 @@ func TestConfig(t *testing.T) { } expected := &kubeConfig{ - PublicKeys: []interface{}{}, - PEMKeys: []string{}, - Host: "host", - CACert: testCACert, + PublicKeys: []interface{}{}, + PEMKeys: []string{}, + Host: "host", + CACert: testCACert, + DisableISSValidation: false, } conf, err := b.(*kubeAuthBackend).config(context.Background(), storage) @@ -175,11 +177,12 @@ func TestConfig(t *testing.T) { } expected = &kubeConfig{ - PublicKeys: []interface{}{}, - PEMKeys: []string{}, - Host: "host", - CACert: testCACert, - TokenReviewerJWT: jwtData, + PublicKeys: []interface{}{}, + PEMKeys: []string{}, + Host: "host", + CACert: testCACert, + TokenReviewerJWT: jwtData, + DisableISSValidation: false, } conf, err = b.(*kubeAuthBackend).config(context.Background(), storage) @@ -216,10 +219,11 @@ func TestConfig(t *testing.T) { } expected = &kubeConfig{ - PublicKeys: []interface{}{cert}, - PEMKeys: []string{testRSACert}, - Host: "host", - CACert: testCACert, + PublicKeys: []interface{}{cert}, + PEMKeys: []string{testRSACert}, + Host: "host", + CACert: testCACert, + DisableISSValidation: false, } conf, err = b.(*kubeAuthBackend).config(context.Background(), storage) @@ -261,10 +265,52 @@ func TestConfig(t *testing.T) { } expected = &kubeConfig{ - PublicKeys: []interface{}{cert, cert2}, - PEMKeys: []string{testRSACert, testECCert}, - Host: "host", - CACert: testCACert, + PublicKeys: []interface{}{cert, cert2}, + PEMKeys: []string{testRSACert, testECCert}, + Host: "host", + CACert: testCACert, + DisableISSValidation: false, + } + + conf, err = b.(*kubeAuthBackend).config(context.Background(), storage) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(expected, conf) { + t.Fatalf("expected did not match actual: expected %#v\n got %#v\n", expected, conf) + } + + // Test success with disabled iss validation + data = map[string]interface{}{ + "kubernetes_host": "host", + "kubernetes_ca_cert": testCACert, + "disable_iss_validation": true, + } + + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: configPath, + Storage: storage, + Data: data, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + cert, err = parsePublicKeyPEM([]byte(testRSACert)) + if err != nil { + t.Fatal(err) + } + + expected = &kubeConfig{ + PublicKeys: []interface{}{}, + PEMKeys: []string{}, + Host: "host", + CACert: testCACert, + DisableISSValidation: true, } conf, err = b.(*kubeAuthBackend).config(context.Background(), storage) diff --git a/path_login.go b/path_login.go index be38486f..504700d8 100644 --- a/path_login.go +++ b/path_login.go @@ -203,11 +203,14 @@ func (b *kubeAuthBackend) parseAndValidateJWT(jwtStr string, role *roleStorageEn }, } - // set the expected issuer to the default kubernetes issuer if the config doesn't specify it - if config.Issuer != "" { - validator.SetIssuer(config.Issuer) - } else { - validator.SetIssuer(defaultJWTIssuer) + // perform ISS Claim validation if configured + if !config.DisableISSValidation { + // set the expected issuer to the default kubernetes issuer if the config doesn't specify it + if config.Issuer != "" { + validator.SetIssuer(config.Issuer) + } else { + validator.SetIssuer(defaultJWTIssuer) + } } // validate the audience if the role expects it diff --git a/path_login_test.go b/path_login_test.go index 893a74f7..661b53a0 100644 --- a/path_login_test.go +++ b/path_login_test.go @@ -561,6 +561,171 @@ func TestAliasLookAhead(t *testing.T) { } } +func TestLoginIssValidation(t *testing.T) { + b, storage := setupBackend(t, testNoPEMs, testName, testNamespace) + + // test iss validation enabled with default "kubernetes/serviceaccount" issuer + data := map[string]interface{}{ + "kubernetes_host": "host", + "kubernetes_ca_cert": testCACert, + "disable_iss_validation": false, + } + + req := &logical.Request{ + Operation: logical.CreateOperation, + Path: "config", + Storage: storage, + Data: data, + } + + resp, err := b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + // test successful login with default issuer + data = map[string]interface{}{ + "role": "plugin-test", + "jwt": jwtData, + } + + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: data, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + } + + // test iss validation enabled with explicitly defined issuer + data = map[string]interface{}{ + "kubernetes_host": "host", + "kubernetes_ca_cert": testCACert, + "disable_iss_validation": false, + "issuer": "kubernetes/serviceaccount", + } + + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: "config", + Storage: storage, + Data: data, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + // test successful login with explicitly defined issuer + data = map[string]interface{}{ + "role": "plugin-test", + "jwt": jwtData, + } + + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: data, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + // test iss validation enabled with custom issuer + data = map[string]interface{}{ + "kubernetes_host": "host", + "kubernetes_ca_cert": testCACert, + "disable_iss_validation": false, + "issuer": "custom-issuer", + } + + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: "config", + Storage: storage, + Data: data, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + // test login fail with enabled iss validation and custom issuer + data = map[string]interface{}{ + "role": "plugin-test", + "jwt": jwtData, + } + + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: data, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatal("expected error") + } + if err.Error() != "claim \"iss\" is invalid" { + t.Fatalf("unexpected error: %s", err) + } + + // test iss validation disabled with custom issuer + data = map[string]interface{}{ + "kubernetes_host": "host", + "kubernetes_ca_cert": testCACert, + "disable_iss_validation": true, + "issuer": "custom-issuer", + } + + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: "config", + Storage: storage, + Data: data, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + // test login success with disabled iss validation and custom issuer + data = map[string]interface{}{ + "role": "plugin-test", + "jwt": jwtData, + } + + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: data, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } +} + var jwtData = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InZhdWx0LWF1dGgtdG9rZW4tdDVwY24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidmF1bHQtYXV0aCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ3N2Y4OWJjLTkwNTUtMTFlNy1hMDY4LTA4MDAyNzZkOTliZiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LWF1dGgifQ.HKUcqgrvan5ZC_mnpaMEx4RW3KrhfyH_u8G_IA2vUfkLK8tH3T7fJuJaPr7W6K_BqCrbeM5y3owszOzb4NR0Lvw6GBt2cFcen2x1Ua4Wokr0bJjTT7xQOIOw7UvUDyVS17wAurlfUnmWMwMMMOebpqj5K1t6GnyqghH1wPdHYRGX-q5a6C323dBCgM5t6JY_zTTaBgM6EkFq0poBaifmSMiJRPrdUN_-IgyK8fgQRiFYYkgS6DMIU4k4nUOb_sUFf5xb8vMs3SMteKiuWFAIt4iszXTj5IyBUNqe0cXA3zSY3QiNCV6bJ2CWW0Qf9WDtniT79VAqcR4GYaTC_gxjNA" var jwtBadServiceAccount = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InZhdWx0LWludmFsaWQtdG9rZW4tZ3ZxcHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidmF1bHQtaW52YWxpZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjA0NGZkNGYxLTk3NGQtMTFlNy05YTE1LTA4MDAyNzZkOTliZiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LWludmFsaWQifQ.BcoOdu5BrIchp66Zl8-dY7HcGHJrVXrUh4SNTlIHR6vDaNH29B7JuI_-B1pvW9GpzQnc-XjZyua_wfSssqe-KYJcq--Qh0yQfbbLE5rvEipBCHH341IqGaTHaBVip8zXqYE-bt-7J6vAH8Azvw46iatDC73tKxh46xDuxK0gKjdprW4cOklDx6ZSxEHpu63ftLYgAgk9c0MUJxKWhu9Jk0aye5pTj_iyBbBy8llZNGaw2gxvhPzFVUEHZUlTRiSIbmPmNqep48RiJoWrq6FM1lijvrtT5y-E7aFk6TpW2BH3VDHy8k10sMIxuRAYrGB3tpUKNyVDI3tJOi_xY7iJvw"