diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index aae3a70eeb..51e74923c2 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -244,7 +244,7 @@ EOF sleep \$((RANDOM%3600)); yunohost tools update apps > /dev/null EOF - # Cron job that renew lets encrypt certificates if there's any that needs renewal + # Cron job that renew certificates from the CA if there's any that needs renewal cat > "$pending_dir/etc/cron.daily/yunohost-certificate-renew" << EOF #!/bin/bash yunohost domain cert renew --email diff --git a/locales/en.json b/locales/en.json index 0b5a0c1000..8024987cfc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -164,22 +164,23 @@ "cannot_open_file": "Could not open file {file} (reason: {error})", "cannot_write_file": "Could not write file {file} (reason: {error})", "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be run for {domain} right now because its nginx conf lacks the corresponding code snippet… Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonCA_cert": "The certificate for the domain '{domain}' is not issued by the certification authority. Cannot renew it automatically!", "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain}' is not issued by Let's Encrypt. Cannot renew it automatically!", "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain} (file: {file}), reason: {reason}", - "certmanager_cert_install_failed": "Let's Encrypt certificate install failed for {domains}", + "certmanager_cert_install_failed": "CA certificate install failed for {domains}", "certmanager_cert_install_failed_selfsigned": "Self-signed certificate install failed for {domains}", - "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain}'", + "certmanager_cert_install_success": "CA certificate now installed for the domain '{domain}'", "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain}'", - "certmanager_cert_renew_failed": "Let's Encrypt certificate renew failed for {domains}", - "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'", + "certmanager_cert_renew_failed": "CA certificate renew failed for {domains}", + "certmanager_cert_renew_success": "CA certificate renewed for the domain '{domain}'", "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work…", "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off these checks.)", "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off these checks.)", - "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for a CA certificate. (Or if you know what you are doing, use '--no-checks' to turn off these checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})", "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", @@ -369,7 +370,7 @@ "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.", "domain_cert_gen_failed": "Could not generate certificate", "domain_config_acme_eligible": "ACME eligibility", - "domain_config_acme_eligible_explain": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.", + "domain_config_acme_eligible_explain": "This domain doesn't seem ready for a certification authority delivered certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.", "domain_config_api_protocol": "API protocol", "domain_config_auth_application_key": "Application key", "domain_config_auth_application_secret": "Application secret key", @@ -378,14 +379,15 @@ "domain_config_auth_key": "Authentication key", "domain_config_auth_secret": "Authentication secret", "domain_config_auth_token": "Authentication token", - "domain_config_cert_install": "Install Let's Encrypt certificate", + "domain_config_cert_install": "Install a certificate", "domain_config_cert_issuer": "Certification authority", "domain_config_cert_name": "Certificate", "domain_config_cert_no_checks": "Ignore diagnosis checks", - "domain_config_cert_renew": "Renew Let's Encrypt certificate", + "domain_config_cert_renew": "Renew a certificate", "domain_config_cert_renew_help": "Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).", "domain_config_cert_summary": "Certificate status", "domain_config_cert_summary_abouttoexpire": "Current certificate is about to expire. It should soon be renewed automatically.", + "domain_config_cert_summary_certauth": "Great! You're using a valid certificate!", "domain_config_cert_summary_expired": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!", "domain_config_cert_summary_letsencrypt": "Great! You're using a valid Let's Encrypt certificate!", "domain_config_cert_summary_ok": "Okay, current certificate looks good!", @@ -490,6 +492,9 @@ "global_settings_setting_backup_compress_tar_archives": "Compress backups", "global_settings_setting_backup_compress_tar_archives_help": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", "global_settings_setting_backup_name": "Backup", + "global_settings_setting_certauth.certification_authority": "Identifier of the certification authority", + "global_settings_setting_certauth.certification_authority_fullname": "Full name of the certification authority", + "global_settings_setting_certauth.certification_authority_acme_url": "ACME URL of the certification authority", "global_settings_setting_dns_custom_resolvers_enabled": "Use custom DNS resolvers", "global_settings_setting_dns_custom_resolvers_enabled_help": "By default, YunoHost uses a list of trustworthy resolvers located in Europe. Advanced users may want to specify custom resolvers instead.", "global_settings_setting_dns_custom_resolvers_list": "Custom resolvers' addresses", @@ -613,6 +618,8 @@ "log_backup_create": "Create a backup archive", "log_backup_restore_app": "Restore '{}' from a backup archive", "log_backup_restore_system": "Restore system from a backup archive", + "log_certauth_cert_install": "Install a certificate from the certification authority on '{}' domain", + "log_certauth_cert_renew": "Renew '{}' certificate from the certification authority", "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'", "log_diagnosis_run": "Run diagnosis", "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 4664aa04f1..013497daa1 100755 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -576,7 +576,7 @@ domain: extra: pattern: *pattern_password --install-letsencrypt-cert: - help: If adding a subdomain of an already added domain, try to install a Let's Encrypt certificate + help: If adding a subdomain of an already added domain, try to install a certificate from the certification authority action: store_true ### domain_remove() @@ -812,7 +812,7 @@ domain: ### certificate_install() install: - action_help: Install Let's Encrypt certificates for given domains (all by default). + action_help: Install certificates from the certification authority for given domains (all by default). api: PUT /domains//cert arguments: domain_list: @@ -827,12 +827,12 @@ domain: help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) action: store_true --self-signed: - help: Install self-signed certificate instead of Let's Encrypt + help: Install self-signed certificate instead of issued by the certification authority action: store_true ### certificate_renew() renew: - action_help: Renew the Let's Encrypt certificates for given domains (all by default). + action_help: Renew the certification authority certificates for given domains (all by default). api: PUT /domains//cert/renew arguments: domain_list: diff --git a/share/config_domain.toml b/share/config_domain.toml index 3218c2af97..5f2f8e28d8 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -114,12 +114,12 @@ i18n = "domain_config" type = "button" icon = "star" style = "success" - visible = "cert_issuer != 'letsencrypt'" + visible = "cert_issuer != certification_authority" enabled = "acme_eligible || cert_no_checks" [cert.cert_.cert_renew] type = "button" icon = "refresh" style = "warning" - visible = "cert_issuer == 'letsencrypt'" + visible = "cert_issuer == certification_authority" enabled = "acme_eligible || cert_no_checks" diff --git a/share/config_global.toml b/share/config_global.toml index cbdb4f4c86..7035ab1855 100644 --- a/share/config_global.toml +++ b/share/config_global.toml @@ -105,6 +105,24 @@ i18n = "global_settings_setting" type = "boolean" default = false + [security.certauth] + visible = false + [security.certauth.certification_authority] + type = "string" + default = "letsencrypt" + + [security.certauth.certification_authority_fullname] + type = "string" + default = "Let's Encrypt" + + [security.certauth.certification_authority_acme_url] + type = "url" + default = "https://acme-v02.api.letsencrypt.org" + + [security.certauth.certification_authority_caa_value] + type = "string" + default = "letsencrypt.org" + [email] [email.pop3] [email.pop3.pop3_enabled] diff --git a/src/certificate.py b/src/certificate.py index b15be931f0..5dff511e18 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -33,6 +33,7 @@ from .log import OperationLogger from .regenconf import regen_conf from .service import _run_service_command +from .settings import settings_get from .utils.error import YunohostError, YunohostValidationError from .utils.file_utils import chmod, chown, read_file from .utils.network import get_public_ip @@ -46,12 +47,18 @@ else: logger = getLogger("yunohost.certmanager") +# For prod +PRODUCTION_CERTIFICATION_AUTHORITY = settings_get("security.certauth") +CERT_AUTH_NAME = PRODUCTION_CERTIFICATION_AUTHORITY['security.certauth.certification_authority']['value'] +CERT_AUTH_FULLNAME = PRODUCTION_CERTIFICATION_AUTHORITY['security.certauth.certification_authority_fullname']['value'] +CERT_AUTH_ACME_URL = PRODUCTION_CERTIFICATION_AUTHORITY['security.certauth.certification_authority_acme_url']['value'] + CERT_FOLDER = "/etc/yunohost/certs/" TMP_FOLDER = "/var/www/.well-known/acme-challenge-private/" WEBROOT_FOLDER = "/var/www/.well-known/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" -ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" +ACCOUNT_KEY_FILE = "/etc/yunohost/" + CERT_AUTH_NAME + "_account.pem" SSL_DIR = "/usr/share/yunohost/ssl" @@ -59,9 +66,6 @@ VALIDITY_LIMIT = 15 # days -# For prod -PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org" - # # Front-end stuff # # @@ -134,20 +138,20 @@ def certificate_install( self_signed: bool = False, ) -> None: """ - Install a Let's Encrypt certificate for given domains (all by default) + Install a certificate from the certification authority for given domains (all by default) Keyword argument: domain_list -- Domains on which to install certificates force -- Install even if current certificate is not self-signed no-check -- Disable some checks about the reachability of web server before attempting the install - self-signed -- Instal self-signed certificates instead of Let's Encrypt + self-signed -- Instal self-signed certificates instead of from the CA """ if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(domain_list, force, no_checks) + _certificate_install_certauth(domain_list, force, no_checks) def _certificate_install_selfsigned(domain_list, force=False): @@ -253,7 +257,7 @@ def _certificate_install_selfsigned(domain_list, force=False): ) -def _certificate_install_letsencrypt(domains, force=False, no_checks=False): +def _certificate_install_certauth(domains, force=False, no_checks=False): from .domain import _assert_domain_exists, domain_list if not os.path.exists(ACCOUNT_KEY_FILE): @@ -294,7 +298,7 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): logger.info("Now attempting install of certificate for domain %s!", domain) operation_logger = OperationLogger( - "letsencrypt_cert_install", + "certauth_cert_install", [("domain", domain)], args={"force": force, "no_checks": no_checks}, ) @@ -308,7 +312,7 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): operation_logger.error(msg) if no_checks: logger.error( - f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." + f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a {CERT_AUTH_FULLNAME} certificate on domain {domain}." ) failed_cert_install.append(domain) else: @@ -329,7 +333,7 @@ def certificate_renew( email: bool = False, ) -> None: """ - Renew Let's Encrypt certificate for given domains (all by default) + Renew certificate from the certification authority for given domains (all by default) Keyword argument: domains -- Domains for which to renew the certificates @@ -341,13 +345,13 @@ def certificate_renew( from .domain import _assert_domain_exists, domain_list - # If no domains given, consider all yunohost domains with Let's Encrypt - # certificates + # If no domains given, consider all yunohost domains with certificates + # from the certification authority if domains == []: for domain in domain_list()["domains"]: - # Does it have a Let's Encrypt cert? + # Does it have a cert from the CA? status = _get_status(domain) - if status["CA_type"] != "letsencrypt": + if status["CA_type"] != CERT_AUTH_NAME: continue # Does it expire soon? @@ -380,10 +384,10 @@ def certificate_renew( "certmanager_attempt_to_renew_valid_cert", domain=domain ) - # Does it have a Let's Encrypt cert? - if status["CA_type"] != "letsencrypt": + # Does it have a cert from the CA? + if status["CA_type"] != CERT_AUTH_NAME: raise YunohostValidationError( - "certmanager_attempt_to_renew_nonLE_cert", domain=domain + "certmanager_attempt_to_renew_nonCA_cert", domain=domain ) # Check ACME challenge configured for given domain @@ -408,7 +412,7 @@ def certificate_renew( logger.info("Now attempting renewing of certificate for domain %s !", domain) operation_logger = OperationLogger( - "letsencrypt_cert_renew", + "certauth_cert_renew", [("domain", domain)], args={ "force": force, @@ -428,7 +432,7 @@ def certificate_renew( traceback.print_exc(file=stack) msg = f"Certificate renewing for {domain} failed!" if no_checks: - msg += f"\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." + msg += f"\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a {CERT_AUTH_FULLNAME} certificate on domain {domain}." logger.error(msg) operation_logger.error(msg) logger.error(stack.getvalue()) @@ -537,7 +541,7 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): WEBROOT_FOLDER, log=logger, disable_check=no_checks, - CA=PRODUCTION_CERTIFICATION_AUTHORITY, + CA=CERT_AUTH_ACME_URL, ) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): @@ -557,7 +561,7 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): # Create corresponding directory date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") - new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-letsencrypt" + new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-{CERT_AUTH_NAME}" os.makedirs(new_cert_folder) @@ -598,7 +602,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Set the domain csr.get_subject().CN = domain - sanlist = [] + sanlist = [domain] hook_results = hook_callback("cert_alternate_names", env={"domain": domain}) for hook_name, results in hook_results.items(): # @@ -681,8 +685,8 @@ def _get_status(domain): # is actually a symlink to a dir ending with -selfsigned if os.path.realpath(os.path.join(CERT_FOLDER, domain)).endswith("-selfsigned"): CA_type = "selfsigned" - elif organization_name == "Let's Encrypt": - CA_type = "letsencrypt" + elif organization_name == CERT_AUTH_FULLNAME: + CA_type = CERT_AUTH_NAME else: CA_type = "other" @@ -698,11 +702,11 @@ def _get_status(domain): elif CA_type == "other": style = "success" summary = "ok" - elif CA_type == "letsencrypt": + elif CA_type == CERT_AUTH_NAME: style = "success" - summary = "letsencrypt" + summary = "certauth" else: - # shouldnt happen, because CA_type can be only selfsigned, letsencrypt, or other + # shouldnt happen, because CA_type can be only selfsigned, the CA name, or other style = "" summary = "wat" diff --git a/src/dns.py b/src/dns.py index cb5d6c4617..61ebe081d1 100644 --- a/src/dns.py +++ b/src/dns.py @@ -38,6 +38,7 @@ ) from .hook import hook_callback from .log import OperationLogger, is_unit_operation +from .settings import settings_get from .utils.dns import dig, is_special_use_tld, is_yunohost_dyndns_domain from .utils.error import YunohostError, YunohostValidationError from .utils.file_utils import mkdir, read_file, read_toml, write_to_file @@ -145,7 +146,7 @@ def _build_dns_conf( {"type": "A", "name": "*", "content": "123.123.123.123", "ttl": 3600}, # if ipv6 available {"type": "AAAA", "name": "*", "content": "valid-ipv6", "ttl": 3600}, - {"type": "CAA", "name": "@", "content": "0 issue \"letsencrypt.org\"", "ttl": 3600}, + {"type": "CAA", "name": "@", "content": "0 issue \"the-ca-identifier\"", "ttl": 3600}, ], "example_of_a_custom_rule": [ {"type": "SRV", "name": "_matrix", "content": "domain.tld.", "ttl": 3600} @@ -186,6 +187,7 @@ def _build_dns_conf( # foo.sub.domain.tld # sub.domain.tld # foo # .foo # basename = _get_relative_name_for_dns_zone(domain, base_dns_zone) suffix = f".{basename}" if basename != "@" else "" + caa_value = settings_get("security.certauth.certification_authority_caa_value") # ttl = settings["ttl"] ttl = 3600 @@ -235,7 +237,7 @@ def _build_dns_conf( elif include_empty_AAAA_if_no_ipv6: extra.append((f"*{suffix}", ttl, "AAAA", None)) # type: ignore[arg-type] - extra.append((basename, ttl, "CAA", '0 issue "letsencrypt.org"')) + extra.append((basename, ttl, "CAA", f"0 issue \"{caa_value}\"")) #################### # Standard records # diff --git a/src/domain.py b/src/domain.py index ad081dc703..0c79441b60 100644 --- a/src/domain.py +++ b/src/domain.py @@ -276,7 +276,7 @@ def domain_add( domain: str, dyndns_recovery_password=None, ignore_dyndns=False, - install_letsencrypt_cert=False, + install_certauth_cert=False, skip_tos=False, ): """ @@ -287,11 +287,11 @@ def domain_add( dyndns -- Subscribe to DynDNS dyndns_recovery_password -- Password used to later unsubscribe from DynDNS ignore_dyndns -- If we want to just add the DynDNS domain to the list, without subscribing - install_letsencrypt_cert -- If adding a subdomain of an already added domain, try to install a Let's Encrypt certificate + install_certauth_cert -- If adding a subdomain of an already added domain, try to install a certificate from the certification authority """ from .app import app_ssowatconf from .certificate import ( - _certificate_install_letsencrypt, + _certificate_install_certauth, _certificate_install_selfsigned, certificate_status, ) @@ -395,32 +395,32 @@ def domain_add( pass raise e - failed_letsencrypt_cert_install = False - if install_letsencrypt_cert: + failed_certauth_cert_install = False + if install_certauth_cert: parent_domain = _get_parent_domain_of(domain) - can_install_letsencrypt = ( + can_install_certauth = ( parent_domain and certificate_status([parent_domain], full=True)["certificates"][ parent_domain ]["has_wildcards"] ) - if can_install_letsencrypt: + if can_install_certauth: try: - _certificate_install_letsencrypt([domain], force=True, no_checks=True) + _certificate_install_certauth([domain], force=True, no_checks=True) except Exception: - failed_letsencrypt_cert_install = True + failed_certauth_cert_install = True else: logger.warning( - "Skipping Let's Encrypt certificate attempt because there's no wildcard configured on the parent domain's DNS records." + "Skipping certificate attempt from the CA because there's no wildcard configured on the parent domain's DNS records." ) - failed_letsencrypt_cert_install = True + failed_certauth_cert_install = True hook_callback("post_domain_add", args=[domain]) logger.success(m18n.n("domain_created")) - if failed_letsencrypt_cert_install: + if failed_certauth_cert_install: logger.warning(m18n.n("certmanager_cert_install_failed", domains=domain)) @@ -792,7 +792,7 @@ def _get_raw_config(self) -> "RawConfig": # i18n: domain_config_cert_summary_selfsigned # i18n: domain_config_cert_summary_abouttoexpire # i18n: domain_config_cert_summary_ok - # i18n: domain_config_cert_summary_letsencrypt + # i18n: domain_config_cert_summary_certauth raw_config["cert"]["cert_"]["cert_summary"]["ask"] = m18n.n( f"domain_config_cert_summary_{status['summary']}" )