Skip to content

Commit

Permalink
feat: support API Token authentication
Browse files Browse the repository at this point in the history
Adds support for using API Tokens to authenticate with the Cloudflare
API, in addition to the existing support for Origin CA Service Keys.

Bug: #135
  • Loading branch information
terinjokes committed Oct 1, 2024
1 parent e09e726 commit 27e6f8e
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions deploy/crds/cert-manager.k8s.cloudflare.com_originissuers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion internal/cfapi/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
type Builder struct {
hc *http.Client
serviceKey []byte
token []byte
}

func NewBuilder() *Builder {
Expand All @@ -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
Expand All @@ -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
}
}
15 changes: 14 additions & 1 deletion internal/cfapi/cfapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Interface interface {

type Client struct {
serviceKey []byte
token []byte
client *http.Client
endpoint string
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions pkgs/apis/v1/types_originissuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions pkgs/apis/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions pkgs/controllers/certificaterequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
127 changes: 127 additions & 0 deletions pkgs/controllers/certificaterequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
46 changes: 46 additions & 0 deletions pkgs/controllers/testdata/working.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 27e6f8e

Please sign in to comment.