Browse Source

Allow to configure per-domain environment variables

pull/5/head
Rodolphe Breard 6 years ago
parent
commit
d24c78ee17
  1. 2
      CHANGELOG.md
  2. 2
      acmed/src/acme_proto.rs
  3. 4
      acmed/src/acme_proto/certificate.rs
  4. 28
      acmed/src/certificate.rs
  5. 2
      acmed/src/config.rs
  6. 20
      acmed/src/hooks.rs
  7. 11
      acmed/src/main_event_loop.rs
  8. 5
      acmed/src/storage.rs
  9. 8
      man/en/acmed.toml.5

2
CHANGELOG.md

@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ACMEd now displays a warning when the server indicates an error in an order or an authorization. - ACMEd now displays a warning when the server indicates an error in an order or an authorization.
- A configuration file can now include several other files. - A configuration file can now include several other files.
- Hooks have access to environment variables. - Hooks have access to environment variables.
- In the configuration, certificates can define environment variables for the hooks.
- In the configuration, certificates and domains can define environment variables for the hooks.
- tacd is now able to listen on a unix socket. - tacd is now able to listen on a unix socket.

2
acmed/src/acme_proto.rs

@ -70,7 +70,7 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String]) -> Result<
let domains = cert let domains = cert
.domains .domains
.iter() .iter()
.map(|d| d.0.to_owned())
.map(|d| d.dns.to_owned())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let mut hook_datas = vec![]; let mut hook_datas = vec![];

4
acmed/src/acme_proto/certificate.rs

