Skip to content

Commit cb81050

Browse files
committed
Allow Azure ClientCertificate authentication
Signed-off-by: Hidde Beydals <[email protected]>
1 parent 35594b0 commit cb81050

File tree

2 files changed

+91
-14
lines changed

2 files changed

+91
-14
lines changed

pkg/azure/blob.go

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ var (
4444
)
4545

4646
const (
47-
resourceIDField = "resourceId"
48-
clientIDField = "clientId"
49-
tenantIDField = "tenantId"
50-
clientSecretField = "clientSecret"
51-
accountKeyField = "accountKey"
47+
resourceIDField = "resourceId"
48+
clientIDField = "clientId"
49+
tenantIDField = "tenantId"
50+
clientSecretField = "clientSecret"
51+
clientCertificateField = "clientCertificate"
52+
clientCertificatePasswordField = "clientCertificatePassword"
53+
accountKeyField = "accountKey"
5254
)
5355

5456
// BlobClient is a minimal Azure Blob client for fetching objects.
@@ -62,13 +64,17 @@ type BlobClient struct {
6264
// order:
6365
//
6466
// - azidentity.ManagedIdentityCredential for a Resource ID, when a
65-
// resourceIDField is found.
66-
// - azidentity.ManagedIdentityCredential for a User ID, when a clientIDField
67-
// but no tenantIDField found.
68-
// - azidentity.ClientSecretCredential when a tenantIDField, clientIDField and
69-
// clientSecretField are found.
70-
// - azblob.SharedKeyCredential when an accountKeyField is found. The Account
71-
// Name is extracted from the endpoint specified on the Bucket object.
67+
// `resourceId` field is found.
68+
// - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
69+
// field but no `tenantId` is found.
70+
// - azidentity.ClientCertificateCredential when `tenantId`,
71+
// `clientCertificate` (and optionally `clientCertificatePassword`) fields
72+
// are found.
73+
// - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
74+
// `clientSecret` fields are found.
75+
// - azblob.SharedKeyCredential when an `accountKey` field is found.
76+
// The account name is extracted from the endpoint specified on the Bucket
77+
// object.
7278
//
7379
// If no credentials are found, a simple client without credentials is
7480
// returned.
@@ -119,6 +125,9 @@ func ValidateSecret(secret *corev1.Secret) error {
119125
if _, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret {
120126
valid = true
121127
}
128+
if _, hasClientCertificate := secret.Data[clientCertificateField]; hasClientCertificate {
129+
valid = true
130+
}
122131
}
123132
}
124133
if _, hasResourceID := secret.Data[resourceIDField]; hasResourceID {
@@ -132,8 +141,8 @@ func ValidateSecret(secret *corev1.Secret) error {
132141
}
133142

134143
if !valid {
135-
return fmt.Errorf("invalid '%s' secret data: requires a '%s', '%s', or '%s' field, or a combination of '%s', '%s' and '%s'",
136-
secret.Name, resourceIDField, clientIDField, accountKeyField, tenantIDField, clientIDField, clientSecretField)
144+
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'",
145+
secret.Name, resourceIDField, clientIDField, accountKeyField, tenantIDField, clientIDField, clientSecretField, tenantIDField, clientIDField, clientCertificateField)
137146
}
138147
return nil
139148
}
@@ -275,6 +284,13 @@ func tokenCredentialFromSecret(secret *corev1.Secret) (azcore.TokenCredential, e
275284
ID: azidentity.ClientID(clientID),
276285
})
277286
}
287+
if clientCertificate, hasClientCertificate := secret.Data[clientCertificateField]; hasClientCertificate {
288+
certs, key, err := azidentity.ParseCertificates(clientCertificate, secret.Data[clientCertificatePasswordField])
289+
if err != nil {
290+
return nil, fmt.Errorf("failed to parse client certificates: %w", err)
291+
}
292+
return azidentity.NewClientCertificateCredential(string(tenantID), string(clientID), certs, key, nil)
293+
}
278294
if clientSecret, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret {
279295
return azidentity.NewClientSecretCredential(string(tenantID), string(clientID), string(clientSecret), nil)
280296
}

pkg/azure/blob_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ limitations under the License.
1717
package azure
1818

1919
import (
20+
"bytes"
21+
"crypto/rand"
22+
"crypto/rsa"
23+
"crypto/x509"
24+
"encoding/pem"
2025
"errors"
2126
"fmt"
27+
"math/big"
2228
"testing"
2329

2430
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -50,6 +56,16 @@ func TestValidateSecret(t *testing.T) {
5056
},
5157
},
5258
},
59+
{
60+
name: "valid ServicePrincipal Certificate Secret",
61+
secret: &corev1.Secret{
62+
Data: map[string][]byte{
63+
tenantIDField: []byte("some-tenant-id-"),
64+
clientIDField: []byte("some-client-id-"),
65+
clientCertificateField: []byte("some-certificate"),
66+
},
67+
},
68+
},
5369
{
5470
name: "valid ServicePrincipal Secret",
5571
secret: &corev1.Secret{
@@ -192,6 +208,17 @@ func Test_tokenCredentialFromSecret(t *testing.T) {
192208
},
193209
want: &azidentity.ManagedIdentityCredential{},
194210
},
211+
{
212+
name: "with TenantID, ClientID and ClientCertificate fields",
213+
secret: &corev1.Secret{
214+
Data: map[string][]byte{
215+
clientIDField: []byte("client-id"),
216+
tenantIDField: []byte("tenant-id"),
217+
clientCertificateField: validTls(t),
218+
},
219+
},
220+
want: &azidentity.ClientCertificateCredential{},
221+
},
195222
{
196223
name: "with TenantID, ClientID and ClientSecret fields",
197224
secret: &corev1.Secret{
@@ -316,3 +343,37 @@ func Test_extractAccountNameFromEndpoint1(t *testing.T) {
316343
func endpointURL(accountName string) string {
317344
return fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
318345
}
346+
347+
func validTls(t *testing.T) []byte {
348+
key, err := rsa.GenerateKey(rand.Reader, 2048)
349+
if err != nil {
350+
t.Fatal("Private key cannot be created.", err.Error())
351+
}
352+
353+
out := bytes.NewBuffer(nil)
354+
355+
var privateKey = &pem.Block{
356+
Type: "PRIVATE KEY",
357+
Bytes: x509.MarshalPKCS1PrivateKey(key),
358+
}
359+
if err = pem.Encode(out, privateKey); err != nil {
360+
t.Fatal("Private key cannot be PEM encoded.", err.Error())
361+
}
362+
363+
certTemplate := x509.Certificate{
364+
SerialNumber: big.NewInt(1337),
365+
}
366+
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
367+
if err != nil {
368+
t.Fatal("Certificate cannot be created.", err.Error())
369+
}
370+
var certificate = &pem.Block{
371+
Type: "CERTIFICATE",
372+
Bytes: cert,
373+
}
374+
if err = pem.Encode(out, certificate); err != nil {
375+
t.Fatal("Certificate cannot be PEM encoded.", err.Error())
376+
}
377+
378+
return out.Bytes()
379+
}

0 commit comments

Comments
 (0)