From 53856ec8a4f0df453bb5ddd821350fcd4d007075 Mon Sep 17 00:00:00 2001 From: Morgan Gangwere <470584+indrora@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:33:12 -0700 Subject: [PATCH 1/8] command-cert-manager-issuer 2.6.0: Optional CA, Updated Default Healthcheck Interval, Updated Service Account Token Lifecycle (#70) (#71) * feat: Add volume and volume mount for service token * chore: fix sources and add changelog * feat(enrollment): make certificateAuthorityLogicalName be optional when using enrollment patterns * updated error messaging on csr enrollment * Update generated docs * chore: update docs + add e2e test for optional CA * Update generated docs * feat(healthcheck): bump default healthcheck interval from 1m to 10m * chore(deps): bump go version to 1.26 * Update generated docs * chore: address copilot feedback. update linter version * Update generated docs * chore(ci): bump controller tools version * chore(docs): document new Helm chart values for serviceAccount --------- Signed-off-by: Matthew H. Irby Co-authored-by: Matthew H. Irby Co-authored-by: Sven Rajala Co-authored-by: Keyfactor --- .github/workflows/test.yml | 2 +- .golangci.yml | 8 + CHANGELOG.md | 22 +++ Dockerfile | 2 +- Makefile | 4 +- README.md | 65 +++++--- api/v1alpha1/issuer_types.go | 7 +- cmd/main.go | 2 +- ...d-issuer.keyfactor.com_clusterissuers.yaml | 9 +- .../command-issuer.keyfactor.com_issuers.yaml | 9 +- .../command-cert-manager-issuer/README.md | 5 +- .../templates/crds/clusterissuers.yaml | 6 +- .../templates/crds/issuers.yaml | 6 +- .../templates/deployment.yaml | 30 ++++ .../templates/serviceaccount.yaml | 1 + .../command-cert-manager-issuer/values.yaml | 9 ++ docsource/content.md | 65 +++++--- e2e/run_tests.sh | 10 ++ go.mod | 2 +- internal/command/command.go | 69 +++++--- internal/command/command_test.go | 150 +++++++++++++++++- internal/controller/issuer_controller_test.go | 22 +-- 22 files changed, 404 insertions(+), 101 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a843b9d..cfdae7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: - name: Run linters uses: golangci/golangci-lint-action@v7 with: - version: v2.4.0 + version: v2.12.2 - name: Regenerate CRDs run: make generate manifests diff --git a/.golangci.yml b/.golangci.yml index bd04587..62b6538 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,6 +32,14 @@ linters: - third_party$ - builtin$ - examples$ + rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec formatters: enable: - goimports diff --git a/CHANGELOG.md b/CHANGELOG.md index fb8ab7f..382671b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# v2.6.0 +## Features +- Allow `certificateAuthorityLogicalName` to be optional when using an enrollment pattern. +- The default healthcheck interval has been bumped from 1 minute to 10 minutes. + +## Security +- The Helm chart now defaults `serviceAccount.automountServiceAccountToken` to `false`, + replacing the long-lived auto-mounted token with a short-lived projected token (~1 hour, automatically rotated by kubelet). The token is still mounted at the standard path `/var/run/secrets/kubernetes.io/serviceaccount` so no application changes are required. +- Go version has been bumped from 1.24 to 1.26.2+ to fix CVE-2026-27143 (affects Go compiler versions below 1.25.9, and 1.26.0-1.26.1). + +> [!IMPORTANT] +> +> ### Upgrade Notes +> +> - **Rolling restart**: Upgrading from v2.5.x will patch the ServiceAccount and update the Deployment spec, triggering an automatic rolling restart. Plan accordingly if downtime is a concern in your environment. +> - **Bring-your-own ServiceAccount**: If you set `serviceAccount.create: false` and manage your own ServiceAccount, you must either set `automountServiceAccountToken: true` in your `values.yaml` to preserve the previous behavior, or manually add `automountServiceAccountToken: false` and the projected volume to your ServiceAccount and Deployment manifests. +> - To restore the previous behavior explicitly, set in your `values.yaml`: +> ```yaml +> serviceAccount: +> automountServiceAccountToken: true +> ``` + # v2.5.3 ## Security - Updated dependencies to address various security vulnerabilities: diff --git a/Dockerfile b/Dockerfile index d5f88f3..56ae630 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24 AS builder +FROM golang:1.26 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index caf2534..023345b 100644 --- a/Makefile +++ b/Makefile @@ -190,9 +190,9 @@ CONFTEST = $(LOCALBIN)/conftest-$(CONFTEST_VERSION) ## Tool Versions KUSTOMIZE_VERSION ?= v5.3.0 -CONTROLLER_TOOLS_VERSION ?= v0.14.0 +CONTROLLER_TOOLS_VERSION ?= v0.17.3 ENVTEST_VERSION ?= latest -GOLANGCI_LINT_VERSION ?= v2.4.0 +GOLANGCI_LINT_VERSION ?= v2.12.2 KUBE_LINTER_VERSION ?= v0.6.8 CONFTEST_VERSION ?= v0.60.0 diff --git a/README.md b/README.md index c6c50b0..d6b6e88 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ Before continuing, ensure that the following requirements are met: - [Keyfactor Command](https://www.keyfactor.com/products/command/) >= 10.5 - Command must be properly configured according to the [product docs](https://software.keyfactor.com/Core-OnPrem/Current/Content/MasterTopics/Portal.htm). - You have access to the Command REST API. The following endpoints must be available: - - `/Status/Endpoints` - - `/Enrollment/CSR` - - `/MetadataFields` - - `/EnrollmentPatterns` (Keyfactor Command 25.1 and above) + - [/Status/Endpoints](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/StatusGetEndpoints.htm) + - [/Enrollment/CSR](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/EnrollmentPOSTCSR.htm) + - [/MetadataFields](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/MetadataFieldsGet.htm) + - [/EnrollmentPatterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/Enrollment-Patterns-GET.htm) (Keyfactor Command 25.1 and above) - Kubernetes >= v1.19 - [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/), etc. > You must have permission to create [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) in your Kubernetes cluster. @@ -56,7 +56,7 @@ Before continuing, ensure that the following requirements are met: ## Configuring Command -Command Issuer enrolls certificates by submitting a POST request to the Command CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template / Enrollment Pattern suitable for your use case. Additionally, you should ensure that the [identity provider](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/AuthenticateAPI.htm#AuthenticatingtotheKeyfactorAPI) used by the Issuer/ClusterIssuer has the appropriate permissions in Command. +Command Issuer enrolls certificates by submitting certificate signing requests to the Keyfactor Command [CSR Enrollment API](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/EnrollmentPOSTCSR.htm). Before using Command Issuer, you must have a Certificate Authority and either a Certificate Template or an Enrollment Pattern configured in Keyfactor Command that is suitable for your use case. You must also configure an [identity provider](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/AuthenticateAPI.htm#AuthenticatingtotheKeyfactorAPI) in Keyfactor Command and map the credentials used by the Issuer or ClusterIssuer to a [security role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm) with the appropriate permissions. 1. **Create or identify a Certificate Authority** @@ -112,7 +112,7 @@ Command Issuer enrolls certificates by submitting a POST request to the Command ## Installing Command Issuer -Command Issuer is installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). +Command Issuer is installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](./deploy/charts/command-cert-manager-issuer). 1. Verify that at least one Kubernetes node is running: @@ -153,15 +153,27 @@ Command Issuer is installed using a Helm chart. The chart is available in the [C ```shell helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ --namespace command-issuer-system \ - --version 2.4.0 + --version 2.4.0 \ --create-namespace ``` -> For all possible configuration values for the command-cert-manager-issuer Helm chart, please refer to [this list](./deploy/charts/command-cert-manager-issuer/README.md#configuration) +> For all possible configuration values for the command-cert-manager-issuer Helm chart, please refer to [the command-cert-manager-issuer Helm chart documentation](./deploy/charts/command-cert-manager-issuer/README.md#configuration). > The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. -> A list of configurable Helm chart parameters can be found [in the Helm chart docs](./deploy/charts/command-cert-manager-issuer/README.md#configuration) +## Healthchecks + +By default, Issuer and ClusterIssuer resources are deployed with a healthcheck that queries the Keyfactor Command API every 10 minutes to verify connectivity. If the healthcheck fails, the issuer marks itself unhealthy and pauses processing certificate requests until connectivity is restored — at which point it automatically recovers without operator intervention. Healthcheck intervals can be modified according to your environment's needs. + +**For production environments, it is not recommended to disable healthchecks**. Although certificate requests are automatically retried if the Command API is unreachable, disabling healthchecks means the issuer remains in a `Ready` state during an outage. Requests will be forwarded to the Command API before failing, resulting in slower recovery and less actionable error messages compared to the clear `issuer is not ready` rejection produced when healthchecks are enabled. + +Healthcheck configuration options: +- `healthcheck.enabled` and `healthcheck.interval` fields on the [Issuer / ClusterIssuer specification](#creating-issuer-and-clusterissuer-resources) +- `defaultHealthCheckInterval` Helm value to configure the default interval for all Command Issuer resources in the Kubernetes cluster +- `default-health-check-interval` argument on the command-cert-manager-issuer Deployment container + +> [!NOTE] +> The interval format for healthchecks is a decimal number with unit syntax ([docs](https://pkg.go.dev/time#ParseDuration)). Examples: `10m`, `30s`, `1.5h`. The minimum allowed interval is 30 seconds (`30s`). # Authentication @@ -172,7 +184,7 @@ Command Issuer supports explicit credentials authentication to Command using one - [Basic Authentication](#basic-auth) (username and password) - [OAuth 2.0 "client credentials" token flow](#oauth) (sometimes called two-legged OAuth 2.0) -These credentials must be configured using a Kubernetes Secret. By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). +These credentials must be configured using a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/). By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). > Command Issuer can read secrets in the Issuer namespace if `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag is set when installing the Helm chart. @@ -258,7 +270,7 @@ For example, ClusterIssuer resources can be used to issue certificates for resou | caSecretName | (optional) The name of the Kubernetes secret containing the CA certificate trust chain. See the [CA Bundle docs](./docs/ca-bundle/README.md) for more information. | | caBundleConfigMapName | (optional) The name of the Kubernetes ConfigMap containing the CA certificate trust chain. See the [CA Bundle docs](./docs/ca-bundle/README.md) for more information. | | caBundleKey | (optional) The name of the key in the ConfigMap or Secret specified by `caSecretName` or `caBundleConfigMapName` that contains the CA bundle. If omitted, the last key of the ConfigMap / Secret resource will be used. | - | certificateAuthorityLogicalName | The logical name of the Certificate Authority to use in Command. For example, `Sub-CA` | + | certificateAuthorityLogicalName | (Optional) The logical name of the Certificate Authority to use in Command. For example, `Sub-CA`. Optional if the enrollment pattern does not target a standalone CA. When not defined, Command will choose an eligible CA within the enrollment pattern's configuration tenant. | | certificateAuthorityHostname | (optional) The hostname of the Certificate Authority specified by `certificateAuthorityLogicalName`. This field is usually only required if the CA in Command is a DCOM (MSCA-like) CA. | | enrollmentPatternId | The ID of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternId` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. | | enrollmentPatternName | The Name of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternName` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. If using `enrollmentPatternName`, your security role must have `/enrollment_pattern/read/` permission. | @@ -267,12 +279,23 @@ For example, ClusterIssuer resources can be used to issue certificates for resou | ownerRoleName | The name of the security role assigned as the certificate owner. The security role must be assigned to the identity context of the issuer. If `ownerRoleId` and `ownerRoleName` are both specified, `ownerRoleId` will take precedence. This field is **required** if the enrollment pattern, certificate template, or system-wide setting requires it. | | scopes | (Optional) Required if using ambient credentials with Azure AKS. If using ambient credentials, these scopes will be put on the access token generated by the ambient credentials' token provider, if applicable. | | audience | (Optional) If using ambient credentials, this audience will be put on the access token generated by the ambient credentials' token provider, if applicable. Google's ambient credential token provider generates an OIDC ID Token. If this value is not provided, it will default to `command`. | - | healthcheck | (Optional) Defines the health check configuration for the issuer. If omitted, health checks will be enabled and default to 60 seconds. If left disabled, the issuer will not perform a health check when the issuer is healthy and may cause CertificateRequest resources to silently fail. | + | healthcheck | (Optional) Defines the health check configuration for the issuer. If omitted, health checks will be enabled and default to 10 minutes. If disabled, the issuer will remain in a Ready state during outages and forward requests to the Command API before failing. Although requests are retried automatically, issues are harder to diagnose without health checks. | | healthcheck.enabled | (Required if health check block provided) Boolean to enable / disable health checks. By default, health checks are enabled. | - | healthcheck.interval | (Optional) Defines the interval between health checks. Example values: `30s`, `1m`, `5.5m`. To prevent overloading the Command instance, this interval must not be less than `30s`. Default value: `60s`. | + | healthcheck.interval | (Optional) Defines the interval between health checks. Example values: `30s`, `1m`, `5.5m`. To prevent overloading the Command instance, this interval must not be less than `30s`. Default value: `10m`. | > If a different combination of hostname/certificate authority/certificate template is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. + > **What is a standalone CA?** + > A standalone CA is a Certificate Authority in Keyfactor Command that is not configured + > as part of a CA pool within the enrollment pattern — typically a DCOM (Microsoft CA) + > configured as a standalone (non-Active-Directory-integrated) CA. When an enrollment + > pattern targets a standalone CA, Command cannot automatically select a CA from a pool + > and requires `certificateAuthorityLogicalName` to be explicitly provided. + > + > If you are unsure whether your CA is a standalone CA, check the CA type and configuration + > in Command under **Certificate Authorities**, or contact your Keyfactor support + > representative. + 2. **Create an Issuer or ClusterIssuer** - **Issuer** @@ -295,10 +318,10 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # caBundleKey: "ca.crt" # references the key in the secret/configmap containing the CA trust chain (see CA Bundle docs for more info) # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required - certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. - # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" # Required if using Keyfactor Command 24.4 and below or if enrollment pattern is associated with a standalone CA + # enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + # certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Uncomment and set if using Keyfactor Command 24.4 and below. + enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. # ownerRoleId: "$OWNER_ROLE_ID" # Uncomment if required # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required @@ -330,10 +353,10 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # caBundleKey: "ca.crt" # references the key in the secret/configmap containing the CA trust chain (see CA Bundle docs for more info) # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required - certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. - # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" # Required if using Keyfactor Command 24.4 and below or if enrollment pattern is associated with a standalone CA + # enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + # certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Uncomment and set if using Keyfactor Command 24.4 and below. + enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. # ownerRoleId: "$OWNER_ROLE_ID" # Uncomment if required # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required diff --git a/api/v1alpha1/issuer_types.go b/api/v1alpha1/issuer_types.go index deba1d6..85328b1 100644 --- a/api/v1alpha1/issuer_types.go +++ b/api/v1alpha1/issuer_types.go @@ -85,8 +85,11 @@ type IssuerSpec struct { // + optional OwnerRoleName string `json:"ownerRoleName,omitempty"` - // CertificateAuthorityLogicalName is the logical name of the certificate authority to use + // CertificateAuthorityLogicalName is the logical name of the certificate authority to use. Not required if an enrollment pattern is specified, + // except if the enrollment pattern targets a standalone CA. If empty, an eligible certificate authority within the enrollment pattern's configuration tenant + // will be used. // E.g. "Keyfactor Root CA" or "Intermediate CA" + // +optional CertificateAuthorityLogicalName string `json:"certificateAuthorityLogicalName,omitempty"` // CertificateAuthorityHostname is the hostname associated with the Certificate Authority specified by @@ -301,7 +304,7 @@ type HealthCheckConfig struct { // Determines whether to enable the health check when the issuer is healthy. Default: true Enabled bool `json:"enabled"` - // The interval at which to health check the issuer when healthy. Defaults to 1 minute. Must not be less than "30s". + // The interval at which to health check the issuer when healthy. Defaults to 10 minutes. Must not be less than "30s". // +kubebuilder:validation:Optional Interval *metav1.Duration `json:"interval,omitempty"` } diff --git a/cmd/main.go b/cmd/main.go index 124a8f4..bf4e603 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -82,7 +82,7 @@ func main() { "If set the metrics endpoint is served securely") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - flag.StringVar(&healthCheckInterval, "default-health-check-interval", "60s", + flag.StringVar(&healthCheckInterval, "default-health-check-interval", "10m", // 10 minutes "If set, it is the default health check interval for issuers.") flag.StringVar(&clusterResourceNamespace, "cluster-resource-namespace", "", "The namespace for secrets in which cluster-scoped resources are found.") flag.BoolVar(&disableApprovedCheck, "disable-approved-check", false, diff --git a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml index 33f2b32..73fab0a 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.17.3 name: clusterissuers.command-issuer.keyfactor.com spec: group: command-issuer.keyfactor.com @@ -79,7 +79,9 @@ spec: type: string certificateAuthorityLogicalName: description: |- - CertificateAuthorityLogicalName is the logical name of the certificate authority to use + CertificateAuthorityLogicalName is the logical name of the certificate authority to use. Not required if an enrollment pattern is specified, + except if the enrollment pattern targets a standalone CA. If empty, an eligible certificate authority within the enrollment pattern's configuration tenant + will be used. E.g. "Keyfactor Root CA" or "Intermediate CA" type: string certificateTemplate: @@ -127,7 +129,8 @@ spec: type: boolean interval: description: The interval at which to health check the issuer - when healthy. Defaults to 1 minute. Must not be less than "30s". + when healthy. Defaults to 10 minutes. Must not be less than + "30s". type: string required: - enabled diff --git a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml index 27db089..0138d7c 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.17.3 name: issuers.command-issuer.keyfactor.com spec: group: command-issuer.keyfactor.com @@ -79,7 +79,9 @@ spec: type: string certificateAuthorityLogicalName: description: |- - CertificateAuthorityLogicalName is the logical name of the certificate authority to use + CertificateAuthorityLogicalName is the logical name of the certificate authority to use. Not required if an enrollment pattern is specified, + except if the enrollment pattern targets a standalone CA. If empty, an eligible certificate authority within the enrollment pattern's configuration tenant + will be used. E.g. "Keyfactor Root CA" or "Intermediate CA" type: string certificateTemplate: @@ -127,7 +129,8 @@ spec: type: boolean interval: description: The interval at which to health check the issuer - when healthy. Defaults to 1 minute. Must not be less than "30s". + when healthy. Defaults to 10 minutes. Must not be less than + "30s". type: string required: - enabled diff --git a/deploy/charts/command-cert-manager-issuer/README.md b/deploy/charts/command-cert-manager-issuer/README.md index 0d64052..3c83a88 100644 --- a/deploy/charts/command-cert-manager-issuer/README.md +++ b/deploy/charts/command-cert-manager-issuer/README.md @@ -77,6 +77,9 @@ The following table lists the configurable parameters of the `command-cert-manag | `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) | +| `serviceAccount.automountServiceAccountToken` | Controls whether Kubernetes automatically mounts the service account token into the pod. When `false` (default), a projected volume is used instead, giving explicit control over token expiration and file permissions. Setting to `true` uses Kubernetes' default token mount, which has no expiration and is less restrictive — only recommended if the projected volume approach causes compatibility issues. | `false` | +| `serviceAccount.projectedTokenVolume.expirationSeconds` | Lifetime in seconds of the projected service account token. The kubelet will rotate the token before it expires. Only applies when `automountServiceAccountToken` is `false`. | `3607` | +| `serviceAccount.projectedTokenVolume.defaultMode` | File permission bits for the projected token volume. Only applies when `automountServiceAccountToken` is `false`. | `0444` | | `podAnnotations` | Annotations for the pod | `{}` | | `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | | `securityContext` | Security context for the pod | `{}` (with commented out options) | @@ -85,4 +88,4 @@ The following table lists the configurable parameters of the `command-cert-manag | `tolerations` | Tolerations for pod assignment | `[]` | | `env` | Environmental variables set for pod | `{}` | | `secretConfig.useClusterRoleForSecretAccess` | Specifies if the ServiceAccount should be granted access to the Secret resource using a ClusterRole | `false` | -| `defaultHealthCheckInterval` | Specifies the default health check interval for issuers | `""` (uses the default in the code which is 60s) | +| `defaultHealthCheckInterval` | Specifies the default health check interval for issuers | `""` (uses the default in the code which is 10 minutes) | diff --git a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml index 4206341..cb827fe 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml @@ -73,7 +73,9 @@ spec: type: string certificateAuthorityLogicalName: description: |- - CertificateAuthorityLogicalName is the logical name of the certificate authority to use + CertificateAuthorityLogicalName is the logical name of the certificate authority to use. Not required if an enrollment pattern is specified, + except if the enrollment pattern targets a standalone CA. If empty, an eligible certificate authority within the enrollment pattern's configuration tenant + will be used. E.g. "Keyfactor Root CA" or "Intermediate CA" type: string enrollmentPatternId: @@ -104,7 +106,7 @@ spec: type: boolean interval: description: The interval at which to health check the issuer - when healthy. Defaults to 1 minute. + when healthy. Defaults to 10 minutes. type: string required: - enabled diff --git a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml index efb2dea..d5f70e0 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml @@ -73,7 +73,9 @@ spec: type: string certificateAuthorityLogicalName: description: |- - CertificateAuthorityLogicalName is the logical name of the certificate authority to use + CertificateAuthorityLogicalName is the logical name of the certificate authority to use. Not required if an enrollment pattern is specified, + except if the enrollment pattern targets a standalone CA. If empty, an eligible certificate authority within the enrollment pattern's configuration tenant + will be used. E.g. "Keyfactor Root CA" or "Intermediate CA" type: string enrollmentPatternId: @@ -104,7 +106,7 @@ spec: type: boolean interval: description: The interval at which to health check the issuer - when healthy. Defaults to 1 minute. + when healthy. Defaults to 10 minutes. type: string required: - enabled diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index 34e3bd1..2f5de7d 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -26,8 +26,32 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "command-cert-manager-issuer.serviceAccountName" . }} + {{- if not .Values.serviceAccount.automountServiceAccountToken }} + automountServiceAccountToken: false + {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if not .Values.serviceAccount.automountServiceAccountToken }} + volumes: + - name: serviceaccount-token + projected: + defaultMode: {{ .Values.serviceAccount.projectedTokenVolume.defaultMode }} + sources: + - serviceAccountToken: + expirationSeconds: {{ .Values.serviceAccount.projectedTokenVolume.expirationSeconds }} + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + - downwardAPI: + items: + - path: namespace + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + {{- end }} containers: - args: - --health-probe-bind-address=:8081 @@ -67,6 +91,12 @@ spec: {{- toYaml .Values.resources | nindent 12 }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} + {{- if not .Values.serviceAccount.automountServiceAccountToken }} + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: serviceaccount-token + readOnly: true + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml b/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml index 948bf2a..a5bab21 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml @@ -13,4 +13,5 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} {{- end }} diff --git a/deploy/charts/command-cert-manager-issuer/values.yaml b/deploy/charts/command-cert-manager-issuer/values.yaml index c63b5c6..e715b4c 100644 --- a/deploy/charts/command-cert-manager-issuer/values.yaml +++ b/deploy/charts/command-cert-manager-issuer/values.yaml @@ -48,6 +48,15 @@ serviceAccount: # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" + # Specifies whether to automount the service account token + # If false, a projected volume will be used to mount the token + automountServiceAccountToken: false + # Configuration for projected service account token volume (used when automountServiceAccountToken is false) + projectedTokenVolume: + # Token expiration time in seconds + expirationSeconds: 3607 + # File permissions for the token + defaultMode: 0444 podLabels: {} diff --git a/docsource/content.md b/docsource/content.md index 99cd071..b91edb4 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -9,10 +9,10 @@ Before continuing, ensure that the following requirements are met: - [Keyfactor Command](https://www.keyfactor.com/products/command/) >= 10.5 - Command must be properly configured according to the [product docs](https://software.keyfactor.com/Core-OnPrem/Current/Content/MasterTopics/Portal.htm). - You have access to the Command REST API. The following endpoints must be available: - - `/Status/Endpoints` - - `/Enrollment/CSR` - - `/MetadataFields` - - `/EnrollmentPatterns` (Keyfactor Command 25.1 and above) + - [/Status/Endpoints](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/StatusGetEndpoints.htm) + - [/Enrollment/CSR](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/EnrollmentPOSTCSR.htm) + - [/MetadataFields](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/MetadataFieldsGet.htm) + - [/EnrollmentPatterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/Enrollment-Patterns-GET.htm) (Keyfactor Command 25.1 and above) - Kubernetes >= v1.19 - [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/), etc. > You must have permission to create [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) in your Kubernetes cluster. @@ -23,7 +23,7 @@ Before continuing, ensure that the following requirements are met: ## Configuring Command -Command Issuer enrolls certificates by submitting a POST request to the Command CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template / Enrollment Pattern suitable for your use case. Additionally, you should ensure that the [identity provider](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/AuthenticateAPI.htm#AuthenticatingtotheKeyfactorAPI) used by the Issuer/ClusterIssuer has the appropriate permissions in Command. +Command Issuer enrolls certificates by submitting certificate signing requests to the Keyfactor Command [CSR Enrollment API](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/EnrollmentPOSTCSR.htm). Before using Command Issuer, you must have a Certificate Authority and either a Certificate Template or an Enrollment Pattern configured in Keyfactor Command that is suitable for your use case. You must also configure an [identity provider](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/AuthenticateAPI.htm#AuthenticatingtotheKeyfactorAPI) in Keyfactor Command and map the credentials used by the Issuer or ClusterIssuer to a [security role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm) with the appropriate permissions. 1. **Create or identify a Certificate Authority** @@ -80,7 +80,7 @@ Command Issuer enrolls certificates by submitting a POST request to the Command ## Installing Command Issuer -Command Issuer is installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). +Command Issuer is installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](./deploy/charts/command-cert-manager-issuer). 1. Verify that at least one Kubernetes node is running: @@ -121,15 +121,27 @@ Command Issuer is installed using a Helm chart. The chart is available in the [C ```shell helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ --namespace command-issuer-system \ - --version 2.4.0 + --version 2.4.0 \ --create-namespace ``` -> For all possible configuration values for the command-cert-manager-issuer Helm chart, please refer to [this list](./deploy/charts/command-cert-manager-issuer/README.md#configuration) +> For all possible configuration values for the command-cert-manager-issuer Helm chart, please refer to [the command-cert-manager-issuer Helm chart documentation](./deploy/charts/command-cert-manager-issuer/README.md#configuration). > The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. -> A list of configurable Helm chart parameters can be found [in the Helm chart docs](./deploy/charts/command-cert-manager-issuer/README.md#configuration) +## Healthchecks + +By default, Issuer and ClusterIssuer resources are deployed with a healthcheck that queries the Keyfactor Command API every 10 minutes to verify connectivity. If the healthcheck fails, the issuer marks itself unhealthy and pauses processing certificate requests until connectivity is restored — at which point it automatically recovers without operator intervention. Healthcheck intervals can be modified according to your environment's needs. + +**For production environments, it is not recommended to disable healthchecks**. Although certificate requests are automatically retried if the Command API is unreachable, disabling healthchecks means the issuer remains in a `Ready` state during an outage. Requests will be forwarded to the Command API before failing, resulting in slower recovery and less actionable error messages compared to the clear `issuer is not ready` rejection produced when healthchecks are enabled. + +Healthcheck configuration options: +- `healthcheck.enabled` and `healthcheck.interval` fields on the [Issuer / ClusterIssuer specification](#creating-issuer-and-clusterissuer-resources) +- `defaultHealthCheckInterval` Helm value to configure the default interval for all Command Issuer resources in the Kubernetes cluster +- `default-health-check-interval` argument on the command-cert-manager-issuer Deployment container + +> [!NOTE] +> The interval format for healthchecks is a decimal number with unit syntax ([docs](https://pkg.go.dev/time#ParseDuration)). Examples: `10m`, `30s`, `1.5h`. The minimum allowed interval is 30 seconds (`30s`). # Authentication @@ -140,7 +152,7 @@ Command Issuer supports explicit credentials authentication to Command using one - [Basic Authentication](#basic-auth) (username and password) - [OAuth 2.0 "client credentials" token flow](#oauth) (sometimes called two-legged OAuth 2.0) -These credentials must be configured using a Kubernetes Secret. By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). +These credentials must be configured using a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/). By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). > Command Issuer can read secrets in the Issuer namespace if `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag is set when installing the Helm chart. @@ -226,7 +238,7 @@ For example, ClusterIssuer resources can be used to issue certificates for resou | caSecretName | (optional) The name of the Kubernetes secret containing the CA certificate trust chain. See the [CA Bundle docs](./docs/ca-bundle/README.md) for more information. | | caBundleConfigMapName | (optional) The name of the Kubernetes ConfigMap containing the CA certificate trust chain. See the [CA Bundle docs](./docs/ca-bundle/README.md) for more information. | | caBundleKey | (optional) The name of the key in the ConfigMap or Secret specified by `caSecretName` or `caBundleConfigMapName` that contains the CA bundle. If omitted, the last key of the ConfigMap / Secret resource will be used. | - | certificateAuthorityLogicalName | The logical name of the Certificate Authority to use in Command. For example, `Sub-CA` | + | certificateAuthorityLogicalName | (Optional) The logical name of the Certificate Authority to use in Command. For example, `Sub-CA`. Optional if the enrollment pattern does not target a standalone CA. When not defined, Command will choose an eligible CA within the enrollment pattern's configuration tenant. | | certificateAuthorityHostname | (optional) The hostname of the Certificate Authority specified by `certificateAuthorityLogicalName`. This field is usually only required if the CA in Command is a DCOM (MSCA-like) CA. | | enrollmentPatternId | The ID of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternId` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. | | enrollmentPatternName | The Name of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternName` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. If using `enrollmentPatternName`, your security role must have `/enrollment_pattern/read/` permission. | @@ -235,12 +247,23 @@ For example, ClusterIssuer resources can be used to issue certificates for resou | ownerRoleName | The name of the security role assigned as the certificate owner. The security role must be assigned to the identity context of the issuer. If `ownerRoleId` and `ownerRoleName` are both specified, `ownerRoleId` will take precedence. This field is **required** if the enrollment pattern, certificate template, or system-wide setting requires it. | | scopes | (Optional) Required if using ambient credentials with Azure AKS. If using ambient credentials, these scopes will be put on the access token generated by the ambient credentials' token provider, if applicable. | | audience | (Optional) If using ambient credentials, this audience will be put on the access token generated by the ambient credentials' token provider, if applicable. Google's ambient credential token provider generates an OIDC ID Token. If this value is not provided, it will default to `command`. | - | healthcheck | (Optional) Defines the health check configuration for the issuer. If omitted, health checks will be enabled and default to 60 seconds. If left disabled, the issuer will not perform a health check when the issuer is healthy and may cause CertificateRequest resources to silently fail. | + | healthcheck | (Optional) Defines the health check configuration for the issuer. If omitted, health checks will be enabled and default to 10 minutes. If disabled, the issuer will remain in a Ready state during outages and forward requests to the Command API before failing. Although requests are retried automatically, issues are harder to diagnose without health checks. | | healthcheck.enabled | (Required if health check block provided) Boolean to enable / disable health checks. By default, health checks are enabled. | - | healthcheck.interval | (Optional) Defines the interval between health checks. Example values: `30s`, `1m`, `5.5m`. To prevent overloading the Command instance, this interval must not be less than `30s`. Default value: `60s`. | + | healthcheck.interval | (Optional) Defines the interval between health checks. Example values: `30s`, `1m`, `5.5m`. To prevent overloading the Command instance, this interval must not be less than `30s`. Default value: `10m`. | > If a different combination of hostname/certificate authority/certificate template is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. + > **What is a standalone CA?** + > A standalone CA is a Certificate Authority in Keyfactor Command that is not configured + > as part of a CA pool within the enrollment pattern — typically a DCOM (Microsoft CA) + > configured as a standalone (non-Active-Directory-integrated) CA. When an enrollment + > pattern targets a standalone CA, Command cannot automatically select a CA from a pool + > and requires `certificateAuthorityLogicalName` to be explicitly provided. + > + > If you are unsure whether your CA is a standalone CA, check the CA type and configuration + > in Command under **Certificate Authorities**, or contact your Keyfactor support + > representative. + 2. **Create an Issuer or ClusterIssuer** - **Issuer** @@ -263,10 +286,10 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # caBundleKey: "ca.crt" # references the key in the secret/configmap containing the CA trust chain (see CA Bundle docs for more info) # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required - certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. - # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" # Required if using Keyfactor Command 24.4 and below or if enrollment pattern is associated with a standalone CA + # enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + # certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Uncomment and set if using Keyfactor Command 24.4 and below. + enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. # ownerRoleId: "$OWNER_ROLE_ID" # Uncomment if required # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required @@ -298,10 +321,10 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # caBundleKey: "ca.crt" # references the key in the secret/configmap containing the CA trust chain (see CA Bundle docs for more info) # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required - certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. - # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" # Required if using Keyfactor Command 24.4 and below or if enrollment pattern is associated with a standalone CA + # enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + # certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Uncomment and set if using Keyfactor Command 24.4 and below. + enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. # ownerRoleId: "$OWNER_ROLE_ID" # Uncomment if required # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required diff --git a/e2e/run_tests.sh b/e2e/run_tests.sh index fab2f06..8e7117b 100755 --- a/e2e/run_tests.sh +++ b/e2e/run_tests.sh @@ -1096,6 +1096,16 @@ check_certificate_request_status echo "🧪✅ Test 6 completed successfully." echo "" +echo "🧪💬 Test 7: Certificate Authority is optional when Enrollment Pattern is used" +regenerate_issuer +delete_issuer_specification_field certificateAuthorityLogicalName Issuer +add_issuer_specification_field enrollmentPatternName "$ENROLLMENT_PATTERN_NAME" Issuer +regenerate_certificate_request Issuer +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 7 completed successfully." +echo "" + ## =================== END: Issuer & ClusterIssuer Tests ============================ ## =================== BEGIN: Annotation Tests ============================ diff --git a/go.mod b/go.mod index 0dcacbc..6583b09 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Keyfactor/command-cert-manager-issuer -go 1.24.0 +go 1.26.2 require ( github.com/Keyfactor/keyfactor-auth-client-go v1.3.1 diff --git a/internal/command/command.go b/internal/command/command.go index a88a3d5..2af71e4 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -260,9 +260,11 @@ func (s *SignConfig) validate() error { if s.CertificateTemplate == "" && s.EnrollmentPatternName == "" && s.EnrollmentPatternId == 0 { return errors.New("either certificateTemplate, enrollmentPatternName, or enrollmentPatternId must be specified") } - if s.CertificateAuthorityLogicalName == "" { - return errors.New("certificateAuthorityLogicalName is required") + + if !s.IsCertificateAuthorityDefined() && !s.IsEnrollmentPatternDefined() { + return errors.New("certificateAuthorityLogicalName is required if enrollmentPatternName or enrollmentPatternId are not provided") } + return nil } @@ -373,6 +375,15 @@ func (s *signer) Sign(ctx context.Context, csrBytes []byte, config *SignConfig) return nil, nil, err } + // If certificate authority is not defined alongside the enrollment pattern, Command will choose + // a CA within the same configuration tenant that best suits the certificate template, unless + // the target is a standalone CA. + // + // https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/EnrollmentPOSTCSR.htm + if config.IsEnrollmentPatternDefined() && !config.IsCertificateAuthorityDefined() { + k8sLog.Info("certificateAuthorityLogicalName is not set; the Command API may require a certificate authority if the enrollment pattern targets a standalone CA") + } + request, model, caBuilder, err := s.buildCsrEnrollRequest(config, k8sLog, csrBytes) if err != nil { return nil, nil, err @@ -398,39 +409,41 @@ func (s *signer) Sign(ctx context.Context, csrBytes []byte, config *SignConfig) commandCsrResponseObject, httpResp, err := s.client.EnrollCSR(req) if err != nil { - detail := fmt.Sprintf("error enrolling certificate with Command. Verify that the certificate authority %q (%s) is configured correctly", config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname) - - if template != nil { - detail += fmt.Sprintf(" and that the certificate template %q exists in Command", *template) - } - - if enrollmentPatternId != nil { - detail += fmt.Sprintf(". Make sure enrollment pattern ID %d is configured to use certificate authority %q (%s) and the security role is configured to use this enrollment pattern.", *enrollmentPatternId, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname) + hasPattern := enrollmentPatternId != nil + hasCA := config.CertificateAuthorityLogicalName != "" + hasTemplate := template != nil + + var hints []string + + switch { + case hasPattern && !hasCA: + hints = append(hints, fmt.Sprintf("Ensure that enrollment pattern ID %d has CSR Enrollment enabled and the security role is configured to use this enrollment pattern.", loggedEnrollmentPatternId)) + case hasPattern && hasCA && !hasTemplate: + hints = append(hints, fmt.Sprintf("Ensure that enrollment pattern ID %d has CSR Enrollment enabled, is configured to use certificate authority '%s', and the security role is configured to use this enrollment pattern.", loggedEnrollmentPatternId, config.CertificateAuthorityLogicalName)) + case hasPattern && hasCA && hasTemplate: + hints = append(hints, fmt.Sprintf("Verify that certificate template '%s' exists in Command and belongs to certificate authority '%s'. Verify the configuration for Enrollment Pattern ID %d in Keyfactor Command, and ensure CSR Enrollment is enabled.", *template, config.CertificateAuthorityLogicalName, loggedEnrollmentPatternId)) + case !hasPattern && hasCA: + hints = append(hints, fmt.Sprintf("Verify that certificate template '%s' exists in Command and belongs to certificate authority '%s'. If applicable, verify the Enrollment Pattern configuration in Keyfactor Command, and ensure CSR Enrollment is enabled.", *template, config.CertificateAuthorityLogicalName)) } if certificateOwnerId != nil || certificateOwnerName != nil { - detail += ". Make sure the cert manager issuer's security context is assigned to the owner role name or ID." + hints = append(hints, "Verify the issuer's security role is assigned to the configured owner role.") } if len(extractMetadataFromAnnotations(config.Annotations)) > 0 { - detail += ". Also verify that the metadata fields provided exist in Command" + hints = append(hints, "Verify that the metadata fields provided exist in Command.") } - if httpResp != nil { - if httpResp.Body != nil { - defer httpResp.Body.Close() - - bodyBytes, err := io.ReadAll(httpResp.Body) - if err == nil { - detail += fmt.Sprintf(". Error response from EnrollCSR API call: %s", string(bodyBytes)) - } else { - k8sLog.Error(err, "failed to read response body from failed EnrollCSR API call") - } + if httpResp != nil && httpResp.Body != nil { + defer httpResp.Body.Close() + if bodyBytes, readErr := io.ReadAll(httpResp.Body); readErr == nil && len(bodyBytes) > 0 { + hints = append(hints, fmt.Sprintf("Error response from Command: %s", string(bodyBytes))) + } else if readErr != nil { + k8sLog.Error(readErr, "failed to read response body from failed EnrollCSR API call") } } - err = fmt.Errorf("%w: %s: %w", errCommandEnrollmentFailure, detail, err) - return nil, nil, err + return nil, nil, fmt.Errorf("%w: %s: %w", errCommandEnrollmentFailure, strings.Join(hints, " "), err) } var certBytes []byte @@ -689,3 +702,11 @@ func getEnrollmentPatternByName(log logr.Logger, s *signer, enrollmentPatternNam return model, nil } + +func (s *SignConfig) IsEnrollmentPatternDefined() bool { + return s.EnrollmentPatternId != 0 || s.EnrollmentPatternName != "" +} + +func (s *SignConfig) IsCertificateAuthorityDefined() bool { + return s.CertificateAuthorityLogicalName != "" +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go index ac0faf5..99f8ac4 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2025 Keyfactor +Copyright © 2026 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -208,9 +208,14 @@ func TestSignConfigValidate(t *testing.T) { wantErr: "either certificateTemplate, enrollmentPatternName, or enrollmentPatternId must be specified", }, { - name: "missing certificateAuthorityLogicalName", - config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "", CertificateAuthorityHostname: "ca.example.com"}, - wantErr: "certificateAuthorityLogicalName is required", + name: "missing certificateAuthorityLogicalName and enrollmentPatternName", + config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "", CertificateAuthorityHostname: "ca.example.com", EnrollmentPatternName: ""}, + wantErr: "certificateAuthorityLogicalName is required if enrollmentPatternName or enrollmentPatternId are not provided", + }, + { + name: "missing certificateAuthorityLogicalName and enrollmentPatternId", + config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "", CertificateAuthorityHostname: "ca.example.com", EnrollmentPatternId: 0}, + wantErr: "certificateAuthorityLogicalName is required if enrollmentPatternName or enrollmentPatternId are not provided", }, { name: "all valid fields (both certificateTemplate and enrollmentPatternName specified)", @@ -224,12 +229,12 @@ func TestSignConfigValidate(t *testing.T) { }, { name: "all valid fields (only enrollmentPatternName specified)", - config: &SignConfig{EnrollmentPatternName: "My Enrollment Pattern", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + config: &SignConfig{EnrollmentPatternName: "My Enrollment Pattern", CertificateAuthorityLogicalName: "", CertificateAuthorityHostname: "ca.example.com"}, wantErr: "", }, { name: "all valid fields (only enrollmentPatternId specified)", - config: &SignConfig{EnrollmentPatternId: 123, CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + config: &SignConfig{EnrollmentPatternId: 123, CertificateAuthorityLogicalName: "", CertificateAuthorityHostname: "ca.example.com"}, wantErr: "", }, { @@ -649,6 +654,139 @@ func TestSign(t *testing.T) { } } +func TestSign_ErrorMessages(t *testing.T) { + caCert, rootKey := issueTestCertificate(t, "Root-CA", nil, nil) + issuingCert, issuingKey := issueTestCertificate(t, "Sub-CA", caCert, rootKey) + leafCert, _ := issueTestCertificate(t, "LeafCert", issuingCert, issuingKey) + + expectedLeafAndChain := append([]*x509.Certificate{leafCert}, issuingCert) + + // enrollmentPatternName := "fake-enrollment-pattern" + certificateTemplateName := "fake-cert-template" + certificateAuthorityLogicalName := "fake-issuing-ca" + enrollmentPatternId := 123 + + testCases := map[string]struct { + enrollCSRFunctionError error + enrollHTTPResponse *http.Response + enrollmentPatterns []v1.EnrollmentPatternsEnrollmentPatternResponse + + // Request + config *SignConfig + + // Expected + expectedSignError error + expectedErrorContent *string + }{ + "enroll-csr-err-enrollment-pattern": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + EnrollmentPatternId: int32(enrollmentPatternId), + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr(fmt.Sprintf("Ensure that enrollment pattern ID %d has CSR Enrollment enabled and the security role is configured to use this enrollment pattern.", enrollmentPatternId)), + }, + "enroll-csr-err-certificate-authority-and-enrollment-pattern": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + EnrollmentPatternId: int32(enrollmentPatternId), + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr(fmt.Sprintf("Ensure that enrollment pattern ID %d has CSR Enrollment enabled, is configured to use certificate authority '%s', and the security role is configured to use this enrollment pattern.", enrollmentPatternId, certificateAuthorityLogicalName)), + }, + "enroll-csr-err-certificate-authority-and-template-name": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateTemplate: certificateTemplateName, + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr(fmt.Sprintf("Verify that certificate template '%s' exists in Command and belongs to certificate authority '%s'. If applicable, verify the Enrollment Pattern configuration in Keyfactor Command, and ensure CSR Enrollment is enabled.", certificateTemplateName, certificateAuthorityLogicalName)), + }, + "enroll-csr-err-certificate-authority-and-template-name-and-enrollment-pattern": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateTemplate: certificateTemplateName, + EnrollmentPatternId: int32(enrollmentPatternId), + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr(fmt.Sprintf("Verify that certificate template '%s' exists in Command and belongs to certificate authority '%s'. Verify the configuration for Enrollment Pattern ID %d in Keyfactor Command, and ensure CSR Enrollment is enabled.", certificateTemplateName, certificateAuthorityLogicalName, enrollmentPatternId)), + }, + "enroll-csr-err-certificateOwnerId": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + EnrollmentPatternId: 10, + OwnerRoleId: 1000, + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr("Verify the issuer's security role is assigned to the configured owner role"), + }, + "enroll-csr-err-certificateOwnerName": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + EnrollmentPatternId: 10, + OwnerRoleName: "foobar", + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr("Verify the issuer's security role is assigned to the configured owner role"), + }, + "enroll-csr-err-annotations": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + EnrollmentPatternId: 10, + Annotations: map[string]string{ + "metadata.command-issuer.keyfactor.com/fizz": "buzz", + }, + }, + + expectedSignError: errCommandEnrollmentFailure, + expectedErrorContent: ptr("Verify that the metadata fields provided exist in Command"), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cb := func(req v1.ApiCreateEnrollmentCSRRequest) { + require.NotNil(t, req) + } + + client := fakeClient{ + err: tc.enrollCSRFunctionError, + + enrollResponse: certificateRestResponseFromExpectedCerts(t, expectedLeafAndChain, []*x509.Certificate{caCert}), + enrollHTTPResponse: tc.enrollHTTPResponse, + enrollCallback: cb, + } + signer := signer{ + client: &client, + } + + csrBytes, err := generateCSR("CN=command.example.org", []string{"dns.example.com"}, []string{}, []string{}) + require.NoError(t, err) + csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes.Raw}) + + _, _, err = signer.Sign(context.Background(), csrPem, tc.config) + assert.ErrorIs(t, err, errCommandEnrollmentFailure) + assert.Contains(t, err.Error(), *tc.expectedErrorContent, "error message should contain content from response body when enrollCSR returns an error") + }) + } +} + func TestCommandSupportsMetadata(t *testing.T) { testCases := map[string]struct { presentMeta []v1.CSSCMSDataModelModelsMetadataType diff --git a/internal/controller/issuer_controller_test.go b/internal/controller/issuer_controller_test.go index 54fda8b..371a1c2 100644 --- a/internal/controller/issuer_controller_test.go +++ b/internal/controller/issuer_controller_test.go @@ -60,6 +60,8 @@ var newFakeHealthCheckerBuilder = func(builderErr error, checkerErr error, suppo } } +const defaultHealthcheckInterval = time.Minute * 10 + func TestIssuerReconcile(t *testing.T) { type testCase struct { kind string @@ -115,7 +117,7 @@ func TestIssuerReconcile(t *testing.T) { healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false), expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionFalse, - expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "issuer-basicauth-no-username": { kind: "Issuer", @@ -237,7 +239,7 @@ func TestIssuerReconcile(t *testing.T) { clusterResourceNamespace: "kube-system", expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionFalse, - expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "success-issuer-oauth": { kind: "Issuer", @@ -278,7 +280,7 @@ func TestIssuerReconcile(t *testing.T) { healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false), expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionFalse, - expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "issuer-oauth-no-tokenurl": { kind: "Issuer", @@ -448,7 +450,7 @@ func TestIssuerReconcile(t *testing.T) { clusterResourceNamespace: "kube-system", expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionFalse, - expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "issuer-kind-Unrecognized": { kind: "UnrecognizedType", @@ -734,7 +736,7 @@ func TestIssuerReconcile(t *testing.T) { clusterResourceNamespace: "kube-system", expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionTrue, - expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "success-nil-healthcheck-interval-defaults": { kind: "ClusterIssuer", @@ -776,7 +778,7 @@ func TestIssuerReconcile(t *testing.T) { clusterResourceNamespace: "kube-system", expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionTrue, - expectedResult: ctrl.Result{RequeueAfter: time.Duration(60) * time.Second}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "success-default-healthcheck-interval": { kind: "ClusterIssuer", @@ -855,7 +857,7 @@ func TestIssuerReconcile(t *testing.T) { clusterResourceNamespace: "kube-system", expectedReadyConditionStatus: commandissuer.ConditionTrue, expectedMetadataSupportedConditionStatus: commandissuer.ConditionTrue, - expectedResult: ctrl.Result{RequeueAfter: time.Duration(60) * time.Second}, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthcheckInterval}, }, "error-healthcheck-minimum-value": { kind: "Issuer", @@ -916,10 +918,10 @@ func TestIssuerReconcile(t *testing.T) { tc.kind = "Issuer" } - defaultHealthcheckInterval := time.Minute + testCaseHealthcheckInterval := defaultHealthcheckInterval if tc.defaultHealthCheckInterval != nil { - defaultHealthcheckInterval = *tc.defaultHealthCheckInterval + testCaseHealthcheckInterval = *tc.defaultHealthCheckInterval } controller := IssuerReconciler{ @@ -929,7 +931,7 @@ func TestIssuerReconcile(t *testing.T) { HealthCheckerBuilder: tc.healthCheckerBuilder, ClusterResourceNamespace: tc.clusterResourceNamespace, SecretAccessGrantedAtClusterLevel: true, - DefaultHealthCheckInterval: defaultHealthcheckInterval, + DefaultHealthCheckInterval: testCaseHealthcheckInterval, } result, err := controller.Reconcile( From 0eb6bcc468c662c9d92dc9bd7dd77aebd354b010 Mon Sep 17 00:00:00 2001 From: slammajamma28 Date: Wed, 10 Jun 2026 21:35:49 -0400 Subject: [PATCH 2/8] Add priorityClassName --- .../command-cert-manager-issuer/templates/deployment.yaml | 3 +++ deploy/charts/command-cert-manager-issuer/values.yaml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index 2f5de7d..4d95538 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -121,4 +121,7 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- end }} terminationGracePeriodSeconds: 10 diff --git a/deploy/charts/command-cert-manager-issuer/values.yaml b/deploy/charts/command-cert-manager-issuer/values.yaml index e715b4c..cb9716f 100644 --- a/deploy/charts/command-cert-manager-issuer/values.yaml +++ b/deploy/charts/command-cert-manager-issuer/values.yaml @@ -89,6 +89,8 @@ nodeSelector: {} tolerations: [] +priorityClassName: "" + defaultHealthCheckInterval: "" env: {} From ff50682c8bc4d589feb70fb5d06521f5e729948c Mon Sep 17 00:00:00 2001 From: slammajamma28 Date: Wed, 10 Jun 2026 21:40:04 -0400 Subject: [PATCH 3/8] remove quote --- .../command-cert-manager-issuer/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index 4d95538..e9d3af7 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -122,6 +122,6 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.priorityClassName }} - priorityClassName: {{ .Values.priorityClassName | quote }} + priorityClassName: {{ .Values.priorityClassName }} {{- end }} terminationGracePeriodSeconds: 10 From 5369f77eb42b0a29f93b5fb50d8f6d938b72815c Mon Sep 17 00:00:00 2001 From: slammajamma28 Date: Mon, 29 Jun 2026 11:23:02 -0400 Subject: [PATCH 4/8] Update README --- .../command-cert-manager-issuer/README.md | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/deploy/charts/command-cert-manager-issuer/README.md b/deploy/charts/command-cert-manager-issuer/README.md index 3c83a88..7bfffd5 100644 --- a/deploy/charts/command-cert-manager-issuer/README.md +++ b/deploy/charts/command-cert-manager-issuer/README.md @@ -63,29 +63,33 @@ helm install command-cert-manager-issuer command-issuer/command-cert-manager-iss The following table lists the configurable parameters of the `command-cert-manager-issuer` chart and their default values. -| Parameter | Description | Default | -|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------| -| `replicaCount` | Number of replica command-cert-manager-issuers to run | `1` | -| `image.repository` | Image repository | `ghcr.io/keyfactor/command-cert-manager-issuer` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | -| `image.tag` | Image tag | `""` | -| `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) | +| Parameter | Description | Default | +|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| `replicaCount` | Number of replica command-cert-manager-issuers to run | `1` | +| `image.repository` | Image repository | `ghcr.io/keyfactor/command-cert-manager-issuer` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag | `""` | +| `imagePullSecrets` | Image pull secrets | `[]` | +| `nameOverride` | Name override | `""` | +| `fullnameOverride` | Full name override | `""` | +| `secretConfig.useClusterRoleForSecretAccess` | Specifies if the ServiceAccount should be granted access to the Secret resource using a ClusterRole | `false` | +| `secretConfig.useClusterRoleForConfigMapAccess` | Specifies if the ServiceAccount should be granted access to the ConfigMap resource using a ClusterRole | `false` | +| `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.labels` | Labels to add to the service account | `{}` | +| `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) | | `serviceAccount.automountServiceAccountToken` | Controls whether Kubernetes automatically mounts the service account token into the pod. When `false` (default), a projected volume is used instead, giving explicit control over token expiration and file permissions. Setting to `true` uses Kubernetes' default token mount, which has no expiration and is less restrictive — only recommended if the projected volume approach causes compatibility issues. | `false` | | `serviceAccount.projectedTokenVolume.expirationSeconds` | Lifetime in seconds of the projected service account token. The kubelet will rotate the token before it expires. Only applies when `automountServiceAccountToken` is `false`. | `3607` | -| `serviceAccount.projectedTokenVolume.defaultMode` | File permission bits for the projected token volume. Only applies when `automountServiceAccountToken` is `false`. | `0444` | -| `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 | `[]` | -| `env` | Environmental variables set for pod | `{}` | -| `secretConfig.useClusterRoleForSecretAccess` | Specifies if the ServiceAccount should be granted access to the Secret resource using a ClusterRole | `false` | -| `defaultHealthCheckInterval` | Specifies the default health check interval for issuers | `""` (uses the default in the code which is 10 minutes) | +| `serviceAccount.projectedTokenVolume.defaultMode` | File permission bits for the projected token volume. Only applies when `automountServiceAccountToken` is `false`. | `0444` | +| `podLabels` | Labels for the pod | `{}` | +| `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 | `[]` | +| `priorityClassName` | Priority class to set for pod | `""` | +| `defaultHealthCheckInterval` | Specifies the default health check interval for issuers | `""` (uses the default in the code which is 10 minutes)| +| `env` | Environmental variables set for pod | `{}` | \ No newline at end of file From 8a10d32ffbbfeb8e537c9c07b13030dd6307afc9 Mon Sep 17 00:00:00 2001 From: "Matthew H. Irby" Date: Wed, 1 Jul 2026 14:28:46 -0400 Subject: [PATCH 5/8] chore(AB#89226): add docs on how to contribute to the project Signed-off-by: Matthew H. Irby --- CONTRIBUTING.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2669087..6ffbef7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,24 @@ # Command Cert Manager Issuer Contribution Guide -## Requirements +## How to contribute + +### External Contributors - Submitting a PR + +Since external contributors can't create branches directly on our repositories, go ahead and open your PR against `main`. Our team will handle retargeting it to the appropriate release branch as part of our internal review process. You may see your PR retargeted or, in some cases, closed and replaced with an internal PR carrying your commits. In either case, your contribution will be preserved. + +### Release cadence + +We have an internal review process for every release we make. This process can sometimes take a few days or weeks depending on resourcing. + +## Development + +### Requirements - Go (>= 1.24) - golangci-lint (>= 2.4.0) ([installation notes](https://github.com/golangci/golangci-lint?tab=readme-ov-file#install-golangci-lint)) - helm (>= 3.x) — required to render chart templates for manifest linting ([installation notes](https://helm.sh/docs/intro/install/)) - conftest — policy testing tool powered by Open Policy Agent; installed automatically by `make lint-manifests` -## Installing dependencies +### Installing dependencies Project dependencies can be installed by running the following: ```bash @@ -19,14 +31,14 @@ The following command can be used to add missing requirements or remove unused m go mod tidy ``` -## Running unit tests +### Running unit tests The following command can be run to run the project unit tests: ```bash go test -v ./... ``` -## Running linters +### Running linters The project uses golangci-lint to lint the codebase. The following command can be run to run the linters: ```bash @@ -39,7 +51,7 @@ or, alternatively: make lint ``` -## Updating generated manifests +### Updating generated manifests This command will update the generated custom resource definitions under `config/crd/bases`: @@ -50,7 +62,7 @@ make generate manifests > [!IMPORTANT] > There is no automated process to automatically update the CRDs under `deploy/charts/command-cert-manager-issuer`. If any changes are made to the CRDs, the generated CRDs under `config/crd/bases` must be copied to `deploy/charts/command-cert-manager-issuer/crds` to ensure the Helm chart is up to date. -## Linting Helm manifests +### Linting Helm manifests The Helm chart under `deploy/charts/command-cert-manager-issuer` is linted with two tools on every PR: - **conftest** — runs custom Rego policies located in the [`policy/`](policy/) directory against the rendered manifests @@ -69,13 +81,13 @@ To inspect the rendered templates without linting: make helm-template ``` -### Adding or modifying policies +#### Adding or modifying policies Rego policies live in [`policy/`](policy/). Each `.rego` file in that directory is evaluated by conftest against every resource in the rendered chart. Add a new `.rego` file to enforce additional rules. For example, `policy/roles.rego` enforces that all `Role` resources declare an explicit namespace. kube-linter checks can be tuned in [.kube-linter.yaml](.kube-linter.yaml). To exclude a check, add its name under the `exclude` key. -## Running end-to-end tests +### Running end-to-end tests A comprehensive end-to-end test suite is available to verify the issuer code works against cert-manager and a Keyfactor Command instance. Instructions on how to run the end-to-end test suite can be found [here](./e2e/README.md). \ No newline at end of file From 481ff1541de529b68a2de18b921a68958633374c Mon Sep 17 00:00:00 2001 From: "Matthew H. Irby" Date: Wed, 1 Jul 2026 14:32:22 -0400 Subject: [PATCH 6/8] chore: update CHANGELOG Signed-off-by: Matthew H. Irby --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 382671b..fdde3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v2.6.1 +## Features +- Added [priorityClassName](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) as an optional specification on the Deployment resource. +## Chores +- Updated the `CONTRIBUTING.md` guide with notes for how external contributors can contribute to this project. + # v2.6.0 ## Features - Allow `certificateAuthorityLogicalName` to be optional when using an enrollment pattern. From f83c7131a219c45f580c83a7d75fc49e00342afd Mon Sep 17 00:00:00 2001 From: "Matthew H. Irby" Date: Wed, 1 Jul 2026 14:36:25 -0400 Subject: [PATCH 7/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ffbef7..1c95169 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ We have an internal review process for every release we make. This process can s ## Development ### Requirements -- Go (>= 1.24) +- Go (>= 1.26.2) - golangci-lint (>= 2.4.0) ([installation notes](https://github.com/golangci/golangci-lint?tab=readme-ov-file#install-golangci-lint)) - helm (>= 3.x) — required to render chart templates for manifest linting ([installation notes](https://helm.sh/docs/intro/install/)) - conftest — policy testing tool powered by Open Policy Agent; installed automatically by `make lint-manifests` From a169931f2ef844e22f963fa613722a9cc729502e Mon Sep 17 00:00:00 2001 From: "Matthew H. Irby" Date: Wed, 1 Jul 2026 14:43:28 -0400 Subject: [PATCH 8/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../command-cert-manager-issuer/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index e9d3af7..4d95538 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -122,6 +122,6 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.priorityClassName }} - priorityClassName: {{ .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} {{- end }} terminationGracePeriodSeconds: 10