Skip to content

Commit

Permalink
Merge branch 'igornovg/icbn-tls' into 'master'
Browse files Browse the repository at this point in the history
feat(BOUN-991): refactor TLS

Prepare `ic-boundary` to run without `nginx` on a standalone node (decentralise).

* Add `--hostname` and `--renew-days-before` CLI args
* Remove NNS scaffolding code
* Remove old firewall code (non-operational)
* Make TLS Acceptor update conditional on certificate renewal
* Make `TokenOwner` (former `TokenSetter` ) a `State` , not `Extension` for compile-time type safety ( `Extension` extractor evaluated at runtime)
* Fix infinite recursion in `Loader`
* Switch to `TLSCert` type to hold keypair instead of `(String, String)` tuple
* Run reconfiguration every 10min, not every 10sec
* Refactor the whole code a bit

Tested in DEV, works as intended. 

See merge request dfinity-lab/public/ic!16669
  • Loading branch information
blind-oracle committed Dec 12, 2023
2 parents f53527b + 09ab21c commit 50886fa
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 316 deletions.
8 changes: 4 additions & 4 deletions rs/boundary_node/ic_boundary/src/acme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,10 @@ impl<T: Obtain> Obtain for WithRetry<T> {
}

// Retry
if let Err(ObtainError::OrderNotValid(_)) = out {
continue;
}
if let Err(ObtainError::CertificateNotReady) = out {
if matches!(
out,
Err(ObtainError::OrderNotValid(_)) | Err(ObtainError::CertificateNotReady)
) {
continue;
}

Expand Down
9 changes: 8 additions & 1 deletion rs/boundary_node/ic_boundary/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,15 @@ pub struct FirewallConfig {
#[cfg(feature = "tls")]
#[derive(Args)]
pub struct TlsConfig {
/// The path to the ACME credentials file
/// Hostname to request TLS certificate for
#[clap(long)]
pub hostname: String,

/// How many days before certificate expires to start renewing it
#[clap(long, default_value = "30", value_parser = clap::value_parser!(u32).range(1..90))]
pub renew_days_before: u32,

/// The path to the ACME credentials file
#[clap(long, default_value = "acme.json")]
pub acme_credentials_path: PathBuf,

Expand Down
115 changes: 51 additions & 64 deletions rs/boundary_node/ic_boundary/src/configuration.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use anyhow::Error;
use async_trait::async_trait;
use std::time::Instant;
use tracing::info;

#[cfg(feature = "tls")]
use {
anyhow::Context,
arc_swap::ArcSwapOption,
Expand All @@ -11,23 +11,19 @@ use {
};

use crate::{
firewall::Rule,
core::Run,
metrics::{MetricParams, WithMetrics},
tls::{self, Provision, ProvisionResult, TLSCert},
};

#[cfg(feature = "tls")]
use crate::tls::{self, Provision};

#[allow(dead_code)] // TODO: remove when Firewall() is used.
#[non_exhaustive]
#[derive(Clone, PartialEq)]
pub enum ServiceConfiguration {
Tls(String),
Firewall(Vec<Rule>),
}

#[derive(Debug, thiserror::Error)]
pub enum ConfigureError {
#[cfg(feature = "tls")]
#[error(transparent)]
ProvisionError(#[from] tls::ProvisionError),

Expand Down Expand Up @@ -65,49 +61,24 @@ impl<T: Configure> Configure for WithMetrics<T> {
}
}

pub struct WithDeduplication<T>(T, Option<ServiceConfiguration>);

impl<T> WithDeduplication<T> {
pub fn wrap(v: T) -> Self {
Self(v, None)
}
}

#[async_trait]
impl<T: Configure> Configure for WithDeduplication<T> {
async fn configure(&mut self, cfg: &ServiceConfiguration) -> Result<(), ConfigureError> {
if self.1.as_ref() == Some(cfg) {
return Ok(());
}

let out = self.0.configure(cfg).await?;
self.1 = Some(cfg.to_owned());
Ok(out)
}
}

pub struct Configurator {
pub tls: Box<dyn Configure>,
pub firewall: Box<dyn Configure>,
}

#[async_trait]
impl Configure for Configurator {
async fn configure(&mut self, cfg: &ServiceConfiguration) -> Result<(), ConfigureError> {
match cfg {
ServiceConfiguration::Tls(..) => self.tls.configure(cfg).await,
ServiceConfiguration::Firewall(..) => self.firewall.configure(cfg).await,
}
}
}

#[cfg(feature = "tls")]
pub struct TlsConfigurator {
acceptor: Arc<ArcSwapOption<RustlsAcceptor>>,
provisioner: Box<dyn Provision>,
}

#[cfg(feature = "tls")]
impl TlsConfigurator {
pub fn new(
acceptor: Arc<ArcSwapOption<RustlsAcceptor>>,
Expand All @@ -118,52 +89,68 @@ impl TlsConfigurator {
provisioner,
}
}
}

#[cfg(feature = "tls")]
#[async_trait]
impl Configure for TlsConfigurator {
async fn configure(&mut self, cfg: &ServiceConfiguration) -> Result<(), ConfigureError> {
if let ServiceConfiguration::Tls(name) = cfg {
// Provision new certificate
let (cert, pkey) = self.provisioner.provision(name).await?;

// Replace with new acceptor
let cfg = RustlsConfig::from_pem(cert.into_bytes(), pkey.into_bytes())
.await
.context("failed to create rustls config")?;
async fn apply(&self, tls_cert: TLSCert) -> Result<(), ConfigureError> {
let cfg = RustlsConfig::from_pem(tls_cert.0.into_bytes(), tls_cert.1.into_bytes())
.await
.context("failed to parse certificate")?;

let acceptor = RustlsAcceptor::new(cfg);
let acceptor = Arc::new(acceptor);
let acceptor = Some(acceptor);
// Construct new acceptor
let acceptor = RustlsAcceptor::new(cfg);
let acceptor = Arc::new(acceptor);
let acceptor = Some(acceptor);

self.acceptor.store(acceptor);
}
// Replace current acceptor
self.acceptor.store(acceptor);

Ok(())
}
}

// No-op configurator when the feature is off
#[cfg(not(feature = "tls"))]
pub struct TlsConfigurator {}

#[cfg(not(feature = "tls"))]
#[async_trait]
impl Configure for TlsConfigurator {
async fn configure(&mut self, _cfg: &ServiceConfiguration) -> Result<(), ConfigureError> {
Ok(())
async fn configure(&mut self, cfg: &ServiceConfiguration) -> Result<(), ConfigureError> {
let ServiceConfiguration::Tls(name) = cfg;

// Try to provision a certificate
match self.provisioner.provision(name).await? {
// If there was a new certificate issued - apply it
ProvisionResult::Issued(tls_cert) => self.apply(tls_cert).await,

// If it's still valid - apply it only if we don't have one yet loaded
ProvisionResult::StillValid(tls_cert) => {
if self.acceptor.load().is_none() {
self.apply(tls_cert).await
} else {
Ok(())
}
}
}
}
}

pub struct FirewallConfigurator {}
pub struct ConfigurationRunner<C> {
hostname: String,
configurator: C,
}

#[async_trait]
impl Configure for FirewallConfigurator {
async fn configure(&mut self, cfg: &ServiceConfiguration) -> Result<(), ConfigureError> {
if let ServiceConfiguration::Firewall(rules) = cfg {
println!("configuring firewall: {rules:?}");
impl<C> ConfigurationRunner<C> {
pub fn new(hostname: String, configurator: C) -> Self {
Self {
hostname,
configurator,
}
}
}

#[async_trait]
impl<C: Configure> Run for ConfigurationRunner<C> {
async fn run(&mut self) -> Result<(), Error> {
// TLS
self.configurator
.configure(&ServiceConfiguration::Tls(self.hostname.clone()))
.await
.context("failed to apply tls configuration")?;

Ok(())
}
Expand Down

0 comments on commit 50886fa

Please sign in to comment.