Browse Source

Merge branch 'master' of github.com:breard-r/acmed

pull/31/head
Rodolphe Breard 4 years ago
parent
commit
d80ad4af9d
  1. 2
      .travis.yml
  2. 3
      CHANGELOG.md
  3. 18
      Makefile
  4. 7
      README.md
  5. 17
      acme_common/src/lib.rs
  6. 29
      acmed.service.example
  7. 2
      acmed/src/acme_proto.rs
  8. 6
      acmed/src/acme_proto/account.rs
  9. 16
      acmed/src/certificate.rs
  10. 8
      acmed/src/main.rs
  11. 12
      man/en/acmed.toml.5
  12. 8
      tacd/src/main.rs

2
.travis.yml

@ -8,6 +8,8 @@ rust:
- "1.39.0" - "1.39.0"
- "1.40.0" - "1.40.0"
- "1.41.1" - "1.41.1"
- "1.42.0"
- "1.43.1"
- "stable" - "stable"
- "beta" - "beta"
- "nightly" - "nightly"

3
CHANGELOG.md

@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- The HTTP(S) part is now handled by `reqwest` instead of `http_req`. - The HTTP(S) part is now handled by `reqwest` instead of `http_req`.
## Fixed
- `make install` now work with the busybox toolchain.
## [0.7.0] - 2020-03-12 ## [0.7.0] - 2020-03-12

18
Makefile

@ -35,15 +35,15 @@ update:
cargo update cargo update
install: install:
install -D --mode=0755 $(TARGET_DIR)/acmed $(DESTDIR)$(BINDIR)/acmed
install -D --mode=0755 $(TARGET_DIR)/tacd $(DESTDIR)$(BINDIR)/tacd
install -D --mode=0644 $(TARGET_DIR)/man/acmed.8.gz $(DESTDIR)$(DATADIR)/man/man8/acmed.8.gz
install -D --mode=0644 $(TARGET_DIR)/man/acmed.toml.5.gz $(DESTDIR)$(DATADIR)/man/man5/acmed.toml.5.gz
install -D --mode=0644 $(TARGET_DIR)/man/tacd.8.gz $(DESTDIR)$(DATADIR)/man/man8/tacd.8.gz
install -D --mode=0644 acmed/config/acmed.toml $(DESTDIR)$(SYSCONFDIR)/acmed/acmed.toml
install -D --mode=0644 acmed/config/default_hooks.toml $(DESTDIR)$(SYSCONFDIR)/acmed/default_hooks.toml
install -d --mode=0700 $(DESTDIR)$(SYSCONFDIR)/acmed/accounts
install -d --mode=0755 $(DESTDIR)$(SYSCONFDIR)/acmed/certs
install -D -m 0755 $(TARGET_DIR)/acmed $(DESTDIR)$(BINDIR)/acmed
install -D -m 0755 $(TARGET_DIR)/tacd $(DESTDIR)$(BINDIR)/tacd
install -D -m 0644 $(TARGET_DIR)/man/acmed.8.gz $(DESTDIR)$(DATADIR)/man/man8/acmed.8.gz
install -D -m 0644 $(TARGET_DIR)/man/acmed.toml.5.gz $(DESTDIR)$(DATADIR)/man/man5/acmed.toml.5.gz
install -D -m 0644 $(TARGET_DIR)/man/tacd.8.gz $(DESTDIR)$(DATADIR)/man/man8/tacd.8.gz
install -D -m 0644 acmed/config/acmed.toml $(DESTDIR)$(SYSCONFDIR)/acmed/acmed.toml
install -D -m 0644 acmed/config/default_hooks.toml $(DESTDIR)$(SYSCONFDIR)/acmed/default_hooks.toml
install -d -m 0700 $(DESTDIR)$(SYSCONFDIR)/acmed/accounts
install -d -m 0755 $(DESTDIR)$(SYSCONFDIR)/acmed/certs
clean: clean:
cargo clean cargo clean

7
README.md

