Browse Source

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.
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
8d9ef17e1c
  1. 1
      CHANGELOG.md
  2. 26
      acmed/config/default_hooks.toml
  3. 7
      acmed/src/certificate.rs
  4. 3
      acmed/src/config.rs
  5. 30
      acmed/src/hooks.rs
  6. 1
      acmed/src/main_event_loop.rs
  7. 8
      acmed/src/storage.rs
  8. 13
      man/en/acmed.toml.5

1
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. - 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.
- tacd is now able to listen on a unix socket. - tacd is now able to listen on a unix socket.

26
acmed/config/default_hooks.toml

@ -21,7 +21,7 @@ type = ["challenge-http-01"]
cmd = "mkdir" cmd = "mkdir"
args = [ args = [
"-m", "0755", "-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]] [[hook]]
@ -29,7 +29,7 @@ name = "http-01-echo-echo"
type = ["challenge-http-01"] type = ["challenge-http-01"]
cmd = "echo" cmd = "echo"
args = ["{{proof}}"] 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]] [[hook]]
name = "http-01-echo-chmod" name = "http-01-echo-chmod"
@ -37,7 +37,7 @@ type = ["challenge-http-01-clean"]
cmd = "chmod" cmd = "chmod"
args = [ args = [
"a+r", "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]] [[hook]]
@ -46,11 +46,11 @@ type = ["challenge-http-01-clean"]
cmd = "rm" cmd = "rm"
args = [ args = [
"-f", "-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]] [[group]]
name = "http-01-echo-var-www"
name = "http-01-echo"
hooks = [ hooks = [
"http-01-echo-mkdir", "http-01-echo-mkdir",
"http-01-echo-echo", "http-01-echo-echo",
@ -68,10 +68,10 @@ name = "tls-alpn-01-tacd-start-tcp"
type = ["challenge-tls-alpn-01"] type = ["challenge-tls-alpn-01"]
cmd = "tacd" cmd = "tacd"
args = [ 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}}", "--domain", "{{domain}}",
"--acme-ext", "{{proof}}", "--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]] [[hook]]
@ -79,10 +79,10 @@ name = "tls-alpn-01-tacd-start-unix"
type = ["challenge-tls-alpn-01"] type = ["challenge-tls-alpn-01"]
cmd = "tacd" cmd = "tacd"
args = [ 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}}", "--domain", "{{domain}}",
"--acme-ext", "{{proof}}", "--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]] [[hook]]
@ -90,7 +90,7 @@ name = "tls-alpn-01-tacd-kill"
type = ["challenge-tls-alpn-01-clean"] type = ["challenge-tls-alpn-01-clean"]
cmd = "pkill" cmd = "pkill"
args = [ args = [
"-F", "/tmp/tacd_{{domain}}.pid"
"-F", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid",
] ]
[[hook]] [[hook]]
@ -98,7 +98,7 @@ name = "tls-alpn-01-tacd-rm"
type = ["challenge-tls-alpn-01-clean"] type = ["challenge-tls-alpn-01-clean"]
cmd = "rm" cmd = "rm"
args = [ args = [
"-f", "/tmp/tacd_{{domain}}.pid"
"-f", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid",
] ]
[[group]] [[group]]
@ -138,8 +138,8 @@ type = ["file-post-create", "file-post-edit"]
cmd = "git" cmd = "git"
args = [ args = [
"-C", "{{file_directory}}", "-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", "commit",
"-m", "{{file_name}}", "-m", "{{file_name}}",
"--only", "{{file_name}}" "--only", "{{file_name}}"

7
acmed/src/certificate.rs

@ -88,6 +88,7 @@ pub struct Certificate {
pub pk_file_mode: u32, pub pk_file_mode: u32,
pub pk_file_owner: Option<String>, pub pk_file_owner: Option<String>,
pub pk_file_group: Option<String>, pub pk_file_group: Option<String>,
pub env: HashMap<String, String>,
} }
impl fmt::Display for Certificate { impl fmt::Display for Certificate {
@ -213,7 +214,7 @@ impl Certificate {
HookType::ChallengeTlsAlpn01Clean, 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)) Ok((hook_data, hook_type.1))
} }
@ -222,7 +223,7 @@ impl Certificate {
data: &ChallengeHookData, data: &ChallengeHookData,
hook_type: HookType, hook_type: HookType,
) -> Result<(), Error> { ) -> 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> { pub fn call_post_operation_hooks(&self, status: &str, is_success: bool) -> Result<(), Error> {
@ -238,7 +239,7 @@ impl Certificate {
is_success, is_success,
env: HashMap::new(), env: HashMap::new(),
}; };
hooks::call(&hook_data, &self.hooks, HookType::PostOperation)?;
hooks::call(self, &hook_data, HookType::PostOperation)?;
Ok(()) Ok(())
} }
} }

3
acmed/src/config.rs

@ -3,6 +3,7 @@ use crate::hooks;
use acme_common::error::Error; use acme_common::error::Error;
use log::info; use log::info;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::prelude::*; use std::io::prelude::*;
@ -206,6 +207,8 @@ pub struct Certificate {
pub name_format: Option<String>, pub name_format: Option<String>,
pub formats: Option<Vec<String>>, pub formats: Option<Vec<String>>,
pub hooks: Vec<String>, pub hooks: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
} }
impl Certificate { impl Certificate {

30
acmed/src/hooks.rs

@ -1,3 +1,4 @@
use crate::certificate::Certificate;
use crate::config::HookType; use crate::config::HookType;
use acme_common::error::Error; use acme_common::error::Error;
use handlebars::Handlebars; use handlebars::Handlebars;
@ -11,14 +12,22 @@ use std::process::{Command, Stdio};
use std::{env, fmt}; use std::{env, fmt};
pub trait HookEnvData { pub trait HookEnvData {
fn set_env(&mut self);
fn set_env(&mut self, cert: &Certificate);
}
fn deref<F, G>(t: (&F, &G)) -> (F, G)
where
F: Clone,
G: Clone,
{
((*(t.0)).to_owned(), (*(t.1)).to_owned())
} }
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) {
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); self.env.insert(key, value);
} }
} }
@ -90,13 +99,13 @@ macro_rules! get_hook_output {
}}; }};
} }
fn call_single<T>(data: &T, hook: &Hook) -> Result<(), Error>
fn call_single<T>(cert: &Certificate, 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(); let mut data = (*data).clone();
data.set_env();
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 {
@ -112,6 +121,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())
.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))
@ -135,12 +145,16 @@ where
Ok(()) Ok(())
} }
pub fn call<T>(data: &T, hooks: &[Hook], hook_type: HookType) -> Result<(), Error>
pub fn call<T>(cert: &Certificate, data: &T, hook_type: HookType) -> Result<(), Error>
where where
T: Clone + HookEnvData + Serialize, 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(()) Ok(())
} }

