Compare commits

...

17 Commits

Author SHA1 Message Date
Markus 9ca6dae048 version: bump to 1.0.5 2023-07-13 15:36:44 +02:00
Kishi85 274351415d Update github action workflow to build debian packages with gzip format for older OS versions 2023-07-12 17:46:06 +02:00
Rudolf Mayerhofer f78cb5c554 Github Action: Update to newer github action syntax/standards, change image to ubuntu-latest, change pypi-publish to supported version and check if we have credentials to publish at all 2023-07-12 16:10:21 +02:00
Rudolf Mayerhofer c3736c0838 Allow multiple sets of the same domain to defined in a single config file (necessary for multiple certs using different key_algorithm) in a list style notation (lists of maps) 2023-07-12 16:10:21 +02:00
Rudolf Mayerhofer 1a98f86aad Fix idna conversion for force-renew (probably broken since the IDNA cleanup) 2023-07-12 16:10:21 +02:00
Rudolf Mayerhofer ef81ea62d1 Unify key_algorithm handling for elipic curves (change naming to ECC but stay backwards compatible) 2023-07-12 16:10:21 +02:00
Rudolf Mayerhofer d1caaf80ef Fix LOG_REPLACEMENTS determination when multiple domain sets exist and we are on a newer version of python 2023-07-12 16:10:21 +02:00
Rudolf Mayerhofer ba644d44f1 Update config id if we have a key algorithm set to allow for multiple certs with different algorithms for the same set of domains
This is a breaking change!
Changes the id for configurations with a key algorithm set, which by default results in changes to serveral dependent configuration values as well,
such as cert_file/key_file/csr_file. This will require existing ECC setups to append the ecc suffix to files in the acertmgr configuration directory
2023-07-12 16:10:21 +02:00
Jan c15b6ec441 Instantiate HashAlgorithm in OCSPRequestBuilder
Installations of more recent cryptography require parameter hash
algorithm to be an instance of hashes.HashAlgorithm, not the bare object
itself.

Fixes #63
2023-07-10 19:27:44 +02:00
Kishi85 2d230e30d9 Clarify expected authority format (at least for v2) and add an example 2021-10-31 09:57:31 +01:00
Kishi85 6f0ccfdc91 logging: Add real counterparts of IDNA-mapped domains in brackets 2021-09-20 09:26:47 +02:00
Kishi85 460b0119ac configuration: Simplify too complex IDNA conversion 2021-09-13 09:00:59 +02:00
David Klaftenegger e2f7b09b18 certs already contain idna domain names
The idna_convert call here does nothing: when reading a certificate, it
already contains idna domain names. Converting them to idna is
equivalent to the identity function, and can thus be removed.
2021-05-30 16:21:54 +02:00
Markus 93e28437ff version: bump to 1.0.4 2021-05-21 22:52:34 +02:00
Kishi85 2e1f5cd894 acertmgr/v2: Handle CA certificate chains properly 2021-05-21 22:50:44 +02:00
Kishi85 ce157a5c8a CI: Build on Ubuntu 18.04 while we are Python 2 compatbile and OS version is not EOL 2021-03-23 18:43:07 +01:00
Kishi85 9953cb4527 standalone: Fix multiple challange handlers on same port
If you define challenge handlers on a per-domain basis multiple will be
created. This would cause the standalone handler to potientially try
to bind the same port (when configured) multiple times, which would only
work on the first try. Subsequent tries would fail with "Address already
in use". To fix this only bind the server between start and stop of the
challenge and cleanup afterwards.
2021-03-23 18:43:07 +01:00
9 changed files with 149 additions and 75 deletions

View File

