mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
Add support for Authentication against Azure Key Vault using Client Certificate (#3469)
* Implementation of Certificate Based Authz against Azure Key Vault Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com> * Add tests for new Azure certificate auth functionality Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com> * Add documentation for Azure Cert based Auth Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com> * Generate spec.md Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com> * Add changes from code review Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com> * Fix naming in test error case Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com> --------- Signed-off-by: Luis Schweigard <luis.schweigard@gmail.com>
This commit is contained in:
parent
4b8b8788bf
commit
0abb3e9cc4
10 changed files with 420 additions and 38 deletions
|
@ -99,4 +99,8 @@ type AzureKVAuth struct {
|
|||
// The Azure ClientSecret of the service principle used for authentication.
|
||||
// +optional
|
||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret,omitempty"`
|
||||
|
||||
// The Azure ClientCertificate of the service principle used for authentication.
|
||||
// +optional
|
||||
ClientCertificate *smmeta.SecretKeySelector `json:"clientCertificate,omitempty"`
|
||||
}
|
||||
|
|
|
@ -329,6 +329,11 @@ func (in *AzureKVAuth) DeepCopyInto(out *AzureKVAuth) {
|
|||
*out = new(metav1.SecretKeySelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ClientCertificate != nil {
|
||||
in, out := &in.ClientCertificate, &out.ClientCertificate
|
||||
*out = new(metav1.SecretKeySelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKVAuth.
|
||||
|
|
|
@ -2144,6 +2144,25 @@ spec:
|
|||
with Azure. Required for ServicePrincipal auth type. Optional
|
||||
for WorkloadIdentity.
|
||||
properties:
|
||||
clientCertificate:
|
||||
description: The Azure ClientCertificate of the service
|
||||
principle used for authentication.
|
||||
properties:
|
||||
key:
|
||||
description: |-
|
||||
The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
|
||||
defaulted, in others it may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
|
||||
to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
clientId:
|
||||
description: The Azure clientId of the service principle
|
||||
or managed identity used for authentication.
|
||||
|
|
|
@ -2144,6 +2144,25 @@ spec:
|
|||
with Azure. Required for ServicePrincipal auth type. Optional
|
||||
for WorkloadIdentity.
|
||||
properties:
|
||||
clientCertificate:
|
||||
description: The Azure ClientCertificate of the service
|
||||
principle used for authentication.
|
||||
properties:
|
||||
key:
|
||||
description: |-
|
||||
The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
|
||||
defaulted, in others it may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
|
||||
to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
clientId:
|
||||
description: The Azure clientId of the service principle
|
||||
or managed identity used for authentication.
|
||||
|
|
|
@ -2665,6 +2665,23 @@ spec:
|
|||
authSecretRef:
|
||||
description: Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
|
||||
properties:
|
||||
clientCertificate:
|
||||
description: The Azure ClientCertificate of the service principle used for authentication.
|
||||
properties:
|
||||
key:
|
||||
description: |-
|
||||
The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
|
||||
defaulted, in others it may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
|
||||
to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
clientId:
|
||||
description: The Azure clientId of the service principle or managed identity used for authentication.
|
||||
properties:
|
||||
|
@ -8010,6 +8027,23 @@ spec:
|
|||
authSecretRef:
|
||||
description: Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
|
||||
properties:
|
||||
clientCertificate:
|
||||
description: The Azure ClientCertificate of the service principle used for authentication.
|
||||
properties:
|
||||
key:
|
||||
description: |-
|
||||
The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
|
||||
defaulted, in others it may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
|
||||
to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
clientId:
|
||||
description: The Azure clientId of the service principle or managed identity used for authentication.
|
||||
properties:
|
||||
|
|
|
@ -869,6 +869,20 @@ External Secrets meta/v1.SecretKeySelector
|
|||
<p>The Azure ClientSecret of the service principle used for authentication.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>clientCertificate</code></br>
|
||||
<em>
|
||||
<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
|
||||
External Secrets meta/v1.SecretKeySelector
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The Azure ClientCertificate of the service principle used for authentication.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1beta1.AzureKVProvider">AzureKVProvider
|
||||
|
|
|
@ -34,7 +34,7 @@ az keyvault set-policy --name kv-name-with-certs --object-id "$KUBELET_IDENTITY_
|
|||
|
||||
#### Service Principal key authentication
|
||||
|
||||
A service Principal client and Secret is created and the JSON keyfile is stored in a `Kind=Secret`. The `ClientID` and `ClientSecret` should be configured for the secret. This service principal should have proper access rights to the keyvault to be managed by the operator
|
||||
A service Principal client and Secret is created and the JSON keyfile is stored in a `Kind=Secret`. The `ClientID` and `ClientSecret` or `ClientCertificate` (in PEM format) should be configured for the secret. This service principal should have proper access rights to the keyvault to be managed by the operator.
|
||||
|
||||
#### Managed Identity authentication
|
||||
|
||||
|
|
|
@ -64,30 +64,32 @@ const (
|
|||
AnnotationTenantID = "azure.workload.identity/tenant-id"
|
||||
managerLabel = "external-secrets"
|
||||
|
||||
errUnexpectedStoreSpec = "unexpected store spec"
|
||||
errMissingAuthType = "cannot initialize Azure Client: no valid authType was specified"
|
||||
errPropNotExist = "property %s does not exist in key %s"
|
||||
errTagNotExist = "tag %s does not exist"
|
||||
errUnknownObjectType = "unknown Azure Keyvault object Type for %s"
|
||||
errUnmarshalJSONData = "error unmarshalling json data: %w"
|
||||
errDataFromCert = "cannot get use dataFrom to get certificate secret"
|
||||
errDataFromKey = "cannot get use dataFrom to get key secret"
|
||||
errMissingTenant = "missing tenantID in store config"
|
||||
errMissingClient = "missing clientID: either serviceAccountRef or service account annotation '%s' is missing"
|
||||
errMissingSecretRef = "missing secretRef in provider config"
|
||||
errMissingClientIDSecret = "missing accessKeyID/secretAccessKey in store config"
|
||||
errMultipleClientID = "multiple clientID found. Check secretRef and serviceAccountRef"
|
||||
errMultipleTenantID = "multiple tenantID found. Check secretRef, 'spec.provider.azurekv.tenantId', and serviceAccountRef"
|
||||
errFindSecret = "could not find secret %s/%s: %w"
|
||||
errFindDataKey = "no data for %q in secret '%s/%s'"
|
||||
errUnexpectedStoreSpec = "unexpected store spec"
|
||||
errMissingAuthType = "cannot initialize Azure Client: no valid authType was specified"
|
||||
errPropNotExist = "property %s does not exist in key %s"
|
||||
errTagNotExist = "tag %s does not exist"
|
||||
errUnknownObjectType = "unknown Azure Keyvault object Type for %s"
|
||||
errUnmarshalJSONData = "error unmarshalling json data: %w"
|
||||
errDataFromCert = "cannot get use dataFrom to get certificate secret"
|
||||
errDataFromKey = "cannot get use dataFrom to get key secret"
|
||||
errMissingTenant = "missing tenantID in store config"
|
||||
errMissingClient = "missing clientID: either serviceAccountRef or service account annotation '%s' is missing"
|
||||
errMissingSecretRef = "missing secretRef in provider config"
|
||||
errMissingClientIDSecret = "missing accessKeyID/secretAccessKey in store config"
|
||||
errInvalidClientCredentials = "both clientSecret and clientCredentials set"
|
||||
errMultipleClientID = "multiple clientID found. Check secretRef and serviceAccountRef"
|
||||
errMultipleTenantID = "multiple tenantID found. Check secretRef, 'spec.provider.azurekv.tenantId', and serviceAccountRef"
|
||||
errFindSecret = "could not find secret %s/%s: %w"
|
||||
errFindDataKey = "no data for %q in secret '%s/%s'"
|
||||
|
||||
errInvalidStore = "invalid store"
|
||||
errInvalidStoreSpec = "invalid store spec"
|
||||
errInvalidStoreProv = "invalid store provider"
|
||||
errInvalidAzureProv = "invalid azure keyvault provider"
|
||||
errInvalidSecRefClientID = "invalid AuthSecretRef.ClientID: %w"
|
||||
errInvalidSecRefClientSecret = "invalid AuthSecretRef.ClientSecret: %w"
|
||||
errInvalidSARef = "invalid ServiceAccountRef: %w"
|
||||
errInvalidStore = "invalid store"
|
||||
errInvalidStoreSpec = "invalid store spec"
|
||||
errInvalidStoreProv = "invalid store provider"
|
||||
errInvalidAzureProv = "invalid azure keyvault provider"
|
||||
errInvalidSecRefClientID = "invalid AuthSecretRef.ClientID: %w"
|
||||
errInvalidSecRefClientSecret = "invalid AuthSecretRef.ClientSecret: %w"
|
||||
errInvalidSecRefClientCertificate = "invalid AuthSecretRef.ClientCertificate: %w"
|
||||
errInvalidSARef = "invalid ServiceAccountRef: %w"
|
||||
|
||||
errMissingWorkloadEnvVars = "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set"
|
||||
errReadTokenFile = "unable to read token file %s: %w"
|
||||
|
@ -877,7 +879,7 @@ func (a *Azure) authorizerForWorkloadIdentity(ctx context.Context, tokenProvider
|
|||
}
|
||||
}
|
||||
}
|
||||
// Check if spec.provider.azurekv.tenantId is set
|
||||
// Check if spec.provider.azurekv.tenantID is set
|
||||
if tenantID == "" && a.provider.TenantID != nil {
|
||||
tenantID = *a.provider.TenantID
|
||||
}
|
||||
|
@ -979,31 +981,81 @@ func (a *Azure) authorizerForServicePrincipal(ctx context.Context) (autorest.Aut
|
|||
if a.provider.AuthSecretRef == nil {
|
||||
return nil, fmt.Errorf(errMissingSecretRef)
|
||||
}
|
||||
if a.provider.AuthSecretRef.ClientID == nil || a.provider.AuthSecretRef.ClientSecret == nil {
|
||||
if a.provider.AuthSecretRef.ClientID == nil || (a.provider.AuthSecretRef.ClientSecret == nil && a.provider.AuthSecretRef.ClientCertificate == nil) {
|
||||
return nil, fmt.Errorf(errMissingClientIDSecret)
|
||||
}
|
||||
if a.provider.AuthSecretRef.ClientSecret != nil && a.provider.AuthSecretRef.ClientCertificate != nil {
|
||||
return nil, fmt.Errorf(errInvalidClientCredentials)
|
||||
}
|
||||
|
||||
return a.getAuthorizerFromCredentials(ctx)
|
||||
}
|
||||
|
||||
func (a *Azure) getAuthorizerFromCredentials(ctx context.Context) (autorest.Authorizer, error) {
|
||||
clientID, err := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
a.crClient,
|
||||
a.store.GetKind(),
|
||||
a.namespace, a.provider.AuthSecretRef.ClientID)
|
||||
a.namespace, a.provider.AuthSecretRef.ClientID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientSecret, err := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
a.crClient,
|
||||
a.store.GetKind(),
|
||||
a.namespace, a.provider.AuthSecretRef.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
if a.provider.AuthSecretRef.ClientSecret != nil {
|
||||
clientSecret, err := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
a.crClient,
|
||||
a.store.GetKind(),
|
||||
a.namespace, a.provider.AuthSecretRef.ClientSecret,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getAuthorizerForClientSecret(
|
||||
clientID,
|
||||
clientSecret,
|
||||
*a.provider.TenantID,
|
||||
a.provider.EnvironmentType,
|
||||
)
|
||||
} else {
|
||||
clientCertificate, err := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
a.crClient,
|
||||
a.store.GetKind(),
|
||||
a.namespace, a.provider.AuthSecretRef.ClientCertificate,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getAuthorizerForClientCertificate(
|
||||
clientID,
|
||||
[]byte(clientCertificate),
|
||||
*a.provider.TenantID,
|
||||
a.provider.EnvironmentType,
|
||||
)
|
||||
}
|
||||
clientCredentialsConfig := kvauth.NewClientCredentialsConfig(clientID, clientSecret, *a.provider.TenantID)
|
||||
clientCredentialsConfig.Resource = kvResourceForProviderConfig(a.provider.EnvironmentType)
|
||||
clientCredentialsConfig.AADEndpoint = AadEndpointForType(a.provider.EnvironmentType)
|
||||
}
|
||||
|
||||
func getAuthorizerForClientSecret(clientID, clientSecret, tenantID string, environmentType esv1beta1.AzureEnvironmentType) (autorest.Authorizer, error) {
|
||||
clientCredentialsConfig := kvauth.NewClientCredentialsConfig(clientID, clientSecret, tenantID)
|
||||
clientCredentialsConfig.Resource = kvResourceForProviderConfig(environmentType)
|
||||
clientCredentialsConfig.AADEndpoint = AadEndpointForType(environmentType)
|
||||
return clientCredentialsConfig.Authorizer()
|
||||
}
|
||||
|
||||
func getAuthorizerForClientCertificate(clientID string, certificateBytes []byte, tenantID string, environmentType esv1beta1.AzureEnvironmentType) (autorest.Authorizer, error) {
|
||||
clientCertificateConfig := NewClientInMemoryCertificateConfig(clientID, certificateBytes, tenantID)
|
||||
clientCertificateConfig.Resource = kvResourceForProviderConfig(environmentType)
|
||||
clientCertificateConfig.AADEndpoint = AadEndpointForType(environmentType)
|
||||
return clientCertificateConfig.Authorizer()
|
||||
}
|
||||
|
||||
func (a *Azure) Close(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -37,6 +37,32 @@ import (
|
|||
|
||||
var vaultURL = "https://local.vault.url"
|
||||
|
||||
var mockCertificate = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICBzCCAbGgAwIBAgIUSoCD1fgywDbmeRaGrkYzGWUd1wMwDQYJKoZIhvcNAQEL
|
||||
BQAwcTELMAkGA1UEBhMCQVoxGTAXBgNVBAgMEE1vY2sgQ2VydGlmaWNhdGUxMzAx
|
||||
BgNVBAoMKkV4dGVybmFsIFNlY3JldHMgT3BlcmF0b3IgTW9jayBDZXJ0aWZpY2F0
|
||||
ZTESMBAGA1UEAwwJTW9jayBDZXJ0MB4XDTI0MDUwODA4NDkzMFoXDTI1MDUwODA4
|
||||
NDkzMFowcTELMAkGA1UEBhMCQVoxGTAXBgNVBAgMEE1vY2sgQ2VydGlmaWNhdGUx
|
||||
MzAxBgNVBAoMKkV4dGVybmFsIFNlY3JldHMgT3BlcmF0b3IgTW9jayBDZXJ0aWZp
|
||||
Y2F0ZTESMBAGA1UEAwwJTW9jayBDZXJ0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJB
|
||||
ALkU1YgMk1Dk149F/HsHA0TjzLwfDa9tT0cfqA1u0hoJkb2r9jdWUyiugGaEz/PU
|
||||
TGWrvp8aiXPrGuu5Y6PY27ECAwEAAaMhMB8wHQYDVR0OBBYEFAMB0YwnYjUm00og
|
||||
kGce8Yhr4I03MA0GCSqGSIb3DQEBCwUAA0EAr0BMs/3hIOdZc0WHZUCTZ0GGor3G
|
||||
ViYUPHOw8z6UZGPGN6qiAejmkT6uP3LkkSW+7TIIQ1pkQxcn5xfFJXBexw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuRTViAyTUOTXj0X8
|
||||
ewcDROPMvB8Nr21PRx+oDW7SGgmRvav2N1ZTKK6AZoTP89RMZau+nxqJc+sa67lj
|
||||
o9jbsQIDAQABAkA35CnDpwCJykGqW5kuUeTT1fMK0FnioyDwuoeWXuQFxmB6Md89
|
||||
+ABxyjAt3nmwRRVBrVFdNibb9asR5KFHwn1NAiEA4NlrSnJrY1xODIjEXf0fLTwu
|
||||
wpyUO1lX585OjYDiOYsCIQDSuP4ttH/1Hg3f9veEE4RgDEk+QcisrzF8q4Oa5sDP
|
||||
MwIgfejiTtcR0ZsPza8Mn0EuIyuPV8VMsItQUWtSy6R/ig8CIQC86cBmNUXp+HGz
|
||||
8fLg46ZvfVREjjFcLwwMmq83tdvxZQIgPAbezuRCrduH19xgMO8BXndS5DAovgvE
|
||||
/MpQnEyQtVA=
|
||||
-----END PRIVATE KEY-----
|
||||
`
|
||||
|
||||
func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
|
||||
namespace := "internal"
|
||||
identityID := "1234"
|
||||
|
@ -405,7 +431,7 @@ func TestAuth(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "correct cluster secret store",
|
||||
name: "correct cluster secret store with ClientSecret",
|
||||
objects: []client.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "password",
|
||||
|
@ -432,6 +458,94 @@ func TestAuth(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad config: both clientSecret and clientCredentials are configured",
|
||||
expErr: "both clientSecret and clientCredentials set",
|
||||
objects: []client.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "password",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"id": []byte("foo"),
|
||||
"certificate": []byte("bar"),
|
||||
"secret": []byte("bar"),
|
||||
},
|
||||
}},
|
||||
store: &esv1beta1.ClusterSecretStore{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: esv1beta1.ClusterSecretStoreKind,
|
||||
},
|
||||
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
||||
},
|
||||
provider: &esv1beta1.AzureKVProvider{
|
||||
AuthType: &authType,
|
||||
VaultURL: &vaultURL,
|
||||
TenantID: pointer.To("mytenant"),
|
||||
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
||||
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "id"},
|
||||
ClientCertificate: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "certificate"},
|
||||
ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad config: no valid client certificate in pem file",
|
||||
expErr: "failed to get oauth token from certificate auth: failed to decode certificate: no certificate found in PEM file",
|
||||
objects: []client.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "password",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"id": []byte("foo"),
|
||||
"certificate": []byte("bar"),
|
||||
},
|
||||
}},
|
||||
store: &esv1beta1.ClusterSecretStore{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: esv1beta1.ClusterSecretStoreKind,
|
||||
},
|
||||
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
||||
},
|
||||
provider: &esv1beta1.AzureKVProvider{
|
||||
AuthType: &authType,
|
||||
VaultURL: &vaultURL,
|
||||
TenantID: pointer.To("mytenant"),
|
||||
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
||||
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "id"},
|
||||
ClientCertificate: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "certificate"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "correct configuration with certificate authentication",
|
||||
objects: []client.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "password",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"id": []byte("foo"),
|
||||
"certificate": []byte(mockCertificate),
|
||||
},
|
||||
}},
|
||||
store: &esv1beta1.ClusterSecretStore{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: esv1beta1.ClusterSecretStoreKind,
|
||||
},
|
||||
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
||||
},
|
||||
provider: &esv1beta1.AzureKVProvider{
|
||||
AuthType: &authType,
|
||||
VaultURL: &vaultURL,
|
||||
TenantID: pointer.To("mytenant"),
|
||||
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
||||
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "id"},
|
||||
ClientCertificate: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "certificate"},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
k8sClient := clientfake.NewClientBuilder().WithObjects(row.objects...).Build()
|
||||
|
|
121
pkg/provider/azure/keyvault/keyvault_certificate.go
Normal file
121
pkg/provider/azure/keyvault/keyvault_certificate.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
// /*
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// */
|
||||
package keyvault
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
)
|
||||
|
||||
// ClientInMemoryCertificateConfig struct includes a Certificate field to hold the certificate data as a byte slice.
|
||||
type ClientInMemoryCertificateConfig struct {
|
||||
ClientID string
|
||||
Certificate []byte // Certificate data as a byte slice
|
||||
TenantID string
|
||||
AuxTenants []string
|
||||
AADEndpoint string
|
||||
Resource string
|
||||
}
|
||||
|
||||
func NewClientInMemoryCertificateConfig(clientID string, certificate []byte, tenantID string) ClientInMemoryCertificateConfig {
|
||||
return ClientInMemoryCertificateConfig{
|
||||
ClientID: clientID,
|
||||
Certificate: certificate,
|
||||
TenantID: tenantID,
|
||||
Resource: azure.PublicCloud.ResourceManagerEndpoint,
|
||||
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// ServicePrincipalToken creates a adal.ServicePrincipalToken from client certificate using the certificate byte slice.
|
||||
func (ccc ClientInMemoryCertificateConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Use the byte slice directly instead of reading from a file
|
||||
certificate, rsaPrivateKey, err := loadCertificateFromBytes(ccc.Certificate)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode certificate: %w", err)
|
||||
}
|
||||
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
|
||||
}
|
||||
|
||||
func loadCertificateFromBytes(certificateBytes []byte) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
var cert *x509.Certificate
|
||||
var privateKey *rsa.PrivateKey
|
||||
var err error
|
||||
|
||||
// Extract certificate and private key
|
||||
for {
|
||||
block, rest := pem.Decode(certificateBytes)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type == "CERTIFICATE" {
|
||||
cert, err = x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse PEM certificate: %w", err)
|
||||
}
|
||||
} else {
|
||||
privateKey, err = parsePrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to extract private key from PEM certificate: %w", err)
|
||||
}
|
||||
}
|
||||
certificateBytes = rest
|
||||
}
|
||||
|
||||
if cert == nil {
|
||||
return nil, nil, errors.New("no certificate found in PEM file")
|
||||
}
|
||||
|
||||
if privateKey == nil {
|
||||
return nil, nil, errors.New("no private key found in PEM file")
|
||||
}
|
||||
|
||||
return cert, privateKey, nil
|
||||
}
|
||||
|
||||
func parsePrivateKey(der []byte) (*rsa.PrivateKey, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("found unknown private key type in PKCS#8 wrapping")
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to parse private key")
|
||||
}
|
||||
|
||||
// Implementation of the AuthorizerConfig interface.
|
||||
func (ccc ClientInMemoryCertificateConfig) Authorizer() (autorest.Authorizer, error) {
|
||||
spToken, err := ccc.ServicePrincipalToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %w", err)
|
||||
}
|
||||
return autorest.NewBearerAuthorizer(spToken), nil
|
||||
}
|
Loading…
Reference in a new issue