Skip to content

Commit 0d42865

Browse files
committed
feat: allow reading api key from secret
1 parent 8704893 commit 0d42865

File tree

8 files changed

+138
-41
lines changed

8 files changed

+138
-41
lines changed

challenge_context.go

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
acmev1alpha1 "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
1010
egoscale "github.com/exoscale/egoscale/v3"
1111
"github.com/exoscale/egoscale/v3/credentials"
12-
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1312
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1413
"k8s.io/client-go/kubernetes"
1514
"k8s.io/klog/v2"
@@ -18,9 +17,9 @@ import (
1817
type challengeContext struct {
1918
k8s *kubernetes.Clientset
2019
exo *egoscale.Client
21-
cfg customDNSProviderConfig
20+
ch *acmev1alpha1.ChallengeRequest
21+
cfg *customDNSProviderConfig
2222
RecordName string
23-
RecordKey string
2423
Record *egoscale.DNSDomainRecord
2524
DNSDomain *egoscale.DNSDomain
2625
}
@@ -30,21 +29,21 @@ func NewChallengeContext(
3029
clientset *kubernetes.Clientset,
3130
ch *acmev1alpha1.ChallengeRequest,
3231
) (*challengeContext, error) {
33-
cc := challengeContext{k8s: clientset, RecordKey: ch.Key}
32+
cc := challengeContext{k8s: clientset, ch: ch}
3433

35-
if err := cc.initConfig(ctx, ch.Config); err != nil {
34+
if err := cc.initConfig(ctx); err != nil {
3635
return nil, err
3736
}
3837

39-
if err := cc.initExoClient(ctx, ch); err != nil {
38+
if err := cc.initExoClient(ctx); err != nil {
4039
return nil, err
4140
}
4241

43-
if err := cc.initDomain(ctx, ch.ResolvedFQDN); err != nil {
42+
if err := cc.initDomain(ctx); err != nil {
4443
return nil, err
4544
}
4645

47-
if err := cc.initRecordName(ch.ResolvedFQDN); err != nil {
46+
if err := cc.initRecordName(); err != nil {
4847
return nil, err
4948
}
5049

@@ -55,12 +54,12 @@ func NewChallengeContext(
5554
return &cc, nil
5655
}
5756

58-
func (cc *challengeContext) initConfig(ctx context.Context, cfgJSON *apiext.JSON) error {
57+
func (cc *challengeContext) initConfig(ctx context.Context) error {
5958
log := klog.FromContext(ctx)
60-
if cfgJSON == nil {
59+
if cc.ch.Config == nil {
6160
return nil
6261
}
63-
if err := json.Unmarshal(cfgJSON.Raw, &cc.cfg); err != nil {
62+
if err := json.Unmarshal(cc.ch.Config.Raw, &cc.cfg); err != nil {
6463
return fmt.Errorf("error decoding solver config: %v", err)
6564
}
6665

@@ -69,23 +68,34 @@ func (cc *challengeContext) initConfig(ctx context.Context, cfgJSON *apiext.JSON
6968
return nil
7069
}
7170

72-
func (cc *challengeContext) loadClientCredentials(
73-
ctx context.Context,
74-
ch *acmev1alpha1.ChallengeRequest,
75-
) (*credentials.Credentials, error) {
76-
if cc.cfg.SecretName != "" {
77-
secret, err := cc.k8s.CoreV1().Secrets(ch.ResourceNamespace).Get(ctx, cc.cfg.SecretName, metav1.GetOptions{})
71+
func (cc *challengeContext) loadClientCredentials(ctx context.Context) (*credentials.Credentials, error) {
72+
if apiKey, err := cc.resolveValue(ctx, cc.cfg.APIKey); err != nil {
73+
return nil, err
74+
} else if apiSecret, err := cc.resolveValue(ctx, cc.cfg.APISecret); err != nil {
75+
return nil, err
76+
} else {
77+
return credentials.NewStaticCredentials(apiKey, apiSecret), nil
78+
}
79+
}
80+
81+
func (cc *challengeContext) resolveValue(ctx context.Context, v valueOrSecretRef) (string, error) {
82+
if v.FromSecret != nil {
83+
secret, err := cc.k8s.CoreV1().Secrets(cc.ch.ResourceNamespace).
84+
Get(ctx, v.FromSecret.Name, metav1.GetOptions{})
7885
if err != nil {
79-
return nil, err
86+
return "", err
87+
} else if value, ok := secret.Data[v.FromSecret.Key]; !ok {
88+
return "", fmt.Errorf("secret %v does not have key %v", v.FromSecret.Name, v.FromSecret.Key)
89+
} else {
90+
return string(value), nil
8091
}
81-
return credentials.NewStaticCredentials(string(secret.Data["apiKey"]), string(secret.Data["apiSecret"])), nil
8292
} else {
83-
return credentials.NewStaticCredentials(cc.cfg.APIKey, cc.cfg.APISecret), nil
93+
return v.Value, nil
8494
}
8595
}
8696

87-
func (cc *challengeContext) initExoClient(ctx context.Context, ch *acmev1alpha1.ChallengeRequest) error {
88-
if creds, err := cc.loadClientCredentials(ctx, ch); err != nil {
97+
func (cc *challengeContext) initExoClient(ctx context.Context) error {
98+
if creds, err := cc.loadClientCredentials(ctx); err != nil {
8999
return err
90100
} else if client, err := egoscale.NewClient(creds); err != nil {
91101
return err
@@ -95,7 +105,8 @@ func (cc *challengeContext) initExoClient(ctx context.Context, ch *acmev1alpha1.
95105
}
96106
}
97107

98-
func (cc *challengeContext) initDomain(ctx context.Context, fqdn string) error {
108+
func (cc *challengeContext) initDomain(ctx context.Context) error {
109+
log := klog.FromContext(ctx)
99110
if cc.cfg.DomainID != "" {
100111
if domain, err := cc.exo.GetDNSDomain(ctx, cc.cfg.DomainID); err != nil {
101112
return err
@@ -109,14 +120,16 @@ func (cc *challengeContext) initDomain(ctx context.Context, fqdn string) error {
109120
} else {
110121
var found *egoscale.DNSDomain
111122
for _, domain := range result.DNSDomains {
112-
if isInZone(fqdn, domain.UnicodeName) && (found == nil || len(domain.UnicodeName) > len(found.UnicodeName)) {
123+
if isInZone(cc.ch.ResolvedFQDN, domain.UnicodeName) &&
124+
(found == nil || len(domain.UnicodeName) > len(found.UnicodeName)) {
113125
found = &domain
114126
}
115127
}
116128

117129
if found == nil {
118-
return fmt.Errorf("no zone found to host FQDN %v", fqdn)
130+
return fmt.Errorf("no zone found to host FQDN %v", cc.ch.ResolvedFQDN)
119131
}
132+
log.Info(fmt.Sprintf("found domain %+v", found))
120133
cc.DNSDomain = found
121134
return nil
122135
}
@@ -137,18 +150,18 @@ func normalizeZone(zone string) string {
137150
return zone
138151
}
139152

140-
func (cc *challengeContext) initRecordName(fqdn string) error {
141-
if !isInZone(fqdn, cc.DNSDomain.UnicodeName) {
142-
return fmt.Errorf("%v is not a subdomain of %v", fqdn, cc.DNSDomain.UnicodeName)
153+
func (cc *challengeContext) initRecordName() error {
154+
if !isInZone(cc.ch.ResolvedFQDN, cc.DNSDomain.UnicodeName) {
155+
return fmt.Errorf("%v is not a subdomain of %v", cc.ch.ResolvedFQDN, cc.DNSDomain.UnicodeName)
143156
}
144-
cc.RecordName = strings.TrimSuffix(fqdn, normalizeZone(cc.DNSDomain.UnicodeName))
157+
cc.RecordName = strings.TrimSuffix(cc.ch.ResolvedFQDN, normalizeZone(cc.DNSDomain.UnicodeName))
145158
return nil
146159
}
147160

148161
func (cc *challengeContext) initRecord(ctx context.Context) error {
149-
log := klog.FromContext(ctx).WithValues("recordName", cc.RecordName, "recordKey", cc.RecordKey)
162+
log := klog.FromContext(ctx).WithValues("recordName", cc.RecordName, "recordKey", cc.ch.Key)
150163
log.Info(fmt.Sprintf("looking for existing record in domain %+v", cc.DNSDomain))
151-
expectedContent := fmt.Sprintf(`"%v"`, cc.RecordKey)
164+
expectedContent := fmt.Sprintf(`"%v"`, cc.ch.Key)
152165
if result, err := cc.exo.ListDNSDomainRecords(ctx, cc.DNSDomain.ID); err != nil {
153166
return err
154167
} else {
@@ -171,7 +184,7 @@ func (cc *challengeContext) CreateRecord(ctx context.Context) (*egoscale.Operati
171184
log := klog.FromContext(ctx)
172185
req := egoscale.CreateDNSDomainRecordRequest{
173186
Name: cc.RecordName,
174-
Content: cc.RecordKey,
187+
Content: cc.ch.Key,
175188
Type: egoscale.CreateDNSDomainRecordRequestTypeTXT,
176189
Ttl: 60,
177190
}

deploy/cert-manager-webhook-exoscale/templates/rbac.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,41 @@ subjects:
7474
kind: ServiceAccount
7575
name: {{ .Values.certManager.serviceAccountName }}
7676
namespace: {{ .Values.certManager.namespace }}
77+
78+
---
79+
apiVersion: rbac.authorization.k8s.io/v1
80+
kind: Role
81+
metadata:
82+
name: {{ include "cert-manager-webhook-exoscale.fullname" . }}:secret-reader
83+
namespace: {{ .Release.Namespace }}
84+
labels:
85+
{{- include "cert-manager-webhook-exoscale.labels" . | nindent 4 }}
86+
rules:
87+
- apiGroups:
88+
- ""
89+
resources:
90+
- "secrets"
91+
{{- with .Values.issuerSecrets }}
92+
resourceNames:
93+
{{ toYaml . | indent 2 }}
94+
{{- end }}
95+
verbs:
96+
- "get"
97+
98+
---
99+
apiVersion: rbac.authorization.k8s.io/v1
100+
kind: RoleBinding
101+
metadata:
102+
name: {{ include "cert-manager-webhook-exoscale.fullname" . }}:secret-reader
103+
namespace: {{ .Release.Namespace }}
104+
labels:
105+
{{- include "cert-manager-webhook-exoscale.labels" . | nindent 4 }}
106+
roleRef:
107+
apiGroup: rbac.authorization.k8s.io
108+
kind: Role
109+
name: {{ include "cert-manager-webhook-exoscale.fullname" . }}:secret-reader
110+
subjects:
111+
- apiGroup: ""
112+
kind: ServiceAccount
113+
name: {{ include "cert-manager-webhook-exoscale.fullname" . }}
114+
namespace: {{ .Release.Namespace }}

deploy/cert-manager-webhook-exoscale/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
# here is recommended.
99
groupName: acme.mycompany.com
1010

11+
# Names of secrets the webhook should be allowed to read in order to access Exoscale API keys.
12+
# If you leave this empty, it is allowed to read all secrets in the namespace it was installed in
13+
issuerSecrets:
14+
# - exoscale-api-secret
15+
1116
certManager:
1217
namespace: cert-manager
1318
serviceAccountName: cert-manager
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: cert-manager.io/v1
2+
kind: Certificate
3+
metadata:
4+
name: test
5+
spec:
6+
secretName: test-tls
7+
dnsNames:
8+
- hello-webhook-1.kosmoz.local.gkube.eu
9+
issuerRef:
10+
group: cert-manager.io
11+
kind: ClusterIssuer
12+
name: exoscale-example
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
apiVersion: cert-manager.io/v1
2+
kind: ClusterIssuer
3+
metadata:
4+
name: exoscale-example
5+
spec:
6+
acme:
7+
server: https://acme-staging-v02.api.letsencrypt.org/directory
8+
privateKeySecretRef:
9+
name: exoscale-example-account-key
10+
11+
solvers:
12+
- dns01:
13+
webhook:
14+
groupName: acme.mycompany.com
15+
solverName: exoscale
16+
config:
17+
apiKey:
18+
fromSecret:
19+
name: exoscale-api
20+
key: apiKey
21+
apiSecret:
22+
fromSecret:
23+
name: exoscale-api
24+
key: apiSecret

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
require (
66
github.com/cert-manager/cert-manager v1.16.3
77
github.com/exoscale/egoscale/v3 v3.1.9
8-
k8s.io/apiextensions-apiserver v0.31.1
8+
k8s.io/api v0.31.1
99
k8s.io/apimachinery v0.31.1
1010
k8s.io/client-go v0.31.1
1111
k8s.io/klog/v2 v2.130.1
@@ -117,7 +117,7 @@ require (
117117
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
118118
gopkg.in/yaml.v2 v2.4.0 // indirect
119119
gopkg.in/yaml.v3 v3.0.1 // indirect
120-
k8s.io/api v0.31.1 // indirect
120+
k8s.io/apiextensions-apiserver v0.31.1 // indirect
121121
k8s.io/apiserver v0.31.1 // indirect
122122
k8s.io/component-base v0.31.1 // indirect
123123
k8s.io/kms v0.31.1 // indirect

main.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
acmev1alpha1 "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
88
"github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd"
99
egoscale "github.com/exoscale/egoscale/v3"
10+
corev1 "k8s.io/api/core/v1"
1011
"k8s.io/client-go/kubernetes"
1112
"k8s.io/client-go/rest"
1213
"k8s.io/klog/v2"
@@ -63,10 +64,14 @@ type customDNSProviderConfig struct {
6364
// These fields will be set by users in the
6465
// `issuer.spec.acme.dns01.providers.webhook.config` field.
6566

66-
APIKey string `json:"apiKey"`
67-
APISecret string `json:"apiSecret"`
68-
SecretName string `json:"secretName"`
69-
DomainID egoscale.UUID `json:"domainId"`
67+
APIKey valueOrSecretRef `json:"apiKey"`
68+
APISecret valueOrSecretRef `json:"apiSecret"`
69+
DomainID egoscale.UUID `json:"domainId"`
70+
}
71+
72+
type valueOrSecretRef struct {
73+
Value string `json:"value"`
74+
FromSecret *corev1.SecretKeySelector `json:"fromSecret"`
7075
}
7176

7277
// Name is used as the name for this DNS solver when referencing it on the ACME

testdata/exoscale-solver/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ Config format:
44

55
```json
66
{
7-
"apiKey": "EXO.....................",
8-
"apiSecret": ".........................",
9-
"domainId": ".........................."
7+
"apiKey": "EXOf2b10337b1a9dd059ade5890",
8+
"apiSecret": "tx11Nh3oVb6HYuT085uolfga1RNK7ykJo0JQlUXJ7rw",
9+
"domainId": "89083a5c-b648-474a-0000-0000001223e3"
1010
}
1111
```

0 commit comments

Comments
 (0)