diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfb1438..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" build + - run: nix --experimental-features "nix-command flakes" flake check 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/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 76% rename from config.json rename to config-samples/vault-nsupdate.json index 9dcfd1a..e0eb92e 100644 --- a/config.json +++ b/config-samples/vault-nsupdate.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/flake.nix b/flake.nix index 464e3fb..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,6 +53,17 @@ }; }; + 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 = [ rust-analyzer 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 ]; diff --git a/src/common.rs b/src/common.rs index 8806efd..0546cbf 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 }); @@ -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.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.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.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.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/config.rs b/src/config.rs index 6d96e6e..ac039c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -91,13 +91,42 @@ 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, + Webhook(WebhookDriver), +} + +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct NSUpdateDriver { + pub server: String, + 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.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..655f2df --- /dev/null +++ b/src/dns/mod.rs @@ -0,0 +1,145 @@ +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; +mod webhook; + +#[derive(Debug)] +pub enum DNSError { + Exec(ExecErrorInfo), + IO(std::io::Error), + OutputFormat, + ResolveError(ResolveError), + WrongAnswer(String), + WrongSpec, + Reqwest(reqwest::Error), +} + +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(()), + DriverConfig::Webhook(webhook) => { + webhook.add(&challenge_host, proof) + } + } +} + +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 => (), + DriverConfig::Webhook(webhook) => { + webhook.delete(&host)? + } + }; + 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 => (), + DriverConfig::Webhook(webhook) => { + webhook.delete(&host)? + } + } + } + Ok(()) +} + +pub fn query(resolver: &Resolver, host: &DNSName, proof: &String) -> Result<(), DNSError> { + let challenge_host = challenge_host(host, None); + match resolver.txt_lookup(&challenge_host) { + Ok(res) => { + let trim_chars: &[_] = &['"', '\n']; + 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(challenge_host.clone())).and(Ok(())) + }, + Err(e) => { + match e.kind() { + ResolveErrorKind::NoRecordsFound{..} => Err(DNSError::WrongAnswer(challenge_host.clone())), + _ => Err(DNSError::ResolveError(e)) + } + } + } +} + +fn challenge_host(host: &DNSName, zone: Option<&Zone>) -> String { + let suffix = match zone { + Some(zone) => match &zone.challenge_suffix { + Some(suffix) => format!(".{}", suffix), + None => String::new() + } + None => String::new() + }; + format!("_acme-challenge.{}{}.", &host.to_parent_domain_string(), &suffix) +} + +impl From for DNSError { + fn from(err: std::io::Error) -> DNSError { + DNSError::IO(err) + } +} + +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 + } +} + +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/nsupdate.rs b/src/dns/nsupdate.rs new file mode 100644 index 0000000..c1316e0 --- /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/dns/webhook.rs b/src/dns/webhook.rs new file mode 100644 index 0000000..244aa8c --- /dev/null +++ b/src/dns/webhook.rs @@ -0,0 +1,79 @@ +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> { + 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 = Client::builder(); + let client = client.timeout(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(Into::into) + } + + fn exec_delete(&self, body: Payload) -> Result<(), DNSError> { + self.get_client()? + .delete(self.url.clone()) + .json(&body) + .send() + .and(Ok(())) + .map_err(Into::into) + } +} 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 }