initial commit

0 parents
Pipeline #1974 for 0941bff4 skipped in 0 seconds
# конфигурационный файл для letsencrypt, лежит в /etc/acme.conf
[general]
# боевые сертификаты
server_url=https://acme-v01.api.letsencrypt.org
# тестовые сертификаты
#server_url=https://acme-staging.api.letsencrypt.org
# основной домен сайта
primary_domain=test.ashmanov.com
# дополнительные домены сайта
alt_domains=test.ashmanov.com
# папка для хранения сертификатов
acme_dir=/var/cache/acme
#!/usr/bin/env python
import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging
try:
from urllib.request import urlopen # Python 3
from configparser import ConfigParser
except ImportError:
from urllib2 import urlopen # Python 2
from ConfigParser import ConfigParser
try:
config = ConfigParser()
config.read(['/etc/acme.conf'])
DEFAULT_CA = config.get("general", "server_url")
print('server_url =', DEFAULT_CA)
except:
print('Cannot read server_url from section [general] in /etc/acme.conf')
exit(1)
print(DEFAULT_CA)
exit()
with open('/etc/amce.config','r') as f:
output = f.read()
LOGGER = logging.getLogger(__name__)
LOGGER.addHandler(logging.StreamHandler())
LOGGER.setLevel(logging.INFO)
def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA):
# helper function base64 encode for jose spec
def _b64(b):
return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "")
# parse account key to get public key
log.info("Parsing account key...")
proc = subprocess.Popen(["openssl", "rsa", "-in", account_key, "-noout", "-text"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
raise IOError("OpenSSL Error: {0}".format(err))
pub_hex, pub_exp = re.search(
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
header = {
"alg": "RS256",
"jwk": {
"e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"kty": "RSA",
"n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
},
}
accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':'))
thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
# helper function make signed requests
def _send_signed_request(url, payload):
payload64 = _b64(json.dumps(payload).encode('utf8'))
protected = copy.deepcopy(header)
protected["nonce"] = urlopen(CA + "/directory").headers['Replay-Nonce']
protected64 = _b64(json.dumps(protected).encode('utf8'))
proc = subprocess.Popen(["openssl", "dgst", "-sha256", "-sign", account_key],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8'))
if proc.returncode != 0:
raise IOError("OpenSSL Error: {0}".format(err))
data = json.dumps({
"header": header, "protected": protected64,
"payload": payload64, "signature": _b64(out),
})
try:
resp = urlopen(url, data.encode('utf8'))
return resp.getcode(), resp.read()
except IOError as e:
return getattr(e, "code", None), getattr(e, "read", e.__str__)()
# find domains
log.info("Parsing CSR...")
proc = subprocess.Popen(["openssl", "req", "-in", csr, "-noout", "-text"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
raise IOError("Error loading {0}: {1}".format(csr, err))
domains = set([])
common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8'))
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"):
domains.add(san[4:])
# get the certificate domains and expiration
log.info("Registering account...")
code, result = _send_signed_request(CA + "/acme/new-reg", {
"resource": "new-reg",
"agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf",
})
if code == 201:
log.info("Registered!")
elif code == 409:
log.info("Already registered!")
else:
raise ValueError("Error registering: {0} {1}".format(code, result))
# verify each domain
for domain in domains:
log.info("Verifying {0}...".format(domain))
# get new challenge
code, result = _send_signed_request(CA + "/acme/new-authz", {
"resource": "new-authz",
"identifier": {"type": "dns", "value": domain},
})
if code != 201:
raise ValueError("Error requesting challenges: {0} {1}".format(code, result))
# make the challenge file
challenge = [c for c in json.loads(result.decode('utf8'))['challenges'] if c['type'] == "http-01"][0]
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
keyauthorization = "{0}.{1}".format(token, thumbprint)
wellknown_path = os.path.join(acme_dir, token)
with open(wellknown_path, "w") as wellknown_file:
wellknown_file.write(keyauthorization)
# check that the file is in place
wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token)
try:
resp = urlopen(wellknown_url)
resp_data = resp.read().decode('utf8').strip()
assert resp_data == keyauthorization
except (IOError, AssertionError):
os.remove(wellknown_path)
raise ValueError("Wrote file to {0}, but couldn't download {1}".format(
wellknown_path, wellknown_url))
# notify challenge are met
code, result = _send_signed_request(challenge['uri'], {
"resource": "challenge",
"keyAuthorization": keyauthorization,
})
if code != 202:
raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
# wait for challenge to be verified
while True:
try:
resp = urlopen(challenge['uri'])
challenge_status = json.loads(resp.read().decode('utf8'))
except IOError as e:
raise ValueError("Error checking challenge: {0} {1}".format(
e.code, json.loads(e.read().decode('utf8'))))
if challenge_status['status'] == "pending":
time.sleep(2)
elif challenge_status['status'] == "valid":
log.info("{0} verified!".format(domain))
os.remove(wellknown_path)
break
else:
raise ValueError("{0} challenge did not pass: {1}".format(
domain, challenge_status))
# get the new certificate
log.info("Signing certificate...")
proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
csr_der, err = proc.communicate()
code, result = _send_signed_request(CA + "/acme/new-cert", {
"resource": "new-cert",
"csr": _b64(csr_der),
})
if code != 201:
raise ValueError("Error signing certificate: {0} {1}".format(code, result))
# return signed certificate!
log.info("Certificate signed!")
return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
"\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64)))
def main(argv):
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent("""\
This script automates the process of getting a signed TLS certificate from
Let's Encrypt using the ACME protocol. It will need to be run on your server
and have access to your private account key, so PLEASE READ THROUGH IT! It's
only ~200 lines, so it won't take long.
===Example Usage===
python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > signed.crt
===================
===Example Crontab Renewal (once per month)===
0 0 1 * * python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > /path/to/signed.crt 2>> /var/log/acme_tiny.log
==============================================
""")
)
parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key")
parser.add_argument("--csr", required=True, help="path to your certificate signing request")
parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory")
parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
parser.add_argument("--ca", default=DEFAULT_CA, help="certificate authority, default is Let's Encrypt")
args = parser.parse_args(argv)
LOGGER.setLevel(args.quiet or LOGGER.level)
signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca)
sys.stdout.write(signed_crt)
if __name__ == "__main__": # pragma: no cover
main(sys.argv[1:])
# cron.d format:
# m h dom m dow user command
0 3 26 * * root /usr/local/bin/acme_refresh >> /var/log/acme.log 2>&1
# crontab format:
# m h dom m dow command
0 3 26 * * /usr/local/bin/acme_refresh >> /var/log/acme.log 2>&1
#!/usr/bin/env bash
# скрипт инициализирует папку ACMEDIR
# создаётся временный сертификат на 1 день
# подготавливаются необходимые ключи и файлы запросов
# проставляются права
set -e
CONFIGFILE=/etc/acme.conf
# считать настройку из конфига
function readconfig {
OPTION=$1
# test file
if [[ ! -f $CONFIGFILE ]] ; then
echo "Configuration file $CONFIGFILE not found!"
exit 1
fi
# read option
readconfig_return_value="$(cat $CONFIGFILE \
| grep -v "^[[:space:]]*#" \
| awk -F "=" '/'$OPTION'/ {print $2}' \
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
)"
# test option
if [[ -z "$readconfig_return_value" ]] ; then
echo "Option $OPTION not found in $CONFIGFILE!"
exit 1
fi
}
OPENSSL=$(/usr/bin/env which openssl)
if [[ ! -x $OPENSSL ]] ; then
echo Cannot find openssl
exit 1
fi
# теперь считываем конфиг
readconfig acme_dir
ACMEDIR=$readconfig_return_value
readconfig primary_domain
PRIMARY_DOMAIN=$readconfig_return_value
readconfig alt_domains
ALT_DOMAINS=$readconfig_return_value
# если файл уже есть, ничего не делаем
if [[ -f "$ACMEDIR/site.csr" ]]
then
(>&2 echo "Directory '$ACMEDIR' already exists")
exit 0
fi
mkdir -p $ACMEDIR/challenges
# создаём необходимые сертификаты
$OPENSSL genrsa 4096 > $ACMEDIR/account.key
$OPENSSL genrsa 4096 > $ACMEDIR/site.key
$OPENSSL req \
-new \
-key $ACMEDIR/site.key \
-days 1 \
-nodes \
-x509 \
-subj "/CN=$PRIMARY_DOMAIN" \
-out $ACMEDIR/site.crt
# если нет дополнительных доменов
if [[ -z "$ALT_DOMAINS" ]]
then
$OPENSSL req \
-new \
-sha256 \
-key $ACMEDIR/site.key \
-subj "/CN=$PRIMARY_DOMAIN"
else
$OPENSSL req \
-new \
-sha256 \
-key $ACMEDIR/site.key \
-subj "/CN=$PRIMARY_DOMAIN" \
-reqexts SAN \
-config \
<(cat /etc/ssl/openssl.cnf \
<(sed -e 's/[[:space:],]\+/,DNS:/g' -e 's/^/[SAN]\nsubjectAltName=DNS:/' \
<(echo "$ALT_DOMAINS")))
fi \
> $ACMEDIR/site.csr
echo "`date` initialized" > $ACMEDIR/log.txt
#!/usr/bin/env bash
# скрипт запрашивает новый сертификат с помощью утилиты acme_client
# кладёт его на место старого и перезапускает NGINX
# сертификат выдаётся на 90 дней
# перевыпускать следует раз в месяц
set -e
CONFIGFILE=/etc/acme.conf
# считать настройку из конфига
function readconfig {
OPTION=$1
# test file
if [[ ! -f $CONFIGFILE ]] ; then
echo "Configuration file $CONFIGFILE not found!"
exit 1
fi
# read option
readconfig_return_value="$(cat $CONFIGFILE \
| grep -v "^[[:space:]]*#" \
| awk -F "=" '/'$OPTION'/ {print $2}' \
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
)"
# test option
if [[ -z "$readconfig_return_value" ]] ; then
echo "Option $OPTION not found in $CONFIGFILE!"
exit 1
fi
}
# теперь считываем конфиг
readconfig acme_dir
ACMEDIR=$readconfig_return_value
# выводим дату (для лога)
date
# если нет папки, ничего не делаем
if [[ ! -d "$ACMEDIR" ]] || [[ ! -w "$ACMEDIR" ]]
then
echo "Check that directory '$ACMEDIR' exists and is writable"
exit 1
fi
# запускаем клиент для обновления сертификата
set +e && /usr/local/bin/acme_client \
--account-key $ACMEDIR/account.key \
--csr $ACMEDIR/site.csr \
--acme-dir $ACMEDIR/challenges/ > $ACMEDIR/acme_site.crt
ERRCODE=$?
set -e
# проверяем, получилось ли
if [[ $ERRCODE -ne 0 ]] ; then
echo "Error '$ERRCODE 'while refreshing the certificate"
exit 1
fi
# вытащить из сертификата адрес issuer, скачать его и конвертировать в .crt
function get_issuer_cer {
FROM_CERT=$1
SAVE_FILENAME_BIN=$2
SAVE_FILENAME_CRT=$3
# находим нужное поле в сертификате
ISSUER_URL=$(/usr/bin/openssl x509 -in $FROM_CERT -noout -text \
| grep "^ *Authority Information Access: $" -A 5 \
| grep "^ *CA Issuers - URI:http://" | cut -d ":" -f 2-)
echo downloading $ISSUER_URL
# если получилось, скачиваем и конвертируем
if [[ ! -z $ISSUER_URL ]] ; then
/usr/bin/curl -fSL $ISSUER_URL -o $SAVE_FILENAME_BIN
echo converting x509/der
# пробуем прочесть через x509
set +e && /usr/bin/openssl x509 -inform der -in $SAVE_FILENAME_BIN -out $SAVE_FILENAME_CRT
ERRCODE=$?
set -e
# если не вышло, пробуем pkcs7
if [[ $ERRCODE -ne 0 ]] ; then
echo x509/der failed, try pkcs7/der instead
openssl pkcs7 -print_certs -inform der -in $SAVE_FILENAME_BIN -out $SAVE_FILENAME_CRT
fi
fi
}
# получаем промежуточный сертификат
get_issuer_cer $ACMEDIR/acme_site.crt $ACMEDIR/intermediate.der $ACMEDIR/intermediate.crt
# получаем корневой сертификат
get_issuer_cer $ACMEDIR/intermediate.crt $ACMEDIR/root-ca.der $ACMEDIR/root-ca.crt
# записываем в файл
cat $ACMEDIR/acme_site.crt > $ACMEDIR/site.crt
cat $ACMEDIR/intermediate.crt >> $ACMEDIR/site.crt
cat $ACMEDIR/root-ca.crt >> $ACMEDIR/site.crt
# дёргаем nginx
/usr/bin/env nginx -s reload
#!/usr/bin/env bash
echo "Пример конфигурации nginx:"
echo "=========================="
cat ./nginx.conf
echo "=========================="
echo
echo "Пример кронтаба:"
echo "=========================="
cat ./acme_crontab
echo "=========================="
echo
echo Устанавливаю файлы:
echo
set -e
set -x
# ставим сюда
PREFIX=/usr/local/bin
mkdir -p $PREFIX
# копируем с перезаписью
cp ./acme_init $PREFIX/acme_init
cp ./acme_refresh $PREFIX/acme_refresh
cp ./acme_client $PREFIX/acme_client
# конфиг перезаписывать не нужно
if [[ ! -f /etc/acme.conf ]] ; then
cp ./acme.conf /etc/acme.conf
fi
exit
server {
# слушаем порты и настраиваем SSL:
listen 80;
listen 443 ssl;
# сертификаты для сервера
ssl_certificate /var/cache/acme/site.crt;
ssl_certificate_key /var/cache/acme/site.key;
# простейшая защита от XSS
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
# запросы acme-challenge
location ~ ^/\.well-known/acme-challenge/([a-zA-Z0-9_-]*)$ {
default_type "text/plain";
alias /var/cache/acme/challenges/$1;
}
# редирект на https
if ($scheme != https) {
rewrite ^ https://$http_host$request_uri permanent;
}
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!