From cdab9b1326ffee5bf372517ad203537a42f7f7f5 Mon Sep 17 00:00:00 2001 From: Bharath B Date: Mon, 8 Sep 2025 10:07:39 +0530 Subject: [PATCH 1/6] CM-706: Revisits istiocsr API for GA release --- .../cert-manager/istio-csr-controller.md | 747 ++++++++++++------ .../cert-manager/istio-csr-delete.png | Bin 20365 -> 17164 bytes .../cert-manager/istio-csr-delete.puml | 4 +- 3 files changed, 519 insertions(+), 232 deletions(-) diff --git a/enhancements/cert-manager/istio-csr-controller.md b/enhancements/cert-manager/istio-csr-controller.md index e0e2222411..63fc78d8a0 100644 --- a/enhancements/cert-manager/istio-csr-controller.md +++ b/enhancements/cert-manager/istio-csr-controller.md @@ -9,7 +9,7 @@ approvers: api-approvers: - "@tgeer" ## approver for cert-manager component creation-date: 2024-01-22 -last-updated: 2024-11-21 +last-updated: 2025-09-05 tracking-link: - https://issues.redhat.com/browse/CM-234 see-also: @@ -56,7 +56,7 @@ manager, requiring integration with OSSM so that certificate requests are signed - As an OpenShift user, I want to have an option to dynamically configure `istio-csr` agent, so that only the required features can be enabled by updating the custom resource. - As an OpenShift user, I should be able to remove `istio-csr` agent when not required by removing the custom resource, - and controller should cleanup all resources created for the `istio-csr` agent deployment. + and controller should clean up all resources created for the `istio-csr` agent deployment. - As an OpenShift user, I should be able to install `istio-csr` agent in the upgrade cluster where istiod control plane is active and should be able to update the certificate endpoint to `istio-csr` agent endpoint. - As an OpenShift user, I want to have an option to dynamically enable monitoring for the `istio-csr` project and @@ -71,9 +71,6 @@ manager, requiring integration with OSSM so that certificate requests are signed - `istio-csr` agent can be used only with supported version of `OpenShift Service Mesh`. Please refer `Version Skew Strategy` section for more details. -- Removing `istiocsr.operator.openshift.io` CR object will not remove `istio-csr` agent deployment. But will only stop - the reconciliation of kubernetes resources created for agent deployment. (Note: This will be a limitation for TechPreview - and will be re-evaluated for GA) ## Proposal @@ -103,10 +100,13 @@ Each of the resource created for `istio-csr` agent deployment will have below se * `app: cert-manager-istio-csr` * `app.kubernetes.io/name: cert-manager-istio-csr` * `app.kubernetes.io/instance: cert-manager-istio-csr` -* `app.kubernetes.io/version: "v0.12.0"` +* `app.kubernetes.io/version: "v0.14.2"` * `app.kubernetes.io/managed-by: cert-manager-operator` * `app.kubernetes.io/part-of: cert-manager-operator` +These labels adhere to Kubernetes and OpenShift conventions, aiding in identifying, categorizing, and managing the +`istio-csr` components within the cluster environment, thereby facilitating operations like monitoring and resource discovery. + Refer below links for more information on the labels used - [Guidelines for Labels and Annotations for OpenShift applications](https://github.com/redhat-developer/app-labels/blob/master/labels-annotation-for-openshift.adoc) - [Well-Known Labels, Annotations and Taints](https://kubernetes.io/docs/reference/labels-annotations-taints/) @@ -117,7 +117,7 @@ Refer below links for more information on the labels used Configurations made available in the spec of `istiocsr.operator.openshift.io` CR are passed as command line arguments to `istio-csr` agent and updating these configurations would cause new rollout of the `istio-csr` agent deployment, which means a new pod will be created and old pod will terminate resulting in `istio-csr` agent restart. -All configurations can be updated on-demand, except for below which can be configured only while creating the `istiocsr.operator.openshift.io` +All configurations can be updated on-demand, except for below which can be configured only once during or after creating `istiocsr.operator.openshift.io` CR, which is enforced in the CRD using CEL validations. - `.spec.istioCSRConfig.certmanager.issuerRef` - `.spec.istioCSRConfig.istiodTLSConfig.privateKeySize` @@ -126,10 +126,6 @@ CR, which is enforced in the CRD using CEL validations. - `.spec.istioCSRConfig.istio.revisions` - `.spec.istioCSRConfig.istio.namespace` -When an OpenShift user deletes `istiocsr.operator.openshift.io` CR object, `istio-csr-controller` will stop managing all -the resources created for installing `istio-csr` agent and user will manually clean up the resources. Please refer -`Operational Aspects of API Extensions` section for command to list all the resources. - `istiocsr.operator.openshift.io` CR status sub-resource will be used for updating the status of the `istio-csr` agent installation, any error encountered while creating the required resources or the reconciling the state. @@ -147,8 +143,7 @@ for better version management. - Uninstallation of `istio-csr` agent - An OpenShift user deletes the `istiocsr.operator.openshift.io` CR. - - `istio-csr-controller` will not uninstall `istio-csr` agent, but will only stop reconciling the kubernetes resources - created for installing the agent. Please refer `Non-Goals` section for more details. + - `istio-csr-controller` will remove all the resources created for installing `istio-csr` agent. ![alt text](./istio-csr-delete.png). @@ -158,18 +153,39 @@ Below new API `istiocsrs.operator.openshift.io` is introduced for managing istio ```golang package v1alpha1 +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// IstioCSRList is a list of IstioCSR objects. +type IstioCSRList struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard list's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ListMeta `json:"metadata"` + Items []IstioCSR `json:"items"` +} + // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true // +kubebuilder:subresource:status - -// IstioCSR describes configuration and information about the managed istio-csr -// agent. The name must be `default`. +// +kubebuilder:resource:path=istiocsrs,scope=Namespaced,categories={cert-manager-operator, istio-csr, istiocsr},shortName=istiocsr;icsr +// +kubebuilder:printcolumn:name="GRPC Endpoint",type="string",JSONPath=".status.istioCSRGRPCEndpoint" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:metadata:labels={"app.kubernetes.io/name=istiocsr", "app.kubernetes.io/part-of=cert-manager-operator"} + +// IstioCSR describes the configuration and information about the managed istio-csr +// agent. The name must be `default` to make IstioCSR a singleton that is, to +// allow only one instance of IstioCSR per namespace. +// +// When an IstioCSR is created, istio-csr agent is deployed in the IstioCSR +// created namespace. // -// When an IstioCSR is created, a new deployment is created which manages the -// istio-csr agent and keeps it in the desired state. -// +kubebuilder:object:root=true -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="istiocsr is a singleton, .metadata.name must be 'default'" +// +operator-sdk:csv:customresourcedefinitions:displayName="IstioCSR" type IstioCSR struct { metav1.TypeMeta `json:",inline"` @@ -178,197 +194,330 @@ type IstioCSR struct { metav1.ObjectMeta `json:"metadata,omitempty"` // spec is the specification of the desired behavior of the IstioCSR. - Spec IstioCSRSpec `json:"spec,omitempty"` + // +kubebuilder:validation:Required + // +required + Spec IstioCSRSpec `json:"spec"` // status is the most recently observed status of the IstioCSR. + // +kubebuilder:validation:Optional + // +optional Status IstioCSRStatus `json:"status,omitempty"` } // IstioCSRSpec is the specification of the desired behavior of the IstioCSR. type IstioCSRSpec struct { - // istioCSRConfig is for configuring the istio-csr agent behavior. - IstioCSRConfig *IstioCSRConfig `json:"istioCSRConfig,omitempty"` + // istioCSRConfig configures the istio-csr agent's behavior. + // +kubebuilder:validation:Required + // +required + IstioCSRConfig IstioCSRConfig `json:"istioCSRConfig"` - // controllerConfig is for configuring the controller for setting up - // defaults to enable istio-csr agent. + // controllerConfig configures the controller for setting up defaults to + // enable the istio-csr agent. + // +kubebuilder:validation:Optional + // +optional ControllerConfig *ControllerConfig `json:"controllerConfig,omitempty"` + + // cleanupOnDeletion indicates that the operator should remove all resources + // created for the istio-csr agent installation, including the cert-manager `Certificate` + // resource created for obtaining a certificate for istiod using the configured issuer. + // This field is immutable once set. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="cleanupOnDeletion is immutable once set" + // +kubebuilder:validation:Enum:="true";"false" + // +kubebuilder:default:="false" + // +kubebuilder:validation:Optional + // +optional + CleanupOnDeletion string `json:"cleanupOnDeletion,omitempty"` } -// IstioCSRConfig is for configuring the istio-csr agent behavior. +// IstioCSRConfig configures the istio-csr agent's behavior. type IstioCSRConfig struct { - // logLevel is for setting verbosity of istio-csr agent logging. - // Supported log levels: 1-5. + // logLevel supports a value range as per [Kubernetes logging guidelines](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use). // +kubebuilder:default:=1 // +kubebuilder:validation:Minimum:=1 // +kubebuilder:validation:Maximum:=5 + // +kubebuilder:validation:Optional // +optional LogLevel int32 `json:"logLevel,omitempty"` - // logFormat is for specifying the output format of istio-csr agent logging. - // Support log formats are text and json. - // +kubebuilder:default:=text + // logFormat specifies the output format for istio-csr agent logging. + // Supported log formats are text and json. + // +kubebuilder:validation:Enum:="text";"json" + // +kubebuilder:default:="text" + // +kubebuilder:validation:Optional // +optional LogFormat string `json:"logFormat,omitempty"` - // certmanager is for configuring cert-manager specifics. + // Istio-csr creates a ConfigMap named `istio-ca-root-cert` containing the root CA certificate, which the Istio data + // plane uses to verify server certificates. Its default behavior is to create and monitor ConfigMaps in all namespaces. + // The istioDataPlaneNamespaceSelector restricts the namespaces where the ConfigMap is created by using label selectors, + // such as maistra.io/member-of=istio-system. This selector is also attached to all desired namespaces that are part of + // the data plane. istioDataPlaneNamespaceSelector must not exceed 4096 characters. + // +kubebuilder:example:="maistra.io/member-of=istio-system" + // +kubebuilder:validation:MinLength:=0 + // +kubebuilder:validation:MaxLength:=4096 + // +kubebuilder:validation:Optional + // +optional + IstioDataPlaneNamespaceSelector string `json:"istioDataPlaneNamespaceSelector,omitempty"` + + // certManager is for configuring cert-manager specifics. + // +kubebuilder:validation:Required // +required - CertManager *CertManagerConfig `json:"certmanager,omitempty"` + CertManager CertManagerConfig `json:"certManager"` // istiodTLSConfig is for configuring istiod certificate specifics. + // +kubebuilder:validation:Required // +required - IstiodTLSConfig *IstiodTLSConfig `json:"istiodTLSConfig,omitempty"` + IstiodTLSConfig IstiodTLSConfig `json:"istiodTLSConfig"` // server is for configuring the server endpoint used by istio // for obtaining the certificates. + // +kubebuilder:validation:Optional // +optional Server *ServerConfig `json:"server,omitempty"` // istio is for configuring the istio specifics. - Istio *IstioConfig `json:"istio,omitempty"` + // +kubebuilder:validation:Required + // +required + Istio IstioConfig `json:"istio"` - // Resources is for defining the resource requirements. + // resources is for defining the resource requirements. + // Cannot be updated. // ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + // +kubebuilder:validation:Optional // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty"` - // Affinity is for setting scheduling affinity rules. + // affinity is for setting scheduling affinity rules. // ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ + // +kubebuilder:validation:Optional // +optional Affinity *corev1.Affinity `json:"affinity,omitempty"` - // Tolerations is for setting the pod tolerations. + // tolerations is for setting the pod tolerations. + // tolerations can have a maximum of 10 entries. // ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ + // +listType=atomic + // +kubebuilder:validation:MinItems:=0 + // +kubebuilder:validation:MaxItems:=10 + // +kubebuilder:validation:Optional // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - // NodeSelector is for defining the scheduling criteria using node labels. + // nodeSelector is for defining the scheduling criteria using node labels. + // nodeSelector can have a maximum of 10 entries. // ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + // +mapType=atomic + // +kubebuilder:validation:MinProperties:=0 + // +kubebuilder:validation:MaxProperties:=10 + // +kubebuilder:validation:Optional // +optional NodeSelector map[string]string `json:"nodeSelector,omitempty"` } // CertManagerConfig is for configuring cert-manager specifics. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.issuerRef) && !has(self.issuerRef) || has(oldSelf.issuerRef) && has(self.issuerRef)",message="issuerRef may only be configured during creation" type CertManagerConfig struct { - // issuerRef contains details to the referenced object used for - // obtaining the certificates. - // When unset operator will create Issuer in the configured istio's - // namespace. - // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="issuerRef is immutable once set" + // issuerRef contains details of the referenced object used for obtaining certificates. When + // `issuerRef.Kind` is `Issuer`, it must exist in the `.spec.istioCSRConfig.istio.namespace`. + // This field is immutable once set. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="issuerRef is immutable once set" + // +kubebuilder:validation:XValidation:rule="self.kind.lowerAscii() == 'issuer' || self.kind.lowerAscii() == 'clusterissuer'",message="kind must be either 'Issuer' or 'ClusterIssuer'" + // +kubebuilder:validation:XValidation:rule="self.group.lowerAscii() == 'cert-manager.io'",message="group must be 'cert-manager.io'" + // +kubebuilder:validation:Required // +required - IssuerRef certmanagerv1.ObjectReference `json:"issuerRef,omitempty"` + IssuerRef certmanagerv1.ObjectReference `json:"issuerRef"` + + // istioCACertificate when provided, the operator will use the CA certificate from the specified ConfigMap. If empty, the operator will + // automatically extract the CA certificate from the Secret containing the istiod certificate obtained from cert-manager. + // +kubebuilder:validation:Optional + // +optional + IstioCACertificate *ConfigMapReference `json:"istioCACertificate,omitempty"` } -// IstiodTLSConfig is for configuring certificate specifics. +// IstiodTLSConfig is for configuring istiod certificate specifics. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.privateKeyAlgorithm) && !has(self.privateKeyAlgorithm) || has(oldSelf.privateKeyAlgorithm) && has(self.privateKeyAlgorithm)",message="privateKeyAlgorithm may only be configured during creation" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.privateKeySize) && !has(self.privateKeySize) || has(oldSelf.privateKeySize) && has(self.privateKeySize)",message="privateKeySize may only be configured during creation" type IstiodTLSConfig struct { - // TrustDomain is the cluster's trust domain. - // +required - TrustDomain string `json:"trustDomain,omitempty"` - - // RootCAFile is for setting the file location containing the root CA which is - // present in the configured IssuerRef. File should be made available using the - // Volume and VolumeMount options. + // commonName is the common name to be set in the cert-manager.io Certificate created for istiod. + // The commonName will be of the form istiod..svc when not set. + // The commonName must not exceed 64 characters. + // +kubebuilder:validation:MinLength:=0 + // +kubebuilder:validation:MaxLength:=64 + // +kubebuilder:example:="istiod.istio-system.svc" + // +kubebuilder:validation:Optional // +optional - RootCAFile string `json:"rootCAFile,omitempty"` + CommonName string `json:"commonName,omitempty"` - // CertificateDNSNames contains DNS names to be added to the certificate SAN. + // trustDomain is the Istio cluster's trust domain, which will also be used for deriving + // the SPIFFE URI. trustDomain must not exceed 63 characters. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=63 + // +kubebuilder:validation:Required // +required + TrustDomain string `json:"trustDomain"` + + // certificateDNSNames contains the additional DNS names to be added to the istiod certificate SAN. + // This field can have a maximum of 25 entries. + // +kubebuilder:validation:MinItems:=0 + // +kubebuilder:validation:MaxItems:=25 + // +listType=set + // +kubebuilder:validation:Optional + // +optional CertificateDNSNames []string `json:"certificateDNSNames,omitempty"` - // certificateDuration is the istio's certificate validity period. + // certificateDuration is the validity period for the istio-csr and istiod certificates. // +kubebuilder:default:="1h" + // +kubebuilder:validation:Optional // +optional - CertificateDuration time.Duration `json:"certificateDuration,omitempty"` + CertificateDuration *metav1.Duration `json:"certificateDuration,omitempty"` - // certificateRenewBefore is the ahead time to renew the istio's certificate before - // expiry. + // certificateRenewBefore is the time before expiry to renew the istio-csr and istiod certificates. + // before expiry. // +kubebuilder:default:="30m" + // +kubebuilder:validation:Optional // +optional - CertificateRenewBefore time.Duration `json:"certificateRenewBefore,omitempty"` + CertificateRenewBefore *metav1.Duration `json:"certificateRenewBefore,omitempty"` - // privateKeySize is the key size to be for RSAKey. - // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="privateKeySize is immutable once set" + // privateKeySize is the key size for the istio-csr and istiod certificates. Allowed values when privateKeyAlgorithm + // is RSA are 2048, 4096, 8192; and for ECDSA, they are 256, 384. This field is immutable once set. + // +kubebuilder:validation:Enum:=256;384;2048;4096;8192 // +kubebuilder:default:=2048 + // +kubebuilder:validation:XValidation:rule="oldSelf == 0 || self == oldSelf",message="privateKeySize is immutable once set" + // +kubebuilder:validation:Optional // +optional PrivateKeySize int32 `json:"privateKeySize,omitempty"` -} - -// ServerConfig is for configuring the server endpoint used by istio -// for obtaining the certificates. -type ServerConfig struct { - // Address to serve istio-csr gRPC service. - // +kubebuilder:default:="0.0.0.0" - // +optional - Address string `json:"address,omitempty"` - - // Port to serve istio-csr gRPC service. - // +kubebuilder:default:="6443" - // +optional - Port string `json:"port,omitempty"` - // CertificateKeySize is the server's serving certificate's key size. - // +kubebuilder:default:=2048 - // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="certificateKeySize is immutable once set" - // +optional - CertificateKeySize int32 `json:"certificateKeySize,omitempty"` - - // SignatureAlgorithm is the signature algorithm to use when generating - // private keys. At present only RSA and ECDSA are supported. + // privateKeyAlgorithm is the algorithm to use when generating private keys. Allowed values are RSA, and ECDSA. + // This field is immutable once set. // +kubebuilder:default:="RSA" // +kubebuilder:validation:Enum:="RSA";"ECDSA" - // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="signatureAlgorithm is immutable once set" + // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="privateKeyAlgorithm is immutable once set" + // +kubebuilder:validation:Optional // +optional - SignatureAlgorithm string `json:"signatureAlgorithm,omitempty"` + PrivateKeyAlgorithm string `json:"privateKeyAlgorithm,omitempty"` // MaxCertificateDuration is the maximum validity duration that can be // requested for a certificate. // +kubebuilder:default:="1h" + // +kubebuilder:validation:Optional + // +optional + MaxCertificateDuration *metav1.Duration `json:"maxCertificateDuration,omitempty"` +} + +// ServerConfig is for configuring the server endpoint used by istio +// for obtaining the certificates. +type ServerConfig struct { + // clusterID is the Istio cluster ID used to verify incoming CSRs. + // +kubebuilder:default:="Kubernetes" + // +kubebuilder:validation:MinLength:=0 + // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:Optional + // +optional + ClusterID string `json:"clusterID,omitempty"` + + // port to serve the istio-csr gRPC service. + // +kubebuilder:default:=443 + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=65535 + // +kubebuilder:validation:XValidation:rule="oldSelf == 0 || self == oldSelf",message="port is immutable once set" + // +kubebuilder:validation:Optional // +optional - MaxCertificateDuration time.Duration `json:"certificateDuration,omitempty"` + Port int32 `json:"port,omitempty"` } // IstioConfig is for configuring the istio specifics. type IstioConfig struct { - // Revisions are the istio revisions that are currently installed in the cluster. - // Changing this field will modify the DNS names that will be requested for - // the istiod certificate. - // +kubebuilder:default:=["default"] - // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="revisions is immutable once set" + // revisions are the Istio revisions that are currently installed in the cluster. + // Changing this field will modify the DNS names that will be requested for the + // istiod certificate. This field is immutable once set and can have a maximum of 10 entries. + // +listType=set + // +kubebuilder:default:={"default"} + // +kubebuilder:validation:XValidation:rule="self.all(x, x in oldSelf) && oldSelf.all(x, x in self)",message="revisions is immutable once set" + // +kubebuilder:validation:MinItems=0 + // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:Optional // +optional Revisions []string `json:"revisions,omitempty"` - // namespace of the istio control-plane. In the same namespace issuer will be created - // used for obtaining the serving certificates. - // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="namespace is immutable once set" + // namespace of the Istio control plane. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="namespace is immutable once set" + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:Required // +required - Namespace string `json:"namespace,omitempty"` + Namespace string `json:"namespace"` } -// ControllerConfig is for configuring the controller for setting up -// defaults to enable istio-csr agent. +// ControllerConfig configures the controller for setting up defaults to +// enable the istio-csr agent. type ControllerConfig struct { - // labels to apply to all resources created for istio-csr agent deployment. + // labels to apply to all resources created for the istio-csr agent deployment. + // This field can have a maximum of 20 entries. + // +mapType=granular + // +kubebuilder:validation:MinProperties:=0 + // +kubebuilder:validation:MaxProperties:=20 + // +kubebuilder:validation:Optional + // +optional Labels map[string]string `json:"labels,omitempty"` } // IstioCSRStatus is the most recently observed status of the IstioCSR. type IstioCSRStatus struct { - // conditions holds information of the current state of the istio-csr agent deployment. - Conditions *metav1.Condition `json:"conditions,omitempty"` + // conditions holds information about the current state of the istio-csr agent deployment. + ConditionalStatus `json:",inline,omitempty"` // istioCSRImage is the name of the image and the tag used for deploying istio-csr. IstioCSRImage string `json:"istioCSRImage,omitempty"` - // istioCSRGRPCEndpoint is the service endpoint of istio-csr made available for user - // to configure the same in istiod config to enable istio to use istio-csr for - // certificate requests. + // istioCSRGRPCEndpoint is the service endpoint of istio-csr, made available for users + // to configure in the istiod config to enable Istio to use istio-csr for certificate requests. IstioCSRGRPCEndpoint string `json:"istioCSRGRPCEndpoint,omitempty"` // serviceAccount created by the controller for the istio-csr agent. ServiceAccount string `json:"serviceAccount,omitempty"` + // clusterRole created by the controller for the istio-csr agent. + ClusterRole string `json:"clusterRole,omitempty"` + // clusterRoleBinding created by the controller for the istio-csr agent. ClusterRoleBinding string `json:"clusterRoleBinding,omitempty"` } + +type ConditionalStatus struct { + // conditions holds information about the current state of the istio-csr agent deployment. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// ConfigMapReference holds the details of a configmap. +type ConfigMapReference struct { + // name of the ConfigMap. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + // +kubebuilder:validation:Required + // +required + Name string `json:"name"` + + // namespace in which the ConfigMap exists. If empty, ConfigMap will be looked up in IstioCSR created namespace. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=63 + // +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + // +kubebuilder:validation:Optional + // +optional + Namespace string `json:"namespace,omitempty"` + + // key name holding the required data. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:Pattern:=^[-._a-zA-Z0-9]+$ + // +kubebuilder:validation:Required + // +required + Key string `json:"key"` +} ``` ### Topology Considerations @@ -387,6 +536,48 @@ None ### Implementation Details/Notes/Constraints +#### New API changes for GA release + +Below new fields are added in `istiocsrs.operator.openshift.io` to support functionality not included in TP and also +based on user feedback. +- `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` - `istio-csr` agent by default creates a configmap with the + CA certificate used for signing the istio server certificate and is required by the proxies in the data plane to verify + the server certificate to enable mTLS. `istio-csr` agent just creates and updates the configmap `istio-ca-root-cert` + and does not delete/removes it. Creating the configmap in all namespaces would not be required for and namespaces created + for OpenShift control plane like `kube-system`, and those starting `openshift` can be excluded. `istioDataPlaneNamespaceSelector` + helps in filtering the namespaces. Please refer `Risks and Mitigations` section for other details. +- `spec.istioCSRConfig.certManager.istioCACertificate` - In the TP release, operator was extracting the CA certificate + from the configured self-signed issuer and was making it available in the container using volumes. But for issuer types + like vault, venafi, the CA certificate used is not available in issuer. `istioCACertificate` allows configuring the configmap + which contains the CA certificate used for signing the istiod server certificates, and it will be made available to `istio-csr` + using volumes, after basic validations like certificate is indeed CA and the CA attribute in certificates `Basic Constraints` + section it set to true and was used for signing the istiod server certificate. When `istioCACertificate` is not provided + operator instead of looking in the configured `Issuer/ClusterIssuer` will instead look in the secret containing the + certificates obtained from cert-manager for istiod server. The `Certificate` object having the specifics of the istiod + server certificate is managed by operator. Please refer `Risks and Mitigations` section for other details. + Use case reference: https://issues.redhat.com/browse/CM-564 +- `spec.istioCSRConfig.server.clusterID` - The `clusterID` field is used to configure the Istiod cluster ID. This ensures + that only certificate signing requests (CSRs) from the matching Istio control plane are allowed. + +Validations for fields in the `istiocsrs.operator.openshift.io` API are updated with minimum and maximum limits to +restrict the values that can be configured, preventing users from setting arbitrarily large data that could cause +system instability. +For below fields, the lower and upper bounds have been set based on what could be considered as reasonable, as there is +no defined standard. And can be updated in future based on user feedback. +- `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is for setting a label selector to filter the namespaces + where `isito-csr` should create the config containing the istiod CA certificate. An upper limit of `4096` is fixed allowing + to configure up to 10 namespaces considering the limitations on the label selectors and when equality operators are used. +- `spec.istioCSRConfig.tolerations` and `spec.istioCSRConfig.nodeSelector` fields are allowed to max of `10` entries. +- `spec.istioCSRConfig.istio.revisions` field has upper bound limit of `10`. +- `spec.controllerConfig.labels` field allows to configure a maximum of `20` labels to be attached to all the resources created + by the operator to deploy `istio-csr` agent. + +#### Operator uninstallation or `istiocsrs.operator.openshift.io` instance deletion. + +Operator will remove all the resources created for installing `istio-csr` agent when `spec.cleanupOnDeletion` is enabled. + +#### Manifests for installing `istio-csr` agent. + Below are the example static manifests used for creating required resources for installing `istio-csr` agent. 1. Service for creating istio-csr grpc server, for serving CertificateRequests endpoint. ```yaml @@ -394,16 +585,20 @@ Below are the example static manifests used for creating required resources for kind: Service metadata: name: cert-manager-istio-csr - namespace: istio-system + namespace: cert-manager labels: app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator spec: type: ClusterIP ports: - - port: 443 - targetPort: 6443 - protocol: TCP - name: grpc + - port: 443 + targetPort: 6443 + protocol: TCP + name: web selector: app: cert-manager-istio-csr ``` @@ -415,9 +610,12 @@ Below are the example static manifests used for creating required resources for kind: ServiceAccount metadata: labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator name: cert-manager-istio-csr - namespace: istio-system + namespace: cert-manager ``` 3. ClusterRoles and Roles required by istio-csr. @@ -427,26 +625,37 @@ Below are the example static manifests used for creating required resources for kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: cert-manager-istio-csr labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator + name: cert-manager-istio-csr rules: - - apiGroups: - - "" - resources: - - "configmaps" - verbs: ["get", "list", "create", "update", "watch"] - - apiGroups: - - "" - resources: - - "namespaces" - verbs: ["get", "list", "watch"] - - apiGroups: - - "authentication.k8s.io" - resources: - - "tokenreviews" - verbs: - - "create" + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - create + - update + - watch + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create ``` ```yaml @@ -454,16 +663,19 @@ Below are the example static manifests used for creating required resources for apiVersion: rbac.authorization.k8s.io/v1 metadata: labels: - app: cert-manager-istio-csr - generateName: cert-manager-istio-csr- + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator + name: cert-manager-istio-csr roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cert-manager-istio-csr subjects: - - kind: ServiceAccount - name: cert-manager-istio-csr - namespace: istio-system + - kind: ServiceAccount + name: cert-manager-istio-csr + namespace: cert-manager ``` ```yaml @@ -471,25 +683,31 @@ Below are the example static manifests used for creating required resources for kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: + labels: + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator name: cert-manager-istio-csr namespace: istio-system - labels: - app: cert-manager-istio-csr rules: - - apiGroups: - - "cert-manager.io" - resources: - - "certificaterequests" - verbs: - - "get" - - "list" - - "create" - - "update" - - "delete" - - "watch" - - apiGroups: [""] - resources: ["events"] - verbs: ["create"] + - apiGroups: + - cert-manager.io + resources: + - certificaterequests + verbs: + - get + - list + - create + - update + - delete + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create ``` ```yaml @@ -499,15 +717,18 @@ Below are the example static manifests used for creating required resources for name: cert-manager-istio-csr namespace: istio-system labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: cert-manager-istio-csr subjects: - - kind: ServiceAccount - name: cert-manager-istio-csr - namespace: istio-system + - kind: ServiceAccount + name: cert-manager-istio-csr + namespace: cert-manager ``` ```yaml @@ -515,20 +736,29 @@ Below are the example static manifests used for creating required resources for apiVersion: rbac.authorization.k8s.io/v1 metadata: labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator name: cert-manager-istio-csr-leases namespace: istio-system rules: - - apiGroups: - - "coordination.k8s.io" - resources: - - "leases" - verbs: - - "get" - - "create" - - "update" - - "watch" - - "list" + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - create + - update + - watch + - list + - apiGroups: + - "" + resources: + - events + verbs: + - create ``` ```yaml @@ -538,15 +768,18 @@ Below are the example static manifests used for creating required resources for name: cert-manager-istio-csr-leases namespace: istio-system labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: cert-manager-istio-csr-leases subjects: - - kind: ServiceAccount - name: cert-manager-istio-csr - namespace: istio-system + - kind: ServiceAccount + name: cert-manager-istio-csr + namespace: cert-manager ``` 4. Certificate required by the istiod. @@ -558,13 +791,16 @@ Below are the example static manifests used for creating required resources for name: istiod namespace: istio-system labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator spec: commonName: istiod.istio-system.svc dnsNames: - - istiod-basic.istio-system.svc + - istiod.istio-system.svc uris: - - spiffe://cluster.local/ns/istio-system/sa/istiod-service-account + - spiffe://cluster.local/ns/istio-system/sa/istiod-service-account secretName: istiod-tls duration: 1h renewBefore: 30m @@ -574,7 +810,7 @@ Below are the example static manifests used for creating required resources for size: 2048 revisionHistoryLimit: 1 issuerRef: - name: istio-csr-issuer + name: istio-ca kind: Issuer group: cert-manager.io ``` @@ -585,9 +821,12 @@ Below are the example static manifests used for creating required resources for kind: Deployment metadata: name: cert-manager-istio-csr - namespace: istio-system + namespace: cert-manager labels: - app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 + app.kubernetes.io/managed-by: cert-manager-operator spec: replicas: 1 selector: @@ -597,66 +836,97 @@ Below are the example static manifests used for creating required resources for metadata: labels: app: cert-manager-istio-csr + app.kubernetes.io/name: cert-manager-istio-csr + app.kubernetes.io/instance: cert-manager-istio-csr + app.kubernetes.io/version: v0.14.2 spec: serviceAccountName: cert-manager-istio-csr + nodeSelector: + kubernetes.io/os: linux containers: - - name: cert-manager-istio-csr - image: "quay.io/jetstack/cert-manager-istio-csr:v0.7.1" - imagePullPolicy: IfNotPresent - ports: - - containerPort: 6443 - - containerPort: 9402 - readinessProbe: - httpGet: - port: 6060 - path: /readyz - initialDelaySeconds: 3 - periodSeconds: 7 - command: ["cert-manager-istio-csr"] - args: - - "--log-level=1" - - "--metrics-port=9402" - - "--readiness-probe-path=/readyz" - - "--readiness-probe-port=6060" - - # cert-manager - - "--certificate-namespace=istio-system" - - "--issuer-group=cert-manager.io" - - "--issuer-kind=Issuer" - - "--issuer-name=istio-csr-issuer" - - "--preserve-certificate-requests=false" - - # AdditionalAnnotations - - # tls - - "--root-ca-file=/var/run/secrets/istio-csr/ca.crt" - - "--serving-certificate-dns-names=cert-manager-istio-csr.istio-system.svc" - - "--serving-certificate-duration=1h" - - "--serving-certificate-key-size=2048" - - "--serving-signature-algorithm=RSA" - - "--trust-domain=cluster.local" - - # server - - "--max-client-certificate-duration=1h" - - "--serving-address=0.0.0.0:6443" - - # controller - - "--configmap-namespace-selector=maistra.io/member-of=istio-system" - - "--leader-election-namespace=istio-system" - volumeMounts: - - mountPath: /var/run/secrets/istio-csr - name: root-ca - volumes: - - name: root-ca - secret: - secretName: istio-csr-ca + - name: cert-manager-istio-csr + image: quay.io/jetstack/cert-manager-istio-csr:v0.14.2 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6443 + - containerPort: 9402 + readinessProbe: + httpGet: + port: 6060 + path: /readyz + initialDelaySeconds: 3 + periodSeconds: 7 + args: + - --log-level=1 + - --log-format=text + - --metrics-port=9402 + - --readiness-probe-port=6060 + - --readiness-probe-path=/readyz + - --certificate-namespace=istio-system + - --issuer-enabled=true + - --issuer-name=istio-ca + - --issuer-kind=Issuer + - --issuer-group=cert-manager.io + - --preserve-certificate-requests=false + - --root-ca-file= + - --serving-certificate-dns-names=cert-manager-istio-csr.cert-manager.svc + - --serving-certificate-duration=1h + - --trust-domain=cluster.local + - --cluster-id=Kubernetes + - --max-client-certificate-duration=1h + - --serving-address=0.0.0.0:6443 + - --serving-certificate-key-size=2048 + - --serving-signature-algorithm=RSA + - --enable-client-cert-authenticator=false + - --leader-election-namespace=istio-system + - --disable-kubernetes-client-rate-limiter=false + - --runtime-issuance-config-map-name= + - --runtime-issuance-config-map-namespace=cert-manager + - --istiod-cert-enabled=false + - --istiod-cert-name=istiod-dynamic + - --istiod-cert-namespace=istio-system + - --istiod-cert-duration=1h + - --istiod-cert-renew-before=30m + - --istiod-cert-key-algorithm=RSA + - --istiod-cert-key-size=2048 + - --istiod-cert-additional-dns-names= + - --istiod-cert-istio-revisions=default + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault ``` ### Risks and Mitigations -An OpenShift administrator configuring `istiocsr.operator.openshift.io` CR object could configure insecure certificate +- An OpenShift administrator configuring `istiocsr.operator.openshift.io` CR object could configure insecure certificate signature algorithm, certificate key size or certificate validity to be too long which could cause vulnerability. - These configurations could be validated and can be overridden with default values. +- `istio-csr` agents creates the configmap `istio-ca-root-cert` with the CA certificate used for signing the istio server + certificates. And istio also creates the same configmap when it's configured to obtain certificates from `istio-csr`. Due + to both `istio` and `istio-csr` creating the configmap with same name for same purpose below issues can occur + - When multiple istio instance are installed, and not all configured to use `istio-csr` for certificate needs, conflict + will happen with both trying to update the CA certificate configmaps. + - Mitigation is to make use the namespace filtering `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` in `istiocsrs.operator.openshift.io` + and the equivalent in `istio` to avoid the conflict. When using `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` + one should be mindful to label the namespaces to not overlap with multiple instances of istio. +- When the `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is updated or added later after `istiocsr.operator.openshift.io` creation, + and in this scenario where namespaces matched by the earlier selector are excluded, the `istio-ca-root-cert` ConfigMaps in these + excluded requires manual cleanup. +- If the `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is updated or added after `istiocsr.operator.openshift.io` creation, and + the revised selector no longer includes namespaces that were previously targeted, then the `istio-ca-root-cert` ConfigMaps within + those excluded namespaces will require manual cleanup. +- When user wants to delete `istiocsrs.operator.openshift.io` or uninstall cert-manager-operator, must make sure the `istio-csr` + service is not configured in any istio instances for certificate needs. If deleted without proper checks + - Fetching certificates for new members of mesh would fail, as the endpoint would not be available. + - The configmap `istio-ca-root-cert` would not be updated whenever the CA has been updated and would degrade + functionalities of multiple services part of the mesh. ### Drawbacks @@ -690,7 +960,10 @@ None ### Tech Preview -> GA -N/A. This feature is for Tech Preview, until decided for GA. +- Feature is enabled by default, with option to disable. +- Feature available for end-to-end usage. +- Complete end user documentation. +- UTs and e2e tests are present. ### Removing a deprecated feature @@ -704,7 +977,7 @@ On upgrade: with Service Mesh. - Enabling istio-csr when Service Mesh was already deployed before upgrade is not [supported](https://cert-manager.io/docs/usage/istio-csr/#installing-istio-csr-after-istio) but can be made possible by following certain steps. Please refer - `Operational Aspects of API Extensions` section for more details.. + `Operational Aspects of API Extensions` section for more details. ## Version Skew Strategy @@ -752,19 +1025,33 @@ Istio-csr will be supported for OpenShift Service Mesh Operator 2.4+, Istio v1.1 - Listing all the resources created for installing the `istio-csr` agent ```bash - oc get all -l "app=cert-manager-istio-csr,app.kubernetes.io/name=cert-manager-istio-csr" -A + oc get Certificates,ClusterRoles,ClusterRoleBindings,Deployments,Roles,RoleBindings,Services,ServiceAccounts -l "app=cert-manager-istio-csr" -n ``` +- Listing all the `OpenShift Service Mesh` instances configured to use `istio-csr` for certificate needs + - `OpenShift Service Mesh v2` + ```bash + oc get servicemeshcontrolplanes.maistra.io -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name --no-headers | while read namespace name; do value=$(oc get servicemeshcontrolplanes.maistra.io $name -n $namespace -o jsonpath='{.spec.security.certificateAuthority.cert-manager.address}'); echo "servicemeshcontrolplanes.maistra.io $namespace/$name - $value"; done + ``` + - `OpenShift Service Mesh v3` + ```bash + for resource in $(oc get istios.sailoperator.io -o name -A); do value=$(oc get $resource -o jsonpath='{.spec.values.global.caAddress}'); if [[ -n "$value" ]]; then echo "$resource - $value"; fi; done; + ``` + ## Support Procedures None ## Alternatives (Not Implemented) -Instead of having `cert-manager-operator` manage `istio-csr`, having `istio-csr` itself as a product was +- Instead of having `cert-manager-operator` manage `istio-csr`, having `istio-csr` itself as a product was considered. But since `istio-csr` is an agent dependent on `cert-manager` for obtaining the certificates and the configurations supported are specific to service mesh, approach described in this proposal was considered to be logical. +- When `istiocsr.operator.openshift.io` is deleted instead of asking user to manually check if any istiod +is configured to use `istio-csr` agent for certificate needs, option to use validatingwebhook was +considered. But since this would add dependency on the operator managing istiod, instead the OpenShift docs +will have the steps to check the configuration and decision to proceed is left to user. ## Infrastructure Needed [optional] diff --git a/enhancements/cert-manager/istio-csr-delete.png b/enhancements/cert-manager/istio-csr-delete.png index d5ea211bd715cba32207df26f988b2d15abc59da..a4ad45b3ec1bd65a4c70eb072adb7bf38fdcd463 100644 GIT binary patch literal 17164 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 literal 20365 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 diff --git a/enhancements/cert-manager/istio-csr-delete.puml b/enhancements/cert-manager/istio-csr-delete.puml index 0f720ffa7a..457888d63c 100644 --- a/enhancements/cert-manager/istio-csr-delete.puml +++ b/enhancements/cert-manager/istio-csr-delete.puml @@ -12,8 +12,8 @@ participant "cert-manager-operator" as Operator User -> API : Delete istiocsr CR API -> Operator : Reconcile istiocsr delete event note over Operator -stop managing resources created +deletes all resources created for istio-csr agent installation. endnote -@enduml \ No newline at end of file +@enduml From b4b2e89d33570cde04c6d75f646f9ac368886940 Mon Sep 17 00:00:00 2001 From: Bharath B Date: Tue, 16 Sep 2025 10:49:03 +0530 Subject: [PATCH 2/6] CM-706: incorporate review suggestions --- .../cert-manager/istio-csr-controller.md | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/enhancements/cert-manager/istio-csr-controller.md b/enhancements/cert-manager/istio-csr-controller.md index 63fc78d8a0..0c83c66822 100644 --- a/enhancements/cert-manager/istio-csr-controller.md +++ b/enhancements/cert-manager/istio-csr-controller.md @@ -9,7 +9,7 @@ approvers: api-approvers: - "@tgeer" ## approver for cert-manager component creation-date: 2024-01-22 -last-updated: 2025-09-05 +last-updated: 2025-09-11 tracking-link: - https://issues.redhat.com/browse/CM-234 see-also: @@ -61,6 +61,8 @@ manager, requiring integration with OSSM so that certificate requests are signed is active and should be able to update the certificate endpoint to `istio-csr` agent endpoint. - As an OpenShift user, I want to have an option to dynamically enable monitoring for the `istio-csr` project and to use the OpenShift monitoring solution when required. +- As an OpenShift user, I want to limit istio-csr functionality to specific namespaces for better security and control. +- As an OpenShift user, I want to limit the istiod requests to specific clusters by configuring the cluster name. ### Goals @@ -217,16 +219,21 @@ type IstioCSRSpec struct { // +optional ControllerConfig *ControllerConfig `json:"controllerConfig,omitempty"` - // cleanupOnDeletion indicates that the operator should remove all resources + // cleanupOnDeletion indicates whether the operator should remove all resources // created for the istio-csr agent installation, including the cert-manager `Certificate` // resource created for obtaining a certificate for istiod using the configured issuer. // This field is immutable once set. + // PurgeAll: Removes all resources created `istio-csr` agent installation. + // PurgeNone: Disables reconciliation of the resource created for `istio-csr`. + // The istio-csr agent resources will remain in their current state and will not be managed by the operator. + // PurgeExceptCertificates: Disables reconciliation of the resource created for `istio-csr` and removes + // all resources created for `istio-csr` agent installation except `Certificate` resource created for istiod. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="cleanupOnDeletion is immutable once set" - // +kubebuilder:validation:Enum:="true";"false" - // +kubebuilder:default:="false" + // +kubebuilder:validation:Enum:=PurgeAll;PurgeNone;PurgeExceptCertificates + // +kubebuilder:default:=PurgeNone // +kubebuilder:validation:Optional // +optional - CleanupOnDeletion string `json:"cleanupOnDeletion,omitempty"` + CleanupOnDeletion PurgePolicy `json:"cleanupOnDeletion,omitempty"` } // IstioCSRConfig configures the istio-csr agent's behavior. @@ -298,7 +305,7 @@ type IstioCSRConfig struct { // ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ // +listType=atomic // +kubebuilder:validation:MinItems:=0 - // +kubebuilder:validation:MaxItems:=10 + // +kubebuilder:validation:MaxItems:=50 // +kubebuilder:validation:Optional // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` @@ -308,7 +315,7 @@ type IstioCSRConfig struct { // ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ // +mapType=atomic // +kubebuilder:validation:MinProperties:=0 - // +kubebuilder:validation:MaxProperties:=10 + // +kubebuilder:validation:MaxProperties:=50 // +kubebuilder:validation:Optional // +optional NodeSelector map[string]string `json:"nodeSelector,omitempty"` @@ -352,6 +359,7 @@ type IstiodTLSConfig struct { // the SPIFFE URI. trustDomain must not exceed 63 characters. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=63 + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="a lowercase RFC 1123 subdomain must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." // +kubebuilder:validation:Required // +required TrustDomain string `json:"trustDomain"` @@ -360,6 +368,8 @@ type IstiodTLSConfig struct { // This field can have a maximum of 25 entries. // +kubebuilder:validation:MinItems:=0 // +kubebuilder:validation:MaxItems:=25 + // +kubebuilder:validation:items:MinLength:=1 + // +kubebuilder:validation:items:MaxLength:=253 // +listType=set // +kubebuilder:validation:Optional // +optional @@ -411,6 +421,7 @@ type ServerConfig struct { // +kubebuilder:default:="Kubernetes" // +kubebuilder:validation:MinLength:=0 // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="a lowercase RFC 1123 subdomain must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." // +kubebuilder:validation:Optional // +optional ClusterID string `json:"clusterID,omitempty"` @@ -429,12 +440,13 @@ type ServerConfig struct { type IstioConfig struct { // revisions are the Istio revisions that are currently installed in the cluster. // Changing this field will modify the DNS names that will be requested for the - // istiod certificate. This field is immutable once set and can have a maximum of 10 entries. + // istiod certificate. This field can have a maximum of 10 entries. // +listType=set // +kubebuilder:default:={"default"} - // +kubebuilder:validation:XValidation:rule="self.all(x, x in oldSelf) && oldSelf.all(x, x in self)",message="revisions is immutable once set" // +kubebuilder:validation:MinItems=0 // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:items:MinLength:=1 + // +kubebuilder:validation:items:MaxLength:=63 // +kubebuilder:validation:Optional // +optional Revisions []string `json:"revisions,omitempty"` @@ -548,14 +560,17 @@ based on user feedback. helps in filtering the namespaces. Please refer `Risks and Mitigations` section for other details. - `spec.istioCSRConfig.certManager.istioCACertificate` - In the TP release, operator was extracting the CA certificate from the configured self-signed issuer and was making it available in the container using volumes. But for issuer types - like vault, venafi, the CA certificate used is not available in issuer. `istioCACertificate` allows configuring the configmap + like vault, venafi, the CA certificate used is not available in the issuer. `istioCACertificate` allows configuring the configmap which contains the CA certificate used for signing the istiod server certificates, and it will be made available to `istio-csr` using volumes, after basic validations like certificate is indeed CA and the CA attribute in certificates `Basic Constraints` section it set to true and was used for signing the istiod server certificate. When `istioCACertificate` is not provided operator instead of looking in the configured `Issuer/ClusterIssuer` will instead look in the secret containing the certificates obtained from cert-manager for istiod server. The `Certificate` object having the specifics of the istiod - server certificate is managed by operator. Please refer `Risks and Mitigations` section for other details. - Use case reference: https://issues.redhat.com/browse/CM-564 + server certificate is managed by operator. The operator will not directly mount the configured ConfigMap, but instead + will create a copy of its own and manage it by watching the both the ConfigMaps. This is required because, whenever the + ConfigMaps are updated, the changes get automatically propagated to the pod. And by operator having a copy of its own, can + validate any changes on the ConfigMap before making it available to the `istio-csr` agent. Please refer `Risks and Mitigations` + section for other details. Use case reference: https://issues.redhat.com/browse/CM-564 - `spec.istioCSRConfig.server.clusterID` - The `clusterID` field is used to configure the Istiod cluster ID. This ensures that only certificate signing requests (CSRs) from the matching Istio control plane are allowed. @@ -568,9 +583,11 @@ no defined standard. And can be updated in future based on user feedback. where `isito-csr` should create the config containing the istiod CA certificate. An upper limit of `4096` is fixed allowing to configure up to 10 namespaces considering the limitations on the label selectors and when equality operators are used. - `spec.istioCSRConfig.tolerations` and `spec.istioCSRConfig.nodeSelector` fields are allowed to max of `10` entries. -- `spec.istioCSRConfig.istio.revisions` field has upper bound limit of `10`. +- `spec.istioCSRConfig.istio.revisions` field has upper bound limit of `10` and each revision name can be of maximum `63` characters. - `spec.controllerConfig.labels` field allows to configure a maximum of `20` labels to be attached to all the resources created by the operator to deploy `istio-csr` agent. +- `spec.istioCSRConfig.istiodTLSConfig.certificateDNSNames` field allows to configure a maximum of `25` DNS names in the SAN section + of the Certificate, and each DNS Name can have a maximum of `253` characters. #### Operator uninstallation or `istiocsrs.operator.openshift.io` instance deletion. @@ -927,6 +944,9 @@ signature algorithm, certificate key size or certificate validity to be too long - Fetching certificates for new members of mesh would fail, as the endpoint would not be available. - The configmap `istio-ca-root-cert` would not be updated whenever the CA has been updated and would degrade functionalities of multiple services part of the mesh. +- Operator creates a copy of ConfigMap assigned to `spec.istioCSRConfig.certManager.istioCACertificate` to validate the content before + making it available to the `istio-csr` agent, since ConfigMap changes gets automatically propagated to the pods. But if the user + modifies both the created and operator copy, this goes unnoticed and could cause istiod to be in degraded state. ### Drawbacks From 06c1358ddd86dc9fed9f57f571420849969b06d0 Mon Sep 17 00:00:00 2001 From: Bharath B Date: Thu, 18 Sep 2025 20:37:58 +0530 Subject: [PATCH 3/6] CM-706: incorporate review suggestions on user stories --- .../cert-manager/istio-csr-controller.md | 122 ++++++++---------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/enhancements/cert-manager/istio-csr-controller.md b/enhancements/cert-manager/istio-csr-controller.md index 0c83c66822..a5ed3091d0 100644 --- a/enhancements/cert-manager/istio-csr-controller.md +++ b/enhancements/cert-manager/istio-csr-controller.md @@ -9,7 +9,7 @@ approvers: api-approvers: - "@tgeer" ## approver for cert-manager component creation-date: 2024-01-22 -last-updated: 2025-09-11 +last-updated: 2025-09-18 tracking-link: - https://issues.redhat.com/browse/CM-234 see-also: @@ -51,18 +51,23 @@ manager, requiring integration with OSSM so that certificate requests are signed ### User Stories -- As an OpenShift user, I want to have an option to dynamically deploy `istio-csr` agent, so that it can be used only - when required by creating the custom resource. -- As an OpenShift user, I want to have an option to dynamically configure `istio-csr` agent, so that only the required - features can be enabled by updating the custom resource. -- As an OpenShift user, I should be able to remove `istio-csr` agent when not required by removing the custom resource, - and controller should clean up all resources created for the `istio-csr` agent deployment. -- As an OpenShift user, I should be able to install `istio-csr` agent in the upgrade cluster where istiod control plane - is active and should be able to update the certificate endpoint to `istio-csr` agent endpoint. -- As an OpenShift user, I want to have an option to dynamically enable monitoring for the `istio-csr` project and - to use the OpenShift monitoring solution when required. -- As an OpenShift user, I want to limit istio-csr functionality to specific namespaces for better security and control. -- As an OpenShift user, I want to limit the istiod requests to specific clusters by configuring the cluster name. +- As an OpenShift administrator, I want to have an option to deploy istio-csr agent, so that it can be enabled as a day2 operation. +- As an OpenShift administrator, I want to be able to configure istio-csr agent, so that only required +features can be enabled. +- As an OpenShift administrator, I should be able to uninstall istio-csr agent when not required as a day2 operation without disrupting + the cert-manager installation. +- As an OpenShift administrator, I should be able to choose to keep secrets and related controller should clean up all resources created + for the istio-csr agent deployment. +- As an OpenShift security engineer, I want to be able to identify all artefacts created by istio-csr agent, for better auditability. +- As an OpensShift SRE, I should be able to get details information as part of different status conditions and messages to identify the + reasons of failures and carry out corrective actions successfully. +- As an OpenShift service mesh administrator, I should be able to use istio-csr endpoint to automate certificate requests via istiod on + the pre-installed service mesh clusters. +- As an OpenShift SRE, I should be able to collect metrics for istio-csr for monitoring. +- As an OpenShift security engineer, I want to restrict istio-csr operation to only member namespaces of the Service Mesh, to prevent the + ConfigMap with the `istiod` CA certificates from being unnecessarily provisioned in namespaces outside the mesh. +- As an OpenShift administrator, I want to configure istio cluster id that can be verified against the csr to avoid misconfiguration or + infer any default value ### Goals @@ -73,6 +78,12 @@ manager, requiring integration with OSSM so that certificate requests are signed - `istio-csr` agent can be used only with supported version of `OpenShift Service Mesh`. Please refer `Version Skew Strategy` section for more details. +- Removing `istiocsr.operator.openshift.io` CR object will not remove `istio-csr` agent deployment. But will only stop the reconciliation + of the Kubernetes resources created for the operand installation. (Note: This is a limitation for GA release and will be + re-evaluated in future releases). +- As an OpenShift security engineer, I want an automatic deletion of istio-ca-root-cert configmap from a selected namespace. +- As an OpenShift administrator, I want the namespaces chosen for istio-ca-root-cert configmap injection to validated against + service mesh configuration to avoid drift in the namespace selection. ## Proposal @@ -179,12 +190,10 @@ type IstioCSRList struct { // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:metadata:labels={"app.kubernetes.io/name=istiocsr", "app.kubernetes.io/part-of=cert-manager-operator"} -// IstioCSR describes the configuration and information about the managed istio-csr -// agent. The name must be `default` to make IstioCSR a singleton that is, to -// allow only one instance of IstioCSR per namespace. +// IstioCSR describes the configuration and information about the managed istio-csr agent. +// The name must be `default` to make IstioCSR a singleton that is, to allow only one instance of IstioCSR per namespace. // -// When an IstioCSR is created, istio-csr agent is deployed in the IstioCSR -// created namespace. +// When an IstioCSR is created, istio-csr agent is deployed in the IstioCSR created namespace. // // +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="istiocsr is a singleton, .metadata.name must be 'default'" // +operator-sdk:csv:customresourcedefinitions:displayName="IstioCSR" @@ -213,27 +222,10 @@ type IstioCSRSpec struct { // +required IstioCSRConfig IstioCSRConfig `json:"istioCSRConfig"` - // controllerConfig configures the controller for setting up defaults to - // enable the istio-csr agent. + // controllerConfig configures the controller for setting up defaults to enable the istio-csr agent. // +kubebuilder:validation:Optional // +optional ControllerConfig *ControllerConfig `json:"controllerConfig,omitempty"` - - // cleanupOnDeletion indicates whether the operator should remove all resources - // created for the istio-csr agent installation, including the cert-manager `Certificate` - // resource created for obtaining a certificate for istiod using the configured issuer. - // This field is immutable once set. - // PurgeAll: Removes all resources created `istio-csr` agent installation. - // PurgeNone: Disables reconciliation of the resource created for `istio-csr`. - // The istio-csr agent resources will remain in their current state and will not be managed by the operator. - // PurgeExceptCertificates: Disables reconciliation of the resource created for `istio-csr` and removes - // all resources created for `istio-csr` agent installation except `Certificate` resource created for istiod. - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="cleanupOnDeletion is immutable once set" - // +kubebuilder:validation:Enum:=PurgeAll;PurgeNone;PurgeExceptCertificates - // +kubebuilder:default:=PurgeNone - // +kubebuilder:validation:Optional - // +optional - CleanupOnDeletion PurgePolicy `json:"cleanupOnDeletion,omitempty"` } // IstioCSRConfig configures the istio-csr agent's behavior. @@ -254,11 +246,9 @@ type IstioCSRConfig struct { // +optional LogFormat string `json:"logFormat,omitempty"` - // Istio-csr creates a ConfigMap named `istio-ca-root-cert` containing the root CA certificate, which the Istio data - // plane uses to verify server certificates. Its default behavior is to create and monitor ConfigMaps in all namespaces. - // The istioDataPlaneNamespaceSelector restricts the namespaces where the ConfigMap is created by using label selectors, - // such as maistra.io/member-of=istio-system. This selector is also attached to all desired namespaces that are part of - // the data plane. istioDataPlaneNamespaceSelector must not exceed 4096 characters. + // Istio-csr creates a ConfigMap named `istio-ca-root-cert` containing the root CA certificate, which the Istio data plane uses to verify server certificates. Its default behavior is to create and monitor ConfigMaps in all namespaces. + // The istioDataPlaneNamespaceSelector restricts the namespaces where the ConfigMap is created by using label selectors, such as maistra.io/member-of=istio-system. This selector is also attached to all desired namespaces that are part of the data plane. + // This field can have a maximum of 4096 characters. // +kubebuilder:example:="maistra.io/member-of=istio-system" // +kubebuilder:validation:MinLength:=0 // +kubebuilder:validation:MaxLength:=4096 @@ -276,8 +266,7 @@ type IstioCSRConfig struct { // +required IstiodTLSConfig IstiodTLSConfig `json:"istiodTLSConfig"` - // server is for configuring the server endpoint used by istio - // for obtaining the certificates. + // server is for configuring the server endpoint used by istio for obtaining the certificates. // +kubebuilder:validation:Optional // +optional Server *ServerConfig `json:"server,omitempty"` @@ -301,7 +290,7 @@ type IstioCSRConfig struct { Affinity *corev1.Affinity `json:"affinity,omitempty"` // tolerations is for setting the pod tolerations. - // tolerations can have a maximum of 10 entries. + // This field can have a maximum of 50 entries. // ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ // +listType=atomic // +kubebuilder:validation:MinItems:=0 @@ -311,7 +300,7 @@ type IstioCSRConfig struct { Tolerations []corev1.Toleration `json:"tolerations,omitempty"` // nodeSelector is for defining the scheduling criteria using node labels. - // nodeSelector can have a maximum of 10 entries. + // This field can have a maximum of 50 entries. // ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ // +mapType=atomic // +kubebuilder:validation:MinProperties:=0 @@ -324,8 +313,8 @@ type IstioCSRConfig struct { // CertManagerConfig is for configuring cert-manager specifics. // +kubebuilder:validation:XValidation:rule="!has(oldSelf.issuerRef) && !has(self.issuerRef) || has(oldSelf.issuerRef) && has(self.issuerRef)",message="issuerRef may only be configured during creation" type CertManagerConfig struct { - // issuerRef contains details of the referenced object used for obtaining certificates. When - // `issuerRef.Kind` is `Issuer`, it must exist in the `.spec.istioCSRConfig.istio.namespace`. + // issuerRef contains details of the referenced object used for obtaining certificates. + // When `issuerRef.Kind` is `Issuer`, it must exist in the `.spec.istioCSRConfig.istio.namespace`. // This field is immutable once set. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="issuerRef is immutable once set" // +kubebuilder:validation:XValidation:rule="self.kind.lowerAscii() == 'issuer' || self.kind.lowerAscii() == 'clusterissuer'",message="kind must be either 'Issuer' or 'ClusterIssuer'" @@ -334,8 +323,8 @@ type CertManagerConfig struct { // +required IssuerRef certmanagerv1.ObjectReference `json:"issuerRef"` - // istioCACertificate when provided, the operator will use the CA certificate from the specified ConfigMap. If empty, the operator will - // automatically extract the CA certificate from the Secret containing the istiod certificate obtained from cert-manager. + // istioCACertificate when provided, the operator will use the CA certificate from the specified ConfigMap. + // If empty, the operator will automatically extract the CA certificate from the Secret containing the istiod certificate obtained from cert-manager. // +kubebuilder:validation:Optional // +optional IstioCACertificate *ConfigMapReference `json:"istioCACertificate,omitempty"` @@ -347,7 +336,7 @@ type CertManagerConfig struct { type IstiodTLSConfig struct { // commonName is the common name to be set in the cert-manager.io Certificate created for istiod. // The commonName will be of the form istiod..svc when not set. - // The commonName must not exceed 64 characters. + // This field can have a maximum of 64 characters. // +kubebuilder:validation:MinLength:=0 // +kubebuilder:validation:MaxLength:=64 // +kubebuilder:example:="istiod.istio-system.svc" @@ -355,8 +344,8 @@ type IstiodTLSConfig struct { // +optional CommonName string `json:"commonName,omitempty"` - // trustDomain is the Istio cluster's trust domain, which will also be used for deriving - // the SPIFFE URI. trustDomain must not exceed 63 characters. + // trustDomain is the Istio cluster's trust domain, which will also be used for deriving the SPIFFE URI. + // This field can have a maximum of 63 characters. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=63 // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="a lowercase RFC 1123 subdomain must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." @@ -388,8 +377,8 @@ type IstiodTLSConfig struct { // +optional CertificateRenewBefore *metav1.Duration `json:"certificateRenewBefore,omitempty"` - // privateKeySize is the key size for the istio-csr and istiod certificates. Allowed values when privateKeyAlgorithm - // is RSA are 2048, 4096, 8192; and for ECDSA, they are 256, 384. This field is immutable once set. + // privateKeySize is the key size for the istio-csr and istiod certificates. Allowed values when privateKeyAlgorithm is RSA are 2048, 4096, 8192; and for ECDSA, they are 256, 384. + // This field is immutable once set. // +kubebuilder:validation:Enum:=256;384;2048;4096;8192 // +kubebuilder:default:=2048 // +kubebuilder:validation:XValidation:rule="oldSelf == 0 || self == oldSelf",message="privateKeySize is immutable once set" @@ -406,8 +395,7 @@ type IstiodTLSConfig struct { // +optional PrivateKeyAlgorithm string `json:"privateKeyAlgorithm,omitempty"` - // MaxCertificateDuration is the maximum validity duration that can be - // requested for a certificate. + // MaxCertificateDuration is the maximum validity duration that can be requested for a certificate. // +kubebuilder:default:="1h" // +kubebuilder:validation:Optional // +optional @@ -439,12 +427,12 @@ type ServerConfig struct { // IstioConfig is for configuring the istio specifics. type IstioConfig struct { // revisions are the Istio revisions that are currently installed in the cluster. - // Changing this field will modify the DNS names that will be requested for the - // istiod certificate. This field can have a maximum of 10 entries. + // Changing this field will modify the DNS names that will be requested for the istiod certificate. + // This field can have a maximum of 25 entries. // +listType=set // +kubebuilder:default:={"default"} // +kubebuilder:validation:MinItems=0 - // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:MaxItems=25 // +kubebuilder:validation:items:MinLength:=1 // +kubebuilder:validation:items:MaxLength:=63 // +kubebuilder:validation:Optional @@ -452,6 +440,7 @@ type IstioConfig struct { Revisions []string `json:"revisions,omitempty"` // namespace of the Istio control plane. + // This field can have a maximum of 253 characters. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="namespace is immutable once set" // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=253 @@ -481,8 +470,7 @@ type IstioCSRStatus struct { // istioCSRImage is the name of the image and the tag used for deploying istio-csr. IstioCSRImage string `json:"istioCSRImage,omitempty"` - // istioCSRGRPCEndpoint is the service endpoint of istio-csr, made available for users - // to configure in the istiod config to enable Istio to use istio-csr for certificate requests. + // istioCSRGRPCEndpoint is the service endpoint of istio-csr, made available for users to configure in the istiod config to enable Istio to use istio-csr for certificate requests. IstioCSRGRPCEndpoint string `json:"istioCSRGRPCEndpoint,omitempty"` // serviceAccount created by the controller for the istio-csr agent. @@ -582,17 +570,13 @@ no defined standard. And can be updated in future based on user feedback. - `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is for setting a label selector to filter the namespaces where `isito-csr` should create the config containing the istiod CA certificate. An upper limit of `4096` is fixed allowing to configure up to 10 namespaces considering the limitations on the label selectors and when equality operators are used. -- `spec.istioCSRConfig.tolerations` and `spec.istioCSRConfig.nodeSelector` fields are allowed to max of `10` entries. -- `spec.istioCSRConfig.istio.revisions` field has upper bound limit of `10` and each revision name can be of maximum `63` characters. +- `spec.istioCSRConfig.tolerations` and `spec.istioCSRConfig.nodeSelector` fields are allowed to max of `50` entries. +- `spec.istioCSRConfig.istio.revisions` field has upper bound limit of `25` and each revision name can be of maximum `63` characters. - `spec.controllerConfig.labels` field allows to configure a maximum of `20` labels to be attached to all the resources created by the operator to deploy `istio-csr` agent. - `spec.istioCSRConfig.istiodTLSConfig.certificateDNSNames` field allows to configure a maximum of `25` DNS names in the SAN section of the Certificate, and each DNS Name can have a maximum of `253` characters. -#### Operator uninstallation or `istiocsrs.operator.openshift.io` instance deletion. - -Operator will remove all the resources created for installing `istio-csr` agent when `spec.cleanupOnDeletion` is enabled. - #### Manifests for installing `istio-csr` agent. Below are the example static manifests used for creating required resources for installing `istio-csr` agent. @@ -939,8 +923,8 @@ signature algorithm, certificate key size or certificate validity to be too long - If the `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is updated or added after `istiocsr.operator.openshift.io` creation, and the revised selector no longer includes namespaces that were previously targeted, then the `istio-ca-root-cert` ConfigMaps within those excluded namespaces will require manual cleanup. -- When user wants to delete `istiocsrs.operator.openshift.io` or uninstall cert-manager-operator, must make sure the `istio-csr` - service is not configured in any istio instances for certificate needs. If deleted without proper checks +- When user wants to delete `istio-csr` agent deployment, must make sure the `istio-csr`service is not configured in any istio instances + for certificate needs. If deleted without proper checks - Fetching certificates for new members of mesh would fail, as the endpoint would not be available. - The configmap `istio-ca-root-cert` would not be updated whenever the CA has been updated and would degrade functionalities of multiple services part of the mesh. From 30d9c476c851514ec79c9cde248dc92c57e93173 Mon Sep 17 00:00:00 2001 From: Bharath B Date: Wed, 24 Sep 2025 21:35:03 +0530 Subject: [PATCH 4/6] CM-706: updates API validations Signed-off-by: Bharath B --- .../cert-manager/istio-csr-controller.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/enhancements/cert-manager/istio-csr-controller.md b/enhancements/cert-manager/istio-csr-controller.md index a5ed3091d0..7b55af6e70 100644 --- a/enhancements/cert-manager/istio-csr-controller.md +++ b/enhancements/cert-manager/istio-csr-controller.md @@ -9,7 +9,7 @@ approvers: api-approvers: - "@tgeer" ## approver for cert-manager component creation-date: 2024-01-22 -last-updated: 2025-09-18 +last-updated: 2025-09-24 tracking-link: - https://issues.redhat.com/browse/CM-234 see-also: @@ -333,6 +333,7 @@ type CertManagerConfig struct { // IstiodTLSConfig is for configuring istiod certificate specifics. // +kubebuilder:validation:XValidation:rule="!has(oldSelf.privateKeyAlgorithm) && !has(self.privateKeyAlgorithm) || has(oldSelf.privateKeyAlgorithm) && has(self.privateKeyAlgorithm)",message="privateKeyAlgorithm may only be configured during creation" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.privateKeySize) && !has(self.privateKeySize) || has(oldSelf.privateKeySize) && has(self.privateKeySize)",message="privateKeySize may only be configured during creation" +// +kubebuilder:validation:XValidation:rule="(!has(self.privateKeyAlgorithm) || self.privateKeyAlgorithm == 'RSA') ? (self.privateKeySize in [2048,4096,8192]) : (self.privateKeySize in [256,384])",message="privateKeySize must match with configured privateKeyAlgorithm" type IstiodTLSConfig struct { // commonName is the common name to be set in the cert-manager.io Certificate created for istiod. // The commonName will be of the form istiod..svc when not set. @@ -348,7 +349,7 @@ type IstiodTLSConfig struct { // This field can have a maximum of 63 characters. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=63 - // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="a lowercase RFC 1123 subdomain must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="trustDomain must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." // +kubebuilder:validation:Required // +required TrustDomain string `json:"trustDomain"` @@ -406,10 +407,10 @@ type IstiodTLSConfig struct { // for obtaining the certificates. type ServerConfig struct { // clusterID is the Istio cluster ID used to verify incoming CSRs. + // This field can have a maximum of 253 characters. // +kubebuilder:default:="Kubernetes" // +kubebuilder:validation:MinLength:=0 // +kubebuilder:validation:MaxLength:=253 - // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="a lowercase RFC 1123 subdomain must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." // +kubebuilder:validation:Optional // +optional ClusterID string `json:"clusterID,omitempty"` @@ -440,10 +441,11 @@ type IstioConfig struct { Revisions []string `json:"revisions,omitempty"` // namespace of the Istio control plane. - // This field can have a maximum of 253 characters. + // This field can have a maximum of 63 characters. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="namespace is immutable once set" // +kubebuilder:validation:MinLength:=1 - // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:MaxLength:=63 + // +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="namespace must consist of only lowercase alphanumeric characters and hyphens, and must start with an alphabetic character and end with an alphanumeric character." // +kubebuilder:validation:Required // +required Namespace string `json:"namespace"` @@ -497,15 +499,15 @@ type ConfigMapReference struct { // name of the ConfigMap. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=253 - // +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + // +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="name must consist of lowercase alphanumeric characters, hyphens ('-'), and periods ('.'). Each block, separated by periods, must start and end with an alphanumeric character. Hyphens are not allowed at the start or end of a block, and consecutive periods are not permitted." // +kubebuilder:validation:Required // +required Name string `json:"name"` // namespace in which the ConfigMap exists. If empty, ConfigMap will be looked up in IstioCSR created namespace. - // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MinLength:=0 // +kubebuilder:validation:MaxLength:=63 - // +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + // +kubebuilder:validation:XValidation:rule=`size(self) == 0 || !format.dns1123Label().validate(self).hasValue()`,message="namespace must consist of only lowercase alphanumeric characters and hyphens, and must start with an alphabetic character and end with an alphanumeric character." // +kubebuilder:validation:Optional // +optional Namespace string `json:"namespace,omitempty"` @@ -576,6 +578,8 @@ no defined standard. And can be updated in future based on user feedback. by the operator to deploy `istio-csr` agent. - `spec.istioCSRConfig.istiodTLSConfig.certificateDNSNames` field allows to configure a maximum of `25` DNS names in the SAN section of the Certificate, and each DNS Name can have a maximum of `253` characters. +- `spec.istioCSRConfig.certManager.istioCACertificate.key` is for setting the key name to be looked up in the referenced ConfigMap + containing the CA certificates. An upper limit of `253` characters is fixed. #### Manifests for installing `istio-csr` agent. From 703803b3c6424c9cf8ac85f7ca0232277e7868c1 Mon Sep 17 00:00:00 2001 From: Bharath B Date: Fri, 26 Sep 2025 15:04:22 +0530 Subject: [PATCH 5/6] CM-706: updates review suggestions for non-goals and risk section --- .../cert-manager/istio-csr-controller.md | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/enhancements/cert-manager/istio-csr-controller.md b/enhancements/cert-manager/istio-csr-controller.md index 7b55af6e70..055a67e0e4 100644 --- a/enhancements/cert-manager/istio-csr-controller.md +++ b/enhancements/cert-manager/istio-csr-controller.md @@ -81,9 +81,12 @@ features can be enabled. - Removing `istiocsr.operator.openshift.io` CR object will not remove `istio-csr` agent deployment. But will only stop the reconciliation of the Kubernetes resources created for the operand installation. (Note: This is a limitation for GA release and will be re-evaluated in future releases). -- As an OpenShift security engineer, I want an automatic deletion of istio-ca-root-cert configmap from a selected namespace. -- As an OpenShift administrator, I want the namespaces chosen for istio-ca-root-cert configmap injection to validated against +- As an OpenShift security engineer, I want an automatic cleanup of `istio-ca-root-cert` configmap from the selected namespaces which + were previously part of the mesh. +- As an OpenShift administrator, I want the namespaces chosen for istio-ca-root-cert configmap injection to be validated against service mesh configuration to avoid drift in the namespace selection. +- As an OpenShift security engineer, I want to harden RBAC permissions of `istio-csr` agent to restrict ConfigMap creation permission + to only selected namespaces. ## Proposal @@ -193,7 +196,7 @@ type IstioCSRList struct { // IstioCSR describes the configuration and information about the managed istio-csr agent. // The name must be `default` to make IstioCSR a singleton that is, to allow only one instance of IstioCSR per namespace. // -// When an IstioCSR is created, istio-csr agent is deployed in the IstioCSR created namespace. +// When an IstioCSR is created, istio-csr agent is deployed in the IstioCSR-created namespace. // // +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="istiocsr is a singleton, .metadata.name must be 'default'" // +operator-sdk:csv:customresourcedefinitions:displayName="IstioCSR" @@ -372,7 +375,6 @@ type IstiodTLSConfig struct { CertificateDuration *metav1.Duration `json:"certificateDuration,omitempty"` // certificateRenewBefore is the time before expiry to renew the istio-csr and istiod certificates. - // before expiry. // +kubebuilder:default:="30m" // +kubebuilder:validation:Optional // +optional @@ -924,6 +926,13 @@ signature algorithm, certificate key size or certificate validity to be too long - When the `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is updated or added later after `istiocsr.operator.openshift.io` creation, and in this scenario where namespaces matched by the earlier selector are excluded, the `istio-ca-root-cert` ConfigMaps in these excluded requires manual cleanup. + Below command can be made use of to list all the ConfigMaps not part of `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` matching + namespaces. In the example, `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is having `maistra.io/member-of=istio-system` value. + ```shell + printf "%-25s %10s\n" "ConfigMap" "Namespace"; for ns in $(oc get namespaces -l "maistra.io/member-of!=istio-system" \ + -o=jsonpath='{.items[*].metadata.name}'); do oc get configmaps -l "istio.io/config=true" \ + -n $ns --no-headers -o jsonpath='{.items[*].metadata.name}{"\t"}{.items[*].metadata.namespace}{"\n"}'; done + ``` - If the `spec.istioCSRConfig.istioDataPlaneNamespaceSelector` is updated or added after `istiocsr.operator.openshift.io` creation, and the revised selector no longer includes namespaces that were previously targeted, then the `istio-ca-root-cert` ConfigMaps within those excluded namespaces will require manual cleanup. @@ -935,6 +944,9 @@ signature algorithm, certificate key size or certificate validity to be too long - Operator creates a copy of ConfigMap assigned to `spec.istioCSRConfig.certManager.istioCACertificate` to validate the content before making it available to the `istio-csr` agent, since ConfigMap changes gets automatically propagated to the pods. But if the user modifies both the created and operator copy, this goes unnoticed and could cause istiod to be in degraded state. +- The secret zero problem is inherent, i.e. the certificate key pairs issued by cert-manager is stored in Kubernetes + native `Secret` object which would need to be secured with additional encryption and fine-grained permissions. The `Secret` + object containing the `istiod` certificate key pair can be misused to request certificates from the `istio-csr` agent. ### Drawbacks From a7a7c39cfda357c57586893af356558365ce8a39 Mon Sep 17 00:00:00 2001 From: Bharath B Date: Fri, 26 Sep 2025 17:18:15 +0530 Subject: [PATCH 6/6] CM-706: incorporate review suggestion on resources cleanup Signed-off-by: Bharath B --- .../cert-manager/istio-csr-controller.md | 9 +++++++-- .../cert-manager/istio-csr-delete.png | Bin 17164 -> 9975 bytes .../cert-manager/istio-csr-delete.puml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/enhancements/cert-manager/istio-csr-controller.md b/enhancements/cert-manager/istio-csr-controller.md index 055a67e0e4..703bf16c86 100644 --- a/enhancements/cert-manager/istio-csr-controller.md +++ b/enhancements/cert-manager/istio-csr-controller.md @@ -59,7 +59,7 @@ features can be enabled. - As an OpenShift administrator, I should be able to choose to keep secrets and related controller should clean up all resources created for the istio-csr agent deployment. - As an OpenShift security engineer, I want to be able to identify all artefacts created by istio-csr agent, for better auditability. -- As an OpensShift SRE, I should be able to get details information as part of different status conditions and messages to identify the +- As an OpenShift SRE, I should be able to get details information as part of different status conditions and messages to identify the reasons of failures and carry out corrective actions successfully. - As an OpenShift service mesh administrator, I should be able to use istio-csr endpoint to automate certificate requests via istiod on the pre-installed service mesh clusters. @@ -142,6 +142,10 @@ CR, which is enforced in the CRD using CEL validations. - `.spec.istioCSRConfig.istio.revisions` - `.spec.istioCSRConfig.istio.namespace` +When an OpenShift user deletes `istiocsr.operator.openshift.io` CR object, `istio-csr-controller` will stop managing all +the resources created for installing `istio-csr` agent and user will have to manually clean up the resources. Please refer +`Operational Aspects of API Extensions` section for command to list all the resources. + `istiocsr.operator.openshift.io` CR status sub-resource will be used for updating the status of the `istio-csr` agent installation, any error encountered while creating the required resources or the reconciling the state. @@ -159,7 +163,8 @@ for better version management. - Uninstallation of `istio-csr` agent - An OpenShift user deletes the `istiocsr.operator.openshift.io` CR. - - `istio-csr-controller` will remove all the resources created for installing `istio-csr` agent. + - `istio-csr-controller` will not uninstall `istio-csr` agent, but will only stop reconciling the kubernetes resources + created for installing the agent. Please refer `Non-Goals` section for more details. ![alt text](./istio-csr-delete.png). diff --git a/enhancements/cert-manager/istio-csr-delete.png b/enhancements/cert-manager/istio-csr-delete.png index a4ad45b3ec1bd65a4c70eb072adb7bf38fdcd463..960dd10a2dcdf8d21615f8ce7dd455bfdf8b1bfa 100644 GIT binary patch literal 9975 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 IeKPX@U-pF literal 17164 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 diff --git a/enhancements/cert-manager/istio-csr-delete.puml b/enhancements/cert-manager/istio-csr-delete.puml index 457888d63c..f42fbd259c 100644 --- a/enhancements/cert-manager/istio-csr-delete.puml +++ b/enhancements/cert-manager/istio-csr-delete.puml @@ -12,7 +12,7 @@ participant "cert-manager-operator" as Operator User -> API : Delete istiocsr CR API -> Operator : Reconcile istiocsr delete event note over Operator -deletes all resources created +stop managing the resources created for istio-csr agent installation. endnote