Allow Azure ClientCertificate authentication

This commit allows for a Secret to be configured with `tenantId`,
`clientId` and `clientCertificate` data fields (with optionally
`clientCertificatePassword`) to authenticate using TLS.

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2022-03-03 12:19:52 +01:00
parent 94c8185d87
commit bd12cdba17
2 changed files with 91 additions and 14 deletions

View File

@ -48,6 +48,8 @@ const (
clientIDField = "clientId" clientIDField = "clientId"
tenantIDField = "tenantId" tenantIDField = "tenantId"
clientSecretField = "clientSecret" clientSecretField = "clientSecret"
clientCertificateField = "clientCertificate"
clientCertificatePasswordField = "clientCertificatePassword"
accountKeyField = "accountKey" accountKeyField = "accountKey"
) )
@ -62,13 +64,17 @@ type BlobClient struct {
// order: // order:
// //
// - azidentity.ManagedIdentityCredential for a Resource ID, when a // - azidentity.ManagedIdentityCredential for a Resource ID, when a
// resourceIDField is found. // `resourceId` field is found.
// - azidentity.ManagedIdentityCredential for a User ID, when a clientIDField // - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
// but no tenantIDField found. // field but no `tenantId` is found.
// - azidentity.ClientSecretCredential when a tenantIDField, clientIDField and // - azidentity.ClientCertificateCredential when `tenantId`,
// clientSecretField are found. // `clientCertificate` (and optionally `clientCertificatePassword`) fields
// - azblob.SharedKeyCredential when an accountKeyField is found. The Account // are found.
// Name is extracted from the endpoint specified on the Bucket object. // - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
// `clientSecret` fields are found.
// - azblob.SharedKeyCredential when an `accountKey` field is found.
// The account name is extracted from the endpoint specified on the Bucket
// object.
// //
// If no credentials are found, a simple client without credentials is // If no credentials are found, a simple client without credentials is
// returned. // returned.
@ -119,6 +125,9 @@ func ValidateSecret(secret *corev1.Secret) error {
if _, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret { if _, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret {
valid = true valid = true
} }
if _, hasClientCertificate := secret.Data[clientCertificateField]; hasClientCertificate {
valid = true
}
} }
} }
if _, hasResourceID := secret.Data[resourceIDField]; hasResourceID { if _, hasResourceID := secret.Data[resourceIDField]; hasResourceID {
@ -132,8 +141,8 @@ func ValidateSecret(secret *corev1.Secret) error {
} }
if !valid { if !valid {
return fmt.Errorf("invalid '%s' secret data: requires a '%s', '%s', or '%s' field, or a combination of '%s', '%s' and '%s'", return fmt.Errorf("invalid '%s' secret data: requires a '%s', '%s', or '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'",
secret.Name, resourceIDField, clientIDField, accountKeyField, tenantIDField, clientIDField, clientSecretField) secret.Name, resourceIDField, clientIDField, accountKeyField, tenantIDField, clientIDField, clientSecretField, tenantIDField, clientIDField, clientCertificateField)
} }
return nil return nil
} }
@ -275,6 +284,13 @@ func tokenCredentialFromSecret(secret *corev1.Secret) (azcore.TokenCredential, e
ID: azidentity.ClientID(clientID), ID: azidentity.ClientID(clientID),
}) })
} }
if clientCertificate, hasClientCertificate := secret.Data[clientCertificateField]; hasClientCertificate {
certs, key, err := azidentity.ParseCertificates(clientCertificate, secret.Data[clientCertificatePasswordField])
if err != nil {
return nil, fmt.Errorf("failed to parse client certificates: %w", err)
}
return azidentity.NewClientCertificateCredential(string(tenantID), string(clientID), certs, key, nil)
}
if clientSecret, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret { if clientSecret, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret {
return azidentity.NewClientSecretCredential(string(tenantID), string(clientID), string(clientSecret), nil) return azidentity.NewClientSecretCredential(string(tenantID), string(clientID), string(clientSecret), nil)
} }

View File

@ -17,8 +17,14 @@ limitations under the License.
package azure package azure
import ( import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"math/big"
"testing" "testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore"
@ -50,6 +56,16 @@ func TestValidateSecret(t *testing.T) {
}, },
}, },
}, },
{
name: "valid ServicePrincipal Certificate Secret",
secret: &corev1.Secret{
Data: map[string][]byte{
tenantIDField: []byte("some-tenant-id-"),
clientIDField: []byte("some-client-id-"),
clientCertificateField: []byte("some-certificate"),
},
},
},
{ {
name: "valid ServicePrincipal Secret", name: "valid ServicePrincipal Secret",
secret: &corev1.Secret{ secret: &corev1.Secret{
@ -192,6 +208,17 @@ func Test_tokenCredentialFromSecret(t *testing.T) {
}, },
want: &azidentity.ManagedIdentityCredential{}, want: &azidentity.ManagedIdentityCredential{},
}, },
{
name: "with TenantID, ClientID and ClientCertificate fields",
secret: &corev1.Secret{
Data: map[string][]byte{
clientIDField: []byte("client-id"),
tenantIDField: []byte("tenant-id"),
clientCertificateField: validTls(t),
},
},
want: &azidentity.ClientCertificateCredential{},
},
{ {
name: "with TenantID, ClientID and ClientSecret fields", name: "with TenantID, ClientID and ClientSecret fields",
secret: &corev1.Secret{ secret: &corev1.Secret{
@ -316,3 +343,37 @@ func Test_extractAccountNameFromEndpoint1(t *testing.T) {
func endpointURL(accountName string) string { func endpointURL(accountName string) string {
return fmt.Sprintf("https://%s.blob.core.windows.net", accountName) return fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
} }
func validTls(t *testing.T) []byte {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal("Private key cannot be created.", err.Error())
}
out := bytes.NewBuffer(nil)
var privateKey = &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
if err = pem.Encode(out, privateKey); err != nil {
t.Fatal("Private key cannot be PEM encoded.", err.Error())
}
certTemplate := x509.Certificate{
SerialNumber: big.NewInt(1337),
}
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
if err != nil {
t.Fatal("Certificate cannot be created.", err.Error())
}
var certificate = &pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
}
if err = pem.Encode(out, certificate); err != nil {
t.Fatal("Certificate cannot be PEM encoded.", err.Error())
}
return out.Bytes()
}