1
0
mirror of https://github.com/moepman/acertmgr.git synced 2024-11-14 17:25:26 +01:00

configuration: cleanup handling+defaults and add commandline options

This adds a few basic command line parameters to allow further
customization of the configuration locations. As well as defining new
default locations for the acertmgr config files and updating the parser
with missing values, so that the config dictionary provided to the
acertmgr process after parsing is complete and no cross reference to the
configuration module is necessary. The parser error handling is also
improved.
This commit is contained in:
Kishi85 2019-02-20 11:57:15 +01:00
parent 33678aac8e
commit 67c83d8fce
4 changed files with 92 additions and 54 deletions

View File

@ -44,11 +44,12 @@ While testing, you can use the acme-staging authority instead, in order to avoid
Configuration Configuration
------------- -------------
The main configuration is read from `/etc/acme/acme.conf`, domains for which certificates should be obtained/renewed should be configured in `/etc/acme/domains.d/*.conf`. Unless specified with a commandline parameter (see acertmgr.py --help) the optional global configuration is read from '/etc/acertmgr/acertmgr.conf'.
Domains for which certificates should be obtained/renewed should be configured in `/etc/acertmgr/*.conf` (the global configuration is automatically excluded if it is in the same directory).
All configuration files can use yaml (requires PyYAML) or json syntax. All configuration files can use yaml (requires PyYAML) or json syntax.
* Example global configuration file (YAML syntax): * Example optional global configuration file (YAML syntax):
```yaml ```yaml
--- ---
# Required: Authority API endpoint to use # Required: Authority API endpoint to use
@ -138,7 +139,7 @@ mail.example.com smtp.example.com webmail.example.net:
``` ```
* Example global configuration file (JSON syntax): * Example optional global configuration file (JSON syntax):
```json ```json
--- ---
{ {
@ -150,11 +151,6 @@ mail.example.com smtp.example.com webmail.example.net:
"webdir": "/var/www/acme-challenge/", "webdir": "/var/www/acme-challenge/",
"authority": "https://acme-v01.api.letsencrypt.org", "authority": "https://acme-v01.api.letsencrypt.org",
"defaults":
{
"cafile": "/etc/acme/lets-encrypt-x3-cross-signed.pem"
}
} }
``` ```

View File

@ -34,20 +34,15 @@ def target_is_current(target, file):
# @brief create a authority for the given configuration # @brief create a authority for the given configuration
# @param settings the authority configuration options # @param settings the authority configuration options
def create_authority(settings): def create_authority(settings):
if "api" in settings:
api = settings["api"]
else:
api = "v1"
acc_file = settings['account_key'] acc_file = settings['account_key']
if not os.path.isfile(acc_file): if not os.path.isfile(acc_file):
print("Account key not found at '{0}'. Creating RSA key.".format(acc_file)) print("Account key not found at '{0}'. Creating RSA key.".format(acc_file))
tools.new_rsa_key(acc_file) tools.new_rsa_key(acc_file)
acc_key = tools.read_key(acc_file) acc_key = tools.read_key(acc_file)
authority_module = importlib.import_module("acertmgr.authority.{0}".format(api)) authority_module = importlib.import_module("acertmgr.authority.{0}".format(settings["api"]))
authority_class = getattr(authority_module, "ACMEAuthority") authority_class = getattr(authority_module, "ACMEAuthority")
return authority_class(settings.get('authority'), acc_key) return authority_class(settings['authority'], acc_key)
# @brief create a challenge handler for the given configuration # @brief create a challenge handler for the given configuration
@ -171,7 +166,7 @@ def main():
# check certificate validity and obtain/renew certificates if needed # check certificate validity and obtain/renew certificates if needed
for config in configs: for config in configs:
cert_file = config['cert_file'] cert_file = config['cert_file']
ttl_days = int(config.get('ttl_days', configuration.ACME_DEFAULT_TTL)) ttl_days = int(config['ttl_days'])
if not tools.is_cert_valid(cert_file, ttl_days): if not tools.is_cert_valid(cert_file, ttl_days):
cert_get(config) cert_get(config)
for cfg in config['actions']: for cfg in config['actions']:

View File

