--- /dev/null
+ * mod_md:
+ - New directive `MDMatchNames all|servernames` to allow more control over how
+ MDomains are matched to VirtualHosts.
+ - New directive `MDChallengeDns01Version`. Setting this to `2` will provide
+ the command also with the challenge value on `teardown` invocation. In version
+ 1, the default, only the `setup` invocation gets this parameter.
+ Refs #312. Thanks to @domrim for the idea.
+ - For Managed Domain in "manual" mode, the checks if all used ServerName and
+ ServerAlias are part of the MDomain now reports a warning instead of an error
+ (AH10040) when not all names are present.
+ - MDChallengeDns01 can now be configured for individual domains.
+ Using PR from Jérôme Billiras (@bilhackmac) and adding test case and fixing proper working
+ - Fixed a bug found by Jérôme Billiras (@bilhackmac) that caused the challenge
+ teardown not being invoked as it should.
<p>
Define a program to be called when the `dns-01` challenge needs to be setup/torn down.
The program is given the argument `setup` or `teardown` followed by the domain name.
- For `setup` the challenge content is additionally given.
+ For `setup` the challenge content is additionally given. When
+ <directive module="mod_md">MDChallengeDns01Version</directive> is set to 2,
+ the `teardown` also gets the challenge content as argument.
</p><p>
You do not need to specify this, as long as a 'http:' or 'https:' challenge
method is possible. However, Let's Encrypt makes 'dns-01' the only
</usage>
</directivesynopsis>
+ <directivesynopsis>
+ <name>MDChallengeDns01Version</name>
+ <description></description>
+ <syntax>MDChallengeDns01Version 1|2</syntax>
+ <default>MDChallengeDns01Version 1</default>
+ <contextlist>
+ <context>server config</context>
+ </contextlist>
+ <compatibility>Available in version 2.4.58 and later</compatibility>
+ <usage>
+ <p>
+ Set the way MDChallengeDns01 command is invoked, e.g the number and
+ types of arguments. See <directive module="mod_md">MDChallengeDns01</directive>
+ for the differences.
+ This setting is global and cannot be varied per domain.
+ </p>
+ </usage>
+ </directivesynopsis>
+
+ <directivesynopsis>
+ <name>MDMatchNames</name>
+ <description></description>
+ <syntax>MDMatchNames all|servernames</syntax>
+ <default>MDMatchNames all</default>
+ <contextlist>
+ <context>server config</context>
+ </contextlist>
+ <compatibility>Available in version 2.4.58 and later</compatibility>
+ <usage>
+ <p>
+ The mode `all` is the behaviour as in all previous versions. Both ServerName
+ and ServerAlias are inspected to find the MDomain matching a VirtualHost.
+ This automatically detects coverage, even when you only have added
+ one of the names to an MDomain.
+ </p><p>
+ However, this auto-magic has drawbacks in more complex setups. If you set
+ this directive to `servernames`, only the ServerName of a virtual host is
+ used for matching. ServerAliases are disregarded then, for matching.
+ Aliases will still be added to the certificate obtained, unless you also
+ run `MDMembers manual`.
+ </p><p>
+ Another advantage of `servernames` is that it gives you more flexibility
+ with sub-domains and wildcards. You can define one MDomain with a wildcard
+ and have other MDomains for specific sub-domain names.
+ </p>
+ </usage>
+ </directivesynopsis>
+
</modulesynopsis>
struct apr_array_header_t *domains; /* all DNS names this MD includes */
struct apr_array_header_t *contacts; /* list of contact uris, e.g. mailto:xxx */
- int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */
- md_require_t require_https; /* Iff https: is required for this MD */
-
- int renew_mode; /* mode of obtaining credentials */
struct md_pkeys_spec_t *pks; /* specification for generating private keys */
- int must_staple; /* certificates should set the OCSP Must Staple extension */
md_timeslice_t *renew_window; /* time before expiration that starts renewal */
md_timeslice_t *warn_window; /* time before expiration that warnings are sent out */
const char *ca_eab_kid; /* optional KEYID for external account binding */
const char *ca_eab_hmac; /* optional HMAC for external account binding */
- md_state_t state; /* state of this MD */
const char *state_descr; /* description of state of NULL */
struct apr_array_header_t *acme_tls_1_domains; /* domains supporting "acme-tls/1" protocol */
- int stapling; /* if OCSP stapling is enabled */
const char *dns01_cmd; /* DNS challenge command, override global command */
- int watched; /* if certificate is supervised (renew or expiration warning) */
const struct md_srv_conf_t *sc; /* server config where it was defined or NULL */
const char *defn_name; /* config file this MD was defined */
unsigned defn_line_number; /* line number of definition */
-
const char *configured_name; /* name this MD was configured with, if different */
+
+ int renew_mode; /* mode of obtaining credentials */
+ md_require_t require_https; /* Iff https: is required for this MD */
+ md_state_t state; /* state of this MD */
+ int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */
+ int must_staple; /* certificates should set the OCSP Must Staple extension */
+ int stapling; /* if OCSP stapling is enabled */
+ int watched; /* if certificate is supervised (renew or expiration warning) */
};
#define MD_KEY_ACCOUNT "account"
#define MD_KEY_CHALLENGE "challenge"
#define MD_KEY_CHALLENGES "challenges"
#define MD_KEY_CMD_DNS01 "cmd-dns-01"
+#define MD_KEY_DNS01_VERSION "cmd-dns-01-version"
#define MD_KEY_COMPLETE "complete"
#define MD_KEY_CONTACT "contact"
#define MD_KEY_CONTACTS "contacts"
md_acme_t *acme, md_store_t *store,
md_pkeys_spec_t *key_specs,
apr_array_header_t *acme_tls_1_domains, const md_t *md,
- apr_table_t *env, md_result_t *result, apr_pool_t *p)
+ apr_table_t *env, md_result_t *result,
+ const char **psetup_token, apr_pool_t *p)
{
const char *data;
apr_status_t rv;
rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx);
}
out:
+ *psetup_token = (APR_SUCCESS == rv)?
+ apr_psprintf(p, "%s:%s", MD_AUTHZ_TYPE_HTTP01, authz->domain) : NULL;
return rv;
}
md_acme_t *acme, md_store_t *store,
md_pkeys_spec_t *key_specs,
apr_array_header_t *acme_tls_1_domains, const md_t *md,
- apr_table_t *env, md_result_t *result, apr_pool_t *p)
+ apr_table_t *env, md_result_t *result,
+ const char **psetup_token, apr_pool_t *p)
{
const char *acme_id, *token;
apr_status_t rv;
rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx);
}
out:
+ *psetup_token = (APR_SUCCESS == rv)?
+ apr_psprintf(p, "%s:%s", MD_AUTHZ_TYPE_TLSALPN01, authz->domain) : NULL;
return rv;
}
md_acme_t *acme, md_store_t *store,
md_pkeys_spec_t *key_specs,
apr_array_header_t *acme_tls_1_domains, const md_t *md,
- apr_table_t *env, md_result_t *result, apr_pool_t *p)
+ apr_table_t *env, md_result_t *result,
+ const char **psetup_token, apr_pool_t *p)
{
const char *token;
const char * const *argv;
rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx);
out:
+ *psetup_token = (APR_SUCCESS == rv)?
+ apr_psprintf(p, "%s:%s %s", MD_AUTHZ_TYPE_DNS01, authz->domain, token) : NULL;
return rv;
}
apr_table_t *env, apr_pool_t *p)
{
const char * const *argv;
- const char *cmdline, *dns01_cmd;
+ const char *cmdline, *dns01_cmd, *dns01v;
+ char *tmp, *s;
apr_status_t rv;
int exit_code;
md->name, domain);
goto out;
}
-
+ dns01v = apr_table_get(env, MD_KEY_DNS01_VERSION);
+ if (!dns01v || strcmp(dns01v, "2")) {
+ /* use older version of teardown args with only domain, remove token */
+ tmp = apr_pstrdup(p, domain);
+ s = strchr(tmp, ' ');
+ if (s) {
+ *s = '\0';
+ domain = tmp;
+ }
+ }
+
cmdline = apr_psprintf(p, "%s teardown %s", dns01_cmd, domain);
apr_tokenize_to_argv(cmdline, (char***)&argv, p);
if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, NULL, &exit_code)) || exit_code) {
md_acme_t *acme, md_store_t *store,
md_pkeys_spec_t *key_specs,
apr_array_header_t *acme_tls_1_domains, const md_t *md,
- apr_table_t *env, md_result_t *result, apr_pool_t *p);
+ apr_table_t *env, md_result_t *result,
+ const char **psetup_token, apr_pool_t *p);
typedef apr_status_t cha_teardown(md_store_t *store, const char *domain, const md_t *md,
apr_table_t *env, apr_pool_t *p);
apr_status_t rv;
int i, j;
cha_find_ctx fctx;
- const char *challenge_setup;
-
+
assert(acme);
assert(authz);
assert(authz->resource);
"type, this domain supports %s",
authz->domain, apr_array_pstrcat(p, challenges, ' '));
rv = APR_ENOTIMPL;
- challenge_setup = NULL;
+ *psetup_token = NULL;
for (i = 0; i < challenges->nelts; ++i) {
fctx.type = APR_ARRAY_IDX(challenges, i, const char *);
fctx.accepted = NULL;
md_result_activity_printf(result, "Setting up challenge '%s' for domain %s",
fctx.accepted->type, authz->domain);
rv = CHA_TYPES[j].setup(fctx.accepted, authz, acme, store, key_specs,
- acme_tls_1_domains, md, env, result, p);
+ acme_tls_1_domains, md, env, result,
+ psetup_token, p);
if (APR_SUCCESS == rv) {
md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
"%s: set up challenge '%s' for %s",
authz->domain, fctx.accepted->type, md->name);
- challenge_setup = CHA_TYPES[j].name;
goto out;
}
md_result_printf(result, rv, "error setting up challenge '%s' for %s, "
}
out:
- *psetup_token = (APR_SUCCESS == rv)? apr_psprintf(p, "%s:%s", challenge_setup, authz->domain) : NULL;
if (!fctx.accepted || APR_ENOTIMPL == rv) {
rv = APR_EINVAL;
fctx.offered = apr_array_make(p, 5, sizeof(const char*));
const char *method, const char *url,
struct apr_table_t *headers)
{
- md_http_request_t *req = NULL;
+ md_http_request_t *req;
apr_pool_t *pool;
apr_status_t rv;
return 0;
}
+int md_is_wild_match(const apr_array_header_t *domains, const char *name)
+{
+ const char *domain;
+ int i;
+
+ for (i = 0; i < domains->nelts; ++i) {
+ domain = APR_ARRAY_IDX(domains, i, const char*);
+ if (md_dns_matches(domain, name))
+ return (domain[0] == '*' && domain[1] == '.');
+ }
+ return 0;
+}
+
const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme)
{
const char *cp = s;
*/
int md_dns_domains_match(const apr_array_header_t *domains, const char *name);
+/**
+ * @return != 0 iff `name` is matched by a wildcard pattern in `domains`
+ */
+int md_is_wild_match(const apr_array_header_t *domains, const char *name);
+
/**************************************************************************************************/
/* file system related */
* @macro
* Version number of the md module as c string
*/
-#define MOD_MD_VERSION "2.4.21"
+#define MOD_MD_VERSION "2.4.23"
/**
* @macro
* release. This is a 24 bit number with 8 bits for major number, 8 bits
* for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
*/
-#define MOD_MD_VERSION_NUM 0x020415
+#define MOD_MD_VERSION_NUM 0x020417
#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory"
#define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock"
return APR_SUCCESS;
}
else {
- ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10040)
+ ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10040)
"Virtual Host %s:%d matches Managed Domain '%s', but the "
"name/alias %s itself is not managed. A requested MD certificate "
"will not match ServerName.",
s->server_hostname, s->port, md->name, domain);
- return APR_EINVAL;
+ return APR_SUCCESS;
}
}
for (i = 0; i < md->domains->nelts; ++i) {
domain = APR_ARRAY_IDX(md->domains, i, const char*);
- if (ap_matches_request_vhost(&r, domain, s->port)
- || (md_dns_is_wildcard(p, domain) && md_dns_matches(domain, s->server_hostname))) {
+ if ((mc->match_mode == MD_MATCH_ALL &&
+ ap_matches_request_vhost(&r, domain, s->port))
+ || (((mc->match_mode == MD_MATCH_SERVERNAMES) || md_dns_is_wildcard(p, domain)) &&
+ md_dns_matches(domain, s->server_hostname))) {
/* Create a unique md_srv_conf_t record for this server, if there is none yet */
sc = md_config_get_unique(s, p);
if (!sc->assigned) sc->assigned = apr_array_make(p, 2, sizeof(md_t*));
-
+ if (sc->assigned->nelts == 1 && mc->match_mode == MD_MATCH_SERVERNAMES) {
+ /* there is already an MD assigned for this server. But in
+ * this match mode, wildcard matches are pre-empted by non-wildcards */
+ int existing_wild = md_is_wild_match(
+ APR_ARRAY_IDX(sc->assigned, 0, const md_t*)->domains,
+ s->server_hostname);
+ if (!existing_wild && md_dns_is_wildcard(p, domain))
+ continue; /* do not add */
+ if (existing_wild && !md_dns_is_wildcard(p, domain))
+ sc->assigned->nelts = 0; /* overwrite existing */
+ }
APR_ARRAY_PUSH(sc->assigned, md_t*) = md;
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10041)
- "Server %s:%d matches md %s (config %s) for domain %s, "
- "has now %d MDs",
+ "Server %s:%d matches md %s (config %s, match-mode=%d) "
+ "for domain %s, has now %d MDs",
s->server_hostname, s->port, md->name, sc->name,
- domain, (int)sc->assigned->nelts);
+ mc->match_mode, domain, (int)sc->assigned->nelts);
if (md->contacts && md->contacts->nelts > 0) {
/* set explicitly */
md = APR_ARRAY_IDX(mc->mds, i, md_t*);
merge_srv_config(md, base_conf, p);
- /* Check that we have no overlap with the MDs already completed */
- for (j = 0; j < i; ++j) {
- omd = APR_ARRAY_IDX(mc->mds, j, md_t*);
- if ((domain = md_common_name(md, omd)) != NULL) {
- ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10038)
- "two Managed Domains have an overlap in domain '%s'"
- ", first definition in %s(line %d), second in %s(line %d)",
- domain, md->defn_name, md->defn_line_number,
- omd->defn_name, omd->defn_line_number);
- return APR_EINVAL;
- }
+ if (mc->match_mode == MD_MATCH_ALL) {
+ /* Check that we have no overlap with the MDs already completed */
+ for (j = 0; j < i; ++j) {
+ omd = APR_ARRAY_IDX(mc->mds, j, md_t*);
+ if ((domain = md_common_name(md, omd)) != NULL) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10038)
+ "two Managed Domains have an overlap in domain '%s'"
+ ", first definition in %s(line %d), second in %s(line %d)",
+ domain, md->defn_name, md->defn_line_number,
+ omd->defn_name, omd->defn_line_number);
+ return APR_EINVAL;
+ }
+ }
}
if (md->cert_files && md->cert_files->nelts) {
13, /* retry_failover after 14 errors, with 5s delay ~ half a day */
0, /* store locks, disabled by default */
apr_time_from_sec(5), /* max time to wait to obaint a store lock */
+ MD_MATCH_ALL, /* match vhost severname and aliases */
};
static md_timeslice_t def_renew_window = {
return NULL;
}
+static const char *md_config_set_match_mode(cmd_parms *cmd, void *dc, const char *s)
+{
+ md_srv_conf_t *config = md_config_get(cmd->server);
+ const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD);
+
+ (void)dc;
+ if (err) {
+ return err;
+ }
+ else if (!apr_strnatcasecmp("all", s)) {
+ config->mc->match_mode = MD_MATCH_ALL;
+ }
+ else if (!apr_strnatcasecmp("servernames", s)) {
+ config->mc->match_mode = MD_MATCH_SERVERNAMES;
+ }
+ else {
+ return "invalid argument, must be a 'all' or 'servernames'";
+ }
+ return NULL;
+}
+
static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value)
{
md_srv_conf_t *config = md_config_get(cmd->server);
return NULL;
}
+static const char *md_config_set_dns01_version(cmd_parms *cmd, void *mconfig, const char *value)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ (void)mconfig;
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+ if (!strcmp("1", value) || !strcmp("2", value)) {
+ apr_table_set(sc->mc->env, MD_KEY_DNS01_VERSION, value);
+ }
+ else {
+ return "Only versions `1` and `2` are supported";
+ }
+ return NULL;
+}
+
static const char *md_config_add_cert_file(cmd_parms *cmd, void *mconfig, const char *arg)
{
md_srv_conf_t *sc = md_config_get(cmd->server);
"Allow managing of base server outside virtual hosts."),
AP_INIT_RAW_ARGS("MDChallengeDns01", md_config_set_dns01_cmd, NULL, RSRC_CONF,
"Set the command for setup/teardown of dns-01 challenges"),
- AP_INIT_TAKE1("MDCertificateFile", md_config_add_cert_file, NULL, RSRC_CONF,
+ AP_INIT_TAKE1("MDChallengeDns01Version", md_config_set_dns01_version, NULL, RSRC_CONF,
+ "Set the type of arguments to call `MDChallengeDns01` with"),
+ AP_INIT_TAKE1("MDCertificateFile", md_config_add_cert_file, NULL, RSRC_CONF,
"set the static certificate (chain) file to use for this domain."),
AP_INIT_TAKE1("MDCertificateKeyFile", md_config_add_key_file, NULL, RSRC_CONF,
"set the static private key file to use for this domain."),
"The number of errors before a failover to another CA is triggered."),
AP_INIT_TAKE1("MDStoreLocks", md_config_set_store_locks, NULL, RSRC_CONF,
"Configure locking of store for updates."),
+ AP_INIT_TAKE1("MDMatchNames", md_config_set_match_mode, NULL, RSRC_CONF,
+ "Determines how DNS names are matched to vhosts."),
AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
};
MD_CONFIG_STAPLE_OTHERS,
} md_config_var_t;
+typedef enum {
+ MD_MATCH_ALL,
+ MD_MATCH_SERVERNAMES,
+} md_match_mode_t;
+
typedef struct md_mod_conf_t md_mod_conf_t;
struct md_mod_conf_t {
apr_array_header_t *mds; /* all md_t* defined in the config, shared */
int retry_failover; /* number of errors to trigger CA failover */
int use_store_locks; /* use locks when updating store */
apr_time_t lock_wait_timeout; /* fail after this time when unable to obtain lock */
+ md_match_mode_t match_mode; /* how dns names are match to vhosts */
};
typedef struct md_srv_conf_t {
--- /dev/null
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+curl = "curl"
+challtestsrv = "localhost:8055"
+
+
+def run(args):
+ sys.stderr.write(f"run: {' '.join(args)}\n")
+ p = subprocess.Popen(args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ output, errput = p.communicate(None)
+ rv = p.wait()
+ if rv != 0:
+ sys.stderr.write(errput.decode())
+ sys.stdout.write(output.decode())
+ return rv
+
+
+def teardown(domain):
+ rv = run([curl, '-s', '-d', f'{{"host":"_acme-challenge.{domain}"}}',
+ f'{challtestsrv}/clear-txt'])
+ if rv == 0:
+ rv = run([curl, '-s', '-d', f'{{"host":"{domain}"}}',
+ f'{challtestsrv}/set-txt'])
+ return rv
+
+
+def setup(domain, challenge):
+ teardown(domain)
+ rv = run([curl, '-s', '-d', f'{{"host":"{domain}", "addresses":["127.0.0.1"]}}',
+ f'{challtestsrv}/set-txt'])
+ if rv == 0:
+ rv = run([curl, '-s', '-d', f'{{"host":"_acme-challenge.{domain}.", "value":"{challenge}"}}',
+ f'{challtestsrv}/set-txt'])
+ return rv
+
+
+def main(argv):
+ if len(argv) > 1:
+ if argv[1] == 'setup':
+ if len(argv) != 4:
+ sys.stderr.write("wrong number of arguments: dns01.py setup <domain> <challenge>\n")
+ sys.exit(2)
+ rv = setup(argv[2], argv[3])
+ elif argv[1] == 'teardown':
+ if len(argv) != 4:
+ sys.stderr.write("wrong number of arguments: dns01.py teardown <domain> <challenge>\n")
+ sys.exit(1)
+ rv = teardown(argv[2])
+ else:
+ sys.stderr.write(f"unknown option {argv[1]}\n")
+ rv = 2
+ else:
+ sys.stderr.write("dns01.py wrong number of arguments\n")
+ rv = 2
+ sys.exit(rv)
+
+
+if __name__ == "__main__":
+ main(sys.argv)
assert len(md['ca']['urls']) == len(cas)
else:
assert rv != 0, "Server should not have accepted CAs '{}'".format(cas)
+
+ # messy ServerAliases, see #301
+ def test_md_300_028(self, env):
+ assert env.apache_stop() == 0
+ conf = MDConf(env)
+ domaina = f"t300_028a.{env.http_tld}"
+ domainb = f"t300_028b.{env.http_tld}"
+ dalias = f"t300_028alias.{env.http_tld}"
+ conf.add_vhost(port=env.http_port, domains=[domaina, domainb, dalias], with_ssl=False)
+ conf.add(f"""
+ MDomain {domaina}
+ MDomain {domainb} {dalias}
+ """)
+ conf.add(f"""
+ <VirtualHost 10.0.0.1:{env.https_port}>
+ ServerName {domaina}
+ ServerAlias {dalias}
+ SSLEngine on
+ </VirtualHost>
+ <VirtualHost 10.0.0.1:{env.https_port}>
+ ServerName {domainb}
+ ServerAlias {dalias}
+ SSLEngine on
+ </VirtualHost>
+ """)
+ conf.install()
+ # This does not work as we have both MDs match domaina's vhost
+ assert env.apache_fail() == 0
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10238" # 2 MDs match the same vhost
+ ]
+ )
+ # It works, if we only match on ServerNames
+ conf.add("MDMatchNames servernames")
+ conf.install()
+ assert env.apache_restart() == 0
+
+ # wildcard and specfic MD overlaps
+ def test_md_300_029(self, env):
+ assert env.apache_stop() == 0
+ conf = MDConf(env)
+ domain = f"t300_029.{env.http_tld}"
+ subdomain = f"sub.{domain}"
+ conf.add_vhost(port=env.http_port, domains=[domain, subdomain], with_ssl=False)
+ conf.add(f"""
+ MDMembers manual
+ MDomain {domain} *.{domain}
+ MDomain {subdomain}
+ """)
+ conf.add(f"""
+ <VirtualHost 10.0.0.1:{env.https_port}>
+ ServerName {domain}
+ SSLEngine on
+ </VirtualHost>
+ <VirtualHost 10.0.0.1:{env.https_port}>
+ ServerName another.{domain}
+ SSLEngine on
+ </VirtualHost>
+ <VirtualHost 10.0.0.1:{env.https_port}>
+ ServerName {subdomain}
+ SSLEngine on
+ </VirtualHost>
+ """)
+ conf.install()
+ # This does not work as we have overlapping names in MDs
+ assert env.apache_fail() == 0
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10038" # 2 MDs overlap
+ ]
+ )
+ # It works, if we only match on ServerNames
+ conf.add("MDMatchNames servernames")
+ conf.install()
+ assert env.apache_restart() == 0
+
assert env.apache_restart() == 0
for i in range(0, len(dns_lists)):
env.check_md(dns_lists[i], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: add managed domains as separate steps
def test_md_310_101(self, env):
assert env.apache_restart() == 0
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
env.check_md(["testdomain2.org", "www.testdomain2.org", "mail.testdomain2.org"], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: add dns to existing md
def test_md_310_102(self, env):
""").install()
assert env.apache_restart() == 0
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: add new md definition with acme url, acme protocol, acme agreement
def test_md_310_103(self, env):
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
ca="http://acme.test.org:4000/directory", protocol="ACME",
agreement="http://acme.test.org:4000/terms/v1")
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: add to existing md: acme url, acme protocol
def test_md_310_104(self, env):
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
ca="http://acme.test.org:4000/directory", protocol="ACME",
agreement="http://acme.test.org:4000/terms/v1")
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: add new md definition with server admin
def test_md_310_105(self, env):
name = "testdomain.org"
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
contacts=["mailto:admin@testdomain.org"])
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: add to existing md: server admin
def test_md_310_106(self, env):
assert env.apache_restart() == 0
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
contacts=["mailto:admin@testdomain.org"])
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: assign separate contact info based on VirtualHost
def test_md_310_107(self, env):
""").install()
assert env.apache_restart() == 0
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: default drive mode - auto
def test_md_310_109(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: drive mode manual
def test_md_310_110(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 0
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: drive mode auto
def test_md_310_111(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: drive mode always
def test_md_310_112(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-window'] == '14d'
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: renew window - 10 percent
def test_md_310_113b(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-window'] == '10%'
-
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
+
# test case: ca challenge type - http-01
def test_md_310_114(self, env):
MDConf(env, text="""
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01']
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: ca challenge type - http-01
def test_md_310_115(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['tls-alpn-01']
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: ca challenge type - all
def test_md_310_116(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01', 'tls-alpn-01']
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: automatically collect md names from vhost config
def test_md_310_117(self, env):
assert env.apache_restart() == 0
stat = env.get_md_status("testdomain.org")
assert stat['renew-window'] == '14d'
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: set RSA key length 2048
def test_md_310_119(self, env):
"type": "RSA",
"bits": 2048
}
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: set RSA key length 4096
def test_md_310_120(self, env):
"type": "RSA",
"bits": 4096
}
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: require HTTPS
def test_md_310_121(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['require-https'] == "temporary"
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045", # No VirtualHost matches Managed Domain
+ "AH10105" # no domain match
+ ]
+ )
# test case: require OCSP stapling
def test_md_310_122(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['must-staple'] is True
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove managed domain from config
def test_md_310_200(self, env):
assert env.apache_restart() == 0
# check: DNS has been removed from md in store
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove primary name from managed domain
def test_md_310_202(self, env):
# check: md overwrite previous name and changes name
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"],
md="testdomain.org", state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove one md, keep another
def test_md_310_203(self, env):
# all mds stay in store
env.check_md(dns_list1, state=1)
env.check_md(dns_list2, state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove ca info from md, should switch over to new defaults
def test_md_310_204(self, env):
assert env.apache_restart() == 0
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
ca="https://acme-v02.api.letsencrypt.org/directory", protocol="ACME")
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove server admin from md
def test_md_310_205(self, env):
# check: md stays the same with previous admin info
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
contacts=["mailto:admin@testdomain.org"])
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove renew window from conf -> fallback to default
def test_md_310_206(self, env):
assert env.apache_restart() == 0
# check: renew window not set
assert env.a2md(["list"]).json['output'][0]['renew-window'] == '33%'
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove drive mode from conf -> fallback to default (auto)
@pytest.mark.parametrize("renew_mode,exp_code", [
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: remove challenges from conf -> fallback to default (not set)
def test_md_310_208(self, env):
""").install()
assert env.apache_restart() == 0
assert 'challenges' not in env.a2md(["list"]).json['output'][0]['ca']
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: specify RSA key
@pytest.mark.parametrize("key_size", ["2048", "4096"])
""").install()
assert env.apache_restart() == 0
assert "privkey" not in env.a2md(["list"]).json['output'][0]
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: require HTTPS
@pytest.mark.parametrize("mode", ["temporary", "permanent"])
assert env.apache_restart() == 0
assert "require-https" not in env.a2md(["list"]).json['output'][0], \
"HTTPS require still persisted in store. config: {}".format(mode)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045", # No VirtualHost matches Managed Domain
+ "AH10105", # MDomain does not match any vhost
+ ]
+ )
# test case: require OCSP stapling
def test_md_310_211(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['must-staple'] is False
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: reorder DNS names in md definition
def test_md_310_300(self, env):
assert env.apache_restart() == 0
# check: dns list changes
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: move DNS from one md to another
def test_md_310_301(self, env):
assert env.apache_restart() == 0
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
env.check_md(["testdomain2.org", "www.testdomain2.org", "mail.testdomain2.org"], state=1)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change ca info
def test_md_310_302(self, env):
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
ca="http://somewhere.com:6666/directory", protocol="ACME",
agreement="http://somewhere.com:6666/terms/v1")
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change server admin
def test_md_310_303(self, env):
# check: md stays the same with previous admin info
env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
contacts=["mailto:webmaster@testdomain.org"])
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change drive mode - manual -> auto -> always
def test_md_310_304(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 2
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change config value for renew window, use various syntax alternatives
def test_md_310_305(self, env):
assert env.apache_restart() == 0
md = env.a2md(["list"]).json['output'][0]
assert md['renew-window'] == '10%'
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change challenge types - http -> tls-sni -> all
def test_md_310_306(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01', 'tls-alpn-01']
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: RSA key length: 4096 -> 2048 -> 4096
def test_md_310_307(self, env):
"type": "RSA",
"bits": 4096
}
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change HTTPS require settings on existing md
def test_md_310_308(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['require-https'] == "permanent"
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045", # No VirtualHost matches Managed Domain
+ "AH10105", # MDomain matches no vhost
+ ]
+ )
# test case: change OCSP stapling settings on existing md
def test_md_310_309(self, env):
""").install()
assert env.apache_restart() == 0
assert env.a2md(["list"]).json['output'][0]['must-staple'] is False
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: change renew window parameter
@pytest.mark.parametrize("window", [
env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
env.clear_store()
env.set_store_dir_default()
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test case: place an unexpected file into the store, check startup survival, see #218
def test_md_310_501(self, env):
md = env.a2md(["list", name]).json['output'][0]
assert md["renew"] == tc["renew"], \
"Expected renew == {} indicator in {}, test case {}".format(tc["renew"], md, tc)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
@pytest.mark.parametrize("key_type,key_params,exp_key_length", [
("RSA", [2048], 2048),
# check cert key length
cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
assert cert.get_key_length() == exp_key_length
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# test_502_203 removed, as ToS agreement is not really checked in ACMEv2
# check: SSL is running OK
cert = env.get_cert(domain)
assert domain in cert.get_san_list()
-
# check file system permissions:
env.check_file_permissions(domain)
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
def test_md_602_001(self, env):
# test case: same as test_600_000, but with two parallel managed domains
assert domains_a == cert_a.get_san_list()
cert_b = env.get_cert(domain_b)
assert domains_b == cert_b.get_san_list()
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
def test_md_602_002(self, env):
# test case: one md, that covers two vhosts
assert cert_a.same_serial_as(cert_b)
assert env.get_content(name_a, "/name.txt") == name_a
assert env.get_content(name_b, "/name.txt") == name_b
+ env.httpd_error_log.ignore_recent(
+ lognos = [
+ "AH10045" # No VirtualHost matches Managed Domain
+ ]
+ )
# --------- _utils_ ---------