Skip to content

Commit ed58563

Browse files
committed
Actually rotate certs
1 parent 0ef29ab commit ed58563

7 files changed

Lines changed: 304 additions & 151 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repository = "https://github.com/stackabletech/operator-rs"
1111
[workspace.dependencies]
1212
product-config = { git = "https://github.com/stackabletech/product-config.git", tag = "0.7.0" }
1313

14+
arc-swap = "1.7"
1415
axum = { version = "0.8.1", features = ["http2"] }
1516
chrono = { version = "0.4.38", default-features = false }
1617
clap = { version = "4.5.17", features = ["derive", "cargo", "env"] }

crates/stackable-webhook/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ stackable-certs = { path = "../stackable-certs", features = ["rustls"] }
1111
stackable-telemetry = { path = "../stackable-telemetry" }
1212
stackable-operator = { path = "../stackable-operator" }
1313

14+
arc-swap.workspace = true
1415
axum.workspace = true
1516
futures-util.workspace = true
1617
hyper-util.workspace = true

crates/stackable-webhook/src/lib.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ pub trait WebhookHandler<Req, Res> {
6060
#[derive(Debug, Snafu)]
6161
pub enum WebhookError {
6262
#[snafu(display("failed to create TLS server"))]
63-
CreateTlsServer { source: tls::Error },
63+
CreateTlsServer { source: tls::TlsServerError },
6464

6565
#[snafu(display("failed to run TLS server"))]
66-
RunTlsServer { source: tls::Error },
66+
RunTlsServer { source: tls::TlsServerError },
6767
}
6868

6969
/// A ready-to-use webhook server.
@@ -123,8 +123,7 @@ impl WebhookServer {
123123
router: Router,
124124
options: Options,
125125
subject_alterative_dns_names: Vec<String>,
126-
cert_tx: mpsc::Sender<Certificate>,
127-
) -> Result<Self, WebhookError> {
126+
) -> Result<(Self, mpsc::Receiver<Certificate>), WebhookError> {
128127
tracing::trace!("create new webhook server");
129128

130129
// TODO (@Techassi): Make opt-in configurable from the outside
@@ -147,16 +146,12 @@ impl WebhookServer {
147146
.route("/health", get(|| async { "ok" }));
148147

149148
tracing::debug!("create TLS server");
150-
let tls_server = TlsServer::new(
151-
options.socket_addr,
152-
router,
153-
subject_alterative_dns_names,
154-
cert_tx,
155-
)
156-
.await
157-
.context(CreateTlsServerSnafu)?;
158-
159-
Ok(Self { tls_server })
149+
let (tls_server, cert_rx) =
150+
TlsServer::new(options.socket_addr, router, subject_alterative_dns_names)
151+
.await
152+
.context(CreateTlsServerSnafu)?;
153+
154+
Ok((Self { tls_server }, cert_rx))
160155
}
161156

162157
/// Runs the Webhook server and sets up signal handlers for shutting down.

crates/stackable-webhook/src/servers/conversion.rs

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use kube::{
1919
};
2020
use snafu::{OptionExt, ResultExt, Snafu};
2121
use stackable_operator::cli::OperatorEnvironmentOpts;
22-
use tokio::sync::mpsc;
22+
use tokio::{sync::mpsc, try_join};
2323
use tracing::instrument;
2424
use x509_cert::{
2525
Certificate,
@@ -44,6 +44,12 @@ pub enum ConversionWebhookError {
4444
#[snafu(display("failed to convert CA certificate into PEM format"))]
4545
ConvertCaToPem { source: x509_cert::der::Error },
4646

47+
#[snafu(display("failed to reconcile CRDs"))]
48+
ReconcileCRDs {
49+
#[snafu(source(from(ConversionWebhookError, Box::new)))]
50+
source: Box<ConversionWebhookError>,
51+
},
52+
4753
#[snafu(display("failed to update CRD {crd_name:?}"))]
4854
UpdateCRD {
4955
source: stackable_operator::kube::Error,
@@ -65,8 +71,7 @@ where
6571
/// See [`ConversionWebhookServer::new()`] for usage examples.
6672
pub struct ConversionWebhookServer {
6773
server: WebhookServer,
68-
current_cert: Certificate,
69-
74+
cert_rx: mpsc::Receiver<Certificate>,
7075
client: Client,
7176
field_manager: String,
7277
crds: HashMap<String, CustomResourceDefinition>,
@@ -144,6 +149,7 @@ impl ConversionWebhookServer {
144149
H: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static,
145150
{
146151
tracing::debug!("create new conversion webhook server");
152+
let field_manager: String = field_manager.into();
147153

148154
let mut router = Router::new();
149155
let mut crds = HashMap::new();
@@ -160,52 +166,111 @@ impl ConversionWebhookServer {
160166

161167
// This is how Kubernetes calls us, so it decides about the naming.
162168
// AFAIK we can not influence this, so this is the only SAN entry needed.
163-
let webhook_domain_name = format!(
169+
let sans = vec![format!(
164170
"{service_name}.{operator_namespace}.svc",
165171
service_name = operator_environment.operator_service_name,
166172
operator_namespace = operator_environment.operator_namespace,
167-
);
173+
)];
168174

169-
let (cert_tx, mut cert_rx) = mpsc::channel(1);
170-
let server = WebhookServer::new(router, options, vec![webhook_domain_name], cert_tx)
175+
let (server, mut cert_rx) = WebhookServer::new(router, options, sans)
171176
.await
172177
.context(CreateWebhookServerSnafu)?;
178+
179+
// We block the ConversionWebhookServer creation until the certificates have been generated.
180+
// This way we
181+
// 1. Are able to apply the CRDs before we start the actual controllers relying on them
182+
// 2. Avoid updating them shortly after as cert have been generated. Doing so would cause
183+
// unnecessary "too old resource version" errors in the controllers as the CRD was updated.
173184
let current_cert = cert_rx
174185
.recv()
175186
.await
176187
.context(ReceiverCertificateFromChannelSnafu)?;
188+
Self::reconcile_crds(
189+
&client,
190+
&field_manager,
191+
&crds,
192+
&operator_environment,
193+
&current_cert,
194+
)
195+
.await
196+
.context(ReconcileCRDsSnafu)?;
177197

178198
Ok(Self {
179199
server,
180-
current_cert,
200+
cert_rx,
181201
client,
182-
field_manager: field_manager.into(),
202+
field_manager,
183203
crds,
184204
operator_environment,
185205
})
186206
}
187207

188-
/// Starts the conversion webhook server
189-
///
190-
/// Use [`Self::reconcile_crds`] first to avoid "too old resource version" error
191208
pub async fn run(self) -> Result<(), ConversionWebhookError> {
192209
tracing::info!("starting conversion webhook server");
193210

194-
self.server.run().await.context(RunWebhookServerSnafu)?;
211+
let Self {
212+
server,
213+
cert_rx,
214+
client,
215+
field_manager,
216+
crds,
217+
operator_environment,
218+
} = self;
195219

220+
try_join!(
221+
Self::run_webhook_server(server),
222+
Self::run_cert_update_loop(
223+
cert_rx,
224+
&client,
225+
&field_manager,
226+
&crds,
227+
&operator_environment
228+
),
229+
)?;
230+
231+
Ok(())
232+
}
233+
234+
async fn run_webhook_server(server: WebhookServer) -> Result<(), ConversionWebhookError> {
235+
server.run().await.context(RunWebhookServerSnafu)
236+
}
237+
238+
async fn run_cert_update_loop(
239+
mut cert_rx: mpsc::Receiver<Certificate>,
240+
client: &Client,
241+
field_manager: &str,
242+
crds: &HashMap<String, CustomResourceDefinition>,
243+
operator_environment: &OperatorEnvironmentOpts,
244+
) -> Result<(), ConversionWebhookError> {
245+
while let Some(current_cert) = cert_rx.recv().await {
246+
Self::reconcile_crds(
247+
client,
248+
field_manager,
249+
crds,
250+
operator_environment,
251+
&current_cert,
252+
)
253+
.await
254+
.context(ReconcileCRDsSnafu)?;
255+
}
196256
Ok(())
197257
}
198258

199259
#[instrument(skip_all)]
200-
pub async fn reconcile_crds(&self) -> Result<(), ConversionWebhookError> {
201-
tracing::info!(kinds = ?self.crds.keys(), "Reconciling CRDs");
202-
let ca_bundle = self
203-
.current_cert
260+
async fn reconcile_crds(
261+
client: &Client,
262+
field_manager: &str,
263+
crds: &HashMap<String, CustomResourceDefinition>,
264+
operator_environment: &OperatorEnvironmentOpts,
265+
current_cert: &Certificate,
266+
) -> Result<(), ConversionWebhookError> {
267+
tracing::info!(kinds = ?crds.keys(), "Reconciling CRDs");
268+
let ca_bundle = current_cert
204269
.to_pem(LineEnding::LF)
205270
.context(ConvertCaToPemSnafu)?;
206271

207-
let crd_api: Api<CustomResourceDefinition> = Api::all(self.client.clone());
208-
for (kind, crd) in &self.crds {
272+
let crd_api: Api<CustomResourceDefinition> = Api::all(client.clone());
273+
for (kind, crd) in crds {
209274
let mut crd = crd.clone();
210275

211276
crd.spec.conversion = Some(CustomResourceConversion {
@@ -217,8 +282,8 @@ impl ConversionWebhookServer {
217282
conversion_review_versions: vec!["v1".to_string()],
218283
client_config: Some(WebhookClientConfig {
219284
service: Some(ServiceReference {
220-
name: self.operator_environment.operator_service_name.clone(),
221-
namespace: self.operator_environment.operator_namespace.clone(),
285+
name: operator_environment.operator_service_name.clone(),
286+
namespace: operator_environment.operator_namespace.clone(),
222287
path: Some(format!("/convert/{kind}")),
223288
port: Some(
224289
DEFAULT_HTTPS_PORT
@@ -235,14 +300,14 @@ impl ConversionWebhookServer {
235300
// TODO: Move this into function and do a more clever update mechanism
236301
let crd_name = crd.name_any();
237302
let patch = Patch::Apply(&crd);
238-
let patch_params = PatchParams::apply(&self.field_manager);
303+
let patch_params = PatchParams::apply(field_manager);
239304
crd_api
240305
.patch(&crd_name, &patch_params, &patch)
241306
.await
242307
.with_context(|_| UpdateCRDSnafu {
243308
crd_name: crd_name.to_string(),
244309
})?;
245-
tracing::info!(crd_name, "Reconciled CRDs");
310+
tracing::info!(crd.name = crd_name, "Reconciled CRD");
246311
}
247312
Ok(())
248313
}

0 commit comments

Comments
 (0)