From 7f87b1ad44893212857e67b5442f65b8b24e3530 Mon Sep 17 00:00:00 2001 From: Johan Thomsen Date: Wed, 5 Jun 2024 22:05:51 +0200 Subject: [PATCH 1/7] chanllenges: prepare for plugging multiple drivers alongside the nsupdate exec --- config.json | 9 +- src/common.rs | 26 +++--- src/config.rs | 19 +++- src/dns.rs | 210 -------------------------------------------- src/dns/mod.rs | 127 +++++++++++++++++++++++++++ src/dns/nsupdate.rs | 153 ++++++++++++++++++++++++++++++++ src/issuer.rs | 2 +- 7 files changed, 318 insertions(+), 228 deletions(-) delete mode 100644 src/dns.rs create mode 100644 src/dns/mod.rs create mode 100644 src/dns/nsupdate.rs diff --git a/config.json b/config.json index 9dcfd1a..e0eb92e 100644 --- a/config.json +++ b/config.json @@ -17,8 +17,13 @@ "lets_encrypt_email": "le@example.com", "zones": { "acme.example.com": { - "server": "ns.example.com", - "key": "acme.example.com.key" + "auth_dns_server": "ns.example.com", + "challenge_driver": { + "nsupdate": { + "server": "ns.example.com", + "key": "acme.example.com.key" + } + } } }, "val_dns_servers": ["8.8.8.8", "8.8.4.4"] diff --git a/src/common.rs b/src/common.rs index 8806efd..041a20f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -139,9 +139,9 @@ impl CertSpec { } pub fn get_auth_dns_servers(&self, config: &FaytheConfig) -> Result, SpecError> { let mut res = HashSet::new(); - res.insert(self.cn.find_zone(&config)?.server.clone()); + res.insert(self.cn.find_zone(&config)?.auth_dns_server.clone()); for s in &self.sans { - res.insert(s.find_zone(&config)?.server.clone()); + res.insert(s.find_zone(&config)?.auth_dns_server.clone()); } Ok(res) } @@ -396,7 +396,7 @@ pub mod tests { use std::collections::HashMap; use crate::set; use super::DNSName; - use crate::config::{KubeMonitorConfig, FileMonitorConfig, MonitorConfig}; + use crate::config::{ChallengeDriver, KubeMonitorConfig, FileMonitorConfig, MonitorConfig}; use chrono::DateTime; const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z"; // 2019-10-09T11:50:22+0200 @@ -432,20 +432,20 @@ pub mod tests { fn create_zones(issue_wildcard_certs: bool) -> HashMap { let mut zones = HashMap::new(); zones.insert(String::from("unit.test"), Zone{ - server: String::from("ns.unit.test"), - key: String::new(), + auth_dns_server: String::from("ns.unit.test"), + challenge_driver: ChallengeDriver::NoOp, challenge_suffix: None, issue_wildcard_certs }); zones.insert(String::from("alternative.unit.test"), Zone{ - server: String::from("ns.alternative.unit.test"), - key: String::new(), + auth_dns_server: String::from("ns.alternative.unit.test"), + challenge_driver: ChallengeDriver::NoOp, challenge_suffix: None, issue_wildcard_certs }); zones.insert(String::from("suffixed.unit.test"), Zone{ - server: String::from("ns.suffixed.unit.test"), - key: String::new(), + auth_dns_server: String::from("ns.suffixed.unit.test"), + challenge_driver: ChallengeDriver::NoOp, challenge_suffix: Some(String::from("acme.example.com")), issue_wildcard_certs }); @@ -664,19 +664,19 @@ pub mod tests { let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.unit.test")).unwrap(); let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.server, "ns.unit.test"); + assert_eq!(z.auth_dns_server, "ns.unit.test"); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.alternative.unit.test")).unwrap(); let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.server, "ns.alternative.unit.test"); + assert_eq!(z.auth_dns_server, "ns.alternative.unit.test"); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.other-alternative.unit.test")).unwrap(); let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.server, "ns.unit.test"); + assert_eq!(z.auth_dns_server, "ns.unit.test"); let host: DNSName = DNSName::try_from(&String::from("unit.test")).unwrap(); let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.server, "ns.unit.test"); + assert_eq!(z.auth_dns_server, "ns.unit.test"); } } diff --git a/src/config.rs b/src/config.rs index 6d96e6e..6b20d5a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -91,13 +91,28 @@ pub struct ConfigContainer { #[derive(Clone, Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct Zone { - pub server: String, - pub key: String, + pub auth_dns_server: String, + pub challenge_driver: ChallengeDriver, pub challenge_suffix: Option, #[serde(default = "default_issue_wildcard_certs")] pub issue_wildcard_certs: bool, } +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "lowercase")] +pub enum ChallengeDriver { + NSUpdate(NSUpdateDriver), + NoOp, +} + +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct NSUpdateDriver { + pub server: String, + pub key: String, +} + impl ConfigContainer { pub fn get_kube_monitor_config(&self) -> Result<&KubeMonitorConfig, SpecError> { Ok(match &self.monitor_config { diff --git a/src/dns.rs b/src/dns.rs deleted file mode 100644 index e567040..0000000 --- a/src/dns.rs +++ /dev/null @@ -1,210 +0,0 @@ -extern crate trust_dns_resolver; - -use std::process::{Command, Stdio}; -use std::result::Result; -use crate::FaytheConfig; - -use std::convert::From; - -use crate::exec::{SpawnOk, OpenStdin, Wait, ExecErrorInfo}; -use crate::log; -use crate::common::{CertSpec, DNSName, SpecError}; -use crate::config::Zone; -use self::trust_dns_resolver::Resolver; -use self::trust_dns_resolver::error::{ResolveError,ResolveErrorKind}; -use std::string::String; - -#[derive(Debug)] -pub enum DNSError { - Exec(ExecErrorInfo), - IO(std::io::Error), - OutputFormat, - ResolveError(ResolveError), - WrongAnswer(String), - WrongSpec -} - -pub fn add(config: &FaytheConfig, name: &DNSName, proof: &String) -> Result<(), DNSError> { - let zone = name.find_zone(&config)?; - let command = add_cmd(zone, &name, &proof); - update_dns(&command, &zone) -} - -fn add_cmd(zone: &Zone, name: &DNSName, proof: &String) -> String { - format!("server {server}\n\ - update add {host} 120 TXT \"{proof}\"\n\ - send\n", - server=&zone.server, - host=&challenge_host(&name, Some(&zone)), - proof=&proof) -} - -pub fn delete(config: &FaytheConfig, spec: &CertSpec) -> Result<(), DNSError> { - let zone = spec.cn.find_zone(&config)?; - let command = delete_cmd(zone, &spec.cn); - update_dns(&command, &zone)?; - for s in &spec.sans { - let zone = s.find_zone(&config)?; - let command = delete_cmd(zone, &s); - update_dns(&command, &zone)? - } - Ok(()) -} - -fn delete_cmd(zone: &Zone, name: &DNSName) -> String { - format!("server {server}\n\ - update delete {host} TXT\n\ - send\n", - server=&zone.server, - host=challenge_host(&name, Some(&zone))) -} - -pub fn query(resolver: &Resolver, host: &DNSName, proof: &String) -> Result<(), DNSError> { - let c_host = challenge_host(host, None); - match resolver.txt_lookup(c_host.as_str()) { - Ok(res) => { - let trim_chars: &[_] = &['"', '\n']; - res.iter().find(|rr| - rr.iter().find(|r| { - match String::from_utf8((*r).to_vec()) { - Ok(txt) => &txt.trim_matches(trim_chars) == proof, - Err(_) => false, - } - }).is_some() - ).ok_or(DNSError::WrongAnswer(c_host.clone())).and(Ok(())) - }, - Err(e) => { - match e.kind() { - ResolveErrorKind::NoRecordsFound{..} => Err(DNSError::WrongAnswer(c_host.clone())), - _ => Err(DNSError::ResolveError(e)) - } - } - } -} - -fn challenge_host(host: &DNSName, zone: Option<&Zone>) -> String { - let suffix = match zone { - Some(z) => match &z.challenge_suffix { - Some(s) => format!(".{}", s), - None => String::new() - } - None => String::new() - }; - format!("_acme-challenge.{}{}.", &host.to_parent_domain_string(), &suffix) -} - -fn update_dns(command: &String, zone: &Zone) -> Result<(), DNSError> { - let mut cmd = Command::new("nsupdate"); - let mut child = cmd.arg("-k") - .arg(&zone.key) - .stdin(Stdio::piped()) - .spawn_ok()?; - { - child.stdin_write(command)?; - } - - Ok(child.wait()?) -} - - -impl From for DNSError { - fn from(e: std::io::Error) -> DNSError { - DNSError::IO(e) - } -} - -impl From for DNSError { - fn from(_: std::string::FromUtf8Error) -> DNSError { - DNSError::OutputFormat - } -} - -impl std::convert::From for DNSError { - fn from(err: ExecErrorInfo) -> Self { - log::error("Failed to exec dns command", &err); - DNSError::Exec(err) - } -} - -impl std::convert::From for DNSError { - fn from(err: SpecError) -> Self { - log::error("Faythe does not know a dns-server authoritative for", &err); - DNSError::WrongSpec - } -} - -#[cfg(test)] -mod tests { - // Note this useful idiom: importing names from outer (for mod tests) scope. - use super::*; - use crate::common::PersistSpec::DONTPERSIST; - use std::convert::TryFrom; - use std::collections::HashSet; - use crate::common::tests::*; - - fn create_cert_spec(cn: &String) -> CertSpec { - let dns_name = DNSName::try_from(cn).unwrap(); - CertSpec{ - name: String::from("test"), - cn: dns_name, - sans: HashSet::new(), - persist_spec: DONTPERSIST, - } - } - - #[test] - fn test_add_normal() { - let config = create_test_kubernetes_config(false); - let spec = create_cert_spec(&String::from("moo.unit.test")); - let proof = String::from("abcdef1234"); - let zone = config.faythe_config.zones.get("unit.test").unwrap(); - - assert_eq!(add_cmd(zone, &spec.cn, &proof), - "server ns.unit.test\nupdate add _acme-challenge.moo.unit.test. 120 TXT \"abcdef1234\"\nsend\n") - } - - #[test] - fn test_add_wildcard() { - let config = create_test_kubernetes_config(false); - let spec = create_cert_spec(&String::from("*.unit.test")); - let proof = String::from("abcdef1234"); - let zone = config.faythe_config.zones.get("unit.test").unwrap(); - - assert_eq!(add_cmd(zone, &spec.cn, &proof), - "server ns.unit.test\nupdate add _acme-challenge.unit.test. 120 TXT \"abcdef1234\"\nsend\n") - } - - #[test] - fn test_delete_normal() { - let config = create_test_kubernetes_config(false); - let spec = create_cert_spec(&String::from("moo.unit.test")); - let zone = config.faythe_config.zones.get("unit.test").unwrap(); - - assert_eq!(delete_cmd(zone, &spec.cn), - "server ns.unit.test\nupdate delete _acme-challenge.moo.unit.test. TXT\nsend\n") - } - - #[test] - fn test_delete_wildcard() { - let config = create_test_kubernetes_config(false); - let spec = create_cert_spec(&String::from("*.unit.test")); - let zone = config.faythe_config.zones.get("unit.test").unwrap(); - - assert_eq!(delete_cmd(zone, &spec.cn), - "server ns.unit.test\nupdate delete _acme-challenge.unit.test. TXT\nsend\n") - } - - #[test] - fn test_challenge_suffix() { - let config = create_test_kubernetes_config(false); - let spec = create_cert_spec(&String::from("*.suffixed.unit.test")); - let proof = String::from("abcdef1234"); - let zone = config.faythe_config.zones.get("suffixed.unit.test").unwrap(); - - assert_eq!(add_cmd(zone, &spec.cn, &proof), - "server ns.suffixed.unit.test\nupdate add _acme-challenge.suffixed.unit.test.acme.example.com. 120 TXT \"abcdef1234\"\nsend\n"); - - assert_eq!(delete_cmd(zone, &spec.cn), - "server ns.suffixed.unit.test\nupdate delete _acme-challenge.suffixed.unit.test.acme.example.com. TXT\nsend\n") - } -} diff --git a/src/dns/mod.rs b/src/dns/mod.rs new file mode 100644 index 0000000..d77f464 --- /dev/null +++ b/src/dns/mod.rs @@ -0,0 +1,127 @@ +extern crate trust_dns_resolver; + +use std::result::Result; +use crate::FaytheConfig; + +use std::convert::From; + +use crate::log; +use crate::common::{CertSpec, DNSName, SpecError}; +use crate::config::Zone; +use self::trust_dns_resolver::Resolver; +use self::trust_dns_resolver::error::{ResolveError,ResolveErrorKind}; +use std::string::String; + +use crate::exec::ExecErrorInfo; + +use crate::config::{ChallengeDriver as DriverConfig}; + +mod nsupdate; + +#[derive(Debug)] +pub enum DNSError { + Exec(ExecErrorInfo), + IO(std::io::Error), + OutputFormat, + ResolveError(ResolveError), + WrongAnswer(String), + WrongSpec +} + +pub trait ChallengeDriver { + fn add(&self, challenge_host: &String, proof: &String) -> Result<(), DNSError>; + fn delete(&self, challenge_host: &String) -> Result<(), DNSError>; +} + +pub fn add(config: &FaytheConfig, name: &DNSName, proof: &String) -> Result<(), DNSError> { + let zone = name.find_zone(&config)?; + let challenge_host = challenge_host(name, Some(&zone)); + match &zone.challenge_driver { + DriverConfig::NSUpdate(nsupdate) => { + nsupdate.add(&challenge_host, proof) + }, + DriverConfig::NoOp => Ok(()) + } +} + +pub fn delete(config: &FaytheConfig, spec: &CertSpec) -> Result<(), DNSError> { + let zone = spec.cn.find_zone(&config)?; + let host = challenge_host(&spec.cn, Some(&zone)); + match &zone.challenge_driver { + DriverConfig::NSUpdate(nsupdate) => { + nsupdate.delete(&host)? + }, + DriverConfig::NoOp => () + }; + for s in &spec.sans { + let zone = s.find_zone(&config)?; + let host = challenge_host(s, Some(&zone)); + match &zone.challenge_driver { + DriverConfig::NSUpdate(nsupdate) => { + nsupdate.delete(&host)? + }, + DriverConfig::NoOp => () + } + } + Ok(()) +} + +pub fn query(resolver: &Resolver, host: &DNSName, proof: &String) -> Result<(), DNSError> { + let c_host = challenge_host(host, None); + match resolver.txt_lookup(&c_host) { + Ok(res) => { + let trim_chars: &[_] = &['"', '\n']; + res.iter().find(|rr| + rr.iter().find(|r| { + match String::from_utf8((*r).to_vec()) { + Ok(txt) => &txt.trim_matches(trim_chars) == proof, + Err(_) => false, + } + }).is_some() + ).ok_or(DNSError::WrongAnswer(c_host.clone())).and(Ok(())) + }, + Err(e) => { + match e.kind() { + ResolveErrorKind::NoRecordsFound{..} => Err(DNSError::WrongAnswer(c_host.clone())), + _ => Err(DNSError::ResolveError(e)) + } + } + } +} + +fn challenge_host(host: &DNSName, zone: Option<&Zone>) -> String { + let suffix = match zone { + Some(z) => match &z.challenge_suffix { + Some(s) => format!(".{}", s), + None => String::new() + } + None => String::new() + }; + format!("_acme-challenge.{}{}.", &host.to_parent_domain_string(), &suffix) +} + +impl From for DNSError { + fn from(e: std::io::Error) -> DNSError { + DNSError::IO(e) + } +} + +impl From for DNSError { + fn from(_: std::string::FromUtf8Error) -> DNSError { + DNSError::OutputFormat + } +} + +impl std::convert::From for DNSError { + fn from(err: ExecErrorInfo) -> Self { + log::error("Failed to exec dns command", &err); + DNSError::Exec(err) + } +} + +impl std::convert::From for DNSError { + fn from(err: SpecError) -> Self { + log::error("Faythe does not know a dns-server authoritative for", &err); + DNSError::WrongSpec + } +} diff --git a/src/dns/nsupdate.rs b/src/dns/nsupdate.rs new file mode 100644 index 0000000..99f2883 --- /dev/null +++ b/src/dns/nsupdate.rs @@ -0,0 +1,153 @@ +use std::process::{Command, Stdio}; +use crate::exec::{SpawnOk, OpenStdin, Wait}; +use crate::dns::DNSError; + +use super::ChallengeDriver; +use crate::config::NSUpdateDriver; + +impl ChallengeDriver for NSUpdateDriver { + fn add(&self, challenge_host: &String, proof: &String) -> Result<(), DNSError> { + let command = self.add_cmd(&challenge_host, &proof); + self.update_dns(&command) + } + + fn delete(&self, challenge_host: &String) -> Result<(), DNSError> { + let command = self.delete_cmd(&challenge_host); + self.update_dns(&command) + } +} + +impl NSUpdateDriver { + fn update_dns(&self, command: &String) -> Result<(), DNSError> { + let mut cmd = Command::new("nsupdate"); + let mut child = cmd.arg("-k") + .arg(&self.key) + .stdin(Stdio::piped()) + .spawn_ok()?; + { + child.stdin_write(command)?; + } + + Ok(child.wait()?) + } + + fn add_cmd(&self, name: &String, proof: &String) -> String { + format!("server {server}\n\ + update add {host} 120 TXT \"{proof}\"\n\ + send\n", + server=&self.server, + host=&name, + proof=&proof) + } + + fn delete_cmd(&self, name: &String) -> String { + format!("server {server}\n\ + update delete {host} TXT\n\ + send\n", + server=&self.server, + host=&name) + } +} + +#[cfg(test)] +mod tests { + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + use crate::common::PersistSpec::DONTPERSIST; + use std::convert::TryFrom; + use std::collections::HashSet; + use crate::common::tests::*; + use crate::common::{CertSpec, DNSName}; + use crate::dns::challenge_host; + + fn create_cert_spec(cn: &String) -> CertSpec { + let dns_name = DNSName::try_from(cn).unwrap(); + CertSpec{ + name: String::from("test"), + cn: dns_name, + sans: HashSet::new(), + persist_spec: DONTPERSIST, + } + } + + #[test] + fn test_add_normal() { + let config = create_test_kubernetes_config(false); + let spec = create_cert_spec(&String::from("moo.unit.test")); + let proof = String::from("abcdef1234"); + let zone = config.faythe_config.zones.get("unit.test").unwrap(); + let driver = NSUpdateDriver { + server: String::from("ns.unit.test"), + key: String::from("key") + }; + + let host = challenge_host(&spec.cn, Some(&zone)); + assert_eq!(driver.add_cmd(&host, &proof), + "server ns.unit.test\nupdate add _acme-challenge.moo.unit.test. 120 TXT \"abcdef1234\"\nsend\n") + } + + #[test] + fn test_add_wildcard() { + let config = create_test_kubernetes_config(false); + let spec = create_cert_spec(&String::from("*.unit.test")); + let proof = String::from("abcdef1234"); + let zone = config.faythe_config.zones.get("unit.test").unwrap(); + let driver = NSUpdateDriver { + server: String::from("ns.unit.test"), + key: String::from("key") + }; + + let host = challenge_host(&spec.cn, Some(&zone)); + assert_eq!(driver.add_cmd(&host, &proof), + "server ns.unit.test\nupdate add _acme-challenge.unit.test. 120 TXT \"abcdef1234\"\nsend\n") + } + + #[test] + fn test_delete_normal() { + let config = create_test_kubernetes_config(false); + let spec = create_cert_spec(&String::from("moo.unit.test")); + let zone = config.faythe_config.zones.get("unit.test").unwrap(); + let driver = NSUpdateDriver { + server: String::from("ns.unit.test"), + key: String::from("key") + }; + + let host = challenge_host(&spec.cn, Some(&zone)); + assert_eq!(driver.delete_cmd(&host), + "server ns.unit.test\nupdate delete _acme-challenge.moo.unit.test. TXT\nsend\n") + } + + #[test] + fn test_delete_wildcard() { + let config = create_test_kubernetes_config(false); + let spec = create_cert_spec(&String::from("*.unit.test")); + let zone = config.faythe_config.zones.get("unit.test").unwrap(); + let driver = NSUpdateDriver { + server: String::from("ns.unit.test"), + key: String::from("key") + }; + + let host = challenge_host(&spec.cn, Some(&zone)); + assert_eq!(driver.delete_cmd(&host), + "server ns.unit.test\nupdate delete _acme-challenge.unit.test. TXT\nsend\n") + } + + #[test] + fn test_challenge_suffix() { + let config = create_test_kubernetes_config(false); + let spec = create_cert_spec(&String::from("*.suffixed.unit.test")); + let proof = String::from("abcdef1234"); + let zone = config.faythe_config.zones.get("suffixed.unit.test").unwrap(); + let driver = NSUpdateDriver { + server: String::from("ns.suffixed.unit.test"), + key: String::from("key") + }; + + let host = challenge_host(&spec.cn, Some(&zone)); + assert_eq!(driver.add_cmd(&host, &proof), + "server ns.suffixed.unit.test\nupdate add _acme-challenge.suffixed.unit.test.acme.example.com. 120 TXT \"abcdef1234\"\nsend\n"); + + assert_eq!(driver.delete_cmd(&host), + "server ns.suffixed.unit.test\nupdate delete _acme-challenge.suffixed.unit.test.acme.example.com. TXT\nsend\n") + } +} diff --git a/src/issuer.rs b/src/issuer.rs index a739cc4..9a2a0e9 100644 --- a/src/issuer.rs +++ b/src/issuer.rs @@ -309,7 +309,7 @@ fn init_resolvers<'l>(config: &FaytheConfig) -> HashMap { }; }; - config.zones.iter().for_each(|(_, z)| create_resolvers(&z.server, &mut resolvers)); + config.zones.iter().for_each(|(_, z)| create_resolvers(&z.auth_dns_server, &mut resolvers)); config.val_dns_servers.iter().for_each(|s| create_resolvers(&s, &mut resolvers)); resolvers } From 5bab1312ddba71bd020ea07f08eac979d097af6f Mon Sep 17 00:00:00 2001 From: Johan Thomsen Date: Thu, 6 Jun 2024 23:25:54 +0200 Subject: [PATCH 2/7] challenges: implement simple webhook driver --- Cargo.lock | 1 + Cargo.toml | 1 + src/config.rs | 14 +++++++++ src/dns/mod.rs | 26 +++++++++++++--- src/dns/webhook.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/dns/webhook.rs diff --git a/Cargo.lock b/Cargo.lock index 7788bfb..7f7f1d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,6 +476,7 @@ dependencies = [ "openssl", "prometheus_exporter_base", "regex", + "reqwest", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c32e10d..f4af059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ vaultrs-login = "0" url = "2" chrono = "0" num-traits = "0" +reqwest = { version = "0", features = ["json"] } [profile.release] panic = "abort" diff --git a/src/config.rs b/src/config.rs index 6b20d5a..ac039c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,6 +104,7 @@ pub struct Zone { pub enum ChallengeDriver { NSUpdate(NSUpdateDriver), NoOp, + Webhook(WebhookDriver), } #[derive(Clone, Deserialize, Debug)] @@ -113,6 +114,19 @@ pub struct NSUpdateDriver { pub key: String, } +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct WebhookDriver { + #[serde(deserialize_with = "deserialize_url")] + pub url: Url, + #[serde(default = "default_timeout_secs")] + pub timeout_secs: u8, +} + +fn default_timeout_secs() -> u8 { + 10 +} + impl ConfigContainer { pub fn get_kube_monitor_config(&self) -> Result<&KubeMonitorConfig, SpecError> { Ok(match &self.monitor_config { diff --git a/src/dns/mod.rs b/src/dns/mod.rs index d77f464..8a594f4 100644 --- a/src/dns/mod.rs +++ b/src/dns/mod.rs @@ -17,6 +17,7 @@ use crate::exec::ExecErrorInfo; use crate::config::{ChallengeDriver as DriverConfig}; mod nsupdate; +mod webhook; #[derive(Debug)] pub enum DNSError { @@ -25,7 +26,8 @@ pub enum DNSError { OutputFormat, ResolveError(ResolveError), WrongAnswer(String), - WrongSpec + WrongSpec, + Reqwest(reqwest::Error), } pub trait ChallengeDriver { @@ -40,7 +42,10 @@ pub fn add(config: &FaytheConfig, name: &DNSName, proof: &String) -> Result<(), DriverConfig::NSUpdate(nsupdate) => { nsupdate.add(&challenge_host, proof) }, - DriverConfig::NoOp => Ok(()) + DriverConfig::NoOp => Ok(()), + DriverConfig::Webhook(webhook) => { + webhook.add(&challenge_host, proof) + } } } @@ -51,7 +56,10 @@ pub fn delete(config: &FaytheConfig, spec: &CertSpec) -> Result<(), DNSError> { DriverConfig::NSUpdate(nsupdate) => { nsupdate.delete(&host)? }, - DriverConfig::NoOp => () + DriverConfig::NoOp => (), + DriverConfig::Webhook(webhook) => { + webhook.delete(&host)? + } }; for s in &spec.sans { let zone = s.find_zone(&config)?; @@ -60,7 +68,10 @@ pub fn delete(config: &FaytheConfig, spec: &CertSpec) -> Result<(), DNSError> { DriverConfig::NSUpdate(nsupdate) => { nsupdate.delete(&host)? }, - DriverConfig::NoOp => () + DriverConfig::NoOp => (), + DriverConfig::Webhook(webhook) => { + webhook.delete(&host)? + } } } Ok(()) @@ -125,3 +136,10 @@ impl std::convert::From for DNSError { DNSError::WrongSpec } } + +impl std::convert::From for DNSError { + fn from(err: reqwest::Error) -> Self { + log::error("Error with webhook invocation", &err); + DNSError::Reqwest(err) + } +} diff --git a/src/dns/webhook.rs b/src/dns/webhook.rs new file mode 100644 index 0000000..f37409f --- /dev/null +++ b/src/dns/webhook.rs @@ -0,0 +1,77 @@ +use crate::dns::DNSError; + +use super::ChallengeDriver; +use crate::config::WebhookDriver; + +use std::collections::HashMap; + +impl ChallengeDriver for WebhookDriver { + fn add(&self, challenge_host: &String, proof: &String) -> Result<(), DNSError> { + self.exec_add(Payload { + records: vec![(challenge_host.clone(), Record { + record_type: RecordType::TXT, + content: Some(proof.clone()), + })].into_iter().collect() + }) + } + + fn delete(&self, challenge_host: &String) -> Result<(), DNSError> { + self.exec_delete(Payload { + records: vec![(challenge_host.clone(), Record { + record_type: RecordType::TXT, + content: None, + })].into_iter().collect() + }) + } +} + +#[derive(Debug, Serialize)] +struct Payload { + records: HashMap, +} + +#[derive(Debug, Serialize)] +struct Record { + #[serde(rename = "type")] + record_type: RecordType, + content: Option, +} + +#[derive(Debug, Serialize)] +enum RecordType { + TXT, +} + +static APP_USER_AGENT: &str = concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION"), +); + +impl WebhookDriver { + + fn get_client(&self) -> Result { + let client = reqwest::blocking::Client::builder(); + let client = client.timeout(std::time::Duration::from_secs(self.timeout_secs as u64)); + let client = client.user_agent(APP_USER_AGENT); + client.build() + } + + fn exec_add(&self, body: Payload) -> Result<(), DNSError> { + self.get_client()? + .put(self.url.clone()) + .json(&body) + .send() + .and(Ok(())) + .map_err(std::convert::Into::into) + } + + fn exec_delete(&self, body: Payload) -> Result<(), DNSError> { + self.get_client()? + .delete(self.url.clone()) + .json(&body) + .send() + .and(Ok(())) + .map_err(std::convert::Into::into) + } +} From a1532c056243d8e3fb17218abf7e84d4939b7e41 Mon Sep 17 00:00:00 2001 From: Johan Thomsen Date: Fri, 7 Jun 2024 09:11:11 +0200 Subject: [PATCH 3/7] flake/check: very rudimentary check parse of sample config files --- .github/workflows/build.yml | 2 +- config-samples/file-webhook.json | 27 +++++++++++++++++++ .../vault-nsupdate.json | 0 flake.nix | 8 ++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 config-samples/file-webhook.json rename config.json => config-samples/vault-nsupdate.json (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfb1438..90ec1fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,4 +8,4 @@ jobs: - uses: cachix/install-nix-action@v27 with: nix_path: nixpkgs=channel:nixos-24.05 - - run: nix --experimental-features "nix-command flakes" build + - run: nix --experimental-features "nix-command flakes" check diff --git a/config-samples/file-webhook.json b/config-samples/file-webhook.json new file mode 100644 index 0000000..c8f28f3 --- /dev/null +++ b/config-samples/file-webhook.json @@ -0,0 +1,27 @@ +{ + "file_monitor_configs": [{ + "directory": "test", + "prune": false, + "specs": [{ + "name": "jth-test", + "cn": "jth.faythe-test.acme.example.com", + "sans": [ + "san1.jth.faythe-test.acme.example.com", + "san2.jth.faythe-test.acme.example.com" + ] + }] + }], + "lets_encrypt_url": "https://acme-staging-v02.api.letsencrypt.org/directory", + "lets_encrypt_email": "le@example.com", + "zones": { + "acme.example.com": { + "auth_dns_server": "ns.example.com", + "challenge_driver": { + "webhook": { + "url": "localhost:8080" + } + } + } + }, + "val_dns_servers": ["8.8.8.8", "8.8.4.4"] +} diff --git a/config.json b/config-samples/vault-nsupdate.json similarity index 100% rename from config.json rename to config-samples/vault-nsupdate.json diff --git a/flake.nix b/flake.nix index 464e3fb..ffaea52 100644 --- a/flake.nix +++ b/flake.nix @@ -54,6 +54,14 @@ }; }; + checks.${system}.sample-configs = pkgs.runCommandNoCC "check-sample-configs" { nativeBuildInputs = [ pkgs.${pname} ]; } '' + D=${./config-samples} + for F in $(ls -1 $D); do + echo "Testing: $D/$F" + faythe $D/$F --config-check >>$out + done + ''; + devShell.${system} = with pkgs; mkShell { buildInputs = [ rust-analyzer From 79065b033f51263ba46e009cc6caef801d3cbdf9 Mon Sep 17 00:00:00 2001 From: Johan Thomsen Date: Thu, 22 Aug 2024 23:02:50 +0200 Subject: [PATCH 4/7] addressing review comments --- flake.nix | 8 ++++---- src/common.rs | 36 ++++++++++++++++++------------------ src/dns/mod.rs | 22 +++++++++++----------- src/dns/nsupdate.rs | 14 +++++++------- src/dns/webhook.rs | 14 ++++++++------ 5 files changed, 48 insertions(+), 46 deletions(-) diff --git a/flake.nix b/flake.nix index ffaea52..0fd2f5a 100644 --- a/flake.nix +++ b/flake.nix @@ -55,10 +55,10 @@ }; checks.${system}.sample-configs = pkgs.runCommandNoCC "check-sample-configs" { nativeBuildInputs = [ pkgs.${pname} ]; } '' - D=${./config-samples} - for F in $(ls -1 $D); do - echo "Testing: $D/$F" - faythe $D/$F --config-check >>$out + DIR=${./config-samples} + for FILE in $(ls -1 $DIR); do + echo "Testing: $DIR/$FILE" + faythe $DIR/$FILE --config-check >>$out done ''; diff --git a/src/common.rs b/src/common.rs index 041a20f..0546cbf 100644 --- a/src/common.rs +++ b/src/common.rs @@ -639,44 +639,44 @@ pub mod tests { let config = create_test_kubernetes_config(false); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.unit.wrongtest")).unwrap(); - let z = host.find_zone(&config.faythe_config); - assert!(z.is_err()); + let zone = host.find_zone(&config.faythe_config); + assert!(zone.is_err()); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.foo.test")).unwrap(); - let z = host.find_zone(&config.faythe_config); - assert!(z.is_err()); + let zone = host.find_zone(&config.faythe_config); + assert!(zone.is_err()); let host: DNSName = DNSName::try_from(&String::from("test")).unwrap(); - let z = host.find_zone(&config.faythe_config); - assert!(z.is_err()); + let zone = host.find_zone(&config.faythe_config); + assert!(zone.is_err()); let host: DNSName = DNSName::try_from(&String::from("google.com")).unwrap(); - let z = host.find_zone(&config.faythe_config); - assert!(z.is_err()); + let zone = host.find_zone(&config.faythe_config); + assert!(zone.is_err()); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.unit.test")).unwrap(); - let z = host.find_zone(&config.faythe_config); - assert!(z.is_ok()); + let zone = host.find_zone(&config.faythe_config); + assert!(zone.is_ok()); } { let config = create_test_kubernetes_config(false); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.unit.test")).unwrap(); - let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.auth_dns_server, "ns.unit.test"); + let zone = host.find_zone(&config.faythe_config).unwrap(); + assert_eq!(zone.auth_dns_server, "ns.unit.test"); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.alternative.unit.test")).unwrap(); - let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.auth_dns_server, "ns.alternative.unit.test"); + let zone = host.find_zone(&config.faythe_config).unwrap(); + assert_eq!(zone.auth_dns_server, "ns.alternative.unit.test"); let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.other-alternative.unit.test")).unwrap(); - let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.auth_dns_server, "ns.unit.test"); + let zone = host.find_zone(&config.faythe_config).unwrap(); + assert_eq!(zone.auth_dns_server, "ns.unit.test"); let host: DNSName = DNSName::try_from(&String::from("unit.test")).unwrap(); - let z = host.find_zone(&config.faythe_config).unwrap(); - assert_eq!(z.auth_dns_server, "ns.unit.test"); + let zone = host.find_zone(&config.faythe_config).unwrap(); + assert_eq!(zone.auth_dns_server, "ns.unit.test"); } } diff --git a/src/dns/mod.rs b/src/dns/mod.rs index 8a594f4..655f2df 100644 --- a/src/dns/mod.rs +++ b/src/dns/mod.rs @@ -78,22 +78,22 @@ pub fn delete(config: &FaytheConfig, spec: &CertSpec) -> Result<(), DNSError> { } pub fn query(resolver: &Resolver, host: &DNSName, proof: &String) -> Result<(), DNSError> { - let c_host = challenge_host(host, None); - match resolver.txt_lookup(&c_host) { + let challenge_host = challenge_host(host, None); + match resolver.txt_lookup(&challenge_host) { Ok(res) => { let trim_chars: &[_] = &['"', '\n']; - res.iter().find(|rr| - rr.iter().find(|r| { - match String::from_utf8((*r).to_vec()) { + res.iter().find(|record_set| + record_set.iter().find(|record| { + match String::from_utf8((*record).to_vec()) { Ok(txt) => &txt.trim_matches(trim_chars) == proof, Err(_) => false, } }).is_some() - ).ok_or(DNSError::WrongAnswer(c_host.clone())).and(Ok(())) + ).ok_or(DNSError::WrongAnswer(challenge_host.clone())).and(Ok(())) }, Err(e) => { match e.kind() { - ResolveErrorKind::NoRecordsFound{..} => Err(DNSError::WrongAnswer(c_host.clone())), + ResolveErrorKind::NoRecordsFound{..} => Err(DNSError::WrongAnswer(challenge_host.clone())), _ => Err(DNSError::ResolveError(e)) } } @@ -102,8 +102,8 @@ pub fn query(resolver: &Resolver, host: &DNSName, proof: &String) -> Result<(), fn challenge_host(host: &DNSName, zone: Option<&Zone>) -> String { let suffix = match zone { - Some(z) => match &z.challenge_suffix { - Some(s) => format!(".{}", s), + Some(zone) => match &zone.challenge_suffix { + Some(suffix) => format!(".{}", suffix), None => String::new() } None => String::new() @@ -112,8 +112,8 @@ fn challenge_host(host: &DNSName, zone: Option<&Zone>) -> String { } impl From for DNSError { - fn from(e: std::io::Error) -> DNSError { - DNSError::IO(e) + fn from(err: std::io::Error) -> DNSError { + DNSError::IO(err) } } diff --git a/src/dns/nsupdate.rs b/src/dns/nsupdate.rs index 99f2883..c1316e0 100644 --- a/src/dns/nsupdate.rs +++ b/src/dns/nsupdate.rs @@ -33,17 +33,17 @@ impl NSUpdateDriver { fn add_cmd(&self, name: &String, proof: &String) -> String { format!("server {server}\n\ - update add {host} 120 TXT \"{proof}\"\n\ - send\n", - server=&self.server, - host=&name, - proof=&proof) + update add {host} 120 TXT \"{proof}\"\n\ + send\n", + server=&self.server, + host=&name, + proof=&proof) } fn delete_cmd(&self, name: &String) -> String { format!("server {server}\n\ - update delete {host} TXT\n\ - send\n", + update delete {host} TXT\n\ + send\n", server=&self.server, host=&name) } diff --git a/src/dns/webhook.rs b/src/dns/webhook.rs index f37409f..244aa8c 100644 --- a/src/dns/webhook.rs +++ b/src/dns/webhook.rs @@ -3,7 +3,10 @@ use crate::dns::DNSError; use super::ChallengeDriver; use crate::config::WebhookDriver; +use reqwest::blocking::Client; use std::collections::HashMap; +use std::convert::Into; +use std::time::Duration; impl ChallengeDriver for WebhookDriver { fn add(&self, challenge_host: &String, proof: &String) -> Result<(), DNSError> { @@ -49,10 +52,9 @@ static APP_USER_AGENT: &str = concat!( ); impl WebhookDriver { - - fn get_client(&self) -> Result { - let client = reqwest::blocking::Client::builder(); - let client = client.timeout(std::time::Duration::from_secs(self.timeout_secs as u64)); + fn get_client(&self) -> Result { + let client = Client::builder(); + let client = client.timeout(Duration::from_secs(self.timeout_secs as u64)); let client = client.user_agent(APP_USER_AGENT); client.build() } @@ -63,7 +65,7 @@ impl WebhookDriver { .json(&body) .send() .and(Ok(())) - .map_err(std::convert::Into::into) + .map_err(Into::into) } fn exec_delete(&self, body: Payload) -> Result<(), DNSError> { @@ -72,6 +74,6 @@ impl WebhookDriver { .json(&body) .send() .and(Ok(())) - .map_err(std::convert::Into::into) + .map_err(Into::into) } } From 449346f53fea69ba2e98fd42d44e4006acc4e609 Mon Sep 17 00:00:00 2001 From: Johan Thomsen Date: Fri, 6 Sep 2024 22:21:16 +0200 Subject: [PATCH 5/7] flake: collapse checks under a single dynamic attr --- flake.nix | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/flake.nix b/flake.nix index 0fd2f5a..ae14d56 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,6 @@ in { packages.${system}.${pname} = pkgs.${pname}; defaultPackage.${system} = pkgs.${pname}; - checks.${system}.vault = pkgs.callPackage ./nixos/vault-test.nix {}; overlays.default = final: prev: { "${pname}" = final.craneLib.buildPackage { @@ -54,13 +53,16 @@ }; }; - checks.${system}.sample-configs = pkgs.runCommandNoCC "check-sample-configs" { nativeBuildInputs = [ pkgs.${pname} ]; } '' - DIR=${./config-samples} - for FILE in $(ls -1 $DIR); do - echo "Testing: $DIR/$FILE" - faythe $DIR/$FILE --config-check >>$out - done - ''; + checks.${system} = { + sample-configs = pkgs.runCommandNoCC "check-sample-configs" { nativeBuildInputs = [ pkgs.${pname} ]; } '' + DIR=${./config-samples} + for FILE in $(ls -1 $DIR); do + echo "Testing: $DIR/$FILE" + faythe $DIR/$FILE --config-check >>$out + done + ''; + vault = pkgs.callPackage ./nixos/vault-test.nix {}; + }; devShell.${system} = with pkgs; mkShell { buildInputs = [ From 7562d8d069a1e7ca00998a7dbf544c578ae668b2 Mon Sep 17 00:00:00 2001 From: Johan Thomsen Date: Fri, 6 Sep 2024 22:29:44 +0200 Subject: [PATCH 6/7] checks/vault: adapting test faythe-config to match future format --- nixos/vault-test.nix | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nixos/vault-test.nix b/nixos/vault-test.nix index 701f314..0c74a6a 100644 --- a/nixos/vault-test.nix +++ b/nixos/vault-test.nix @@ -108,8 +108,11 @@ nixos-lib.runTest ( lets_encrypt_email = "test_mail@${domain}"; zones = { "${domain}" = { - server = ns_host; - key = "test"; + auth_dns_server = ns_host; + challenge_driver.nsupdate ={ + server = ns_host; + key = "test"; + }; }; }; val_dns_servers = [ ns_host ]; From cfc38faa52058f22b6ed03f93ab46e3de1a08a53 Mon Sep 17 00:00:00 2001 From: Sarah Brofeldt Date: Mon, 9 Sep 2024 06:40:02 +0200 Subject: [PATCH 7/7] gh workflows: fix flake check invocation --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 90ec1fd..be3a9f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,4 +8,4 @@ jobs: - uses: cachix/install-nix-action@v27 with: nix_path: nixpkgs=channel:nixos-24.05 - - run: nix --experimental-features "nix-command flakes" check + - run: nix --experimental-features "nix-command flakes" flake check