@ -65,7 +65,7 @@ ACMEd depends OpenSSL 1.1.0 or higher.
On systems based on Debian/Ubuntu, you may need to install the `libssl-dev`, `build-essential` and `pkg-config` packages. On systems based on Debian/Ubuntu, you may need to install the `libssl-dev`, `build-essential` and `pkg-config` packages.
On Alpine Linux, you may need to install the `openssl-dev` and `alpine-sdk` packages. Also, you should use the `rust` and `cargo` packages in the community repository: installing Rust using rustup will result in compilation errors.
On Alpine Linux, you may need to install the `openssl-dev` and `alpine-sdk` packages. Since Alpine Linux 3.11 you can use the `rust` and `cargo` packages from the community repository. Older versions of Alpine Linux will require you to install Rust 1.44 or later using rustup.
``` ```
$ make $ make
@ -97,11 +97,14 @@ Running ACMEd as root is the simplest configuration since you do not have to wor
However, if you are concerned with safety, you should create a dedicated user for ACMEd. Before doing so, please consider the following points: "Will your services be able to read both the private key and the certificate?" and "Will the ACMEd user be able to execute the hooks?". The later could be achieved using sudo or Polkit. However, if you are concerned with safety, you should create a dedicated user for ACMEd. Before doing so, please consider the following points: "Will your services be able to read both the private key and the certificate?" and "Will the ACMEd user be able to execute the hooks?". The later could be achieved using sudo or Polkit.
### Why is there no option to run ACMEd as a specific user or group? ### Why is there no option to run ACMEd as a specific user or group?
The reason some services has such an option is because at startup they may have to load data only accessible by root, hence they have to change the user themselves after those data are loaded. For example, this is wildly used in web servers so they load a private key, which should only be accessible by root. Since ACMEd does not have such requirement, it should be run directly as the correct user. The reason some services has such an option is because at startup they may have to load data only accessible by root, hence they have to change the user themselves after those data are loaded. For example, this is wildly used in web servers so they load a private key, which should only be accessible by root. Since ACMEd does not have such requirement, it should be run directly as the correct user.
### How can I run ACMEd with systemd?
An example service file is provided (see `acmed.service.example`). The file might need adjustments in order to work on your system (e.g. binary path, user, group, directories...), but it's probably a good starting point.
### Is it suitable for beginners? ### Is it suitable for beginners?
It depends on your definition of a beginner. This software is intended to be used by system administrator with a certain knowledge of their environment. Furthermore, it is also expected to know the bases of the ACME protocol. Let's Encrypt wrote a nice article about [how it works](https://letsencrypt.org/how-it-works/). It depends on your definition of a beginner. This software is intended to be used by system administrator with a certain knowledge of their environment. Furthermore, it is also expected to know the bases of the ACME protocol. Let's Encrypt wrote a nice article about [how it works](https://letsencrypt.org/how-it-works/).

17
acme_common/src/lib.rs

@ -1,7 +1,7 @@
use daemonize::Daemonize; use daemonize::Daemonize;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::process;
use std::{fs, process};
pub mod crypto; pub mod crypto;
pub mod error; pub mod error;
@ -40,12 +40,12 @@ pub fn b64_encode<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
base64::encode_config(input, base64::URL_SAFE_NO_PAD) base64::encode_config(input, base64::URL_SAFE_NO_PAD)
} }
pub fn init_server(foreground: bool, pid_file: &str) {
pub fn init_server(foreground: bool, pid_file: Option<&str>, default_pid_file: &str) {
if !foreground { if !foreground {
let daemonize = Daemonize::new().pid_file(pid_file);
let daemonize = Daemonize::new().pid_file(pid_file.unwrap_or(default_pid_file));
exit_match!(daemonize.start()); exit_match!(daemonize.start());
} else {
exit_match!(write_pid_file(pid_file));
} else if let Some(f) = pid_file {
exit_match!(write_pid_file(f));
} }
} }
@ -57,6 +57,13 @@ fn write_pid_file(pid_file: &str) -> Result<(), error::Error> {
Ok(()) Ok(())
} }
pub fn clean_pid_file(pid_file: Option<&str>) -> Result<(), error::Error> {
if let Some(f) = pid_file {
fs::remove_file(f)?;
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::to_idna; use super::to_idna;

29
acmed.service.example

@ -0,0 +1,29 @@
# systemd example unit file. Please adjust.
[Unit]
Description=ACME client daemon
After=network.target
[Service]
User=acmed
Group=acmed
# Working directory
WorkingDirectory=/etc/acmed
# Starting, stopping, timeouts
ExecStart=/usr/local/bin/acmed --foreground --pid-file /etc/acmed/acmed.pid --log-level debug --log-stderr
TimeoutStartSec=3
TimeoutStopSec=5
Restart=on-failure
KillSignal=SIGINT
# Sandboxing, reduce privileges, only allow write access to working directory
NoNewPrivileges=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectSystem=strict
ReadWritePaths=/etc/acmed/
[Install]
WantedBy=multi-user.target

2
acmed/src/acme_proto.rs

@ -177,6 +177,6 @@ pub fn request_certificate(
let crt = http::get_certificate(endpoint, root_certs, &data_builder, &crt_url)?; let crt = http::get_certificate(endpoint, root_certs, &data_builder, &crt_url)?;
storage::write_certificate(cert, &crt.as_bytes())?; storage::write_certificate(cert, &crt.as_bytes())?;
cert.info("Certificate renewed");
cert.info(&format!("Certificate renewed (domains: {})", cert.domain_list()));
Ok(()) Ok(())
} }

6
acmed/src/acme_proto/account.rs

@ -45,13 +45,11 @@ pub fn init_account(cert: &Certificate) -> Result<(), Error> {
let sign_alg = SignatureAlgorithm::from_str(crate::DEFAULT_JWS_SIGN_ALGO)?; let sign_alg = SignatureAlgorithm::from_str(crate::DEFAULT_JWS_SIGN_ALGO)?;
let key_pair = sign_alg.gen_key_pair()?; let key_pair = sign_alg.gen_key_pair()?;
storage::set_account_keypair(cert, &key_pair)?; storage::set_account_keypair(cert, &key_pair)?;
let msg = format!("Account {} created.", &cert.account.name);
cert.info(&msg)
cert.info(&format!("Account {} created", &cert.account.name));
} else { } else {
// TODO: check if the keys are suitable for the specified signature algorithm // TODO: check if the keys are suitable for the specified signature algorithm
// and, if not, initiate a key rollover. // and, if not, initiate a key rollover.
let msg = format!("Account {} already exists.", &cert.account.name);
cert.debug(&msg)
cert.debug(&format!("Account {} already exists", &cert.account.name));
} }
Ok(()) Ok(())
} }

