Extreme Thinking
Let's Encrypt 免費SSL 憑證 (certbot + cloudflare)

2021-08-22


Let’s Encrypt 免費SSL 憑證

certbot + cloudflare 免費 SSL 申請

也不知裝有沒有錯反正可以用

yum update
yum install certbot
yum install certbot python-certbot-nginx
yum install python3-certbot-dns-cloudflare
yum list available | grep cloudflare
yum install python2-certbot-dns-cloudflare.noarch

設定

cat /etc/letsencrypt/cli.ini
rsa-key-size = 4096
email = webmaster@gamil.com

傳統做法

[root@localhost ~]# certbot certonly -d "*.abcde.com" --manual --preferred-challenges \
>    dns-01 --server https://acme-v02.api.letsencrypt.org/directory 

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org
Requesting a certificate for *.abcde.com
Performing the following challenges:
dns-01 challenge for abcde.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.abcde.com with the following value:

0IHPpgkjEBg6Wmi7vte43zDrd_B1PqVNuFJqUsnsDwE

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

cloudflare API 設定

[root@localhost ~]# cat /root//.secrets/certbot/cloudflare.ini
dns_cloudflare_email = abcde@gmail.com
dns_cloudflare_api_key = 12a933133045909646ca34aa59737f76d999

cloudflare.ini 權限要對

[root@localhost ~]# chmod 600 ~/.secrets/certbot/cloudflare.ini

測試可以的

[root@localhost ~]# certbot certonly \
>   --dns-cloudflare \
>   --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
>   -d jzsticy.com \
>   -d *.jzsticy.com \
>

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-cloudflare, Installer None
Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org
Requesting a certificate for jzsticy.com and *.jzsticy.com
Performing the following challenges:
dns-01 challenge for jzsticy.com
dns-01 challenge for jzsticy.com
Starting new HTTPS connection (1): api.cloudflare.com
Starting new HTTPS connection (1): api.cloudflare.com
Waiting 10 seconds for DNS changes to propagate
Waiting for verification...
Cleaning up challenges
Starting new HTTPS connection (1): api.cloudflare.com
Starting new HTTPS connection (1): api.cloudflare.com
Subscribe to the EFF mailing list (email: antillean@gamil.com).
Starting new HTTPS connection (1): supporters.eff.org

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/jzsticy.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/jzsticy.com/privkey.pem
   Your certificate will expire on 2021-11-20. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

客人說要在 godaddy 10個域名還要SSL

godaddy 就亂碼產生器去買 ….

NS 然後設定到 cloudflare (可以一次選那十個然後轉到 cloudflare NS)

建立 cloudflare 域名管理 當然用 API 建立

zone_names = ["fnnfra.com","bebsya.com","eonfij.com","vzfqoi.com","chyese.com","azxgaq.com","mopoie.com","odyauu.com","yleoyn.com","hvppah.com"]
for zone_name in zone_names:
    print(zone_name)
    cf = CloudFlare.CloudFlare(email='abcde@gmail.com', token='12a933133045909646ca34aa59737f76d999')
    zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name})
    zone_id = zone_info['id']
    print(zone_id)

檢查 NS

import dns.resolver
zone_names = ["fnnfra.com","bebsya.com","eonfij.com","vzfqoi.com","chyese.com","azxgaq.com","mopoie.com","odyauu.com","yleoyn.com","hvppah.com"]
for zone_name in zone_names:
    answers = dns.resolver.query(zone_name,'NS')
    for server in answers:
        print(server.target)

建立 SSL

import subprocess
import shlex
zone_names = ["fnnfra.com","bebsya.com","eonfij.com","vzfqoi.com","chyese.com","azxgaq.com","mopoie.com","odyauu.com","yleoyn.com","hvppah.com"]
for zone_name in zone_names:
    do = 'certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini -d *.'+ zone_name +' -d '+ zone_name
    #print (shlex.split(do))
    subprocess.call(shlex.split(do))

察到期日

CERT=/etc/letsencrypt/live/chyese.com/fullchain1.pem
date --date="$(openssl x509 -enddate -noout -in $CERT | cut -d= -f 2)" --iso-8601

轉 key 與 crt

openssl rsa -in /etc/letsencrypt/archive/chyese.com/privkey1.pem -out /etc/letsencrypt/archive/chyese.com/privkey1.key
openssl x509 -in /etc/letsencrypt/archive/chyese.com/fullchain1.pem -out /etc/letsencrypt/archive/chyese.com/fullchain1.crt

