1
0
mirror of https://github.com/moepman/acertmgr.git synced 2024-11-14 06:55:29 +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
-------------
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.
* Example global configuration file (YAML syntax):
* Example optional global configuration file (YAML syntax):
```yaml
---
# 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
---
{
@ -150,11 +151,6 @@ mail.example.com smtp.example.com webmail.example.net:
"webdir": "/var/www/acme-challenge/",
"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
# @param settings the authority configuration options
def create_authority(settings):
if "api" in settings:
api = settings["api"]
else:
api = "v1"
acc_file = settings['account_key']
if not os.path.isfile(acc_file):
print("Account key not found at '{0}'. Creating RSA key.".format(acc_file))
tools.new_rsa_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")
return authority_class(settings.get('authority'), acc_key)
return authority_class(settings['authority'], acc_key)
# @brief create a challenge handler for the given configuration
@ -171,7 +166,7 @@ def main():
# check certificate validity and obtain/renew certificates if needed
for config in configs:
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):
cert_get(config)
for cfg in config['actions']:

View File

@ -6,19 +6,24 @@
# Copyright (c) Rudolf Mayerhofer, 2019.
# available under the ISC license, see LICENSE
import argparse
import copy
import io
import hashlib
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"
ACME_CONF = os.path.join(ACME_DIR, "acme.conf")
ACME_CONFD = os.path.join(ACME_DIR, "domains.d")
ACME_DEFAULT_ACCOUNT_KEY = os.path.join(ACME_DIR, "account.key")
ACME_DEFAULT_KEY_LENGTH = 4096 # bits
ACME_DEFAULT_TTL = 15 # days
# Configuration defaults to use if not specified otherwise
DEFAULT_CONF_FILE = "/etc/acertmgr/acertmgr.conf"
DEFAULT_CONF_DIR = "/etc/acertmgr"
DEFAULT_KEY_LENGTH = 4096 # bits
DEFAULT_TTL = 15 # days
DEFAULT_API = "v1"
DEFAULT_AUTHORITY = "https://acme-v01.api.letsencrypt.org"
# @brief augment configuration with defaults
@ -39,42 +44,51 @@ def complete_action_config(domainconfig, config):
# @brief load the configuration from a file
def parse_config_entry(entry, globalconfig):
def parse_config_entry(entry, globalconfig, work_dir):
config = dict()
# Basic domain information
config['domains'], data = entry
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', {})
# API version
apis = [x for x in entry if 'api' in x]
if len(apis) > 0:
config['api'] = apis[0]
else:
config['api'] = globalconfig.get('api', DEFAULT_API)
# Certificate authority
authorities = [x for x in entry if 'authority' in x]
if len(authorities) > 0:
config['authority'] = authorities[0]
else:
config['authority'] = globalconfig.get('authority')
config['authority'] = globalconfig.get('authority', DEFAULT_AUTHORITY)
# Account key
acc_keys = [x for x in entry if 'account_key' in x]
if len(acc_keys) > 0:
config['account_key'] = acc_keys[0]
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
cert_dirs = [x for x in entry if 'cert_dir' in x]
if len(cert_dirs) > 0:
config['cert_dir'] = cert_dirs[0]
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
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:
config['key_length'] = int(key_lengths[0])
else:
config['key_length'] = ACME_DEFAULT_KEY_LENGTH
config['key_length'] = DEFAULT_KEY_LENGTH
# Domain action configuration
config['actions'] = list()
@ -140,10 +154,44 @@ def parse_config_entry(entry, globalconfig):
# @brief load the configuration from a file
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
if os.path.isfile(ACME_CONF):
with io.open(ACME_CONF) as config_fd:
globalconfig = dict()
if os.path.isfile(global_config_file):
with io.open(global_config_file) as config_fd:
try:
import json
globalconfig = json.load(config_fd)
@ -152,19 +200,25 @@ def load():
config_fd.seek(0)
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
for config_file in os.listdir(ACME_CONFD):
if config_file.endswith(".conf"):
with io.open(os.path.join(ACME_CONFD, config_file)) as config_fd:
config = list()
if os.path.isdir(domain_config_dir):
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:
import json
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:
import yaml
config_fd.seek(0)
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

View File

@ -158,10 +158,3 @@ def byte_string_format(num):
n = format(num, 'x')
n = "0{0}".format(n) if len(n) % 2 else 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()