16
acmed/src/certificate.rs

@ -102,7 +102,7 @@ impl Certificate {
fn is_expiring(&self, cert: &X509Certificate) -> Result<bool, Error> { fn is_expiring(&self, cert: &X509Certificate) -> Result<bool, Error> {
let expires_in = cert.expires_in()?; let expires_in = cert.expires_in()?;
self.debug(&format!("expires in {} days", expires_in.as_secs() / 86400));
self.debug(&format!("Certificate expires in {} days", expires_in.as_secs() / 86400));
// TODO: allow a custom duration (using time-parse ?) // TODO: allow a custom duration (using time-parse ?)
// 1814400 is 3 weeks (3 * 7 * 24 * 60 * 60) // 1814400 is 3 weeks (3 * 7 * 24 * 60 * 60)
let renewal_time = Duration::new(1_814_400, 0); let renewal_time = Duration::new(1_814_400, 0);
@ -131,7 +131,17 @@ impl Certificate {
has_miss has_miss
} }
/// Return a comma-separated list of the domains this certificate is valid for.
pub fn domain_list(&self) -> String {
self.domains
.iter()
.map(|domain| &*domain.dns)
.collect::<Vec<&str>>()
.join(",")
}
pub fn should_renew(&self) -> Result<bool, Error> { pub fn should_renew(&self) -> Result<bool, Error> {
self.debug(&format!("Checking for renewal (domains: {})", self.domain_list()));
if !certificate_files_exists(&self) { if !certificate_files_exists(&self) {
self.debug("certificate does not exist: requesting one"); self.debug("certificate does not exist: requesting one");
return Ok(true); return Ok(true);
@ -142,9 +152,9 @@ impl Certificate {
let renew = renew || self.is_expiring(&cert)?; let renew = renew || self.is_expiring(&cert)?;
if renew { if renew {
self.debug("The certificate will be renewed now.");
self.debug("The certificate will be renewed now");
} else { } else {
self.debug("The certificate will not be renewed now.");
self.debug("The certificate will not be renewed now");
} }
Ok(renew) Ok(renew)
} }

8
acmed/src/main.rs

@ -1,5 +1,5 @@
use crate::main_event_loop::MainEventLoop; use crate::main_event_loop::MainEventLoop;
use acme_common::init_server;
use acme_common::{clean_pid_file, init_server};
use clap::{App, Arg}; use clap::{App, Arg};
use log::error; use log::error;
@ -15,7 +15,7 @@ mod storage;
pub const APP_NAME: &str = "ACMEd"; pub const APP_NAME: &str = "ACMEd";
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_PID_FILE: &str = "/var/run/admed.pid";
pub const DEFAULT_PID_FILE: &str = "/var/run/acmed.pid";
pub const DEFAULT_CONFIG_FILE: &str = "/etc/acmed/acmed.toml"; pub const DEFAULT_CONFIG_FILE: &str = "/etc/acmed/acmed.toml";
pub const DEFAULT_ACCOUNTS_DIR: &str = "/etc/acmed/accounts"; pub const DEFAULT_ACCOUNTS_DIR: &str = "/etc/acmed/accounts";
pub const DEFAULT_CERT_DIR: &str = "/etc/acmed/certs"; pub const DEFAULT_CERT_DIR: &str = "/etc/acmed/certs";
@ -118,7 +118,8 @@ fn main() {
init_server( init_server(
matches.is_present("foreground"), matches.is_present("foreground"),
matches.value_of("pid-file").unwrap_or(DEFAULT_PID_FILE),
matches.value_of("pid-file"),
DEFAULT_PID_FILE,
); );
let config_file = matches.value_of("config").unwrap_or(DEFAULT_CONFIG_FILE); let config_file = matches.value_of("config").unwrap_or(DEFAULT_CONFIG_FILE);
@ -126,6 +127,7 @@ fn main() {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);
let _ = clean_pid_file(matches.value_of("pid-file"));
std::process::exit(1); std::process::exit(1);
} }
}; };

12
man/en/acmed.toml.5

@ -99,7 +99,7 @@ for more details.
Defines if an error return value for this hook is allowed or not. If not allowed, a failure in this hook will fail the whole certificate request process. Default is false. Defines if an error return value for this hook is allowed or not. If not allowed, a failure in this hook will fail the whole certificate request process. Default is false.
.It Cm name Ar string .It Cm name Ar string
The name the hook is registered under. Must be unique. The name the hook is registered under. Must be unique.
.It Cm hook_type Ar array
.It Cm type Ar array
Array of strings. Possible types are: Array of strings. Possible types are:
.Bl -dash -compact .Bl -dash -compact
.It .It
@ -158,6 +158,8 @@ The email address used to contact the account's holder.
.El .El
.It Ic certificate .It Ic certificate
Array of table representing a certificate that will be requested to a CA. Array of table representing a certificate that will be requested to a CA.
.Pp
Note that certificates are identified by the first domain in the list of domains. That means that if you reorder the domains so that a different domain is at the first position, a new certificate with a new name will be issued.
.Bl -tag .Bl -tag
.It Ic account Ar string .It Ic account Ar string
Name of the account to use. Name of the account to use.
@ -204,15 +206,15 @@ Path to the directory where certificates and their associated private keys are s
Names of hooks that will be called when requesting a new certificate. The hooks are guaranteed to be called sequentially in the declaration order. Names of hooks that will be called when requesting a new certificate. The hooks are guaranteed to be called sequentially in the declaration order.
.El .El
.Sh WRITING A HOOK .Sh WRITING A HOOK
When requesting a certificate to a CA using ACME, there is three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every domains to be included: it requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that uses a given certificate, for the same reason. The last one is archiving: although several default methods can be implemented, sometimes admins wants or are required to do it in a different way.
When requesting a certificate from a CA using ACME, there are three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every domains to be included: It requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that use a given certificate, for the same reason. The last one is archiving: Although several default methods can be implemented, sometimes admins wants or are required to do it in a different way.
.Pp .Pp
In order to allow a full automation of the three above steps without imposing arbitrary restrictions or methods,
In order to allow full automation of the three above steps without imposing arbitrary restrictions or methods,
.Xr acmed 8 .Xr acmed 8
uses hooks. Fundamentally, a hook is a command line template that will be called at a specific time of the process. Such approach allows admins to use any executable script or program located on the machine to customize the process.
uses hooks. Fundamentally, a hook is a command line template that will be called at a specific time of the process. Such an approach allows admins to use any executable script or program located on the machine to customize the process.
.Pp .Pp
For a given certificate, hooks are guaranteed to be called sequentially in the declaration order. It is therefore possible to have a hook that depends on another one. Nevertheless, several certificates may be renewed at the same time. Hence, hooks shall not use globing or any other action that may disrupt hooks called by a different certificate. For a given certificate, hooks are guaranteed to be called sequentially in the declaration order. It is therefore possible to have a hook that depends on another one. Nevertheless, several certificates may be renewed at the same time. Hence, hooks shall not use globing or any other action that may disrupt hooks called by a different certificate.
.Pp .Pp
A hook have a type that will influence both the moment it is called and the available template variables. It is possible to declare several types. In such a case, the hook will be invoked whenever one of its type request it. When called, the hook only have access to template variable for the current type. If a hook uses a template variable that does not exists for the current type it is invoked for, the variable is empty.
A hook has a type that will influence both the moment it is called and the available template variables. It is possible to declare several types. In such a case, the hook will be invoked whenever one of its type request it. When called, the hook only have access to template variable for the current type. If a hook uses a template variable that does not exists for the current type it is invoked for, the variable is empty.
.Pp .Pp
When writing a hook, the values of When writing a hook, the values of
.Em args , .Em args ,

8
tacd/src/main.rs

@ -3,7 +3,7 @@ mod openssl_server;
use crate::openssl_server::start as server_start; use crate::openssl_server::start as server_start;
use acme_common::crypto::X509Certificate; use acme_common::crypto::X509Certificate;
use acme_common::error::Error; use acme_common::error::Error;
use acme_common::to_idna;
use acme_common::{clean_pid_file, to_idna};
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use log::{debug, error, info}; use log::{debug, error, info};
use std::fs::File; use std::fs::File;
@ -11,7 +11,7 @@ use std::io::{self, Read};
const APP_NAME: &str = env!("CARGO_PKG_NAME"); const APP_NAME: &str = env!("CARGO_PKG_NAME");
const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_PID_FILE: &str = "/var/run/admed.pid";
const DEFAULT_PID_FILE: &str = "/var/run/tacd.pid";
const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:5001"; const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:5001";
const ALPN_ACME_PROTO_NAME: &[u8] = b"\x0aacme-tls/1"; const ALPN_ACME_PROTO_NAME: &[u8] = b"\x0aacme-tls/1";
@ -42,7 +42,8 @@ fn get_acme_value(cnf: &ArgMatches, opt: &str, opt_file: &str) -> Result<String,
fn init(cnf: &ArgMatches) -> Result<(), Error> { fn init(cnf: &ArgMatches) -> Result<(), Error> {
acme_common::init_server( acme_common::init_server(
cnf.is_present("foreground"), cnf.is_present("foreground"),
cnf.value_of("pid-file").unwrap_or(DEFAULT_PID_FILE),
cnf.value_of("pid-file"),
DEFAULT_PID_FILE,
); );
let domain = get_acme_value(cnf, "domain", "domain-file")?; let domain = get_acme_value(cnf, "domain", "domain-file")?;
let domain = to_idna(&domain)?; let domain = to_idna(&domain)?;
@ -150,6 +151,7 @@ fn main() {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);
let _ = clean_pid_file(matches.value_of("pid-file"));
std::process::exit(1); std::process::exit(1);
} }
}; };

Loading…
Cancel
Save