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 2, 2024
1 parent e09e726 commit 1531b86
Show file tree
Hide file tree
Showing 13 changed files with 413 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
27 changes: 27 additions & 0 deletions pkgs/controllers/clusteroriginissuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ func (r *ClusterOriginIssuerController) Reconcile(ctx context.Context, iss *v1.C
log.Error(err, "failed to retrieve ClusterOriginIssuer auth secret")
_ = r.setStatus(ctx, iss, v1.ConditionFalse, "NotFound", fmt.Sprintf("Failed to retrieve auth secret: %v", err))

return reconcile.Result{}, err
}
case iss.Spec.Auth.TokenRef != nil:
secret := &core.Secret{}
secretNamespaceName := types.NamespacedName{
Namespace: r.ClusterResourceNamespace,
Name: iss.Spec.Auth.TokenRef.Name,
}

if err := r.Reader.Get(ctx, secretNamespaceName, secret); err != nil {
log.Error(err, "failed to retieve ClusterOriginIssuer auth secret", "namespace", secretNamespaceName.Namespace, "name", secretNamespaceName.Name)

if apierrors.IsNotFound(err) {
_ = r.setStatus(ctx, iss, v1.ConditionFalse, "NotFound", fmt.Sprintf("Failed to retrieve auth secret: %v", err))
} else {
_ = r.setStatus(ctx, iss, v1.ConditionFalse, "Error", fmt.Sprintf("Failed to retrieve auth secret: %v", err))
}

return reconcile.Result{}, err
}

_, ok := secret.Data[iss.Spec.Auth.TokenRef.Key]
if !ok {
err := fmt.Errorf("secret %s does not contain key %q", secret.Name, iss.Spec.Auth.TokenRef.Key)
log.Error(err, "failed to retrieve ClusterOriginIssuer auth secret")
_ = r.setStatus(ctx, iss, v1.ConditionFalse, "NotFound", fmt.Sprintf("Failed to retrieve auth secret: %v", err))

return reconcile.Result{}, err
}
default:
Expand Down
42 changes: 42 additions & 0 deletions pkgs/controllers/clusteroriginissuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,48 @@ func TestClusterOriginIssuerReconcile(t *testing.T) {
Name: "foo",
},
},
{
name: "working tokenRef",
objects: []runtime.Object{
&v1.ClusterOriginIssuer{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1.OriginIssuerSpec{
RequestType: v1.RequestTypeOriginRSA,
Auth: v1.OriginIssuerAuthentication{
TokenRef: &v1.SecretKeySelector{
Name: "issuer-api-token",
Key: "token",
},
},
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "issuer-api-token",
Namespace: "super-secret",
},
Data: map[string][]byte{
"token": []byte("djEuMC0weDAwQkFCMTBD"),
},
},
},
expected: v1.OriginIssuerStatus{
Conditions: []v1.OriginIssuerCondition{
{
Type: v1.ConditionReady,
Status: v1.ConditionTrue,
LastTransitionTime: &now,
Reason: "Verified",
Message: "ClusterOriginIssuer verified and ready to sign certificates",
},
},
},
namespaceName: types.NamespacedName{
Name: "foo",
},
},
{
name: "unset authentication",
objects: []runtime.Object{
Expand Down
Loading

0 comments on commit 1531b86

Please sign in to comment.