From b5fbf37287ada6bb8b0ba399edea20128ffe23c5 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Thu, 21 Sep 2023 17:44:19 -0700 Subject: [PATCH 01/14] feat(helm): Create Helm Chart for EJBCA External Issuer for cert-manager --- .gitignore | 3 + Makefile | 2 +- config/manager/kustomization.yaml | 4 +- config/manager/manager.yaml | 2 +- .../ejbca-cert-manager-issuer/.helmignore | 23 ++++ .../ejbca-cert-manager-issuer/Chart.yaml | 14 +++ .../ejbca-cert-manager-issuer/README.md | 40 +++++++ .../templates/_helpers.tpl | 62 +++++++++++ .../templates/clusterrole.yaml | 87 +++++++++++++++ .../templates/clusterrolebinding.yaml | 29 +++++ .../templates/crds/clusterissuers.yaml | 99 +++++++++++++++++ .../templates/crds/issuers.yaml | 99 +++++++++++++++++ .../templates/deployment.yaml | 101 ++++++++++++++++++ .../templates/role.yaml | 38 +++++++ .../templates/rolebinding.yaml | 14 +++ .../templates/service.yaml | 14 +++ .../templates/serviceaccount.yaml | 12 +++ .../ejbca-cert-manager-issuer/values.yaml | 58 ++++++++++ 18 files changed, 697 insertions(+), 4 deletions(-) create mode 100644 deploy/charts/ejbca-cert-manager-issuer/.helmignore create mode 100644 deploy/charts/ejbca-cert-manager-issuer/Chart.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/README.md create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/_helpers.tpl create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/crds/clusterissuers.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/crds/issuers.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/rolebinding.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/serviceaccount.yaml create mode 100644 deploy/charts/ejbca-cert-manager-issuer/values.yaml diff --git a/.gitignore b/.gitignore index 07a1e2d..ef9e7e2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ vendor/ .idea bin +# Helm +*.tgz + .DS_Store \ No newline at end of file diff --git a/Makefile b/Makefile index aa71d92..eba22e6 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # The version which will be reported by the --version argument of each binary # and which will be used as the Docker image tag -VERSION ?= v1.2.2 +VERSION ?= v1.3.1 # The Docker repository name, overridden in CI. DOCKER_REGISTRY ?= m8rmclarenkf DOCKER_IMAGE_NAME ?= ejbca-cert-manager-external-issuer-controller diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 4a0b565..aa40c30 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: ejbca-issuer-dev - newTag: latest + newName: keyfactor/ejbca-cert-manager-external-issuer-controller + newTag: v1.3.1 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index db6531c..101d40b 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -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 diff --git a/deploy/charts/ejbca-cert-manager-issuer/.helmignore b/deploy/charts/ejbca-cert-manager-issuer/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml b/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml new file mode 100644 index 0000000..f282736 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 + +name: ejbca-cert-manager-issuer +description: A Helm chart for Kubernetes +type: application + +home: https://github.com/Keyfactor/command-cert-manager-issuer +maintainers: + - name: Hayden Roszell + email: 49427552+m8rmclaren@users.noreply.github.com +sources: ["https://github.com/Keyfactor/command-cert-manager-issuer"] + +version: 0.1.0 +appVersion: "v1.3.1" diff --git a/deploy/charts/ejbca-cert-manager-issuer/README.md b/deploy/charts/ejbca-cert-manager-issuer/README.md new file mode 100644 index 0000000..9470bfe --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/README.md @@ -0,0 +1,40 @@ + + Terraform logo + + +# Keyfactor EJBCA Issuer for cert-manager + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) +![Version: v0.1.0](https://img.shields.io/badge/Version-v0.1.0-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) +![AppVersion: v1.3.1](https://img.shields.io/badge/AppVersion-v1.3.1-informational?style=flat-square) + +A Helm chart for the Keyfactor EJBCA External Issuer for cert-manager. + +The EJBCA external issuer for cert-manager allows users to enroll certificates from Keyfactor EJBCA using cert-manager. + +## Configuration + +The following table lists the configurable parameters of the `ejbca-cert-manager-issuer` chart and their default values. + +| Parameter | Description | Default | +|-----------------------------------|-----------------------------------------------------|--------------------------------------------------------------| +| `replicaCount` | Number of replica ejbca-cert-manager-issuers to run | `1` | +| `image.repository` | Image repository | `m8rmclarenkf/ejbca-cert-manager-external-issuer-controller` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag | `v1.3.1` | +| `imagePullSecrets` | Image pull secrets | `[]` | +| `nameOverride` | Name override | `""` | +| `fullnameOverride` | Full name override | `""` | +| `crd.create` | Specifies if CRDs will be created | `true` | +| `crd.annotations` | Annotations to add to the CRD | `{}` | +| `serviceAccount.create` | Specifies if a service account should be created | `true` | +| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | +| `serviceAccount.name` | Name of the service account to use | `""` (uses the fullname template if `create` is true) | +| `podAnnotations` | Annotations for the pod | `{}` | +| `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | +| `securityContext` | Security context for the pod | `{}` (with commented out options) | +| `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `tolerations` | Tolerations for pod assignment | `[]` | diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/_helpers.tpl b/deploy/charts/ejbca-cert-manager-issuer/templates/_helpers.tpl new file mode 100644 index 0000000..497a1b8 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ejbca-cert-manager-issuer.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ejbca-cert-manager-issuer.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ejbca-cert-manager-issuer.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ejbca-cert-manager-issuer.labels" -}} +helm.sh/chart: {{ include "ejbca-cert-manager-issuer.chart" . }} +{{ include "ejbca-cert-manager-issuer.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ejbca-cert-manager-issuer.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ejbca-cert-manager-issuer.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ejbca-cert-manager-issuer.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ejbca-cert-manager-issuer.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml new file mode 100644 index 0000000..45070e4 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml @@ -0,0 +1,87 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-manager-role +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - cert-manager.io + resources: + - certificaterequests + verbs: + - get + - list + - watch + - apiGroups: + - cert-manager.io + resources: + - certificaterequests/status + verbs: + - get + - patch + - update + - apiGroups: + - ejbca-issuer.keyfactor.com + resources: + - clusterissuers + - issuers + verbs: + - get + - list + - watch + - apiGroups: + - ejbca-issuer.keyfactor.com + resources: + - clusterissuers/status + - issuers/status + verbs: + - get + - patch + - update + - apiGroups: + - ejbca-issuer.keyfactor.com + resources: + - issuers/finalizers + verbs: + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..c0f3c94 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml @@ -0,0 +1,29 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "ejbca-cert-manager-issuer.name" . }}-manager-role +subjects: + - kind: ServiceAccount + name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "ejbca-cert-manager-issuer.name" . }}-proxy-role +subjects: + - kind: ServiceAccount + name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/crds/clusterissuers.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/crds/clusterissuers.yaml new file mode 100644 index 0000000..7db47fe --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/crds/clusterissuers.yaml @@ -0,0 +1,99 @@ +{{- if .Values.crd.create -}} +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + {{- with .Values.crd.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + name: clusterissuers.ejbca-issuer.keyfactor.com +spec: + group: ejbca-issuer.keyfactor.com + names: + kind: ClusterIssuer + listKind: ClusterIssuerList + plural: clusterissuers + singular: clusterissuer + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterIssuer is the Schema for the clusterissuers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IssuerSpec defines the desired state of Issuer + properties: + caBundleSecretName: + description: The name of the secret containing the CA bundle to use when verifying EJBCA's server certificate. If specified, the CA bundle will be added to the client trust roots for the EJBCA issuer. + type: string + certificateAuthorityName: + type: string + certificateProfileName: + type: string + ejbcaSecretName: + description: A reference to a Secret in the same namespace as the referent. If the referent is a ClusterIssuer, the reference instead refers to the resource with the given name in the configured 'cluster 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: + description: Hostname is the hostname of the EJBCA server + type: string + required: + - certificateAuthorityName + - certificateProfileName + - ejbcaSecretName + - endEntityProfileName + - hostname + type: object + status: + description: IssuerStatus defines the observed state of Issuer + properties: + conditions: + description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`. + items: + description: IssuerCondition contains condition information for an Issuer. + properties: + lastTransitionTime: + description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. + format: date-time + type: string + message: + description: Message is a human readable description of the details of the last transition, complementing reason. + type: string + reason: + description: Reason is a brief machine readable explanation for the condition's last transition. + type: string + status: + description: Status of the condition, one of ('True', 'False', 'Unknown'). + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of the condition, known values are ('Ready'). + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/crds/issuers.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/crds/issuers.yaml new file mode 100644 index 0000000..174bf90 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/crds/issuers.yaml @@ -0,0 +1,99 @@ +{{- if .Values.crd.create -}} +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + {{- with .Values.crd.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + name: issuers.ejbca-issuer.keyfactor.com +spec: + group: ejbca-issuer.keyfactor.com + names: + kind: Issuer + listKind: IssuerList + plural: issuers + singular: issuer + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Issuer is the Schema for the issuers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IssuerSpec defines the desired state of Issuer + properties: + caBundleSecretName: + description: The name of the secret containing the CA bundle to use when verifying EJBCA's server certificate. If specified, the CA bundle will be added to the client trust roots for the EJBCA issuer. + type: string + certificateAuthorityName: + type: string + certificateProfileName: + type: string + ejbcaSecretName: + description: A reference to a Secret in the same namespace as the referent. If the referent is a ClusterIssuer, the reference instead refers to the resource with the given name in the configured 'cluster 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: + description: Hostname is the hostname of the EJBCA server + type: string + required: + - certificateAuthorityName + - certificateProfileName + - ejbcaSecretName + - endEntityProfileName + - hostname + type: object + status: + description: IssuerStatus defines the observed state of Issuer + properties: + conditions: + description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`. + items: + description: IssuerCondition contains condition information for an Issuer. + properties: + lastTransitionTime: + description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. + format: date-time + type: string + message: + description: Message is a human readable description of the details of the last transition, complementing reason. + type: string + reason: + description: Reason is a brief machine readable explanation for the condition's last transition. + type: string + status: + description: Status of the condition, one of ('True', 'False', 'Unknown'). + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of the condition, known values are ('Ready'). + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml new file mode 100644 index 0000000..ecdfdfa --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ejbca-cert-manager-issuer.fullname" . }} + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "ejbca-cert-manager-issuer.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ejbca-cert-manager-issuer.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: {{ .Chart.Name }} + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: 10 diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml new file mode 100644 index 0000000..3f9fb4e --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml @@ -0,0 +1,38 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-leader-election-role +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/rolebinding.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/rolebinding.yaml new file mode 100644 index 0000000..23bf6d9 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "ejbca-cert-manager-issuer.name" . }}-leader-election-role +subjects: + - kind: ServiceAccount + name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml new file mode 100644 index 0000000..7edd9b5 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-metrics-service +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + {{- include "ejbca-cert-manager-issuer.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/serviceaccount.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/serviceaccount.yaml new file mode 100644 index 0000000..cf64989 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/charts/ejbca-cert-manager-issuer/values.yaml b/deploy/charts/ejbca-cert-manager-issuer/values.yaml new file mode 100644 index 0000000..ca5f86f --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/values.yaml @@ -0,0 +1,58 @@ +# Default values for ejbca-cert-manager-issuer chart. + +# The number of replica ejbca-cert-manager-issuers to run +replicaCount: 1 + +image: + repository: m8rmclarenkf/ejbca-cert-manager-external-issuer-controller + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "v1.3.1" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +crd: + # Specifies whether CRDs will be created + create: true + # Annotations to add to the CRD + annotations: {} + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: + runAsNonRoot: true + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] From 5ee6798904f46499efc577ef0a80237a458c7f4d Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Thu, 21 Sep 2023 17:53:44 -0700 Subject: [PATCH 02/14] docs() Change chart source to correct URL --- deploy/charts/ejbca-cert-manager-issuer/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml b/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml index f282736..81e4103 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml @@ -4,11 +4,11 @@ name: ejbca-cert-manager-issuer description: A Helm chart for Kubernetes type: application -home: https://github.com/Keyfactor/command-cert-manager-issuer +home: https://github.com/Keyfactor/ejbca-cert-manager-issuer maintainers: - name: Hayden Roszell email: 49427552+m8rmclaren@users.noreply.github.com -sources: ["https://github.com/Keyfactor/command-cert-manager-issuer"] +sources: ["https://github.com/Keyfactor/ejbca-cert-manager-issuer"] version: 0.1.0 appVersion: "v1.3.1" From 50540688f0e6efa84bfd5bf24417a58911453a53 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Thu, 21 Sep 2023 21:41:20 -0700 Subject: [PATCH 03/14] Remove default image tag from values --- deploy/charts/ejbca-cert-manager-issuer/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/charts/ejbca-cert-manager-issuer/values.yaml b/deploy/charts/ejbca-cert-manager-issuer/values.yaml index ca5f86f..13226c0 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/values.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/values.yaml @@ -7,7 +7,7 @@ image: repository: m8rmclarenkf/ejbca-cert-manager-external-issuer-controller pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "v1.3.1" + tag: "" imagePullSecrets: [] nameOverride: "" From 434ba04e30aec108c587de36653f7b747f1e77c2 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Thu, 21 Sep 2023 23:08:52 -0700 Subject: [PATCH 04/14] feat(helm): Create release actions --- .github/workflows/release.yml | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f611e45 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: helm_release +on: + pull_request: + branches: + - 'v*' + types: + - closed +jobs: + helm: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - name: Extract Version Tag + id: extract_version + run: /bin/bash -c 'echo ::set-output name=VERSION::$(echo ${GITHUB_REF##*/} | cut -c2-)' + + - name: Checkout + uses: actions/checkout@v3 + + # Change version and appVersion in Chart.yaml to the tag in the closed PR + - name: Update Helm App/Chart Version + shell: bash + run: | + sed -i "s/^version: .*/version: ${{ steps.extract_version.outputs.VERSION }}/g" deploy/charts/ejbca-cert-manager-issuer/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${{ steps.extract_version.outputs.VERSION }}\"/g" deploy/charts/ejbca-cert-manager-issuer/Chart.yaml + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v3 + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.5.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + with: + pages_branch: gh-pages + charts_dir: deploy/charts + mark_as_latest: true + packages_with_index: true \ No newline at end of file From c8d123fd04280cf3bc0ec59e62f3686f7180559b Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Thu, 21 Sep 2023 23:12:29 -0700 Subject: [PATCH 05/14] feat(helm): Change semantic version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index eba22e6..75ef984 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # The version which will be reported by the --version argument of each binary # and which will be used as the Docker image tag -VERSION ?= v1.3.1 +VERSION ?= 1.3.1 # The Docker repository name, overridden in CI. DOCKER_REGISTRY ?= m8rmclarenkf DOCKER_IMAGE_NAME ?= ejbca-cert-manager-external-issuer-controller From 09e17546ac9dcd22ca103ff26714e20f966be3cd Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 26 Sep 2023 15:10:41 -0700 Subject: [PATCH 06/14] Detailed chart description --- deploy/charts/ejbca-cert-manager-issuer/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml b/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml index 81e4103..0b68d2f 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: ejbca-cert-manager-issuer -description: A Helm chart for Kubernetes +description: A helm chart to deploy the cert-manager issuer for Keyfactor EJBCA type: application home: https://github.com/Keyfactor/ejbca-cert-manager-issuer From 6b3b2b1a66cca578c86039d2c215b7e05544faab Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Wed, 22 Nov 2023 10:48:58 -0700 Subject: [PATCH 07/14] chore: Remove unnecessary permissions from Helm chart, feature parity with CM [#3], refactor docs --- Makefile | 10 +- README.md | 321 +----------------- api/v1alpha1/issuer_types.go | 6 - .../ejbca-cert-manager-issuer/README.md | 34 ++ .../templates/clusterrole.yaml | 4 +- .../templates/clusterrolebinding.yaml | 4 +- .../templates/deployment.yaml | 2 + .../templates/role.yaml | 12 - .../templates/service.yaml | 4 +- .../ejbca-cert-manager-issuer/values.yaml | 5 + docs/annotations.md | 53 +++ docs/config_usage.md | 155 +++++++++ docs/endentitynamecustomization.md | 34 ++ docs/install.md | 107 ++++++ docs/testing.md | 21 ++ .../certificaterequest_controller.go | 5 +- .../certificaterequest_controller_test.go | 4 +- internal/issuer/signer/signer.go | 45 ++- internal/issuer/signer/signer_test.go | 5 +- 19 files changed, 470 insertions(+), 361 deletions(-) create mode 100644 docs/annotations.md create mode 100644 docs/config_usage.md create mode 100644 docs/endentitynamecustomization.md create mode 100644 docs/install.md create mode 100644 docs/testing.md diff --git a/Makefile b/Makefile index 75ef984..799228a 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ # The version which will be reported by the --version argument of each binary # and which will be used as the Docker image tag -VERSION ?= 1.3.1 +VERSION ?= latest # The Docker repository name, overridden in CI. -DOCKER_REGISTRY ?= m8rmclarenkf -DOCKER_IMAGE_NAME ?= ejbca-cert-manager-external-issuer-controller +DOCKER_REGISTRY ?= "" +DOCKER_IMAGE_NAME ?= "" # Image URL to use all building/pushing image targets IMG ?= ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${VERSION} @@ -78,7 +78,7 @@ run: manifests generate fmt vet ## Run a controller from your host. # (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build -docker-build: test ## Build docker image with the manager. +docker-build: ## Build docker image with the manager. docker build -t ${IMG} . .PHONY: docker-push @@ -93,7 +93,7 @@ docker-push: ## Push docker image with the manager. # To properly provided solutions that supports more than one platform you should use this option. PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le .PHONY: docker-buildx -docker-buildx: test ## Build and push docker image for the manager for cross-platform support +docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - docker buildx create --name project-v3-builder diff --git a/README.md b/README.md index 09b4c39..e71df75 100644 --- a/README.md +++ b/README.md @@ -23,315 +23,12 @@ The EJBCA Issuer for cert-manager requires the following API endpoints: * `/ejbca-rest-api/v1/certificate/pkcs10enroll` * `/ejbca/ejbca-rest-api/v1/certificate/status` -## Quick Start - -The quick start guide will walk you through the process of installing the cert-manager external issuer for Keyfactor EJBCA. -The controller image is pulled from [Docker Hub](https://hub.docker.com/r/m8rmclarenkf/command-external-issuer). - -###### To build the container from sources, refer to the [Building Container Image from Source](#building-container-image-from-source) section. - -### Requirements -* [Git](https://git-scm.com/) -* [Make](https://www.gnu.org/software/make/) -* [Docker](https://docs.docker.com/engine/install/) >= v20.10.0 -* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) >= v1.11.3 -* Kubernetes >= v1.19 - * [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), or [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) -* [Keyfactor EJBCA](https://www.keyfactor.com/products/ejbca-enterprise/) >= v7.7 -* [cert-manager](https://cert-manager.io/docs/installation/) >= v1.11.0 -* [cmctl](https://cert-manager.io/docs/reference/cmctl/) - -Before starting, ensure that all the requirements above are met, and that at least one Kubernetes node is running by running the following command: -```shell -kubectl get nodes -``` - -Once Kubernetes is running, a static installation of cert-manager can be installed with the following command: -```shell -kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml -``` - -Then, install the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor EJBCA: -```shell -make install -``` - -Finally, deploy the controller to the cluster: -```shell -make deploy -``` - -## Usage -The cert-manager external issuer for Keyfactor EJBCA can be used to issue certificates from Keyfactor EJBCA using cert-manager. - -### Authentication -Authentication to the EJBCA platform is done using a client certificate and key. The client certificate and key must be provided as a Kubernetes secret. - -Create a K8s TLS secret containing the client certificate and key to authenticate with EJBCA: -```shell -kubectl -n ejbca-issuer-system create secret tls ejbca-secret --cert=client.crt --key=client.key -``` - -If the EJBCA API is configured to use a self-signed certificate or with a certificate signed by an untrusted root, the CA certificate must be provided as a Kubernetes secret. -```shell -kubectl -n ejbca-issuer-system create secret generic ejbca-ca-secret --from-file=ca.crt -``` - -### Creating Issuer and ClusterIssuer resources -The `ejbca-issuer.keyfactor.com/v1alpha1` API version supports Issuer and ClusterIssuer resources. -The ejbca controller will automatically detect and process resources of both types. - -The Issuer resource is namespaced, while the ClusterIssuer resource is cluster-scoped. -For example, ClusterIssuer resources can be used to issue certificates for resources in multiple namespaces, whereas Issuer resources can only be used to issue certificates for resources in the same namespace. - -The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: -* `hostname` - The hostname of the EJBCA instance -* `ejbcaSecretName` - The name of the Kubernetes secret containing the client certificate and key -* `certificateAuthorityName` - The name of the EJBCA certificate authority to use. For example, `ManagementCA` -* `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. - -The following is an example of an Issuer resource: -```yaml -apiVersion: ejbca-issuer.keyfactor.com/v1alpha1 -kind: Issuer -metadata: - labels: - app.kubernetes.io/name: issuer - app.kubernetes.io/instance: issuer-sample - app.kubernetes.io/part-of: ejbca-issuer - app.kubernetes.io/created-by: ejbca-issuer - name: issuer-sample -spec: - hostname: "" - ejbcaBundleSecretName: "" - certificateAuthorityName: "" - certificateProfileName: "" - endEntityProfileName: "" - caBundleSecretName: "" - endEntityName: "" -``` - -The following is an example of a ClusterIssuer resource: -```yaml -apiVersion: ejbca-issuer.keyfactor.com/v1alpha1 -kind: ClusterIssuer -metadata: - labels: - app.kubernetes.io/name: clusterissuer - app.kubernetes.io/instance: clusterissuer-sample - app.kubernetes.io/part-of: ejbca-issuer - app.kubernetes.io/created-by: ejbca-issuer - name: clusterissuer-sample -spec: - hostname: "" - ejbcaBundleSecretName: "" - certificateAuthorityName: "" - 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: -```shell -kubectl -n ejbca-issuer-system apply -f issuer.yaml -kubectl -n ejbca-issuer-system apply -f clusterissuer.yaml -``` - -To verify that Issuer and ClusterIssuer resources were created successfully, run the following commands: -```shell -kubectl -n ejbca-issuer-system get issuers.ejbca-issuer.keyfactor.com -kubectl -n ejbca-issuer-system get clusterissuers.ejbca-issuer.keyfactor.com -``` - -### Using Issuer and ClusterIssuer resources -Once the Issuer and ClusterIssuer resources are created, they can be used to issue certificates using cert-manager. -The two most important concepts are `Certificate` and `CertificateRequest` resources. `Certificate` -resources represent a single X.509 certificate and its associated attributes, and automatically renews the certificate -and keeps it up to date. When `Certificate` resources are created, they create `CertificateRequest` resources, which -use an Issuer or ClusterIssuer to actually issue the certificate. - -###### To learn more about cert-manager, see the [cert-manager documentation](https://cert-manager.io/docs/). - -The following is an example of a Certificate resource. This resource will create a corresponding CertificateRequest resource, -and will use the `issuer-sample` Issuer resource to issue the certificate. Once issued, the certificate will be stored in a -Kubernetes secret named `ejbca-certificate`. -```yaml -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: ejbca-certificate -spec: - commonName: ejbca-issuer-sample - secretName: ejbca-certificate - issuerRef: - name: issuer-sample - group: ejbca-issuer.keyfactor.com - kind: Issuer -``` - -###### Certificate resources support many more fields than the above example. See the [Certificate resource documentation](https://cert-manager.io/docs/usage/certificate/) for more information. - -Similarly, a CertificateRequest resource can be created directly. The following is an example of a CertificateRequest resource. -```yaml -apiVersion: cert-manager.io/v1 -kind: CertificateRequest -metadata: - name: ejbca-certificate -spec: - request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ2REQ0NBVndDQVFBd0x6RUxNQWtHQTFVRUN4TUNTVlF4SURBZUJnTlZCQU1NRjJWcVltTmhYM1JsY25KaApabTl5YlY5MFpYTjBZV05qTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4blNSCklqZDZSN2NYdUNWRHZscXlFcUhKalhIazljN21pNTdFY3A1RXVnblBXa0YwTHBVc25PMld6WTE1bjV2MHBTdXMKMnpYSURhS3NtZU9ZQzlNOWtyRjFvOGZBelEreHJJWk5SWmg0cUZXRmpyNFV3a0EySTdUb05veitET2lWZzJkUgo1cnNmaFdHMmwrOVNPT3VscUJFcWVEcVROaWxyNS85OVpaemlBTnlnL2RiQXJibWRQQ1o5OGhQLzU0NDZhci9NCjdSd2ludjVCMnNRcWM0VFZwTTh3Nm5uUHJaQXA3RG16SktZbzVOQ3JyTmw4elhIRGEzc3hIQncrTU9DQUw0T00KTkJuZHpHSm5KenVyS0c3RU5UT3FjRlZ6Z3liamZLMktyMXRLS3pyVW5keTF1bTlmTWtWMEZCQnZ0SGt1ZG0xdwpMUzRleW1CemVtakZXQi9yRVFJREFRQUJvQUF3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUJhdFpIVTdOczg2Cmgxc1h0d0tsSi95MG1peG1vRWJhUTNRYXAzQXVFQ2x1U09mdjFDZXBQZjF1N2dydEp5ZGRha1NLeUlNMVNzazAKcWNER2NncUsxVVZDR21vRkp2REZEaEUxMkVnM0ZBQ056UytFNFBoSko1N0JBSkxWNGZaeEpZQ3JyRDUxWnk3NgpPd01ORGRYTEVib0w0T3oxV3k5ZHQ3bngyd3IwWTNZVjAyL2c0dlBwaDVzTHl0NVZOWVd6eXJTMzJYckJwUWhPCnhGMmNNUkVEMUlaRHhuMjR2ZEtINjMzSFo1QXd0YzRYamdYQ3N5VW5mVUE0ZjR1cHBEZWJWYmxlRFlyTW1iUlcKWW1NTzdLTjlPb0MyZ1lVVVpZUVltdHlKZTJkYXlZSHVyUUlpK0ZsUU5zZjhna1hYeG45V2drTnV4ZTY3U0x5dApVNHF4amE4OCs1ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0t - issuerRef: - name: issuer-sample - group: ejbca-issuer.keyfactor.com - kind: Issuer -``` - -### Approving Certificate Requests -Unless the cert-manager internal approver automatically approves the request, newly created CertificateRequest resources -will be in a `Pending` state until they are approved. CertificateRequest resources can be approved manually by using -[cmctl](https://cert-manager.io/docs/reference/cmctl/#approve-and-deny-certificaterequests). The following is an example -of approving a CertificateRequest resource named `ejbca-certificate` in the `ejbca-issuer-system` namespace. -```shell -cmctl -n ejbca-issuer-system approve ejbca-certificate -``` - -Once a certificate request has been approved, the certificate will be issued and stored in the secret specified in the -CertificateRequest resource. The following is an example of retrieving the certificate from the secret. -```shell -kubectl get secret ejbca-certificate -n ejbca-issuer-system -o jsonpath='{.data.tls\.crt}' | base64 -d -``` - -###### 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 -kubectl get certificates -n ejbca-issuer-system -kubectl get certificaterequests -n ejbca-issuer-system -``` - -To remove the certificate and certificate request resources, run the following commands: -```shell -kubectl delete certificate ejbca-certificate -n ejbca-issuer-system -kubectl delete certificaterequest ejbca-certificate -n ejbca-issuer-system -``` - -To list the issuer and cluster issuer resources created, run the following commands: -```shell -kubectl -n ejbca-issuer-system get issuers.ejbca-issuer.keyfactor.com -kubectl -n ejbca-issuer-system get clusterissuers.ejbca-issuer.keyfactor.com -``` - -To remove the issuer and cluster issuer resources, run the following commands: -```shell -kubectl -n ejbca-issuer-system delete issuers.ejbca-issuer.keyfactor.com -kubectl -n ejbca-issuer-system delete clusterissuers.ejbca-issuer.keyfactor.com -``` - -To remove the controller from the cluster, run: -```shell -make undeploy -``` - -To remove the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor EJBCA, run: -```shell -make uninstall -``` - -## Building Container Image from Source - -### Requirements -* [Golang](https://golang.org/) >= v1.19 - -Building the container from source first runs appropriate test cases, which requires all requirements also listed in the -Quick Start section. As part of this testing is an enrollment of a certificate with EJBCA, so a running instance of EJBCA -is also required. - -The following environment variables must be exported before building the container image: -* `EJBCA_HOSTNAME` - The hostname of the EJBCA instance to use for testing. -* `EJBCA_CLIENT_CERT_PATH` - A relative or absolute path to a client certificate that is authorized to enroll certificates in EJBCA. The file must include the certificate and associated private key in unencrypted PKCS#8 format. -* `EJBCA_CA_NAME` - The name of the CA in EJBCA to use for testing. -* `EJBCA_CERTIFICATE_PROFILE_NAME` - The name of the certificate profile in EJBCA to use for testing. -* `EJBCA_END_ENTITY_PROFILE_NAME` - The name of the end entity profile in EJBCA to use for testing. -* `EJBCA_CSR_SUBJECT` - The subject of the certificate signing request (CSR) to use for testing. -* `EJBCA_CA_CERT_PATH` - A relative or absolute path to the CA certificate that the EJBCA instance uses for TLS. The file must include the certificate in PEM format. - -To build the cert-manager external issuer for Keyfactor EJBCA, run: -```shell -make docker-build -``` +## Docs + +* [Installation](docs/install.md) +* Usage + * [Usage](docs/config_usage.md) + * [Customization](docs/annotations.md) + * [End Entity Name Selection](docs/endentitynamecustomization.md) +* [Testing the Source](docs/testing.md) +* [License](LICENSE) diff --git a/api/v1alpha1/issuer_types.go b/api/v1alpha1/issuer_types.go index 67f7e31..33f254d 100644 --- a/api/v1alpha1/issuer_types.go +++ b/api/v1alpha1/issuer_types.go @@ -35,12 +35,6 @@ type IssuerSpec struct { // namespace that the controller runs in). EjbcaSecretName string `json:"ejbcaSecretName"` - // A reference to a Secret in the same namespace as the referent. If the - // referent is a ClusterIssuer, the reference instead refers to the resource - // with the given name in the configured 'cluster resource namespace', which - // is set as a flag on the controller component (and defaults to the - // namespace that the controller runs in). - // The name of the secret containing the CA bundle to use when verifying // EJBCA's server certificate. If specified, the CA bundle will be added to // the client trust roots for the EJBCA issuer. diff --git a/deploy/charts/ejbca-cert-manager-issuer/README.md b/deploy/charts/ejbca-cert-manager-issuer/README.md index 9470bfe..d05c309 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/README.md +++ b/deploy/charts/ejbca-cert-manager-issuer/README.md @@ -14,6 +14,39 @@ A Helm chart for the Keyfactor EJBCA External Issuer for cert-manager. The EJBCA external issuer for cert-manager allows users to enroll certificates from Keyfactor EJBCA using cert-manager. +## Installation + +### Add Helm Repository + +```bash +helm repo add ejbca-issuer https://keyfactor.github.io/ejbca-cert-manager-issuer +helm repo update +``` + +### Install Chart + +```bash +helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer +``` + +Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following ejbca: +```bash +helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --set replicaCount=2 +``` + +Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the `replicaCount` value, modify the `replicaCount` value in the `values.yaml` file: +```yaml +cat < override.yaml +replicaCount: 2 +EOF +``` +Then, use the `-f` flag to specify the `values.yaml` file: +```bash +helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + -f override.yaml +``` + ## Configuration The following table lists the configurable parameters of the `ejbca-cert-manager-issuer` chart and their default values. @@ -35,6 +68,7 @@ The following table lists the configurable parameters of the `ejbca-cert-manager | `podAnnotations` | Annotations for the pod | `{}` | | `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | | `securityContext` | Security context for the pod | `{}` (with commented out options) | +| `secureMetrics.enabled` | Enable secure metrics via the Kube RBAC Proy | `false` | | `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | | `nodeSelector` | Node labels for pod assignment | `{}` | | `tolerations` | Tolerations for pod assignment | `[]` | diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml index 45070e4..f8bcd69 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml @@ -53,6 +53,7 @@ rules: - issuers/finalizers verbs: - update +{{- if .Values.secureMetrics.enabled }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -84,4 +85,5 @@ rules: - nonResourceURLs: - /metrics verbs: - - get \ No newline at end of file + - get +{{- end }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml index c0f3c94..18bddf0 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrolebinding.yaml @@ -12,6 +12,7 @@ subjects: - kind: ServiceAccount name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} namespace: {{ .Release.Namespace }} +{{- if .Values.secureMetrics.enabled }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -26,4 +27,5 @@ roleRef: subjects: - kind: ServiceAccount name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} \ No newline at end of file + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml index ecdfdfa..2707d04 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml @@ -26,6 +26,7 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: + {{- if .Values.secureMetrics.enabled }} - args: - --secure-listen-address=0.0.0.0:8443 - --upstream=http://127.0.0.1:8080/ @@ -49,6 +50,7 @@ spec: capabilities: drop: - ALL + {{- end }}} - args: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml index 3f9fb4e..13f93c8 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/role.yaml @@ -5,18 +5,6 @@ metadata: {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} name: {{ include "ejbca-cert-manager-issuer.name" . }}-leader-election-role rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - apiGroups: - coordination.k8s.io resources: diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml index 7edd9b5..00ba749 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.secureMetrics.enabled }} apiVersion: v1 kind: Service metadata: @@ -11,4 +12,5 @@ spec: protocol: TCP targetPort: https selector: - {{- include "ejbca-cert-manager-issuer.selectorLabels" . | nindent 4 }} \ No newline at end of file + {{- include "ejbca-cert-manager-issuer.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/values.yaml b/deploy/charts/ejbca-cert-manager-issuer/values.yaml index 13226c0..881005b 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/values.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/values.yaml @@ -13,6 +13,11 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# Whether to enable and configure the kube-rbac-proxy sidecar for authorized and authenticated +# use of the /metrics endpoint by Prometheus. +secureMetrics: + enabled: false + crd: # Specifies whether CRDs will be created create: true diff --git a/docs/annotations.md b/docs/annotations.md new file mode 100644 index 0000000..db66268 --- /dev/null +++ b/docs/annotations.md @@ -0,0 +1,53 @@ + + Terraform logo + + +## Annotation Overrides for Issuer and ClusterIssuer Resources + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +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 +``` diff --git a/docs/config_usage.md b/docs/config_usage.md new file mode 100644 index 0000000..8194f54 --- /dev/null +++ b/docs/config_usage.md @@ -0,0 +1,155 @@ + + Terraform logo + + +# Installing the Keyfactor EJBCA Issuer for cert-manager + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +The cert-manager external issuer for Keyfactor EJBCA can be used to issue certificates from Keyfactor EJBCA using cert-manager. + +### Authentication +Authentication to the EJBCA platform is done using a client certificate and key. The client certificate and key must be provided as a Kubernetes secret. + +Create a K8s TLS secret containing the client certificate and key to authenticate with EJBCA: +```shell +kubectl -n ejbca-issuer-system create secret tls ejbca-secret --cert=client.crt --key=client.key +``` + +If the EJBCA API is configured to use a self-signed certificate or with a certificate signed by an untrusted root, the CA certificate must be provided as a Kubernetes secret. +```shell +kubectl -n ejbca-issuer-system create secret generic ejbca-ca-secret --from-file=ca.crt +``` + +### Creating Issuer and ClusterIssuer resources +The `ejbca-issuer.keyfactor.com/v1alpha1` API version supports Issuer and ClusterIssuer resources. +The ejbca controller will automatically detect and process resources of both types. + +The Issuer resource is namespaced, while the ClusterIssuer resource is cluster-scoped. +For example, ClusterIssuer resources can be used to issue certificates for resources in multiple namespaces, whereas Issuer resources can only be used to issue certificates for resources in the same namespace. + +The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: +* `hostname` - The hostname of the EJBCA instance +* `ejbcaSecretName` - The name of the Kubernetes secret containing the client certificate and key +* `certificateAuthorityName` - The name of the EJBCA certificate authority to use. For example, `ManagementCA` +* `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. + +The following is an example of an Issuer resource: +```yaml +apiVersion: ejbca-issuer.keyfactor.com/v1alpha1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: issuer + app.kubernetes.io/instance: issuer-sample + app.kubernetes.io/part-of: ejbca-issuer + app.kubernetes.io/created-by: ejbca-issuer + name: issuer-sample +spec: + hostname: "" + ejbcaBundleSecretName: "" + certificateAuthorityName: "" + certificateProfileName: "" + endEntityProfileName: "" + caBundleSecretName: "" + endEntityName: "" +``` + +The following is an example of a ClusterIssuer resource: +```yaml +apiVersion: ejbca-issuer.keyfactor.com/v1alpha1 +kind: ClusterIssuer +metadata: + labels: + app.kubernetes.io/name: clusterissuer + app.kubernetes.io/instance: clusterissuer-sample + app.kubernetes.io/part-of: ejbca-issuer + app.kubernetes.io/created-by: ejbca-issuer + name: clusterissuer-sample +spec: + hostname: "" + ejbcaBundleSecretName: "" + certificateAuthorityName: "" + 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: +```shell +kubectl -n ejbca-issuer-system apply -f issuer.yaml +kubectl -n ejbca-issuer-system apply -f clusterissuer.yaml +``` + +To verify that Issuer and ClusterIssuer resources were created successfully, run the following commands: +```shell +kubectl -n ejbca-issuer-system get issuers.ejbca-issuer.keyfactor.com +kubectl -n ejbca-issuer-system get clusterissuers.ejbca-issuer.keyfactor.com +``` + +### Using Issuer and ClusterIssuer resources +Once the Issuer and ClusterIssuer resources are created, they can be used to issue certificates using cert-manager. +The two most important concepts are `Certificate` and `CertificateRequest` resources. `Certificate` +resources represent a single X.509 certificate and its associated attributes, and automatically renews the certificate +and keeps it up to date. When `Certificate` resources are created, they create `CertificateRequest` resources, which +use an Issuer or ClusterIssuer to actually issue the certificate. + +###### To learn more about cert-manager, see the [cert-manager documentation](https://cert-manager.io/docs/). + +The following is an example of a Certificate resource. This resource will create a corresponding CertificateRequest resource, +and will use the `issuer-sample` Issuer resource to issue the certificate. Once issued, the certificate will be stored in a +Kubernetes secret named `ejbca-certificate`. +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ejbca-certificate +spec: + commonName: ejbca-issuer-sample + secretName: ejbca-certificate + issuerRef: + name: issuer-sample + group: ejbca-issuer.keyfactor.com + kind: Issuer +``` + +###### Certificate resources support many more fields than the above example. See the [Certificate resource documentation](https://cert-manager.io/docs/usage/certificate/) for more information. + +Similarly, a CertificateRequest resource can be created directly. The following is an example of a CertificateRequest resource. +```yaml +apiVersion: cert-manager.io/v1 +kind: CertificateRequest +metadata: + name: ejbca-certificate +spec: + request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ2REQ0NBVndDQVFBd0x6RUxNQWtHQTFVRUN4TUNTVlF4SURBZUJnTlZCQU1NRjJWcVltTmhYM1JsY25KaApabTl5YlY5MFpYTjBZV05qTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4blNSCklqZDZSN2NYdUNWRHZscXlFcUhKalhIazljN21pNTdFY3A1RXVnblBXa0YwTHBVc25PMld6WTE1bjV2MHBTdXMKMnpYSURhS3NtZU9ZQzlNOWtyRjFvOGZBelEreHJJWk5SWmg0cUZXRmpyNFV3a0EySTdUb05veitET2lWZzJkUgo1cnNmaFdHMmwrOVNPT3VscUJFcWVEcVROaWxyNS85OVpaemlBTnlnL2RiQXJibWRQQ1o5OGhQLzU0NDZhci9NCjdSd2ludjVCMnNRcWM0VFZwTTh3Nm5uUHJaQXA3RG16SktZbzVOQ3JyTmw4elhIRGEzc3hIQncrTU9DQUw0T00KTkJuZHpHSm5KenVyS0c3RU5UT3FjRlZ6Z3liamZLMktyMXRLS3pyVW5keTF1bTlmTWtWMEZCQnZ0SGt1ZG0xdwpMUzRleW1CemVtakZXQi9yRVFJREFRQUJvQUF3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUJhdFpIVTdOczg2Cmgxc1h0d0tsSi95MG1peG1vRWJhUTNRYXAzQXVFQ2x1U09mdjFDZXBQZjF1N2dydEp5ZGRha1NLeUlNMVNzazAKcWNER2NncUsxVVZDR21vRkp2REZEaEUxMkVnM0ZBQ056UytFNFBoSko1N0JBSkxWNGZaeEpZQ3JyRDUxWnk3NgpPd01ORGRYTEVib0w0T3oxV3k5ZHQ3bngyd3IwWTNZVjAyL2c0dlBwaDVzTHl0NVZOWVd6eXJTMzJYckJwUWhPCnhGMmNNUkVEMUlaRHhuMjR2ZEtINjMzSFo1QXd0YzRYamdYQ3N5VW5mVUE0ZjR1cHBEZWJWYmxlRFlyTW1iUlcKWW1NTzdLTjlPb0MyZ1lVVVpZUVltdHlKZTJkYXlZSHVyUUlpK0ZsUU5zZjhna1hYeG45V2drTnV4ZTY3U0x5dApVNHF4amE4OCs1ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0t + issuerRef: + name: issuer-sample + group: ejbca-issuer.keyfactor.com + kind: Issuer +``` + +### Approving Certificate Requests +Unless the cert-manager internal approver automatically approves the request, newly created CertificateRequest resources +will be in a `Pending` state until they are approved. CertificateRequest resources can be approved manually by using +[cmctl](https://cert-manager.io/docs/reference/cmctl/#approve-and-deny-certificaterequests). The following is an example +of approving a CertificateRequest resource named `ejbca-certificate` in the `ejbca-issuer-system` namespace. +```shell +cmctl -n ejbca-issuer-system approve ejbca-certificate +``` + +Once a certificate request has been approved, the certificate will be issued and stored in the secret specified in the +CertificateRequest resource. The following is an example of retrieving the certificate from the secret. +```shell +kubectl get secret ejbca-certificate -n ejbca-issuer-system -o jsonpath='{.data.tls\.crt}' | base64 -d +``` + +###### To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval). + diff --git a/docs/endentitynamecustomization.md b/docs/endentitynamecustomization.md new file mode 100644 index 0000000..90304d1 --- /dev/null +++ b/docs/endentitynamecustomization.md @@ -0,0 +1,34 @@ + + Terraform logo + + +# EJBCA End Entity Name Configuration + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +The `defaultEndEntityName` 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. + +## 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. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..100c9de --- /dev/null +++ b/docs/install.md @@ -0,0 +1,107 @@ + + Terraform logo + + +# Installing the Keyfactor EJBCA Issuer for cert-manager + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +### Requirements +* [Git](https://git-scm.com/) +* [Make](https://www.gnu.org/software/make/) +* [Docker](https://docs.docker.com/engine/install/) >= v20.10.0 +* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) >= v1.11.3 +* Kubernetes >= v1.19 + * [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), or [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) +* [Keyfactor EJBCA](https://www.keyfactor.com/products/ejbca-enterprise/) >= v7.7 +* [cert-manager](https://cert-manager.io/docs/installation/) >= v1.11.0 +* [cmctl](https://cert-manager.io/docs/reference/cmctl/) + +Before starting, ensure that all of the above requirements are met, and that Keyfactor EJBCA is properly configured according to the [product docs](https://software.keyfactor.com/Content/MasterTopics/Home.htm). Additionally, verify that at least one Kubernetes node is running by running the following command: + +```shell +kubectl get nodes +``` + +A static installation of cert-manager can be installed with the following command: + +```shell +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml +``` + +###### :pushpin: Running the static cert-manager configuration is not recommended for production use. For more information, see [Installing cert-manager](https://cert-manager.io/docs/installation/). + +### Building the Container Image + +The cert-manager external issuer for Keyfactor EJBCA is distributed as source code, and the container must be built manually. The container image can be built using the following command: +```shell +make docker-build DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/ejbca-cert-manager-issuer +``` + +###### :pushpin: The container image can be built using Docker Buildx by running `make docker-buildx`. This will build the image for all supported platforms. + +To push the container image to a container registry, run the following command: +```shell +docker login +make docker-push DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/ejbca-cert-manager-issuer +``` + +### Installation from Helm Chart + +The cert-manager external issuer for Keyfactor EJBCA can also be installed using a Helm chart. The chart is available in the [EJBCA cert-manager Helm repository](https://keyfactor.github.io/ejbca-cert-manager-issuer/). + +1. Add the Helm repository: + + ```bash + helm repo add ejbca-issuer https://keyfactor.github.io/ejbca-cert-manager-issuer + helm repo update + ``` + +2. Then, install the chart: + + ```bash + helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace ejbca-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ + --set image.tag= + # --set image.pullPolicy=Never # Only required if using a local image + ``` + + a. Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following command: + + ```shell + helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace ejbca-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ + --set image.tag= + --set replicaCount=2 + ``` + + b. Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the + `replicaCount` value, modify the `replicaCount` value in the `values.yaml` file: + + ```yaml + cat < override.yaml + image: + repository: /keyfactor/ejbca-cert-manager-issuer + pullPolicy: Never + tag: "latest" + replicaCount: 2 + EOF + ``` + + Then, use the `-f` flag to specify the `values.yaml` file: + + ```yaml + helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + -f override.yaml + ``` + +Next, complete the [Usage](config_usage.md) steps to configure the cert-manager external issuer for Keyfactor EJBCA. + + + + diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..ae740b5 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,21 @@ +# Testing the Keyfactor EJBCA Issuer for cert-manager + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +The test cases for the controller require a set of environment variables to be set. These variables are used to +authenticate to an EJBCA API server and to enroll a certificate. The test cases are run using the `make test` command. + +The following environment variables must be exported before testing the controller: +* `EJBCA_HOSTNAME` - The hostname of the EJBCA instance to use for testing. +* `EJBCA_CLIENT_CERT_PATH` - A relative or absolute path to a client certificate that is authorized to enroll certificates in EJBCA. The file must include the certificate and associated private key in unencrypted PKCS#8 format. +* `EJBCA_CA_NAME` - The name of the CA in EJBCA to use for testing. +* `EJBCA_CERTIFICATE_PROFILE_NAME` - The name of the certificate profile in EJBCA to use for testing. +* `EJBCA_END_ENTITY_PROFILE_NAME` - The name of the end entity profile in EJBCA to use for testing. +* `EJBCA_CSR_SUBJECT` - The subject of the certificate signing request (CSR) to use for testing. +* `EJBCA_CA_CERT_PATH` - A relative or absolute path to the CA certificate that the EJBCA instance uses for TLS. The file must include the certificate in PEM format. + +To run the test cases, run: +```shell +make test +``` diff --git a/internal/controllers/certificaterequest_controller.go b/internal/controllers/certificaterequest_controller.go index 28e48e9..f48e760 100644 --- a/internal/controllers/certificaterequest_controller.go +++ b/internal/controllers/certificaterequest_controller.go @@ -224,11 +224,12 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, fmt.Errorf("%w: %v", errSignerBuilder, err) } - signed, err := ejbcaSigner.Sign(ctx, certificateRequest.Spec.Request) + leaf, chain, err := ejbcaSigner.Sign(ctx, certificateRequest.Spec.Request) if err != nil { return ctrl.Result{}, fmt.Errorf("%w: %v", errSignerSign, err) } - certificateRequest.Status.Certificate = signed + certificateRequest.Status.Certificate = leaf + certificateRequest.Status.CA = chain issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed") return ctrl.Result{}, nil diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controllers/certificaterequest_controller_test.go index 07d046f..1687556 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controllers/certificaterequest_controller_test.go @@ -54,8 +54,8 @@ type fakeSigner struct { errSign error } -func (o *fakeSigner) Sign(context.Context, []byte) ([]byte, error) { - return []byte("fake signed certificate"), o.errSign +func (o *fakeSigner) Sign(context.Context, []byte) ([]byte, []byte, error) { + return []byte("fake signed certificate"), []byte("fake chain"), o.errSign } func TestCertificateRequestReconcile(t *testing.T) { diff --git a/internal/issuer/signer/signer.go b/internal/issuer/signer/signer.go index 83a9142..6fdb509 100644 --- a/internal/issuer/signer/signer.go +++ b/internal/issuer/signer/signer.go @@ -49,7 +49,7 @@ type HealthCheckerBuilder func(context.Context, *ejbcaissuer.IssuerSpec, map[str type EjbcaSignerBuilder func(context.Context, *ejbcaissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (Signer, error) type Signer interface { - Sign(context.Context, []byte) ([]byte, error) + Sign(context.Context, []byte) ([]byte, []byte, error) } func EjbcaHealthCheckerFromIssuerAndSecretData(ctx context.Context, spec *ejbcaissuer.IssuerSpec, clientCertSecretData map[string][]byte, caCertSecretData map[string][]byte) (HealthChecker, error) { @@ -192,12 +192,12 @@ func (s *ejbcaSigner) getEndEntityName(ctx context.Context, csr *x509.Certificat return eeName } -func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, error) { +func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, []byte, error) { k8sLog := log.FromContext(ctx) csr, err := parseCSR(csrBytes) if err != nil { - return nil, err + return nil, nil, err } // Log the common metadata of the CSR @@ -206,7 +206,7 @@ func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, error) // If the CSR has a CommonName, use it as the EJBCA end entity name ejbcaEeName := s.getEndEntityName(ctx, csr) if ejbcaEeName == "" { - return nil, errors.New("failed to determine the EJBCA end entity name") + return nil, nil, errors.New("failed to determine the EJBCA end entity name") } k8sLog.Info(fmt.Sprintf("Using or Creating EJBCA End Entity called %q", ejbcaEeName)) @@ -237,13 +237,13 @@ func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, error) k8sLog.Error(err, detail) - return nil, fmt.Errorf(detail) + return nil, nil, fmt.Errorf(detail) } certAndChain, _, err := getCertificatesFromEjbcaObject(*certificateObject) if err != nil { k8sLog.Error(err, fmt.Sprintf("error getting certificate from EJBCA response: %s", err.Error())) - return nil, err + return nil, nil, err } k8sLog.Info(fmt.Sprintf("Successfully enrolled certificate with EJBCA")) @@ -425,20 +425,31 @@ func getCertificatesFromEjbcaObject(ejbcaCert ejbca.CertificateRestResponse) ([] // compileCertificatesToPemString takes a slice of x509 certificates and returns a string containing the certificates in PEM format // If an error occurred, the function logs the error and continues to parse the remaining objects. -func compileCertificatesToPemBytes(certificates []*x509.Certificate) ([]byte, error) { - var pemBuilder strings.Builder - - for _, certificate := range certificates { - err := pem.Encode(&pemBuilder, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certificate.Raw, - }) - if err != nil { - return make([]byte, 0, 0), err +func compileCertificatesToPemBytes(certificates []*x509.Certificate) ([]byte, []byte, error) { + var leaf strings.Builder + var chain strings.Builder + + for i, certificate := range certificates { + if i == 0 { + err := pem.Encode(&leaf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + }) + if err != nil { + return make([]byte, 0), make([]byte, 0), err + } + } else { + err := pem.Encode(&chain, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + }) + if err != nil { + return make([]byte, 0), make([]byte, 0), err + } } } - return []byte(pemBuilder.String()), nil + return []byte(leaf.String()), []byte(chain.String()), nil } func decodePEMBytes(buf []byte) ([]*pem.Block, *pem.Block) { diff --git a/internal/issuer/signer/signer_test.go b/internal/issuer/signer/signer_test.go index e258721..6cfa927 100644 --- a/internal/issuer/signer/signer_test.go +++ b/internal/issuer/signer/signer_test.go @@ -145,12 +145,13 @@ func TestEjbcaSignerFromIssuerAndSecretData(t *testing.T) { t.Fatal(err) } - signedCert, err := signer.Sign(context.Background(), csr) + signedCert, chain, err := signer.Sign(context.Background(), csr) if err != nil { - return + t.Fatal(err) } t.Log(fmt.Sprintf("Signed certificate: %s", string(signedCert))) + t.Log(fmt.Sprintf("Chain: %s", string(chain))) }) t.Run("With Annotations", func(t *testing.T) { From 8fcd8a5ff96582ae51bc598dab6c114545e1d9e3 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 5 Dec 2023 12:32:40 -0700 Subject: [PATCH 08/14] chore(secret): Add config to values.yaml to configure how reconciler searches for secret --- .../ejbca-cert-manager-issuer/README.md | 76 ++++++++++++------- .../templates/deployment.yaml | 3 + .../ejbca-cert-manager-issuer/values.yaml | 10 +++ docs/config_usage.md | 2 +- docs/install.md | 68 ++++++++--------- .../certificaterequest_controller.go | 10 ++- .../certificaterequest_controller_test.go | 13 ++-- internal/controllers/issuer_controller.go | 14 +++- .../controllers/issuer_controller_test.go | 11 +-- main.go | 44 +++++++---- 10 files changed, 154 insertions(+), 97 deletions(-) diff --git a/deploy/charts/ejbca-cert-manager-issuer/README.md b/deploy/charts/ejbca-cert-manager-issuer/README.md index d05c309..2644c79 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/README.md +++ b/deploy/charts/ejbca-cert-manager-issuer/README.md @@ -25,25 +25,44 @@ helm repo update ### Install Chart -```bash -helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer +```shell +helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace ejbca-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ + --set image.tag= + # --set image.pullPolicy=Never # Only required if using a local image ``` -Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following ejbca: -```bash +Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `secretConfig.useClusterRoleForSecretAccess` to configure the chart to use a cluster role for secret access, run the following command: + +```shell helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace ejbca-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ + --set image.tag= --set replicaCount=2 ``` -Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the `replicaCount` value, modify the `replicaCount` value in the `values.yaml` file: +Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the `secretConfig.useClusterRoleForSecretAccess` value to configure the chart to use a cluster role for secret access, modify the `secretConfig.useClusterRoleForSecretAccess` value in the `values.yaml` file by creating an override file: + ```yaml cat < override.yaml -replicaCount: 2 +image: + repository: /keyfactor/ejbca-cert-manager-issuer + pullPolicy: Never + tag: "latest" +secretConfig: + useClusterRoleForSecretAccess: true EOF ``` + Then, use the `-f` flag to specify the `values.yaml` file: -```bash + +```shell helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace command-issuer-system \ -f override.yaml ``` @@ -51,24 +70,25 @@ helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ The following table lists the configurable parameters of the `ejbca-cert-manager-issuer` chart and their default values. -| Parameter | Description | Default | -|-----------------------------------|-----------------------------------------------------|--------------------------------------------------------------| -| `replicaCount` | Number of replica ejbca-cert-manager-issuers to run | `1` | -| `image.repository` | Image repository | `m8rmclarenkf/ejbca-cert-manager-external-issuer-controller` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | -| `image.tag` | Image tag | `v1.3.1` | -| `imagePullSecrets` | Image pull secrets | `[]` | -| `nameOverride` | Name override | `""` | -| `fullnameOverride` | Full name override | `""` | -| `crd.create` | Specifies if CRDs will be created | `true` | -| `crd.annotations` | Annotations to add to the CRD | `{}` | -| `serviceAccount.create` | Specifies if a service account should be created | `true` | -| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | -| `serviceAccount.name` | Name of the service account to use | `""` (uses the fullname template if `create` is true) | -| `podAnnotations` | Annotations for the pod | `{}` | -| `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | -| `securityContext` | Security context for the pod | `{}` (with commented out options) | -| `secureMetrics.enabled` | Enable secure metrics via the Kube RBAC Proy | `false` | -| `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | -| `nodeSelector` | Node labels for pod assignment | `{}` | -| `tolerations` | Tolerations for pod assignment | `[]` | +| Parameter | Description | Default | +|----------------------------------------------|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `replicaCount` | Number of replica ejbca-cert-manager-issuers to run | `1` | +| `image.repository` | Image repository | `m8rmclarenkf/ejbca-cert-manager-external-issuer-controller` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag | `v1.3.1` | +| `imagePullSecrets` | Image pull secrets | `[]` | +| `nameOverride` | Name override | `""` | +| `fullnameOverride` | Full name override | `""` | +| `crd.create` | Specifies if CRDs will be created | `true` | +| `crd.annotations` | Annotations to add to the CRD | `{}` | +| `serviceAccount.create` | Specifies if a service account should be created | `true` | +| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | +| `serviceAccount.name` | Name of the service account to use | `""` (uses the fullname template if `create` is true) | +| `podAnnotations` | Annotations for the pod | `{}` | +| `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | +| `securityContext` | Security context for the pod | `{}` (with commented out options) | +| `secureMetrics.enabled` | Enable secure metrics via the Kube RBAC Proy | `false` | +| `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `tolerations` | Tolerations for pod assignment | `[]` | +| `secretConfig.useClusterRoleForSecretAccess` | Specifies if the ServiceAccount should be granted access to the Secret resource using a ClusterRole | `false` | \ No newline at end of file diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml index 2707d04..18284e9 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml @@ -55,6 +55,9 @@ spec: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 - --leader-elect + {{- if .Values.secretConfig.useClusterRoleForSecretAccess}} + - --secret-access-granted-at-cluster-level + {{- end}} command: - /manager image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" diff --git a/deploy/charts/ejbca-cert-manager-issuer/values.yaml b/deploy/charts/ejbca-cert-manager-issuer/values.yaml index 881005b..ba06a35 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/values.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/values.yaml @@ -18,6 +18,16 @@ fullnameOverride: "" secureMetrics: enabled: false +secretConfig: + # If true, when using Issuer resources, the credential secret must be created in the same namespace as the + # Issuer resource. This access is facilitated by granting the ServiceAccount [get, list, watch] for the secret + # API at the cluster level. + # + # If false, both Issuer and ClusterIssuer must reference a secret in the same namespace as the chart/reconciler. + # This access is facilitated by granting the ServiceAccount [get, list, watch] for the secret API only for the + # namespace the chart is deployed in. + useClusterRoleForSecretAccess: false + crd: # Specifies whether CRDs will be created create: true diff --git a/docs/config_usage.md b/docs/config_usage.md index 8194f54..cd28c0f 100644 --- a/docs/config_usage.md +++ b/docs/config_usage.md @@ -10,7 +10,7 @@ The cert-manager external issuer for Keyfactor EJBCA can be used to issue certificates from Keyfactor EJBCA using cert-manager. ### Authentication -Authentication to the EJBCA platform is done using a client certificate and key. The client certificate and key must be provided as a Kubernetes secret. +Authentication to the EJBCA platform is done using a client certificate and key. The client certificate and key must be provided as a Kubernetes secret. If the Helm chart was deployed with the `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag, the secret must be created in the same namespace as any Issuer resources deployed. Otherwise, the secret must be created in the same namespace as the controller. Create a K8s TLS secret containing the client certificate and key to authenticate with EJBCA: ```shell diff --git a/docs/install.md b/docs/install.md index 100c9de..8380361 100644 --- a/docs/install.md +++ b/docs/install.md @@ -53,55 +53,53 @@ The cert-manager external issuer for Keyfactor EJBCA can also be installed using 1. Add the Helm repository: - ```bash + ```shell helm repo add ejbca-issuer https://keyfactor.github.io/ejbca-cert-manager-issuer helm repo update ``` 2. Then, install the chart: - ```bash - helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ - --namespace ejbca-issuer-system \ - --create-namespace \ - --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ - --set image.tag= - # --set image.pullPolicy=Never # Only required if using a local image - ``` - - a. Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following command: - ```shell helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ --namespace ejbca-issuer-system \ --create-namespace \ --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ --set image.tag= - --set replicaCount=2 - ``` - - b. Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the - `replicaCount` value, modify the `replicaCount` value in the `values.yaml` file: - - ```yaml - cat < override.yaml - image: - repository: /keyfactor/ejbca-cert-manager-issuer - pullPolicy: Never - tag: "latest" - replicaCount: 2 - EOF + # --set image.pullPolicy=Never # Only required if using a local image ``` - Then, use the `-f` flag to specify the `values.yaml` file: - - ```yaml - helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ - -f override.yaml - ``` + 1. Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `secretConfig.useClusterRoleForSecretAccess` to configure the chart to use a cluster role for secret access, run the following command: + + ```shell + helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace ejbca-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/ejbca-cert-manager-issuer \ + --set image.tag= + --set replicaCount=2 + ``` + + 2. Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the `secretConfig.useClusterRoleForSecretAccess` value to configure the chart to use a cluster role for secret access, modify the `secretConfig.useClusterRoleForSecretAccess` value in the `values.yaml` file by creating an override file: + + ```yaml + cat < override.yaml + image: + repository: /keyfactor/ejbca-cert-manager-issuer + pullPolicy: Never + tag: "latest" + secretConfig: + useClusterRoleForSecretAccess: true + EOF + ``` + + Then, use the `-f` flag to specify the `values.yaml` file: + + ```shell + helm install ejbca-cert-manager-issuer ejbca-issuer/ejbca-cert-manager-issuer \ + --namespace command-issuer-system \ + -f override.yaml + ``` Next, complete the [Usage](config_usage.md) steps to configure the cert-manager external issuer for Keyfactor EJBCA. - - - diff --git a/internal/controllers/certificaterequest_controller.go b/internal/controllers/certificaterequest_controller.go index f48e760..060fd95 100644 --- a/internal/controllers/certificaterequest_controller.go +++ b/internal/controllers/certificaterequest_controller.go @@ -51,8 +51,9 @@ type CertificateRequestReconciler struct { SignerBuilder signer.EjbcaSignerBuilder ClusterResourceNamespace string - Clock clock.Clock - CheckApprovedCondition bool + Clock clock.Clock + CheckApprovedCondition bool + SecretAccessGrantedAtClusterLevel bool } // +kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests,verbs=get;list;watch @@ -177,6 +178,11 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } + // If SecretAccessGrantedAtClusterLevel is false, we always look for the Secret in the same namespace as the Issuer + if !r.SecretAccessGrantedAtClusterLevel { + secretNamespace = r.ClusterResourceNamespace + } + // Get the Issuer or ClusterIssuer if err := r.Get(ctx, issuerName, issuer); err != nil { return ctrl.Result{}, fmt.Errorf("%w: %v", errGetIssuer, err) diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controllers/certificaterequest_controller_test.go index 1687556..56c1cd6 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controllers/certificaterequest_controller_test.go @@ -605,12 +605,13 @@ func TestCertificateRequestReconcile(t *testing.T) { WithObjects(tc.objects...). Build() controller := CertificateRequestReconciler{ - Client: fakeClient, - Scheme: scheme, - ClusterResourceNamespace: tc.clusterResourceNamespace, - SignerBuilder: tc.Builder, - CheckApprovedCondition: true, - Clock: fixedClock, + Client: fakeClient, + Scheme: scheme, + ClusterResourceNamespace: tc.clusterResourceNamespace, + SignerBuilder: tc.Builder, + CheckApprovedCondition: true, + Clock: fixedClock, + SecretAccessGrantedAtClusterLevel: true, } result, err := controller.Reconcile( ctrl.LoggerInto(context.TODO(), logrtesting.NewTestLogger(t)), diff --git a/internal/controllers/issuer_controller.go b/internal/controllers/issuer_controller.go index de64cd6..e5dab0c 100644 --- a/internal/controllers/issuer_controller.go +++ b/internal/controllers/issuer_controller.go @@ -48,10 +48,11 @@ var ( // IssuerReconciler reconciles a Issuer object type IssuerReconciler struct { client.Client - Kind string - Scheme *runtime.Scheme - ClusterResourceNamespace string - HealthCheckerBuilder signer.HealthCheckerBuilder + Kind string + Scheme *runtime.Scheme + ClusterResourceNamespace string + HealthCheckerBuilder signer.HealthCheckerBuilder + SecretAccessGrantedAtClusterLevel bool } //+kubebuilder:rbac:groups=ejbca-issuer.keyfactor.com,resources=issuers;clusterissuers,verbs=get;list;watch @@ -124,6 +125,11 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res return ctrl.Result{}, nil } + // If SecretAccessGrantedAtClusterLevel is false, we always look for the Secret in the same namespace as the Issuer + if !r.SecretAccessGrantedAtClusterLevel { + secretName.Namespace = r.ClusterResourceNamespace + } + var authSecret corev1.Secret if err := r.Get(ctx, secretName, &authSecret); err != nil { return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetAuthSecret, secretName, err) diff --git a/internal/controllers/issuer_controller_test.go b/internal/controllers/issuer_controller_test.go index 30ee66d..9c95b9d 100644 --- a/internal/controllers/issuer_controller_test.go +++ b/internal/controllers/issuer_controller_test.go @@ -251,11 +251,12 @@ func TestIssuerReconcile(t *testing.T) { tc.kind = "Issuer" } controller := IssuerReconciler{ - Kind: tc.kind, - Client: fakeClient, - Scheme: scheme, - HealthCheckerBuilder: tc.healthCheckerBuilder, - ClusterResourceNamespace: tc.clusterResourceNamespace, + Kind: tc.kind, + Client: fakeClient, + Scheme: scheme, + HealthCheckerBuilder: tc.healthCheckerBuilder, + ClusterResourceNamespace: tc.clusterResourceNamespace, + SecretAccessGrantedAtClusterLevel: true, } result, err := controller.Reconcile( ctrl.LoggerInto(context.TODO(), logrtesting.NewTestLogger(t)), diff --git a/main.go b/main.go index 3518f78..a0401e4 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,7 @@ func main() { var clusterResourceNamespace string var printVersion bool var disableApprovedCheck bool + var secretAccessGrantedAtClusterLevel bool flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -74,6 +75,8 @@ func main() { flag.BoolVar(&printVersion, "version", false, "Print version to stdout and exit") flag.BoolVar(&disableApprovedCheck, "disable-approved-check", false, "Disables waiting for CertificateRequests to have an approved condition before signing.") + flag.BoolVar(&secretAccessGrantedAtClusterLevel, "secret-access-granted-at-cluster-level", false, + "Set this flag to true if the secret access is granted at cluster level. This will allow the controller to access secrets in any namespace. ") opts := zap.Options{ Development: true, @@ -96,6 +99,12 @@ func main() { } } + if secretAccessGrantedAtClusterLevel { + setupLog.Info("expecting secret access at cluster level") + } else { + setupLog.Info(fmt.Sprintf("expecting secret access at namespace level (%s)", clusterResourceNamespace)) + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, @@ -121,32 +130,35 @@ func main() { } if err = (&controllers.IssuerReconciler{ - Kind: "Issuer", - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ClusterResourceNamespace: clusterResourceNamespace, - HealthCheckerBuilder: signer.EjbcaHealthCheckerFromIssuerAndSecretData, + Kind: "Issuer", + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ClusterResourceNamespace: clusterResourceNamespace, + HealthCheckerBuilder: signer.EjbcaHealthCheckerFromIssuerAndSecretData, + SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Issuer") os.Exit(1) } if err = (&controllers.IssuerReconciler{ - Kind: "ClusterIssuer", - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ClusterResourceNamespace: clusterResourceNamespace, - HealthCheckerBuilder: signer.EjbcaHealthCheckerFromIssuerAndSecretData, + Kind: "ClusterIssuer", + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ClusterResourceNamespace: clusterResourceNamespace, + HealthCheckerBuilder: signer.EjbcaHealthCheckerFromIssuerAndSecretData, + SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterIssuer") os.Exit(1) } if err = (&controllers.CertificateRequestReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ClusterResourceNamespace: clusterResourceNamespace, - SignerBuilder: signer.EjbcaSignerFromIssuerAndSecretData, - CheckApprovedCondition: !disableApprovedCheck, - Clock: clock.RealClock{}, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ClusterResourceNamespace: clusterResourceNamespace, + SignerBuilder: signer.EjbcaSignerFromIssuerAndSecretData, + CheckApprovedCondition: !disableApprovedCheck, + Clock: clock.RealClock{}, + SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CertificateRequest") os.Exit(1) From 91fb0f1d446607a56394b32bfaa449a5cb26bfc4 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 5 Dec 2023 12:35:31 -0700 Subject: [PATCH 09/14] chore(lint): Conform code to Go 1.19 standard --- internal/controllers/certificaterequest_controller_test.go | 7 ++----- internal/issuer/signer/signer.go | 6 +++--- internal/issuer/signer/signer_test.go | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controllers/certificaterequest_controller_test.go index 56c1cd6..c928699 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controllers/certificaterequest_controller_test.go @@ -19,9 +19,6 @@ package controllers import ( "context" "errors" - "testing" - "time" - cmutil "github.com/cert-manager/cert-manager/pkg/api/util" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" @@ -40,14 +37,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "testing" ejbcaissuer "github.com/Keyfactor/ejbca-issuer/api/v1alpha1" "github.com/Keyfactor/ejbca-issuer/internal/issuer/signer" ) var ( - fixedClockStart = time.Date(2021, time.January, 1, 1, 0, 0, 0, time.UTC) - fixedClock = clock.RealClock{} + fixedClock = clock.RealClock{} ) type fakeSigner struct { diff --git a/internal/issuer/signer/signer.go b/internal/issuer/signer/signer.go index 6fdb509..0782fbd 100644 --- a/internal/issuer/signer/signer.go +++ b/internal/issuer/signer/signer.go @@ -227,7 +227,7 @@ func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, []byte // Enroll certificate certificateObject, _, err := s.client.V1CertificateApi.EnrollPkcs10Certificate(context.Background()).EnrollCertificateRestRequest(enroll).Execute() if err != nil { - detail := fmt.Sprintf("error enrolling certificate with EJBCA. verify that the certificate profile name, end entity profile name, and certificate authority name are appropriate for the certificate request.") + detail := "error enrolling certificate with EJBCA. verify that the certificate profile name, end entity profile name, and certificate authority name are appropriate for the certificate request." var bodyError *ejbca.GenericOpenAPIError ok := errors.As(err, &bodyError) @@ -246,7 +246,7 @@ func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, []byte return nil, nil, err } - k8sLog.Info(fmt.Sprintf("Successfully enrolled certificate with EJBCA")) + k8sLog.Info("Successfully enrolled certificate with EJBCA") // Return the certificate and chain in PEM format return compileCertificatesToPemBytes(certAndChain) @@ -323,7 +323,7 @@ func createClientFromSecretMap(ctx context.Context, hostname string, clientCertS ejbcaConfig.SetClientCertificate(&tlsCert) // If the CA certificate is provided, add it to the EJBCA configuration - if caCertSecretData != nil && len(caCertSecretData) > 0 { + if len(caCertSecretData) > 0 { // There is no requirement that the CA certificate is stored under a specific key in the secret, so we can just iterate over the map var caCertBytes []byte for _, caCertBytes = range caCertSecretData { diff --git a/internal/issuer/signer/signer_test.go b/internal/issuer/signer/signer_test.go index 6cfa927..26434e2 100644 --- a/internal/issuer/signer/signer_test.go +++ b/internal/issuer/signer/signer_test.go @@ -150,8 +150,8 @@ func TestEjbcaSignerFromIssuerAndSecretData(t *testing.T) { t.Fatal(err) } - t.Log(fmt.Sprintf("Signed certificate: %s", string(signedCert))) - t.Log(fmt.Sprintf("Chain: %s", string(chain))) + t.Logf("Signed certificate: %s", string(signedCert)) + t.Logf("Chain: %s", string(chain)) }) t.Run("With Annotations", func(t *testing.T) { @@ -449,7 +449,7 @@ func parseSubjectDN(subject string, randomizeCn bool) (pkix.Name, error) { name.OrganizationalUnit = []string{value} case "CN": if randomizeCn { - value = fmt.Sprintf("%s-%s", value, generateRandomString(5)) + name.CommonName = fmt.Sprintf("%s-%s", value, generateRandomString(5)) } else { name.CommonName = value } From adab123717902dd278473460b2824e7541ebc276 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 5 Dec 2023 13:39:06 -0700 Subject: [PATCH 10/14] chore(helm): Add secret role and deprovision cluster-wide access to secret API --- .../templates/clusterrole.yaml | 8 ----- .../templates/deployment.yaml | 2 +- .../templates/secretrole.yaml | 30 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 deploy/charts/ejbca-cert-manager-issuer/templates/secretrole.yaml diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml index f8bcd69..959997f 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/clusterrole.yaml @@ -5,14 +5,6 @@ metadata: {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} name: {{ include "ejbca-cert-manager-issuer.name" . }}-manager-role rules: - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch - apiGroups: - cert-manager.io resources: diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml index 18284e9..4bfffac 100644 --- a/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/deployment.yaml @@ -50,7 +50,7 @@ spec: capabilities: drop: - ALL - {{- end }}} + {{- end }} - args: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 diff --git a/deploy/charts/ejbca-cert-manager-issuer/templates/secretrole.yaml b/deploy/charts/ejbca-cert-manager-issuer/templates/secretrole.yaml new file mode 100644 index 0000000..aa6a7b2 --- /dev/null +++ b/deploy/charts/ejbca-cert-manager-issuer/templates/secretrole.yaml @@ -0,0 +1,30 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRole{{ else }}Role{{ end }} +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-secret-reader-role +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRoleBinding{{ else }}RoleBinding{{ end }} +metadata: + labels: + {{- include "ejbca-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-secret-reader-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRole{{ else }}Role{{ end }} + name: {{ include "ejbca-cert-manager-issuer.name" . }}-secret-reader-role +subjects: + - kind: ServiceAccount + name: {{ include "ejbca-cert-manager-issuer.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file From 7276d00000c2ead1b30e4647cf38610614660315 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Wed, 6 Dec 2023 15:58:01 -0700 Subject: [PATCH 11/14] fix(config): Implement K8s client-go in secondary API client for secret/config retrieval --- go.mod | 2 +- go.sum | 6 - .../certificaterequest_controller.go | 12 +- .../certificaterequest_controller_test.go | 1 + .../controllers/fake_configclient_test.go | 57 +++++++ internal/controllers/issuer_controller.go | 10 +- .../controllers/issuer_controller_test.go | 3 +- internal/issuer/util/configclient.go | 152 ++++++++++++++++++ internal/issuer/util/configclient_test.go | 88 ++++++++++ main.go | 11 ++ 10 files changed, 327 insertions(+), 15 deletions(-) create mode 100644 internal/controllers/fake_configclient_test.go create mode 100644 internal/issuer/util/configclient.go create mode 100644 internal/issuer/util/configclient_test.go diff --git a/go.mod b/go.mod index 32fe7cc..227671b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( k8s.io/api v0.26.1 k8s.io/apimachinery v0.26.3 k8s.io/client-go v0.26.1 + k8s.io/klog/v2 v2.80.1 k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 sigs.k8s.io/controller-runtime v0.14.5 ) @@ -72,7 +73,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.26.1 // indirect k8s.io/component-base v0.26.1 // indirect - k8s.io/klog/v2 v2.80.1 // indirect k8s.io/kube-aggregator v0.26.0 // indirect k8s.io/kube-openapi v0.0.0-20221207184640-f3cff1453715 // indirect sigs.k8s.io/gateway-api v0.6.0 // indirect diff --git a/go.sum b/go.sum index e2697f3..c9f526d 100644 --- a/go.sum +++ b/go.sum @@ -282,19 +282,13 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -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= diff --git a/internal/controllers/certificaterequest_controller.go b/internal/controllers/certificaterequest_controller.go index 060fd95..2857d3f 100644 --- a/internal/controllers/certificaterequest_controller.go +++ b/internal/controllers/certificaterequest_controller.go @@ -47,6 +47,7 @@ var ( type CertificateRequestReconciler struct { client.Client + ConfigClient issuerutil.ConfigClient Scheme *runtime.Scheme SignerBuilder signer.EjbcaSignerBuilder ClusterResourceNamespace string @@ -143,8 +144,8 @@ 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") - issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initialising") + log.Info("Initializing Ready condition") + issuerutil.SetCertificateRequestReadyCondition(ctx, &certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initializing") return ctrl.Result{}, nil } @@ -199,6 +200,9 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, errIssuerNotReady } + // Set the context on the config client + r.ConfigClient.SetContext(ctx) + // Retrieve the auth secret authSecretName := types.NamespacedName{ Name: issuerSpec.EjbcaSecretName, @@ -206,7 +210,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R } var authSecret corev1.Secret - if err := r.Get(ctx, authSecretName, &authSecret); err != nil { + if err := r.ConfigClient.GetSecret(authSecretName, &authSecret); err != nil { return ctrl.Result{}, fmt.Errorf("%w, authSecret name: %s, reason: %v", errGetAuthSecret, authSecretName, err) } @@ -219,7 +223,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R var caSecret corev1.Secret if issuerSpec.CaBundleSecretName != "" { // If the CA secret name is not specified, we will not attempt to retrieve it - err = r.Get(ctx, caSecretName, &caSecret) + err = r.ConfigClient.GetSecret(caSecretName, &caSecret) if err != nil { return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetCaSecret, caSecretName, err) } diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controllers/certificaterequest_controller_test.go index c928699..ebea901 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controllers/certificaterequest_controller_test.go @@ -603,6 +603,7 @@ func TestCertificateRequestReconcile(t *testing.T) { Build() controller := CertificateRequestReconciler{ Client: fakeClient, + ConfigClient: NewFakeConfigClient(fakeClient), Scheme: scheme, ClusterResourceNamespace: tc.clusterResourceNamespace, SignerBuilder: tc.Builder, diff --git a/internal/controllers/fake_configclient_test.go b/internal/controllers/fake_configclient_test.go new file mode 100644 index 0000000..a0cb3e0 --- /dev/null +++ b/internal/controllers/fake_configclient_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2023 The Keyfactor Command Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "github.com/Keyfactor/ejbca-issuer/internal/issuer/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// FakeConfigClient is a fake implementation of the util.ConfigClient interface +// It forwards requests destined for the Kubernetes API server implemented by +// the util.ConfigClient interface to a fake Kubernetes API server implemented +// by the client.Client interface. + +// Force the compiler to check that FakeConfigClient implements the util.ConfigClient interface +var _ util.ConfigClient = &FakeConfigClient{} + +type FakeConfigClient struct { + client client.Client + ctx context.Context +} + +// NewFakeConfigClient uses the +func NewFakeConfigClient(fakeControllerRuntimeClient client.Client) util.ConfigClient { + return &FakeConfigClient{ + client: fakeControllerRuntimeClient, + } +} + +func (f FakeConfigClient) SetContext(ctx context.Context) { + f.ctx = ctx +} + +func (f FakeConfigClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { + return f.client.Get(f.ctx, name, out) +} + +func (f FakeConfigClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { + return f.client.Get(f.ctx, name, out) +} diff --git a/internal/controllers/issuer_controller.go b/internal/controllers/issuer_controller.go index e5dab0c..d77cb2f 100644 --- a/internal/controllers/issuer_controller.go +++ b/internal/controllers/issuer_controller.go @@ -48,6 +48,7 @@ var ( // IssuerReconciler reconciles a Issuer object type IssuerReconciler struct { client.Client + ConfigClient issuerutil.ConfigClient Kind string Scheme *runtime.Scheme ClusterResourceNamespace string @@ -73,7 +74,7 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res issuer, err := r.newIssuer() if err != nil { - log.Error(err, "Unrecognised issuer type") + log.Error(err, "Unrecognized issuer type") return ctrl.Result{}, nil } if err := r.Get(ctx, req.NamespacedName, issuer); err != nil { @@ -130,8 +131,11 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res secretName.Namespace = r.ClusterResourceNamespace } + // Set the context on the config client + r.ConfigClient.SetContext(ctx) + var authSecret corev1.Secret - if err := r.Get(ctx, secretName, &authSecret); err != nil { + if err := r.ConfigClient.GetSecret(secretName, &authSecret); err != nil { return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetAuthSecret, secretName, err) } @@ -144,7 +148,7 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res var caSecret corev1.Secret if issuerSpec.CaBundleSecretName != "" { // If the CA secret name is not specified, we will not attempt to retrieve it - err = r.Get(ctx, caSecretName, &caSecret) + err = r.ConfigClient.GetSecret(caSecretName, &caSecret) if err != nil { return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetCaSecret, caSecretName, err) } diff --git a/internal/controllers/issuer_controller_test.go b/internal/controllers/issuer_controller_test.go index 9c95b9d..2728913 100644 --- a/internal/controllers/issuer_controller_test.go +++ b/internal/controllers/issuer_controller_test.go @@ -126,7 +126,7 @@ func TestIssuerReconcile(t *testing.T) { expectedReadyConditionStatus: ejbcaissuer.ConditionTrue, expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, }, - "issuer-kind-unrecognised": { + "issuer-kind-Unrecognized": { kind: "UnrecognizedType", name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, }, @@ -253,6 +253,7 @@ func TestIssuerReconcile(t *testing.T) { controller := IssuerReconciler{ Kind: tc.kind, Client: fakeClient, + ConfigClient: NewFakeConfigClient(fakeClient), Scheme: scheme, HealthCheckerBuilder: tc.healthCheckerBuilder, ClusterResourceNamespace: tc.clusterResourceNamespace, diff --git a/internal/issuer/util/configclient.go b/internal/issuer/util/configclient.go new file mode 100644 index 0000000..f8f9944 --- /dev/null +++ b/internal/issuer/util/configclient.go @@ -0,0 +1,152 @@ +/* +Copyright 2023 The Keyfactor Command Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "fmt" + authv1 "k8s.io/api/authorization/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" +) + +type ConfigClient interface { + SetContext(ctx context.Context) + GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error + GetSecret(name types.NamespacedName, out *corev1.Secret) error +} + +type configClient struct { + ctx context.Context + logger klog.Logger + client kubernetes.Interface + accessCache map[string]bool + + verifyAccessFunc func(apiResource string, resource types.NamespacedName) error +} + +func NewConfigClient(ctx context.Context) (ConfigClient, error) { + config := ctrl.GetConfigOrDie() + + // Create the clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create clientset: %w", err) + } + + client := &configClient{ + client: clientset, + accessCache: make(map[string]bool), + ctx: ctx, + logger: klog.NewKlogr(), + } + + client.verifyAccessFunc = client.verifyAccessToResource + + return client, nil +} + +func (c *configClient) SetContext(ctx context.Context) { + c.ctx = ctx + c.logger = klog.FromContext(ctx) +} + +func (c *configClient) verifyAccessToResource(apiResource string, resource types.NamespacedName) error { + verbs := []string{"get", "list", "watch"} + + for _, verb := range verbs { + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Name: resource.Name, + Namespace: resource.Namespace, + + Group: "", + Resource: apiResource, + Verb: verb, + }, + }, + } + + ssar, err := c.client.AuthorizationV1().SelfSubjectAccessReviews().Create(c.ctx, ssar, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create SelfSubjectAccessReview to check access to %s for verb %q: %w", apiResource, verb, err) + } + + if !ssar.Status.Allowed { + return fmt.Errorf("client does not have access to %s called %q for verb %q, reason: %v", apiResource, resource.String(), verb, ssar.Status.String()) + } + } + + c.logger.Info(fmt.Sprintf("Client has access to %s called %q", apiResource, resource.String())) + + return nil +} + +func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { + if c == nil { + return fmt.Errorf("config client is nil") + } + + // Check if the client has access to the configmap resource + if ok, _ := c.accessCache[name.String()]; !ok { + err := c.verifyAccessFunc("configmaps", name) + if err != nil { + return err + } + c.accessCache[name.String()] = true + } + + // Get the configmap + configmap, err := c.client.CoreV1().ConfigMaps(name.Namespace).Get(c.ctx, name.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + // Copy the configmap into the out parameter + configmap.DeepCopyInto(out) + return nil +} + +func (c *configClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { + if c == nil { + return fmt.Errorf("config client is nil") + } + + // Check if the client has access to the secret resource + if ok, _ := c.accessCache[name.String()]; !ok { + err := c.verifyAccessFunc("secrets", name) + if err != nil { + return err + } + c.accessCache[name.String()] = true + } + + // Get the secret + secret, err := c.client.CoreV1().Secrets(name.Namespace).Get(c.ctx, name.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + // Copy the secret into the out parameter + secret.DeepCopyInto(out) + return nil +} diff --git a/internal/issuer/util/configclient_test.go b/internal/issuer/util/configclient_test.go new file mode 100644 index 0000000..e57f922 --- /dev/null +++ b/internal/issuer/util/configclient_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2023 The Keyfactor Command Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + logrtesting "github.com/go-logr/logr/testr" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" + ctrl "sigs.k8s.io/controller-runtime" + "testing" +) + +func TestConfigClient(t *testing.T) { + var err error + + // Define namespaced names for test objects + configMapName := types.NamespacedName{Name: "test-configmap", Namespace: "default"} + secretName := types.NamespacedName{Name: "test-secret", Namespace: "default"} + + // Create and inject fake ConfigMap + testConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: configMapName.Name, Namespace: configMapName.Namespace}, + } + + // Create and inject fake Secret + testSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName.Name, Namespace: secretName.Namespace}, + } + + // Create a fake clientset with the test objects + clientset := fake.NewSimpleClientset([]runtime.Object{ + testConfigMap, + testSecret, + }...) + + // We can't test NewConfigClient unless we can mock ctrl.GetConfigOrDie() and kubernetes.NewForConfig() + // So we'll just test the methods that use the clientset + + // Create a ConfigClient + client := &configClient{ + client: clientset, + accessCache: make(map[string]bool), + } + + // The fake client doesn't implement authorization.k8s.io/v1 SelfSubjectAccessReview + // So we'll mock the verifyAccessFunc + client.verifyAccessFunc = func(apiResource string, resource types.NamespacedName) error { + return nil + } + + // Setup logging for test environment by setting the context + client.SetContext(ctrl.LoggerInto(context.TODO(), logrtesting.New(t))) + + t.Run("GetConfigMap", func(t *testing.T) { + // Test GetConfigMap + var out corev1.ConfigMap + err = client.GetConfigMap(configMapName, &out) + assert.NoError(t, err) + assert.Equal(t, testConfigMap, &out) + }) + + t.Run("GetSecret", func(t *testing.T) { + // Test GetSecret + var out corev1.Secret + err = client.GetSecret(secretName, &out) + assert.NoError(t, err) + assert.Equal(t, testSecret, &out) + }) +} diff --git a/main.go b/main.go index a0401e4..e830a0d 100644 --- a/main.go +++ b/main.go @@ -17,11 +17,13 @@ limitations under the License. package main import ( + "context" "errors" "flag" "fmt" "github.com/Keyfactor/ejbca-issuer/internal/controllers" signer "github.com/Keyfactor/ejbca-issuer/internal/issuer/signer" + "github.com/Keyfactor/ejbca-issuer/internal/issuer/util" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "k8s.io/utils/clock" "os" @@ -105,6 +107,12 @@ func main() { setupLog.Info(fmt.Sprintf("expecting secret access at namespace level (%s)", clusterResourceNamespace)) } + ctx := context.Background() + configClient, err := util.NewConfigClient(ctx) + if err != nil { + setupLog.Error(err, "error creating config client") + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, @@ -132,6 +140,7 @@ func main() { if err = (&controllers.IssuerReconciler{ Kind: "Issuer", Client: mgr.GetClient(), + ConfigClient: configClient, Scheme: mgr.GetScheme(), ClusterResourceNamespace: clusterResourceNamespace, HealthCheckerBuilder: signer.EjbcaHealthCheckerFromIssuerAndSecretData, @@ -143,6 +152,7 @@ func main() { if err = (&controllers.IssuerReconciler{ Kind: "ClusterIssuer", Client: mgr.GetClient(), + ConfigClient: configClient, Scheme: mgr.GetScheme(), ClusterResourceNamespace: clusterResourceNamespace, HealthCheckerBuilder: signer.EjbcaHealthCheckerFromIssuerAndSecretData, @@ -153,6 +163,7 @@ func main() { } if err = (&controllers.CertificateRequestReconciler{ Client: mgr.GetClient(), + ConfigClient: configClient, Scheme: mgr.GetScheme(), ClusterResourceNamespace: clusterResourceNamespace, SignerBuilder: signer.EjbcaSignerFromIssuerAndSecretData, From a688ef862cd4eb1227c1f5f7806f2a263bbe8d3e Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Wed, 6 Dec 2023 16:06:50 -0700 Subject: [PATCH 12/14] fix(lint): Run linters --- internal/controllers/fake_configclient_test.go | 6 +++--- internal/issuer/util/configclient.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/controllers/fake_configclient_test.go b/internal/controllers/fake_configclient_test.go index a0cb3e0..bcb128d 100644 --- a/internal/controllers/fake_configclient_test.go +++ b/internal/controllers/fake_configclient_test.go @@ -44,14 +44,14 @@ func NewFakeConfigClient(fakeControllerRuntimeClient client.Client) util.ConfigC } } -func (f FakeConfigClient) SetContext(ctx context.Context) { +func (f *FakeConfigClient) SetContext(ctx context.Context) { f.ctx = ctx } -func (f FakeConfigClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { +func (f *FakeConfigClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { return f.client.Get(f.ctx, name, out) } -func (f FakeConfigClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { +func (f *FakeConfigClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { return f.client.Get(f.ctx, name, out) } diff --git a/internal/issuer/util/configclient.go b/internal/issuer/util/configclient.go index f8f9944..990e9a1 100644 --- a/internal/issuer/util/configclient.go +++ b/internal/issuer/util/configclient.go @@ -107,7 +107,7 @@ func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.Confi } // Check if the client has access to the configmap resource - if ok, _ := c.accessCache[name.String()]; !ok { + if _, ok := c.accessCache[name.String()]; !ok { err := c.verifyAccessFunc("configmaps", name) if err != nil { return err @@ -132,7 +132,7 @@ func (c *configClient) GetSecret(name types.NamespacedName, out *corev1.Secret) } // Check if the client has access to the secret resource - if ok, _ := c.accessCache[name.String()]; !ok { + if _, ok := c.accessCache[name.String()]; !ok { err := c.verifyAccessFunc("secrets", name) if err != nil { return err From a2f5acb68d2343b6c54a97ff985768e656292774 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Wed, 6 Dec 2023 17:15:59 -0700 Subject: [PATCH 13/14] chore(changelog): Update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19e0112 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# v1.3.1 + +## Features +* feat(controller): Implement Kubernetes `client-go` REST client for Secret/ConfigMap retrieval to bypass `controller-runtime` caching system. This enables the reconciler to retrieve Secret and ConfigMap resources at the namespace scope with only namespace-level permissions. +* feat(ci): Add GitHub Actions workflows to run unit tests and release container images when appropriate +* feat(helm): Create Helm chart to deploy the controller to a Kubernetes or OpenShift cluster + +## Fixes +* fix(controller): Add logic to read secret from reconciler namespace or Issuer namespace depending on Helm configuration. \ No newline at end of file From 00742cd423e78e1ed4984afd720face66afacf14 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Thu, 14 Dec 2023 11:47:54 -0700 Subject: [PATCH 14/14] chore(comments): Write function comments and update license header --- .../certificaterequest_controller.go | 6 +++++- .../certificaterequest_controller_test.go | 2 +- internal/controllers/fake_configclient_test.go | 2 +- internal/controllers/issuer_controller.go | 4 ++++ internal/controllers/issuer_controller_test.go | 2 +- internal/issuer/signer/signer.go | 10 ++++++++++ internal/issuer/signer/signer_test.go | 2 +- internal/issuer/util/configclient.go | 14 +++++++++++++- internal/issuer/util/configclient_test.go | 2 +- internal/issuer/util/util.go | 17 ++++++----------- main.go | 2 +- 11 files changed, 44 insertions(+), 19 deletions(-) diff --git a/internal/controllers/certificaterequest_controller.go b/internal/controllers/certificaterequest_controller.go index 2857d3f..df059fd 100644 --- a/internal/controllers/certificaterequest_controller.go +++ b/internal/controllers/certificaterequest_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Keyfactor. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -61,6 +61,8 @@ type CertificateRequestReconciler struct { // +kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +// Reconcile attempts to sign a CertificateRequest given the configuration provided and a configured +// EJBCA signer instance. func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { log := ctrl.LoggerFrom(ctx) @@ -245,6 +247,8 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } +// SetupWithManager registers the CertificateRequestReconciler with the controller manager. +// It configures controller-runtime to reconcile cert-manager CertificateRequests in the cluster. func (r *CertificateRequestReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cmapi.CertificateRequest{}). diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controllers/certificaterequest_controller_test.go index ebea901..53ba3b5 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controllers/certificaterequest_controller_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Keyfactor Command Authors. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/controllers/fake_configclient_test.go b/internal/controllers/fake_configclient_test.go index bcb128d..9a33ef0 100644 --- a/internal/controllers/fake_configclient_test.go +++ b/internal/controllers/fake_configclient_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Keyfactor Command Authors. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/controllers/issuer_controller.go b/internal/controllers/issuer_controller.go index d77cb2f..8287035 100644 --- a/internal/controllers/issuer_controller.go +++ b/internal/controllers/issuer_controller.go @@ -60,6 +60,7 @@ type IssuerReconciler struct { //+kubebuilder:rbac:groups=ejbca-issuer.keyfactor.com,resources=issuers/status;clusterissuers/status,verbs=get;update;patch //+kubebuilder:rbac:groups=ejbca-issuer.keyfactor.com,resources=issuers/finalizers,verbs=update +// newIssuer returns a new Issuer or ClusterIssuer object func (r *IssuerReconciler) newIssuer() (client.Object, error) { issuerGVK := ejbcaissuer.GroupVersion.WithKind(r.Kind) ro, err := r.Scheme.New(issuerGVK) @@ -69,6 +70,7 @@ func (r *IssuerReconciler) newIssuer() (client.Object, error) { return ro.(client.Object), nil } +// Reconcile reconciles and updates the status of an Issuer or ClusterIssuer object func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { log := ctrl.LoggerFrom(ctx) @@ -167,6 +169,8 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res return ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, nil } +// SetupWithManager registers the IssuerReconciler with the controller manager. +// It configures controller-runtime to reconcile Keyfactor EJBCA Issuers/ClusterIssuers in the cluster. func (r *IssuerReconciler) SetupWithManager(mgr ctrl.Manager) error { issuerType, err := r.newIssuer() if err != nil { diff --git a/internal/controllers/issuer_controller_test.go b/internal/controllers/issuer_controller_test.go index 2728913..d34d0f2 100644 --- a/internal/controllers/issuer_controller_test.go +++ b/internal/controllers/issuer_controller_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Keyfactor Command Authors. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/issuer/signer/signer.go b/internal/issuer/signer/signer.go index 0782fbd..5736d74 100644 --- a/internal/issuer/signer/signer.go +++ b/internal/issuer/signer/signer.go @@ -52,6 +52,7 @@ type Signer interface { Sign(context.Context, []byte) ([]byte, []byte, error) } +// EjbcaHealthCheckerFromIssuerAndSecretData creates a HealthChecker from an IssuerSpec and a map of secret data func EjbcaHealthCheckerFromIssuerAndSecretData(ctx context.Context, spec *ejbcaissuer.IssuerSpec, clientCertSecretData map[string][]byte, caCertSecretData map[string][]byte) (HealthChecker, error) { signer := ejbcaSigner{} @@ -65,6 +66,7 @@ func EjbcaHealthCheckerFromIssuerAndSecretData(ctx context.Context, spec *ejbcai return &signer, nil } +// ejbcaSignerFromIssuerAndSecretData creates a Signer from an IssuerSpec and a map of secret data func ejbcaSignerFromIssuerAndSecretData(ctx context.Context, spec *ejbcaissuer.IssuerSpec, annotations map[string]string, clientCertSecretData map[string][]byte, caCertSecretData map[string][]byte) (*ejbcaSigner, error) { signLog := log.FromContext(ctx) signer := ejbcaSigner{} @@ -116,10 +118,12 @@ func ejbcaSignerFromIssuerAndSecretData(ctx context.Context, spec *ejbcaissuer.I return &signer, nil } +// EjbcaSignerFromIssuerAndSecretData is a wrapper around ejbcaSignerFromIssuerAndSecretData that returns a Signer interface func EjbcaSignerFromIssuerAndSecretData(ctx context.Context, spec *ejbcaissuer.IssuerSpec, annotations map[string]string, clientCertSecretData map[string][]byte, caCertSecretData map[string][]byte) (Signer, error) { return ejbcaSignerFromIssuerAndSecretData(ctx, spec, annotations, clientCertSecretData, caCertSecretData) } +// Check checks the status of the EJBCA API func (s *ejbcaSigner) Check() error { // Check EJBCA API status _, _, err := s.client.V1CertificateApi.Status2(context.Background()).Execute() @@ -130,6 +134,7 @@ func (s *ejbcaSigner) Check() error { return nil } +// getEndEntityName determines the end entity name to use for the EJBCA request func (s *ejbcaSigner) getEndEntityName(ctx context.Context, csr *x509.CertificateRequest) string { eeLog := log.FromContext(ctx) eeName := "" @@ -192,6 +197,7 @@ func (s *ejbcaSigner) getEndEntityName(ctx context.Context, csr *x509.Certificat return eeName } +// Sign signs a CSR with EJBCA func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, []byte, error) { k8sLog := log.FromContext(ctx) @@ -252,6 +258,7 @@ func (s *ejbcaSigner) Sign(ctx context.Context, csrBytes []byte) ([]byte, []byte return compileCertificatesToPemBytes(certAndChain) } +// createClientFromSecretMap creates an EJBCA API client from a map of secret data func createClientFromSecretMap(ctx context.Context, hostname string, clientCertSecretData map[string][]byte, caCertSecretData map[string][]byte) (*ejbca.APIClient, error) { var err error k8sLog := log.FromContext(ctx) @@ -452,6 +459,7 @@ func compileCertificatesToPemBytes(certificates []*x509.Certificate) ([]byte, [] return []byte(leaf.String()), []byte(chain.String()), nil } +// decodePEMBytes takes a byte array containing PEM encoded data and returns a slice of PEM blocks and a private key PEM block func decodePEMBytes(buf []byte) ([]*pem.Block, *pem.Block) { var privKey *pem.Block var certificates []*pem.Block @@ -469,10 +477,12 @@ func decodePEMBytes(buf []byte) ([]*pem.Block, *pem.Block) { return certificates, privKey } +// ptr is a helper function that returns a pointer to the provided value func ptr[T any](v T) *T { return &v } +// generateRandomString generates a random string of length n func generateRandomString(length int) string { rand.Seed(time.Now().UnixNano()) letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") diff --git a/internal/issuer/signer/signer_test.go b/internal/issuer/signer/signer_test.go index 26434e2..70855cc 100644 --- a/internal/issuer/signer/signer_test.go +++ b/internal/issuer/signer/signer_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Keyfactor Command Authors. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/issuer/util/configclient.go b/internal/issuer/util/configclient.go index 990e9a1..db86470 100644 --- a/internal/issuer/util/configclient.go +++ b/internal/issuer/util/configclient.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Keyfactor Command Authors. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) +// ConfigClient is an interface for a K8s REST client. type ConfigClient interface { SetContext(ctx context.Context) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error @@ -43,6 +44,7 @@ type configClient struct { verifyAccessFunc func(apiResource string, resource types.NamespacedName) error } +// NewConfigClient creates a new K8s REST client using the configuration from the controller-runtime. func NewConfigClient(ctx context.Context) (ConfigClient, error) { config := ctrl.GetConfigOrDie() @@ -64,11 +66,15 @@ func NewConfigClient(ctx context.Context) (ConfigClient, error) { return client, nil } +// SetContext sets the context for the client. func (c *configClient) SetContext(ctx context.Context) { c.ctx = ctx c.logger = klog.FromContext(ctx) } +// verifyAccessToResource verifies that the client has access to a given resource in a given namespace +// by creating a SelfSubjectAccessReview. This is done to avoid errors when the client does not have +// access to the resource. func (c *configClient) verifyAccessToResource(apiResource string, resource types.NamespacedName) error { verbs := []string{"get", "list", "watch"} @@ -101,6 +107,7 @@ func (c *configClient) verifyAccessToResource(apiResource string, resource types return nil } +// GetConfigMap gets the configmap with the given name and namespace and copies it into the out parameter. func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { if c == nil { return fmt.Errorf("config client is nil") @@ -108,6 +115,8 @@ func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.Confi // Check if the client has access to the configmap resource if _, ok := c.accessCache[name.String()]; !ok { + // If this is the first time the client is accessing the resource and it does have + // permission, add it to the access cache so that it does not need to be checked again. err := c.verifyAccessFunc("configmaps", name) if err != nil { return err @@ -126,6 +135,7 @@ func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.Confi return nil } +// GetSecret gets the secret with the given name and namespace and copies it into the out parameter. func (c *configClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { if c == nil { return fmt.Errorf("config client is nil") @@ -133,6 +143,8 @@ func (c *configClient) GetSecret(name types.NamespacedName, out *corev1.Secret) // Check if the client has access to the secret resource if _, ok := c.accessCache[name.String()]; !ok { + // If this is the first time the client is accessing the resource and it does have + // permission, add it to the access cache so that it does not need to be checked again. err := c.verifyAccessFunc("secrets", name) if err != nil { return err diff --git a/internal/issuer/util/configclient_test.go b/internal/issuer/util/configclient_test.go index e57f922..5c30aad 100644 --- a/internal/issuer/util/configclient_test.go +++ b/internal/issuer/util/configclient_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Keyfactor Command Authors. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/issuer/util/util.go b/internal/issuer/util/util.go index d4b840e..8df80c6 100644 --- a/internal/issuer/util/util.go +++ b/internal/issuer/util/util.go @@ -30,17 +30,7 @@ import ( ejbcaissuer "github.com/Keyfactor/ejbca-issuer/api/v1alpha1" ) -func GetCertificateRequestAnnotations(issuer client.Object) (map[string]string, error) { - switch t := issuer.(type) { - case *ejbcaissuer.Issuer: - return t.GetAnnotations(), nil - case *ejbcaissuer.ClusterIssuer: - return t.GetAnnotations(), nil - default: - return nil, fmt.Errorf("not an issuer type: %t", t) - } -} - +// GetName is a helper function that returns the name of an Issuer object. func GetName(issuer client.Object) (string, error) { switch t := issuer.(type) { case *ejbcaissuer.Issuer: @@ -52,6 +42,7 @@ func GetName(issuer client.Object) (string, error) { } } +// GetSpecAndStatus is a helper function that returns the Spec and Status of an Issuer object. func GetSpecAndStatus(issuer client.Object) (*ejbcaissuer.IssuerSpec, *ejbcaissuer.IssuerStatus, error) { switch t := issuer.(type) { case *ejbcaissuer.Issuer: @@ -63,6 +54,7 @@ func GetSpecAndStatus(issuer client.Object) (*ejbcaissuer.IssuerSpec, *ejbcaissu } } +// SetCertificateRequestReadyCondition is a helper function that sets the Ready condition on an IssuerStatus. func SetCertificateRequestReadyCondition(ctx context.Context, csr *cmapi.CertificateRequest, status cmmeta.ConditionStatus, reason, message string) { log := ctrl.LoggerFrom(ctx) @@ -79,6 +71,7 @@ func SetCertificateRequestReadyCondition(ctx context.Context, csr *cmapi.Certifi ) } +// SetIssuerReadyCondition is a helper function that sets the Ready condition on an IssuerStatus. func SetIssuerReadyCondition(ctx context.Context, name, kind string, status *ejbcaissuer.IssuerStatus, conditionStatus ejbcaissuer.ConditionStatus, reason, message string) { log := ctrl.LoggerFrom(ctx) @@ -107,6 +100,7 @@ func SetIssuerReadyCondition(ctx context.Context, name, kind string, status *ejb } } +// GetReadyCondition is a helper function that returns the Ready condition from an IssuerStatus. func GetReadyCondition(status *ejbcaissuer.IssuerStatus) *ejbcaissuer.IssuerCondition { for _, c := range status.Conditions { if c.Type == ejbcaissuer.IssuerConditionReady { @@ -116,6 +110,7 @@ func GetReadyCondition(status *ejbcaissuer.IssuerStatus) *ejbcaissuer.IssuerCond return nil } +// IsReady is a helper function that returns true if the Ready condition is set to True. func IsReady(status *ejbcaissuer.IssuerStatus) bool { if c := GetReadyCondition(status); c != nil { return c.Status == ejbcaissuer.ConditionTrue diff --git a/main.go b/main.go index e830a0d..3223e6b 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Keyfactor. +Copyright © 2023 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.