diff --git a/deploy/crds/cert-manager.k8s.cloudflare.com_clusteroriginissuers.yaml b/deploy/crds/cert-manager.k8s.cloudflare.com_clusteroriginissuers.yaml index 61d9af6..dcff482 100644 --- a/deploy/crds/cert-manager.k8s.cloudflare.com_clusteroriginissuers.yaml +++ b/deploy/crds/cert-manager.k8s.cloudflare.com_clusteroriginissuers.yaml @@ -57,6 +57,23 @@ spec: - key - name type: object + tokenRef: + description: TokenRef authenticates with an API Token. + properties: + key: + description: Key of the secret to select from. Must be a valid + secret key. + type: string + name: + description: Name of the secret in the issuer's namespace + to select. If a cluster-scoped issuer, the secret is selected + from the "cluster resource namespace" configured on the + controller. + type: string + required: + - key + - name + type: object type: object requestType: description: RequestType is the signature algorithm Cloudflare should diff --git a/deploy/crds/cert-manager.k8s.cloudflare.com_originissuers.yaml b/deploy/crds/cert-manager.k8s.cloudflare.com_originissuers.yaml index 6f6eab1..7d66627 100644 --- a/deploy/crds/cert-manager.k8s.cloudflare.com_originissuers.yaml +++ b/deploy/crds/cert-manager.k8s.cloudflare.com_originissuers.yaml @@ -57,6 +57,23 @@ spec: - key - name type: object + tokenRef: + description: TokenRef authenticates with an API Token. + properties: + key: + description: Key of the secret to select from. Must be a valid + secret key. + type: string + name: + description: Name of the secret in the issuer's namespace + to select. If a cluster-scoped issuer, the secret is selected + from the "cluster resource namespace" configured on the + controller. + type: string + required: + - key + - name + type: object type: object requestType: description: RequestType is the signature algorithm Cloudflare should diff --git a/internal/cfapi/builder.go b/internal/cfapi/builder.go index 710c819..dd191f7 100644 --- a/internal/cfapi/builder.go +++ b/internal/cfapi/builder.go @@ -7,6 +7,7 @@ import ( type Builder struct { hc *http.Client serviceKey []byte + token []byte } func NewBuilder() *Builder { @@ -18,6 +19,11 @@ func (b *Builder) WithServiceKey(key []byte) *Builder { return b } +func (b *Builder) WithToken(token []byte) *Builder { + b.token = token + return b +} + func (b *Builder) WithClient(hc *http.Client) *Builder { b.hc = hc return b @@ -31,5 +37,12 @@ func (b *Builder) Clone() *Builder { } func (b *Builder) Build() *Client { - return New(WithServiceKey(b.serviceKey), WithClient(b.hc)) + switch { + case b.serviceKey != nil: + return New(WithServiceKey(b.serviceKey), WithClient(b.hc)) + case b.token != nil: + return New(WithToken(b.token), WithClient(b.hc)) + default: + return nil + } } diff --git a/internal/cfapi/cfapi.go b/internal/cfapi/cfapi.go index 85ed39e..86d2eb3 100644 --- a/internal/cfapi/cfapi.go +++ b/internal/cfapi/cfapi.go @@ -16,6 +16,7 @@ type Interface interface { type Client struct { serviceKey []byte + token []byte client *http.Client endpoint string } @@ -41,6 +42,12 @@ func WithServiceKey(key []byte) Options { } } +func WithToken(token []byte) Options { + return func(c *Client) { + c.token = token + } +} + func WithClient(client *http.Client) Options { return func(c *Client) { c.client = client @@ -106,7 +113,13 @@ func (c *Client) Sign(ctx context.Context, req *SignRequest) (*SignResponse, err } r.Header.Add("User-Agent", "github.com/cloudflare/origin-ca-issuer") - r.Header.Add("X-Auth-User-Service-Key", string(c.serviceKey)) + + if c.serviceKey != nil { + r.Header.Add("X-Auth-User-Service-Key", string(c.serviceKey)) + } + if c.token != nil { + r.Header.Add("Authorization", "Bearer "+string(c.token)) + } resp, err := c.client.Do(r) if err != nil { diff --git a/pkgs/apis/v1/types_originissuer.go b/pkgs/apis/v1/types_originissuer.go index 1826399..26a5690 100644 --- a/pkgs/apis/v1/types_originissuer.go +++ b/pkgs/apis/v1/types_originissuer.go @@ -85,6 +85,10 @@ type OriginIssuerAuthentication struct { // ServiceKeyRef authenticates with an API Service Key. // +optional ServiceKeyRef *SecretKeySelector `json:"serviceKeyRef,omitempty"` + + // TokenRef authenticates with an API Token. + // +optional + TokenRef *SecretKeySelector `json:"tokenRef,omitempty"` } // SecretKeySelector contains a reference to a secret. diff --git a/pkgs/apis/v1/zz_generated.deepcopy.go b/pkgs/apis/v1/zz_generated.deepcopy.go index cde284b..ad1cbd6 100644 --- a/pkgs/apis/v1/zz_generated.deepcopy.go +++ b/pkgs/apis/v1/zz_generated.deepcopy.go @@ -102,6 +102,11 @@ func (in *OriginIssuerAuthentication) DeepCopyInto(out *OriginIssuerAuthenticati *out = new(SecretKeySelector) **out = **in } + if in.TokenRef != nil { + in, out := &in.TokenRef, &out.TokenRef + *out = new(SecretKeySelector) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OriginIssuerAuthentication. diff --git a/pkgs/controllers/certificaterequest.go b/pkgs/controllers/certificaterequest.go index 5faf74f..c7d5c3f 100644 --- a/pkgs/controllers/certificaterequest.go +++ b/pkgs/controllers/certificaterequest.go @@ -203,6 +203,35 @@ func (r *CertificateRequestController) Reconcile(ctx context.Context, cr *certma } c = r.Builder.Clone().WithServiceKey(serviceKey).Build() + case issuerspec.Auth.TokenRef != nil: + var secret core.Secret + + secretNamespaceName := types.NamespacedName{ + Namespace: secretNamespace, + Name: issuerspec.Auth.TokenRef.Name, + } + + if err := r.Reader.Get(ctx, secretNamespaceName, &secret); err != nil { + log.Error(err, "failed to retieve OriginIssuer auth secret", "namespace", secretNamespaceName.Namespace, "name", secretNamespaceName.Name) + if apierrors.IsNotFound(err) { + _ = r.setStatus(ctx, cr, cmmeta.ConditionFalse, "NotFound", fmt.Sprintf("Failed to retrieve auth secret: %v", err)) + } else { + _ = r.setStatus(ctx, cr, cmmeta.ConditionFalse, "Error", fmt.Sprintf("Failed to retrieve auth secret: %v", err)) + } + + return reconcile.Result{}, err + } + + token, ok := secret.Data[issuerspec.Auth.TokenRef.Key] + if !ok { + err := fmt.Errorf("secret %s does not contain key %q", secret.Name, issuerspec.Auth.TokenRef.Key) + log.Error(err, "failed to retrieve OriginIssuer auth secret") + _ = r.setStatus(ctx, cr, cmmeta.ConditionFalse, "NotFound", fmt.Sprintf("Failed to retrieve auth secret: %v", err)) + + return reconcile.Result{}, err + } + + c = r.Builder.Clone().WithToken(token).Build() default: // This issuer should not be ready! err := fmt.Errorf("issuer %s does not have an authentication method configured", cr.Spec.IssuerRef.Name) diff --git a/pkgs/controllers/certificaterequest_test.go b/pkgs/controllers/certificaterequest_test.go index f9a2f2d..90cdb38 100644 --- a/pkgs/controllers/certificaterequest_test.go +++ b/pkgs/controllers/certificaterequest_test.go @@ -175,6 +175,133 @@ func TestCertificateRequestReconcile(t *testing.T) { Name: "foobar", }, }, + { + name: "working OriginIssuer with tokenRef", + objects: []runtime.Object{ + cmgen.CertificateRequest("foobar", + cmgen.SetCertificateRequestNamespace("default"), + cmgen.SetCertificateRequestDuration(&metav1.Duration{Duration: 7 * 24 * time.Hour}), + cmgen.SetCertificateRequestCSR(golden.Get(t, "csr.golden")), + cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ + Name: "foobar", + Kind: "OriginIssuer", + Group: "cert-manager.k8s.cloudflare.com", + }), + ), + &v1.OriginIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foobar", + Namespace: "default", + }, + Spec: v1.OriginIssuerSpec{ + RequestType: v1.RequestTypeOriginECC, + Auth: v1.OriginIssuerAuthentication{ + TokenRef: &v1.SecretKeySelector{ + Name: "token-issuer", + Key: "token", + }, + }, + }, + Status: v1.OriginIssuerStatus{ + Conditions: []v1.OriginIssuerCondition{ + { + Type: v1.ConditionReady, + Status: v1.ConditionTrue, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "token-issuer", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("api-token"), + }, + }, + }, + recorder: RecorderMust(t, "testdata/working"), + expected: cmapi.CertificateRequestStatus{ + Conditions: []cmapi.CertificateRequestCondition{ + { + Type: cmapi.CertificateRequestConditionReady, + Status: cmmeta.ConditionTrue, + LastTransitionTime: &now, + Reason: "Issued", + Message: "Certificate issued", + }, + }, + Certificate: golden.Get(t, "certificate.golden"), + }, + namespaceName: types.NamespacedName{ + Namespace: "default", + Name: "foobar", + }, + }, + { + name: "working ClusterOriginIssuer with tokenRef", + objects: []runtime.Object{ + cmgen.CertificateRequest("foobar", + cmgen.SetCertificateRequestNamespace("default"), + cmgen.SetCertificateRequestDuration(&metav1.Duration{Duration: 7 * 24 * time.Hour}), + cmgen.SetCertificateRequestCSR(golden.Get(t, "csr.golden")), + cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ + Name: "foobar", + Kind: "ClusterOriginIssuer", + Group: "cert-manager.k8s.cloudflare.com", + }), + ), + &v1.ClusterOriginIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foobar", + }, + Spec: v1.OriginIssuerSpec{ + RequestType: v1.RequestTypeOriginECC, + Auth: v1.OriginIssuerAuthentication{ + TokenRef: &v1.SecretKeySelector{ + Name: "token-issuer", + Key: "token", + }, + }, + }, + Status: v1.OriginIssuerStatus{ + Conditions: []v1.OriginIssuerCondition{ + { + Type: v1.ConditionReady, + Status: v1.ConditionTrue, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "token-issuer", + Namespace: "super-secret", + }, + Data: map[string][]byte{ + "token": []byte("api-token"), + }, + }, + }, + recorder: RecorderMust(t, "testdata/working"), + expected: cmapi.CertificateRequestStatus{ + Conditions: []cmapi.CertificateRequestCondition{ + { + Type: cmapi.CertificateRequestConditionReady, + Status: cmmeta.ConditionTrue, + LastTransitionTime: &now, + Reason: "Issued", + Message: "Certificate issued", + }, + }, + Certificate: golden.Get(t, "certificate.golden"), + }, + namespaceName: types.NamespacedName{ + Namespace: "default", + Name: "foobar", + }, + }, { name: "OriginIssuer without authentication", objects: []runtime.Object{ diff --git a/pkgs/controllers/testdata/working.yaml b/pkgs/controllers/testdata/working.yaml index e49ec61..83dbd45 100644 --- a/pkgs/controllers/testdata/working.yaml +++ b/pkgs/controllers/testdata/working.yaml @@ -47,3 +47,49 @@ interactions: status: 200 OK code: 200 duration: 212.568428ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 1165 + transfer_encoding: [] + trailer: {} + host: api.cloudflare.com + remote_addr: "" + request_uri: "" + body: '{"hostnames":["example.net","www.example.net"],"requested_validity":7,"request_type":"origin-ecc","csr":"-----BEGIN CERTIFICATE REQUEST-----\nMIICxzCCAa8CAQAwSDELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lz\nY28xCzAJBgNVBAcTAkNBMRQwEgYDVQQDEwtleGFtcGxlLm5ldDCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBALxejtu4b+jPdFeFi6OUsye8TYJQBm3WfCvL\nHu5EvijMO/4Z2TImwASbwUF7Ir8OLgH+mGlQZeqyNvGoSOMEaZVXcYfpR1hlVak8\n4GGVr+04IGfOCqaBokaBFIwzclGZbzKmLGwIQioNxGfqFm6RGYGA3be2Je2iseBc\nN8GV1wYmvYE0RR+yWweJCTJ157exyRzu7sVxaEW9F87zBQLyOnwXc64rflXslRqi\ng7F7w5IaQYOl8yvmk/jEPCAha7fkiUfEpj4N12+oPRiMvleJF98chxjD4MH39c5I\nuOslULhrWunfh7GB1jwWNA9y44H0snrf+xvoy2TcHmxvma9Eln8CAwEAAaA6MDgG\nCSqGSIb3DQEJDjErMCkwJwYDVR0RBCAwHoILZXhhbXBsZS5uZXSCD3d3dy5leGFt\ncGxlLm5ldDANBgkqhkiG9w0BAQsFAAOCAQEAcBaX6dOnI8ncARrI9ZSF2AJX+8mx\npTHY2+Y2C0VvrVDGMtbBRH8R9yMbqWtlxeeNGf//LeMkSKSFa4kbpdx226lfui8/\nauRDBTJGx2R1ccUxmLZXx4my0W5iIMxunu+kez+BDlu7bTT2io0uXMRHue4i6quH\nyc5ibxvbJMjR7dqbcanVE10/34oprzXQsJ/VmSuZNXtjbtSKDlmcpw6To/eeAJ+J\nhXykcUihvHyG4A1m2R6qpANBjnA0pHexfwM/SgfzvpbvUg0T1ubmer8BgTwCKIWs\ndcWYTthM51JIqRBfNqy4QcBnX+GY05yltEEswQI55wdiS3CjTTA67sdbcQ==\n-----END CERTIFICATE REQUEST-----\n"}' + form: {} + headers: + User-Agent: + - github.com/cloudflare/origin-ca-issuer + Authorization: + - Bearer api-token + url: https://api.cloudflare.com/client/v4/certificates + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"success":true,"result":{"certificate":"-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNV\nBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGln\naUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmo\nwp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c\n1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiI\nWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZ\nwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPR\nBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJ\nKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6D\nhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpY\nQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/\nZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn\n29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO2\n97Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=\n-----END CERTIFICATE-----","csr":"-----BEGIN CERTIFICATE REQUEST-----\nMIICxzCCAa8CAQAwSDELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lz\nY28xCzAJBgNVBAcTAkNBMRQwEgYDVQQDEwtleGFtcGxlLm5ldDCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBALxejtu4b+jPdFeFi6OUsye8TYJQBm3WfCvL\nHu5EvijMO/4Z2TImwASbwUF7Ir8OLgH+mGlQZeqyNvGoSOMEaZVXcYfpR1hlVak8\n4GGVr+04IGfOCqaBokaBFIwzclGZbzKmLGwIQioNxGfqFm6RGYGA3be2Je2iseBc\nN8GV1wYmvYE0RR+yWweJCTJ157exyRzu7sVxaEW9F87zBQLyOnwXc64rflXslRqi\ng7F7w5IaQYOl8yvmk/jEPCAha7fkiUfEpj4N12+oPRiMvleJF98chxjD4MH39c5I\nuOslULhrWunfh7GB1jwWNA9y44H0snrf+xvoy2TcHmxvma9Eln8CAwEAAaA6MDgG\nCSqGSIb3DQEJDjErMCkwJwYDVR0RBCAwHoILZXhhbXBsZS5uZXSCD3d3dy5leGFt\ncGxlLm5ldDANBgkqhkiG9w0BAQsFAAOCAQEAcBaX6dOnI8ncARrI9ZSF2AJX+8mx\npTHY2+Y2C0VvrVDGMtbBRH8R9yMbqWtlxeeNGf//LeMkSKSFa4kbpdx226lfui8/\nauRDBTJGx2R1ccUxmLZXx4my0W5iIMxunu+kez+BDlu7bTT2io0uXMRHue4i6quH\nyc5ibxvbJMjR7dqbcanVE10/34oprzXQsJ/VmSuZNXtjbtSKDlmcpw6To/eeAJ+J\nhXykcUihvHyG4A1m2R6qpANBjnA0pHexfwM/SgfzvpbvUg0T1ubmer8BgTwCKIWs\ndcWYTthM51JIqRBfNqy4QcBnX+GY05yltEEswQI55wdiS3CjTTA67sdbcQ==\n-----END CERTIFICATE REQUEST-----","expires_on":"2014-01-01 05:20:00 +0000 UTC","hostnames":["example.com","www.example.com"],"id":"023e105f4ecef8ad9ca31a8372d0c353","request_type":"origin-ecc","requested_validity":7}} + headers: + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 0123456789abcdef-ABC + Content-Type: + - application/json + Date: + - Tue, 01 Oct 2024 02:25:18 GMT + Server: + - cloudflare + Vary: + - Accept-Encoding + status: 200 OK + code: 200 + duration: 212.568428ms