diff --git a/.travis.yml b/.travis.yml index 43c0d20..3ed0147 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ rust: - "1.39.0" - "1.40.0" - "1.41.1" + - "1.42.0" + - "1.43.1" - "stable" - "beta" - "nightly" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f79b56..9dde0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - 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 diff --git a/Makefile b/Makefile index 8898179..dec4bef 100644 --- a/Makefile +++ b/Makefile @@ -35,15 +35,15 @@ update: cargo update 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: cargo clean diff --git a/README.md b/README.md index a305f01..82c8b3c 100644 --- a/README.md +++ b/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 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 @@ -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. - ### 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. +### 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? 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/). diff --git a/acme_common/src/lib.rs b/acme_common/src/lib.rs index 0e6b3aa..147945d 100644 --- a/acme_common/src/lib.rs +++ b/acme_common/src/lib.rs @@ -1,7 +1,7 @@ use daemonize::Daemonize; use std::fs::File; use std::io::prelude::*; -use std::process; +use std::{fs, process}; pub mod crypto; pub mod error; @@ -40,12 +40,12 @@ pub fn b64_encode>(input: &T) -> String { 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 { - 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()); - } 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(()) } +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)] mod tests { use super::to_idna; diff --git a/acmed.service.example b/acmed.service.example new file mode 100644 index 0000000..96c2bb9 --- /dev/null +++ b/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 diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index 3eb2600..8e471ac 100644 --- a/acmed/src/acme_proto.rs +++ b/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)?; storage::write_certificate(cert, &crt.as_bytes())?; - cert.info("Certificate renewed"); + cert.info(&format!("Certificate renewed (domains: {})", cert.domain_list())); Ok(()) } diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index 96fbada..9c34045 100644 --- a/acmed/src/acme_proto/account.rs +++ b/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 key_pair = sign_alg.gen_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 { // TODO: check if the keys are suitable for the specified signature algorithm // 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(()) } diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index de3d1e9..29f2d28 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -102,7 +102,7 @@ impl Certificate { fn is_expiring(&self, cert: &X509Certificate) -> Result { 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 ?) // 1814400 is 3 weeks (3 * 7 * 24 * 60 * 60) let renewal_time = Duration::new(1_814_400, 0); @@ -131,7 +131,17 @@ impl Certificate { 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::>() + .join(",") + } + pub fn should_renew(&self) -> Result { + self.debug(&format!("Checking for renewal (domains: {})", self.domain_list())); if !certificate_files_exists(&self) { self.debug("certificate does not exist: requesting one"); return Ok(true); @@ -142,9 +152,9 @@ impl Certificate { let renew = renew || self.is_expiring(&cert)?; if renew { - self.debug("The certificate will be renewed now."); + self.debug("The certificate will be renewed now"); } else { - self.debug("The certificate will not be renewed now."); + self.debug("The certificate will not be renewed now"); } Ok(renew) } diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 026beb2..fe930c7 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -1,5 +1,5 @@ use crate::main_event_loop::MainEventLoop; -use acme_common::init_server; +use acme_common::{clean_pid_file, init_server}; use clap::{App, Arg}; use log::error; @@ -15,7 +15,7 @@ mod storage; pub const APP_NAME: &str = "ACMEd"; 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_ACCOUNTS_DIR: &str = "/etc/acmed/accounts"; pub const DEFAULT_CERT_DIR: &str = "/etc/acmed/certs"; @@ -118,7 +118,8 @@ fn main() { init_server( 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); @@ -126,6 +127,7 @@ fn main() { Ok(s) => s, Err(e) => { error!("{}", e); + let _ = clean_pid_file(matches.value_of("pid-file")); std::process::exit(1); } }; diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 6ab9438..be89e8f 100644 --- a/man/en/acmed.toml.5 +++ b/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. .It Cm name Ar string 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: .Bl -dash -compact .It @@ -158,6 +158,8 @@ The email address used to contact the account's holder. .El .It Ic certificate 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 .It Ic account Ar string 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. .El .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 -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 -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 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 -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 When writing a hook, the values of .Em args , diff --git a/tacd/src/main.rs b/tacd/src/main.rs index ce57bd3..cc772aa 100644 --- a/tacd/src/main.rs +++ b/tacd/src/main.rs @@ -3,7 +3,7 @@ mod openssl_server; use crate::openssl_server::start as server_start; use acme_common::crypto::X509Certificate; use acme_common::error::Error; -use acme_common::to_idna; +use acme_common::{clean_pid_file, to_idna}; use clap::{App, Arg, ArgMatches}; use log::{debug, error, info}; use std::fs::File; @@ -11,7 +11,7 @@ use std::io::{self, Read}; const APP_NAME: &str = env!("CARGO_PKG_NAME"); 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 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 Result<(), Error> { acme_common::init_server( 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 = to_idna(&domain)?; @@ -150,6 +151,7 @@ fn main() { Ok(_) => {} Err(e) => { error!("{}", e); + let _ = clean_pid_file(matches.value_of("pid-file")); std::process::exit(1); } };