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.
- A configuration file can now include several other files.
- 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.

2
acmed/src/acme_proto.rs

@ -70,7 +70,7 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String]) -> Result<
let domains = cert
.domains
.iter()
.map(|d| d.0.to_owned())
.map(|d| d.dns.to_owned())
.collect::<Vec<String>>();
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)?;
let ctx = builder.x509v3_context(None);
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 mut ext_stack = Stack::new()?;

28
acmed/src/certificate.rs

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

2
acmed/src/config.rs

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

20
acmed/src/hooks.rs

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

11
acmed/src/main_event_loop.rs

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

5
acmed/src/storage.rs

@ -1,6 +1,6 @@
use crate::certificate::Certificate;
use crate::config::HookType;
use crate::hooks::{self, FileStorageHookData};
use crate::hooks::{self, FileStorageHookData, HookEnvData};
use acme_common::b64_encode;
use acme_common::error::Error;
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> {
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_directory,
file_path: path.to_path_buf(),
env: HashMap::new(),
};
hook_data.set_env(&cert.env);
let is_new = !path.is_file();
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
Array of tables listing the domains that should be included in the certificate along with the challenge to use for each one.
.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:
.Bl -dash -compact
.It
@ -158,6 +156,10 @@ dns-01
.It
tls-alpn-01
.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
.It Ic algorithm Ar string
Name of the asymetric cryptography algorithm used to generate the certificate's key pair. Possible values are :

Loading…
Cancel
Save