@ -46,8 +46,8 @@ pub fn generate_csr(
builder.set_pubkey(pub_key)?; builder.set_pubkey(pub_key)?;
let ctx = builder.x509v3_context(None); let ctx = builder.x509v3_context(None);
let mut san = SubjectAlternativeName::new(); let mut san = SubjectAlternativeName::new();
for name in cert.domains.iter().map(|d| d.0.to_owned()) {
san.dns(&name);
for c in cert.domains.iter() {
san.dns(&c.dns);
} }
let san = san.build(&ctx)?; let san = san.build(&ctx)?;
let mut ext_stack = Stack::new()?; let mut ext_stack = Stack::new()?;

28
acmed/src/certificate.rs

@ -1,6 +1,6 @@
use crate::acme_proto::Challenge; use crate::acme_proto::Challenge;
use crate::config::{Account, HookType};
use crate::hooks::{self, ChallengeHookData, Hook, PostOperationHookData};
use crate::config::{Account, Domain, HookType};
use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, PostOperationHookData};
use crate::storage::{certificate_files_exists, get_certificate}; use crate::storage::{certificate_files_exists, get_certificate};
use acme_common::error::Error; use acme_common::error::Error;
use log::{debug, trace}; use log::{debug, trace};
@ -72,7 +72,7 @@ impl fmt::Display for Algorithm {
#[derive(Debug)] #[derive(Debug)]
pub struct Certificate { pub struct Certificate {
pub account: Account, pub account: Account,
pub domains: Vec<(String, Challenge)>,
pub domains: Vec<Domain>,
pub algo: Algorithm, pub algo: Algorithm,
pub kp_reuse: bool, pub kp_reuse: bool,
pub remote_url: String, pub remote_url: String,
@ -102,7 +102,7 @@ impl fmt::Display for Certificate {
let domains = self let domains = self
.domains .domains
.iter() .iter()
.map(|d| format!("{} ({})", d.0, d.1))
.map(|d| format!("{} ({})", d.dns, d.challenge))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "); .join(", ");
write!( write!(
@ -125,9 +125,10 @@ Hooks: {hooks}",
impl Certificate { impl Certificate {
pub fn get_domain_challenge(&self, domain_name: &str) -> Result<Challenge, Error> { pub fn get_domain_challenge(&self, domain_name: &str) -> Result<Challenge, Error> {
let domain_name = domain_name.to_string(); let domain_name = domain_name.to_string();
for (domain, challenge) in self.domains.iter() {
if *domain == domain_name {
return Ok((*challenge).to_owned());
for d in self.domains.iter() {
if d.dns == domain_name {
let c = Challenge::from_str(&d.challenge)?;
return Ok(c);
} }
} }
let msg = format!("{}: domain name not found", domain_name); let msg = format!("{}: domain name not found", domain_name);
@ -156,7 +157,7 @@ impl Certificate {
let req_names = self let req_names = self
.domains .domains
.iter() .iter()
.map(|v| v.0.to_owned())
.map(|v| v.dns.to_owned())
.collect::<HashSet<String>>(); .collect::<HashSet<String>>();
let has_miss = req_names.difference(&cert_names).count() != 0; let has_miss = req_names.difference(&cert_names).count() != 0;
if has_miss { if has_miss {
@ -198,7 +199,7 @@ impl Certificate {
domain: &str, domain: &str,
) -> Result<(ChallengeHookData, HookType), Error> { ) -> Result<(ChallengeHookData, HookType), Error> {
let challenge = self.get_domain_challenge(domain)?; let challenge = self.get_domain_challenge(domain)?;
let hook_data = ChallengeHookData {
let mut hook_data = ChallengeHookData {
challenge: challenge.to_string(), challenge: challenge.to_string(),
domain: domain.to_string(), domain: domain.to_string(),
file_name: file_name.to_string(), file_name: file_name.to_string(),
@ -206,6 +207,10 @@ impl Certificate {
is_clean_hook: false, is_clean_hook: false,
env: HashMap::new(), env: HashMap::new(),
}; };
hook_data.set_env(&self.env);
for d in self.domains.iter().filter(|d| d.dns == domain) {
hook_data.set_env(&d.env);
}
let hook_type = match challenge { let hook_type = match challenge {
Challenge::Http01 => (HookType::ChallengeHttp01, HookType::ChallengeHttp01Clean), Challenge::Http01 => (HookType::ChallengeHttp01, HookType::ChallengeHttp01Clean),
Challenge::Dns01 => (HookType::ChallengeDns01, HookType::ChallengeDns01Clean), Challenge::Dns01 => (HookType::ChallengeDns01, HookType::ChallengeDns01Clean),
@ -230,15 +235,16 @@ impl Certificate {
let domains = self let domains = self
.domains .domains
.iter() .iter()
.map(|d| format!("{} ({})", d.0, d.1))
.map(|d| format!("{} ({})", d.dns, d.challenge))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let hook_data = PostOperationHookData {
let mut hook_data = PostOperationHookData {
domains, domains,
algorithm: self.algo.to_string(), algorithm: self.algo.to_string(),
status: status.to_string(), status: status.to_string(),
is_success, is_success,
env: HashMap::new(), env: HashMap::new(),
}; };
hook_data.set_env(&self.env);
hooks::call(self, &hook_data, HookType::PostOperation)?; hooks::call(self, &hook_data, HookType::PostOperation)?;
Ok(()) Ok(())
} }

2
acmed/src/config.rs

@ -298,6 +298,8 @@ impl Certificate {
pub struct Domain { pub struct Domain {
pub challenge: String, pub challenge: String,
pub dns: String, pub dns: String,
#[serde(default)]
pub env: HashMap<String, String>,
} }
impl fmt::Display for Domain { impl fmt::Display for Domain {

20
acmed/src/hooks.rs

@ -4,6 +4,7 @@ use acme_common::error::Error;
use handlebars::Handlebars; use handlebars::Handlebars;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use std::collections::hash_map::Iter;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
@ -12,7 +13,8 @@ use std::process::{Command, Stdio};
use std::{env, fmt}; use std::{env, fmt};
pub trait HookEnvData { pub trait HookEnvData {
fn set_env(&mut self, cert: &Certificate);
fn set_env(&mut self, env: &HashMap<String, String>);
fn get_env(&self) -> Iter<String, String>;
} }
fn deref<F, G>(t: (&F, &G)) -> (F, G) fn deref<F, G>(t: (&F, &G)) -> (F, G)
@ -26,11 +28,15 @@ where
macro_rules! imple_hook_data_env { macro_rules! imple_hook_data_env {
($t: ty) => { ($t: ty) => {
impl HookEnvData for $t { impl HookEnvData for $t {
fn set_env(&mut self, cert: &Certificate) {
for (key, value) in env::vars().chain(cert.env.iter().map(deref)) {
fn set_env(&mut self, env: &HashMap<String, String>) {
for (key, value) in env::vars().chain(env.iter().map(deref)) {
self.env.insert(key, value); self.env.insert(key, value);
} }
} }
fn get_env(&self) -> Iter<String, String> {
self.env.iter()
}
} }
}; };
} }
@ -99,13 +105,11 @@ macro_rules! get_hook_output {
}}; }};
} }
fn call_single<T>(cert: &Certificate, data: &T, hook: &Hook) -> Result<(), Error>
fn call_single<T>(data: &T, hook: &Hook) -> Result<(), Error>
where where
T: Clone + HookEnvData + Serialize, T: Clone + HookEnvData + Serialize,
{ {
debug!("Calling hook: {}", hook.name); debug!("Calling hook: {}", hook.name);
let mut data = (*data).clone();
data.set_env(cert);
let reg = Handlebars::new(); let reg = Handlebars::new();
let mut v = vec![]; let mut v = vec![];
let args = match &hook.args { let args = match &hook.args {
@ -121,7 +125,7 @@ where
debug!("Hook {}: cmd: {}", hook.name, hook.cmd); debug!("Hook {}: cmd: {}", hook.name, hook.cmd);
debug!("Hook {}: args: {:?}", hook.name, args); debug!("Hook {}: args: {:?}", hook.name, args);
let mut cmd = Command::new(&hook.cmd) let mut cmd = Command::new(&hook.cmd)
.envs(cert.env.iter())
.envs(data.get_env())
.args(args) .args(args)
.stdout(get_hook_output!(&hook.stdout, reg, &data)) .stdout(get_hook_output!(&hook.stdout, reg, &data))
.stderr(get_hook_output!(&hook.stderr, reg, &data)) .stderr(get_hook_output!(&hook.stderr, reg, &data))
@ -154,7 +158,7 @@ where
.iter() .iter()
.filter(|h| h.hook_type.contains(&hook_type)) .filter(|h| h.hook_type.contains(&hook_type))
{ {
call_single(cert, data, &hook)?;
call_single(data, &hook)?;
} }
Ok(()) Ok(())
} }

11
acmed/src/main_event_loop.rs

@ -1,5 +1,4 @@
use crate::acme_proto::request_certificate; use crate::acme_proto::request_certificate;
use crate::acme_proto::Challenge;
use crate::certificate::Certificate; use crate::certificate::Certificate;
use crate::config; use crate::config;
use acme_common::error::Error; use acme_common::error::Error;
@ -20,11 +19,7 @@ impl MainEventLoop {
for crt in cnf.certificate.iter() { for crt in cnf.certificate.iter() {
let cert = Certificate { let cert = Certificate {
account: crt.get_account(&cnf)?, account: crt.get_account(&cnf)?,
domains: crt
.domains
.iter()
.map(|d| (d.dns.to_owned(), Challenge::from_str(&d.challenge).unwrap()))
.collect(),
domains: crt.domains.to_owned(),
algo: crt.get_algorithm()?, algo: crt.get_algorithm()?,
kp_reuse: crt.get_kp_reuse(), kp_reuse: crt.get_kp_reuse(),
remote_url: crt.get_remote_url(&cnf)?, remote_url: crt.get_remote_url(&cnf)?,
@ -65,7 +60,7 @@ impl MainEventLoop {
let msg = format!( let msg = format!(
"Unable to renew the {} certificate for {}: {}", "Unable to renew the {} certificate for {}: {}",
crt.algo, crt.algo,
crt.domains.first().unwrap().0,
&crt.domains.first().unwrap().dns,
e e
); );
warn!("{}", msg); warn!("{}", msg);
@ -78,7 +73,7 @@ impl MainEventLoop {
let msg = format!( let msg = format!(
"{} certificate for {}: post-operation hook error: {}", "{} certificate for {}: post-operation hook error: {}",
crt.algo, crt.algo,
crt.domains.first().unwrap().0,
&crt.domains.first().unwrap().dns,
e e
); );
warn!("{}", msg); warn!("{}", msg);

5
acmed/src/storage.rs

@ -1,6 +1,6 @@
use crate::certificate::Certificate; use crate::certificate::Certificate;
use crate::config::HookType; use crate::config::HookType;
use crate::hooks::{self, FileStorageHookData};
use crate::hooks::{self, FileStorageHookData, HookEnvData};
use acme_common::b64_encode; use acme_common::b64_encode;
use acme_common::error::Error; use acme_common::error::Error;
use log::trace; use log::trace;
@ -135,12 +135,13 @@ fn set_owner(cert: &Certificate, path: &PathBuf, file_type: FileType) -> Result<
fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<(), Error> { fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<(), Error> {
let (file_directory, file_name, path) = get_file_full_path(cert, file_type.clone())?; let (file_directory, file_name, path) = get_file_full_path(cert, file_type.clone())?;
let hook_data = FileStorageHookData {
let mut hook_data = FileStorageHookData {
file_name, file_name,
file_directory, file_directory,
file_path: path.to_path_buf(), file_path: path.to_path_buf(),
env: HashMap::new(), env: HashMap::new(),
}; };
hook_data.set_env(&cert.env);
let is_new = !path.is_file(); let is_new = !path.is_file();
if is_new { if is_new {

8
man/en/acmed.toml.5

@ -146,9 +146,7 @@ Table of environment variables that will be accessible from hooks.
.It Ic domains Ar array .It Ic domains Ar array
Array of tables listing the domains that should be included in the certificate along with the challenge to use for each one. Array of tables listing the domains that should be included in the certificate along with the challenge to use for each one.
.Bl -tag .Bl -tag
.It Ic dns
The domain name.
.It Ic challenge
.It Ic challenge Ar string
The name of the challenge to use to prove the domain's ownership. Possible values are: The name of the challenge to use to prove the domain's ownership. Possible values are:
.Bl -dash -compact .Bl -dash -compact
.It .It
@ -158,6 +156,10 @@ dns-01
.It .It
tls-alpn-01 tls-alpn-01
.El .El
.It Ic dns Ar string
The domain name.
.It Ic env Ar table
Table of environment variables that will be accessible from hooks.
.El .El
.It Ic algorithm Ar string .It Ic algorithm Ar string
Name of the asymetric cryptography algorithm used to generate the certificate's key pair. Possible values are : Name of the asymetric cryptography algorithm used to generate the certificate's key pair. Possible values are :

Loading…
Cancel
Save