mirror of
https://github.com/moepman/acertmgr.git
synced 2024-11-13 06:45:24 +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:
parent
33678aac8e
commit
67c83d8fce
12
README.md
12
README.md
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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']:
|
||||
|
@ -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:
|
||||
try:
|
||||
import json
|
||||
for entry in json.load(config_fd).items():
|
||||
config.append(parse_config_entry(entry, globalconfig))
|
||||
except ValueError:
|
||||
import yaml
|
||||
config_fd.seek(0)
|
||||
for entry in yaml.load(config_fd).items():
|
||||
config.append(parse_config_entry(entry, globalconfig))
|
||||
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, 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, work_dir))
|
||||
|
||||
return config
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user