@ -6,19 +6,24 @@
# Copyright (c) Rudolf Mayerhofer, 2019. # Copyright (c) Rudolf Mayerhofer, 2019.
# available under the ISC license, see LICENSE # available under the ISC license, see LICENSE
import argparse
import copy import copy
import io import io
import hashlib
import os import os
from acertmgr import tools # Backward compatiblity for older versions/installations of acertmgr
LEGACY_WORK_DIR = "/etc/acme"
LEGACY_CONF_FILE = os.path.join(LEGACY_WORK_DIR, "acme.conf")
LEGACY_CONF_DIR = os.path.join(LEGACY_WORK_DIR, "domains.d")
ACME_DIR = "/etc/acme" # Configuration defaults to use if not specified otherwise
ACME_CONF = os.path.join(ACME_DIR, "acme.conf") DEFAULT_CONF_FILE = "/etc/acertmgr/acertmgr.conf"
ACME_CONFD = os.path.join(ACME_DIR, "domains.d") DEFAULT_CONF_DIR = "/etc/acertmgr"
DEFAULT_KEY_LENGTH = 4096 # bits
ACME_DEFAULT_ACCOUNT_KEY = os.path.join(ACME_DIR, "account.key") DEFAULT_TTL = 15 # days
ACME_DEFAULT_KEY_LENGTH = 4096 # bits DEFAULT_API = "v1"
ACME_DEFAULT_TTL = 15 # days DEFAULT_AUTHORITY = "https://acme-v01.api.letsencrypt.org"
# @brief augment configuration with defaults # @brief augment configuration with defaults
@ -39,42 +44,51 @@ def complete_action_config(domainconfig, config):
# @brief load the configuration from a file # @brief load the configuration from a file
def parse_config_entry(entry, globalconfig): def parse_config_entry(entry, globalconfig, work_dir):
config = dict() config = dict()
# Basic domain information # Basic domain information
config['domains'], data = entry config['domains'], data = entry
config['domainlist'] = config['domains'].split(' ') config['domainlist'] = config['domains'].split(' ')
config['id'] = tools.to_unique_id(config['domains']) config['id'] = hashlib.md5(config['domains'].encode('utf-8')).hexdigest()
# Defaults # Action config defaults
config['defaults'] = globalconfig.get('defaults', {}) config['defaults'] = globalconfig.get('defaults', {})
# API version # API version
apis = [x for x in entry if 'api' in x] apis = [x for x in entry if 'api' in x]
if len(apis) > 0: if len(apis) > 0:
config['api'] = apis[0] config['api'] = apis[0]
else:
config['api'] = globalconfig.get('api', DEFAULT_API)
# Certificate authority # Certificate authority
authorities = [x for x in entry if 'authority' in x] authorities = [x for x in entry if 'authority' in x]
if len(authorities) > 0: if len(authorities) > 0:
config['authority'] = authorities[0] config['authority'] = authorities[0]
else: else:
config['authority'] = globalconfig.get('authority') config['authority'] = globalconfig.get('authority', DEFAULT_AUTHORITY)
# Account key # Account key
acc_keys = [x for x in entry if 'account_key' in x] acc_keys = [x for x in entry if 'account_key' in x]
if len(acc_keys) > 0: if len(acc_keys) > 0:
config['account_key'] = acc_keys[0] config['account_key'] = acc_keys[0]
else: else:
config['account_key'] = globalconfig.get('account_key', ACME_DEFAULT_ACCOUNT_KEY) config['account_key'] = globalconfig.get('account_key', os.path.join(work_dir, "account.key"))
# Certificate directory # Certificate directory
cert_dirs = [x for x in entry if 'cert_dir' in x] cert_dirs = [x for x in entry if 'cert_dir' in x]
if len(cert_dirs) > 0: if len(cert_dirs) > 0:
config['cert_dir'] = cert_dirs[0] config['cert_dir'] = cert_dirs[0]
else: else:
config['cert_dir'] = globalconfig.get('cert_dir', ACME_DIR) config['cert_dir'] = globalconfig.get('cert_dir', work_dir)
# TTL days
cert_dirs = [x for x in entry if 'ttl_days' in x]
if len(cert_dirs) > 0:
config['ttl_days'] = cert_dirs[0]
else:
config['ttl_days'] = globalconfig.get('ttl_days', DEFAULT_TTL)
# SSL CA location # SSL CA location
ca_files = [x for x in entry if 'ca_file' in x] ca_files = [x for x in entry if 'ca_file' in x]
@ -109,7 +123,7 @@ def parse_config_entry(entry, globalconfig):
if len(key_lengths) > 0: if len(key_lengths) > 0:
config['key_length'] = int(key_lengths[0]) config['key_length'] = int(key_lengths[0])
else: else:
config['key_length'] = ACME_DEFAULT_KEY_LENGTH config['key_length'] = DEFAULT_KEY_LENGTH
# Domain action configuration # Domain action configuration
config['actions'] = list() config['actions'] = list()
@ -140,10 +154,44 @@ def parse_config_entry(entry, globalconfig):
# @brief load the configuration from a file # @brief load the configuration from a file
def load(): def load():
globalconfig = dict() parser = argparse.ArgumentParser(description="acertmgr - Automated Certificate Manager using ACME/Let's Encrypt")
parser.add_argument("-c", "--config-file", nargs="?",
help="global configuration file (default='{}')".format(DEFAULT_CONF_FILE))
parser.add_argument("-d", "--config-dir", nargs="?",
help="domain configuration directory (default='{}')".format(DEFAULT_CONF_DIR))
parser.add_argument("-w", "--work-dir", nargs="?",
help="persistent work data directory (default=config_dir)")
args = parser.parse_args()
# Determine global configuration file
if args.config_file:
global_config_file = args.config_file
elif os.path.isfile(LEGACY_CONF_FILE):
global_config_file = LEGACY_CONF_FILE
else:
global_config_file = DEFAULT_CONF_FILE
# Determine domain configuration directory
if args.config_dir:
domain_config_dir = args.config_dir
elif os.path.isdir(LEGACY_CONF_DIR):
domain_config_dir = LEGACY_CONF_DIR
else:
domain_config_dir = DEFAULT_CONF_DIR
# Determine work directory...
if args.work_dir:
work_dir = args.work_dir
elif os.path.isdir(LEGACY_WORK_DIR):
work_dir = LEGACY_WORK_DIR
else:
# .. or use the domain configuration directory otherwise
work_dir = domain_config_dir
# load global configuration # load global configuration
if os.path.isfile(ACME_CONF): globalconfig = dict()
with io.open(ACME_CONF) as config_fd: if os.path.isfile(global_config_file):
with io.open(global_config_file) as config_fd:
try: try:
import json import json
globalconfig = json.load(config_fd) globalconfig = json.load(config_fd)
@ -152,19 +200,25 @@ def load():
config_fd.seek(0) config_fd.seek(0)
globalconfig = yaml.load(config_fd) globalconfig = yaml.load(config_fd)
config = list() # create work directory if it does not exist
if not os.path.isdir(work_dir):
os.mkdir(work_dir, int("0700", 8))
# load domain configuration # load domain configuration
for config_file in os.listdir(ACME_CONFD): config = list()
if config_file.endswith(".conf"): if os.path.isdir(domain_config_dir):
with io.open(os.path.join(ACME_CONFD, config_file)) as config_fd: for domain_config_file in os.listdir(domain_config_dir):
# check file extension and skip if global config file
if domain_config_file.endswith(".conf") and domain_config_file != global_config_file:
with io.open(os.path.join(domain_config_dir, domain_config_file)) as config_fd:
try: try:
import json import json
for entry in json.load(config_fd).items(): for entry in json.load(config_fd).items():
config.append(parse_config_entry(entry, globalconfig)) config.append(parse_config_entry(entry, globalconfig, work_dir))
except ValueError: except ValueError:
import yaml import yaml
config_fd.seek(0) config_fd.seek(0)
for entry in yaml.load(config_fd).items(): for entry in yaml.load(config_fd).items():
config.append(parse_config_entry(entry, globalconfig)) config.append(parse_config_entry(entry, globalconfig, work_dir))
return config return config

View File

@ -158,10 +158,3 @@ def byte_string_format(num):
n = format(num, 'x') n = format(num, 'x')
n = "0{0}".format(n) if len(n) % 2 else n n = "0{0}".format(n) if len(n) % 2 else n
return binascii.unhexlify(n) return binascii.unhexlify(n)
# @brief convert a string to an ID
# @param data data to convert to id
# @return unique id string
def to_unique_id(data):
return hashlib.md5(data.encode('utf-8')).hexdigest()