網路上找的 https://codebox.net/pages/https-certificate-expiry-checker 還不錯用

import sys
import multiprocessing
import ssl
import socket
import datetime
import concurrent.futures
import math


DEFAULT_HTTPS_PORT = 443
WORKER_THREAD_COUNT = multiprocessing.cpu_count()
SOCKET_CONNECTION_TIMEOUT_SECONDS = 10
WARN_IF_DAYS_LESS_THAN = 7
EXIT_SUCCESS = 0
EXIT_EXPIRING_SOON = 1
EXIT_ERROR = 2
EXIT_NO_HOST_LIST = 9

def make_host_port_pair(endpoint):
    host, _, specified_port = endpoint.partition(':')
    port = int(specified_port or DEFAULT_HTTPS_PORT)

    return host, port

def pluralise(singular, count):
    return '{} {}{}'.format(count, singular, '' if count == 1 else 's')

def get_certificate_expiry_date_time(context, host, port):
    with socket.create_connection((host, port), SOCKET_CONNECTION_TIMEOUT_SECONDS) as tcp_socket:
        with context.wrap_socket(tcp_socket, server_hostname=host) as ssl_socket:
            # certificate_info is a dict with lots of information about the certificate
            certificate_info = ssl_socket.getpeercert()
            exp_date_text = certificate_info['notAfter']
            # date format is like 'Sep  9 12:00:00 2016 GMT'
            return datetime.datetime.strptime(exp_date_text, r'%b %d %H:%M:%S %Y %Z')


def format_time_remaining(time_remaining):
    day_count = time_remaining.days

    if day_count >= WARN_IF_DAYS_LESS_THAN:
        return pluralise('day', day_count)

    else:
        seconds_per_minute = 60
        seconds_per_hour = seconds_per_minute * 60
        seconds_unaccounted_for = time_remaining.seconds

        hours = int(seconds_unaccounted_for / seconds_per_hour)
        seconds_unaccounted_for -= hours * seconds_per_hour

        minutes = int(seconds_unaccounted_for / seconds_per_minute)

        return '{} {} {}'.format(
            pluralise('day', day_count),
            pluralise('hour', hours),
            pluralise('min', minutes)
        )

def get_exit_code(err_count, min_days):
    code = EXIT_SUCCESS

    if err_count:
        code += EXIT_ERROR

    if min_days < WARN_IF_DAYS_LESS_THAN:
        code += EXIT_EXPIRING_SOON

    return code

def format_host_port(host, port):
    return host + ('' if port == DEFAULT_HTTPS_PORT else ':{}'.format(port))

def check_certificates(endpoints):
    context = ssl.create_default_context()
    host_port_pairs = [make_host_port_pair(endpoint) for endpoint in endpoints]

    with concurrent.futures.ThreadPoolExecutor(max_workers=WORKER_THREAD_COUNT) as executor:
        futures = {
            executor.submit(get_certificate_expiry_date_time, context, host, port):
            (host, port) for host, port in host_port_pairs
        }

        endpoint_count = len(endpoints)
        err_count = 0
        min_days = math.inf
        max_host_port_len = max([len(format_host_port(host, port)) for host, port in host_port_pairs])
        print('Checking {}...'.format(pluralise('endpoint', endpoint_count)))
        for future in concurrent.futures.as_completed(futures):
            host, port = futures[future]
            try:
                expiry_time = future.result()
            except Exception as ex:
                err_count += 1
                print('{} ERROR {}'.format(format_host_port(host, port).ljust(max_host_port_len), ex))
            else:
                time_remaining = expiry_time - datetime.datetime.utcnow()
                time_remaining_txt = format_time_remaining(time_remaining)
                days_remaining = time_remaining.days
                min_days = min(min_days, days_remaining)

                print('{} {:<5} expires in {}'.format(
                    format_host_port(host, port).ljust(max_host_port_len),
                    'WARN' if days_remaining < WARN_IF_DAYS_LESS_THAN else 'OK',
                    time_remaining_txt))

    exit_code = get_exit_code(err_count, min_days)
    sys.exit(exit_code)

if __name__ == '__main__':
    endpoints = sys.argv[1:]

    if len(endpoints):
        check_certificates(endpoints)
    else:
        print('Usage: {} <list of endpoints>'.format(sys.argv[0]))
        sys.exit(EXIT_NO_HOST_LIST)