1
acmed/src/main_event_loop.rs

@ -40,6 +40,7 @@ impl MainEventLoop {
pk_file_mode: cnf.get_pk_file_mode(), pk_file_mode: cnf.get_pk_file_mode(),
pk_file_owner: cnf.get_pk_file_user(), pk_file_owner: cnf.get_pk_file_user(),
pk_file_group: cnf.get_pk_file_group(), pk_file_group: cnf.get_pk_file_group(),
env: crt.env.to_owned(),
}; };
certs.push(cert); certs.push(cert);
} }

8
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(); let is_new = !path.is_file();
if is_new { if is_new {
hooks::call(&hook_data, &cert.hooks, HookType::FilePreCreate)?;
hooks::call(cert, &hook_data, HookType::FilePreCreate)?;
} else { } else {
hooks::call(&hook_data, &cert.hooks, HookType::FilePreEdit)?;
hooks::call(cert, &hook_data, HookType::FilePreEdit)?;
} }
trace!("Writing file {:?}", path); trace!("Writing file {:?}", path);
@ -168,9 +168,9 @@ fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<()
} }
if is_new { if is_new {
hooks::call(&hook_data, &cert.hooks, HookType::FilePostCreate)?;
hooks::call(cert, &hook_data, HookType::FilePostCreate)?;
} else { } else {
hooks::call(&hook_data, &cert.hooks, HookType::FilePostEdit)?;
hooks::call(cert, &hook_data, HookType::FilePostEdit)?;
} }
Ok(()) Ok(())
} }

13
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. Name of the account to use.
.It Ic endpoint Ar string .It Ic endpoint Ar string
Name of the endpoint to use. 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 .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
@ -410,7 +412,7 @@ type = ["challenge-http-01"]
cmd = "mkdir" cmd = "mkdir"
args = [ args = [
"-m", "0755", "-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]] [[hook]]
@ -418,7 +420,7 @@ name = "http-01-echo-echo"
type = ["challenge-http-01"] type = ["challenge-http-01"]
cmd = "echo" cmd = "echo"
args = ["{{proof}}"] 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]] [[hook]]
name = "http-01-echo-chmod" name = "http-01-echo-chmod"
@ -426,7 +428,7 @@ type = ["challenge-http-01-clean"]
cmd = "chmod" cmd = "chmod"
args = [ args = [
"a+r", "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]] [[hook]]
@ -435,7 +437,7 @@ type = ["challenge-http-01-clean"]
cmd = "rm" cmd = "rm"
args = [ args = [
"-f", "-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 .Ed
.Pp .Pp
@ -452,7 +454,8 @@ hooks = [
[[certificate]] [[certificate]]
# Some fields omitted # Some fields omitted
hooks = ["http-01-echo-var-www"]
hooks = ["http-01-echo"]
env.HTTP_ROOT = "/srv/http"
.Ed .Ed
.Pp .Pp

Loading…
Cancel
Save