mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
First draft of certificate management integration (e.g. Let's Encrypt certificate install)
This commit is contained in:
parent
2c0c53d06f
commit
35fa386ce3
10 changed files with 748 additions and 41 deletions
|
@ -305,8 +305,64 @@ domain:
|
|||
- !!str ^[0-9]+$
|
||||
- "pattern_positive_number"
|
||||
|
||||
### certificate_status()
|
||||
cert-status:
|
||||
action_help: List status of current certificates (all by default).
|
||||
api: GET /certs/status/<domains>
|
||||
configuration:
|
||||
authenticate: all
|
||||
authenticator: ldap-anonymous
|
||||
arguments:
|
||||
domainList:
|
||||
help: Domains to check
|
||||
nargs: "*"
|
||||
--full:
|
||||
help: Show more details
|
||||
action: store_true
|
||||
|
||||
### domain_info()
|
||||
### certificate_install()
|
||||
cert-install:
|
||||
action_help: Install Let's Encrypt certificates for given domains (all by default).
|
||||
api: POST /certs/enable/<domains>
|
||||
configuration:
|
||||
authenticate: all
|
||||
authenticator: ldap-anonymous
|
||||
arguments:
|
||||
domainList:
|
||||
help: Domains for which to install the certificates
|
||||
nargs: "*"
|
||||
--force:
|
||||
help: Install even if current certificate is not self-signed
|
||||
action: store_true
|
||||
--no-checks:
|
||||
help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to install. (Not recommended)
|
||||
action: store_true
|
||||
--self-signed:
|
||||
help: Install self-signed certificate instead of Let's Encrypt
|
||||
action: store_true
|
||||
|
||||
### certificate_renew()
|
||||
cert-renew:
|
||||
action_help: Renew the Let's Encrypt certificates for given domains (all by default).
|
||||
api: POST /certs/renew/<domains>
|
||||
configuration:
|
||||
authenticate: all
|
||||
authenticator: ldap-anonymous
|
||||
arguments:
|
||||
domainList:
|
||||
help: Domains for which to renew the certificates
|
||||
nargs: "*"
|
||||
--force:
|
||||
help: Ignore the validity treshold (30 days)
|
||||
action: store_true
|
||||
--email:
|
||||
help: Send an email to root with logs if some renewing fails
|
||||
action: store_true
|
||||
--no-checks:
|
||||
help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended)
|
||||
action: store_true
|
||||
|
||||
### domain_info()
|
||||
# info:
|
||||
# action_help: Get domain informations
|
||||
# api: GET /domains/<domain>
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
"custom_app_url_required": "Es muss eine URL angegeben um deine benutzerdefinierte App {app:s} zu aktualisieren",
|
||||
"custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben",
|
||||
"dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus",
|
||||
"domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden",
|
||||
"certmanager_domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden",
|
||||
"domain_created": "Domain erfolgreich erzeugt",
|
||||
"domain_creation_failed": "Konnte Domain nicht erzeugen",
|
||||
"domain_deleted": "Domain erfolgreich gelöscht",
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
"diagnostic_monitor_system_error": "Can't monitor system: {error}",
|
||||
"diagnostic_no_apps": "No installed application",
|
||||
"dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'",
|
||||
"domain_cert_gen_failed": "Unable to generate certificate",
|
||||
"domain_created": "The domain has been created",
|
||||
"domain_creation_failed": "Unable to create domain",
|
||||
"domain_deleted": "The domain has been deleted",
|
||||
|
@ -237,5 +236,20 @@
|
|||
"yunohost_ca_creation_failed": "Unable to create certificate authority",
|
||||
"yunohost_configured": "YunoHost has been configured",
|
||||
"yunohost_installing": "Installing YunoHost...",
|
||||
"yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'."
|
||||
"yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.",
|
||||
"certmanager_domain_cert_gen_failed": "Unable to generate certificate",
|
||||
"certmanager_attempt_to_replace_valid_cert" : "You are attempting to overwrite a good and valid certificate for domain {domain:s} ! (Use --force to bypass)",
|
||||
"certmanager_domain_unknown": "Unknown domain {domain:s}",
|
||||
"certmanager_domain_cert_not_selfsigned" : "The certificate of domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)",
|
||||
"certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...",
|
||||
"certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !",
|
||||
"certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass",
|
||||
"certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.",
|
||||
"certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}). Use --no-checks to disable checks.",
|
||||
"certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.",
|
||||
"certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.",
|
||||
"certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !",
|
||||
"certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s})",
|
||||
"certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !",
|
||||
"certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !"
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
"diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}",
|
||||
"diagnostic_no_apps": "Aplicación no instalada",
|
||||
"dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'",
|
||||
"domain_cert_gen_failed": "No se pudo crear el certificado",
|
||||
"certmanager_domain_cert_gen_failed": "No se pudo crear el certificado",
|
||||
"domain_created": "El dominio ha sido creado",
|
||||
"domain_creation_failed": "No se pudo crear el dominio",
|
||||
"domain_deleted": "El dominio ha sido eliminado",
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
"diagnostic_monitor_system_error": "Impossible de superviser le système : {error}",
|
||||
"diagnostic_no_apps": "Aucune application installée",
|
||||
"dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer « apt-get remove bind9 && apt-get install dnsmasq »",
|
||||
"domain_cert_gen_failed": "Impossible de générer le certificat",
|
||||
"certmanager_domain_cert_gen_failed": "Impossible de générer le certificat",
|
||||
"domain_created": "Le domaine a été créé",
|
||||
"domain_creation_failed": "Impossible de créer le domaine",
|
||||
"domain_deleted": "Le domaine a été supprimé",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken",
|
||||
"custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst",
|
||||
"dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'",
|
||||
"domain_cert_gen_failed": "Kan certificaat niet genereren",
|
||||
"certmanager_domain_cert_gen_failed": "Kan certificaat niet genereren",
|
||||
"domain_created": "Domein succesvol aangemaakt",
|
||||
"domain_creation_failed": "Kan domein niet aanmaken",
|
||||
"domain_deleted": "Domein succesvol verwijderd",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"backup_output_directory_not_empty": "A pasta de destino não se encontra vazia",
|
||||
"custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {app:s}",
|
||||
"custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada",
|
||||
"domain_cert_gen_failed": "Não foi possível gerar o certificado",
|
||||
"certmanager_domain_cert_gen_failed": "Não foi possível gerar o certificado",
|
||||
"domain_created": "Domínio criado com êxito",
|
||||
"domain_creation_failed": "Não foi possível criar o domínio",
|
||||
"domain_deleted": "Domínio removido com êxito",
|
||||
|
|
|
@ -1031,6 +1031,22 @@ def app_ssowatconf(auth):
|
|||
for domain in domains:
|
||||
skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api'])
|
||||
|
||||
# Authorize ACME challenge url if a domain seems configured for it...
|
||||
for domain in domains:
|
||||
|
||||
# Check ACME challenge file is present in nginx conf
|
||||
nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf"
|
||||
if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue
|
||||
|
||||
# Check the file contains the ACME challenge uri
|
||||
acme_uri = '/.well-known/acme-challenge'
|
||||
if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue
|
||||
|
||||
# If so, then authorize the ACME challenge uri to unprotected regex
|
||||
regex = domain+"/%.well%-known/acme%-challenge/.*$"
|
||||
unprotected_regex.append(regex)
|
||||
|
||||
|
||||
conf_dict = {
|
||||
'portal_domain': main_domain,
|
||||
'portal_path': '/yunohost/sso/',
|
||||
|
|
641
src/yunohost/certificate.py
Normal file
641
src/yunohost/certificate.py
Normal file
|
@ -0,0 +1,641 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" License
|
||||
|
||||
Copyright (C) 2016 YUNOHOST.ORG
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program; if not, see http://www.gnu.org/licenses
|
||||
|
||||
"""
|
||||
|
||||
""" yunohost_certificate.py
|
||||
|
||||
Manage certificates, in particular Let's encrypt
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
import requests
|
||||
import shutil
|
||||
import pwd
|
||||
import grp
|
||||
import json
|
||||
import smtplib
|
||||
|
||||
from OpenSSL import crypto
|
||||
from datetime import datetime
|
||||
from tabulate import tabulate
|
||||
from acme_tiny import get_crt as sign_certificate
|
||||
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
import yunohost.domain
|
||||
from yunohost.service import _run_service_command
|
||||
from yunohost.app import app_setting, app_ssowatconf
|
||||
|
||||
|
||||
logger = getActionLogger('yunohost.certmanager')
|
||||
|
||||
# Misc stuff we need
|
||||
|
||||
cert_folder = "/etc/yunohost/certs/"
|
||||
tmp_folder = "/tmp/acme-challenge-private/"
|
||||
webroot_folder = "/tmp/acme-challenge-public/"
|
||||
|
||||
selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem"
|
||||
account_key_file = "/etc/yunohost/letsencrypt_account.pem"
|
||||
|
||||
key_size = 2048
|
||||
|
||||
validity_limit = 15 # days
|
||||
|
||||
# For tests
|
||||
#certification_authority = "https://acme-staging.api.letsencrypt.org"
|
||||
# For prod
|
||||
certification_authority = "https://acme-v01.api.letsencrypt.org"
|
||||
|
||||
intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem"
|
||||
|
||||
###############################################################################
|
||||
# Front-end stuff #
|
||||
###############################################################################
|
||||
|
||||
# Status
|
||||
|
||||
def certificate_status(auth, domainList, full = False):
|
||||
"""
|
||||
Print the status of certificate for given domains (all by default)
|
||||
|
||||
Keyword argument:
|
||||
domainList -- Domains to be checked
|
||||
full -- Display more info about the certificates
|
||||
"""
|
||||
|
||||
# If no domains given, consider all yunohost domains
|
||||
if (domainList == []) : domainList = yunohost.domain.domain_list(auth)['domains']
|
||||
# Else, validate that yunohost knows the domains given
|
||||
else :
|
||||
for domain in domainList :
|
||||
# Is it in Yunohost dmomain list ?
|
||||
if domain not in yunohost.domain.domain_list(auth)['domains']:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain))
|
||||
|
||||
# Get status for each domain, and prepare display using tabulate
|
||||
if not full :
|
||||
headers = [ "Domain", "Certificate status", "Authority type", "Days remaining"]
|
||||
else :
|
||||
headers = [ "Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"]
|
||||
lines = []
|
||||
for domain in domainList :
|
||||
status = _get_status(domain)
|
||||
|
||||
line = []
|
||||
line.append(domain)
|
||||
if (full) : line.append(status["subject"])
|
||||
line.append(_summary_code_to_string(status["summaryCode"]))
|
||||
line.append(status["CAtype"])
|
||||
if (full) : line.append(status["CAname"])
|
||||
line.append(status["validity"])
|
||||
lines.append(line)
|
||||
|
||||
print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center"))
|
||||
|
||||
|
||||
def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False) :
|
||||
"""
|
||||
Install a Let's Encrypt certificate for given domains (all by default)
|
||||
|
||||
Keyword argument:
|
||||
domainList -- 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
|
||||
"""
|
||||
if (self_signed) :
|
||||
certificate_install_selfsigned(domainList, force)
|
||||
else :
|
||||
certificate_install_letsencrypt(auth, domainList, force, no_checks)
|
||||
|
||||
|
||||
# Install self-signed
|
||||
|
||||
def certificate_install_selfsigned(domainList, force=False) :
|
||||
|
||||
for domain in domainList :
|
||||
|
||||
# Check we ain't trying to overwrite a good cert !
|
||||
status = _get_status(domain)
|
||||
if (status != {}) and (status["summaryCode"] > 0) and (not force) :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain))
|
||||
|
||||
|
||||
cert_folder_domain = cert_folder+"/"+domain
|
||||
|
||||
# Create cert folder if it does not exists yet
|
||||
try:
|
||||
os.listdir(cert_folder_domain)
|
||||
except OSError:
|
||||
os.makedirs(cert_folder_domain)
|
||||
|
||||
# Get serial
|
||||
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
||||
with open('%s/serial' % ssl_dir, 'r') as f:
|
||||
serial = f.readline().rstrip()
|
||||
|
||||
# FIXME : should refactor this to avoid so many os.system() calls...
|
||||
# We should be able to do all this using OpenSSL.crypto and os/shutil
|
||||
command_list = [
|
||||
'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain),
|
||||
'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain),
|
||||
'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch'
|
||||
% (cert_folder_domain, ssl_dir, ssl_dir),
|
||||
'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch'
|
||||
% (cert_folder_domain, ssl_dir, ssl_dir),
|
||||
'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain,
|
||||
'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain),
|
||||
'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain),
|
||||
'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain)
|
||||
]
|
||||
|
||||
for command in command_list:
|
||||
if os.system(command) != 0:
|
||||
raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed'))
|
||||
|
||||
_set_permissions(cert_folder_domain, "root", "root", 0755);
|
||||
_set_permissions(cert_folder_domain+"/key.pem", "root", "metronome", 0640);
|
||||
_set_permissions(cert_folder_domain+"/crt.pem", "root", "metronome", 0640);
|
||||
_set_permissions(cert_folder_domain+"/openssl.cnf", "root", "root", 0600);
|
||||
|
||||
|
||||
|
||||
# Install ACME / Let's Encrypt certificate
|
||||
|
||||
def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False):
|
||||
|
||||
|
||||
if not os.path.exists(account_key_file) :
|
||||
_generate_account_key()
|
||||
|
||||
# If no domains given, consider all yunohost domains with self-signed
|
||||
# certificates
|
||||
if (domainList == []) :
|
||||
for domain in yunohost.domain.domain_list(auth)['domains'] :
|
||||
|
||||
# Is it self-signed ?
|
||||
status = _get_status(domain)
|
||||
if (status["CAtype"] != "Self-signed") : continue
|
||||
|
||||
domainList.append(domain)
|
||||
|
||||
# Else, validate that yunohost knows the domains given
|
||||
else :
|
||||
for domain in domainList :
|
||||
# Is it in Yunohost dmomain list ?
|
||||
if domain not in yunohost.domain.domain_list(auth)['domains']:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain))
|
||||
|
||||
# Is it self-signed ?
|
||||
status = _get_status(domain)
|
||||
if (not force) and (status["CAtype"] != "Self-signed") :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain))
|
||||
|
||||
|
||||
# Actual install steps
|
||||
for domain in domainList :
|
||||
|
||||
logger.info("Now attempting install of certificate for domain "+domain+" !")
|
||||
|
||||
try :
|
||||
|
||||
if not no_checks : _check_domain_is_correctly_configured(domain)
|
||||
_backup_current_cert(domain)
|
||||
_configure_for_acme_challenge(auth, domain)
|
||||
_fetch_and_enable_new_certificate(domain)
|
||||
_install_cron()
|
||||
|
||||
logger.success(m18n.n("certmanager_cert_install_success", domain=domain))
|
||||
|
||||
except Exception as e :
|
||||
|
||||
logger.error("Certificate installation for "+domain+" failed !")
|
||||
logger.error(str(e))
|
||||
|
||||
|
||||
# Renew
|
||||
|
||||
|
||||
def certificate_renew(auth, domainList, force=False, no_checks=False, email=False):
|
||||
"""
|
||||
Renew Let's Encrypt certificate for given domains (all by default)
|
||||
|
||||
Keyword argument:
|
||||
domainList -- Domains for which to renew the certificates
|
||||
force -- Ignore the validity threshold (15 days)
|
||||
no-check -- Disable some checks about the reachability of web server
|
||||
before attempting the renewing
|
||||
email -- Emails root if some renewing failed
|
||||
"""
|
||||
|
||||
# If no domains given, consider all yunohost domains with Let's Encrypt
|
||||
# certificates
|
||||
if (domainList == []) :
|
||||
for domain in yunohost.domain.domain_list(auth)['domains'] :
|
||||
|
||||
# Does it has a Let's Encrypt cert ?
|
||||
status = _get_status(domain)
|
||||
if (status["CAtype"] != "Let's Encrypt") : continue
|
||||
|
||||
# Does it expires soon ?
|
||||
if (force) or (status["validity"] <= validity_limit) :
|
||||
domainList.append(domain)
|
||||
|
||||
if (len(domainList) == 0) :
|
||||
logger.info("No certificate needs to be renewed.")
|
||||
|
||||
# Else, validate the domain list given
|
||||
else :
|
||||
for domain in domainList :
|
||||
|
||||
# Is it in Yunohost dmomain list ?
|
||||
if domain not in yunohost.domain.domain_list(auth)['domains']:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain))
|
||||
|
||||
status = _get_status(domain)
|
||||
|
||||
# Does it expires soon ?
|
||||
if not ((force) or (status["validity"] <= validity_limit)) :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain))
|
||||
|
||||
|
||||
# Does it has a Let's Encrypt cert ?
|
||||
if (status["CAtype"] != "Let's Encrypt") :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain))
|
||||
|
||||
|
||||
# Actual renew steps
|
||||
for domain in domainList :
|
||||
|
||||
logger.info("Now attempting renewing of certificate for domain "+domain+" !")
|
||||
|
||||
try :
|
||||
|
||||
if not no_checks : _check_domain_is_correctly_configured(domain)
|
||||
_backup_current_cert(domain)
|
||||
_fetch_and_enable_new_certificate(domain)
|
||||
|
||||
logger.success(m18n.n("certmanager_cert_renew_success", domain=domain))
|
||||
|
||||
except Exception as e :
|
||||
|
||||
logger.error("Certificate renewing for "+domain+" failed !")
|
||||
logger.error(str(e))
|
||||
|
||||
if (email) :
|
||||
logger.error("Sending email with details to root ...")
|
||||
_email_renewing_failed(domain, e)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Back-end stuff #
|
||||
###############################################################################
|
||||
|
||||
def _install_cron() :
|
||||
|
||||
cron_job_file = "/etc/cron.weekly/certificateRenewer"
|
||||
|
||||
with open(cron_job_file, "w") as f :
|
||||
|
||||
f.write("#!/bin/bash\n")
|
||||
f.write("yunohost domain cert-renew --email\n")
|
||||
|
||||
_set_permissions(cron_job_file, "root", "root", 0755);
|
||||
|
||||
def _email_renewing_failed(domain, e) :
|
||||
|
||||
from_ = "certmanager@"+domain+" (Certificate Manager)"
|
||||
to_ = "root"
|
||||
subject_ = "Certificate renewing attempt for "+domain+" failed!"
|
||||
|
||||
exceptionMessage = str(e)
|
||||
logs = _tail(50, "/var/log/yunohost/yunohost-cli.log")
|
||||
text = """
|
||||
At attempt for renewing the certificate for domain %s failed with the following
|
||||
error :
|
||||
|
||||
%s
|
||||
|
||||
Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to
|
||||
investigate :
|
||||
|
||||
%s
|
||||
|
||||
-- Certificate Manager
|
||||
|
||||
""" % (domain, exceptionMessage, logs)
|
||||
|
||||
message = """
|
||||
From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
|
||||
%s
|
||||
""" % (from_, to_, subject_, text)
|
||||
|
||||
smtp = smtplib.SMTP("localhost")
|
||||
smtp.sendmail(from_, [ to_ ], message)
|
||||
smtp.quit()
|
||||
|
||||
|
||||
|
||||
def _configure_for_acme_challenge(auth, domain) :
|
||||
|
||||
nginx_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf"
|
||||
|
||||
nginx_configuration = '''
|
||||
location '/.well-known/acme-challenge'
|
||||
{
|
||||
default_type "text/plain";
|
||||
alias '''+webroot_folder+''';
|
||||
}
|
||||
'''
|
||||
|
||||
# Write the conf
|
||||
if os.path.exists(nginx_conf_file) :
|
||||
logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.")
|
||||
else :
|
||||
logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".")
|
||||
with open(nginx_conf_file, "w") as f :
|
||||
f.write(nginx_configuration)
|
||||
|
||||
# Assume nginx conf is okay, and reload it
|
||||
# (FIXME : maybe add a check that it is, using nginx -t, haven't found
|
||||
# any clean function already implemented in yunohost to do this though)
|
||||
_run_service_command("reload", "nginx")
|
||||
|
||||
app_ssowatconf(auth)
|
||||
|
||||
def _fetch_and_enable_new_certificate(domain) :
|
||||
|
||||
# Make sure tmp folder exists
|
||||
logger.debug("Making sure tmp folders exists...")
|
||||
|
||||
if not (os.path.exists(webroot_folder)) : os.makedirs(webroot_folder)
|
||||
if not (os.path.exists(tmp_folder)) : os.makedirs(tmp_folder)
|
||||
_set_permissions(webroot_folder, "root", "www-data", 0650);
|
||||
_set_permissions(tmp_folder, "root", "root", 0640);
|
||||
|
||||
# Prepare certificate signing request
|
||||
logger.info("Prepare key and certificate signing request (CSR) for "+domain+"...")
|
||||
|
||||
domain_key_file = tmp_folder+"/"+domain+".pem"
|
||||
_generate_key(domain_key_file)
|
||||
_set_permissions(domain_key_file, "root", "metronome", 0640);
|
||||
|
||||
_prepare_certificate_signing_request(domain, domain_key_file, tmp_folder)
|
||||
|
||||
# Sign the certificate
|
||||
logger.info("Now using ACME Tiny to sign the certificate...")
|
||||
|
||||
domain_csr_file = tmp_folder+"/"+domain+".csr"
|
||||
|
||||
signed_certificate = sign_certificate(account_key_file,
|
||||
domain_csr_file,
|
||||
webroot_folder,
|
||||
log=logger,
|
||||
CA=certification_authority)
|
||||
intermediate_certificate = requests.get(intermediate_certificate_url).text
|
||||
|
||||
# Now save the key and signed certificate
|
||||
logger.info("Saving the key and signed certificate...")
|
||||
|
||||
# Create corresponding directory
|
||||
date_tag = datetime.now().strftime("%Y%m%d.%H%M%S")
|
||||
new_cert_folder = cert_folder+"/" + domain + "." + date_tag
|
||||
os.makedirs(new_cert_folder)
|
||||
_set_permissions(new_cert_folder, "root", "root", 0655);
|
||||
|
||||
# Move the private key
|
||||
shutil.move(domain_key_file, new_cert_folder+"/key.pem")
|
||||
|
||||
# Write the cert
|
||||
domain_cert_file = new_cert_folder+"/crt.pem"
|
||||
with open(domain_cert_file, "w") as f :
|
||||
f.write(signed_certificate)
|
||||
f.write(intermediate_certificate)
|
||||
_set_permissions(domain_cert_file, "root", "metronome", 0640);
|
||||
|
||||
|
||||
|
||||
logger.info("Enabling the new certificate...")
|
||||
|
||||
# Replace (if necessary) the link or folder for live cert
|
||||
live_link = cert_folder+"/"+domain
|
||||
|
||||
if not os.path.islink(live_link) :
|
||||
shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command)
|
||||
elif os.path.lexists(live_link) :
|
||||
os.remove(live_link)
|
||||
|
||||
os.symlink(new_cert_folder, live_link)
|
||||
|
||||
# Check the status of the certificate is now good
|
||||
|
||||
statusSummaryCode = _get_status(domain)["summaryCode"]
|
||||
if (statusSummaryCode < 20) :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain))
|
||||
|
||||
|
||||
logger.info("Restarting services...")
|
||||
|
||||
for s in [ "nginx", "postfix", "dovecot", "metronome" ] :
|
||||
_run_service_command("restart", s)
|
||||
|
||||
|
||||
def _prepare_certificate_signing_request(domain, key_file, output_folder) :
|
||||
|
||||
# Init a request
|
||||
csr = crypto.X509Req()
|
||||
|
||||
# Set the domain
|
||||
csr.get_subject().CN = domain
|
||||
|
||||
# Set the key
|
||||
with open(key_file, 'rt') as f :
|
||||
key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
|
||||
csr.set_pubkey(key)
|
||||
|
||||
# Sign the request
|
||||
csr.sign(key, "sha256")
|
||||
|
||||
# Save the request in tmp folder
|
||||
csr_file = output_folder+domain+".csr"
|
||||
logger.info("Saving to "+csr_file+" .")
|
||||
with open(csr_file, "w") as f :
|
||||
f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr))
|
||||
|
||||
|
||||
def _get_status(domain) :
|
||||
|
||||
cert_file = cert_folder+"/"+domain+"/crt.pem"
|
||||
|
||||
if (not os.path.isfile(cert_file)) : return {}
|
||||
|
||||
try :
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read())
|
||||
except :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file))
|
||||
|
||||
certSubject = cert.get_subject().CN
|
||||
certIssuer = cert.get_issuer().CN
|
||||
validUpTo = datetime.strptime(cert.get_notAfter(),"%Y%m%d%H%M%SZ")
|
||||
daysRemaining = (validUpTo - datetime.now()).days
|
||||
|
||||
CAtype = None
|
||||
if (certIssuer == _name_selfCA()) :
|
||||
CAtype = "Self-signed"
|
||||
elif (certIssuer.startswith("Let's Encrypt")) :
|
||||
CAtype = "Let's Encrypt"
|
||||
elif (certIssuer.startswith("Fake LE")) :
|
||||
CAtype = "Fake Let's Encrypt"
|
||||
else :
|
||||
CAtype = "Other / Unknown"
|
||||
|
||||
# Unknown by default
|
||||
statusSummaryCode = 0
|
||||
# Critical
|
||||
if (daysRemaining <= 0) : statusSummaryCode = -30
|
||||
# Warning, self-signed, browser will display a warning discouraging visitors to enter website
|
||||
elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt") : statusSummaryCode = -20
|
||||
# Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt)
|
||||
elif (daysRemaining < validity_limit) : statusSummaryCode = -10
|
||||
# CA not known, but still a valid certificate, so okay !
|
||||
elif (CAtype == "Other / Unknown") : statusSummaryCode = 10
|
||||
# Let's Encrypt, great !
|
||||
elif (CAtype == "Let's Encrypt") : statusSummaryCode = 20
|
||||
|
||||
return { "domain" : domain,
|
||||
"subject" : certSubject,
|
||||
"CAname" : certIssuer,
|
||||
"CAtype" : CAtype,
|
||||
"validity" : daysRemaining,
|
||||
"summaryCode" : statusSummaryCode
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Misc small stuff ... #
|
||||
###############################################################################
|
||||
|
||||
def _generate_account_key() :
|
||||
|
||||
logger.info("Generating account key ...")
|
||||
_generate_key(account_key_file)
|
||||
_set_permissions(account_key_file, "root", "root", 0400)
|
||||
|
||||
def _generate_key(destinationPath) :
|
||||
|
||||
k = crypto.PKey()
|
||||
k.generate_key(crypto.TYPE_RSA, key_size)
|
||||
|
||||
with open(destinationPath, "w") as f :
|
||||
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
|
||||
|
||||
def _set_permissions(path, user, group, permissions) :
|
||||
|
||||
uid = pwd.getpwnam(user).pw_uid
|
||||
gid = grp.getgrnam(group).gr_gid
|
||||
|
||||
os.chown(path, uid, gid)
|
||||
os.chmod(path, permissions)
|
||||
|
||||
def _backup_current_cert(domain):
|
||||
|
||||
logger.info("Backuping existing certificate for domain "+domain)
|
||||
|
||||
cert_folder_domain = cert_folder+"/"+domain
|
||||
|
||||
dateTag = datetime.now().strftime("%Y%m%d.%H%M%S")
|
||||
backup_folder = cert_folder_domain+"-backup-"+dateTag
|
||||
|
||||
shutil.copytree(cert_folder_domain, backup_folder)
|
||||
|
||||
|
||||
def _check_domain_is_correctly_configured(domain) :
|
||||
|
||||
public_ip = yunohost.domain.get_public_ip()
|
||||
|
||||
# Check if IP from DNS matches public IP
|
||||
if not _dns_ip_match_public_ip(public_ip, domain) :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain))
|
||||
|
||||
# Check if domain seems to be accessible through HTTP ?
|
||||
if not _domain_is_accessible_through_HTTP(public_ip, domain) :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain))
|
||||
|
||||
def _dns_ip_match_public_ip(public_ip, domain) :
|
||||
|
||||
try :
|
||||
r = requests.get("http://dns-api.org/A/"+domain)
|
||||
except :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org"))
|
||||
|
||||
if (r.text == "[{\"error\":\"NXDOMAIN\"}]") :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain))
|
||||
|
||||
try :
|
||||
dns_ip = json.loads(r.text)[0]["value"]
|
||||
except :
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text))
|
||||
|
||||
if (dns_ip != public_ip) :
|
||||
return False
|
||||
else :
|
||||
return True
|
||||
|
||||
def _domain_is_accessible_through_HTTP(ip, domain) :
|
||||
|
||||
try :
|
||||
requests.head("http://"+ip, headers = { "Host" : domain })
|
||||
except Exception :
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _summary_code_to_string(code) :
|
||||
|
||||
if (code <= -30) : return "CRITICAL"
|
||||
elif (code <= -20) : return "WARNING"
|
||||
elif (code <= -10) : return "Attention"
|
||||
elif (code <= 0) : return "Unknown?"
|
||||
elif (code <= 10) : return "Good"
|
||||
elif (code <= 20) : return "Great!"
|
||||
|
||||
return "Unknown?"
|
||||
|
||||
def _name_selfCA() :
|
||||
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read())
|
||||
return cert.get_subject().CN
|
||||
|
||||
|
||||
def _tail(n, filePath):
|
||||
stdin,stdout = os.popen2("tail -n "+str(n)+" "+filePath)
|
||||
stdin.close()
|
||||
lines = stdout.readlines(); stdout.close()
|
||||
lines = "".join(lines)
|
||||
return lines
|
|
@ -34,10 +34,11 @@ import errno
|
|||
import requests
|
||||
from urllib import urlopen
|
||||
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
|
||||
from yunohost.service import service_regen_conf
|
||||
from yunohost.service import service_regen_conf
|
||||
import yunohost.certificate
|
||||
|
||||
logger = getActionLogger('yunohost.domain')
|
||||
|
||||
|
@ -113,37 +114,7 @@ def domain_add(auth, domain, dyndns=False):
|
|||
m18n.n('domain_dyndns_root_unknown'))
|
||||
|
||||
try:
|
||||
# Commands
|
||||
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
||||
ssl_domain_path = '/etc/yunohost/certs/%s' % domain
|
||||
with open('%s/serial' % ssl_dir, 'r') as f:
|
||||
serial = f.readline().rstrip()
|
||||
try: os.listdir(ssl_domain_path)
|
||||
except OSError: os.makedirs(ssl_domain_path)
|
||||
|
||||
command_list = [
|
||||
'cp %s/openssl.cnf %s' % (ssl_dir, ssl_domain_path),
|
||||
'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, ssl_domain_path),
|
||||
'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch'
|
||||
% (ssl_domain_path, ssl_dir, ssl_dir),
|
||||
'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch'
|
||||
% (ssl_domain_path, ssl_dir, ssl_dir),
|
||||
'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % ssl_domain_path,
|
||||
'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, ssl_domain_path),
|
||||
'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, ssl_domain_path),
|
||||
'chmod 755 %s' % ssl_domain_path,
|
||||
'chmod 640 %s/key.pem' % ssl_domain_path,
|
||||
'chmod 640 %s/crt.pem' % ssl_domain_path,
|
||||
'chmod 600 %s/openssl.cnf' % ssl_domain_path,
|
||||
'chown root:metronome %s/key.pem' % ssl_domain_path,
|
||||
'chown root:metronome %s/crt.pem' % ssl_domain_path,
|
||||
'cat %s/ca.pem >> %s/crt.pem' % (ssl_domain_path, ssl_domain_path)
|
||||
]
|
||||
|
||||
for command in command_list:
|
||||
if os.system(command) != 0:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.n('domain_cert_gen_failed'))
|
||||
yunohost.certificate.certificate_install_selfsigned([domain], False)
|
||||
|
||||
try:
|
||||
auth.validate_uniqueness({ 'virtualdomain': domain })
|
||||
|
@ -285,6 +256,14 @@ def domain_dns_conf(domain, ttl=None):
|
|||
|
||||
return result
|
||||
|
||||
def domain_cert_status(auth, domainList, full=False):
|
||||
return yunohost.certificate.certificate_status(auth, domainList, full)
|
||||
|
||||
def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False):
|
||||
return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed)
|
||||
|
||||
def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False):
|
||||
return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email)
|
||||
|
||||
def get_public_ip(protocol=4):
|
||||
"""Retrieve the public IP address from ip.yunohost.org"""
|
||||
|
@ -301,3 +280,4 @@ def get_public_ip(protocol=4):
|
|||
logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1)
|
||||
raise MoulinetteError(errno.ENETUNREACH,
|
||||
m18n.n('no_internet_connection'))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue