From 8d9ef17e1cff853132afd1625a3f6ce8ed180313 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Thu, 9 May 2019 17:39:30 +0200 Subject: [PATCH] Allow to configure environment variables for a given certificate Sometimes, you want hooks to use different parameters depending on the certificate. In order to achieve this, it is now now possible to configure environment variables at certificate scope. Thanks to this, the default hooks have been rewrote in order to use cover more use cases. --- CHANGELOG.md | 1 + acmed/config/default_hooks.toml | 26 +++++++++++++------------- acmed/src/certificate.rs | 7 ++++--- acmed/src/config.rs | 3 +++ acmed/src/hooks.rs | 30 ++++++++++++++++++++++-------- acmed/src/main_event_loop.rs | 1 + acmed/src/storage.rs | 8 ++++---- man/en/acmed.toml.5 | 13 ++++++++----- 8 files changed, 56 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721d0ee..686bd10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +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. - tacd is now able to listen on a unix socket. diff --git a/acmed/config/default_hooks.toml b/acmed/config/default_hooks.toml index 0b93fc8..0c60939 100644 --- a/acmed/config/default_hooks.toml +++ b/acmed/config/default_hooks.toml @@ -21,7 +21,7 @@ type = ["challenge-http-01"] cmd = "mkdir" args = [ "-m", "0755", - "-p", "/var/www/{{domain}}/.well-known/acme-challenge" + "-p", "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge" ] [[hook]] @@ -29,7 +29,7 @@ name = "http-01-echo-echo" type = ["challenge-http-01"] cmd = "echo" args = ["{{proof}}"] -stdout = "/var/www/{{domain}}/.well-known/acme-challenge/{{file_name}}" +stdout = "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" [[hook]] name = "http-01-echo-chmod" @@ -37,7 +37,7 @@ type = ["challenge-http-01-clean"] cmd = "chmod" args = [ "a+r", - "/var/www/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" ] [[hook]] @@ -46,11 +46,11 @@ type = ["challenge-http-01-clean"] cmd = "rm" args = [ "-f", - "/var/www/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" ] [[group]] -name = "http-01-echo-var-www" +name = "http-01-echo" hooks = [ "http-01-echo-mkdir", "http-01-echo-echo", @@ -68,10 +68,10 @@ name = "tls-alpn-01-tacd-start-tcp" type = ["challenge-tls-alpn-01"] cmd = "tacd" args = [ - "--pid-file", "/tmp/tacd_{{domain}}.pid", + "--pid-file", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", "--domain", "{{domain}}", "--acme-ext", "{{proof}}", - "--listen", "{{domain}}:5001" + "--listen", "{{#if env.TACD_HOST}}{{env.TACD_HOST}}{{else}}{{domain}}{{/if}}:{{#if env.TACD_PORT}}{{env.TACD_PORT}}{{else}}5001{{/if}}" ] [[hook]] @@ -79,10 +79,10 @@ name = "tls-alpn-01-tacd-start-unix" type = ["challenge-tls-alpn-01"] cmd = "tacd" args = [ - "--pid-file", "/tmp/tacd_{{domain}}.pid", + "--pid-file", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", "--domain", "{{domain}}", "--acme-ext", "{{proof}}", - "--listen", "unix:/tmp/tacd_{{domain}}.sock" + "--listen", "unix:{{#if env.TACD_SOCK_ROOT}}{{env.TACD_SOCK_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.sock" ] [[hook]] @@ -90,7 +90,7 @@ name = "tls-alpn-01-tacd-kill" type = ["challenge-tls-alpn-01-clean"] cmd = "pkill" args = [ - "-F", "/tmp/tacd_{{domain}}.pid" + "-F", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", ] [[hook]] @@ -98,7 +98,7 @@ name = "tls-alpn-01-tacd-rm" type = ["challenge-tls-alpn-01-clean"] cmd = "rm" args = [ - "-f", "/tmp/tacd_{{domain}}.pid" + "-f", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", ] [[group]] @@ -138,8 +138,8 @@ type = ["file-post-create", "file-post-edit"] cmd = "git" args = [ "-C", "{{file_directory}}", - "-c", "user.name=ACMEd", - "-c", "user.email=acmed@localhost", + "-c", "user.name='{{#if env.GIT_USERNAME}}{{env.GIT_USERNAME}}{{else}}ACMEd{{/if}}'", + "-c", "user.email='{{#if env.GIT_EMAIL}}{{env.GIT_EMAIL}}{{else}}acmed@localhost{{/if}}'", "commit", "-m", "{{file_name}}", "--only", "{{file_name}}" diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index f6b6fc9..620ed1e 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -88,6 +88,7 @@ pub struct Certificate { pub pk_file_mode: u32, pub pk_file_owner: Option, pub pk_file_group: Option, + pub env: HashMap, } impl fmt::Display for Certificate { @@ -213,7 +214,7 @@ impl Certificate { HookType::ChallengeTlsAlpn01Clean, ), }; - hooks::call(&hook_data, &self.hooks, hook_type.0)?; + hooks::call(self, &hook_data, hook_type.0)?; Ok((hook_data, hook_type.1)) } @@ -222,7 +223,7 @@ impl Certificate { data: &ChallengeHookData, hook_type: HookType, ) -> Result<(), Error> { - hooks::call(data, &self.hooks, hook_type) + hooks::call(self, data, hook_type) } pub fn call_post_operation_hooks(&self, status: &str, is_success: bool) -> Result<(), Error> { @@ -238,7 +239,7 @@ impl Certificate { is_success, env: HashMap::new(), }; - hooks::call(&hook_data, &self.hooks, HookType::PostOperation)?; + hooks::call(self, &hook_data, HookType::PostOperation)?; Ok(()) } } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index a1e85f0..1d00aac 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -3,6 +3,7 @@ use crate::hooks; use acme_common::error::Error; use log::info; use serde::Deserialize; +use std::collections::HashMap; use std::fmt; use std::fs::{self, File}; use std::io::prelude::*; @@ -206,6 +207,8 @@ pub struct Certificate { pub name_format: Option, pub formats: Option>, pub hooks: Vec, + #[serde(default)] + pub env: HashMap, } impl Certificate { diff --git a/acmed/src/hooks.rs b/acmed/src/hooks.rs index d8456cd..3343992 100644 --- a/acmed/src/hooks.rs +++ b/acmed/src/hooks.rs @@ -1,3 +1,4 @@ +use crate::certificate::Certificate; use crate::config::HookType; use acme_common::error::Error; use handlebars::Handlebars; @@ -11,14 +12,22 @@ use std::process::{Command, Stdio}; use std::{env, fmt}; pub trait HookEnvData { - fn set_env(&mut self); + fn set_env(&mut self, cert: &Certificate); +} + +fn deref(t: (&F, &G)) -> (F, G) +where + F: Clone, + G: Clone, +{ + ((*(t.0)).to_owned(), (*(t.1)).to_owned()) } macro_rules! imple_hook_data_env { ($t: ty) => { impl HookEnvData for $t { - fn set_env(&mut self) { - for (key, value) in env::vars() { + fn set_env(&mut self, cert: &Certificate) { + for (key, value) in env::vars().chain(cert.env.iter().map(deref)) { self.env.insert(key, value); } } @@ -90,13 +99,13 @@ macro_rules! get_hook_output { }}; } -fn call_single(data: &T, hook: &Hook) -> Result<(), Error> +fn call_single(cert: &Certificate, data: &T, hook: &Hook) -> Result<(), Error> where T: Clone + HookEnvData + Serialize, { debug!("Calling hook: {}", hook.name); let mut data = (*data).clone(); - data.set_env(); + data.set_env(cert); let reg = Handlebars::new(); let mut v = vec![]; let args = match &hook.args { @@ -112,6 +121,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()) .args(args) .stdout(get_hook_output!(&hook.stdout, reg, &data)) .stderr(get_hook_output!(&hook.stderr, reg, &data)) @@ -135,12 +145,16 @@ where Ok(()) } -pub fn call(data: &T, hooks: &[Hook], hook_type: HookType) -> Result<(), Error> +pub fn call(cert: &Certificate, data: &T, hook_type: HookType) -> Result<(), Error> where T: Clone + HookEnvData + Serialize, { - for hook in hooks.iter().filter(|h| h.hook_type.contains(&hook_type)) { - call_single(data, &hook)?; + for hook in cert + .hooks + .iter() + .filter(|h| h.hook_type.contains(&hook_type)) + { + call_single(cert, data, &hook)?; } Ok(()) } diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 73bcdd3..ba5f885 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -40,6 +40,7 @@ impl MainEventLoop { pk_file_mode: cnf.get_pk_file_mode(), pk_file_owner: cnf.get_pk_file_user(), pk_file_group: cnf.get_pk_file_group(), + env: crt.env.to_owned(), }; certs.push(cert); } diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index 51e4c08..75b297f 100644 --- a/acmed/src/storage.rs +++ b/acmed/src/storage.rs @@ -144,9 +144,9 @@ fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<() let is_new = !path.is_file(); if is_new { - hooks::call(&hook_data, &cert.hooks, HookType::FilePreCreate)?; + hooks::call(cert, &hook_data, HookType::FilePreCreate)?; } else { - hooks::call(&hook_data, &cert.hooks, HookType::FilePreEdit)?; + hooks::call(cert, &hook_data, HookType::FilePreEdit)?; } trace!("Writing file {:?}", path); @@ -168,9 +168,9 @@ fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<() } if is_new { - hooks::call(&hook_data, &cert.hooks, HookType::FilePostCreate)?; + hooks::call(cert, &hook_data, HookType::FilePostCreate)?; } else { - hooks::call(&hook_data, &cert.hooks, HookType::FilePostEdit)?; + hooks::call(cert, &hook_data, HookType::FilePostEdit)?; } Ok(()) } diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 2a65546..b0f41d9 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -141,6 +141,8 @@ Array of table representing a certificate that will be requested to a CA. Name of the account to use. .It Ic endpoint Ar string Name of the endpoint to use. +.It Ic env Ar table +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 @@ -410,7 +412,7 @@ type = ["challenge-http-01"] cmd = "mkdir" args = [ "-m", "0755", - "-p", "/var/www/{{domain}}/.well-known/acme-challenge" + "-p", "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge" ] [[hook]] @@ -418,7 +420,7 @@ name = "http-01-echo-echo" type = ["challenge-http-01"] cmd = "echo" args = ["{{proof}}"] -stdout = "/var/www/{{domain}}/.well-known/acme-challenge/{{file_name}}" +stdout = "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" [[hook]] name = "http-01-echo-chmod" @@ -426,7 +428,7 @@ type = ["challenge-http-01-clean"] cmd = "chmod" args = [ "a+r", - "/var/www/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" ] [[hook]] @@ -435,7 +437,7 @@ type = ["challenge-http-01-clean"] cmd = "rm" args = [ "-f", - "/var/www/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" ] .Ed .Pp @@ -452,7 +454,8 @@ hooks = [ [[certificate]] # Some fields omitted -hooks = ["http-01-echo-var-www"] +hooks = ["http-01-echo"] +env.HTTP_ROOT = "/srv/http" .Ed .Pp