Skip to content

Commit

Permalink
gateway/certs: removed certificates renewal automation...
Browse files Browse the repository at this point in the history
and added a new `request-gateway-certificate` admin command to
renew the gateway certificate on-deman.

Addressed @Brokard feedback by removing the automation for certificates
renewal, keeping only the APIs to ask for certificates renewal.

Note: requesting renewal for the gateway certificate requires inserting
manually a DNS TXT record to complete the ACME DNS-01 challenge.

Signed-off-by: Iulian Barbu <iulianbarbu2@gmail.com>
  • Loading branch information
iulianbarbu committed Feb 21, 2023
1 parent 861b868 commit 7af6922
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 226 deletions.
18 changes: 13 additions & 5 deletions admin/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub enum Command {
/// Try to revive projects in the crashed state
Revive,

/// Manage custom domains
/// Manage domains
#[command(subcommand)]
Acme(AcmeCommand),

Expand Down Expand Up @@ -59,13 +59,13 @@ pub enum AcmeCommand {
credentials: PathBuf,
},

/// Automate certificate renewal for a FQDN
AutomateCertificateRenewal {
/// Fqdn to automate certificate renewal for
/// Renew the certificate for a FQDN
RenewProjectCertificate {
/// Fqdn to renew the certificate for
#[arg(long)]
fqdn: String,

/// Project to automate certificate renewal for
/// Project to renew the certificate for
#[arg(long)]
project: ProjectName,

Expand All @@ -74,6 +74,14 @@ pub enum AcmeCommand {
#[arg(long)]
credentials: PathBuf,
},

/// Renew certificate for the shuttle gateway
RenewGatewayCertificate {
/// Path to acme credentials file
/// This should have been created with `acme create-account`
#[arg(long)]
credentials: PathBuf,
},
}

#[derive(Subcommand, Debug)]
Expand Down
10 changes: 9 additions & 1 deletion admin/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl Client {
self.post(&path, Some(credentials)).await
}

pub async fn acme_renew_certificate(
pub async fn acme_renew_custom_domain_certificate(
&self,
fqdn: &str,
project_name: &ProjectName,
Expand All @@ -49,6 +49,14 @@ impl Client {
self.post(&path, Some(credentials)).await
}

pub async fn acme_renew_gateway_certificate(
&self,
credentials: &serde_json::Value,
) -> Result<String> {
let path = "/admin//acme/gateway/renew".to_string();
self.post(&path, Some(credentials)).await
}

pub async fn get_projects(&self) -> Result<Vec<project::AdminResponse>> {
self.get("/admin/projects").await
}
Expand Down
14 changes: 12 additions & 2 deletions admin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async fn main() {
.await
.expect("to get a certificate challenge response")
}
Command::Acme(AcmeCommand::AutomateCertificateRenewal {
Command::Acme(AcmeCommand::RenewProjectCertificate {
fqdn,
project,
credentials,
Expand All @@ -60,7 +60,17 @@ async fn main() {
serde_json::from_str(&credentials).expect("to parse content of credentials file");

client
.acme_renew_certificate(&fqdn, &project, &credentials)
.acme_renew_custom_domain_certificate(&fqdn, &project, &credentials)
.await
.expect("to get a certificate challenge response")
}
Command::Acme(AcmeCommand::RenewGatewayCertificate { credentials }) => {
let credentials = fs::read_to_string(credentials).expect("to read credentials file");
let credentials =
serde_json::from_str(&credentials).expect("to parse content of credentials file");

client
.acme_renew_gateway_certificate(&credentials)
.await
.expect("to get a certificate challenge response")
}
Expand Down
14 changes: 9 additions & 5 deletions gateway/src/acme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ pub struct CustomDomain {
pub private_key: String,
}

pub enum AcmeCredentials<'a> {
InMemory(AccountCredentials<'a>),
GatewayState,
}

/// An ACME client implementation that completes Http01 challenges
/// It is safe to clone this type as it functions as a singleton
#[derive(Clone, Default)]
Expand Down Expand Up @@ -98,10 +103,10 @@ impl AcmeClient {
&self,
identifier: &str,
challenge_type: ChallengeType,
account: &Account,
creds: AccountCredentials<'_>,
) -> Result<(String, String), AcmeClientError> {
trace!(identifier, "requesting acme certificate");

let account = AccountWrapper::from(creds).0;
let (mut order, state) = account
.new_order(&NewOrder {
identifiers: &[Identifier::Dns(identifier.to_string())],
Expand Down Expand Up @@ -129,7 +134,6 @@ impl AcmeClient {

self.complete_challenge(challenge_type, authorization, &mut order)
.await?;

let certificate = {
let mut params = CertificateParams::new(vec![identifier.to_owned()]);
params.distinguished_name = DistinguishedName::new();
Expand All @@ -138,11 +142,11 @@ impl AcmeClient {
AcmeClientError::CertificateCreation
})?
};

let signing_request = certificate.serialize_request_der().map_err(|error| {
error!(%error, "failed to create certificate signing request");
AcmeClientError::CertificateSigning
})?;

let certificate_chain = order
.finalize(&signing_request, &state.finalize)
.await
Expand Down Expand Up @@ -294,7 +298,7 @@ impl<'a> From<AccountCredentials<'a>> for AccountWrapper {
"failed to convert acme credentials into account"
);
})
.expect("Account credentials malformed"),
.expect("Malformed account credentials."),
)
}
}
Expand Down
151 changes: 72 additions & 79 deletions gateway/src/api/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use uuid::Uuid;
use x509_parser::parse_x509_certificate;
use x509_parser::time::ASN1Time;

use crate::acme::{AccountWrapper, AcmeClient, CustomDomain};
use crate::acme::{AcmeClient, CustomDomain};
use crate::auth::{Admin, ScopedUser, User};
use crate::project::{Project, ProjectCreating};
use crate::task::{self, BoxedTask, TaskResult};
Expand Down Expand Up @@ -303,7 +303,7 @@ async fn create_acme_account(
}

#[instrument(skip_all, fields(%project_name, %fqdn))]
async fn request_acme_certificate(
async fn request_custom_domain_acme_certificate(
_: Admin,
State(RouterState {
service, sender, ..
Expand All @@ -317,15 +317,14 @@ async fn request_acme_certificate(
.parse()
.map_err(|_err| Error::from(ErrorKind::InvalidCustomDomain))?;

let account = AccountWrapper::from(credentials).0;
let (certs, private_key) = service
.get_or_create_certificate(&fqdn, acme_client.clone(), &account, &project_name)
.create_custom_domain_certificate(&fqdn, &acme_client, &project_name, credentials)
.await?;

// Destroy and recreate the project with the new domain.
service
.new_task()
.project(project_name)
.project(project_name.clone())
.and_then(task::destroy())
.and_then(task::run_until_done())
.and_then(task::run({
Expand All @@ -349,15 +348,16 @@ async fn request_acme_certificate(
.serve_pem(&fqdn.to_string(), Cursor::new(buf))
.await?;

Ok("certificate created".to_string())
Ok(format!(
"New certificate created for {} project.",
project_name
))
}

#[instrument(skip_all, fields(%project_name, %fqdn))]
async fn renew_acme_certificate(
async fn renew_custom_domain_acme_certificate(
_: Admin,
State(RouterState {
service, sender, ..
}): State<RouterState>,
State(RouterState { service, .. }): State<RouterState>,
Extension(acme_client): Extension<AcmeClient>,
Extension(resolver): Extension<Arc<GatewayCertResolver>>,
Path((project_name, fqdn)): Path<(ProjectName, String)>,
Expand All @@ -366,75 +366,64 @@ async fn renew_acme_certificate(
let fqdn: FQDN = fqdn
.parse()
.map_err(|_err| Error::from(ErrorKind::InvalidCustomDomain))?;

let account = AccountWrapper::from(credentials).0;
let fqdn_clone = fqdn.clone();
service
.new_task()
.project(project_name)
.and_then(task::run(move |ctx| {
let service_clone = service.clone();
let fqdn_clone_clone = fqdn_clone.clone();
let acme_client_clone = acme_client.clone();
let account_clone = account.clone();
let resolve_clone = resolver.clone();
async move {
// If project not ready yet, don't attept certificare renewal.
if !ctx.state.is_ready() {
return TaskResult::Pending(ctx.state);
}

// Try retrieve the current certificate if any.
match service_clone
.project_details_for_custom_domain(&fqdn_clone_clone)
// Try retrieve the current certificate if any.
match service.project_details_for_custom_domain(&fqdn).await {
Ok(CustomDomain { certificate, .. }) => {
let (_, x509_cert_chain) = parse_x509_certificate(certificate.as_bytes())
.unwrap_or_else(|_| {
panic!(
"Malformed existing X509 certificate for {} project.",
project_name
)
});
let diff = x509_cert_chain
.validity()
.not_after
.sub(ASN1Time::now())
.unwrap();
// If current certificate validity less_or_eq than 30 days, attempt
// renewal.
if diff.whole_days() <= 30 {
return match acme_client
.create_certificate(&fqdn.to_string(), ChallengeType::Http01, credentials)
.await
{
Ok(CustomDomain { certificate, .. }) => {
let (_, x509_cert_chain) =
parse_x509_certificate(certificate.as_bytes()).unwrap();
let diff = x509_cert_chain
.validity()
.not_after
.sub(ASN1Time::now())
.unwrap();
// If current certificate validity less_or_eq than 30 days, attempt
// renewal.
if diff.whole_days() <= 30 {
return match acme_client_clone
.create_certificate(
&fqdn_clone_clone.to_string(),
ChallengeType::Http01,
&account_clone,
)
.await
{
// If successfuly created, save the certificate in memory to be
// served in the future.
Ok((certs, private_key)) => {
let mut buf = Vec::new();
buf.extend(certs.as_bytes());
buf.extend(private_key.as_bytes());
match resolve_clone
.serve_pem(&fqdn_clone_clone.to_string(), Cursor::new(buf))
.await
{
Ok(_) => TaskResult::Pending(ctx.state),
Err(err) => TaskResult::Err(err),
}
}
Err(err) => TaskResult::Err(err.into()),
};
};
TaskResult::Pending(ctx.state)
// If successfuly created, save the certificate in memory to be
// served in the future.
Ok((certs, private_key)) => {
let mut buf = Vec::new();
buf.extend(certs.as_bytes());
buf.extend(private_key.as_bytes());
resolver
.serve_pem(&fqdn.to_string(), Cursor::new(buf))
.await?;
Ok(format!("Certificate renewed for {} project.", project_name))
}
Err(err) => TaskResult::Err(err),
}
Err(err) => Err(err.into()),
};
} else {
Ok(format!(
"Certificate renewal skipped, {} project certificate still valid for {} days.",
project_name, diff
))
}
}))
.send(&sender)
.await?;
}
Err(err) => Err(err),
}
}

Ok("automated certificate renewal started".to_string())
#[instrument(skip_all)]
async fn renew_gateway_acme_certificate(
_: Admin,
State(RouterState { service, .. }): State<RouterState>,
Extension(acme_client): Extension<AcmeClient>,
Extension(resolver): Extension<Arc<GatewayCertResolver>>,
AxumJson(credentials): AxumJson<AccountCredentials<'_>>,
) -> Result<String, Error> {
service
.renew_certificate(&acme_client, resolver, credentials)
.await;
Ok("Renewed the gate certificate.".to_string())
}

async fn get_projects(
Expand Down Expand Up @@ -487,11 +476,15 @@ impl ApiBuilder {
.route("/admin/acme/:email", post(create_acme_account))
.route(
"/admin/acme/request/:project_name/:fqdn",
post(request_acme_certificate),
post(request_custom_domain_acme_certificate),
)
.route(
"/admin/acme/renew/:project_name/:fqdn",
post(renew_acme_certificate),
post(renew_custom_domain_acme_certificate),
)
.route(
"/admin/acme/gateway/renew",
post(renew_gateway_acme_certificate),
)
.layer(Extension(acme))
.layer(Extension(resolver));
Expand Down Expand Up @@ -596,7 +589,7 @@ pub mod tests {
#[tokio::test]
async fn api_create_get_delete_projects() -> anyhow::Result<()> {
let world = World::new().await;
let service = Arc::new(GatewayService::init(world.args(), world.pool()).await);
let service = Arc::new(GatewayService::init(world.args(), world.pool(), "".into()).await);

let (sender, mut receiver) = channel::<BoxedTask>(256);
tokio::spawn(async move {
Expand Down Expand Up @@ -744,7 +737,7 @@ pub mod tests {
#[tokio::test]
async fn api_create_get_users() -> anyhow::Result<()> {
let world = World::new().await;
let service = Arc::new(GatewayService::init(world.args(), world.pool()).await);
let service = Arc::new(GatewayService::init(world.args(), world.pool(), "".into()).await);

let (sender, mut receiver) = channel::<BoxedTask>(256);
tokio::spawn(async move {
Expand Down Expand Up @@ -837,7 +830,7 @@ pub mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn status() {
let world = World::new().await;
let service = Arc::new(GatewayService::init(world.args(), world.pool()).await);
let service = Arc::new(GatewayService::init(world.args(), world.pool(), "".into()).await);

let (sender, mut receiver) = channel::<BoxedTask>(1);
let (ctl_send, ctl_recv) = oneshot::channel();
Expand Down
2 changes: 1 addition & 1 deletion gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ pub mod tests {
#[tokio::test]
async fn end_to_end() {
let world = World::new().await;
let service = Arc::new(GatewayService::init(world.args(), world.pool()).await);
let service = Arc::new(GatewayService::init(world.args(), world.pool(), "".into()).await);
let worker = Worker::new();

let (log_out, mut log_in) = channel(256);
Expand Down
Loading

0 comments on commit 7af6922

Please sign in to comment.