Skip to content

Make CA lifetimes configurable #357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ All notable changes to this project will be documented in this file.
- Use new annotation builder ([#341]).
- `autoTLS` certificate authorities will now be rotated regularly ([#350]).
- [BREAKING] This changes the format of the CA secrets. Old secrets will be migrated automatically, but manual intervention will be required to downgrade back to 23.11.x.
- `autoTLS` certificate authority lifetimes are now configurable ([#357]).

[#333]: https://github.com/stackabletech/secret-operator/pull/333
[#341]: https://github.com/stackabletech/secret-operator/pull/341
[#350]: https://github.com/stackabletech/secret-operator/pull/350
[#352]: https://github.com/stackabletech/secret-operator/pull/352
[#357]: https://github.com/stackabletech/secret-operator/pull/357


## [23.11.0] - 2023-11-24
Expand Down
9 changes: 9 additions & 0 deletions deploy/helm/secret-operator/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ spec:
default: false
description: Whether the certificate authority should be managed by Secret Operator, including being generated if it does not already exist.
type: boolean
caCertificateLifetime:
default: 730d
description: |-
The lifetime of each generated certificate authority.

Should always be more than double `maxCertificateLifetime`.

If `autoGenerate: true` then the Secret Operator will prepare a new CA certificate the old CA approaches expiration. If `autoGenerate: false` then the Secret Operator will log a warning instead.
type: string
secret:
description: Reference (name and namespace) to a Kubernetes Secret object where the CA certificate and key is stored in the keys `ca.crt` and `ca.key` respectively.
properties:
Expand Down
6 changes: 4 additions & 2 deletions docs/modules/secret-operator/pages/secretclass.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ Users can use podOverrides to extend the certificate lifetime by adding volume a

Certificate authorities also have a limited lifetime, and need to be rotated before they expire to avoid cluster disruption.

If configured to provision its own CA (`autoTls.ca.autoGenerate`), the Secret Operator will create CA certificates that are valid for 2 years,
and initiate rotation when there is less than 1 year remaining. If configured _not_ to provision its own CA, a warning will be issued when there is less than 1 year remaining.
If configured to provision its own CA (`autoTls.ca.autoGenerate`), the Secret Operator will create CA certificates that are valid for 2 years (`autoTls.ca.caCertificateLifetime`),
and initiate rotation once less than half of that time remains. If configured _not_ to provision its own CA, a warning will instead be issued in that case.

To avoid disruption and let the new CA propagate through the cluster, the Secret Operator will prefer using the oldest CA that will last for the entire lifetime of the issued certificate.

Expand All @@ -81,6 +81,7 @@ spec:
name: secret-provisioner-tls-ca
namespace: default
autoGenerate: true
caCertificateLifetime: 700d
maxCertificateLifetime: 15d # optional
----

Expand All @@ -89,6 +90,7 @@ spec:
`autoTls.ca.secret`:: Reference (`name` and `namespace`) to a K8s `Secret` object where the CA certificate and key is stored in the keys `ca.crt`
and `ca.key` respectively.
`autoTls.ca.autoGenerate`:: Whether the certificate authority should be provisioned and managed by the Secret Operator.
`autoTls.ca.caCertificateLifetime` :: The lifetime of the certificate authority's root certificate.
`autoTls.maxCertificateLifetime`:: Maximum lifetime the created certificates are allowed to have. In case consumers request a longer lifetime than allowed by this setting, the lifetime will be the minimum of both.

[#backend-kerberoskeytab]
Expand Down
9 changes: 2 additions & 7 deletions rust/operator-binary/src/backend/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,12 @@ pub async fn from_class(
})
}
crd::SecretClassBackend::AutoTls(crd::AutoTlsBackend {
ca:
crd::AutoTlsCa {
secret,
auto_generate,
},
ca,
max_certificate_lifetime,
}) => from(
super::TlsGenerate::get_or_create_k8s_certificate(
client,
&secret,
auto_generate,
&ca,
max_certificate_lifetime,
)
.await?,
Expand Down
6 changes: 3 additions & 3 deletions rust/operator-binary/src/backend/tls/ca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ pub struct Config {
pub manage_ca: bool,

/// The duration of any new CA certificates provisioned.
pub ca_lifetime: Duration,
pub ca_certificate_lifetime: Duration,

/// If no existing CA certificate outlives `rotate_if_ca_expires_before`, a new
/// certificate will be generated.
Expand All @@ -151,7 +151,7 @@ pub struct Config {
/// pods' truststores.
///
/// Hence, this value _should_ be larger than the PKI's maximum certificate lifetime,
/// and smaller than [`Self::ca_lifetime`].
/// and smaller than [`Self::ca_certificate_lifetime`].
pub rotate_if_ca_expires_before: Option<Duration>,
}

Expand Down Expand Up @@ -185,7 +185,7 @@ impl CertificateAuthority {
.build();
let now = OffsetDateTime::now_utc();
let not_before = now - Duration::from_minutes_unchecked(5);
let not_after = now + config.ca_lifetime;
let not_after = now + config.ca_certificate_lifetime;
let conf = Conf::new(ConfMethod::default()).unwrap();
let private_key = Rsa::generate(2048)
.and_then(PKey::try_from)
Expand Down
21 changes: 14 additions & 7 deletions rust/operator-binary/src/backend/tls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ use stackable_operator::{
k8s_openapi::chrono::{self, FixedOffset, TimeZone},
time::Duration,
};
use stackable_secret_operator_crd_utils::SecretReference;
use time::OffsetDateTime;

use crate::{
crd,
format::{well_known, SecretData, WellKnownSecretData},
utils::iterator_try_concat_bytes,
};
Expand All @@ -40,6 +40,10 @@ use super::{

mod ca;

/// How long CA certificates should last for. Also used for calculating when they should be rotated.
/// Must be less than half of [`DEFAULT_MAX_CERT_LIFETIME`].
pub const DEFAULT_CA_CERT_LIFETIME: Duration = Duration::from_days_unchecked(365 * 2);

/// As the Pods will be evicted [`DEFAULT_CERT_RESTART_BUFFER`] before
/// the cert actually expires, this results in a restart in approx every 2 weeks,
/// which matches the rolling re-deploy of k8s nodes of e.g.:
Expand Down Expand Up @@ -130,18 +134,21 @@ impl TlsGenerate {
/// an independent self-signed CA.
pub async fn get_or_create_k8s_certificate(
client: &stackable_operator::client::Client,
secret_ref: &SecretReference,
auto_generate: bool,
crd::AutoTlsCa {
secret: ca_secret,
auto_generate: auto_generate_ca,
ca_certificate_lifetime,
}: &crd::AutoTlsCa,
max_cert_lifetime: Duration,
) -> Result<Self> {
Ok(Self {
ca_manager: ca::Manager::load_or_create(
client,
secret_ref,
ca_secret,
&ca::Config {
manage_ca: auto_generate,
ca_lifetime: Duration::from_days_unchecked(2 * 365),
rotate_if_ca_expires_before: Some(Duration::from_days_unchecked(365)),
manage_ca: *auto_generate_ca,
ca_certificate_lifetime: *ca_certificate_lifetime,
rotate_if_ca_expires_before: Some(*ca_certificate_lifetime / 2),
},
)
.await
Expand Down
30 changes: 25 additions & 5 deletions rust/operator-binary/src/crd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use stackable_operator::{
};
use stackable_secret_operator_crd_utils::SecretReference;

use crate::backend::tls::DEFAULT_MAX_CERT_LIFETIME;
use crate::backend;

/// A [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) is a cluster-global Kubernetes resource
/// that defines a category of secrets that the Secret Operator knows how to provision.
Expand Down Expand Up @@ -82,12 +82,14 @@ pub struct AutoTlsBackend {
/// In case consumers request a longer lifetime than allowed by this setting,
/// the lifetime will be the minimum of both, so this setting takes precedence.
/// The default value is 15 days.
#[serde(default = "default_max_certificate_lifetime")]
#[serde(default = "AutoTlsBackend::default_max_certificate_lifetime")]
pub max_certificate_lifetime: Duration,
}

fn default_max_certificate_lifetime() -> Duration {
DEFAULT_MAX_CERT_LIFETIME
impl AutoTlsBackend {
fn default_max_certificate_lifetime() -> Duration {
backend::tls::DEFAULT_MAX_CERT_LIFETIME
}
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
Expand All @@ -102,6 +104,21 @@ pub struct AutoTlsCa {
// TODO: Consider renaming to `manage` for v1alpha2
#[serde(default)]
pub auto_generate: bool,

/// The lifetime of each generated certificate authority.
///
/// Should always be more than double `maxCertificateLifetime`.
///
/// If `autoGenerate: true` then the Secret Operator will prepare a new CA certificate the old CA approaches expiration.
/// If `autoGenerate: false` then the Secret Operator will log a warning instead.
#[serde(default = "AutoTlsCa::default_ca_certificate_lifetime")]
pub ca_certificate_lifetime: Duration,
}

impl AutoTlsCa {
fn default_ca_certificate_lifetime() -> Duration {
backend::tls::DEFAULT_CA_CERT_LIFETIME
}
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
Expand Down Expand Up @@ -256,7 +273,7 @@ mod test {
use super::*;

use crate::{
backend::tls::DEFAULT_MAX_CERT_LIFETIME,
backend::tls::{DEFAULT_CA_CERT_LIFETIME, DEFAULT_MAX_CERT_LIFETIME},
crd::{AutoTlsBackend, SecretClass, SecretClassSpec},
};

Expand Down Expand Up @@ -288,6 +305,7 @@ mod test {
namespace: "default".to_string(),
},
auto_generate: false,
ca_certificate_lifetime: DEFAULT_CA_CERT_LIFETIME,
},
max_certificate_lifetime: DEFAULT_MAX_CERT_LIFETIME,
})
Expand All @@ -307,6 +325,7 @@ mod test {
name: secret-provisioner-tls-ca
namespace: default
autoGenerate: true
caCertificateLifetime: 100d
maxCertificateLifetime: 31d
"#;
let deserializer = serde_yaml::Deserializer::from_str(input);
Expand All @@ -322,6 +341,7 @@ mod test {
namespace: "default".to_string(),
},
auto_generate: true,
ca_certificate_lifetime: Duration::from_days_unchecked(100)
},
max_certificate_lifetime: Duration::from_days_unchecked(31),
})
Expand Down