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:
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
|
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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']:
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
|
Loading…
Reference in New Issue
Block a user