Implement haproxy live cert reload
- Implemented live certificate reloading for haproxy using the runtime api. This does NOT remove deleted certificates from haproxy - Bump version to 0.2.0
This commit is contained in:
parent
7e1ba11c4d
commit
096ac29f26
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -350,7 +350,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "le-easy-certs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"acme2",
|
||||
"env_logger",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "le-easy-certs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
pub http: ConfigHttp,
|
||||
pub certs: HashMap<String, ConfigCert>,
|
||||
pub haproxy: Option<ConfigHaProxyRuntime>,
|
||||
}
|
||||
|
||||
/// Config for the http server used to serve the challenges
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ConfigHttp {
|
||||
/// Binding IP address
|
||||
pub ip: String,
|
||||
@ -16,8 +17,19 @@ pub struct ConfigHttp {
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Config for a haproxy runtime api endpoint
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ConfigHaProxyRuntime {
|
||||
/// Ip address (or hostname) for the haproxy runtime api
|
||||
pub ip: String,
|
||||
/// Port for the haproxy runtime api
|
||||
pub port: u16,
|
||||
/// The directory path on the haproxy host that contains the certificates
|
||||
pub cert_dir: String,
|
||||
}
|
||||
|
||||
/// Configuration for a single certificate
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ConfigCert {
|
||||
/// Path to the account file to use for this certificate request
|
||||
pub account_file: String,
|
||||
|
||||
109
src/haproxy.rs
Normal file
109
src/haproxy.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
use log::debug;
|
||||
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, time::timeout};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum HaProxyErr {
|
||||
#[error("Connection error")]
|
||||
Connection,
|
||||
#[error("Update failed")]
|
||||
UpdateFailed,
|
||||
}
|
||||
|
||||
pub struct HaProxyApi {
|
||||
ip: String,
|
||||
port: u16,
|
||||
cert_dir: String,
|
||||
}
|
||||
|
||||
impl HaProxyApi {
|
||||
pub fn new(ip: &str, port: u16, cert_dir: &str) -> Self {
|
||||
let ip = ip.to_string();
|
||||
let cert_dir = cert_dir.to_string();
|
||||
Self { ip, port, cert_dir }
|
||||
}
|
||||
|
||||
pub async fn update_cert<T: AsRef<Path>>(&mut self, cert_path: T) -> Result<(), HaProxyErr> {
|
||||
let cert_path = cert_path.as_ref();
|
||||
let cert_name = cert_path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let cert_content = tokio::fs::read_to_string(cert_path).await.unwrap();
|
||||
let haproxy_ssl_dir = &self.cert_dir;
|
||||
|
||||
debug!("Trying to update haproxy cert in memory: {haproxy_ssl_dir}/{cert_name} <- {}", cert_path.display());
|
||||
|
||||
// Check for active transactions and abort them
|
||||
let resp = self.send(&format!("show ssl cert")).await?;
|
||||
let transactions = resp.lines().filter(|line| line.starts_with("*")).map(|line| &line[1..]).collect::<Vec<_>>();
|
||||
for transaction in transactions {
|
||||
self.send(&format!("abort ssl cert {transaction}")).await?;
|
||||
}
|
||||
|
||||
// Try to create a new certificate in memory. This will do nothing when existing
|
||||
let resp = self.send(&format!("new ssl cert {haproxy_ssl_dir}/{cert_name}")).await?;
|
||||
|
||||
if !resp.contains("already exists") && !resp.contains("New empty certificate store ") {
|
||||
return Err(HaProxyErr::UpdateFailed);
|
||||
}
|
||||
|
||||
// Set the cert data in memory to the new cert. This beginns a transaction
|
||||
let resp = self.send(&format!("set ssl cert {haproxy_ssl_dir}/{cert_name} <<\n{cert_content}")).await?;
|
||||
|
||||
if !resp.contains("Transaction created") {
|
||||
return Err(HaProxyErr::UpdateFailed);
|
||||
}
|
||||
|
||||
// Commit the cert data transaction
|
||||
let resp = self.send(&format!("commit ssl cert {haproxy_ssl_dir}/{cert_name}")).await?;
|
||||
|
||||
if !resp.contains("Success") {
|
||||
self.send(&format!("abort ssl cert {haproxy_ssl_dir}/{cert_name}")).await?;
|
||||
return Err(HaProxyErr::UpdateFailed);
|
||||
}
|
||||
|
||||
// Try to add the certificate to the active list. This will do nothing if it is contained already
|
||||
let resp = self.send(&format!("add ssl crt-list {haproxy_ssl_dir} <<\n{haproxy_ssl_dir}/{cert_name}\n")).await?;
|
||||
|
||||
if !resp.contains("already exists") && !resp.contains("Inserting certificate") {
|
||||
return Err(HaProxyErr::UpdateFailed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(&self, cmd: &str) -> Result<String, HaProxyErr> {
|
||||
debug!("[Haproxy] send> '{cmd}'");
|
||||
|
||||
let mut con = tokio::net::TcpStream::connect((self.ip.as_str(), self.port))
|
||||
.await
|
||||
.map_err(|_| HaProxyErr::Connection)?;
|
||||
|
||||
con.write_all(cmd.as_bytes())
|
||||
.await
|
||||
.map_err(|_| HaProxyErr::Connection)?;
|
||||
|
||||
con.write_all(&[b'\n'])
|
||||
.await
|
||||
.map_err(|_| HaProxyErr::Connection)?;
|
||||
|
||||
con.flush().await.unwrap();
|
||||
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0_u8; 1024];
|
||||
|
||||
loop {
|
||||
match timeout(Duration::from_millis(5_000), con.read(&mut buf)).await {
|
||||
// No bytes read, or timeout
|
||||
Ok(Ok(0)) | Err(_) => break,
|
||||
Ok(Ok(read)) => {
|
||||
response.extend_from_slice(&buf[..read]);
|
||||
}
|
||||
Ok(Err(_)) => return Err(HaProxyErr::Connection),
|
||||
}
|
||||
}
|
||||
|
||||
let response = String::from_utf8_lossy(&response).to_string();
|
||||
debug!("[Haproxy] receive> '{response}'");
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
18
src/main.rs
18
src/main.rs
@ -3,12 +3,13 @@ use log::{debug, error, info};
|
||||
use crate::{
|
||||
certs::{load_cert_from_fullchain, AcmeApiEndpoint, CertExt, CertRequester},
|
||||
config::Config,
|
||||
http::ChallengeServer,
|
||||
http::ChallengeServer, haproxy::HaProxyApi,
|
||||
};
|
||||
|
||||
mod certs;
|
||||
mod config;
|
||||
mod http;
|
||||
mod haproxy;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@ -31,6 +32,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
debug!("Config file: {:#?}", &conf);
|
||||
|
||||
let mut haproxyapi = conf.haproxy.map(|ha| HaProxyApi::new(&ha.ip, ha.port, &ha.cert_dir));
|
||||
|
||||
// Create the http server for serving the challenges
|
||||
let srv = ChallengeServer::new(&conf.http.ip, conf.http.port)?;
|
||||
let mgr = srv.clone_challenge_mgr();
|
||||
@ -66,9 +69,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
info!("Certificate {name} needs to be renewed. Using endpoint: {endpoint}");
|
||||
|
||||
let requester = CertRequester::new(endpoint, conf, mgr.clone());
|
||||
if let Err(e) = requester.request_certs().await {
|
||||
error!("Certificate request for {name} failed: {e}");
|
||||
let requester = CertRequester::new(endpoint, conf.clone(), mgr.clone());
|
||||
match (requester.request_certs().await, &mut haproxyapi) {
|
||||
(Ok(_), Some(api)) => {
|
||||
match api.update_cert(&conf.fullchain_file).await {
|
||||
Ok(()) => info!("Certificate update in haproxy completed"),
|
||||
Err(e) => error!("Certificate update in haproxy failed: {e}")
|
||||
}
|
||||
}
|
||||
(Err(e), _) => error!("Certificate request for {name} failed: {e}"),
|
||||
_ => ()
|
||||
}
|
||||
} else {
|
||||
info!("Certificate {name} does not need to be renewed");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user