From a9076aa51dbb5ed801fd75e33fcca564a1c22e3f Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 Jun 2022 11:13:51 +0200 Subject: [PATCH 1/7] mTLS authentication method Implements Mutual Transport Layer Security (mTLS) identity verification in Authorino. Trusted root Certificate Authorities (CA) certificates are stored as Kubernetes TLS Secrets, fetched and cached by Authorino, silimarly to how API key Secrets are handled. Label selectors and namespace/cluster scope are encoraged to be used. It works for both interfaces, i.e. the gRPC Envoy protocol-based authorization interface and the raw HTTP authorization interface. For integrations via Envoy, mTLS authn set as well in Authorino might be seen as redundant and only meaninful for the purpose of combining multiple authentication methods and/or for counting on better structured subject data to apply normalization. For simple use cases, Authorino's 'plain' identity method can be used instead, fetching Envoy-injected principal information (extracted from the client cert) from `context.request.source.principal`. --- api/v1beta1/auth_config_types.go | 14 ++++ api/v1beta1/zz_generated.deepcopy.go | 27 ++++++ controllers/auth_config_controller.go | 8 ++ .../authorino.kuadrant.io_authconfigs.yaml | 19 +++++ install/crd/patches/oneof_in_authconfigs.yaml | 5 ++ install/manifests.yaml | 26 ++++++ main.go | 1 + pkg/evaluators/identity/mtls.go | 83 ++++++++++++++++++- pkg/service/auth.go | 16 ++++ pkg/service/auth_pipeline_test.go | 8 +- 10 files changed, 200 insertions(+), 7 deletions(-) diff --git a/api/v1beta1/auth_config_types.go b/api/v1beta1/auth_config_types.go index 1bf274bb..da199675 100644 --- a/api/v1beta1/auth_config_types.go +++ b/api/v1beta1/auth_config_types.go @@ -27,6 +27,7 @@ const ( IdentityOAuth2 = "IDENTITY_OAUTH2" IdentityOidc = "IDENTITY_OIDC" IdentityApiKey = "IDENTITY_APIKEY" + IdentityMTLS = "IDENTITY_MTLS" IdentityKubernetesAuth = "IDENTITY_KUBERNETESAUTH" IdentityAnonymous = "IDENTITY_ANONYMOUS" IdentityPlain = "IDENTITY_PLAIN" @@ -203,6 +204,7 @@ type Identity struct { OAuth2 *Identity_OAuth2Config `json:"oauth2,omitempty"` Oidc *Identity_OidcConfig `json:"oidc,omitempty"` APIKey *Identity_APIKey `json:"apiKey,omitempty"` + MTLS *Identity_MTLS `json:"mtls,omitempty"` KubernetesAuth *Identity_KubernetesAuth `json:"kubernetes,omitempty"` Anonymous *Identity_Anonymous `json:"anonymous,omitempty"` Plain *Identity_Plain `json:"plain,omitempty"` @@ -215,6 +217,8 @@ func (i *Identity) GetType() string { return IdentityOidc } else if i.APIKey != nil { return IdentityApiKey + } else if i.MTLS != nil { + return IdentityMTLS } else if i.KubernetesAuth != nil { return IdentityKubernetesAuth } else if i.Anonymous != nil { @@ -256,6 +260,16 @@ type Identity_APIKey struct { AllNamespaces bool `json:"allNamespaces,omitempty"` } +type Identity_MTLS struct { + // The map of label selectors used by Authorino to match secrets from the cluster storing trusted CA certificates to validate clients trying to authenticate to this service + LabelSelectors map[string]string `json:"labelSelectors"` + + // Whether Authorino should look for TLS secrets in all namespaces or only in the same namespace of the AuthConfig. + // Enabling this option in namespaced Authorino instances has no effect. + // +kubebuilder:default:=false + AllNamespaces bool `json:"allNamespaces,omitempty"` +} + type Identity_KubernetesAuth struct { // The list of audiences (scopes) that must be claimed in a Kubernetes authentication token supplied in the request, and reviewed by Authorino. // If omitted, Authorino will review tokens expecting the host name of the requested protected service amongst the audiences. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 22b25428..7dca697c 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -457,6 +457,11 @@ func (in *Identity) DeepCopyInto(out *Identity) { *out = new(Identity_APIKey) (*in).DeepCopyInto(*out) } + if in.MTLS != nil { + in, out := &in.MTLS, &out.MTLS + *out = new(Identity_MTLS) + (*in).DeepCopyInto(*out) + } if in.KubernetesAuth != nil { in, out := &in.KubernetesAuth, &out.KubernetesAuth *out = new(Identity_KubernetesAuth) @@ -541,6 +546,28 @@ func (in *Identity_KubernetesAuth) DeepCopy() *Identity_KubernetesAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Identity_MTLS) DeepCopyInto(out *Identity_MTLS) { + *out = *in + if in.LabelSelectors != nil { + in, out := &in.LabelSelectors, &out.LabelSelectors + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity_MTLS. +func (in *Identity_MTLS) DeepCopy() *Identity_MTLS { + if in == nil { + return nil + } + out := new(Identity_MTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Identity_OAuth2Config) DeepCopyInto(out *Identity_OAuth2Config) { *out = *in diff --git a/controllers/auth_config_controller.go b/controllers/auth_config_controller.go index a9114620..f842e8c8 100644 --- a/controllers/auth_config_controller.go +++ b/controllers/auth_config_controller.go @@ -203,6 +203,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf } translatedIdentity.APIKey = identity_evaluators.NewApiKeyIdentity(identity.Name, identity.APIKey.LabelSelectors, namespace, authCred, r.Client, ctxWithLogger) + // MTLS + case api.IdentityMTLS: + namespace := authConfig.Namespace + if identity.MTLS.AllNamespaces && r.ClusterWide() { + namespace = "" + } + translatedIdentity.MTLS = identity_evaluators.NewMTLSIdentity(identity.Name, identity.MTLS.LabelSelectors, namespace, r.Client, ctxWithLogger) + // kubernetes auth case api.IdentityKubernetesAuth: if k8sAuthConfig, err := identity_evaluators.NewKubernetesAuthIdentity(authCred, identity.KubernetesAuth.Audiences); err != nil { diff --git a/install/crd/authorino.kuadrant.io_authconfigs.yaml b/install/crd/authorino.kuadrant.io_authconfigs.yaml index be8583e4..9f6adc42 100644 --- a/install/crd/authorino.kuadrant.io_authconfigs.yaml +++ b/install/crd/authorino.kuadrant.io_authconfigs.yaml @@ -830,6 +830,25 @@ spec: description: Whether this identity config should generate individual observability metrics type: boolean + mtls: + properties: + allNamespaces: + default: false + description: Whether Authorino should look for TLS secrets + in all namespaces or only in the same namespace of the + AuthConfig. Enabling this option in namespaced Authorino + instances has no effect. + type: boolean + labelSelectors: + additionalProperties: + type: string + description: The map of label selectors used by Authorino + to match secrets from the cluster storing trusted CA certificates + to validate clients trying to authenticate to this service + type: object + required: + - labelSelectors + type: object name: description: The name of this identity source/authentication mode. It usually identifies a source of identities or group diff --git a/install/crd/patches/oneof_in_authconfigs.yaml b/install/crd/patches/oneof_in_authconfigs.yaml index 4008ac2a..bc9f3408 100644 --- a/install/crd/patches/oneof_in_authconfigs.yaml +++ b/install/crd/patches/oneof_in_authconfigs.yaml @@ -18,6 +18,11 @@ credentials: {} apiKey: {} required: [name, apiKey] + - properties: + name: {} + credentials: {} + apiKey: {} + required: [name, mtls] - properties: name: {} credentials: {} diff --git a/install/manifests.yaml b/install/manifests.yaml index 019c1bca..244f18ae 100644 --- a/install/manifests.yaml +++ b/install/manifests.yaml @@ -756,6 +756,13 @@ spec: required: - name - apiKey + - properties: + apiKey: {} + credentials: {} + name: {} + required: + - name + - mtls - properties: credentials: {} kubernetes: {} @@ -916,6 +923,25 @@ spec: description: Whether this identity config should generate individual observability metrics type: boolean + mtls: + properties: + allNamespaces: + default: false + description: Whether Authorino should look for TLS secrets + in all namespaces or only in the same namespace of the + AuthConfig. Enabling this option in namespaced Authorino + instances has no effect. + type: boolean + labelSelectors: + additionalProperties: + type: string + description: The map of label selectors used by Authorino + to match secrets from the cluster storing trusted CA certificates + to validate clients trying to authenticate to this service + type: object + required: + - labelSelectors + type: object name: description: The name of this identity source/authentication mode. It usually identifies a source of identities or group diff --git a/main.go b/main.go index cddfeb64..f8138cd5 100644 --- a/main.go +++ b/main.go @@ -336,6 +336,7 @@ func startHTTPService(name, port, basePath, tlsCertPath, tlsCertKeyPath string, server := &http.Server{ TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequestClientCert, }, } err = server.ServeTLS(lis, tlsCertPath, tlsCertKeyPath) diff --git a/pkg/evaluators/identity/mtls.go b/pkg/evaluators/identity/mtls.go index 17babd60..a96e21d0 100644 --- a/pkg/evaluators/identity/mtls.go +++ b/pkg/evaluators/identity/mtls.go @@ -2,16 +2,93 @@ package identity import ( "context" + "crypto/x509" + "encoding/pem" + "fmt" + "net/url" "github.com/kuadrant/authorino/pkg/auth" + "github.com/kuadrant/authorino/pkg/log" + + k8s "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) type MTLS struct { auth.AuthCredentials - PEM string `yaml:"pem"` + Name string + LabelSelectors map[string]string + Namespace string + + rootCerts *x509.CertPool + k8sClient client.Reader +} + +func NewMTLSIdentity(name string, labelSelectors map[string]string, namespace string, k8sClient client.Reader, ctx context.Context) *MTLS { + mtls := &MTLS{ + AuthCredentials: &auth.AuthCredential{KeySelector: "Basic"}, + Name: name, + LabelSelectors: labelSelectors, + Namespace: namespace, + k8sClient: k8sClient, + } + if err := mtls.loadSecrets(context.TODO()); err != nil { + log.FromContext(ctx).WithName("mtls").Error(err, credentialsFetchingErrorMsg) + } + return mtls +} + +// loadSecrets will get the k8s secrets and update the APIKey instance +func (m *MTLS) loadSecrets(ctx context.Context) error { + opts := []client.ListOption{client.MatchingLabels(m.LabelSelectors)} + if namespace := m.Namespace; namespace != "" { + opts = append(opts, client.InNamespace(namespace)) + } + var secretList = &k8s.SecretList{} + if err := m.k8sClient.List(ctx, secretList, opts...); err != nil { + return err + } + m.rootCerts = x509.NewCertPool() + for _, secret := range secretList.Items { + var encodedCert []byte + if v, foundKey := secret.Data[k8s.TLSCertKey]; foundKey { + encodedCert = v + } else if v, foundKey := secret.Data[k8s.ServiceAccountRootCAKey]; foundKey { + encodedCert = v + } else { + continue + } + block, _ := pem.Decode(encodedCert) + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + m.rootCerts.AddCert(cert) + } + } + return nil } -func (self *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { - return "Authenticated with mTLS", nil // TODO: implement +func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { + var cert *x509.Certificate + var err error + + urlEncodedCert := pipeline.GetRequest().Attributes.Source.GetCertificate() + if urlEncodedCert == "" { + return nil, fmt.Errorf("client certificate is missing") + } + pemEncodedCert, err := url.QueryUnescape(urlEncodedCert) + if err != nil { + return nil, fmt.Errorf("invalid client certificate") + } + + block, _ := pem.Decode([]byte(pemEncodedCert)) + + if cert, err = x509.ParseCertificate(block.Bytes); err != nil { + return nil, err + } + + if _, err := cert.Verify(x509.VerifyOptions{Roots: m.rootCerts}); err != nil { + return nil, err + } + + return cert.Subject, nil } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 4332bbb6..3ad12e27 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -3,9 +3,11 @@ package service import ( "crypto/sha256" "encoding/json" + "encoding/pem" "fmt" "io/ioutil" "net/http" + "net/url" "strings" "time" @@ -120,6 +122,20 @@ func (a *AuthService) ServeHTTP(resp http.ResponseWriter, req *http.Request) { }, } + if tls := req.TLS; tls != nil && len(tls.PeerCertificates) > 0 { + pemEncodedCert := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: tls.PeerCertificates[0].Raw, + }) + if pemEncodedCert != nil { + checkRequest.Attributes.Source = &envoy_auth.AttributeContext_Peer{ + Certificate: url.QueryEscape(string(pemEncodedCert)), + } + } else { + logger.V(1).Info("invalid peer certificate") + } + } + if err := context.CheckContext(ctx); err != nil { closeWithStatus(envoy_type.StatusCode_ServiceUnavailable, resp, ctx, nil) return diff --git a/pkg/service/auth_pipeline_test.go b/pkg/service/auth_pipeline_test.go index 4435c44c..59e90a88 100644 --- a/pkg/service/auth_pipeline_test.go +++ b/pkg/service/auth_pipeline_test.go @@ -359,7 +359,7 @@ func TestEvaluatePriorities(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - idConfig1 := &evaluators.IdentityConfig{Priority: 0, MTLS: &identity.MTLS{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it + idConfig1 := &evaluators.IdentityConfig{Priority: 0, Noop: &identity.Noop{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it idConfig2 := &failConfig{priority: 1} // should never be called; otherwise, it would throw an error as it's not a config.IdentityConfig authzConfig1 := &failConfig{priority: 0} @@ -409,7 +409,7 @@ func TestAuthPipelineWithMatchingConditionsInTheAuthConfig(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - idConfig := &evaluators.IdentityConfig{MTLS: &identity.MTLS{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it + idConfig := &evaluators.IdentityConfig{Noop: &identity.Noop{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it authzConfig := &successConfig{} pipeline := newTestAuthPipeline(evaluators.AuthConfig{ @@ -436,7 +436,7 @@ func TestAuthPipelineWithUnmatchingConditionsInTheEvaluator(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - idConfig := &evaluators.IdentityConfig{MTLS: &identity.MTLS{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it + idConfig := &evaluators.IdentityConfig{Noop: &identity.Noop{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it authzConfig := &successConfig{ conditions: []json.JSONPatternMatchingRule{ { @@ -464,7 +464,7 @@ func TestAuthPipelineWithMatchingConditionsInTheEvaluator(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - idConfig := &evaluators.IdentityConfig{MTLS: &identity.MTLS{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it + idConfig := &evaluators.IdentityConfig{Noop: &identity.Noop{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it authzConfig := &successConfig{ conditions: []json.JSONPatternMatchingRule{ { From 463e93bf67a6871a288fc3960f3d02feb9094f39 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 Jun 2022 13:32:40 +0200 Subject: [PATCH 2/7] K8s Secret-based identity interface and controller methods renamed so they are less specific about API keys --- controllers/secret_controller.go | 61 +++++++++++++++------------ controllers/secret_controller_test.go | 12 +++--- pkg/auth/auth.go | 8 ++-- pkg/auth/mocks/mock_auth.go | 58 ++++++++++++------------- pkg/evaluators/identity.go | 12 +++--- pkg/evaluators/identity/api_key.go | 8 ++-- 6 files changed, 83 insertions(+), 76 deletions(-) diff --git a/controllers/secret_controller.go b/controllers/secret_controller.go index 01c0b260..f43ad19c 100644 --- a/controllers/secret_controller.go +++ b/controllers/secret_controller.go @@ -41,8 +41,6 @@ type SecretReconciler struct { func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := r.Logger.WithValues("secret", req.NamespacedName) - var reconcile func(*evaluators.AuthConfig) - secret := v1.Secret{} if err := r.Client.Get(ctx, req.NamespacedName, &secret); err != nil && !errors.IsNotFound(err) { // could not get the resource but not because of a 404 Not found, some error must have happened @@ -50,19 +48,15 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } else if errors.IsNotFound(err) || !Watched(&secret.ObjectMeta, r.LabelSelector) { // could not find the resource (404 Not found, resource must have been deleted) // or the resource is no longer to be watched (labels no longer match) - // => delete the API key from all AuthConfigs - reconcile = func(authConfig *evaluators.AuthConfig) { - r.deleteAPIKey(ctx, authConfig, req.NamespacedName) - } + // => delete the K8s Secret-based identity from all AuthConfigs + r.eachAuthConfigsWithK8sSecretBasedIdentity(ctx, func(authConfig *evaluators.AuthConfig) { + r.revokeK8sSecretBasedIdentity(ctx, authConfig, req.NamespacedName) + }) } else { - // resource found => if the API key labels match, update all AuthConfigs - reconcile = func(authConfig *evaluators.AuthConfig) { - r.updateAPIKey(ctx, authConfig, secret) - } - } - - for authConfig := range r.getAuthConfigsUsingAPIKey(ctx) { - reconcile(authConfig) + // resource found => if the K8s Secret labels match, update all AuthConfigs + r.eachAuthConfigsWithK8sSecretBasedIdentity(ctx, func(authConfig *evaluators.AuthConfig) { + r.refreshK8sSecretBasedIdentity(ctx, authConfig, secret) + }) } logger.Info("resource reconciled") @@ -79,12 +73,18 @@ func (r *SecretReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *SecretReconciler) getAuthConfigsUsingAPIKey(ctx context.Context) authConfigSet { +func (r *SecretReconciler) eachAuthConfigsWithK8sSecretBasedIdentity(ctx context.Context, f func(*evaluators.AuthConfig)) { + for authConfig := range r.getAuthConfigsWithK8sSecretBasedIdentity(ctx) { + f(authConfig) + } +} + +func (r *SecretReconciler) getAuthConfigsWithK8sSecretBasedIdentity(ctx context.Context) authConfigSet { authConfigs := make(authConfigSet) var s struct{} for _, authConfig := range r.Cache.List() { for _, identityEvaluator := range authConfig.IdentityConfigs { - if _, ok := identityEvaluator.(auth.APIKeyIdentityConfigEvaluator); ok { + if _, ok := identityEvaluator.(auth.K8sSecretBasedIdentityConfigEvaluator); ok { authConfigs[authConfig] = s break } @@ -93,25 +93,32 @@ func (r *SecretReconciler) getAuthConfigsUsingAPIKey(ctx context.Context) authCo return authConfigs } -func (r *SecretReconciler) deleteAPIKey(ctx context.Context, authConfig *evaluators.AuthConfig, deleted types.NamespacedName) { +func (r *SecretReconciler) revokeK8sSecretBasedIdentity(ctx context.Context, authConfig *evaluators.AuthConfig, deleted types.NamespacedName) { for _, identityEvaluator := range authConfig.IdentityConfigs { - if ev, ok := identityEvaluator.(auth.APIKeyIdentityConfigEvaluator); ok { - log.FromContext(ctx).V(1).Info("deleting api key from cache", "authconfig", authConfigName(authConfig)) - ev.DeleteAPIKeySecret(ctx, deleted) + if ev, ok := identityEvaluator.(auth.K8sSecretBasedIdentityConfigEvaluator); ok { + log.FromContext(ctx).V(1).Info("deleting k8s secret from cache", "authconfig", authConfigName(authConfig)) + ev.RevokeK8sSecretBasedIdentity(ctx, deleted) } } } -func (r *SecretReconciler) updateAPIKey(ctx context.Context, authConfig *evaluators.AuthConfig, secret v1.Secret) { +func (r *SecretReconciler) refreshK8sSecretBasedIdentity(ctx context.Context, authConfig *evaluators.AuthConfig, secret v1.Secret) { + baseLogger := log.FromContext(ctx).WithValues("authconfig", authConfigName(authConfig)).V(1) for _, identityEvaluator := range authConfig.IdentityConfigs { - if ev, ok := identityEvaluator.(auth.APIKeyIdentityConfigEvaluator); ok { - selector, _ := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: ev.GetAPIKeyLabelSelectors()}) + logger := baseLogger + if logger.Enabled() { + if ev, ok := identityEvaluator.(auth.NamedEvaluator); ok { + logger = baseLogger.WithValues("config", ev.GetName()) + } + } + if ev, ok := identityEvaluator.(auth.K8sSecretBasedIdentityConfigEvaluator); ok { + selector, _ := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: ev.GetK8sSecretLabelSelectors()}) if selector == nil || selector.Matches(labels.Set(secret.Labels)) { - log.FromContext(ctx).V(1).Info("adding api key to cache", "authconfig", authConfigName(authConfig)) - ev.RefreshAPIKeySecret(ctx, secret) + logger.Info("adding k8s secret to cache") + ev.AddK8sSecretBasedIdentity(ctx, secret) } else { - log.FromContext(ctx).V(1).Info("deleting api key from cache", "authconfig", authConfigName(authConfig)) - ev.DeleteAPIKeySecret(ctx, types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name}) + logger.Info("deleting k8s secret from cache") + ev.RevokeK8sSecretBasedIdentity(ctx, types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name}) } } } diff --git a/controllers/secret_controller_test.go b/controllers/secret_controller_test.go index 537dbd40..bb6af16e 100644 --- a/controllers/secret_controller_test.go +++ b/controllers/secret_controller_test.go @@ -47,18 +47,18 @@ func (i *fakeAPIKeyIdentityConfig) Call(_ auth.AuthPipeline, _ context.Context) return nil, nil } -func (i *fakeAPIKeyIdentityConfig) RefreshAPIKeySecret(ctx context.Context, new v1.Secret) { - i.evaluator.RefreshAPIKeySecret(ctx, new) +func (i *fakeAPIKeyIdentityConfig) AddK8sSecretBasedIdentity(ctx context.Context, new v1.Secret) { + i.evaluator.AddK8sSecretBasedIdentity(ctx, new) i.refreshed = true } -func (i *fakeAPIKeyIdentityConfig) DeleteAPIKeySecret(ctx context.Context, deleted types.NamespacedName) { - i.evaluator.DeleteAPIKeySecret(ctx, deleted) +func (i *fakeAPIKeyIdentityConfig) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted types.NamespacedName) { + i.evaluator.RevokeK8sSecretBasedIdentity(ctx, deleted) i.deleted = true } -func (i *fakeAPIKeyIdentityConfig) GetAPIKeyLabelSelectors() map[string]string { - return i.evaluator.GetAPIKeyLabelSelectors() +func (i *fakeAPIKeyIdentityConfig) GetK8sSecretLabelSelectors() map[string]string { + return i.evaluator.GetK8sSecretLabelSelectors() } type secretReconcilerTest struct { diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 573ee867..a4d3318c 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -53,10 +53,10 @@ type IdentityConfigEvaluator interface { ResolveExtendedProperties(AuthPipeline) (interface{}, error) } -type APIKeyIdentityConfigEvaluator interface { - RefreshAPIKeySecret(context.Context, v1.Secret) - DeleteAPIKeySecret(context.Context, types.NamespacedName) - GetAPIKeyLabelSelectors() map[string]string +type K8sSecretBasedIdentityConfigEvaluator interface { + GetK8sSecretLabelSelectors() map[string]string + AddK8sSecretBasedIdentity(context.Context, v1.Secret) + RevokeK8sSecretBasedIdentity(context.Context, types.NamespacedName) } type WristbandIssuer interface { diff --git a/pkg/auth/mocks/mock_auth.go b/pkg/auth/mocks/mock_auth.go index 32c0c118..a3357bfb 100644 --- a/pkg/auth/mocks/mock_auth.go +++ b/pkg/auth/mocks/mock_auth.go @@ -413,65 +413,65 @@ func (mr *MockIdentityConfigEvaluatorMockRecorder) ResolveExtendedProperties(arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveExtendedProperties", reflect.TypeOf((*MockIdentityConfigEvaluator)(nil).ResolveExtendedProperties), arg0) } -// MockAPIKeyIdentityConfigEvaluator is a mock of APIKeyIdentityConfigEvaluator interface. -type MockAPIKeyIdentityConfigEvaluator struct { +// MockK8sSecretBasedIdentityConfigEvaluator is a mock of K8sSecretBasedIdentityConfigEvaluator interface. +type MockK8sSecretBasedIdentityConfigEvaluator struct { ctrl *gomock.Controller - recorder *MockAPIKeyIdentityConfigEvaluatorMockRecorder + recorder *MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder } -// MockAPIKeyIdentityConfigEvaluatorMockRecorder is the mock recorder for MockAPIKeyIdentityConfigEvaluator. -type MockAPIKeyIdentityConfigEvaluatorMockRecorder struct { - mock *MockAPIKeyIdentityConfigEvaluator +// MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder is the mock recorder for MockK8sSecretBasedIdentityConfigEvaluator. +type MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder struct { + mock *MockK8sSecretBasedIdentityConfigEvaluator } -// NewMockAPIKeyIdentityConfigEvaluator creates a new mock instance. -func NewMockAPIKeyIdentityConfigEvaluator(ctrl *gomock.Controller) *MockAPIKeyIdentityConfigEvaluator { - mock := &MockAPIKeyIdentityConfigEvaluator{ctrl: ctrl} - mock.recorder = &MockAPIKeyIdentityConfigEvaluatorMockRecorder{mock} +// NewMockK8sSecretBasedIdentityConfigEvaluator creates a new mock instance. +func NewMockK8sSecretBasedIdentityConfigEvaluator(ctrl *gomock.Controller) *MockK8sSecretBasedIdentityConfigEvaluator { + mock := &MockK8sSecretBasedIdentityConfigEvaluator{ctrl: ctrl} + mock.recorder = &MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAPIKeyIdentityConfigEvaluator) EXPECT() *MockAPIKeyIdentityConfigEvaluatorMockRecorder { +func (m *MockK8sSecretBasedIdentityConfigEvaluator) EXPECT() *MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder { return m.recorder } -// DeleteAPIKeySecret mocks base method. -func (m *MockAPIKeyIdentityConfigEvaluator) DeleteAPIKeySecret(arg0 context.Context, arg1 types.NamespacedName) { +// AddK8sSecretBasedIdentity mocks base method. +func (m *MockK8sSecretBasedIdentityConfigEvaluator) AddK8sSecretBasedIdentity(arg0 context.Context, arg1 v1.Secret) { m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteAPIKeySecret", arg0, arg1) + m.ctrl.Call(m, "AddK8sSecretBasedIdentity", arg0, arg1) } -// DeleteAPIKeySecret indicates an expected call of DeleteAPIKeySecret. -func (mr *MockAPIKeyIdentityConfigEvaluatorMockRecorder) DeleteAPIKeySecret(arg0, arg1 interface{}) *gomock.Call { +// AddK8sSecretBasedIdentity indicates an expected call of AddK8sSecretBasedIdentity. +func (mr *MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder) AddK8sSecretBasedIdentity(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPIKeySecret", reflect.TypeOf((*MockAPIKeyIdentityConfigEvaluator)(nil).DeleteAPIKeySecret), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddK8sSecretBasedIdentity", reflect.TypeOf((*MockK8sSecretBasedIdentityConfigEvaluator)(nil).AddK8sSecretBasedIdentity), arg0, arg1) } -// GetAPIKeyLabelSelectors mocks base method. -func (m *MockAPIKeyIdentityConfigEvaluator) GetAPIKeyLabelSelectors() map[string]string { +// GetK8sSecretLabelSelectors mocks base method. +func (m *MockK8sSecretBasedIdentityConfigEvaluator) GetK8sSecretLabelSelectors() map[string]string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAPIKeyLabelSelectors") + ret := m.ctrl.Call(m, "GetK8sSecretLabelSelectors") ret0, _ := ret[0].(map[string]string) return ret0 } -// GetAPIKeyLabelSelectors indicates an expected call of GetAPIKeyLabelSelectors. -func (mr *MockAPIKeyIdentityConfigEvaluatorMockRecorder) GetAPIKeyLabelSelectors() *gomock.Call { +// GetK8sSecretLabelSelectors indicates an expected call of GetK8sSecretLabelSelectors. +func (mr *MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder) GetK8sSecretLabelSelectors() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeyLabelSelectors", reflect.TypeOf((*MockAPIKeyIdentityConfigEvaluator)(nil).GetAPIKeyLabelSelectors)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetK8sSecretLabelSelectors", reflect.TypeOf((*MockK8sSecretBasedIdentityConfigEvaluator)(nil).GetK8sSecretLabelSelectors)) } -// RefreshAPIKeySecret mocks base method. -func (m *MockAPIKeyIdentityConfigEvaluator) RefreshAPIKeySecret(arg0 context.Context, arg1 v1.Secret) { +// RevokeK8sSecretBasedIdentity mocks base method. +func (m *MockK8sSecretBasedIdentityConfigEvaluator) RevokeK8sSecretBasedIdentity(arg0 context.Context, arg1 types.NamespacedName) { m.ctrl.T.Helper() - m.ctrl.Call(m, "RefreshAPIKeySecret", arg0, arg1) + m.ctrl.Call(m, "RevokeK8sSecretBasedIdentity", arg0, arg1) } -// RefreshAPIKeySecret indicates an expected call of RefreshAPIKeySecret. -func (mr *MockAPIKeyIdentityConfigEvaluatorMockRecorder) RefreshAPIKeySecret(arg0, arg1 interface{}) *gomock.Call { +// RevokeK8sSecretBasedIdentity indicates an expected call of RevokeK8sSecretBasedIdentity. +func (mr *MockK8sSecretBasedIdentityConfigEvaluatorMockRecorder) RevokeK8sSecretBasedIdentity(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshAPIKeySecret", reflect.TypeOf((*MockAPIKeyIdentityConfigEvaluator)(nil).RefreshAPIKeySecret), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeK8sSecretBasedIdentity", reflect.TypeOf((*MockK8sSecretBasedIdentityConfigEvaluator)(nil).RevokeK8sSecretBasedIdentity), arg0, arg1) } // MockWristbandIssuer is a mock of WristbandIssuer interface. diff --git a/pkg/evaluators/identity.go b/pkg/evaluators/identity.go index 7d9b64da..b9094785 100644 --- a/pkg/evaluators/identity.go +++ b/pkg/evaluators/identity.go @@ -202,23 +202,23 @@ func (config *IdentityConfig) ResolveExtendedProperties(pipeline auth.AuthPipeli return extendedIdentityObject, nil } -// impl:APIKeyIdentityConfigEvaluator +// impl:K8sSecretBasedIdentityConfigEvaluator -func (config *IdentityConfig) RefreshAPIKeySecret(ctx context.Context, new v1.Secret) { +func (config *IdentityConfig) AddK8sSecretBasedIdentity(ctx context.Context, new v1.Secret) { if config.APIKey == nil { return } - config.APIKey.RefreshAPIKeySecret(ctx, new) + config.APIKey.AddK8sSecretBasedIdentity(ctx, new) } -func (config *IdentityConfig) DeleteAPIKeySecret(ctx context.Context, deleted types.NamespacedName) { +func (config *IdentityConfig) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted types.NamespacedName) { if config.APIKey == nil { return } - config.APIKey.DeleteAPIKeySecret(ctx, deleted) + config.APIKey.RevokeK8sSecretBasedIdentity(ctx, deleted) } -func (config *IdentityConfig) GetAPIKeyLabelSelectors() map[string]string { +func (config *IdentityConfig) GetK8sSecretLabelSelectors() map[string]string { if config.APIKey == nil { return nil } diff --git a/pkg/evaluators/identity/api_key.go b/pkg/evaluators/identity/api_key.go index 14e144b5..dde5b2b5 100644 --- a/pkg/evaluators/identity/api_key.go +++ b/pkg/evaluators/identity/api_key.go @@ -79,13 +79,13 @@ func (apiKey *APIKey) Call(pipeline auth.AuthPipeline, _ context.Context) (inter return nil, err } -// impl:APIKeyIdentityConfigEvaluator +// impl:K8sSecretBasedIdentityConfigEvaluator -func (apiKey *APIKey) GetAPIKeyLabelSelectors() map[string]string { +func (apiKey *APIKey) GetK8sSecretLabelSelectors() map[string]string { return apiKey.LabelSelectors } -func (apiKey *APIKey) RefreshAPIKeySecret(ctx context.Context, new v1.Secret) { +func (apiKey *APIKey) AddK8sSecretBasedIdentity(ctx context.Context, new v1.Secret) { if !apiKey.withinScope(new.GetNamespace()) { return } @@ -117,7 +117,7 @@ func (apiKey *APIKey) RefreshAPIKeySecret(ctx context.Context, new v1.Secret) { logger.V(1).Info("api key added", "authconfig", newAIKeyName) } -func (apiKey *APIKey) DeleteAPIKeySecret(ctx context.Context, deleted types.NamespacedName) { +func (apiKey *APIKey) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted types.NamespacedName) { if !apiKey.withinScope(deleted.Namespace) { return } From 0458d9135c6f7c1bba7713e2b9b38f3a9f2b16d7 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 Jun 2022 19:43:51 +0200 Subject: [PATCH 3/7] Refactor API key secret identity evaluator - Modified names of variables, pointers, imports - to have a better base for the implementation of the mTLS identity evaluator with propeor reconciliation of k8s secrets - Fixed label of log values for the name of secret --- pkg/evaluators/identity/api_key.go | 106 ++++++++++++++++------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/pkg/evaluators/identity/api_key.go b/pkg/evaluators/identity/api_key.go index dde5b2b5..68ba4d5f 100644 --- a/pkg/evaluators/identity/api_key.go +++ b/pkg/evaluators/identity/api_key.go @@ -8,9 +8,9 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/log" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" + k8s "k8s.io/api/core/v1" + k8s_types "k8s.io/apimachinery/pkg/types" + k8s_client "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -26,18 +26,18 @@ type APIKey struct { LabelSelectors map[string]string `yaml:"labelSelectors"` Namespace string `yaml:"namespace"` - secrets map[string]v1.Secret + secrets map[string]k8s.Secret mutex sync.Mutex - k8sClient client.Reader + k8sClient k8s_client.Reader } -// NewApiKeyIdentity creates a new instance of APIKey -func NewApiKeyIdentity(name string, labelSelectors map[string]string, namespace string, authCred auth.AuthCredentials, k8sClient client.Reader, ctx context.Context) *APIKey { +func NewApiKeyIdentity(name string, labelSelectors map[string]string, namespace string, authCred auth.AuthCredentials, k8sClient k8s_client.Reader, ctx context.Context) *APIKey { apiKey := &APIKey{ AuthCredentials: authCred, Name: name, LabelSelectors: labelSelectors, Namespace: namespace, + secrets: make(map[string]k8s.Secret), k8sClient: k8sClient, } if err := apiKey.loadSecrets(context.TODO()); err != nil { @@ -46,30 +46,33 @@ func NewApiKeyIdentity(name string, labelSelectors map[string]string, namespace return apiKey } -// loadSecrets will get the k8s secrets and update the APIKey instance -func (apiKey *APIKey) loadSecrets(ctx context.Context) error { - opts := []client.ListOption{client.MatchingLabels(apiKey.LabelSelectors)} - if namespace := apiKey.Namespace; namespace != "" { - opts = append(opts, client.InNamespace(namespace)) +// loadSecrets will load the matching k8s secrets from the cluster to the cache of trusted API keys +func (a *APIKey) loadSecrets(ctx context.Context) error { + opts := []k8s_client.ListOption{k8s_client.MatchingLabels(a.LabelSelectors)} + if namespace := a.Namespace; namespace != "" { + opts = append(opts, k8s_client.InNamespace(namespace)) } - var secretList = &v1.SecretList{} - if err := apiKey.k8sClient.List(ctx, secretList, opts...); err != nil { + var secretList = &k8s.SecretList{} + if err := a.k8sClient.List(ctx, secretList, opts...); err != nil { return err } - var secrets = make(map[string]v1.Secret) + + a.mutex.Lock() + defer a.mutex.Unlock() + for _, secret := range secretList.Items { - secrets[string(secret.Data[apiKeySelector])] = secret + a.appendK8sSecretBasedIdentity(secret) } - apiKey.secrets = secrets + return nil } // Call will evaluate the credentials within the request against the authorized ones -func (apiKey *APIKey) Call(pipeline auth.AuthPipeline, _ context.Context) (interface{}, error) { - if reqKey, err := apiKey.GetCredentialsFromReq(pipeline.GetHttp()); err != nil { +func (a *APIKey) Call(pipeline auth.AuthPipeline, _ context.Context) (interface{}, error) { + if reqKey, err := a.GetCredentialsFromReq(pipeline.GetHttp()); err != nil { return nil, err } else { - for key, secret := range apiKey.secrets { + for key, secret := range a.secrets { if key == reqKey { return secret, nil } @@ -81,59 +84,68 @@ func (apiKey *APIKey) Call(pipeline auth.AuthPipeline, _ context.Context) (inter // impl:K8sSecretBasedIdentityConfigEvaluator -func (apiKey *APIKey) GetK8sSecretLabelSelectors() map[string]string { - return apiKey.LabelSelectors +func (a *APIKey) GetK8sSecretLabelSelectors() map[string]string { + return a.LabelSelectors } -func (apiKey *APIKey) AddK8sSecretBasedIdentity(ctx context.Context, new v1.Secret) { - if !apiKey.withinScope(new.GetNamespace()) { +func (a *APIKey) AddK8sSecretBasedIdentity(ctx context.Context, new k8s.Secret) { + if !a.withinScope(new.GetNamespace()) { return } - logger := log.FromContext(ctx).WithName("apikey") - - apiKey.mutex.Lock() - defer apiKey.mutex.Unlock() + a.mutex.Lock() + defer a.mutex.Unlock() - newAPIKeyValue := string(new.Data[apiKeySelector]) - newAIKeyName := fmt.Sprintf("%s/%s", new.GetNamespace(), new.GetName()) + logger := log.FromContext(ctx).WithName("apikey") // updating existing - for _, current := range apiKey.secrets { + newAPIKeyValue := string(new.Data[apiKeySelector]) + for oldAPIKeyValue, current := range a.secrets { if current.GetNamespace() == new.GetNamespace() && current.GetName() == new.GetName() { - oldAPIKeyValue := string(current.Data[apiKeySelector]) if oldAPIKeyValue != newAPIKeyValue { - apiKey.secrets[newAPIKeyValue] = new - delete(apiKey.secrets, oldAPIKeyValue) - logger.V(1).Info("api key updated", "authconfig", newAIKeyName) + a.appendK8sSecretBasedIdentity(new) + delete(a.secrets, oldAPIKeyValue) + logger.V(1).Info("api key updated") } else { - logger.V(1).Info("api key unchanged", "authconfig", newAIKeyName) + logger.V(1).Info("api key unchanged") } return } } - apiKey.secrets[newAPIKeyValue] = new - logger.V(1).Info("api key added", "authconfig", newAIKeyName) + if a.appendK8sSecretBasedIdentity(new) { + logger.V(1).Info("api key added") + } } -func (apiKey *APIKey) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted types.NamespacedName) { - if !apiKey.withinScope(deleted.Namespace) { +func (a *APIKey) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted k8s_types.NamespacedName) { + if !a.withinScope(deleted.Namespace) { return } - apiKey.mutex.Lock() - defer apiKey.mutex.Unlock() + a.mutex.Lock() + defer a.mutex.Unlock() - for key, secret := range apiKey.secrets { + for key, secret := range a.secrets { if secret.GetNamespace() == deleted.Namespace && secret.GetName() == deleted.Name { - delete(apiKey.secrets, key) - log.FromContext(ctx).WithName("apikey").V(1).Info("api key deleted", "authconfig", fmt.Sprintf("%s/%s", deleted.Namespace, deleted.Name)) + delete(a.secrets, key) + log.FromContext(ctx).WithName("apikey").V(1).Info("api key deleted") return } } } -func (apiKey *APIKey) withinScope(namespace string) bool { - return apiKey.Namespace == "" || apiKey.Namespace == namespace +func (a *APIKey) withinScope(namespace string) bool { + return a.Namespace == "" || a.Namespace == namespace +} + +// Appends the K8s Secret to the cache of API keys +// Caution! This function is not thread-safe. Make sure to acquire a lock before calling it. +func (a *APIKey) appendK8sSecretBasedIdentity(secret k8s.Secret) bool { + value, isAPIKeySecret := secret.Data[apiKeySelector] + if isAPIKeySecret && len(value) > 0 { + a.secrets[string(value)] = secret + return true + } + return false } From d48785ee75a9122807796a9ecd0f657790b282de Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 Jun 2022 19:48:18 +0200 Subject: [PATCH 4/7] Reconciliation of trusted mTLS root CA cert k8s secrets Refreshes the entire list of cached root CA certs by reloading them all again from the cluster at any operation (add, update, delete secret). This is because `x509.CertPool` does not provide a good interface for deleting and updating the list of certificates in the pool, despite its convinient `.AppendCertsFromPEM` function. To allow better management of the cache (i.e., addition, update and deletion of individual k8s secrets to avoid reloading all secrets, using the qualified name `/` of the resource as key), another data structure to store the trusted root CA certs must replace the type of `rootCerts: *x509.CertPool`. --- pkg/evaluators/identity.go | 36 ++++++++++--- pkg/evaluators/identity/mtls.go | 93 ++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/pkg/evaluators/identity.go b/pkg/evaluators/identity.go index b9094785..67c1ed08 100644 --- a/pkg/evaluators/identity.go +++ b/pkg/evaluators/identity.go @@ -205,24 +205,48 @@ func (config *IdentityConfig) ResolveExtendedProperties(pipeline auth.AuthPipeli // impl:K8sSecretBasedIdentityConfigEvaluator func (config *IdentityConfig) AddK8sSecretBasedIdentity(ctx context.Context, new v1.Secret) { - if config.APIKey == nil { + var ev auth.K8sSecretBasedIdentityConfigEvaluator + + switch config.GetType() { + case identityMTLS: + ev = config.MTLS + case identityAPIKey: + ev = config.APIKey + default: return } - config.APIKey.AddK8sSecretBasedIdentity(ctx, new) + + ev.AddK8sSecretBasedIdentity(ctx, new) } func (config *IdentityConfig) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted types.NamespacedName) { - if config.APIKey == nil { + var ev auth.K8sSecretBasedIdentityConfigEvaluator + + switch config.GetType() { + case identityMTLS: + ev = config.MTLS + case identityAPIKey: + ev = config.APIKey + default: return } - config.APIKey.RevokeK8sSecretBasedIdentity(ctx, deleted) + + ev.RevokeK8sSecretBasedIdentity(ctx, deleted) } func (config *IdentityConfig) GetK8sSecretLabelSelectors() map[string]string { - if config.APIKey == nil { + var ev auth.K8sSecretBasedIdentityConfigEvaluator + + switch config.GetType() { + case identityMTLS: + ev = config.MTLS + case identityAPIKey: + ev = config.APIKey + default: return nil } - return config.APIKey.LabelSelectors + + return ev.GetK8sSecretLabelSelectors() } // impl:metrics.Object diff --git a/pkg/evaluators/identity/mtls.go b/pkg/evaluators/identity/mtls.go index a96e21d0..8f05549d 100644 --- a/pkg/evaluators/identity/mtls.go +++ b/pkg/evaluators/identity/mtls.go @@ -6,12 +6,15 @@ import ( "encoding/pem" "fmt" "net/url" + "sync" "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/log" k8s "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" + v1 "k8s.io/api/core/v1" + k8s_types "k8s.io/apimachinery/pkg/types" + k8s_client "sigs.k8s.io/controller-runtime/pkg/client" ) type MTLS struct { @@ -22,15 +25,17 @@ type MTLS struct { Namespace string rootCerts *x509.CertPool - k8sClient client.Reader + mutex sync.Mutex + k8sClient k8s_client.Reader } -func NewMTLSIdentity(name string, labelSelectors map[string]string, namespace string, k8sClient client.Reader, ctx context.Context) *MTLS { +func NewMTLSIdentity(name string, labelSelectors map[string]string, namespace string, k8sClient k8s_client.Reader, ctx context.Context) *MTLS { mtls := &MTLS{ AuthCredentials: &auth.AuthCredential{KeySelector: "Basic"}, Name: name, LabelSelectors: labelSelectors, Namespace: namespace, + rootCerts: x509.NewCertPool(), k8sClient: k8sClient, } if err := mtls.loadSecrets(context.TODO()); err != nil { @@ -39,31 +44,24 @@ func NewMTLSIdentity(name string, labelSelectors map[string]string, namespace st return mtls } -// loadSecrets will get the k8s secrets and update the APIKey instance +// loadSecrets will load the matching k8s secrets from the cluster to the cache of trusted root CAs func (m *MTLS) loadSecrets(ctx context.Context) error { - opts := []client.ListOption{client.MatchingLabels(m.LabelSelectors)} + opts := []k8s_client.ListOption{k8s_client.MatchingLabels(m.LabelSelectors)} if namespace := m.Namespace; namespace != "" { - opts = append(opts, client.InNamespace(namespace)) + opts = append(opts, k8s_client.InNamespace(namespace)) } var secretList = &k8s.SecretList{} if err := m.k8sClient.List(ctx, secretList, opts...); err != nil { return err } - m.rootCerts = x509.NewCertPool() + + m.mutex.Lock() + defer m.mutex.Unlock() + for _, secret := range secretList.Items { - var encodedCert []byte - if v, foundKey := secret.Data[k8s.TLSCertKey]; foundKey { - encodedCert = v - } else if v, foundKey := secret.Data[k8s.ServiceAccountRootCAKey]; foundKey { - encodedCert = v - } else { - continue - } - block, _ := pem.Decode(encodedCert) - if cert, err := x509.ParseCertificate(block.Bytes); err == nil { - m.rootCerts.AddCert(cert) - } + m.appendK8sSecretBasedIdentity(secret) } + return nil } @@ -92,3 +90,60 @@ func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{ return cert.Subject, nil } + +// impl:K8sSecretBasedIdentityConfigEvaluator + +func (m *MTLS) GetK8sSecretLabelSelectors() map[string]string { + return m.LabelSelectors +} + +// AddK8sSecretBasedIdentity refreshes the cache of trusted root CA certs by reloading the k8s secrets from the cluster +func (m *MTLS) AddK8sSecretBasedIdentity(ctx context.Context, new k8s.Secret) { + m.refreshK8sSecretBasedIdentity(ctx, k8s_types.NamespacedName{Namespace: new.Namespace, Name: new.Name}) +} + +// RevokeK8sSecretBasedIdentity refreshes the cache of trusted root CA certs by reloading the k8s secrets from the cluster +func (m *MTLS) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted k8s_types.NamespacedName) { + m.refreshK8sSecretBasedIdentity(ctx, deleted) +} + +func (m *MTLS) withinScope(namespace string) bool { + return m.Namespace == "" || m.Namespace == namespace +} + +// Appends the K8s Secret to the cache of trusted root CAs +// Caution! This function is not thread-safe. Make sure to acquire a lock before calling it. +func (m *MTLS) appendK8sSecretBasedIdentity(secret v1.Secret) bool { + var encodedCert []byte + + if v, hasTLSCert := secret.Data[k8s.TLSCertKey]; hasTLSCert { + encodedCert = v + } else if v, hasCACert := secret.Data[k8s.ServiceAccountRootCAKey]; hasCACert { + encodedCert = v + } else { + return false + } + + return m.rootCerts.AppendCertsFromPEM(encodedCert) +} + +func (m *MTLS) refreshK8sSecretBasedIdentity(ctx context.Context, secret k8s_types.NamespacedName) { + if !m.withinScope(secret.Namespace) { + return + } + + logger := log.FromContext(ctx).WithName("mtls").WithValues("secret", secret.String()) + + current := m.rootCerts + m.rootCerts = x509.NewCertPool() + if err := m.loadSecrets(ctx); err != nil { + logger.Error(err, "failed to refresh trusted root ca certs") + // rollback + m.mutex.Lock() + defer m.mutex.Unlock() + m.rootCerts = current + return + } + + logger.V(1).Info("trusted root ca cert refreshed") +} From 1b391d04cc125ead241309659061ec397c18184d Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 Jun 2022 22:01:09 +0200 Subject: [PATCH 5/7] Reconciliation of individual mTLS rot CA cert secrets within an AuthConfig --- pkg/evaluators/identity/mtls.go | 101 +++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 33 deletions(-) diff --git a/pkg/evaluators/identity/mtls.go b/pkg/evaluators/identity/mtls.go index 8f05549d..05c6e55e 100644 --- a/pkg/evaluators/identity/mtls.go +++ b/pkg/evaluators/identity/mtls.go @@ -2,6 +2,7 @@ package identity import ( "context" + "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" @@ -12,7 +13,6 @@ import ( "github.com/kuadrant/authorino/pkg/log" k8s "k8s.io/api/core/v1" - v1 "k8s.io/api/core/v1" k8s_types "k8s.io/apimachinery/pkg/types" k8s_client "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -24,7 +24,7 @@ type MTLS struct { LabelSelectors map[string]string Namespace string - rootCerts *x509.CertPool + rootCerts map[string]*x509.Certificate mutex sync.Mutex k8sClient k8s_client.Reader } @@ -35,7 +35,7 @@ func NewMTLSIdentity(name string, labelSelectors map[string]string, namespace st Name: name, LabelSelectors: labelSelectors, Namespace: namespace, - rootCerts: x509.NewCertPool(), + rootCerts: make(map[string]*x509.Certificate), k8sClient: k8sClient, } if err := mtls.loadSecrets(context.TODO()); err != nil { @@ -59,7 +59,10 @@ func (m *MTLS) loadSecrets(ctx context.Context) error { defer m.mutex.Unlock() for _, secret := range secretList.Items { - m.appendK8sSecretBasedIdentity(secret) + secretName := k8s_types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name} + if cert := decodeCertificate(secret); cert != nil { + m.rootCerts[secretName.String()] = cert + } } return nil @@ -84,7 +87,12 @@ func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{ return nil, err } - if _, err := cert.Verify(x509.VerifyOptions{Roots: m.rootCerts}); err != nil { + certs := x509.NewCertPool() + for _, cert := range m.rootCerts { + certs.AddCert(cert) + } + + if _, err := cert.Verify(x509.VerifyOptions{Roots: certs}); err != nil { return nil, err } @@ -97,23 +105,55 @@ func (m *MTLS) GetK8sSecretLabelSelectors() map[string]string { return m.LabelSelectors } -// AddK8sSecretBasedIdentity refreshes the cache of trusted root CA certs by reloading the k8s secrets from the cluster func (m *MTLS) AddK8sSecretBasedIdentity(ctx context.Context, new k8s.Secret) { - m.refreshK8sSecretBasedIdentity(ctx, k8s_types.NamespacedName{Namespace: new.Namespace, Name: new.Name}) + if !m.withinScope(new.GetNamespace()) { + return + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + secretName := k8s_types.NamespacedName{Namespace: new.Namespace, Name: new.Name}.String() + newCert := decodeCertificate(new) + logger := log.FromContext(ctx).WithName("mtls") + + // updating existing + if currentCert, found := m.rootCerts[secretName]; found { + logger := log.FromContext(ctx).WithName("mtls") + if sha256.Sum224(currentCert.Raw) != sha256.Sum224(newCert.Raw) { + m.rootCerts[secretName] = newCert + logger.V(1).Info("trusted root ca updated") + } else { + logger.V(1).Info("trusted root ca unchanged") + } + return + } + + m.rootCerts[secretName] = newCert + logger.V(1).Info("trusted root ca added") } -// RevokeK8sSecretBasedIdentity refreshes the cache of trusted root CA certs by reloading the k8s secrets from the cluster func (m *MTLS) RevokeK8sSecretBasedIdentity(ctx context.Context, deleted k8s_types.NamespacedName) { - m.refreshK8sSecretBasedIdentity(ctx, deleted) + if !m.withinScope(deleted.Namespace) { + return + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + secretName := deleted.String() + + if _, found := m.rootCerts[secretName]; found { + delete(m.rootCerts, secretName) + log.FromContext(ctx).WithName("mtls").V(1).Info("trusted root ca deleted") + } } func (m *MTLS) withinScope(namespace string) bool { return m.Namespace == "" || m.Namespace == namespace } -// Appends the K8s Secret to the cache of trusted root CAs -// Caution! This function is not thread-safe. Make sure to acquire a lock before calling it. -func (m *MTLS) appendK8sSecretBasedIdentity(secret v1.Secret) bool { +func decodeCertificate(secret k8s.Secret) (cert *x509.Certificate) { var encodedCert []byte if v, hasTLSCert := secret.Data[k8s.TLSCertKey]; hasTLSCert { @@ -121,29 +161,24 @@ func (m *MTLS) appendK8sSecretBasedIdentity(secret v1.Secret) bool { } else if v, hasCACert := secret.Data[k8s.ServiceAccountRootCAKey]; hasCACert { encodedCert = v } else { - return false + return nil } - return m.rootCerts.AppendCertsFromPEM(encodedCert) -} - -func (m *MTLS) refreshK8sSecretBasedIdentity(ctx context.Context, secret k8s_types.NamespacedName) { - if !m.withinScope(secret.Namespace) { - return - } - - logger := log.FromContext(ctx).WithName("mtls").WithValues("secret", secret.String()) - - current := m.rootCerts - m.rootCerts = x509.NewCertPool() - if err := m.loadSecrets(ctx); err != nil { - logger.Error(err, "failed to refresh trusted root ca certs") - // rollback - m.mutex.Lock() - defer m.mutex.Unlock() - m.rootCerts = current - return + for len(encodedCert) > 0 { + var block *pem.Block + block, encodedCert = pem.Decode(encodedCert) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + var err error + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + continue + } } - logger.V(1).Info("trusted root ca cert refreshed") + return cert } From 8a8c676717120958e810328738d5fd573cea02b9 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Sun, 12 Jun 2022 12:00:25 +0200 Subject: [PATCH 6/7] Unit tests mTLS authn method --- pkg/evaluators/identity/api_key_test.go | 73 +++----- pkg/evaluators/identity/mtls.go | 27 +-- pkg/evaluators/identity/mtls_test.go | 239 ++++++++++++++++++++++++ pkg/evaluators/identity/test_mocks.go | 37 ++++ 4 files changed, 314 insertions(+), 62 deletions(-) create mode 100644 pkg/evaluators/identity/mtls_test.go create mode 100644 pkg/evaluators/identity/test_mocks.go diff --git a/pkg/evaluators/identity/api_key_test.go b/pkg/evaluators/identity/api_key_test.go index 9f45fc02..7b0ac758 100644 --- a/pkg/evaluators/identity/api_key_test.go +++ b/pkg/evaluators/identity/api_key_test.go @@ -7,45 +7,30 @@ import ( mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" - . "github.com/golang/mock/gomock" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" + k8s "k8s.io/api/core/v1" + k8s_meta "k8s.io/apimachinery/pkg/apis/meta/v1" + gomock "github.com/golang/mock/gomock" "gotest.tools/assert" ) var ( - apiKeySecret1 = &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "obi-wan", Namespace: "ns1", Labels: map[string]string{"planet": "coruscant"}}, Data: map[string][]byte{"api_key": []byte("ObiWanKenobiLightSaber")}} - apiKeySecret2 = &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "yoda", Namespace: "ns2", Labels: map[string]string{"planet": "coruscant"}}, Data: map[string][]byte{"api_key": []byte("MasterYodaLightSaber")}} - apiKeySecret3 = &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "anakin", Namespace: "ns2", Labels: map[string]string{"planet": "tatooine"}}, Data: map[string][]byte{"api_key": []byte("AnakinSkywalkerLightSaber")}} - k8sClient = mockAPIkeyK8sClient(apiKeySecret1, apiKeySecret2, apiKeySecret3) + testAPIKeyK8sSecret1 = &k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "obi-wan", Namespace: "ns1", Labels: map[string]string{"planet": "coruscant"}}, Data: map[string][]byte{"api_key": []byte("ObiWanKenobiLightSaber")}} + testAPIKeyK8sSecret2 = &k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "yoda", Namespace: "ns2", Labels: map[string]string{"planet": "coruscant"}}, Data: map[string][]byte{"api_key": []byte("MasterYodaLightSaber")}} + testAPIKeyK8sSecret3 = &k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "anakin", Namespace: "ns2", Labels: map[string]string{"planet": "tatooine"}}, Data: map[string][]byte{"api_key": []byte("AnakinSkywalkerLightSaber")}} + testAPIKeyK8sClient = mockK8sClient(testAPIKeyK8sSecret1, testAPIKeyK8sSecret2, testAPIKeyK8sSecret3) ) -func mockAPIkeyK8sClient(initObjs ...runtime.Object) client.WithWatch { - scheme := runtime.NewScheme() - _ = v1.AddToScheme(scheme) - return fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjs...).Build() -} - -func mockAuthPipeline(ctrl *Controller) (pipelineMock *mock_auth.MockAuthPipeline) { - pipelineMock = mock_auth.NewMockAuthPipeline(ctrl) - pipelineMock.EXPECT().GetHttp().Return(nil) - return -} - func TestConstants(t *testing.T) { assert.Equal(t, apiKeySelector, "api_key") assert.Equal(t, invalidApiKeyMsg, "the API Key provided is invalid") } func TestNewApiKeyIdentityAllNamespaces(t *testing.T) { - ctrl := NewController(t) + ctrl := gomock.NewController(t) defer ctrl.Finish() - apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", mock_auth.NewMockAuthCredentials(ctrl), k8sClient, context.TODO()) + apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO()) assert.Equal(t, apiKey.Name, "jedi") assert.Equal(t, apiKey.LabelSelectors["planet"], "coruscant") @@ -60,10 +45,10 @@ func TestNewApiKeyIdentityAllNamespaces(t *testing.T) { } func TestNewApiKeyIdentitySingleNamespace(t *testing.T) { - ctrl := NewController(t) + ctrl := gomock.NewController(t) defer ctrl.Finish() - apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "ns1", mock_auth.NewMockAuthCredentials(ctrl), k8sClient, context.TODO()) + apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "ns1", mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO()) assert.Equal(t, apiKey.Name, "jedi") assert.Equal(t, apiKey.LabelSelectors["planet"], "coruscant") @@ -78,29 +63,29 @@ func TestNewApiKeyIdentitySingleNamespace(t *testing.T) { } func TestCallSuccess(t *testing.T) { - ctrl := NewController(t) + ctrl := gomock.NewController(t) defer ctrl.Finish() pipelineMock := mockAuthPipeline(ctrl) authCredMock := mock_auth.NewMockAuthCredentials(ctrl) - authCredMock.EXPECT().GetCredentialsFromReq(Any()).Return("ObiWanKenobiLightSaber", nil) + authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("ObiWanKenobiLightSaber", nil) - apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", authCredMock, k8sClient, context.TODO()) + apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", authCredMock, testAPIKeyK8sClient, context.TODO()) auth, err := apiKey.Call(pipelineMock, context.TODO()) assert.NilError(t, err) - assert.Equal(t, string(auth.(v1.Secret).Data["api_key"]), "ObiWanKenobiLightSaber") + assert.Equal(t, string(auth.(k8s.Secret).Data["api_key"]), "ObiWanKenobiLightSaber") } func TestCallNoApiKeyFail(t *testing.T) { - ctrl := NewController(t) + ctrl := gomock.NewController(t) defer ctrl.Finish() pipelineMock := mockAuthPipeline(ctrl) authCredMock := mock_auth.NewMockAuthCredentials(ctrl) - authCredMock.EXPECT().GetCredentialsFromReq(Any()).Return("", fmt.Errorf("something went wrong getting the API Key")) + authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("", fmt.Errorf("something went wrong getting the API Key")) - apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", authCredMock, k8sClient, context.TODO()) + apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", authCredMock, testAPIKeyK8sClient, context.TODO()) _, err := apiKey.Call(pipelineMock, context.TODO()) @@ -108,21 +93,21 @@ func TestCallNoApiKeyFail(t *testing.T) { } func TestCallInvalidApiKeyFail(t *testing.T) { - ctrl := NewController(t) + ctrl := gomock.NewController(t) defer ctrl.Finish() pipelineMock := mockAuthPipeline(ctrl) authCredMock := mock_auth.NewMockAuthCredentials(ctrl) - authCredMock.EXPECT().GetCredentialsFromReq(Any()).Return("ASithLightSaber", nil) + authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("ASithLightSaber", nil) - apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", authCredMock, k8sClient, context.TODO()) + apiKey := NewApiKeyIdentity("jedi", map[string]string{"planet": "coruscant"}, "", authCredMock, testAPIKeyK8sClient, context.TODO()) _, err := apiKey.Call(pipelineMock, context.TODO()) assert.Error(t, err, "the API Key provided is invalid") } func TestLoadSecretsSuccess(t *testing.T) { - apiKey := NewApiKeyIdentity("X-API-KEY", map[string]string{"planet": "coruscant"}, "", nil, k8sClient, nil) + apiKey := NewApiKeyIdentity("X-API-KEY", map[string]string{"planet": "coruscant"}, "", nil, testAPIKeyK8sClient, nil) err := apiKey.loadSecrets(context.TODO()) assert.NilError(t, err) @@ -130,21 +115,11 @@ func TestLoadSecretsSuccess(t *testing.T) { secret1, exists := apiKey.secrets["ObiWanKenobiLightSaber"] assert.Check(t, exists) - assert.Equal(t, apiKeySecret1.String(), secret1.String()) + assert.Equal(t, testAPIKeyK8sSecret1.String(), secret1.String()) secret2, exists := apiKey.secrets["MasterYodaLightSaber"] assert.Check(t, exists) - assert.Equal(t, apiKeySecret2.String(), secret2.String()) -} - -type flawedAPIkeyK8sClient struct{} - -func (k *flawedAPIkeyK8sClient) Get(_ context.Context, _ client.ObjectKey, _ client.Object) error { - return nil -} - -func (k *flawedAPIkeyK8sClient) List(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { - return fmt.Errorf("something terribly wrong happened") + assert.Equal(t, testAPIKeyK8sSecret2.String(), secret2.String()) } func TestLoadSecretsFail(t *testing.T) { diff --git a/pkg/evaluators/identity/mtls.go b/pkg/evaluators/identity/mtls.go index 05c6e55e..9346d26c 100644 --- a/pkg/evaluators/identity/mtls.go +++ b/pkg/evaluators/identity/mtls.go @@ -60,7 +60,7 @@ func (m *MTLS) loadSecrets(ctx context.Context) error { for _, secret := range secretList.Items { secretName := k8s_types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name} - if cert := decodeCertificate(secret); cert != nil { + if cert := certificateFromSecret(secret); cert != nil { m.rootCerts[secretName.String()] = cert } } @@ -69,9 +69,6 @@ func (m *MTLS) loadSecrets(ctx context.Context) error { } func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { - var cert *x509.Certificate - var err error - urlEncodedCert := pipeline.GetRequest().Attributes.Source.GetCertificate() if urlEncodedCert == "" { return nil, fmt.Errorf("client certificate is missing") @@ -80,11 +77,9 @@ func (m *MTLS) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{ if err != nil { return nil, fmt.Errorf("invalid client certificate") } - - block, _ := pem.Decode([]byte(pemEncodedCert)) - - if cert, err = x509.ParseCertificate(block.Bytes); err != nil { - return nil, err + cert := decodeCertificate([]byte(pemEncodedCert)) + if cert == nil { + return nil, fmt.Errorf("invalid client certificate") } certs := x509.NewCertPool() @@ -114,9 +109,14 @@ func (m *MTLS) AddK8sSecretBasedIdentity(ctx context.Context, new k8s.Secret) { defer m.mutex.Unlock() secretName := k8s_types.NamespacedName{Namespace: new.Namespace, Name: new.Name}.String() - newCert := decodeCertificate(new) + newCert := certificateFromSecret(new) logger := log.FromContext(ctx).WithName("mtls") + if newCert == nil { + logger.V(1).Info("invalid root ca cert") + return + } + // updating existing if currentCert, found := m.rootCerts[secretName]; found { logger := log.FromContext(ctx).WithName("mtls") @@ -153,9 +153,8 @@ func (m *MTLS) withinScope(namespace string) bool { return m.Namespace == "" || m.Namespace == namespace } -func decodeCertificate(secret k8s.Secret) (cert *x509.Certificate) { +func certificateFromSecret(secret k8s.Secret) (cert *x509.Certificate) { var encodedCert []byte - if v, hasTLSCert := secret.Data[k8s.TLSCertKey]; hasTLSCert { encodedCert = v } else if v, hasCACert := secret.Data[k8s.ServiceAccountRootCAKey]; hasCACert { @@ -163,7 +162,10 @@ func decodeCertificate(secret k8s.Secret) (cert *x509.Certificate) { } else { return nil } + return decodeCertificate(encodedCert) +} +func decodeCertificate(encodedCert []byte) (cert *x509.Certificate) { for len(encodedCert) > 0 { var block *pem.Block block, encodedCert = pem.Decode(encodedCert) @@ -179,6 +181,5 @@ func decodeCertificate(secret k8s.Secret) (cert *x509.Certificate) { continue } } - return cert } diff --git a/pkg/evaluators/identity/mtls_test.go b/pkg/evaluators/identity/mtls_test.go new file mode 100644 index 00000000..1dcc79e0 --- /dev/null +++ b/pkg/evaluators/identity/mtls_test.go @@ -0,0 +1,239 @@ +package identity + +import ( + "context" + "encoding/json" + "net/url" + "testing" + + mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + k8s "k8s.io/api/core/v1" + k8s_meta "k8s.io/apimachinery/pkg/apis/meta/v1" + k8s_types "k8s.io/apimachinery/pkg/types" + k8s_client "sigs.k8s.io/controller-runtime/pkg/client" + + gomock "github.com/golang/mock/gomock" + "gotest.tools/assert" +) + +var ( + testMTLSK8sSecret1, testMTLSK8sSecret2, testMTLSK8sSecret3 *k8s.Secret + testMTLSK8sClient k8s_client.WithWatch + testCerts = map[string]map[string][]byte{} +) + +func init() { + certs := make(map[string]map[string]string) + + for _, k := range []string{"pets", "cars", "books", "john", "aisha", "niko"} { + certs[k] = make(map[string]string) + } + + certs["pets"]["tls.crt"], _ = url.QueryUnescape(`-----BEGIN%20CERTIFICATE-----%0AMIICmjCCAYICCQCmRAsdcSJkgzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARw%0AZXRzMB4XDTIyMDYxMTExMzQzM1oXDTIzMDYxMTExMzQzM1owDzENMAsGA1UEAwwE%0AcGV0czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNBgs%2F1bs9ybvnJ%0AgiXP21n46jvUCUL6ST0gX%2FlcIGAzvBiSg4tPScbMyeQwsPnht5o1de8m06IZ9PKS%0ALpGO5vlM2wr8Fh97ILr8dwYLrV9OegWqtYNfMO%2BXoSqWjSisEdEB%2BwNSI3TIbp7E%0AAkMSZmgrUrKKuVuZM0OIGsQtTG8CZvSPI37OzM%2FmGNTI%2BcYhJzRVhLa61nn4vqVz%0AfgG2tRW5FYW%2Br7qhHcx8hVDv5npwltpoFN0MosrkNMegmIgvcyVmXdibMji2f%2FOh%0A%2FFrAfRr5%2BWs9xVkd2fzuZWq1OLBDXIzhYC0TpX3sytDQhGQi%2F95ZPqPCglBNVh2s%0A3zurxJcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATXxgldjTMOvYz4txYfrHJ76y%0A%2FtSLddLxIJAeFdHmj1i4a%2FDh2%2BRpIwL%2B%2B40WJvpcuYCxqc2cUOelag6WCdd9%2BQdX%0A1nAynbY5KlX9A3PCWJY8OMGWXZ5eKhcQi%2FFGfECI9iCx5edaxjNw9dpPNTPa3Sgt%0A3NMfnR7Wx3bcER35TntGaTdXu6tguPbrbEyNUFbS5JIj%2BNzqWEwi5XaDvuFTZTjt%0AenaZzNi0qxvjFQGlh6AuQ3jIRx0hCQAaaxNwcW7uQfWE3vBEUyC06YH81r1vBzsU%0A%2Ba3fQAptefIL4MmbDL6WWrB0%2FLRF3Lw2lfQ4ptQJqwzG8gk%2BRsno6F1Om6IZVQ%3D%3D%0A-----END%20CERTIFICATE-----%0A`) + certs["pets"]["tls.key"], _ = url.QueryUnescape(`-----BEGIN%20PRIVATE%20KEY-----%0AMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCjQYLP9W7Pcm75%0AyYIlz9tZ%2BOo71AlC%2Bkk9IF%2F5XCBgM7wYkoOLT0nGzMnkMLD54beaNXXvJtOiGfTy%0Aki6Rjub5TNsK%2FBYfeyC6%2FHcGC61fTnoFqrWDXzDvl6Eqlo0orBHRAfsDUiN0yG6e%0AxAJDEmZoK1KyirlbmTNDiBrELUxvAmb0jyN%2BzszP5hjUyPnGISc0VYS2utZ5%2BL6l%0Ac34BtrUVuRWFvq%2B6oR3MfIVQ7%2BZ6cJbaaBTdDKLK5DTHoJiIL3MlZl3YmzI4tn%2Fz%0AofxawH0a%2BflrPcVZHdn87mVqtTiwQ1yM4WAtE6V97MrQ0IRkIv%2FeWT6jwoJQTVYd%0ArN87q8SXAgMBAAECggEAHvqLbBLSmCLK1DNcsvgiU4xcRkYSC9ealjLSg2rr6dVn%0AV%2FJVa9X71fF%2BTgK%2FUmt2f5itbFgdyKDMTktW8t%2F%2FDEd9OTRkrkybBWBq5YbJu1AU%0A74ZZMziY%2FJ31QzOWTaV5LAQIMbUgbUSrWQ0wsLGJJTMzWhXg3nTPuXzWN2uxGU85%0AtjnpKUEgvfORpJcaTmfpsvmBk5oVRGMVVEHcN32jnVTE938IiCVNBJsekYhj3aEl%0Aeawtq8SSXjWLE5U2tVFHK4p4mqLC1Io80uYRaMu2r13DvO%2FETJbTlJTAQdPGw3mu%0ArLsi%2FteftZ6KmVT9iFN7HAke0Dpaw%2BYizY76BBUv4QKBgQDWPs5tlEveBnOvd7ro%0AhF3BTef7K04weWKo6YwoD7T46CVSKgVLmqO9VeQwjmjYt0AqQliOKguADj1ZeUTl%0AEyTbVEty3bXN0D3GXeLZc81%2FsN1OQLpet6lyyqsC9d7PXWffHhBQp0Q9%2BBa%2FkFx9%0Aoq7lpjmmfZJf1BZvFpVDsfPiEwKBgQDDErllEuTgzsrv6Ag%2Bbm3XzSpVz7FNrmbz%0APxcqmxkjdKFNW3om9kmE0al5oq7Lw%2BjOMW3SIA%2BVdr7fura5QFfRxLhaHlDmcJn2%0AfoySf5Zdxenh3hxIpqkEEbtBtrXKv8k%2Fi28pgHyEJu6o2y54m2QOs4x5PkpB%2BcRP%0AR0gyp3XD7QKBgCkqCB%2Blzq3qL3AXYSIrzJfHkDsCJxPJPtuVhAhufCcW85TF3h6Y%0Ap71JM37g3eRF0V5NQRaPnYYNNlxqoIIjG4HIwHZhgvz4deYXQ%2B7kASf3o43VgfmQ%0A8E3OAu2esCDHoZ2M%2BTWF7ea6NCS6aAr7pv8Y4RrMJcOjzGuruyI2ntVhAoGAJT1K%0A1SfBN8ViamASStDL%2BVl6Tn1irKCxmJgftQt8xg76yAjBjfSQXmGkB8ttsQqKQ%2Bqd%0Au3JRZ0gO8ijzvvOwkCQMyW9mJEe0rKDF9yWSL%2F6bQnojTh86vsMfy1C07aqlIZNd%0Auj%2BEBbpk7ylAete3RzMxiufAR04GEthZyQm86pUCgYA7PWLjzI7m4ubXdKpXs8ZW%0AXEtII7CEGvv0Z1r1zvzavnKfp7Um6%2BR1mJ5ro%2FESa%2FUWktxh0OuxS5nVf618Pk%2FM%0AvvBFsshCRuyfA0%2BCB6C%2Fvl%2FQaOV4hLYY6lEoBvimDSO1mf2XtCJWQLskXEnlDp1S%0AIc%2FM9q3gLh0qUBXXP5zSCg%3D%3D%0A-----END%20PRIVATE%20KEY-----%0A`) + certs["cars"]["tls.crt"], _ = url.QueryUnescape(`-----BEGIN%20CERTIFICATE-----%0AMIICmjCCAYICCQC0%2Bxgjh%2FTpvjANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARw%0AZXRzMB4XDTIyMDYxMTExMzQ0MVoXDTIzMDYxMTExMzQ0MVowDzENMAsGA1UEAwwE%0AcGV0czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKWxnlcRi2wfVR42%0A71%2BJdOQE75meYGtvaEK2ek6HXNWz3RsoBmsrbHbtRv6Z3eYuW59god5h7ywGcx0o%0AlEmdVQHIX8sqyJewSLcha9UofSdjOmNFzCqd5M4FBves0cI1%2ByfGvXd11PaRo%2FOV%0AtAsimT6vqgDn6pmjKwefnnsblnho93dDtHQA9aBJGJ45R%2FaScqmyyVxuxjXAeDTZ%0AVhmLeMw4UytjmgnVkpkK2Ef%2Bl3fosqKPqahK28Lx%2BwRY5odPM0S2nOYse0HMwkVX%0A0kCJkOKv4HWZQmUhtERpdgaunWi6HuibCflKYyx4JQbMExEYZCyOqfQKwV3yrVNL%0AS3e8es0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgXWzWoQ8RypgXC0iNI%2FsTnqr%0Aihpvt%2Bh9ocbgQoqeWL%2BU6uOiBmreoja3qnIaGVav4C5fihp7wGQ2CHR0XRqJQDfZ%0AB4fHMCsq3hVtWGZpDAVaZOQSj%2F2YySKHDSQ3DudmmIgp8CAlX%2ByzyDTpGBA4tD%2BC%0AtA8Q%2Bp%2BE%2Fde1SA1jIAJ5BqqAn7y%2FnDAeIYvEvQsJX7ipwzqIGuuPKG%2Fd7Gi%2FO2UT%0AqjHc9l%2Fhm4y52hHbWbXGEOBPXe1TRKiFvmUIQav6C537rizLVRBtX2OeznWqMF9B%0AYd9W%2FPdWD9lWqVaL26wakaJ2Cvcu6GsZl3C%2FYzKFk5btmFe8MjIXpWrhrKS32g%3D%3D%0A-----END%20CERTIFICATE-----%0A`) + certs["cars"]["tls.key"], _ = url.QueryUnescape(`-----BEGIN%20PRIVATE%20KEY-----%0AMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClsZ5XEYtsH1Ue%0ANu9fiXTkBO%2BZnmBrb2hCtnpOh1zVs90bKAZrK2x27Ub%2Bmd3mLlufYKHeYe8sBnMd%0AKJRJnVUByF%2FLKsiXsEi3IWvVKH0nYzpjRcwqneTOBQb3rNHCNfsnxr13ddT2kaPz%0AlbQLIpk%2Br6oA5%2BqZoysHn557G5Z4aPd3Q7R0APWgSRieOUf2knKpsslcbsY1wHg0%0A2VYZi3jMOFMrY5oJ1ZKZCthH%2Fpd36LKij6moStvC8fsEWOaHTzNEtpzmLHtBzMJF%0AV9JAiZDir%2BB1mUJlIbREaXYGrp1ouh7omwn5SmMseCUGzBMRGGQsjqn0CsFd8q1T%0AS0t3vHrNAgMBAAECggEATX0AeOWal2kbzHKShdJp2Q055FS98OB8GN7v2fPSBZsF%0AJ2MThWEca43R6tWYgcJiVOnDKZYRXTxy70r%2F9mFe1OOZcRFEGDR3%2FTTjEh%2FKT%2FZG%0A4xBMSA3paDPPq1qmCjZmi5aVGt3%2FR4Sa8RqsxZxboIZUcfIDs%2FAr%2Bne6jQY823gy%0AxtBV3YxQiuc9tzM8Nb3C%2BomBkY%2Fo1vkyfDgSidYIUd1VRk%2FhNIL3dr5Le3edmFxW%0APH%2FqmkayIT7tMGapXBoz6Tt%2B1Z1k8rvbm%2F6GIfQZLtBZSNic5nrefs64zvnQ79NW%0AZ%2BHmdZZU%2F2uEszLZ9aamK%2Bhq8F3s33WBOuxQc0bbvQKBgQDPZympuEwZi4gB7m3a%0AZ8epY44GULDqK81xa4ladPRDFjea%2FbM%2BoJuusMYNG6AH0JYzsXKT58V9vcAWLxog%0AQLQLlfPMSqrbWcgONiYPq0Trz3bO0rp2nVkmuWONO1ftgERy9Q44iYhwNwkrK6ZM%0AJ53IIDVHSjsDFv17UM2ipDDfswKBgQDMhJQNNOKSqvySKPmhnrZmV8Wf3S4XL1O6%0AwdcsJcBHRwQPMNySVbAY1QBO%2BLllQ6Av5vbA%2FOQu2nqiJI4EmuvXZLppXAyobuxn%0AFSpXhfj4AlidsQ3OiTc5vmX563MFH%2BkYSIKzQSBndqC3B2l1BMDqSoVyZKT5y3xm%0A79OWmrP7fwKBgQCRUYgYmcAAWgqGx%2BeCkxqLbezSMfFzchN1d9J6Zd3Lr6JwX3ga%0A1m%2Bee8%2BY2ZVMRHMpbxiH12pByxTutjwJAyzjvUJgDqUeIg8RHhGXAvq8etWU3oO1%0AnlQb1OOSzlSyXSAYp%2Bk55euKLJWpAOF5FHzx%2Ftc1xyYH6TDcGWaroX15DwKBgQCq%0AgYWlFQgoWyFDAaJNGjLbVCXQx%2BebMLvPobeweLC7O%2FuoZoYeAg5URZCCRl7ai%2BzK%0AwvXJo4zhewhukadNM5OX%2BcRn%2FnQXIJM6xayNV4ZfziTvIyNto3xFSfVezOsRxK7i%0AreE5bPyFBaOrtCQ5iQME0ag73KimEP3gG%2BX9U3DmJQKBgG%2FS%2FA4GRohr1bBkXEeP%0A48GEoJWq4%2B6yVdwpBzPIXBlkNj6ZSEd9VWusIsa%2B2eqnv5tW9OQEdo3XsdWsAqPK%0A7b8M1NTuLzDnouaou%2BjfoBNB9vL8%2BnIQGGqfyl9jtfuYtjEKxWi41RbUZw2kt6cd%0AYAbHhGbdvx9saAqJMKKbE1O3%0A-----END%20PRIVATE%20KEY-----%0A`) + certs["books"]["tls.crt"], _ = url.QueryUnescape(`-----BEGIN%20CERTIFICATE-----%0AMIICmjCCAYICCQCfjfMh66x63jANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARw%0AZXRzMB4XDTIyMDYxMTExMzQ1MFoXDTIzMDYxMTExMzQ1MFowDzENMAsGA1UEAwwE%0AcGV0czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANIRMbdvVSLyDFXu%0ArDYPRHMsxUvfnLQYSxSGOxwNY0Cc1C6aZOWDfTtr4PMyfnYEfn9%2BZm2QhqCYY%2Bm3%0A0ExZbpyxlamQi1EEM6LVNjcdGeDFN24R4fWC%2BMNdvV1L4Uc20Z6vqjr%2Fmw9vEm6K%0Asvz5ChUGBytDXAAkun%2FWNH%2FfN3P7%2B2lWJgAoip%2B2MXKHzgkAim8vwoh8UugoF30J%0APWCHfn37cdEQ9JqAeRSaj6qFHn4QOstxDm5V2lq%2FZs1sozyoHwvun80ECod3fqdV%0A2g4J%2F1527aV%2B8x2TdE6gHp40BPiaWu3RgzvYfH2WUs6D63IrVtHW1k8t%2BWp0WeZL%0AiAgdrckCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA0fAegaugnNgwZ0lYiWO%2FUlsK%0A%2FEKW6j37Um4afVSO9s68jdUuvBTdLrbWSI3ipNyFRjca4h9yx7iyzAv256b8wMJ7%0AzuDNgMcwZQrssjFLtjE4Mz18r9DC0Up8CCEKqzRYx9o90l29o%2BsuikWbMIB3Szum%0AEy%2B6cN29PvjX4oHlYC750IepPQhQVW0DXGAPy1Jllc%2FtjeHNrYZjzOE1q1OqY4EM%0AO91uL6P7TgO9s1iFSIQmWqDSKl11qoRet3PHCoC1Gg6lOubUVWYIC8Cfy5yi87Qy%0A7rMFA3Wu7HNkIUleFqy1CblMNxb2wdqYsRkcID6j7W17NmgpblyzxHYk2Mn3OQ%3D%3D%0A-----END%20CERTIFICATE-----%0A`) + certs["books"]["tls.key"], _ = url.QueryUnescape(`-----BEGIN%20PRIVATE%20KEY-----%0AMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSETG3b1Ui8gxV%0A7qw2D0RzLMVL35y0GEsUhjscDWNAnNQummTlg307a%2BDzMn52BH5%2FfmZtkIagmGPp%0At9BMWW6csZWpkItRBDOi1TY3HRngxTduEeH1gvjDXb1dS%2BFHNtGer6o6%2F5sPbxJu%0AirL8%2BQoVBgcrQ1wAJLp%2F1jR%2F3zdz%2B%2FtpViYAKIqftjFyh84JAIpvL8KIfFLoKBd9%0ACT1gh359%2B3HREPSagHkUmo%2BqhR5%2BEDrLcQ5uVdpav2bNbKM8qB8L7p%2FNBAqHd36n%0AVdoOCf9edu2lfvMdk3ROoB6eNAT4mlrt0YM72Hx9llLOg%2BtyK1bR1tZPLflqdFnm%0AS4gIHa3JAgMBAAECggEAZWC%2B6ZPlNnQx19bTbVN46UyV%2BsPz0EZZFZKiPCuJ1sjY%0A7ZS9VLZcXaz2ZufbeAE7OzQ5Im85SY%2FByC1ZbT9Lzns1ixui4HpyoQbcn0SAFKWY%0A1pnyvpVykHZQyRGxqrid%2BUM1mDt7DbvA3XA6aTOW1gaOtyZO5BLVnpQ1yfBjxqjY%0AQ3B5Y7ryh9QBZDJqXSlgqU9TpR2OBgXDK4kjeuv3ZkIB%2Fe6nnQ8%2FQxGQ%2FGM5DrQG%0ATOBnuYyTCTJMiDtpqttxs9yA6JqSs4PHWrAOrA0Unh7qVe7XRHvGMXB4BdmT1gUS%0AS07ItFu%2BIQIfAucQ0W9UUv0lo7QryncOzmNME6fIMQKBgQD87XSCBKzW34lNRlfY%0AFiU9urBVjGsgmfn0OzVNMwxB7gqenVBub2jf1OtIkXApZP6kqPMTNxMQtsAub4zh%0AXIjiWnBoo9hkmN04aQUsSVAYrvPTy%2BRttn35IaJrNSeKe%2FG8wOeIaNLCp6DE3zSx%0AqRDzwtYGxBYtGmhMg8DcvLGecwKBgQDUnnQSezSNjnUEGBVLVB4LgnnyRkvm3zIc%0AQNql5%2BBtJCX8KKRHhbkOmCmgqudDPSjbYWjuinc7jwnj8OVHP23Xpvw31O5uGShR%0AlmqBDkmMt9Ys0NHwku3lQ2gDVC92s8GDRIH0Utc2m7ZKdEMvGOjehfl0yRQ9pBr%2F%0A%2BCsS%2BOVX0wKBgF%2B0%2FtRALqL0XUE3cAAdiSQNo7ILe3IPscygJvA6c9Xy3GPexVO0%0ApqukJxADsLyJMe5e2%2FQWcAlwDdLEdTvFxypX7Jc8AKM4UOWKn%2BF9MGjWsv8e9SYq%0A2wpNlucYawj1E6lIGZS9jZsI0UYN7COaBQcoX9KZmoagqnzhkjY01MVPAoGAds0u%0A2CDFhY8i7S2zwEp5Gz9Fek0zHgZ6jnTidy8wJGu9Wb8vw9MBSxlUsTStUdG7oZE1%0AO4xdAQd0pEu3IO9dJdFlPqEYtKYT9DqSuhfMmvchkhsAI2dFzAO0%2B58vgikAqKM%2F%0A5c%2Ff9uBcpA%2BAdrF5dNTxRQMR7zth5sK49rniFAcCgYEAwUS7WPYz9yVeVb6PiOg1%0AXVBcYtbSToGrGufvxeQ40fY%2BpS0tVZEwK8Q%2FEN3%2Fp0NTZzitHi4S7CgXw3%2FV1sCi%0AEPKhde5S1fyszYtPmu7u4r7Qn%2FdJvMB2ZGffp0gbC9HZRh4veaPb8cUpdDtHu5%2FG%0A0NLpVsq%2FZKmPbqafwaE72xQ%3D%0A-----END%20PRIVATE%20KEY-----%0A`) + + certs["john"]["tls.crt"], _ = url.QueryUnescape(`-----BEGIN%20CERTIFICATE-----%0AMIICuDCCAaACCQCg8Pi%2BR7xlUTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARw%0AZXRzMB4XDTIyMDYxMTEyMzc0MFoXDTI2MDYxMDEyMzc0MFowLTENMAsGA1UEAwwE%0Aam9objELMAkGA1UEBhMCVUsxDzANBgNVBAcMBkxvbmRvbjCCASIwDQYJKoZIhvcN%0AAQEBBQADggEPADCCAQoCggEBALPIWAAY%2BGCnHxnCd3iJMP5jx8JEvImmII2anU9B%0AB3i%2FdGq4oPv%2BIbQZcWS6YQbJdm8pAwkR9IIhz0csMMPoJuX1ZD7QsNWEEI3hXypZ%0A1iwZNJinU2MvvLuQRnnD1qpwd4SSq%2BNH1IqyCYAHpYq2ROQsFyr1s4iA0y0JM0vH%0AMAxG7QSO8IHAGMG%2FgQWcIge8Ko%2Bu5T%2BJ1IEmlkUrGMj4PvRhD05A7PLXddGZnhrb%0AtWhfN15B5BRJMyD5PlUFiCG4avKXSKO%2FBOCY8aXPraUBiCVzpGtOGPDbVKieZe1i%0AXKbKQUa3KLW%2BlZLUdRSgvmYSVpLtne%2Fjya1V6u0bkpQxniECAwEAATANBgkqhkiG%0A9w0BAQsFAAOCAQEAkHrqNj6idfRrRXAIIAwTDFAJC9KcTVkh3%2F1x0H8FxKyp65we%0A151C8uJMecFfKBdXFQv05IKYWksvEVoEUOODPrM%2Bzl89eKWgoAEtaJ3TAp2cgcqp%0ANo0vKGz6fP03KuHvLDOvCXfqEf8IM%2FN2Lwxfl9r0I5naSgx7QzfsMO3G34bZy4mI%0Aai6Rmth0HklUTDQUOd5ZoxsaTBPFPqApMLYMQzpKKB9mdx5kLiHJsV%2FnVSnzqtgh%0AsHeYSKHwid1Hb6t7%2BjaEHXH3Rj45h1I5Ib0Ax%2Bfo25B0cbVrY8mbZIhWS2jSjBVF%0Ac0tfkcSZ1r3ML8tqkuRpmbiTSm1ObX92sRrV2g%3D%3D%0A-----END%20CERTIFICATE-----%0A`) + certs["aisha"]["tls.crt"], _ = url.QueryUnescape(`-----BEGIN%20CERTIFICATE-----%0AMIICvDCCAaQCCQCJbRrcBLeKnTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARw%0AZXRzMB4XDTIyMDYxMTEyMzc0NVoXDTI2MDYxMDEyMzc0NVowMTEOMAwGA1UEAwwF%0AYWlzaGExCzAJBgNVBAYTAlBLMRIwEAYDVQQHDAlJc2xhbWFiYWQwggEiMA0GCSqG%0ASIb3DQEBAQUAA4IBDwAwggEKAoIBAQDzjA7PABY%2B20cj7ozywdeQL2JQKh%2Bk4B7p%0AfcT0sw3mb0mdyFVDF3xc7o4orWmfAzRwHSSimIeOYEDR%2FkeuKMWQfLMd3MaSLkGT%0Au8R5HYvN9NVxhhUpD%2FTIvCbUgXRQKMNhiDzyL9TL5ija%2BT8DN%2FMuIagzhz1jKAtG%0AWcew4rFWIl298kvKgE4Z8YFI4EDcBqn5FNELkz8%2Fl0T1lORvxc81MtqyR61HkD0s%0A5xYZrHNbmjyByG5UoFwVItQ1VHYQoiYW%2F0ifmYWcDx%2B4H0cux6PRR9Qrz%2FD2RneU%0Ai%2FsHNicFr5KzYaEtHEHiSGNbniD%2BEd%2FoE447IoaPamMVqSQR711zAgMBAAEwDQYJ%0AKoZIhvcNAQELBQADggEBAJ9Y6olsL76KQ%2FopJFFzChXmrIzGGma51bE%2F6jQzSRrD%0AHSZEZQT8O2g20qtgPDhWSe3mkIayCAwZ%2FxzQONNa8bjXsolqxeSyqtQv5MgGcDa%2B%0AumnUtCNcW1k%2F1e8bygV6ijJBM1tNpCzMqrz7F4y4ZnuZ7pkTzai6s7S8woFXLx%2FP%0AeC8uzDhDJTW5J%2F%2Bz%2BxAZl5ZpCwHskA8tgPqCo%2BwLrSevHwQhP%2FqZ8Wgd6DdNJ0w5%0ASieiephzQ3O0wTk6YjjxQGI%2BYprRY2T8bTamQ3A0dQGn0zqjFh2%2FlrIO61RMD8os%0AJdCBjuw4v5yXat5dsymjRffRSCioAslg0hMHWnqZjO0%3D%0A-----END%20CERTIFICATE-----%0A`) + certs["niko"]["tls.crt"], _ = url.QueryUnescape(`-----BEGIN%20CERTIFICATE-----%0AMIICtzCCAZ8CCQCzPRO%2FsBHMzDANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARw%0AZXRzMB4XDTIyMDYxMTEyMzkwMFoXDTI2MDYxMDEyMzkwMFowLDENMAsGA1UEAwwE%0AbmlrbzELMAkGA1UEBhMCSlAxDjAMBgNVBAcMBU9zYWthMIIBIjANBgkqhkiG9w0B%0AAQEFAAOCAQ8AMIIBCgKCAQEAsAVZR9fk1%2BYs%2FXoXy9YDTKcgcrRsPyy72fsIMCxd%0Ahd35Gi3JMf8av35puuJ6zbdIBXbtlAwd8Kwb2YafOaTm0hJGqjyJ44%2FLw%2FzEER3S%0AW1OqNiNBjH2JX5Rgn7e6zP%2BO6W%2Bj9Ier5zkQt%2Fv3qJy5Tqgd%2FLP7BXlswdAYnPt0%0AXhZHsyOvyUAalItYPfU2SLbORz%2BTS1yv9UQj0Wbd3mSd8xK5d1fOYzHDY2qtDXIe%0AVdd6s7RH51OdxQe1nqxNLV7hkNqvMrmKqPOQzdXbylJa8KV%2BCsWikfFgi47%2FOMjl%0AFsXjvjdXK%2B%2By5PKYABH%2FqZgsDyotE0nu6F%2ByfOkxqe0w8QIDAQABMA0GCSqGSIb3%0ADQEBCwUAA4IBAQALFY0JLaX4aHsFc1KL0BxNyUEo8vIBk8oIUr0WwvmNEKnsSNMz%0AeHcj0YUmdiPgFQGbG%2FeDkPMGPBS2F2rrtDgyJ5fYRFRCh6bo6YLdSIljMCCW6MhU%0Ac%2Ffy3DIBaEEcbzPUK%2FP7s1FKiowFthLgRxjOtyD%2BkYlKdAbQ1QUxR43WXBcABFaW%0A3hL%2BrowcwJjJofrH0m78EzM%2BYXk5ldcEOT6%2FiF0eIynx59h0IFe2GyROQAqpoWht%0AiJ9YnZbuWQ1vT1kw1YSZx3ba90P8FSBULephM9rG24R20yUPNGP5Y6jfTh7odyie%0ALNrSFH8bvAz9snt%2B7TvDZBVCB3ZiiXewIBqB%0A-----END%20CERTIFICATE-----%0A`) + + for key, value := range certs { + testCerts[key] = make(map[string][]byte) + testCerts[key]["tls.crt"] = []byte(value["tls.crt"]) + if certKey, found := value["tls.key"]; found { + testCerts[key]["tls.key"] = []byte(certKey) + } + } + + testMTLSK8sSecret1 = &k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "pets", Namespace: "ns1", Labels: map[string]string{"app": "*"}}, Data: testCerts["pets"], Type: k8s.SecretTypeTLS} + testMTLSK8sSecret2 = &k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "cars", Namespace: "ns1", Labels: map[string]string{"app": "*"}}, Data: testCerts["cars"], Type: k8s.SecretTypeTLS} + testMTLSK8sSecret3 = &k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "books", Namespace: "ns2", Labels: map[string]string{"app": "*"}}, Data: testCerts["books"], Type: k8s.SecretTypeTLS} + testMTLSK8sClient = mockK8sClient(testMTLSK8sSecret1, testMTLSK8sSecret2, testMTLSK8sSecret3) +} + +func TestNewMTLSIdentity(t *testing.T) { + var exists bool + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "", testMTLSK8sClient, context.TODO()) + + assert.Equal(t, mtls.Name, "mtls") + assert.Equal(t, mtls.LabelSelectors["app"], "*") + assert.Equal(t, mtls.Namespace, "") + assert.Equal(t, len(mtls.rootCerts), 3) + _, exists = mtls.rootCerts["ns1/pets"] + assert.Check(t, exists) + _, exists = mtls.rootCerts["ns1/cars"] + assert.Check(t, exists) + _, exists = mtls.rootCerts["ns2/books"] + assert.Check(t, exists) +} + +func TestNewMTLSIdentitySingleNamespace(t *testing.T) { + var exists bool + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + + assert.Equal(t, mtls.Name, "mtls") + assert.Equal(t, mtls.LabelSelectors["app"], "*") + assert.Equal(t, mtls.Namespace, "ns1") + assert.Equal(t, len(mtls.rootCerts), 2) + _, exists = mtls.rootCerts["ns1/pets"] + assert.Check(t, exists) + _, exists = mtls.rootCerts["ns1/cars"] + assert.Check(t, exists) + _, exists = mtls.rootCerts["ns2/books"] + assert.Check(t, !exists) +} + +func TestMTLSGetK8sSecretLabelSelectors(t *testing.T) { + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "test"}, "", testMTLSK8sClient, context.TODO()) + labelsSelectors, _ := json.Marshal(mtls.GetK8sSecretLabelSelectors()) + assert.Equal(t, string(labelsSelectors), `{"app":"test"}`) +} + +func TestMTLSAddK8sSecretBasedIdentity(t *testing.T) { + var exists bool + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + + assert.Equal(t, len(mtls.rootCerts), 2) + + newSecretWithinScope := k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "foo", Namespace: "ns1", Labels: map[string]string{"app": "*"}}, Data: testCerts["cars"], Type: k8s.SecretTypeTLS} + mtls.AddK8sSecretBasedIdentity(context.TODO(), newSecretWithinScope) + assert.Equal(t, len(mtls.rootCerts), 3) + _, exists = mtls.rootCerts["ns1/foo"] + assert.Check(t, exists) + + newSecretOutOfScope := k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "bar", Namespace: "ns2", Labels: map[string]string{"app": "*"}}, Data: testCerts["cars"], Type: k8s.SecretTypeTLS} + mtls.AddK8sSecretBasedIdentity(context.TODO(), newSecretOutOfScope) + assert.Equal(t, len(mtls.rootCerts), 3) + _, exists = mtls.rootCerts["ns1/bar"] + assert.Check(t, !exists) + + newSecretInvalid := k8s.Secret{ObjectMeta: k8s_meta.ObjectMeta{Name: "inv", Namespace: "ns1", Labels: map[string]string{"app": "*"}}, Data: map[string][]byte{}, Type: k8s.SecretTypeTLS} + mtls.AddK8sSecretBasedIdentity(context.TODO(), newSecretInvalid) + assert.Equal(t, len(mtls.rootCerts), 3) + _, exists = mtls.rootCerts["ns1/inv"] + assert.Check(t, !exists) +} + +func TestMTLSRevokeK8sSecretBasedIdentity(t *testing.T) { + var exists bool + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + + assert.Equal(t, len(mtls.rootCerts), 2) + + // revoke existing trusted ca cert + mtls.RevokeK8sSecretBasedIdentity(context.TODO(), k8s_types.NamespacedName{Namespace: "ns1", Name: "pets"}) + assert.Equal(t, len(mtls.rootCerts), 1) + _, exists = mtls.rootCerts["ns1/pets"] + assert.Check(t, !exists) + + mtls.AddK8sSecretBasedIdentity(context.TODO(), *testMTLSK8sSecret1) + assert.Equal(t, len(mtls.rootCerts), 2) + + // revoke non-existing trusted ca cert + mtls.RevokeK8sSecretBasedIdentity(context.TODO(), k8s_types.NamespacedName{Namespace: "ns1", Name: "foo"}) + assert.Equal(t, len(mtls.rootCerts), 2) + + // revoke trusted ca cert ot of scope + mtls.RevokeK8sSecretBasedIdentity(context.TODO(), k8s_types.NamespacedName{Namespace: "ns2", Name: "books"}) + assert.Equal(t, len(mtls.rootCerts), 2) +} + +func TestCall(t *testing.T) { + var data []byte + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + pipeline := mock_auth.NewMockAuthPipeline(ctrl) + + // john (ca cert: pets) + pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Source: &envoy_auth.AttributeContext_Peer{ + Certificate: url.QueryEscape(string(testCerts["john"]["tls.crt"])), + }, + }, + }) + obj, err := mtls.Call(pipeline, context.TODO()) + assert.NilError(t, err) + data, _ = json.Marshal(obj) + assert.Equal(t, string(data), `{"Country":["UK"],"Organization":null,"OrganizationalUnit":null,"Locality":["London"],"Province":null,"StreetAddress":null,"PostalCode":null,"SerialNumber":"","CommonName":"john","Names":[{"Type":[2,5,4,3],"Value":"john"},{"Type":[2,5,4,6],"Value":"UK"},{"Type":[2,5,4,7],"Value":"London"}],"ExtraNames":null}`) + + // aisha (ca cert: cars) + pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Source: &envoy_auth.AttributeContext_Peer{ + Certificate: url.QueryEscape(string(testCerts["aisha"]["tls.crt"])), + }, + }, + }) + obj, err = mtls.Call(pipeline, context.TODO()) + assert.NilError(t, err) + data, _ = json.Marshal(obj) + assert.Equal(t, string(data), `{"Country":["PK"],"Organization":null,"OrganizationalUnit":null,"Locality":["Islamabad"],"Province":null,"StreetAddress":null,"PostalCode":null,"SerialNumber":"","CommonName":"aisha","Names":[{"Type":[2,5,4,3],"Value":"aisha"},{"Type":[2,5,4,6],"Value":"PK"},{"Type":[2,5,4,7],"Value":"Islamabad"}],"ExtraNames":null}`) +} + +func TestCallUnknownAuthority(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + pipeline := mock_auth.NewMockAuthPipeline(ctrl) + + pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Source: &envoy_auth.AttributeContext_Peer{ + Certificate: url.QueryEscape(string(testCerts["niko"]["tls.crt"])), + }, + }, + }) + obj, err := mtls.Call(pipeline, context.TODO()) + assert.Check(t, obj == nil) + assert.ErrorContains(t, err, "certificate signed by unknown authority") +} + +func TestCallMissingClientCert(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + pipeline := mock_auth.NewMockAuthPipeline(ctrl) + + pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Source: &envoy_auth.AttributeContext_Peer{}, + }, + }) + obj, err := mtls.Call(pipeline, context.TODO()) + assert.Check(t, obj == nil) + assert.ErrorContains(t, err, "client certificate is missing") +} + +func TestCallInvalidClientCert(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mtls := NewMTLSIdentity("mtls", map[string]string{"app": "*"}, "ns1", testMTLSK8sClient, context.TODO()) + pipeline := mock_auth.NewMockAuthPipeline(ctrl) + + pipeline.EXPECT().GetRequest().Return(&envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Source: &envoy_auth.AttributeContext_Peer{ + Certificate: `-----BEGIN%20CERTIFICATE-----%0Ablahblohbleh%3D%3D%0A-----END%20CERTIFICATE-----%0A`, + }, + }, + }) + obj, err := mtls.Call(pipeline, context.TODO()) + assert.Check(t, obj == nil) + assert.ErrorContains(t, err, "invalid client certificate") +} diff --git a/pkg/evaluators/identity/test_mocks.go b/pkg/evaluators/identity/test_mocks.go new file mode 100644 index 00000000..01de1dcc --- /dev/null +++ b/pkg/evaluators/identity/test_mocks.go @@ -0,0 +1,37 @@ +package identity + +import ( + "context" + "fmt" + + mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + + k8s "k8s.io/api/core/v1" + k8s_runtime "k8s.io/apimachinery/pkg/runtime" + k8s_client "sigs.k8s.io/controller-runtime/pkg/client" + k8s_fake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + gomock "github.com/golang/mock/gomock" +) + +func mockK8sClient(initObjs ...k8s_runtime.Object) k8s_client.WithWatch { + scheme := k8s_runtime.NewScheme() + _ = k8s.AddToScheme(scheme) + return k8s_fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjs...).Build() +} + +func mockAuthPipeline(ctrl *gomock.Controller) (pipelineMock *mock_auth.MockAuthPipeline) { + pipelineMock = mock_auth.NewMockAuthPipeline(ctrl) + pipelineMock.EXPECT().GetHttp().Return(nil) + return +} + +type flawedAPIkeyK8sClient struct{} + +func (k *flawedAPIkeyK8sClient) Get(_ context.Context, _ k8s_client.ObjectKey, _ k8s_client.Object) error { + return nil +} + +func (k *flawedAPIkeyK8sClient) List(_ context.Context, list k8s_client.ObjectList, _ ...k8s_client.ListOption) error { + return fmt.Errorf("something terribly wrong happened") +} From 6fe79a8fd20484a9ae9ab83e4030c5c950c3ce73 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Sun, 12 Jun 2022 13:16:38 +0200 Subject: [PATCH 7/7] [docs] mTLS authn --- README.md | 2 +- docs/features.md | 38 ++- docs/user-guides.md | 3 + docs/user-guides/mtls-authentication.md | 418 ++++++++++++++++++++++++ 4 files changed, 454 insertions(+), 7 deletions(-) create mode 100644 docs/user-guides/mtls-authentication.md diff --git a/README.md b/README.md index e904e456..872c1006 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Under the hood, Authorino is based on Kubernetes [Custom Resource Definitions](h mTLS authentication - Planned (#8) + Ready HMAC authentication diff --git a/docs/features.md b/docs/features.md index b9fc9099..997255a3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -239,13 +239,39 @@ Online token introspection of OpenShift-valid access tokens based on OpenShift's ### Mutual Transport Layer Security (mTLS) authentication (`identity.mtls`) - - - - -
Not implemented - Planned (#8)
+Authorino can verify x509 certificates presented by clients for authentication on the request to the protected APIs, at application level. + +Trusted root Certificate Authorities (CA) are stored in Kubernetes Secrets labeled according to selectors specified in the AuthConfig, watched and cached by Authorino. Make sure to create proper `kubernetes.io/tls`-typed Kubernetes Secrets, containing the public certificates of the CA stored in either a `tls.crt` or `ca.crt` entry inside the secret. + +Truested root CA secrets must be created in the same namespace of the `AuthConfig` (default) or `spec.identity.mtls.allNamespaces` must be set to `true` (only works with [cluster-wide Authorino instances](./architecture.md#cluster-wide-vs-namespaced-instances)). -Authentication based on client X509 certificates presented on the request to the protected APIs. +The identitiy object resolved out of a client x509 certificate is equal to the subject field of the certificate, and it serializes as JSON within the Authorization JSON usually as follows: + +```jsonc +{ + "auth": { + "identity": { + "CommonName": "aisha", + "Country": ["PK"], + "ExtraNames": null, + "Locality": ["Islamabad"], + "Names": [ + { "Type": [2, 5, 4, 3], "Value": "aisha" }, + { "Type": [2, 5, 4, 6], "Value": "PK" }, + { "Type": [2, 5, 4, 7], "Value": "Islamabad" }, + { "Type": [2, 5, 4,10], "Value": "ACME Inc." }, + { "Type": [2, 5, 4,11], "Value": "Engineering" } + ], + "Organization": ["ACME Inc."], + "OrganizationalUnit": ["Engineering"], + "PostalCode": null, + "Province": null, + "SerialNumber": "", + "StreetAddress": null + } + } +} +``` ### Hash Message Authentication Code (HMAC) authentication (`identity.hmac`) diff --git a/docs/user-guides.md b/docs/user-guides.md index e15ec455..dc1c0aca 100644 --- a/docs/user-guides.md +++ b/docs/user-guides.md @@ -9,6 +9,9 @@ Validate Kubernetes Service Account tokens to authenticate requests to your prot - **[Authentication with API keys](./user-guides/api-key-authentication.md)**
Issue API keys stored in Kubernetes `Secret`s for clients to authenticate with your protected hosts. +- **[Authentication with Mutual Transport Layer Security (mTLS)](./user-guides/mtls-authentication.md)**
+Verify client x509 certificates against trusted root CAs. + - **[OpenID Connect Discovery and authentication with JWTs](./user-guides/oidc-jwt-authentication.md)**
Validate JSON Web Tokens (JWT) issued and signed by an OpenID Connect server; leverage OpenID Connect Discovery to automatically fetch JSON Web Key Sets (JWKS). diff --git a/docs/user-guides/mtls-authentication.md b/docs/user-guides/mtls-authentication.md new file mode 100644 index 00000000..1a63b35c --- /dev/null +++ b/docs/user-guides/mtls-authentication.md @@ -0,0 +1,418 @@ +# User guide: Authentication with API keys + +Verify client x509 certificates against trusted root CAs stored in Kubernetes `Secret`s. + +
+ + Authorino features in this guide: + + + + Authorino can verify x509 certificates presented by clients for authentication on the request to the protected APIs, at application level. + + Trusted root Certificate Authorities (CA) are stored as Kubernetes `kubernetes.io/tls` Secrets labeled according to selectors specified in the AuthConfig, watched and cached by Authorino. + + For further details about Authorino features in general, check the [docs](./../features.md). +
+ +
+ +## Requirements + +- Kubernetes server +- [cert-manager](https://github.com/jetstack/cert-manager) + +Create a containerized Kubernetes server locally using [Kind](https://kind.sigs.k8s.io): + +```sh +kind create cluster --name authorino-tutorial +``` + +Install cert-manager in the cluster: + +```sh +kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml +``` + +## 1. Install the Authorino Operator + +```sh +kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +``` + +## 2. Deploy Authorino + +Create the TLS certificates for the Authorino service: + +```sh +curl -sSL https://raw.githubusercontent.com/Kuadrant/authorino/main/deploy/certs.yaml | sed "s/\$(AUTHORINO_INSTANCE)/authorino/g;s/\$(NAMESPACE)/default/g" | kubectl apply -f - +``` + +Deploy an Authorino service: + +```sh +kubectl apply -f -<