Skip to content

Commit 720509a

Browse files
authored
Merge pull request #5200 from bryan-cox/client-cert
Add the ability to auth via certs without storing them in etcd secret
2 parents 0957852 + a32c54f commit 720509a

File tree

8 files changed

+162
-27
lines changed

8 files changed

+162
-27
lines changed

api/v1beta1/azureclusteridentity_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ type AzureClusterIdentitySpec struct {
5959
// ClientSecret is a secret reference which should contain either a Service Principal password or certificate secret.
6060
// +optional
6161
ClientSecret corev1.SecretReference `json:"clientSecret,omitempty"`
62+
// CertPath is the path where certificates exist. When set, it takes precedence over ClientSecret for types that use certs like ServicePrincipalCertificate.
63+
// +optional
64+
CertPath string `json:"certPath,omitempty"`
6265
// TenantID is the service principal primary tenant id.
6366
TenantID string `json:"tenantID"`
6467
// AllowedNamespaces is used to identify the namespaces the clusters are allowed to use the identity from.

azure/scope/identity.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package scope
1818

1919
import (
2020
"context"
21+
"os"
2122
"reflect"
2223

2324
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -127,11 +128,20 @@ func (p *AzureCredentialsProvider) GetTokenCredential(ctx context.Context, resou
127128
cred, authErr = azidentity.NewClientSecretCredential(p.GetTenantID(), p.Identity.Spec.ClientID, clientSecret, &options)
128129

129130
case infrav1.ServicePrincipalCertificate:
130-
clientSecret, err := p.GetClientSecret(ctx)
131-
if err != nil {
132-
return nil, errors.Wrap(err, "failed to get client secret")
131+
var certsContent []byte
132+
if p.Identity.Spec.CertPath != "" {
133+
certsContent, err = os.ReadFile(p.Identity.Spec.CertPath)
134+
if err != nil {
135+
return nil, errors.Wrap(err, "failed to read certificate file")
136+
}
137+
} else {
138+
clientSecret, err := p.GetClientSecret(ctx)
139+
if err != nil {
140+
return nil, errors.Wrap(err, "failed to get client secret")
141+
}
142+
certsContent = []byte(clientSecret)
133143
}
134-
certs, key, err := azidentity.ParseCertificates([]byte(clientSecret), nil)
144+
certs, key, err := azidentity.ParseCertificates(certsContent, nil)
135145
if err != nil {
136146
return nil, errors.Wrap(err, "failed to parse certificate data")
137147
}
@@ -200,7 +210,7 @@ func (p *AzureCredentialsProvider) Type() infrav1.IdentityType {
200210
// This does not include managed identities.
201211
func (p *AzureCredentialsProvider) hasClientSecret() bool {
202212
switch p.Identity.Spec.Type {
203-
case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal, infrav1.ServicePrincipalCertificate:
213+
case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal:
204214
return true
205215
default:
206216
return false

azure/scope/identity_test.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,10 @@ func TestHasClientSecret(t *testing.T) {
163163
name: "service principal with certificate",
164164
identity: &infrav1.AzureClusterIdentity{
165165
Spec: infrav1.AzureClusterIdentitySpec{
166-
Type: infrav1.ServicePrincipalCertificate,
167-
ClientSecret: corev1.SecretReference{Name: "my-client-secret"},
166+
Type: infrav1.ServicePrincipalCertificate,
168167
},
169168
},
170-
want: true,
169+
want: false,
171170
},
172171
{
173172
name: "manual service principal",
@@ -302,9 +301,7 @@ func TestGetTokenCredential(t *testing.T) {
302301
Spec: infrav1.AzureClusterIdentitySpec{
303302
Type: infrav1.ServicePrincipalCertificate,
304303
TenantID: fakeTenantID,
305-
ClientSecret: corev1.SecretReference{
306-
Name: "test-identity-secret",
307-
},
304+
CertPath: "../../test/setup/certificate",
308305
},
309306
},
310307
secret: &corev1.Secret{
@@ -316,6 +313,25 @@ func TestGetTokenCredential(t *testing.T) {
316313
},
317314
},
318315
},
316+
{
317+
name: "service principal certificate with certificate filepath",
318+
cluster: &infrav1.AzureCluster{
319+
Spec: infrav1.AzureClusterSpec{
320+
AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
321+
IdentityRef: &corev1.ObjectReference{
322+
Kind: infrav1.AzureClusterIdentityKind,
323+
},
324+
},
325+
},
326+
},
327+
identity: &infrav1.AzureClusterIdentity{
328+
Spec: infrav1.AzureClusterIdentitySpec{
329+
Type: infrav1.ServicePrincipalCertificate,
330+
TenantID: fakeTenantID,
331+
CertPath: "../../test/setup/certificate",
332+
},
333+
},
334+
},
319335
{
320336
name: "user-assigned identity",
321337
cluster: &infrav1.AzureCluster{

config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusteridentities.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ spec:
123123
type: object
124124
x-kubernetes-map-type: atomic
125125
type: object
126+
certPath:
127+
description: CertPath is the path where certificates exist. When set,
128+
it takes precedence over ClientSecret for types that use certs like
129+
ServicePrincipalCertificate.
130+
type: string
126131
clientID:
127132
description: |-
128133
ClientID is the service principal client ID.

controllers/asosecret_controller.go

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controllers
1919
import (
2020
"context"
2121
"fmt"
22+
"os"
2223

2324
asoconfig "github.com/Azure/azure-service-operator/v2/pkg/common/config"
2425
"github.com/pkg/errors"
@@ -287,23 +288,33 @@ func (asos *ASOSecretReconciler) createSecretFromClusterIdentity(ctx context.Con
287288
return newASOSecret, nil
288289
}
289290

290-
// Fetch identity secret, if it exists
291-
key = types.NamespacedName{
292-
Namespace: identity.Spec.ClientSecret.Namespace,
293-
Name: identity.Spec.ClientSecret.Name,
294-
}
295-
identitySecret := &corev1.Secret{}
296-
err := asos.Get(ctx, key, identitySecret)
297-
if err != nil {
298-
return nil, errors.Wrap(err, "failed to fetch AzureClusterIdentity secret")
299-
}
291+
if identity.Spec.CertPath != "" {
292+
certsContent, err := os.ReadFile(identity.Spec.CertPath)
293+
if err != nil {
294+
return nil, errors.Wrap(err, "failed to read certificate file")
295+
}
300296

301-
switch identity.Spec.Type {
302-
case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal:
303-
newASOSecret.Data[asoconfig.AzureClientSecret] = identitySecret.Data[scope.AzureSecretKey]
304-
case infrav1.ServicePrincipalCertificate:
305-
newASOSecret.Data[asoconfig.AzureClientCertificate] = identitySecret.Data["certificate"]
306-
newASOSecret.Data[asoconfig.AzureClientCertificatePassword] = identitySecret.Data["password"]
297+
newASOSecret.Data[asoconfig.AzureClientCertificate] = certsContent
298+
newASOSecret.Data[asoconfig.AzureClientCertificatePassword] = []byte{}
299+
} else {
300+
// Fetch identity secret, if it exists
301+
key = types.NamespacedName{
302+
Namespace: identity.Spec.ClientSecret.Namespace,
303+
Name: identity.Spec.ClientSecret.Name,
304+
}
305+
identitySecret := &corev1.Secret{}
306+
err := asos.Get(ctx, key, identitySecret)
307+
if err != nil {
308+
return nil, errors.Wrap(err, "failed to fetch AzureClusterIdentity secret")
309+
}
310+
311+
switch identity.Spec.Type {
312+
case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal:
313+
newASOSecret.Data[asoconfig.AzureClientSecret] = identitySecret.Data[scope.AzureSecretKey]
314+
case infrav1.ServicePrincipalCertificate:
315+
newASOSecret.Data[asoconfig.AzureClientCertificate] = identitySecret.Data["certificate"]
316+
newASOSecret.Data[asoconfig.AzureClientCertificatePassword] = identitySecret.Data["password"]
317+
}
307318
}
308319
return newASOSecret, nil
309320
}

controllers/asosecret_controller_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,31 @@ func TestASOSecretReconcile(t *testing.T) {
135135
}
136136
}),
137137
},
138+
"should reconcile normally for AzureManagedControlPlane with IdentityRef configured of type Service Principal with Certificate": {
139+
clusterName: defaultAzureManagedControlPlane.Name,
140+
objects: []runtime.Object{
141+
getASOAzureManagedControlPlane(func(c *infrav1.AzureManagedControlPlane) {
142+
c.Spec.IdentityRef = &corev1.ObjectReference{
143+
Name: "my-azure-cluster-identity",
144+
Namespace: "default",
145+
}
146+
}),
147+
getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
148+
identity.Spec.Type = infrav1.ServicePrincipalCertificate
149+
identity.Spec.CertPath = "../test/setup/certificate"
150+
}),
151+
defaultCluster,
152+
},
153+
asoSecret: getASOSecret(defaultAzureManagedControlPlane, func(s *corev1.Secret) {
154+
s.Data = map[string][]byte{
155+
"AZURE_SUBSCRIPTION_ID": []byte("fooSubscription"),
156+
"AZURE_TENANT_ID": []byte("fooTenant"),
157+
"AZURE_CLIENT_ID": []byte("fooClient"),
158+
"AZURE_CLIENT_CERTIFICATE_PASSWORD": []byte(""),
159+
"AZURE_CLIENT_CERTIFICATE": []byte("-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjrdEr9P0TaUES\ndspE6cyo22NU8yhRrbYlV9VH2vWvnPsThXcxhnd+cUqdNEBswhwgFlUQcg/eSVxw\nrr+3nh+bFTZWPcY+1LQYxfpKGsrCXQfB82LDJIZDX4gHYrWf3Z272jXN1XeFAKti\nwDKgDXXuPH7r5lH7vC3RXeAffqLwQJhZf+NoHNtv9MH9IdUkQfmDFZtI/CQzCrb6\n+vOS6EmUD/Q2FNHBzgxCguGqgNyBcQbxJ9Qng+ZjIFuhGYXJlsyRUtexyzTR5/v0\nVNK8UsZgRBFhXqrBv/RoCCG+xVJYtmd0QsrvNzDqG6QnjUB21zVXqzKEkW2gRtjX\ncw4vYQehAgMBAAECggEAS6xtjg0nAokk0jS+ZOpKlkMZAFaza3ZvyHipkHDz4PMt\ntl7Rb5oQZGvWT2rbEOrxey7BBi7LHGhIu8ExQp/hRGPoBAETP7XlyCghWPkPtEtE\ndU/mXxLoN0NszHuf/2si7pmH8YqGZ6QB0tgr22ut60mbK+AJFsEEf4aSpBUspepJ\n2800sQHsqPE6L6kYkfZ2GRRY1V9vUrYEODKZpWzMhN3UA9nAKH9PB6xvP2OdyMNh\nhKgmUUMNIFtwr8pZlJn60cf0UrWrc5CvqQLuaGYlzDgUQGV4JEVjqm9F6lMfEPUw\neN70MVe1pcLeLq2rGCVWU3gakh/HvJqlR/sa546HgwKBgQDyf1vkyX4w5sboi6DJ\ncl5dMULtMMRpB1OaMFVOJjI9gZJ8mCdRjqXdYo5aS2KIqxie8tGG9+SohxDAWl4t\nlSUtDsE44fSmILqC5zIawNRQnnkv0X8LwmYu0Qd7YAjJMlLTWyDRsjD9XRq4nsR+\nmJVwrt85iSpS5UFyryEzPbFj0wKBgQDwWzraeN0Eccf1iIYmQsYy+yMEAlHNR5yi\ngPXuAhSybv2JReRhdUb39hLr/LvKw0ZeXiLWXmYUGpbyzPyXIm0s+PL3LWl65GTF\nl+cfV5wfAdDkk6rAdEPEE2pxN85ChyaPYPoYr0ohmV97VQcYc5FqY+j1tM6R1RDt\n/fWBSa8iOwKBgQCpa1dtWWTDj4gqUdrswu2wmEkU47xlUIwVLm164u64z/zi9X6K\n2WmCaWfhJ8fYigjyi9zdOfXT1EFc0gX4PLozZ5qRPjQpmLYV3KbB0DTFemJaiTgE\npDW1wa5DgQ3CW1lIduNP/fmCGfkgQTQw6jOF/XbRgMZEEg2OrVI5tYFopwKBgER9\niqjEth5VGejCjY+LiZTvcUvsKUk4tc6stueqmiE6dW7PhsOqup1f9oZej1i5Cm1L\nn9u8LJRf+1GWzgd3HOsqyXlb7GnDeV/A6HBK88b2KoNn/Mk4mDLgYX1/rHvSrU9A\nECRGlvY6ETZAxXPXQsGxVKnnatGtiFR5AKNlzs0PAoGAa5+X+DUqGh9aE5ID3wrv\njkjxQ2KLFJCNSq8f9GSuvpvgXstHh6wKoM6vMwIShjgXuURH8Ub4uhRsWnxMildF\n7EE+QaWU9jnCm2HQYArfXrAWw6DBudiSkBqgKc6HjDHun5fXlYUo8UesNMQOrg7b\nbydQZ5/4V/1oSWPETk7jSr0=\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIIDCTCCAfGgAwIBAgIUFSntEn+Tv6HM2xJReECJpJcC7iUwDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDEwODE5NTQxNFoXDTM0MDEw\nNTE5NTQxNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEA463RK/T9E2lBEnbKROnMqNtjVPMoUa22JVfVR9r1r5z7\nE4V3MYZ3fnFKnTRAbMIcIBZVEHIP3klccK6/t54fmxU2Vj3GPtS0GMX6ShrKwl0H\nwfNiwySGQ1+IB2K1n92du9o1zdV3hQCrYsAyoA117jx+6+ZR+7wt0V3gH36i8ECY\nWX/jaBzbb/TB/SHVJEH5gxWbSPwkMwq2+vrzkuhJlA/0NhTRwc4MQoLhqoDcgXEG\n8SfUJ4PmYyBboRmFyZbMkVLXscs00ef79FTSvFLGYEQRYV6qwb/0aAghvsVSWLZn\ndELK7zcw6hukJ41Adtc1V6syhJFtoEbY13MOL2EHoQIDAQABo1MwUTAdBgNVHQ4E\nFgQUfry/KDtamwMlRQsFPbBhzdv2U5cwHwYDVR0jBBgwFoAUfry/KDtamwMlRQsF\nPbBhzdv2U5cwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAyYst\nVvewKRRpuYRWc4XG6WnYphUdyZLMoIlq0syZ1aj6YbqoK9NMHAYEnCvSov6zIZOa\ntrhuUcf9GFz5e0iJ2zIlDc312Iwsv41xiC/bs16kEn8Yf/SujEXasj7vmA3HrFWf\nwZTH/yFL5azo/f+lA1Q28YwqFpHmle0y0O53Uth4p0tmwlnu+CrO9fHp3kTlb7fD\n6mqfk9Nrt8tOC4aHYDoqtYUgZhx58xsHMOTetKeRlp8HMF9oROtriz4nYm6IhTwo\n5k1A13S3BjaxkZCyPXCgXssuXagNLasrr5Qq+Vgdb/nDhVehV8+Z4J0Ynzy9MZsE\nH1N1NfMtsA+PEqtPXA==\n-----END CERTIFICATE-----\n"),
160+
}
161+
}),
162+
},
138163
"should reconcile normally for AzureCluster with an IdentityRef of type WorkloadIdentity": {
139164
clusterName: defaultAzureCluster.Name,
140165
objects: []runtime.Object{

docs/book/src/topics/identities.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ data:
125125
password: PASSWORD
126126
```
127127

128+
Alternatively, the path to a certificate can be specified instead of the k8s secret:
129+
130+
```yaml
131+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
132+
kind: AzureClusterIdentity
133+
metadata:
134+
name: example-identity
135+
namespace: default
136+
spec:
137+
type: ServicePrincipalCertificate
138+
tenantID: <azure-tenant-id>
139+
clientID: <client-id-of-SP-identity>
140+
certPath: <path-to-the-cert>
141+
allowedNamespaces:
142+
list:
143+
- <cluster-namespace>
144+
```
145+
128146
## User-Assigned Managed Identity
129147

130148
<aside class="note">

test/setup/certificate

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjrdEr9P0TaUES
3+
dspE6cyo22NU8yhRrbYlV9VH2vWvnPsThXcxhnd+cUqdNEBswhwgFlUQcg/eSVxw
4+
rr+3nh+bFTZWPcY+1LQYxfpKGsrCXQfB82LDJIZDX4gHYrWf3Z272jXN1XeFAKti
5+
wDKgDXXuPH7r5lH7vC3RXeAffqLwQJhZf+NoHNtv9MH9IdUkQfmDFZtI/CQzCrb6
6+
+vOS6EmUD/Q2FNHBzgxCguGqgNyBcQbxJ9Qng+ZjIFuhGYXJlsyRUtexyzTR5/v0
7+
VNK8UsZgRBFhXqrBv/RoCCG+xVJYtmd0QsrvNzDqG6QnjUB21zVXqzKEkW2gRtjX
8+
cw4vYQehAgMBAAECggEAS6xtjg0nAokk0jS+ZOpKlkMZAFaza3ZvyHipkHDz4PMt
9+
tl7Rb5oQZGvWT2rbEOrxey7BBi7LHGhIu8ExQp/hRGPoBAETP7XlyCghWPkPtEtE
10+
dU/mXxLoN0NszHuf/2si7pmH8YqGZ6QB0tgr22ut60mbK+AJFsEEf4aSpBUspepJ
11+
2800sQHsqPE6L6kYkfZ2GRRY1V9vUrYEODKZpWzMhN3UA9nAKH9PB6xvP2OdyMNh
12+
hKgmUUMNIFtwr8pZlJn60cf0UrWrc5CvqQLuaGYlzDgUQGV4JEVjqm9F6lMfEPUw
13+
eN70MVe1pcLeLq2rGCVWU3gakh/HvJqlR/sa546HgwKBgQDyf1vkyX4w5sboi6DJ
14+
cl5dMULtMMRpB1OaMFVOJjI9gZJ8mCdRjqXdYo5aS2KIqxie8tGG9+SohxDAWl4t
15+
lSUtDsE44fSmILqC5zIawNRQnnkv0X8LwmYu0Qd7YAjJMlLTWyDRsjD9XRq4nsR+
16+
mJVwrt85iSpS5UFyryEzPbFj0wKBgQDwWzraeN0Eccf1iIYmQsYy+yMEAlHNR5yi
17+
gPXuAhSybv2JReRhdUb39hLr/LvKw0ZeXiLWXmYUGpbyzPyXIm0s+PL3LWl65GTF
18+
l+cfV5wfAdDkk6rAdEPEE2pxN85ChyaPYPoYr0ohmV97VQcYc5FqY+j1tM6R1RDt
19+
/fWBSa8iOwKBgQCpa1dtWWTDj4gqUdrswu2wmEkU47xlUIwVLm164u64z/zi9X6K
20+
2WmCaWfhJ8fYigjyi9zdOfXT1EFc0gX4PLozZ5qRPjQpmLYV3KbB0DTFemJaiTgE
21+
pDW1wa5DgQ3CW1lIduNP/fmCGfkgQTQw6jOF/XbRgMZEEg2OrVI5tYFopwKBgER9
22+
iqjEth5VGejCjY+LiZTvcUvsKUk4tc6stueqmiE6dW7PhsOqup1f9oZej1i5Cm1L
23+
n9u8LJRf+1GWzgd3HOsqyXlb7GnDeV/A6HBK88b2KoNn/Mk4mDLgYX1/rHvSrU9A
24+
ECRGlvY6ETZAxXPXQsGxVKnnatGtiFR5AKNlzs0PAoGAa5+X+DUqGh9aE5ID3wrv
25+
jkjxQ2KLFJCNSq8f9GSuvpvgXstHh6wKoM6vMwIShjgXuURH8Ub4uhRsWnxMildF
26+
7EE+QaWU9jnCm2HQYArfXrAWw6DBudiSkBqgKc6HjDHun5fXlYUo8UesNMQOrg7b
27+
bydQZ5/4V/1oSWPETk7jSr0=
28+
-----END PRIVATE KEY-----
29+
-----BEGIN CERTIFICATE-----
30+
MIIDCTCCAfGgAwIBAgIUFSntEn+Tv6HM2xJReECJpJcC7iUwDQYJKoZIhvcNAQEL
31+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDEwODE5NTQxNFoXDTM0MDEw
32+
NTE5NTQxNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
33+
AAOCAQ8AMIIBCgKCAQEA463RK/T9E2lBEnbKROnMqNtjVPMoUa22JVfVR9r1r5z7
34+
E4V3MYZ3fnFKnTRAbMIcIBZVEHIP3klccK6/t54fmxU2Vj3GPtS0GMX6ShrKwl0H
35+
wfNiwySGQ1+IB2K1n92du9o1zdV3hQCrYsAyoA117jx+6+ZR+7wt0V3gH36i8ECY
36+
WX/jaBzbb/TB/SHVJEH5gxWbSPwkMwq2+vrzkuhJlA/0NhTRwc4MQoLhqoDcgXEG
37+
8SfUJ4PmYyBboRmFyZbMkVLXscs00ef79FTSvFLGYEQRYV6qwb/0aAghvsVSWLZn
38+
dELK7zcw6hukJ41Adtc1V6syhJFtoEbY13MOL2EHoQIDAQABo1MwUTAdBgNVHQ4E
39+
FgQUfry/KDtamwMlRQsFPbBhzdv2U5cwHwYDVR0jBBgwFoAUfry/KDtamwMlRQsF
40+
PbBhzdv2U5cwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAyYst
41+
VvewKRRpuYRWc4XG6WnYphUdyZLMoIlq0syZ1aj6YbqoK9NMHAYEnCvSov6zIZOa
42+
trhuUcf9GFz5e0iJ2zIlDc312Iwsv41xiC/bs16kEn8Yf/SujEXasj7vmA3HrFWf
43+
wZTH/yFL5azo/f+lA1Q28YwqFpHmle0y0O53Uth4p0tmwlnu+CrO9fHp3kTlb7fD
44+
6mqfk9Nrt8tOC4aHYDoqtYUgZhx58xsHMOTetKeRlp8HMF9oROtriz4nYm6IhTwo
45+
5k1A13S3BjaxkZCyPXCgXssuXagNLasrr5Qq+Vgdb/nDhVehV8+Z4J0Ynzy9MZsE
46+
H1N1NfMtsA+PEqtPXA==
47+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)