Skip to content

Commit

Permalink
Add spec value for endEntityName in Issuer/ClusterIssuer CRD, and add…
Browse files Browse the repository at this point in the history
… support for annotations #7
  • Loading branch information
Hayden Roszell committed Sep 1, 2023
1 parent d981de0 commit 37b23ce
Show file tree
Hide file tree
Showing 16 changed files with 564 additions and 79 deletions.
8 changes: 3 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,9 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in
# Build the manager image for local development. This image is not intended to be used in production.
# Then, install it into the K8s cluster
.PHONY: deploy-local
deploy-local: ## Build docker image with the manager.
docker build -t ejbca-issuer-dev:latest -f Dockerfile.local .
make manifests
make kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
deploy-local: manifests kustomize ## Build docker image with the manager.
docker build -t ejbca-issuer-dev:latest -f Dockerfile .
cd config/manager && $(KUSTOMIZE) edit set image controller=ejbca-issuer-dev:latest
$(KUSTOMIZE) build config/default | kubectl apply -f -

.PHONY: undeploy
Expand Down
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ The `spec` field of both the Issuer and ClusterIssuer resources use the followin
* `certificateProfileName` - The name of the EJBCA certificate profile to use. For example, `ENDUSER`
* `endEntityProfileName` - The name of the EJBCA end entity profile to use. For example, `ENDUSER`
* `caBundleSecretName` - The name of the Kubernetes secret containing the CA certificate. This field is optional and only required if the EJBCA API is configured to use a self-signed certificate or with a certificate signed by an untrusted root.
* `endEntityName` - The name of the end entity to use. This field is optional. More information on how the field is used can be found in the [EJBCA End Entity Name Configuration](#ejbca-end-entity-name-configuration) section.

###### If a different combination of hostname/certificate authority/certificate profile/end entity profile is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration.

Expand All @@ -112,6 +113,7 @@ spec:
certificateProfileName: ""
endEntityProfileName: ""
caBundleSecretName: ""
endEntityName: ""
```

The following is an example of a ClusterIssuer resource:
Expand All @@ -132,6 +134,7 @@ spec:
certificateProfileName: ""
endEntityProfileName: ""
caBundleSecretName: ""
endEntityName: ""
```

To create new resources from the above examples, replace the empty strings with the appropriate values and apply the resources to the cluster:
Expand Down Expand Up @@ -205,6 +208,76 @@ kubectl get secret ejbca-certificate -n ejbca-issuer-system -o jsonpath='{.data.

###### To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval).

## EJBCA End Entity Name Configuration
The endEntityName field in the Issuer and ClusterIssuer resource spec allows you to configure how the End Entity Name is selected when issuing certificates through EJBCA. This field offers flexibility by allowing you to select different components from the Certificate Signing Request (CSR) or other contextual data as the End Entity Name.

### Configurable Options
Here are the different options you can set for endEntityName:

* **`cn`:** Uses the Common Name from the CSR's Distinguished Name.
* **`dns`:** Uses the first DNS Name from the CSR's Subject Alternative Names (SANs).
* **`uri`:** Uses the first URI from the CSR's Subject Alternative Names (SANs).
* **`ip`:** Uses the first IP Address from the CSR's Subject Alternative Names (SANs).
* **`certificateName`:** Uses the name of the cert-manager.io/Certificate object.
* **Custom Value:** Any other string will be directly used as the End Entity Name.

### Default Behavior
If the endEntityName field is not explicitly set, the EJBCA Issuer will attempt to determine the End Entity Name using the following default behavior:

* **First, it will try to use the Common Name:** It looks at the Common Name from the CSR's Distinguished Name.
* **If the Common Name is not available, it will use the first DNS Name:** It looks at the first DNS Name from the CSR's Subject Alternative Names (SANs).
* **If the DNS Name is not available, it will use the first URI:** It looks at the first URI from the CSR's Subject Alternative Names (SANs).
* **If the URI is not available, it will use the first IP Address:** It looks at the first IP Address from the CSR's Subject Alternative Names (SANs).
* **If none of the above are available, it will use the name of the cert-manager.io/Certificate object:** It defaults to the name of the certificate object.

If the Issuer is unable to determine a valid End Entity Name through these steps, an error will be logged and no End Entity Name will be set.

## Annotation Overrides for Issuer and ClusterIssuer Resources
The Keyfactor EJBCA external issuer for cert-manager allows you to override default settings in the Issuer and ClusterIssuer resources through the use of annotations. This gives you more granular control on a per-Certificate/CertificateRequest basis.

### Supported Annotations
Here are the supported annotations that can override the default values:

- **`ejbca-issuer.keyfactor.com/endEntityName`**: Overrides the `endEntityName` field from the resource spec. Allowed values include `"cn"`, `"dns"`, `"uri"`, `"ip"`, and `"certificateName"`, or any custom string.

```yaml
ejbca-issuer.keyfactor.com/endEntityName: "dns"
```

- **`ejbca-issuer.keyfactor.com/certificateAuthorityName`**: Specifies the Certificate Authority (CA) name to use, overriding the default CA specified in the resource spec.

```yaml
ejbca-issuer.keyfactor.com/certificateAuthorityName: "ManagementCA"
```

- **`ejbca-issuer.keyfactor.com/certificateProfileName`**: Specifies the Certificate Profile name to use, overriding the default profile specified in the resource spec.

```yaml
ejbca-issuer.keyfactor.com/certificateProfileName: "tlsServerAuth"
```

- **`ejbca-issuer.keyfactor.com/endEntityProfileName`**: Specifies the End Entity Profile name to use, overriding the default profile specified in the resource spec.

```yaml
ejbca-issuer.keyfactor.com/endEntityProfileName: "eep"
```

### How to Apply Annotations

To apply these annotations, include them in the metadata section of your CertificateRequest resource:

```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
annotations:
ejbca-issuer.keyfactor.com/endEntityName: "dns"
ejbca-issuer.keyfactor.com/certificateAuthorityName: "ManagementCA"
# ... other annotations
spec:
# ... rest of the spec
```

## Cleanup
To list the certificates and certificate requests created, run the following commands:
```shell
Expand Down
14 changes: 14 additions & 0 deletions api/v1alpha1/issuer_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ type IssuerSpec struct {
// the client trust roots for the EJBCA issuer.
// +optional
CaBundleSecretName string `json:"caBundleSecretName"`

// Optional field that overrides the default for how the EJBCA issuer should determine the
// name of the end entity to reference or create when signing certificates.
// The options are:
//* cn: Use the CommonName from the CertificateRequest's DN
//* dns: Use the first DNSName from the CertificateRequest's DNSNames SANs
//* uri: Use the first URI from the CertificateRequest's URI Sans
//* ip: Use the first IPAddress from the CertificateRequest's IPAddresses SANs
//* certificateName: Use the value of the CertificateRequest's certificateName annotation
// If none of the above options are used but endEntityName is populated, the
// value of endEntityName will be used as the end entity name. If endEntityName
// is not populated, the default tree listed in the EJBCA documentation will be used.
// +optional
EndEntityName string `json:"endEntityName"`
}

// IssuerStatus defines the observed state of Issuer
Expand Down
14 changes: 14 additions & 0 deletions config/crd/bases/ejbca-issuer.keyfactor.com_clusterissuers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ spec:
resource namespace', which is set as a flag on the controller component
(and defaults to the namespace that the controller runs in).
type: string
endEntityName:
description: 'Optional field that overrides the default for how the
EJBCA issuer should determine the name of the end entity to reference
or create when signing certificates. The options are: * cn: Use
the CommonName from the CertificateRequest''s DN * dns: Use the
first DNSName from the CertificateRequest''s DNSNames SANs * uri:
Use the first URI from the CertificateRequest''s URI Sans * ip:
Use the first IPAddress from the CertificateRequest''s IPAddresses
SANs * certificateName: Use the value of the CertificateRequest''s
certificateName annotation If none of the above options are used
but endEntityName is populated, the value of endEntityName will
be used as the end entity name. If endEntityName is not populated,
the default tree listed in the EJBCA documentation will be used.'
type: string
endEntityProfileName:
type: string
hostname:
Expand Down
14 changes: 14 additions & 0 deletions config/crd/bases/ejbca-issuer.keyfactor.com_issuers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ spec:
resource namespace', which is set as a flag on the controller component
(and defaults to the namespace that the controller runs in).
type: string
endEntityName:
description: 'Optional field that overrides the default for how the
EJBCA issuer should determine the name of the end entity to reference
or create when signing certificates. The options are: * cn: Use
the CommonName from the CertificateRequest''s DN * dns: Use the
first DNSName from the CertificateRequest''s DNSNames SANs * uri:
Use the first URI from the CertificateRequest''s URI Sans * ip:
Use the first IPAddress from the CertificateRequest''s IPAddresses
SANs * certificateName: Use the value of the CertificateRequest''s
certificateName annotation If none of the above options are used
but endEntityName is populated, the value of endEntityName will
be used as the end entity name. If endEntityName is not populated,
the default tree listed in the EJBCA documentation will be used.'
type: string
endEntityProfileName:
type: string
hostname:
Expand Down
4 changes: 2 additions & 2 deletions config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: m8rmclarenkf/ejbca-cert-manager-external-issuer-controller
newTag: v1.2.2
newName: ejbca-issuer-dev
newTag: latest
2 changes: 1 addition & 1 deletion config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ spec:
args:
- --leader-elect
image: controller:latest
#imagePullPolicy: Never # TODO dev field
imagePullPolicy: Never # TODO dev field
name: manager
securityContext:
allowPrivilegeEscalation: false
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/go-logr/logr v1.2.3
github.com/onsi/ginkgo/v2 v2.6.1
github.com/onsi/gomega v1.24.2
github.com/stretchr/testify v1.8.2
github.com/stretchr/testify v1.8.4
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.3
k8s.io/client-go v0.26.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
Expand Down
30 changes: 9 additions & 21 deletions internal/controllers/certificaterequest_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,10 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
return ctrl.Result{}, nil
}

// We now have a CertificateRequest that belongs to us so we are responsible
// for updating its Ready condition.
setReadyCondition := func(status cmmeta.ConditionStatus, reason, message string) {
cmutil.SetCertificateRequestCondition(
&certificateRequest,
cmapi.CertificateRequestConditionReady,
status,
reason,
message,
)
}

// Always attempt to update the Ready condition
defer func() {
if err != nil {
setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, err.Error())
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, err.Error())
}
if updateErr := r.Status().Update(ctx, &certificateRequest); updateErr != nil {
err = utilerrors.NewAggregate([]error{err, updateErr})
Expand All @@ -140,7 +128,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
}

message := "The CertificateRequest was denied by an approval controller"
setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonDenied, message)
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonDenied, message)
return ctrl.Result{}, nil
}

Expand All @@ -155,7 +143,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
// Add a Ready condition if one does not already exist
if ready := cmutil.GetCertificateRequestCondition(&certificateRequest, cmapi.CertificateRequestConditionReady); ready == nil {
log.Info("Initialising Ready condition")
setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initialising")
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initialising")
return ctrl.Result{}, nil
}

Expand All @@ -165,7 +153,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
if err != nil {
err = fmt.Errorf("%w: %v", errIssuerRef, err)
log.Error(err, "Unrecognized kind. Ignoring.")
setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error())
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error())
return ctrl.Result{}, nil
}
issuer := issuerRO.(client.Object)
Expand All @@ -185,7 +173,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
default:
err := fmt.Errorf("unexpected issuer type: %v", t)
log.Error(err, "The issuerRef referred to a registered Kind which is not yet handled. Ignoring.")
setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error())
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error())
return ctrl.Result{}, nil
}

Expand All @@ -194,10 +182,10 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
return ctrl.Result{}, fmt.Errorf("%w: %v", errGetIssuer, err)
}

_, issuerSpec, issuerStatus, err := issuerutil.GetSpecAndStatus(issuer)
issuerSpec, issuerStatus, err := issuerutil.GetSpecAndStatus(issuer)
if err != nil {
log.Error(err, "Unable to get the IssuerStatus. Ignoring.")
setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error())
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error())
return ctrl.Result{}, nil
}

Expand Down Expand Up @@ -231,7 +219,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
}
}

ejbcaSigner, err := r.SignerBuilder(ctx, issuerSpec, authSecret.Data, caSecret.Data)
ejbcaSigner, err := r.SignerBuilder(ctx, issuerSpec, certificateRequest.GetAnnotations(), authSecret.Data, caSecret.Data)
if err != nil {
return ctrl.Result{}, fmt.Errorf("%w: %v", errSignerBuilder, err)
}
Expand All @@ -242,7 +230,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
}
certificateRequest.Status.Certificate = signed

setReadyCondition(cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed")
issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed")
return ctrl.Result{}, nil
}

Expand Down
10 changes: 5 additions & 5 deletions internal/controllers/certificaterequest_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestCertificateRequestReconcile(t *testing.T) {
},
},
},
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.Signer, error) {
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) {
return &fakeSigner{}, nil
},
expectedReadyConditionStatus: cmmeta.ConditionTrue,
Expand Down Expand Up @@ -169,7 +169,7 @@ func TestCertificateRequestReconcile(t *testing.T) {
},
},
},
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.Signer, error) {
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) {
return &fakeSigner{}, nil
},
clusterResourceNamespace: "kube-system",
Expand Down Expand Up @@ -435,7 +435,7 @@ func TestCertificateRequestReconcile(t *testing.T) {
},
},
},
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.Signer, error) {
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) {
return nil, errors.New("simulated signer builder error")
},
expectedError: errSignerBuilder,
Expand Down Expand Up @@ -486,7 +486,7 @@ func TestCertificateRequestReconcile(t *testing.T) {
},
},
},
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.Signer, error) {
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) {
return &fakeSigner{errSign: errors.New("simulated sign error")}, nil
},
expectedError: errSignerSign,
Expand Down Expand Up @@ -533,7 +533,7 @@ func TestCertificateRequestReconcile(t *testing.T) {
},
},
},
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.Signer, error) {
Builder: func(context.Context, *ejbcaissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) {
return &fakeSigner{}, nil
},
expectedFailureTime: nil,
Expand Down
Loading

0 comments on commit 37b23ce

Please sign in to comment.