diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index f72b649..b4fff0e 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -12,7 +12,7 @@ on: jobs: build: - name: Build and Lint + name: Build and Check CRDs runs-on: ubuntu-latest timeout-minutes: 8 steps: @@ -23,11 +23,17 @@ jobs: cache: true - run: go mod download - run: go build -v ./cmd/main.go + - name: Regenerate CRDs + run: make generate manifests + - name: Check for CRD drift + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after code generation. Run 'make generate manifests' and commit."; exit 1) # - name: Run linters # uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 # with: # version: latest - + test: name: Go Test needs: build diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5a6df..d4e62fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v2.4.0 +## Features +- Add a `healthcheck` specification to Issuer / ClusterIssuer resources, allowing flexibility in the health check interval. + # v2.3.1 ## Fixes - Add a manual dispatch of Helm chart release. diff --git a/README.md b/README.md index 2d3ce53..daa7636 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,8 @@ Command Issuer is installed using a Helm chart. The chart is available in the [C --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) + > The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. # Authentication @@ -251,6 +253,9 @@ 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.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`. | > 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. @@ -282,6 +287,9 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired + # healthcheck: # Optional health check configuration + # enabled: true + # interval: 30s EOF kubectl -n default apply -f issuer.yaml @@ -312,6 +320,9 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired + # healthcheck: # Optional health check configuration + # enabled: true + # interval: 30s EOF kubectl apply -f clusterissuer.yaml diff --git a/api/v1alpha1/issuer_types.go b/api/v1alpha1/issuer_types.go index ac0cdc7..418f47b 100644 --- a/api/v1alpha1/issuer_types.go +++ b/api/v1alpha1/issuer_types.go @@ -46,6 +46,11 @@ type IssuerSpec struct { // +kubebuilder:default:=KeyfactorAPI APIPath string `json:"apiPath,omitempty"` + // The healthcheck configuration for the issuer. This configures the frequency at which the issuer will perform + // a health check to determine issuer's connectivity to Command instance. + // +kubebuilder:validation:Optional + HealthCheck *HealthCheckConfig `json:"healthcheck,omitempty"` + // EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. // If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. // If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. @@ -279,6 +284,15 @@ const ( ConditionUnknown ConditionStatus = "Unknown" ) +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". + // +kubebuilder:validation:Optional + Interval *metav1.Duration `json:"interval,omitempty"` +} + func init() { SchemeBuilder.Register(&Issuer{}, &IssuerList{}) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3eb08e1..372708f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -29,7 +30,7 @@ func (in *ClusterIssuer) DeepCopyInto(out *ClusterIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -83,12 +84,32 @@ func (in *ClusterIssuerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthCheckConfig) DeepCopyInto(out *HealthCheckConfig) { + *out = *in + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckConfig. +func (in *HealthCheckConfig) DeepCopy() *HealthCheckConfig { + if in == nil { + return nil + } + out := new(HealthCheckConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Issuer) DeepCopyInto(out *Issuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -164,6 +185,11 @@ func (in *IssuerList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IssuerSpec) DeepCopyInto(out *IssuerSpec) { *out = *in + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(HealthCheckConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IssuerSpec. diff --git a/cmd/main.go b/cmd/main.go index 51ca91e..3a35bab 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,7 @@ import ( "flag" "fmt" "os" + "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -64,6 +65,7 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string + var healthCheckInterval string var secureMetrics bool var enableHTTP2 bool var clusterResourceNamespace string @@ -79,6 +81,8 @@ 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", + "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, "Disables waiting for CertificateRequests to have an approved condition before signing.") @@ -168,6 +172,17 @@ func main() { os.Exit(1) } + defaultHealthCheckInterval, err := time.ParseDuration(healthCheckInterval) + if err != nil { + setupLog.Error(err, "unable to parse default health check interval") + os.Exit(1) + } + + if defaultHealthCheckInterval < time.Duration(30) * time.Second { + setupLog.Error(errors.New(fmt.Sprintf("interval %s is invalid, must be greater than or equal to '30s'", healthCheckInterval)), "invalid health check interval") + os.Exit(1) + } + if err = (&controller.IssuerReconciler{ Client: mgr.GetClient(), Kind: "Issuer", @@ -175,6 +190,7 @@ func main() { SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, Scheme: mgr.GetScheme(), HealthCheckerBuilder: command.NewHealthChecker, + DefaultHealthCheckInterval: defaultHealthCheckInterval, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Issuer") os.Exit(1) @@ -186,6 +202,7 @@ func main() { ClusterResourceNamespace: clusterResourceNamespace, SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, HealthCheckerBuilder: command.NewHealthChecker, + DefaultHealthCheckInterval: defaultHealthCheckInterval, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterIssuer") os.Exit(1) diff --git a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml index 0d7feef..5d7a568 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml @@ -68,31 +68,67 @@ spec: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string + certificateTemplate: + description: |- + Deprecated. CertificateTemplate is the name of the certificate template to use. If using Keyfactor Command 25.1 or later, use EnrollmentPatternName or EnrollmentPatternId instead. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: string + commandSecretName: + description: |- + A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth + credentials for the Command instance configured in Hostname. The secret must + be 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 enrollmentPatternId: description: |- EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. - If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. - type: integer format: int32 + type: integer enrollmentPatternName: description: |- EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. - If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string + healthcheck: + description: |- + The healthcheck configuration for the issuer. This configures the frequency at which the issuer will perform + a health check to determine issuer's connectivity to Command instance. + properties: + enabled: + description: 'Determines whether to enable the health check when + the issuer is healthy. Default: true' + 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". + type: string + required: + - enabled + type: object + hostname: + description: Hostname is the hostname of a Keyfactor Command instance. + type: string ownerRoleId: description: |- OwnerRoleId is the ID of the security role assigned as the certificate owner. The specified security role must be assigned to the authorized identity context. If OwnerRoleId and OwnerRoleName are both specified, OwnerRoleId will take precedence. This field is required if the enrollment pattern, certificate template, or system-wide settings has been configured as Required. - type: integer format: int32 + type: integer ownerRoleName: description: |- OwnerRoleName is the name of the security role assigned as the certificate owner. This name must match the existing name of the security role. @@ -100,26 +136,6 @@ spec: If OwnerRoleId and OwnerRoleName are both specified, OwnerRoleId will take precedence. This field is required if the enrollment pattern, certificate template, or system-wide settings has been configured as Required. type: string - certificateTemplate: - description: |- - CertificateTemplate is the name of the certificate template to use. Deprecated in favor of EnrollmentPattern as of Keyfactor Command 25.1. - If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. - Enrollment will fail if the specified template is not compatible with the enrollment pattern. - Refer to the Keyfactor Command documentation for more information. - type: string - commandSecretName: - description: |- - A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth - credentials for the Command instance configured in Hostname. The secret must - be 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 - hostname: - description: Hostname is the hostname of a Keyfactor Command instance. - type: string scopes: description: |- A list of comma separated scopes used when requesting a Bearer token from an ambient token provider implied diff --git a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml index a4034ce..3695476 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml @@ -68,31 +68,67 @@ spec: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string + certificateTemplate: + description: |- + Deprecated. CertificateTemplate is the name of the certificate template to use. If using Keyfactor Command 25.1 or later, use EnrollmentPatternName or EnrollmentPatternId instead. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: string + commandSecretName: + description: |- + A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth + credentials for the Command instance configured in Hostname. The secret must + be 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 enrollmentPatternId: description: |- EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. - If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. - type: integer format: int32 + type: integer enrollmentPatternName: description: |- EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. - If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string + healthcheck: + description: |- + The healthcheck configuration for the issuer. This configures the frequency at which the issuer will perform + a health check to determine issuer's connectivity to Command instance. + properties: + enabled: + description: 'Determines whether to enable the health check when + the issuer is healthy. Default: true' + 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". + type: string + required: + - enabled + type: object + hostname: + description: Hostname is the hostname of a Keyfactor Command instance. + type: string ownerRoleId: description: |- OwnerRoleId is the ID of the security role assigned as the certificate owner. The specified security role must be assigned to the authorized identity context. If OwnerRoleId and OwnerRoleName are both specified, OwnerRoleId will take precedence. This field is required if the enrollment pattern, certificate template, or system-wide settings has been configured as Required. - type: integer format: int32 + type: integer ownerRoleName: description: |- OwnerRoleName is the name of the security role assigned as the certificate owner. This name must match the existing name of the security role. @@ -100,26 +136,6 @@ spec: If OwnerRoleId and OwnerRoleName are both specified, OwnerRoleId will take precedence. This field is required if the enrollment pattern, certificate template, or system-wide settings has been configured as Required. type: string - certificateTemplate: - description: |- - CertificateTemplate is the name of the certificate template to use. Deprecated in favor of EnrollmentPattern as of Keyfactor Command 25.1. - If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. - Enrollment will fail if the specified template is not compatible with the enrollment pattern. - Refer to the Keyfactor Command documentation for more information. - type: string - commandSecretName: - description: |- - A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth - credentials for the Command instance configured in Hostname. The secret must - be 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 - hostname: - description: Hostname is the hostname of a Keyfactor Command instance. - type: string scopes: description: |- A list of comma separated scopes used when requesting a Bearer token from an ambient token provider implied diff --git a/deploy/charts/command-cert-manager-issuer/README.md b/deploy/charts/command-cert-manager-issuer/README.md index 5256fde..b26bb88 100644 --- a/deploy/charts/command-cert-manager-issuer/README.md +++ b/deploy/charts/command-cert-manager-issuer/README.md @@ -84,3 +84,4 @@ The following table lists the configurable parameters of the `command-cert-manag | `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` | +| `defaultHealthCheckInterval` | Specifies the default health check interval for issuers | `""` (uses the default in the code which is 60s) | 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 ec8b4a9..f45d041 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml @@ -79,6 +79,22 @@ spec: Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string + healthcheck: + description: |- + The healthcheck configuration for the issuer. This configures the frequency at which the issuer will perform + a health check to determine issuer's connectivity to Command instance. + properties: + enabled: + description: 'Determines whether to enable the health check when the + issuer is healthy. Default: true' + type: boolean + interval: + description: The interval at which to health check the issuer + when healthy. Defaults to 1 minute. + type: string + required: + - enabled + type: object ownerRoleId: description: |- OwnerRoleId is the ID of the security role assigned as the certificate owner. 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 ea377ca..10a5214 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml @@ -79,6 +79,22 @@ spec: Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string + healthcheck: + description: |- + The healthcheck configuration for the issuer. This configures the frequency at which the issuer will perform + a health check to determine issuer's connectivity to Command instance. + properties: + enabled: + description: 'Determines whether to enable the health check when the + issuer is healthy. Default: true' + type: boolean + interval: + description: The interval at which to health check the issuer + when healthy. Defaults to 1 minute. + type: string + required: + - enabled + type: object ownerRoleId: description: |- OwnerRoleId is the ID of the security role assigned as the certificate owner. diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index 856ace0..4725013 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -36,6 +36,9 @@ spec: {{- if .Values.secretConfig.useClusterRoleForSecretAccess}} - --secret-access-granted-at-cluster-level {{- end}} + {{- if .Values.defaultHealthCheckInterval }} + - --default-health-check-interval={{ .Values.defaultHealthCheckInterval }} + {{- end }} command: - /manager image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.Version }}" diff --git a/deploy/charts/command-cert-manager-issuer/values.yaml b/deploy/charts/command-cert-manager-issuer/values.yaml index 6fd5bcb..4e8e7cc 100644 --- a/deploy/charts/command-cert-manager-issuer/values.yaml +++ b/deploy/charts/command-cert-manager-issuer/values.yaml @@ -70,3 +70,5 @@ resources: {} nodeSelector: {} tolerations: [] + +defaultHealthCheckInterval: "" diff --git a/docsource/content.md b/docsource/content.md index 898600a..4faaddb 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -112,6 +112,8 @@ Command Issuer is installed using a Helm chart. The chart is available in the [C --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) + > The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. # Authentication @@ -219,6 +221,9 @@ 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.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`. | > 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. @@ -250,6 +255,9 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired + # healthcheck: # Optional health check configuration + # enabled: true + # interval: 30s EOF kubectl -n default apply -f issuer.yaml @@ -280,6 +288,9 @@ For example, ClusterIssuer resources can be used to issue certificates for resou # ownerRoleName: "$OWNER_ROLE_NAME" # Uncomment if required # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired + # healthcheck: # Optional health check configuration + # enabled: true + # interval: 30s EOF kubectl apply -f clusterissuer.yaml diff --git a/e2e/.env.example b/e2e/.env.example index 874e5fa..9dea707 100644 --- a/e2e/.env.example +++ b/e2e/.env.example @@ -7,4 +7,6 @@ export CERTIFICATE_AUTHORITY_LOGICAL_NAME="Sub-CA" export OAUTH_TOKEN_URL="https://example.com/oauth2/token" export OAUTH_CLIENT_ID="changeme" -export OAUTH_CLIENT_SECRET='changeme' \ No newline at end of file +export OAUTH_CLIENT_SECRET='changeme' +export OAUTH_SCOPES='optional' # remove if not needed +export OAUTH_AUDIENCE='optional' # remove if not needed \ No newline at end of file diff --git a/e2e/README.md b/e2e/README.md index f4fbe62..9a0262c 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,7 +1,49 @@ # End-to-End Test Suite -This is a test suite intended to make it easy to run tests on the command-cert-manager-issuer project. This suite can test the local changes of the command issuer, and it is able to test existing Docker images. +This is a test suite intended to make it easy to run end-to-end tests on the command-cert-manager-issuer project. This suite can test the local changes of the Command issuer, and it is able to test existing Docker images. + +The test suite does the following: +- Deploys command-cert-manager-issuer to a Kubernetes cluster with the desired version +- Creates an issuer (Issuer and ClusterIssuer) +- Creates a Certificate custom resource +- Waits for cert-manager to create a CertificateRequest, then signs the request +- Waits for the issuer to handle the CertificateRequest +- Verifies the CertificateRequest has been successfully processed and an issuer secret is created with the related certificate information. This is currently configured as a Bash script, so it is necessary to run this on a UNIX-compatible machine. -Instructions on how to run the e2e test suite are within the [run_tests.sh](./run_tests.sh) file. \ No newline at end of file +Instructions on how to run the e2e test suite are within the [run_tests.sh](./run_tests.sh) file. + +## Requirements +- An available Command instance is running and configured as described in the [root README](../README.md#configuring-command) + - OAuth is used to communicate with Command +- Docker (>= 28.2.2) +- Minikube (>= v1.35.0) +- kubectl (>= v1.32.2) +- helm (>= v3.17.1) +- cmctl (>= v2.1.1) + +## Configuring the environment variables +command-cert-manager-issuer interacts with an external Command instance. An environment variable file `.env` can be used to store the environment variables to be used to talk to the Command instance. + +A `.env.example` file is available as a template for your environment variables. + +```bash +# copy .env.example to .env +cp .env.example .env +``` + +Modify the fields as needed. + +## Running the script + +```bash +# enable the script to be executed +chmod +x ./run_tests.sh + +# load the environment variables +source .env + +# run the end-to-end tests +./run_tests.sh +``` \ No newline at end of file diff --git a/e2e/run_tests.sh b/e2e/run_tests.sh index 84987d6..7541d35 100755 --- a/e2e/run_tests.sh +++ b/e2e/run_tests.sh @@ -26,15 +26,6 @@ ## ======================================================================= -## ======================= Requirements =================================== -# - Minikube running -# - Helm installed -# - Docker installed -# - kubectl installed -# - cmctl installed -# - cert-manager Helm chart available -## =========================================================================== - ## ======================= How to run =================================== # Enable the script to run: # > chmod +x run_tests.sh @@ -52,13 +43,12 @@ IMAGE_TAG="local" # Uncomment if you want to build the image locally FULL_IMAGE_NAME="${IMAGE_REPO}/${IMAGE_NAME}:${IMAGE_TAG}" HELM_CHART_NAME="command-cert-manager-issuer" -#HELM_CHART_VERSION="2.1.0" # Uncomment if you want to use a specific version from the Helm repository +# HELM_CHART_VERSION="2.1.0" # Uncomment if you want to use a specific version from the Helm repository HELM_CHART_VERSION="local" # Uncomment if you want to use the local Helm chart IS_LOCAL_DEPLOYMENT=$([ "$IMAGE_TAG" = "local" ] && echo "true" || echo "false") IS_LOCAL_HELM=$([ "$HELM_CHART_VERSION" = "local" ] && echo "true" || echo "false") -# TODO: Handle both in the e2e tests ISSUER_TYPE="Issuer" CLUSTER_ISSUER_TYPE="ClusterIssuer" @@ -85,13 +75,17 @@ ISSUER_NAMESPACE="issuer-playground" SIGNER_SECRET_NAME="auth-secret" SIGNER_CA_SECRET_NAME="ca-secret" +CERTIFICATE_CRD_FQTN="certificates.cert-manager.io" CERTIFICATEREQUEST_CRD_FQTN="certificaterequests.cert-manager.io" - -CR_CR_NAME="req" +CR_C_NAME="command-cert" +CR_CR_NAME="command-cert-1" +CR_C_SECRET_NAME="$CR_C_NAME-tls" set -e # Exit on any error +# checks if environment variable is available in system. if it is not present but the variable is required +# an error is thrown validate_env_present() { local env_var=$1 local required=$2 @@ -106,6 +100,7 @@ validate_env_present() { fi } +# checks whether the following environment variables are provided. some environment variables are optional. check_env() { validate_env_present HOSTNAME true validate_env_present API_PATH true @@ -114,10 +109,13 @@ check_env() { validate_env_present OAUTH_TOKEN_URL true validate_env_present OAUTH_CLIENT_ID true validate_env_present OAUTH_CLIENT_SECRET true + validate_env_present OAUTH_AUDIENCE false + validate_env_present OAUTH_SCOPES false validate_env_present CERTIFICATE_AUTHORITY_HOSTNAME false } +# checks whether the provided kubernetes namespace exists ns_exists () { local ns=$1 if [ "$(kubectl get namespace -o json | jq --arg namespace "$ns" -e '.items[] | select(.metadata.name == $namespace) | .metadata.name')" ]; then @@ -126,6 +124,7 @@ ns_exists () { return 1 } +# checks whether the provided helm chart has been deployed to the cluster (namespaced) helm_exists () { local namespace=$1 local chart_name=$2 @@ -135,6 +134,7 @@ helm_exists () { return 1 } +# checks whether the provided custom resource can be found in the cluster (namespaced) cr_exists () { local fqtn=$1 local ns=$2 @@ -146,6 +146,7 @@ cr_exists () { return 1 } +# checks whether the provided secret name exists in the cluster (namespaced) secret_exists () { local ns=$1 local name=$2 @@ -156,6 +157,7 @@ secret_exists () { return 1 } +# installs cert-manager onto the Kubernetes cluster install_cert_manager() { echo "๐Ÿ“ฆ Installing cert-manager..." @@ -179,6 +181,7 @@ install_cert_manager() { echo "โœ… cert-manager installed successfully" } +# installs the issuer to the Kubernetes cluster install_cert_manager_issuer() { echo "๐Ÿ“ฆ Installing instance of $IMAGE_NAME with tag $IMAGE_TAG..." @@ -194,9 +197,21 @@ install_cert_manager_issuer() { VERSION_PARAM="" else + # Add command-issuer repository if not already added + if ! helm repo list | grep -q command-issuer; then + echo "Adding command-issuer Helm repository..." + helm repo add command-issuer https://keyfactor.github.io/command-cert-manager-issuer + fi + CHART_PATH="command-issuer/command-cert-manager-issuer" echo "Using Helm chart from repository for version ${HELM_CHART_VERSION}: $CHART_PATH..." - VERSION_PARAM="--version ${HELM_CHART_VERSION}" + + # Only include --devel if HELM_CHART_VERSION is a pre-release (contains -alpha, -beta, -rc, etc.) + if [[ "${HELM_CHART_VERSION}" =~ -alpha|-beta|-rc ]]; then + VERSION_PARAM="--version ${HELM_CHART_VERSION} --devel" + else + VERSION_PARAM="--version ${HELM_CHART_VERSION}" + fi fi # Only set the image repository parameter if we are deploying locally @@ -205,6 +220,15 @@ install_cert_manager_issuer() { else IMAGE_REPO_PARAM="" fi + + + + # Only set the pull policy to Never if we are deploying locally + if [[ "$IS_LOCAL_DEPLOYMENT" == "true" ]]; then + PULL_POLICY_PARAM="--set image.pullPolicy=Never" + else + PULL_POLICY_PARAM="" + fi # Helm chart could be out of date for release candidates, so we will install from # the chart defined in the repository. @@ -214,12 +238,116 @@ install_cert_manager_issuer() { $IMAGE_REPO_PARAM \ --set "fullnameOverride=${IMAGE_NAME}" \ --set image.tag=${IMAGE_TAG} \ - --set image.pullPolicy=Never \ - --wait + $PULL_POLICY_PARAM \ + --wait \ + --timeout 30s echo "โœ… $IMAGE_NAME installed successfully" } +# performs a redeployment of the cert-manager. helpful for recycling TLS certificates that have expired. +deploy_cert_manager() { + # Restart all cert-manager components + kubectl rollout restart deployment/cert-manager -n ${CERT_MANAGER_NAMESPACE} + kubectl rollout restart deployment/cert-manager-webhook -n ${CERT_MANAGER_NAMESPACE} + kubectl rollout restart deployment/cert-manager-cainjector -n ${CERT_MANAGER_NAMESPACE} + + # Wait for them to be ready + kubectl rollout status deployment/cert-manager -n ${CERT_MANAGER_NAMESPACE} + kubectl rollout status deployment/cert-manager-webhook -n ${CERT_MANAGER_NAMESPACE} + kubectl rollout status deployment/cert-manager-cainjector -n ${CERT_MANAGER_NAMESPACE} +} + +# deploys the issuer to the Kubernetes cluster +deploy_cert_manager_issuer() { + # Find the deployment name (assuming it follows a pattern) + DEPLOYMENT_NAME=$(kubectl get deployments -n ${MANAGER_NAMESPACE} -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "$IMAGE_NAME") + + # Between runs, we want to make sure that the running issuer has the latest version of the code we want. + # Doing this patch and redeployment forces the container to restart with the latest desired version + if kubectl get deployment ${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} >/dev/null 2>&1; then + # Patch the deployment + kubectl patch deployment ${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} -p "{ + \"spec\": { + \"template\": { + \"spec\": { + \"containers\": [{ + \"name\": \"${IMAGE_NAME}\", + \"image\": \"${FULL_IMAGE_NAME}\", + \"imagePullPolicy\": \"Never\" + }] + } + } + } + }" + + # Rollout deployment changes and apply the patch + kubectl rollout restart deployment/${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} + kubectl rollout status deployment/${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} --timeout=300s + + + echo "โœ… Deployment patched and rolled out successfully" + else + echo "โš ๏ธ Deployment ${DEPLOYMENT_NAME} not found. The Helm chart might use a different naming convention." + echo "Available deployments in ${MANAGER_NAMESPACE}:" + kubectl get deployments -n ${MANAGER_NAMESPACE} + fi + + echo "" + echo "๐ŸŽ‰ Deployment complete!" + echo "" +} + +# check the expiration of the cert-manager TLS certificate +check_cert_manager_webhook_cert() { + local namespace=${1:-cert-manager} + local secret_name=${2:-cert-manager-webhook-ca} + + echo "๐Ÿ” Checking cert-manager webhook certificate..." + + # Check if secret exists + if ! kubectl get secret "$secret_name" -n "$namespace" >/dev/null 2>&1; then + echo "โŒ Secret $secret_name not found in namespace $namespace" + return 1 + fi + + # Get certificate data + local cert_data=$(kubectl get secret "$secret_name" -n "$namespace" -o jsonpath='{.data.tls\.crt}' 2>/dev/null) + + if [ -z "$cert_data" ]; then + echo "โŒ No certificate data found in secret" + return 1 + fi + + # Decode and check certificate + local cert_info=$(echo "$cert_data" | base64 -d | openssl x509 -noout -dates 2>/dev/null) + + if [ $? -ne 0 ]; then + echo "โŒ Failed to parse certificate" + return 1 + fi + + echo "๐Ÿ“‹ Certificate validity:" + echo "$cert_info" + + # Check if certificate is currently valid + if echo "$cert_data" | base64 -d | openssl x509 -noout -checkend 0 >/dev/null 2>&1; then + echo "โœ… Certificate is currently valid" + + # Check if expires within 7 days + if ! echo "$cert_data" | base64 -d | openssl x509 -noout -checkend 604800 >/dev/null 2>&1; then + echo "โš ๏ธ Certificate expires within 7 days" + return 2 # Warning status + fi + + return 0 # Valid + else + echo "โŒ Certificate is expired or not yet valid" + return 1 # Expired + fi +} + +# creates a new issuer custom resource create_issuer() { echo "๐Ÿ” Creating issuer resource..." @@ -260,6 +388,7 @@ EOF echo "โœ… Issuer resources created successfully" } +# creates a new cluster issuer custom resource create_cluster_issuer() { echo "๐Ÿ” Creating cluster issuer resource..." @@ -300,6 +429,7 @@ EOF echo "โœ… Issuer resources created successfully" } +# deletes Issuer and ClusterIssuer custom resources from the Kubernetes cluster delete_issuers() { echo "๐Ÿ—‘๏ธ Deleting issuer resources..." @@ -323,6 +453,59 @@ delete_issuers() { echo "โœ… Issuer resources deleted successfully" } +# creates a Certificate custom resource. this is picked up by cert-manager and converted to a CertificateRequest. +create_certificate() { + local issuer_type=$1 + + echo "Generating a certificate object for issuer type: $issuer_type" + + kubectl -n "$ISSUER_NAMESPACE" apply -f - <