diff --git a/CHANGELOG.md b/CHANGELOG.md index d973fadc..dd7aea9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Added CA expiry threshold support for filtering out CAs that are about to expire ([#633]). + - New `caExpiryThreshold` field in TrustStore CRD to filter out CAs close to expiry when publishing trust roots. + - New `secrets.stackable.tech/backend.autotls.ca.expiry-threshold` annotation for volume-level CA expiry filtering. + - If no threshold is specified, all CAs (including expired ones) are published for backwards compatibility. + ## [25.7.0] - 2025-07-23 ## [25.7.0-rc1] - 2025-07-18 diff --git a/deploy/helm/secret-operator/crds/crds.yaml b/deploy/helm/secret-operator/crds/crds.yaml index f04a9b8f..cf1a77c0 100644 --- a/deploy/helm/secret-operator/crds/crds.yaml +++ b/deploy/helm/secret-operator/crds/crds.yaml @@ -384,6 +384,10 @@ spec: The requested information is written to a ConfigMap with the same name as the TrustStore. properties: + caExpiryThreshold: + description: The minimum remaining lifetime that a CA must have to be considered valid for publishing in the TrustStore. If not specified, all CAs (including expired ones) will be published. + nullable: true + type: string format: description: The [format](https://docs.stackable.tech/home/nightly/secret-operator/secretclass#format) that the data should be converted into. enum: diff --git a/docs/modules/secret-operator/examples/truststore-tls.yaml b/docs/modules/secret-operator/examples/truststore-tls.yaml index b8239d8e..6c4ff751 100644 --- a/docs/modules/secret-operator/examples/truststore-tls.yaml +++ b/docs/modules/secret-operator/examples/truststore-tls.yaml @@ -6,3 +6,4 @@ metadata: spec: secretClassName: tls # <2> format: tls-pem # <3> + caExpiryThreshold: 1h # <4> diff --git a/docs/modules/secret-operator/pages/truststore.adoc b/docs/modules/secret-operator/pages/truststore.adoc index 0250b5f5..685f68ea 100644 --- a/docs/modules/secret-operator/pages/truststore.adoc +++ b/docs/modules/secret-operator/pages/truststore.adoc @@ -14,6 +14,7 @@ include::example$truststore-tls.yaml[] <1> Also used to name the created ConfigMap <2> The name of the xref:secretclass.adoc[] <3> The requested xref:secretclass.adoc#format[format] +<4> Optional threshold to filter out CAs that are about to expire. If not specified, all CAs (including expired ones) will be included. This will create a ConfigMap named `truststore-pem` containing a `ca.crt` with the trust root certificates. It can then either be mounted into a Pod or retrieved and used from outside of Kubernetes. diff --git a/docs/modules/secret-operator/pages/volume.adoc b/docs/modules/secret-operator/pages/volume.adoc index 38d7608e..78bcfd79 100644 --- a/docs/modules/secret-operator/pages/volume.adoc +++ b/docs/modules/secret-operator/pages/volume.adoc @@ -133,6 +133,25 @@ It can take some hours until all Pods are restarted in a rolling fashion. The format is documented in xref:concepts:duration.adoc[]. +=== `secrets.stackable.tech/backend.autotls.ca.expiry-threshold` + +*Required*: false + +*Default value*: Not set (all CAs including expired ones are published) + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +The minimum remaining lifetime that a CA must have to be considered valid. + +- A CA is only published if its remaining lifetime is greater than or equal to this threshold. +- A CA may only sign a certificate if it outlives the certificate by at least this threshold. + +If not specified, all CAs (including expired ones) will be published. Of course, only the CAs that are still valid will be used to sign certificates. + +Use this to avoid publishing almost-expired CA certificates that might expire during pod startup. + +The format is documented in xref:concepts:duration.adoc[]. + === `secrets.stackable.tech/backend.autotls.cert.jitter-factor` *Required*: false diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index 2c346056..34b1a9ba 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -120,6 +120,19 @@ pub struct SecretVolumeSelector { )] pub autotls_cert_restart_buffer: Duration, + /// The minimum remaining lifetime that a CA must have to be considered valid for + /// publishing and signing certificates. + /// A CA will only be published if it remains valid for at least this duration, + /// and it may only sign if it outlives the issued certificate by at least this duration. + /// If not specified, all CAs (including expired ones) will be published. + /// The format is documented in . + #[serde( + rename = "secrets.stackable.tech/backend.autotls.ca.expiry-threshold", + deserialize_with = "SecretVolumeSelector::deserialize_some", + default + )] + pub autotls_ca_expiry_threshold: Option, + /// The part of the certificate's lifetime that may be removed for jittering. /// Must be within 0.0 and 1.0. #[serde( @@ -144,6 +157,10 @@ pub struct SecretVolumeSelector { pub struct TrustSelector { /// The name of the [`TrustStore`]'s `Namespace`. pub namespace: String, + /// Optional CA expiry threshold for filtering out CAs that are about to expire. + /// If specified, only CAs that are valid for at least this duration will be published. + /// If not specified, all CAs (including expired ones) will be published. + pub ca_expiry_threshold: Option, } /// Internal parameters of [`SecretVolumeSelector`] managed by secret-operator itself. diff --git a/rust/operator-binary/src/backend/tls/ca.rs b/rust/operator-binary/src/backend/tls/ca.rs index e66b5612..e0e8d258 100644 --- a/rust/operator-binary/src/backend/tls/ca.rs +++ b/rust/operator-binary/src/backend/tls/ca.rs @@ -614,27 +614,49 @@ impl Manager { } /// Get an appropriate [`CertificateAuthority`] for signing a given certificate. + /// + /// The selected CA must outlive the certificate by at least `min_remaining_lifetime`. + /// If `min_remaining_lifetime` is None, any CA that outlives the certificate will be acceptable. pub fn find_certificate_authority_for_signing( &self, valid_until_at_least: OffsetDateTime, + min_remaining_lifetime: Option, ) -> Result<&CertificateAuthority, GetCaError> { use get_ca_error::*; + + let cutoff = match min_remaining_lifetime { + Some(min_lifetime) => valid_until_at_least + min_lifetime, + None => valid_until_at_least, + }; + self.certificate_authorities .iter() - .filter(|ca| ca.not_after > valid_until_at_least) + .filter(|ca| ca.not_after > cutoff) // pick the oldest valid CA, since it will be trusted by the most peers .min_by_key(|ca| ca.not_after) .with_context(|| NoCaLivesLongEnoughSnafu { - cutoff: valid_until_at_least, + cutoff, secret: self.source_secret.clone(), }) } /// Get all active trust root certificates. - pub fn trust_roots(&self) -> impl IntoIterator + '_ { + /// If `min_remaining_lifetime` is specified, only CAs that will stay valid for at least that long + /// will be returned. Otherwise all CAs (including expired ones) will be returned. + pub fn trust_roots(&self, min_remaining_lifetime: Option) -> Vec<&X509> { + let cutoff = + min_remaining_lifetime.map(|min_lifetime| OffsetDateTime::now_utc() + min_lifetime); + self.certificate_authorities .iter() + .filter(|ca| cutoff.is_none_or(|cutoff| ca.not_after >= cutoff)) .map(|ca| &ca.certificate) - .chain(&self.additional_trusted_certificates) + .chain(self.additional_trusted_certificates.iter().filter(|cert| { + cutoff.is_none_or(|cutoff| { + crate::utils::asn1time_to_offsetdatetime(cert.not_after()) + .is_ok_and(|not_after| not_after >= cutoff) + }) + })) + .collect() } } diff --git a/rust/operator-binary/src/backend/tls/mod.rs b/rust/operator-binary/src/backend/tls/mod.rs index a298dd1b..0f9e3c84 100644 --- a/rust/operator-binary/src/backend/tls/mod.rs +++ b/rust/operator-binary/src/backend/tls/mod.rs @@ -208,6 +208,7 @@ impl SecretBackend for TlsGenerate { // Extract and convert consumer input from the Volume annotations. let cert_lifetime = selector.autotls_cert_lifetime; let cert_restart_buffer = selector.autotls_cert_restart_buffer; + let ca_expiry_threshold = selector.autotls_ca_expiry_threshold; // We need to check that the cert lifetime it is not longer than allowed, // by capping it to the maximum configured at the SecretClass. @@ -284,7 +285,7 @@ impl SecretBackend for TlsGenerate { } let ca = self .ca_manager - .find_certificate_authority_for_signing(not_after) + .find_certificate_authority_for_signing(not_after, ca_expiry_threshold) .context(PickCaSnafu)?; let pod_cert = X509Builder::new() .and_then(|mut x509| { @@ -347,10 +348,13 @@ impl SecretBackend for TlsGenerate { SecretContents::new(SecretData::WellKnown(WellKnownSecretData::TlsPem( well_known::TlsPem { ca_pem: iterator_try_concat_bytes( - self.ca_manager.trust_roots().into_iter().map(|ca| { - ca.to_pem() - .context(SerializeCertificateSnafu { tpe: CertType::Ca }) - }), + self.ca_manager + .trust_roots(ca_expiry_threshold) + .into_iter() + .map(|ca| { + ca.to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Ca }) + }), )?, certificate_pem: Some( pod_cert @@ -372,16 +376,19 @@ impl SecretBackend for TlsGenerate { async fn get_trust_data( &self, - _selector: &super::TrustSelector, + selector: &super::TrustSelector, ) -> Result { Ok(SecretContents::new(SecretData::WellKnown( WellKnownSecretData::TlsPem(well_known::TlsPem { - ca_pem: iterator_try_concat_bytes(self.ca_manager.trust_roots().into_iter().map( - |ca| { - ca.to_pem() - .context(SerializeCertificateSnafu { tpe: CertType::Ca }) - }, - ))?, + ca_pem: iterator_try_concat_bytes( + self.ca_manager + .trust_roots(selector.ca_expiry_threshold) + .into_iter() + .map(|ca| { + ca.to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Ca }) + }), + )?, certificate_pem: None, key_pem: None, }), diff --git a/rust/operator-binary/src/crd.rs b/rust/operator-binary/src/crd.rs index d5cb1db7..3b38c7b6 100644 --- a/rust/operator-binary/src/crd.rs +++ b/rust/operator-binary/src/crd.rs @@ -520,6 +520,12 @@ pub struct TrustStoreSpec { /// The [format](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#format) that the data should be converted into. pub format: Option, + + /// The minimum remaining lifetime that a CA must have to be considered valid for + /// publishing in the TrustStore. + /// If not specified, all CAs (including expired ones) will be published. + #[serde(default)] + pub ca_expiry_threshold: Option, } #[cfg(test)] diff --git a/rust/operator-binary/src/truststore_controller.rs b/rust/operator-binary/src/truststore_controller.rs index 89ff079f..182b19c4 100644 --- a/rust/operator-binary/src/truststore_controller.rs +++ b/rust/operator-binary/src/truststore_controller.rs @@ -262,6 +262,7 @@ async fn reconcile( .namespace .clone() .context(NoTrustStoreNamespaceSnafu)?, + ca_expiry_threshold: truststore.spec.ca_expiry_threshold, }; let trust_data = backend .get_trust_data(&selector) diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/00-patch-ns.yaml.j2 b/tests/templates/kuttl/tls-ca-expiry-threshold/00-patch-ns.yaml.j2 new file mode 100644 index 00000000..67185acf --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/00-patch-ns.yaml.j2 @@ -0,0 +1,9 @@ +{% if test_scenario['values']['openshift'] == 'true' %} +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: kubectl patch namespace $NAMESPACE -p '{"metadata":{"labels":{"pod-security.kubernetes.io/enforce":"privileged"}}}' + timeout: 120 +{% endif %} diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/01-secretclass.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/01-secretclass.yaml new file mode 100644 index 00000000..5237075f --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/01-secretclass.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst '$NAMESPACE' < 01_secretclass.yaml | kubectl --namespace=$NAMESPACE apply -f - diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/01_secretclass.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/01_secretclass.yaml new file mode 100644 index 00000000..6b149d92 --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/01_secretclass.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: tls-short-ca +spec: + backend: + autoTls: + ca: + secret: + name: secret-provisioner-tls-ca-short + namespace: $NAMESPACE + autoGenerate: true + caCertificateLifetime: 5m +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: tls-normal-ca +spec: + backend: + autoTls: + ca: + secret: + name: secret-provisioner-tls-ca-normal + namespace: $NAMESPACE + autoGenerate: true + caCertificateLifetime: 365d diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/02-rbac.yaml.j2 b/tests/templates/kuttl/tls-ca-expiry-threshold/02-rbac.yaml.j2 new file mode 100644 index 00000000..6b8efd27 --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/02-rbac.yaml.j2 @@ -0,0 +1,38 @@ +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: use-integration-tests-scc +rules: + - apiGroups: + - "" + resources: + - configmaps + - secrets + verbs: + - create + - get + - list +{% if test_scenario['values']['openshift'] == "true" %} + - apiGroups: ["security.openshift.io"] + resources: ["securitycontextconstraints"] + resourceNames: ["privileged"] + verbs: ["use"] +{% endif %} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: integration-tests-sa +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: use-integration-tests-scc +subjects: + - kind: ServiceAccount + name: integration-tests-sa +roleRef: + kind: Role + name: use-integration-tests-scc + apiGroup: rbac.authorization.k8s.io diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/03-truststore.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/03-truststore.yaml new file mode 100644 index 00000000..17e82b4c --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/03-truststore.yaml @@ -0,0 +1,29 @@ +--- +# TrustStore without caExpiryThreshold - should include all CAs even if expired +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-all-cas +spec: + secretClassName: tls-short-ca + format: tls-pem +--- +# TrustStore with caExpiryThreshold - should filter out CAs close to expiry +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-filtered-cas +spec: + secretClassName: tls-short-ca + format: tls-pem + caExpiryThreshold: 1h +--- +# TrustStore with normal CA lifetime for comparison +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-normal-cas +spec: + secretClassName: tls-normal-ca + format: tls-pem + caExpiryThreshold: 1d diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/04-assert.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/04-assert.yaml new file mode 100644 index 00000000..1be5876e --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/04-assert.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 30 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-all-cas +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-filtered-cas +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-normal-cas diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/05-wait-for-expiry.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/05-wait-for-expiry.yaml new file mode 100644 index 00000000..9d370290 --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/05-wait-for-expiry.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: sleep 310 + timeout: 320 diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/06-consumer.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/06-consumer.yaml new file mode 100644 index 00000000..d8f4a4e7 --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/06-consumer.yaml @@ -0,0 +1,229 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: ca-expiry-consumer +spec: + template: + spec: + containers: + - name: consumer + image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev + command: + - bash + args: + - -c + - | + set -euo pipefail + + echo "" + echo "Testing volume with short threshold (should filter out expired CA)..." + if [ -f "/stackable/tls-short-with-threshold/ca.crt" ]; then + CA_COUNT_WITH_THRESHOLD=$(grep -c "BEGIN CERTIFICATE" /stackable/tls-short-with-threshold/ca.crt) + echo "Number of CAs in volume with threshold: $CA_COUNT_WITH_THRESHOLD" + + if [ "$CA_COUNT_WITH_THRESHOLD" -ne 1 ]; then + echo "ERROR: Expected exactly one CA in volume with threshold!" + exit 1 + fi + + # Check that CA is valid for at least 1 minute + if cat /stackable/tls-short-with-threshold/ca.crt | openssl x509 -noout -checkend 60; then + echo "CA in volume with threshold is valid as expected" + else + echo "ERROR: CA in volume with threshold should be valid for at least 1 minute!" + exit 1 + fi + else + echo "ERROR: No ca.crt file found in volume with threshold!" + exit 1 + fi + + # Check that the cert was signed by the CA and is valid + if [ -f "/stackable/tls-short-with-threshold/tls.crt" ]; then + if openssl verify -CAfile /stackable/tls-short-with-threshold/ca.crt /stackable/tls-short-with-threshold/tls.crt; then + echo "TLS certificate is valid and signed by the CA" + else + echo "ERROR: TLS certificate verification failed!" + exit 1 + fi + else + echo "ERROR: No tls.crt file found in volume with threshold!" + exit 1 + fi + + echo "" + echo "Testing volume without threshold (should contain CA even if expired)..." + if [ -f "/stackable/tls-short-without-threshold/ca.crt" ]; then + CA_COUNT_WITHOUT_THRESHOLD=$(grep -c "BEGIN CERTIFICATE" /stackable/tls-short-without-threshold/ca.crt) + echo "Number of CAs in volume without threshold: $CA_COUNT_WITHOUT_THRESHOLD" + + if [ "$CA_COUNT_WITHOUT_THRESHOLD" -lt 2 ]; then + echo "ERROR: Expected at least two CAs in volume without threshold!" + exit 1 + fi + else + echo "ERROR: No ca.crt file found in volume without threshold!" + exit 1 + fi + + echo "" + echo "Testing normal CA volume with threshold (should contain valid CA)..." + if [ -f "/stackable/tls-normal-with-threshold/ca.crt" ]; then + CA_COUNT_NORMAL=$(grep -c "BEGIN CERTIFICATE" /stackable/tls-normal-with-threshold/ca.crt) + echo "Number of CAs in normal volume: $CA_COUNT_NORMAL" + + if [ "$CA_COUNT_NORMAL" -lt 1 ]; then + echo "ERROR: Expected at least one CA in normal volume!" + exit 1 + fi + + # Check that CA is valid for well beyond 1 hour + cat /stackable/tls-normal-with-threshold/ca.crt | openssl x509 -noout -checkend 86400 || { + echo "ERROR: Normal CA should be valid for at least 24 hours!" + exit 1 + } + + echo "Normal CA is valid and passes threshold check" + else + echo "ERROR: No ca.crt file found in normal volume!" + exit 1 + fi + + echo "" + echo "Testing TrustStore ConfigMaps content..." + + # Test TrustStore without threshold (should contain expired CA) + echo "Testing truststore-all-cas ConfigMap (should contain expired CA)..." + TRUSTSTORE_ALL_DATA=$(kubectl get configmap truststore-all-cas -o jsonpath='{.data.ca\.crt}') + if [ -z "$TRUSTSTORE_ALL_DATA" ]; then + echo "ERROR: truststore-all-cas ConfigMap has no ca.crt data!" + exit 1 + fi + + TRUSTSTORE_ALL_COUNT=$(echo "$TRUSTSTORE_ALL_DATA" | grep -c "BEGIN CERTIFICATE") + echo "Number of CAs in truststore-all-cas: $TRUSTSTORE_ALL_COUNT" + + if [ "$TRUSTSTORE_ALL_COUNT" -lt 1 ]; then + echo "ERROR: Expected at least one CA in truststore-all-cas!" + exit 1 + fi + + # Check if at least one CA is expired (it should be) + if ! echo "$TRUSTSTORE_ALL_DATA" | openssl x509 -noout -checkend 0; then + echo "At least one CA in truststore-all-cas is expired, as expected" + else + echo "ERROR: At least one CA in truststore-all-cas should be expired!" + exit 1 + fi + + # Test TrustStore with threshold (should contain only valid CAs) + echo "" + echo "Testing truststore-filtered-cas ConfigMap (should filter out expired CA)..." + TRUSTSTORE_FILTERED_DATA=$(kubectl get configmap truststore-filtered-cas -o jsonpath='{.data.ca\.crt}' || echo "") + + if [ -z "$TRUSTSTORE_FILTERED_DATA" ]; then + echo "truststore-filtered-cas ConfigMap has no ca.crt data (expected - expired CA was filtered)" + else + TRUSTSTORE_FILTERED_COUNT=$(echo "$TRUSTSTORE_FILTERED_DATA" | grep -c "BEGIN CERTIFICATE" || echo "0") + echo "Number of CAs in truststore-filtered-cas: $TRUSTSTORE_FILTERED_COUNT" + + # If there are any CAs, they should be valid + if [ "$TRUSTSTORE_FILTERED_COUNT" -gt 0 ]; then + if echo "$TRUSTSTORE_FILTERED_DATA" | openssl x509 -noout -checkend 3600; then + echo "CA in truststore-filtered-cas is valid" + else + echo "ERROR: CA in truststore-filtered-cas should be valid if present!" + exit 1 + fi + fi + fi + + # Test TrustStore with normal CA (should contain valid CA) + echo "" + echo "Testing truststore-normal-cas ConfigMap (should contain valid CA)..." + TRUSTSTORE_NORMAL_DATA=$(kubectl get configmap truststore-normal-cas -o jsonpath='{.data.ca\.crt}') + if [ -z "$TRUSTSTORE_NORMAL_DATA" ]; then + echo "ERROR: truststore-normal-cas ConfigMap has no ca.crt data!" + exit 1 + fi + + TRUSTSTORE_NORMAL_COUNT=$(echo "$TRUSTSTORE_NORMAL_DATA" | grep -c "BEGIN CERTIFICATE") + echo "Number of CAs in truststore-normal-cas: $TRUSTSTORE_NORMAL_COUNT" + + if [ "$TRUSTSTORE_NORMAL_COUNT" -lt 1 ]; then + echo "ERROR: Expected at least one CA in truststore-normal-cas!" + exit 1 + fi + + # Check that the CA is valid for well beyond the threshold + if echo "$TRUSTSTORE_NORMAL_DATA" | openssl x509 -noout -checkend 86400; then + echo "CA in truststore-normal-cas is valid and passes threshold check" + else + echo "ERROR: CA in truststore-normal-cas should be valid for at least 24 hours!" + exit 1 + fi + + volumeMounts: + - mountPath: /stackable/tls-short-with-threshold + name: tls-short-with-threshold + - mountPath: /stackable/tls-short-without-threshold + name: tls-short-without-threshold + - mountPath: /stackable/tls-normal-with-threshold + name: tls-normal-with-threshold + volumes: + - name: tls-short-with-threshold + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls-short-ca + secrets.stackable.tech/scope: node,pod + secrets.stackable.tech/backend.autotls.ca.expiry-threshold: 1m + secrets.stackable.tech/backend.autotls.cert.restart-buffer: 1s + secrets.stackable.tech/backend.autotls.cert.lifetime: 1m + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + - name: tls-short-without-threshold + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls-short-ca + secrets.stackable.tech/scope: node,pod + secrets.stackable.tech/backend.autotls.cert.restart-buffer: 1s + secrets.stackable.tech/backend.autotls.cert.lifetime: 1m + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + - name: tls-normal-with-threshold + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls-normal-ca + secrets.stackable.tech/scope: node,pod + secrets.stackable.tech/backend.autotls.ca.expiry-threshold: 1d + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + restartPolicy: Never + terminationGracePeriodSeconds: 0 + serviceAccount: integration-tests-sa diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/07-assert.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/07-assert.yaml new file mode 100644 index 00000000..cface54e --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/07-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 60 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: ca-expiry-consumer +status: + conditions: + - type: SuccessCriteriaMet + status: "True" + - type: Complete + status: "True" diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/07-invalid-consumer.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/07-invalid-consumer.yaml new file mode 100644 index 00000000..21ffdc9c --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/07-invalid-consumer.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: ca-expiry-invalid-consumer +spec: + template: + spec: + containers: + - name: invalid-consumer + image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev + command: + - bash + args: + - -c + - | + set -euo pipefail + echo "I should not be running" + exit 0 + + volumeMounts: + - mountPath: /stackable/tls-short-with-threshold + name: tls-short-with-threshold + volumes: + - name: tls-short-with-threshold + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls-short-ca + secrets.stackable.tech/scope: node,pod + secrets.stackable.tech/backend.autotls.ca.expiry-threshold: 10m + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + restartPolicy: Never + terminationGracePeriodSeconds: 0 + serviceAccount: integration-tests-sa diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/08-wait-for-job.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/08-wait-for-job.yaml new file mode 100644 index 00000000..a4cb3ef9 --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/08-wait-for-job.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: sleep 30 + timeout: 40 diff --git a/tests/templates/kuttl/tls-ca-expiry-threshold/09-assert-invalid.yaml b/tests/templates/kuttl/tls-ca-expiry-threshold/09-assert-invalid.yaml new file mode 100644 index 00000000..fab19ac0 --- /dev/null +++ b/tests/templates/kuttl/tls-ca-expiry-threshold/09-assert-invalid.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +# Assert that the invalid consumer job exists but cannot complete successfully +# due to the CA expiry threshold preventing volume mount +apiVersion: batch/v1 +kind: Job +metadata: + name: ca-expiry-invalid-consumer +status: + # Job should be trying but not succeeding + ready: 0 + terminating: 0 + active: 1 diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index b8f29998..9415c301 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -40,6 +40,9 @@ tests: - name: tls-truststore dimensions: - openshift + - name: tls-ca-expiry-threshold + dimensions: + - openshift - name: cert-manager-tls dimensions: - openshift