Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mTLS authn method #301

Merged
merged 7 commits into from
Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Under the hood, Authorino is based on Kubernetes [Custom Resource Definitions](h
</tr>
<tr>
<td>mTLS authentication</td>
<td>Planned (<a href="https://github.com/kuadrant/authorino/issues/8">#8</a>)</td>
<td><i>Ready</i></td>
</tr>
<tr>
<td>HMAC authentication</td>
Expand Down
14 changes: 14 additions & 0 deletions api/v1beta1/auth_config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions controllers/auth_config_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
61 changes: 34 additions & 27 deletions controllers/secret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,22 @@ 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
return ctrl.Result{}, err
} 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")
Expand All @@ -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
}
Expand All @@ -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})
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions controllers/secret_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 32 additions & 6 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,39 @@ Online token introspection of OpenShift-valid access tokens based on OpenShift's

### Mutual Transport Layer Security (mTLS) authentication (`identity.mtls`)

<table>
<tr>
<td><small>Not implemented - Planned (<a href="https://github.com/kuadrant/authorino/issues/8">#8</a>)</small></td>
</tr>
</table>
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`)

Expand Down
3 changes: 3 additions & 0 deletions docs/user-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**<br/>
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)**<br/>
Verify client x509 certificates against trusted root CAs.

- **[OpenID Connect Discovery and authentication with JWTs](./user-guides/oidc-jwt-authentication.md)**<br/>
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).

Expand Down
Loading