@ -5,14 +5,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install dependencies
run: |
sudo apt update -qq -y
sudo apt install -qq -y build-essential fakeroot git python-all python3-cryptography python3-pip python3-stdeb python3-wheel twine
sudo apt install -qq -y build-essential fakeroot git python-all python3-cryptography python3-pip python3-stdeb python3-wheel twine dh-python
sudo pip3 install --upgrade setuptools wheel
- name: Prepare build process
@ -22,7 +22,7 @@ jobs:
git fetch --tags -f
VER="$(python3 setup.py --version)"
echo "Version found: $VER"
echo "::set-output name=version::$VER"
echo "version=$VER" >> $GITHUB_OUTPUT
- name: Build python package using setuptools (source/wheel)
run: |
@ -36,12 +36,20 @@ jobs:
sed "s/=determine_version()/='$(python3 setup.py --version)'/gi" -i setup.py
sed "s@('readme'@('share/doc/python3-acertmgr'@" -i setup.py
# Determine recommended dependencies for deb package
echo "::set-output name=recommends3::$(echo "python3-pkg-resources")"
# Find optional dependencies to suggest in deb package
echo "::set-output name=suggests3::$(python3 -c "from setup import extra_requirements; print('\n'.join(['\n'.join(x) for x in extra_requirements.values()]))" | grep -v cryptography | sed 's/PyYAML/yaml/gi' | awk '{ printf("python3-%s, ",$1)};' | awk '{$1=$1; print}')"
echo "recommends3=$(echo "python3-pkg-resources")" >> $GITHUB_OUTPUT
# Find optional dependencies to suggest in deb package
echo "suggests3=$(python3 -c "from setup import extra_requirements; print('\n'.join(['\n'.join(x) for x in extra_requirements.values()]))" | grep -v cryptography | sed 's/PyYAML/yaml/gi' | awk '{ printf("python3-%s, ",$1)};' | awk '{$1=$1; print}')" >> $GITHUB_OUTPUT
- name: Build debian package using setuptools and stdeb
run: python3 setup.py --command-packages=stdeb.command sdist_dsc --with-python2=False --with-python3=True --recommends3="${{ steps.stdebprep.outputs.recommends3 }}" --suggests3="${{ steps.stdebprep.outputs.suggests3 }}" bdist_deb
run: |
# Create debianized source directory
python3 setup.py --command-packages=stdeb.command sdist_dsc --with-python2=False --with-python3=True --recommends3="${{ steps.stdebprep.outputs.recommends3 }}" --suggests3="${{ steps.stdebprep.outputs.suggests3 }}"
# Enter debianzied source directory (first sub-directory under deb_dist)
cd "$(find deb_dist -maxdepth 1 -type d | grep -v 'deb_dist$\|tmp_sdist_dsc' | head -n1)"
# Enforce GZIP compressed debian package info for older OS versions
echo -e 'override_dh_builddeb:\n\tdh_builddeb -- -Zgzip\n' >> debian/rules
# Build deb package
DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -rfakeroot -uc -us
- name: Create a changelog from git log since last non-pre/rc tag
run: |
@ -85,9 +93,22 @@ jobs:
artifacts/*.whl
artifacts/*.deb
- name: Check PyPI secrets
id: checksecrets
shell: bash
run: |
if [ "$USER" == "" -o "$PASSWORD" == "" ]; then
echo "secretspresent=NO" >> $GITHUB_OUTPUT
else
echo "secretspresent=YES" >> $GITHUB_OUTPUT
fi
env:
USER: ${{ secrets.PYPI_USERNAME }}
PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- name: Create new PyPI release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && (!(contains(github.ref, 'rc') || contains(github.ref, 'pre')))
uses: pypa/gh-action-pypi-publish@master
if: steps.checksecrets.outputs.secretspresent == 'YES' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && (!(contains(github.ref, 'rc') || contains(github.ref, 'pre')))
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PYPI_USERNAME }}
password: ${{ secrets.PYPI_PASSWORD }}

View File

@ -12,7 +12,7 @@ Requirements
------------
* Python (2.7+ and 3.5+ should work)
* cryptography>=0.6 (usually includes the optional idna module)
* cryptography>=0.6
Optional requirements (to use specified features)
------------------------------------------------------
@ -56,7 +56,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
4 configuration contexts are known (*domainconfig (d) > globalconfig (g) > commandline (c) > built-in defaults*) with the following directives (subject to change, usual usage context written bold):
| Directive | Context | Description | Built-in Default |
| --- | --- | --- | --- |
| --- | --- |----------------------------------------------------------------------------------------------------------------------------------------------| --- |
| -c/--config-file | **c** | global configuration file (optional) | /etc/acertmgr/acertmgr.conf |
| -d/--config-dir | **c** | directory containing domain configuration files (ending with .conf, globalconfig will be excluded automatically if in same directory) | /etc/acertmgr/*.conf |
| -w/--work-dir | **c** | working directory containing csr/certificates/keys/ca files | /etc/acertmgr |
@ -65,7 +65,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
| --revoke-reason | **c** | Provide a reason code for the revocation (see https://tools.ietf.org/html/rfc5280#section-5.3.1 for valid values) | |
| domain (san-domain...): | **d** | (domainconfig section start) Domains to use in the cert request. This value will be MD5-hashed as cert_id. | |
| api | d,**g** | Determines the API version used | v2 |
| authority | d,**g** | URL to the certificate authorities API | https://acme-v02.api.letsencrypt.org |
| authority | d,**g** | URL to the certificate authorities ACME API root (without trailing /directory or similar) | https://acme-v02.api.letsencrypt.org |
| authority_tos_agreement | d,**g**,c | Indicates agreement to the ToS of the certificate authority (--authority-tos-agreement on command line) | |
| authority_contact_email | d,**g** | (v2 API only) Contact e-mail to be registered with your account key | |
| account_key | d,**g** | Path to the account key | {work_dir}/account.key |
@ -74,7 +74,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
| ttl_days | d,**g** | Renew certificate if it has less than this value validity left | 30 |
| validate_ocsp | d,**g** | Renew certificate if it's OCSP status is REVOKED. Allowed values for this key are: false, sha1, sha224, sha256, sha384, sha512 | sha1 (as mandated by RFC5019) |
| cert_dir | d,**g** | Directory containing all certificate related data (crt,key,csr) | {work_dir} |
| key_algorithm | d,**g** | Key-algorithm for newly generated private keys (RSA, EC, ED25519, ED448) | RSA |
| key_algorithm | d,**g** | Key-algorithm for newly generated private keys (RSA, ECC, ED25519, ED448) | RSA |
| key_length | d,**g** | Key-length for newly generated RSA private keys (in bits) or EC curve (256=P-256, 384=P-384, 521=P-521) | depends on key_algorithm |
| csr_static | **d**,g | Whether to re-use a static CSR or generate a new dynamic CSR | false |
| csr_file | **d**,g | Path to store (and load) the certificate CSR file | {cert_dir}/{cert_id}.csr |
@ -121,4 +121,4 @@ Please keep the following in mind when using this software:
* Create a dedicated user for acertmgr (e.g. acertmgr)
* Run a acertmgr as that user (add acertmgr to that users cron!)
* Access rights to read/write all files configured with the created user
* Run any programs/scripts defined on cert update as the created user (might need work-arounds with sudo or wrapper scripts)
* Run any programs/scripts defined on cert update as the created user (might need work-arounds with sudo or wrapper scripts)

View File

@ -15,7 +15,7 @@ import sys
from acertmgr import configuration, tools
from acertmgr.authority import authority
from acertmgr.modes import challenge_handler
from acertmgr.tools import log
from acertmgr.tools import log, LOG_REPLACEMENTS
try:
import pwd
@ -139,6 +139,10 @@ def cert_revoke(cert, configs, fallback_authority, reason=None):
def main():
# load config
runtimeconfig, domainconfigs = configuration.load()
# register idna-mapped domains as LOG_REPLACEMENTS for better readability of log output
for domainconfig in domainconfigs:
LOG_REPLACEMENTS.update({k: "{} [{}]".format(k, v) for k, v in domainconfig['domainlist_idna_mapped'].items()})
# Start processing
if runtimeconfig.get('mode') == 'revoke':
# Mode: revoke certificate
log("Revoking {}".format(runtimeconfig['revoke']))

View File

@ -260,8 +260,8 @@ class ACMEAuthority(AbstractACMEAuthority):
if code >= 400:
raise ValueError("Error downloading certificate chain: {0} {1}".format(code, certificate))
cert_dict = re.match((r'(?P<cert>-----BEGIN CERTIFICATE-----[^\-]+-----END CERTIFICATE-----)\n\n'
r'(?P<ca>-----BEGIN CERTIFICATE-----[^\-]+-----END CERTIFICATE-----)?'),
cert_dict = re.match((r'(?P<cert>^-----BEGIN CERTIFICATE-----\n[^\-]+\n-----END CERTIFICATE-----)\n*'
r'(?P<ca>-----BEGIN CERTIFICATE-----\n.+\n-----END CERTIFICATE-----)?$'),
certificate, re.DOTALL).groupdict()
cert = tools.convert_pem_str_to_cert(cert_dict['cert'])
if cert_dict['ca'] is None:

View File

@ -90,9 +90,14 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
config['id'] = hashlib.md5(domains.encode('utf-8')).hexdigest()
# Convert unicode to IDNA domains
config['domaintranslation'] = idna_convert(config['domainlist'])
if len(config['domaintranslation']) > 0:
config['domainlist'] = [x for x, _ in config['domaintranslation']]
config['domainlist_idna_mapped'] = {}
for idx in range(0, len(config['domainlist'])):
if any(ord(c) >= 128 for c in config['domainlist'][idx]):
domain_human = config['domainlist'][idx]
domain_idna = idna_convert(domain_human)
if domain_idna != domain_human:
config['domainlist'][idx] = domain_idna # Update domain with idna counterpart
config['domainlist_idna_mapped'][domain_idna] = domain_human # Store original domain for reference
# Action config defaults
config['defaults'] = globalconfig.get('defaults', {})
@ -119,6 +124,17 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
# Use a static cert request
update_config_value(config, 'csr_static', localconfig, globalconfig, "false")
# SSL key algorithm (if key has to be (re-)generated)
update_config_value(config, 'key_algorithm', localconfig, globalconfig, None)
# Update config id if we have a key algorithm set to allow for
# multiple certs with different algorithms for the same set of domains
if config.get('key_algorithm', None):
config['id'] += "_" + config['key_algorithm'].lower()
# SSL key length (if key has to be (re-)generated, converted to int)
update_config_value(config, 'key_length', localconfig, globalconfig, None)
config['key_length'] = int(config['key_length']) if config['key_length'] else None
# SSL cert request location
update_config_value(config, 'csr_file', localconfig, globalconfig,
os.path.join(config['cert_dir'], "{}.csr".format(config['id'])))
@ -131,13 +147,6 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
update_config_value(config, 'key_file', localconfig, globalconfig,
os.path.join(config['cert_dir'], "{}.key".format(config['id'])))
# SSL key algorithm (if key has to be (re-)generated)
update_config_value(config, 'key_algorithm', localconfig, globalconfig, None)
# SSL key length (if key has to be (re-)generated, converted to int)
update_config_value(config, 'key_length', localconfig, globalconfig, None)
config['key_length'] = int(config['key_length']) if config['key_length'] else None
# SSL CA location / use static
update_config_value(config, 'ca_file', localconfig, globalconfig,
os.path.join(config['cert_dir'], "{}.ca".format(config['id'])))
@ -162,8 +171,8 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
cfg.update(genericfgs[0])
# Update handler config with more specific values (use original names for translated unicode domains)
_domain = _domaintranslation_dict.get(domain, domain)
specificcfgs = [x for x in handlerconfigs if 'domain' in x and x['domain'] == _domain]
specificcfgs = [x for x in handlerconfigs if
'domain' in x and x['domain'] == config['domainlist_idna_mapped'].get(domain, domain)]
if len(specificcfgs) > 0:
cfg.update(specificcfgs[0])
@ -222,9 +231,9 @@ def load():
# - force-rewew
if args.force_renew:
domaintranslation = idna_convert(args.force_renew.split(' '))
domaintranslation = [idna_convert(d) for d in args.force_renew.split(' ')]
if len(domaintranslation) > 0:
runtimeconfig['force_renew'] = [x for x, _ in domaintranslation]
runtimeconfig['force_renew'] = domaintranslation
else:
runtimeconfig['force_renew'] = args.force_renew.split(' ')
@ -255,15 +264,29 @@ def load():
os.path.abspath(domain_config_file) != os.path.abspath(global_config_file):
with io.open(domain_config_file) as config_fd:
try:
for entry in json.load(config_fd).items():
domainconfigs.append(parse_config_entry(entry, globalconfig, runtimeconfig))
data = json.load(config_fd)
except ValueError:
import yaml
config_fd.seek(0)
for entry in yaml.safe_load(config_fd).items():
domainconfigs.append(parse_config_entry(entry, globalconfig, runtimeconfig))
data = yaml.safe_load(config_fd)
if isinstance(data, list):
# Handle newer config in list format (allows for multiple entries with same domains)
entries = list()
for element in data:
entries += element.items()
else:
# Handle older config format with just one entry per same domain set
entries = data.items()
for entry in entries:
domainconfigs.append(parse_config_entry(entry, globalconfig, runtimeconfig))
# Define a fallback authority from global configuration / defaults
runtimeconfig['fallback_authority'] = parse_authority([], globalconfig, runtimeconfig)
return runtimeconfig, domainconfigs
if __name__ == '__main__':
# Simple configuration load test and output
from pprint import pprint
pprint(load())

View File

@ -28,10 +28,20 @@ class HTTPServer6(HTTPServer):
class ChallengeHandler(HTTPChallengeHandler):
def __init__(self, config):
HTTPChallengeHandler.__init__(self, config)
bind_address = config.get("bind_address", "")
port = int(config.get("port", 80))
self.bind_address = config.get("bind_address", "")
self.port = int(config.get("port", 80))
self.challenges = {} # Initialize the challenge data dict
self.server_thread = None
self.server = None
def create_challenge(self, domain, thumbprint, token):
self.challenges[token] = "{0}.{1}".format(token, thumbprint)
def destroy_challenge(self, domain, thumbprint, token):
del self.challenges[token]
def start_challenge(self, domain, thumbprint, token):
_self = self
# Custom HTTP request handler
@ -54,19 +64,11 @@ class ChallengeHandler(HTTPChallengeHandler):
self.end_headers()
self.wfile.write(value)
self.server_thread = None
try:
self.server = HTTPServer6((bind_address, port), _HTTPRequestHandler)
self.server = HTTPServer6((self.bind_address, self.port), _HTTPRequestHandler)
except socket.gaierror:
self.server = HTTPServer((bind_address, port), _HTTPRequestHandler)
self.server = HTTPServer((self.bind_address, self.port), _HTTPRequestHandler)
def create_challenge(self, domain, thumbprint, token):
self.challenges[token] = "{0}.{1}".format(token, thumbprint)
def destroy_challenge(self, domain, thumbprint, token):
del self.challenges[token]
def start_challenge(self, domain, thumbprint, token):
def _serve():
self.server.serve_forever()
@ -78,3 +80,6 @@ class ChallengeHandler(HTTPChallengeHandler):
if self.server_thread.is_alive():
self.server.shutdown()
self.server_thread.join()
self.server.server_close()
self.server = None
self.server_thread = None

View File

@ -10,6 +10,7 @@ import base64
import datetime
import io
import os
import re
import stat
import sys
import traceback
@ -37,6 +38,8 @@ try:
except ImportError:
from urllib2 import urlopen, Request # Python 2
LOG_REPLACEMENTS = {}
class InvalidCertificateError(Exception):
pass
@ -58,6 +61,9 @@ def log(msg, exc=None, error=False, warning=False):
prefix = ""
output = prefix + msg
for k, v in LOG_REPLACEMENTS.items():
output = output.replace(k, v)
if exc:
_, exc_value, _ = sys.exc_info()
if not getattr(exc, '__traceback__', None) and exc == exc_value:
@ -138,7 +144,7 @@ def new_ssl_key(path=None, key_algo=None, key_size=None):
key_size=key_size,
backend=default_backend()
)
elif key_algo.lower() == 'ec':
elif key_algo.lower() == 'ec' or key_algo.lower() == 'ecc':
if not key_size or key_size == 256:
key_curve = ec.SECP256R1
elif key_size == 384:
@ -242,8 +248,7 @@ def get_cert_domains(cert):
if san_cert:
for d in san_cert.value:
domains.add(d.value)
# Convert IDNA domain to correct representation and return the list
return [x for x, _ in idna_convert(domains)]
return domains
# @brief determine certificate cn
@ -257,15 +262,26 @@ def get_cert_valid_until(cert):
# @brief convert certificate to PEM format
# @param cert certificate object in pyopenssl format
# @param cert certificate object or a list thereof
# @return the certificate in PEM format
def convert_cert_to_pem_str(cert):
return cert.public_bytes(serialization.Encoding.PEM).decode('utf8')
if not isinstance(cert, list):
cert = [cert]
result = list()
for data in cert:
result.append(data.public_bytes(serialization.Encoding.PEM).decode('utf8'))
return '\n'.join(result)
# @brief load a PEM certificate from str
# @return a certificate object or a list of objects if multiple are in the string
def convert_pem_str_to_cert(certdata):
return x509.load_pem_x509_certificate(certdata.encode('utf8'), default_backend())
certs = re.findall(r'(-----BEGIN CERTIFICATE-----\n[^\-]+\n-----END CERTIFICATE-----)',
certdata, re.DOTALL)
result = list()
for data in certs:
result.append(x509.load_pem_x509_certificate(data.encode('utf8'), default_backend()))
return result[0] if len(result) == 1 else result
# @brief serialize cert/csr to DER bytes
@ -373,26 +389,19 @@ def target_is_current(target, file):
return target_date >= crt_date
# @brief convert domain list to idna representation (if applicable
def idna_convert(domainlist):
if any(ord(c) >= 128 for c in ''.join(domainlist)):
try:
domaintranslation = list()
for domain in domainlist:
if any(ord(c) >= 128 for c in domain):
# Translate IDNA domain name from a unicode domain (handle wildcards separately)
if domain.startswith('*.'):
idna_domain = "*.{}".format(domain[2:].encode('idna').decode('ascii'))
else:
idna_domain = domain.encode('idna').decode('ascii')
result = idna_domain, domain
else:
result = domain, domain
domaintranslation.append(result)
return domaintranslation
except Exception as e:
log("Unicode domain(s) found but IDNA names could not be translated due to error: {}".format(e), error=True)
return [(x, x) for x in domainlist]
# @brief convert domain to idna representation (if applicable
def idna_convert(domain):
try:
if any(ord(c) >= 128 for c in domain):
# Translate IDNA domain name from a unicode domain (handle wildcards separately)
if domain.startswith('*.'):
idna_domain = "*.{}".format(domain[2:].encode('idna').decode('ascii'))
else:
idna_domain = domain.encode('idna').decode('ascii')
return idna_domain
except Exception as e:
log("Unicode domain(s) found but IDNA names could not be translated due to error: {}".format(e), error=True)
return domain
# @brief validate the OCSP status for a given certificate by the given issuer
@ -411,6 +420,9 @@ def is_ocsp_valid(cert, issuer, hash_algo):
log("Invalid hash algorithm '{}' used for OCSP validation. Validation ignored.".format(hash_algo), warning=True)
return True
if isinstance(issuer, list):
issuer = issuer[0] # First certificate in the CA chain is the immediate issuer
try:
ocsp_urls = []
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
@ -420,7 +432,7 @@ def is_ocsp_valid(cert, issuer, hash_algo):
# This is a bit of a hack due to validation problems within cryptography (TODO: Check if this is still true)
# Correct replacement: ocsprequest = ocsp.OCSPRequestBuilder().add_certificate(cert, issuer, algorithm).build()
ocsprequest = ocsp.OCSPRequestBuilder((cert, issuer, algorithm)).build()
ocsprequest = ocsp.OCSPRequestBuilder((cert, issuer, (algorithm)())).build()
ocsprequestdata = ocsprequest.public_bytes(serialization.Encoding.DER)
for ocsp_url in ocsp_urls:
response = get_url(ocsp_url,

View File

@ -68,3 +68,12 @@ mail.example.com smtp.example.com webmail.example.net *.intra.example.com:
perm: '400'
format: crt,ca
action: '/etc/init.d/postfix reload'
# this will use a different authority for the following set of domains (buypass.com in this example)
buypass-example.com *.buypass-example.com:
- authority: 'https://api.buypass.com/acme' # Removed trailing /directory from buypass docs for API endpoint
mode: dns.nsupdate
nsupdate_keyname: buypass
nsupdate_keyvalue: Test1234512359==
nsupdate_keyalgorithm: HMAC-MD5.SIG-ALG.REG.INT

View File

@ -1 +1 @@
1.0.3
1.0.5