Drollery Medieval drollery of a knight on a horse

🏆 欢迎来到本站: https://xuchangwei.com/希望这里有你感兴趣的内容

flowery border with man falling
flowery border with man falling

scripts-ssl证书

Let's Encrypt 免费证书

https://letsencrypt.org/

有效期 3 个月 支持不用的客户端命令申请证书,官方推荐使用 Certbot

certbot

安装

MacOS

# Install Certbot
brew install certbot

certbot --version # 查看版本

申请证书

# 使用DNS认证的方式来获取证书
sudo certbot certonly  -d "*.aiull.com" --manual --preferred-challenges dns

参数说明

certonly 安装模式
-d 申请证书的域名,如果是通配符域名输入 *.example.com
–manual 手动安装插件
–preferred-challenges dns 使用 DNS 方式校验域名所有权。certbot 将要求您将包含特定内容的 TXT DNS 记录放置在域名下,该域名由要为其颁发证书的主机名组成,前缀为_acme-challenge
–server,Let’s Encrypt ACME v2 版本,默认为 https://acme-staging-v02.api.letsencrypt.org/directory

# 命令行选项
certbot --help all

手动安装插件
--manual-auth-hook 和 --manual-cleanup-hook 标志指定脚本来准备进行验证,执行身份验证过程和 / 或在身份验证之后进行清理。


# 锁定文件
--work-dir--logs-dir 和 --config-dir。默认情况下,这些分别都是 /var/lib/letsencrypt,/var/log/letsencrypt 和 /etc/letsencrypt
如:
mkdir zhengzhu ; cd zhengzhu
sudo certbot certonly  -d "*.aiull.com" --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory --config-dir ./config --work-dir ./work --logs-dir ./log

第一次执行完命令之后会提示需要输入邮箱,和一些协议需要同意和确认,然后每次运行此命令需要记录 IP 信息,需要同意不然不能继续申请

设置DNS TXT记录 将提示的 txt 记录值配置在域名供应商 dns 域名解析中。由于 DNS 记录不会马上生效,所以稍后再按回车键。

# 验证
nslookup -q=txt _acme-challenge.example.com
dig +short -t txt _acme-challenge.example.com
或者
host -t txt _acme-challenge.alicdn.com

验证证书有效期

sudo openssl x509 -noout -text -in /Users/7tq6lr/zhengzhu/config/live/aiull.com/fullchain.pem

默认证书目录为:`/etc/letsencrypt/live/$domain`

查看证书

certbot certificates

自动续订

# 手动续订
sudo certbot renew -dry-run

# 帮助 certbot --help renew
# --force-renewal 强制更新忽略有效期,但证书颁发机构有速率限制不适合每天更新,一周内不超过5次。 https://letsencrypt.org/docs/duplicate-certificate-limit/


# 设置自动续订 我们建议运行以下行,这会将 cron 作业添加到默认 crontab。
echo "0 0,12 * * * root $(command -v python3) -c 'import random; import time; time.sleep(random.random() * 3600)' && sudo $(command -v certbot) renew -q" | sudo tee -a /etc/crontab > /dev/null
# 验证证书有效性

使用dns申请的证书是没办法直接使用certbot renew来更新证书的. 幸亏 cerbot 提供了一个 manual-auth-hook hook 可以编写一个脚本,由这个脚本来先完成 DNS 验证,然后再进行 renew。对应的脚本会自动添加 DNS 记录,从而完成 DNS 校验,并自动 renew 证书。

参考:https://blog.k4nz.com/704d5b9b81a53f14ffc04a07befc7e55/ https://www.cnblogs.com/ellisonzhang/p/14298492.html

pip install certbot-dns-aliyun

# 前往 https://ram.console.aliyun.com 申请阿里云子账号并授予 AliyunDNSFullAccess 权限
# 创建 AccessKey AccessToken

cat > /etc/letsencrypt/dns-aliyun-credentials.ini <<EOF
certbot_dns_aliyun:dns_aliyun_access_key = 12345678
certbot_dns_aliyun:dns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef
EOF

chmod 600 /etc/letsencrypt/dns-aliyun-credentials.ini

certbot certonly \
    -a certbot-dns-aliyun:dns-aliyun \
    --certbot-dns-aliyun:dns-aliyun-credentials /etc/letsencrypt/dns-aliyun-credentials.ini \
    -d "*.example.com" \
    --dry-run

配置 nginx

证书脚本

00-安装配置管理

  1. 证书申请

1.1 证书域名申请服务器

rsync-server 10.200.46.63

1.2 自动申请脚本

[root@rsync-server ~]# crontab -l
# update certificate
30 09 * * * /bin/sh /home/script/update_cer.sh > /dev/null
# 域名检查
40 10 * * * cd /home/yongzhi.shi/python && /usr/bin/python /home/yongzhi.shi/python/check_domain.py &> /dev/null 
  1. nginx证书管理

2.1 挂载NAS

对应机房申请容量型NAS。

# 挂载NAS
yum install nfs-utils -y
mkdir /usr/local/zhengshu/

vim /etc/fstab
对应NAS地址:/ /usr/local/zhengshu    nfs4   vers=4.0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,noresvport 0 0

mount -a 

2.2 ansible推送脚本

1 登录sw执行推送脚本

sudo su - oap
cd tools/ansible/
# hosts文件[ssl]下加上有证书的nginx的IP
ansible-playbook change_ssl.yml

2 修改证书更新策略

证书剩余28天升级

vim change_ssl.yml
# 修改--days 参数
- parameters: "--days 28"

3 修改证书脚本

sudo su - oap
cd tools/ansible/
vim files/ssl_monitor.sh
# 只修改server_name 和 curl_name内容

nginx本地shell邮件配置

rsync -r  [email protected]::file/mailssl/certs.tar.gz /tmp/
# 密码:wowonetwork
tar xf /tmp/certs.tar.gz  -C /root/
cat >>/etc/mail.rc<< \EOF
set bsdcompat
set [email protected]
set smtp=smtps://smtp.cici.com:994
set [email protected]
set smtp-auth-password='LZ6t0IoL'
set smtp-auth=login
set ssl-verify=ignore
set nss-config-dir=/root/.certs
EOF

01-申请过程

### 阿里key

export Ali_Key="LhIS"
export Ali_Secret="yjUJFu3Ut"

测试nginx的公网地址

~/.acme.sh/acme.sh --issue --dnssleep 10 --dns dns_ali -d cici.com -d "*.cici.com" \
--installcert\
--key-file /usr/local/nginx/conf/cici.com.key \
--fullchain-file /usr/local/nginx/conf/fullchain.cer \
--reloadcmd "service nginx force-reload"

#更新
~/.acme.sh/acme.sh --renew --dns -d c2c.com -d *.c2c.com

*.cici.com域名证书申请

~/.acme.sh/acme.sh --issue --dns dns_ali -d cici.com -d "*.cici.com" -d "*.yf.cici.com" -d "*.partnergate.cici.com" -d "*.crossacc.cici.com"

#更新
~/.acme.sh/acme.sh --renew --force --dns -d cici.com -d "*.cici.com" -d "*.yf.cici.com" -d "*.partnergate.cici.com" -d "*.crossacc.cici.com"
#查看证书具体内容
openssl x509 -in /data/zhengshu/cici.com/fullchain.cer  -noout -text
#查看证书时间
openssl x509 -in /data/zhengshu/cici.com/fullchain.cer  -noout -dates

02-自动申请续期

ssl证书自动申请

本证书申请是在rsync的服务器上进行更新证书的,处理逻辑是根据现有的域名证书去检查剩余的时间,如果时间低于28天则去申请更新证书,如果大于28天则只发送邮件告诉sa证书的剩余时间;

定时任务

# update certificate
30 09 * * * /bin/sh /home/script/update_cer.sh > /dev/null

脚本

cat >/home/script/update_cer.sh <<\EOF
#!/bin/sh
###############################
#system variable
timestamp=`date +%s`
cer_dir="/root/.acme.sh/"
domain_name="
cici.com
xxx.com
"
renew_flag=1

###############################
#yunzong domain name info
Cici_Key () {
    export Ali_Key="LTAh"
    export Ali_Secret="Nl2aJ4e"
}

#xxx domain name info
XXX._Key () {
    export Ali_Key="LTAIICw"
    export Ali_Secret="SnpWvDX"
}


#application certificate
App_Cer () {
    ~/.acme.sh/acme.sh --force --renew-all --dnssleep 10
}

#copy certificate to directory
Copy_Cer () {
    /bin/cp -af /root/.acme.sh/cici.com/fullchain.cer /data/zhengshu/cici.com
    /bin/cp -af /root/.acme.sh/XXX.com/fullchain.cer /data/zhengshu/XXX.com
}

#check certificate endtime
declare -A apply_certs
declare -A certs_days
Check_Cer () {
    for domain in ${domain_name}; do
        cd ${cer_dir}${domain}
        check_key=`/bin/openssl x509 -in fullchain.cer -noout -dates|head -n 2|awk -F= '{print $2}'|tail -n 1`
        key_time=$[(`date -d "${check_key}" +%s`-${timestamp})/24/3600]
        if [ ${key_time} -le 28 ];then
            apply_certs[${#apply_certs[*]}]="`pwd` Certificate time remaining ${key_time} day,this applying\n"
            renew_flag=0
        else
            certs_days[${#certs_days[*]}]="`pwd` ${key_time} day \n"
        fi
    done

    if [ $renew_flag -eq 0 ]; then
        Cici_Key
        App_Cer
        sleep 30
        XXX._Key
        App_Cer
        Copy_Cer
    fi
}


#dingding push
dingding() {
local access_tokens=$1
local ding_url='https://oapi.dingtalk.com/robot/send'
local ding_header='Content-Type: application/json'
local msg=$2
for key in $access_tokens; do
curl  -connect-timeout 40 -XPOST -H "$ding_header" "$ding_url?access_token=$key" \
-d '{"msgtype": "text",
    "text": {
        "content": "【ssl证书源文件状态】 \n'"$msg"'"
    },
"at": {
      "atMobiles": [],
      "isAtAll": true
  }
}'
done
}

tokens="a618438957c"
main (){
    Check_Cer
    [ ${#apply_certs[@]} -gt 0 ] && {
        echo -e "${apply_certs[@]}" | mail -s 'Ali Certificate Details' [email protected]
        dingding "$tokens" "$(echo -e "(${apply_certs[@]}")"
    }
    echo -e "rsync服务器上已申请证书的有效期:\n ${certs_days[@]}" | mail  -s 'Ali Certificate Details' [email protected]
    dingding "$tokens" "$(echo -e "rsync服务器上已申请证书的有效期:\n ${certs_days[@]}")"
}

main
EOF

03-nginx证书过期同步

脚本逻辑:

  1. 从rsync服务器获取证书,并存放在/tmp下,脚本执行结束后删除临时文件
  2. 根据server_name域名变量对比本地证书目录,如果不存在域名目录,则创建
  3. 如果本地证书剩40天时,则替换证书。分2种情况替换
    • 假设1:检验证书存储点的证书,如果证书CN域名与证书存储目录不对应,则从/tmp中替换证书,并备份变化之前证书
    • 假设2:如果/tmp目录下的证书剩余大于40天,则从/tmp目录中替换证书
  4. 每次脚本运行会生成证书信息,以待下次对比变化。
  5. 如果发现一个证书事件,则重载nginx配置文件。
```mermaid
graph TD
A(第一次检查)-->|内部|B(循环并发执行)
B-->|内部|C(删除临时证书目录)
C-->D(删除老的证书备份文件)
D-->|删除30天前备份|E(第二次检查)
E-->|内部|F(nginx重启)

A-->AA{证书更新记录是否存在}
AA-->|存在|AA1{本地证书文件是否存在}
AA1-->|存在|AA2{本地证书是否更新过-比对在1天以内误差}
AA2-->|没更新|AA3(创建nginx重启触发文件)
AA3-->F

B-->BB[从rsync服务端拉取证书]
BB-->|放到临时证书目录/tmp/zhengshuXXX下|BB1[备份证书]
BB1-->|备份原证书到/data/backup/zhengshu_bak目录下|BB2[循环域名检查]
BB2-->|内部|C

BB2-->|并发执行|BBB{证书目录及文件是否存在}
BBB-->|不存在|OO[替换此证书到本地证书目录]
BBB-->|存在|BBB1{判断本地证书是否有效}
BBB1-->|无效|BBB2[备份无效证书]
BBB2-->OO
BBB-->|存在|BBB11{判断证书剩余时间}
BBB11-->|小于等于26天|OO
OO-->OOO[更新证书记录-证书要大于26天]
OOO-->C

E-->EE[访问本地证书]
EE-->EE1{判断本地curl_name是否能访问}
EE1-->|能访问|EE2{判断证书剩余时间}
EE2-->|判断证书剩余时间与证书更新记录文件中不相等|AA3
```

免交互式

1.1 rsync同步,免除手动输入密码

yum install -y expect
[root@agent01 ~]# cat rsync_pull.exp
#!/usr/bin/expect -f
set user "sa"
set ip "124.x7.109.95"
set rs_name "zhengshu"
set src_path [lindex $argv 0]
set dest_path [lindex $argv 1]
set passwd "wowonetwork"

set timeout 10

spawn rsync -q -az ${user}@${ip}::${rs_name}/${src_path} ${dest_path}

expect {
   "*Password:" {send "$passwd\r"}
}
expect eof

定时任务

# ssl_monitor 剩余28天更新
0 23 * * * /bin/bash /home/sh/ssl_monitor.sh - -days 28 &>/dev/null

nginx服务器更新证书脚本

3.1 简单版本

[root@agent01 ~]# cat rsync_pull.sh
#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
max_process=5
ssl_path=/data
ssl_pem=fullchain.cer
now_seconds=$(date '+%s')
server_name="
cici.com
xxx.com
"
cd $DIR

Multi_process_init() {
    trap 'exec 5>&-;exec 5<&-;exit 0' 2
    pipe=`mktemp -u tmp.XXXX`
    mkfifo $pipe
    exec 5<>$pipe
    rm -f $pipe

    seq $1 >&5
}

function ssl_pull_all() {
    for s in ${server_name}; do
        src_path=$s
        dest_path=${ssl_path}/$s
        expect rsync_pull.exp $src_path/ $dest_path
    done
}

function ssl_pull_one() {
    for s in $1; do
        src_path=$s
        dest_path=${ssl_path}/$s
        expect rsync_pull.exp $src_path/ $dest_path &>/dev/null
    done
    wait
}

# check
#end_date=$(echo |\
#         openssl s_client -host cmserver.zonghengke.com -port 443 -showcerts </dev/null 2>/dev/null |\
#         openssl x509  -noout -dates |\
#         sed -n 's@notAfter=@@p')
function ssl_check_time() {
    for s in ${server_name}; do
        read -u5
        {
            [ ! -e ${ssl_path}/${s}/ ] && ssl_pull_one $s
            end_date=$(openssl x509  -noout -dates -in ${ssl_path}/${s}/${ssl_pem} | sed -n 's@notAfter=@@p')
            echo $s $end_date
            last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
            echo $last_time
            if [ $last_time -le 55  ]; then
                ssl_pull_one $s
            fi
            echo >&5
            echo ======${ssl_path}/${s}/ certificate finshed
        }&
    done
    wait
}

#ssl_pull
function main() {
Multi_process_init $max_process
ssl_check_time
exec 5>&-;exec 5<&-
}

main

3.2 完整版

[root@agent01 ~]# cat ssl_monitor.sh
#!/bin/bash
#========================
# version= 0.1
# 功能:证书过期替换,nginx服务重载配置。
# 场景:
# 1. 适用于单机nginx环境
# 2. 适用于有共享存储的多机nginx环境
# 运行限制:提前指定每个server_name变量,nginx证书目录规范统一放在脚本定义的ssl_path变量下。
# 脚本逻辑:
# 0. 从rsync服务器获取证书,并存放在/tmp下,脚本执行结束后删除临时文件
# 1. 根据server_name域名变量对比本地证书目录,如果不存在域名目录,则创建
# 2. 如果本地证书剩40天时,则替换证书。分2种情况替换
# 2.1. 假设1:检验证书存储点的证书,如果证书CN域名与证书存储目录不对应,则从/tmp中替换证书,并备份变化之前证书
# 2.2. 假设2:如果/tmp目录下的证书剩余大于40天,则从/tmp目录中替换证书
# 3. 每次脚本运行会生成证书信息,以待下次对比变化。
# 4. 如果发现一个证书事件,则重载nginx���置文件。
#########################
#source /etc/profile
export LANG=en_US.UTF8
host_ip=$(/usr/sbin/ifconfig eth0 | sed -n '/inet /p'| awk '{print $2}')
host_name=$(hostname)
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
max_process=5
tmpfile=$(mktemp -u zhengshu.XXXX)
tmp_path=/tmp/.${tmpfile}
backup_path=/data/backup/zhengshu_bak
ssl_path=/usr/local/zhengshu
ssl_pem=fullchain.cer
now_seconds=$(date '+%s')
event_file=/tmp/ssl_monitor_event
old_info=/data/backup/ssl_old_info
email_file=/tmp/email_file
tokens="a6184389010ffc41f045ab23eab3d40f8ebe1cd9526e7832a131d718aa13057c"
min_days=28
#证书域名变量(重要)
server_name="
cici.com
c2c.com
c3c.com
"

#本地证书有效检查变量(可选)
curl_name="
pur.cici.com
emc.c2c.com
"
# 检查目录是否存存
cd $DIR
[ ! -e  $tmp_path ] && mkdir -p $tmp_path
[ ! -e  $backup_path ] && mkdir -p $backup_path
[ ! -e  $ssl_path ] && mkdir -p $ssl_path
>$email_file
#echo $tmp_path
[ -z "$(which mail)" ] && yum install -y mailx

# 查检程序依赖
create_expect() {
  [ -z "$(which expect)" ] && yum install -y expect
  [ -z "$(which rsync)" ] && yum install -y rsync
    # 创建免交互文件
    cat > ssl_monitor.exp << \EOF
#!/usr/bin/expect -f
set user "sa"
#set ip "124.x7.109.95"
set ip "47.xx.11.58"
set rs_name "zhengshu"
set src_path [lindex $argv 0]
set dest_path [lindex $argv 1]
set passwd "wowonetwork"

set timeout 10

spawn rsync -q -az ${user}@${ip}::${rs_name}/${src_path} ${dest_path}

expect {
    "*Password:" {send "$passwd\r"}
}
expect eof
EOF
chmod +x ssl_monitor.exp
}

# 进度条函数
load() {
    local i=0;
    local str=""
    while [ $i -le 100 ]; do
      printf "\e[0;1m[%-100s]1/1\r\e[0m" "$str"
      usleep 5000
      let i++
      #str+='█'
      str+='#'
    done
    printf "\n"
}

# 并发函数
Multi_process_init() {
    trap 'exec 5>&-;exec 5<&-;exit 0; rm -fr $tmp_path' 2
    pipe=`mktemp -u tmp.XXXX`
    mkfifo $pipe
    exec 5<>$pipe
    rm -f $pipe

    seq $1 >&5
}

# 拉取rsync服务器中证书函数
ssl_pull_all() {
    for s in ${server_name}; do
        read -u5
        {
        src_path=$s
        dest_path=${tmp_path}/$s
        expect ssl_monitor.exp $src_path/ $dest_path &>/dev/null
        echo >&5
        }&
    done
    wait
}

# 本地旧证书备份函数
ssl_old_backup() {
    ssl_change=$1
    ssl_server_name=$2
    if [ "$ssl_change" == "true" ]; then
        echo "Problem: Local ssl $ssl_server_name err "
        rsync -rlpgoDz --delete ${ssl_path}/${ssl_server_name} ${backup_path}/${ssl_path##*/}_ssl_problem_change_before.$(date +%Y%m%d%H)/
    else
        rsync -rlpgoDz --delete ${ssl_path}/ ${backup_path}/${ssl_path##*/}.$(date +%Y%m%d%H%M)/
        #cp -dR ${ssl_path}/ ${backup_path}/${ssl_path##*/}.$(date +%Y%m%d%H%M)/
    fi
}

# 本地旧证书过期删除函数
ssl_old_del() {
    find ${backup_path} -maxdepth 1 -type d -mtime +30 -iname "${ssl_path##*/}*" | xargs rm -rf
}

# 邮件内容函数
send_mail() {
   :
   host_name=$host_name
   host_ip=$host_ip
   email_file=$email_file
   domain=$1
   msg=$2

   [ "`grep $domain $email_file`" ] || echo "[$domain]" >> $email_file
   [ "`sed -rn "/\[$domain\]/,/[^\[]/p" $email_file  |grep "$msg"`" ] || sed -i "/\[${domain}\]/a$host_ip $host_name $msg" $email_file
}

#dingding push
dingding() {
local access_tokens=$1
local ding_url='https://oapi.dingtalk.com/robot/send'
local ding_header='Content-Type: application/json'
local msg=$2
for key in $access_tokens; do
curl  -connect-timeout 40 -XPOST -H "$ding_header" "$ding_url?access_token=$key" \
-d '{"msgtype": "text",
    "text": {
        "content": "【nginx证书更新通知】 \n'"$msg"'"
    },
"at": {
      "atMobiles": [],
      "isAtAll": true
  }
}'
done
}

# 本地证书与拉取到证书比对函数,有效则更新本地证书(重要)
ssl_copy_one() {
    for s in $1; do
        local src_path=${tmp_path}/$s
        local dest_path=${ssl_path}/$s
        local remot_end_date=$(openssl x509  -noout -dates -in ${tmp_path}/${s}/${ssl_pem} | sed -n 's@notAfter=@@p')
        local remot_last_time=$((($(date '+%s' --date "${remot_end_date}") - $now_seconds)/24/3600))
        if [ $remot_last_time -gt $min_days ]; then
            echo "remot_last_time $remot_last_time > min_days $min_days "
            rsync -az --delete --timeout=100 $src_path/ $dest_path
            touch $event_file
            send_mail $s "$2"
        fi
    done
    wait
}

# 本地nginx重载配置函数(重要)
ng_reload() {
    echo "... Reload nginx?"
    ng_p=$(ps -ef|grep nginx|grep master |awk '{print $2}')
    [  -z "$ng_p" ] && { echo nginx is not running; exit 1; }
    ng_cmd=$(ls -l /proc/${ng_p}/exe| awk '{print $11}')
    nginx_reload_sum=0
    if [ -f "$event_file" ]; then
        if  $ng_cmd -t &>/dev/null; then
            let nginx_reload_sum++
            #echo $nginx_reload_sum
        else
            let nginx_reload_sum--
            #echo $nginx_reload_sum
        fi
        rm -f $event_file
        [ $nginx_reload_sum -ge 0 ] && { echo "yes, done" ; $ng_cmd -s reload; } || { $ng_cmd -t; exit 1; }
    else
       echo "no, nothing is changed"
    fi

}

# 检查本地更新文件,变化则生成重载nginx触发事件文件
ssl_check_first() {
    if [ -f $old_info ]; then
        for s in ${server_name}; do
            if [ -e ${ssl_path}/${s}/  -a  -f ${ssl_path}/${s}/fullchain.cer -a -f ${ssl_path}/${s}/${s}.key ]; then
                end_date=$(openssl x509  -noout -dates -in ${ssl_path}/${s}/${ssl_pem} | sed -n 's@notAfter=@@p')
                last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
                yes_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600+1))
                old_time=$(cat ${old_info} |sed -rn "/${s}/s@${s},(.*)@\1@gp" | head -1)
                if [ "$last_time" != "$old_time" -a "$yes_time" != "$old_time" ]; then
                   echo "$s $last_time != $old_time Create event file: $event_file"
                   touch $event_file
                   send_mail $s "触发nginx reload事件. 与本次的更新记录${new_time}不一致,本地证书有效期${last_time}"
                   sleep 2
                fi
            fi
        done
    else
        touch $old_info
    fi
}

# 本地检查nginx重载后,证书验证函数
ssl_check_second() {
  :
  for s in ${curl_name}; do
      if curl -I -v -m 1 --resolve "${s}:443:${host_ip}" "https://${s}/" &>/dev/null; then
          end_date=$(curl -I -v -m 1 --resolve "${s}:443:${host_ip}" "https://${s}/" 2>&1 |sed -nr 's@.*expire date: @@p')
          last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
          s_name=$(echo ${s#*.})
          new_time=$(cat ${old_info} |sed -rn "/${s_name}/s@${s_name},(.*)@\1@gp" | head -1)
          if [ "$last_time" != "$new_time" ]; then
             echo "$s $last_time != $new_time Create event file: $event_file"
             touch $event_file
             send_mail ${s_name} "触发nginx reload事件. 与本次的更新记录${new_time}不一致,模拟访问本地域名证书有效期${last_time}"
             sleep 1
          fi
      fi
  done
}

# check
#end_date=$(echo |\
#         openssl s_client -host cmserver.zizi.com -port 443 -showcerts </dev/null 2>/dev/null |\
#         openssl x509  -noout -dates |\
#         sed -n 's@notAfter=@@p')
# 主逻辑函数,本地证书小于min_days天触发
ssl_check_time() {
    echo "Monitor the certificates ..."
    ssl_pull_all
    ssl_old_backup
    echo "Local certificate validation ..."
    echo 'Remote certificate validation ...'
    > $old_info
    for s in ${server_name}; do
        read -u5
        {
            if [ ! -e ${ssl_path}/${s}/  -o ! -f ${ssl_path}/${s}/fullchain.cer -o  ! -f ${ssl_path}/${s}/${s}.key ]; then
                rsync -az --delete --timeout=100 ${tmp_path}/$s/ ${ssl_path}/$s
                send_mail $s "证书文件不存在,已同步"
                end_date=$(openssl x509  -noout -dates -in ${ssl_path}/${s}/${ssl_pem} | sed -n 's@notAfter=@@p')
                last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
                echo "$s,$lasttime" >>$old_info
            else
                end_date=$(openssl x509  -noout -dates -in ${ssl_path}/${s}/${ssl_pem} | sed -n 's@notAfter=@@p')
                last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
                #echo "$s,$last_time" >>/data/backup/ssl_old_info
                local_ssl_CN=$(openssl x509  -noout -subject -in ${ssl_path}/${s}/${ssl_pem} |sed -r 's@subject= /CN=(.*)@\1@g')
                if ! echo ${local_ssl_CN} | grep "$s" &>/dev/null ; then
                    ssl_old_backup "true" "$s"
                    rsync -az --delete --timeout=100 ${tmp_path}/$s/ ${ssl_path}/$s
                    send_mail $s "证书文件不在该域名中,已替换"
                fi
                if [ $last_time -le $min_days  ]; then
                    ssl_copy_one $s "证书文件更新,当前剩余$last_time天"
                fi
                end_date=$(openssl x509  -noout -dates -in ${ssl_path}/${s}/${ssl_pem} | sed -n 's@notAfter=@@p')
                #echo $s $end_date
                last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
                echo "$s,$last_time" >>$old_info
            fi

            echo >&5
            echo ... ${ssl_path}/${s}/ certificate is fished
        }&
    done
    wait
    #load
}

#ssl_pull
main() {
    ssl_check_first
    Multi_process_init $max_process
    ssl_check_time
    rm -fr $tmp_path
    ssl_old_del
    exec 5>&-;exec 5<&-
    ssl_check_second
    ng_reload
    if  `grep 'nss-config-dir' /etc/mail.rc`; then
        [ `wc -l $email_file|awk '{print $1}'` != "0"  ] && cat $email_file|mail -s "【证书更新】$host_ip" [email protected]
    fi
    [ `wc -l $email_file|awk '{print $1}'` != "0"  ] &&  dingding "$tokens" "$(echo -e "$(cat $email_file)")"
    rm -f $email_file
}
#create_expect
#main

#------------
usage(){
cat <<EOF
Usage: $0 [MODE] [OPTION]

OPTION:
  -s, --show               default show dir of old ssl, show ssl information you can exec ssl -s or https -s
  -r, --rollback <date>    rollback  dir, before you cmd -s.
  -d, --days <num>         unit day.
  --version                show version
  --help                   show help
MODE:
  ssl, https
EOF
}

getopt -T &>/dev/null;[ $? -ne 4 ] && { echo "not enhanced version";exit 1; }

parameters=$(getopt -o r:d:s --long rollback:,days:,show,help,version -n "$0" -- "$@")
[ $? -ne 0 ] && { echo "Try '$0 --help' for more information."; exit 1; }

eval set -- "$parameters"

while true; do
    case "$1" in
    -s|--show) Type="show"; shift ;;
    -r|--rollback) Type="rollback" RollFile=$2 ; shift 2 ;;
    -d|--days) Type="days" Days_to_empty=$2 ; shift 2 ;;
    --version) echo "$0 version V0.1"; exit ;;
    --help) usage;exit ;;
    --)
        shift
        [ "$1" = "" -o "$1" = "ssl" -o "$1" = "https"  ] && MODE=$1 ||  {
        echo -e "$0: invalid floating point argument: $@\nTry '$0 --help' for more information." ; exit 1; }
        [ "$#" -ge 2 ] && {
        echo -e "$0: invalid floating point argument: $@\nTry '$0 --help' for more information." ; exit 1; }
        FIRST=$1
        SECOND=$2
        LAST=$3
        break ;;
    *) echo "0k"  ;;
    esac
done

#echo $parameters
case "$Type" in
"")
   [ -n "$MODE" ] &&  { echo -e "$0: invalid floating point argument: $@\nTry '$0 --help' for more information." ; exit 1; }
   create_expect
   main
   rm -f ssl_monitor.exp
   echo "exec main" ;;
show)
   if [ -n "$MODE" ]; then
       echo "Before the change ssl information"
       [ -f $old_info ] && cat $old_info || echo "cannot access $old_info: No such file"
   else
       [ -d $backup_path ] && ls -l $backup_path | less || echo "cannot access $backup_path: No such directory"
   fi
   ;;
rollback)
   [ -n "$MODE" ] &&  { echo -e "$0: invalid floating point argument: $@\nTry '$0 --help' for more information." ; exit 1; }
   [ ! -d $backup_path/zhengshu.$RollFile ] && { echo "cannot access $backup_path/zhengshu.$RollFile: No such directory"; exit 1; }
   echo $RollFile
   ;;
days)
   #[ -n "$MODE" ] &&  { echo -e "$0: invalid floating point argument: $@\nTry '$0 --help' for more information." ; exit 1; }
   [  $(echo ${Days_to_empty} |grep '^[^[:digit:]]*$')  ] && { echo -e "$0: invalid floating point argument: $@\nTry '$0 --help' for more information." ; exit 1; }
   min_days=${Days_to_empty}
   create_expect
   main
   rm -f ssl_monitor.exp
   echo "exec main"
   ;;
*) echo "no" ;;
esac

04-使用Python检查域名证书剩余时间

通过Python检查设定好的域名,通过域名的访问去检查证书的剩余时间,并且发送邮件通知

# !/usr/bin/env python
# -*- coding:utf-8 -*-
#

import commands

import smtplib
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header

def send_email(to_addr, cc_addr, subject, content):
    # to_addr = ['[email protected]']  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱
    # cc_addr = ['xcwhxxe'] # 抄送
    # subject = 'Python SMTP 邮件测试' # 主题

    mail_host = "smtp.cici.com"  # 设置服务器
    mail_user = "[email protected]"  # 用户名
    mail_pass = "LZ"  # 口令
    sender = 'scans'

    msgRoot = MIMEMultipart('related')
    msgRoot['From'] = Header(mail_user, 'utf-8')
    msgRoot['To'] = Header(",".join(to_addr), 'utf-8')
    #msgRoot['Cc'] = Header(",".join(cc_addr)) # 抄送
    msgRoot['Subject'] = Header(subject, 'utf-8')

    if is_html:
        msgAlternative = MIMEMultipart('alternative')
        msgRoot.attach(msgAlternative)

        mail_msg ='<h2>图示:</h2>'
        msgAlternative.attach(MIMEText(content+mail_msg, 'html', 'utf-8'))
    else:
        msg_content = MIMEText(content, 'plain', 'utf-8')
        msgRoot.attach(msg_content)

    try:
        smtpObj = smtplib.SMTP_SSL()
        smtpObj.connect(mail_host, 994)  # 25 为 SMTP 端口号
        smtpObj.login(mail_user, mail_pass)
        smtpObj.sendmail(mail_user, to_addr, msgRoot.as_string())
        #smtpObj.sendmail(mail_user, to_addr+cc_addr, msgRoot.as_string())
        print("邮件发送成功")
    except smtplib.SMTPException:
        print("Error: 无法发送邮件")


dict = {'xy_hd1':['emc.cici.net'], \
        'xy_hd2':['hd2-1.btrade.cici.com'], \
        'zhiyunshan':['hd2-1.zysmul.zizi.com','hb2-1.zysmul.zizi.com']
        }

f = open('~/python/domain_info.txt', 'w')

for machine_room in dict.keys():
    for domain in dict[machine_room]:
        print "========%s"%domain
        cmd = "echo | openssl s_client -connect %s:443 2>/dev/null | openssl x509 -noout -dates | awk -F= '{print $2}'|tail -n 1" %domain
        cer_time = commands.getoutput(cmd)
        print cer_time
        cmd1 = "date -d '%s' +%%s" %cer_time
        end_time = commands.getoutput(cmd1)
        print end_time
        now_time = commands.getoutput('date +%s')
        time = (int(end_time) - int(now_time))/24/3600
        print "time = %s"%time
        info = "机房:%s 域名:%s 剩余天数%s\n" %(machine_room,domain,time)
        f.write(info)
f.close()

send_cmd = "mail -s '域名证书检查报告' [email protected] < domain_info.txt"
send_mail = commands.getoutput(send_cmd)

f1 = open("domain_info.txt","r")
content = "".join(f1.readlines())
f1.close()
to_addr = ['[email protected]']
cc_addr = ['[email protected]']
subject = '域名证书检查报告'  # 主题
is_html = False
send_email(to_addr,cc_addr,subject,content)

05-自动扫描自动监控

邮件

# 创建证书目录
mkdir -p /root/.certs/
# 获取证书内容(可分开执行查看过程)
echo -n | openssl s_client -connect smtp.exmail.qq.com:465 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ~/.certs/qq.crt
# 添加一个证书到证书数据库中
certutil -A -n "GeoTrust SSL CA" -t "C,," -d ~/.certs -i ~/.certs/qq.crt 
certutil -A -n "GeoTrust Global CA" -t "C,," -d ~/.certs -i ~/.certs/qq.crt 
# 列出目录下证书
certutil -L -d /root/.certs
# 指明受新人证书、防报错
cd /root/.certs/
certutil -A -n "GeoTrust SSL CA - G3" -t "Pu,Pu,Pu" -d ./ -i qq.crt

# 编辑mail的配置文件
cat >>/etc/mail.rc<< \EOF
set bsdcompat
set [email protected]
set smtp=smtps://smtp.exmail.qq.com:465
set [email protected]
set smtp-auth-password='NHFNpVxQrshes'
set smtp-auth=login
set ssl-verify=ignore
set nss-config-dir=/root/.certs
EOF

echo "aaa" |mail -s "title" -a  /etc/fstab [email protected]

依赖

# 安装py3
yum -y install python36 python36-devel
python3.6 -m venv /opt/py3 
source /opt/py3/bin/activate

cat << EOF >/data/ops/requirements.txt 
requests == 2.24.0
aliyun-python-sdk-alidns == 2.6.18
aliyun-python-sdk-core  == 2.13.25
EOF

pip install -r /data/ops/requirements.txt

配置文件

cat <<EOF > /data/ops/qsdnskeyconfig.conf 
# login aliyun rest api info
# slb type: nginx/none(dubbo)/slb
# if slb_type = slb which be need
[aliyun_u_p]
aki = LTAIF
aks = GRlA0T
domains = cici.com
#mail config
mail_host = 
mail_user = 
user = 'scans'
mail_pass = 
#config more mail, plsae use ',' split
[email protected]
[email protected]
#dingdingconfig
dindin_token=14896a600456c3c661bcc69
EOF

执行文件

cat <<\EOF >start_check_ssl.sh 
#!/usr/bin/env bash
tokens="14896a600bcc69"
#--------------------
#dingding push
function dingding() {
local access_tokens=$1
local ding_url='https://oapi.dingtalk.com/robot/send'
local ding_header='Content-Type: application/json'
local msg=$2
for key in $access_tokens; do
curl -s -connect-timeout 40 -XPOST -H "$ding_header" "$ding_url?access_token=$key" \
-d '{"msgtype": "text",
    "text": {
        "content": "【k8s报警】 \n'"$msg"'"
    },
"at": {
      "atMobiles": [],
      "isAtAll": true
  }
}'
done
}
/opt/py3/bin/python /data/ops/check_ssl.py
if [ `wc -l dns.log |awk '{print $1}'` -gt 0 ] ; then
    conect=`cat dns.log|sed '1d'|awk '{print $1}' | grep -v 'CNAME'|sort -t '|' -nr -k1,1`
    conect="${conect}\\n `cat dns.log|sed '1d'|awk '{print $1}' | grep  'CNAME'|sort -t '|' -nr -k1,1`"
    dingding "$tokens" "$(echo -e "域名证书小于28天有效期如下:\\n ${conect}")\\n"
    echo -e "域名证书小于28天有效期如下:\\n ${conect}"  |mail -s "证书检测" -a /data/ops/dns.log  [email protected]
fi 
EOF

脚本内容

cat <<\EOF > check_ssl.py 
#coding=utf-8
#!/usr/bin/env python
#基础内置模块
import json
import ssl
import sys
import configparser
import telnetlib
import time
import datetime
import os
import math
import requests
#邮箱模块
from email.mime.text import MIMEText
from email.utils import formatdate
from email.header import Header
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
#导入阿里模块
from aliyunsdkcore.client import AcsClient
from aliyunsdkalidns.request.v20150109.DescribeDomainRecordsRequest import DescribeDomainRecordsRequest
#设置全局每次请求阿里云不使用证书
ssl._create_default_https_context = ssl._create_unverified_context
'''
配置文件中读取阿里验证需要的key信息
'''
class ali_operation():
    '''
    配置文件中读取阿里验证需要的key信息
    '''
    def dnsconfig(self):
        cf = configparser.ConfigParser()
        cf.read("qsdnskeyconfig.conf")
        secs = cf.sections()
        #print(secs)
        if secs.count('aliyun_u_p') != 1:
           sys.exit(' Error: need a aliyun_u_p config')
        ali_auth={'aki':cf.get('aliyun_u_p','aki'),'aks':cf.get('aliyun_u_p','aks'),
                  'domains':cf.get('aliyun_u_p','domains'),
                  'mail_host':cf.get('aliyun_u_p','mail_host'),
                  'mail_user':cf.get('aliyun_u_p','mail_user'),
                  'user':cf.get('aliyun_u_p','user'),
                  'mail_pass':cf.get('aliyun_u_p','mail_pass'),
                  'to_addr':cf.get('aliyun_u_p','to_addr'),
                  'cc_addr':cf.get('aliyun_u_p','cc_addr'),
                  'dindin_token':cf.get('aliyun_u_p','dindin_token'),
                  }
        return ali_auth
    '''
    对阿里接口发起请求
    '''
    def connect_retry(self,aki,aks,request, print_def):
        client =  AcsClient(aki,aks)
        for i in range(3):
            try:
                responese = client.do_action_with_exception(request)
            except Exception as e:
                print('--Error ' + print_def + ' error, error info: \n'+' '*8+'%s'%(e))
                if i == 2:
                    return(1)
                else:
                    continue
            return responese
    #域名查询接口参数配置
    def dns_query(self,domainname,pagenum):
        request = DescribeDomainRecordsRequest()
        #request.set_action_name('DescribeDomainRecords')
        request.set_DomainName(domainname)
        if pagenum >= 1:
           request.set_PageNumber(pagenum)
        return  request
    #分页获取二级域名
    def  dnslist(self,pagenum,domainnames):
        ali_key = self.dnsconfig()
        if pagenum >= 1:
            dnslists = self.dns_query(domainnames,pagenum=pagenum)
        else:
            dnslists = self.dns_query(domainnames,pagenum=0)

        info = self.connect_retry(ali_key['aki'],ali_key['aks'],dnslists,'dns')
        return info
    #域名数据处理,默认是bite数据需要转换为unicode
    def dataprocess(self,pagenum,domainname):
        dnsdata = json.loads(self.dnslist(pagenum,domainname).decode('utf-8'))
        return  dnsdata
    #域名做判断,过滤调'@'和'*'的二级域名
    def dnsifprocess(self,dnsdata):
        subdomainnames=[]
        for i in dnsdata['DomainRecords']['Record']:
            if i['RR'] == "@":
                continue
                subdomainname=i['Value']+'|'+i['DomainName']+'|'+i['Type']
            elif '*' in i['RR']:
                continue
            elif 'TXT' in i['Type']:
                continue
            else:
                subdomainname=i['Value']+'|'+i['RR']+'.'+i['DomainName']+'|'+i['Type']
            subdomainnames.append(subdomainname)
        return  subdomainnames
    #汇总所有二级域名
    def dnslists(self):
        domains = self.dnsconfig()['domains']
        allsubdomainnames=[]
        for domainname in domains.split(','):
            print(domainname)
            domainname = domainname
            pagenum=1
            dnsdata = self.dataprocess(pagenum,domainname)
            pagenum=math.ceil(dnsdata['TotalCount']/dnsdata['PageSize'])
            if pagenum >= 1:
                for c in range(pagenum):
                    c += 1
                    dnsdata =  self.dataprocess(c,domainname)
                    allsubdomainnames.append(self.dnsifprocess(dnsdata))
        #print(allsubdomainnames)
        return  allsubdomainnames

class TelnetClient():
    '''
        telnet 模块
    '''
    def __init__(self,):
        self.tn = telnetlib.Telnet()

    # 此函数实现telnet登录主机
    def login_host(self,host_ip):
        try:
            # self.tn = telnetlib.Telnet(host_ip,port=23)
            self.tn.open(host_ip,port=443,timeout=3)
            return True
        except:
            return False
    def logout_host(self):
        self.tn.write(b"exit\n")

class https_certificate_verification():

    '''
    获取每个域名的证书时间
    '''
    #执行shell命令
    def commd(self,commnd):
        #data=subprocess.Popen(commnd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.readlines()
        data=os.popen(commnd).readlines()
        return data
    #下载证书,并且获取证书时间
    def https_download(self,domain):
        # commd =  "echo | openssl s_client -connect %s:443 2>/dev/null | openssl x509 -noout -dates | awk -F= '{print $2}'|tail -n 1" %domain
        commd =  "curl -I -v -m 1  'https://%s/' 2>&1 |sed -nr 's@.*expire date: @@p'" %domain
        return  self.commd(commd)
    #将证书的GMT时间字符串转换为时间格式
    def string_toDatetime(self,st):
        return  datetime.datetime.strptime(st, "%b %d  %H:%M:%S %Y GMT")
    #获取证书有效期剩余天数
    def https_ca_verification(self,domain):
        dm=domain.split('|')[1]
        cur_time = self.https_download(dm)
        if cur_time:
            cur_time="%s" % (cur_time[0].strip('\n'))
            cur_time=self.string_toDatetime(cur_time)
            ca_time=time.mktime(cur_time.timetuple())
            nowtime=time.time()
            time1 = (int(ca_time+28800) - int(nowtime))/24/3600
            return "%s,%s,%d" % (domain,True,math.floor(time1))
        else:
            return  "%s,%s" % (domain,False)
    #域名拆分
    def domanin_split(self,domain):
        domain_split=domain.split('.')
        if len(domain_split) > 2:
            return '.'.join(domain_split[1:])
        else:
            return domain

    #确认证书是否和域名相匹配
    def ca_is_match(self,domain):
        commd = "echo | openssl s_client -connect  %s:443  2>/dev/null | openssl x509 -noout   -text | grep -i 'DNS' | sed 's/[ ]*//g'" % domain
        ca_support_domain=self.commd(commd)
        ca_support_domain = ca_support_domain[0].replace('DNS:','').replace('*.','').strip('\n')
        domain_split=self.domanin_split(domain)
        if domain_split in ca_support_domain.split(','):
            return True
        else:
            return  False


class call():
    def __init__(self):
        self.confg = ali_operation().dnsconfig()
    '''
        通知模块、邮箱、钉钉
    '''
    def dindin(self,msg):
       '''
        只是把钉钉的接口封装了下,目的大家别重复造轮子
        url请求的模块是requests
        调用函数方式 dindin(token=token,msg=msg)
        token是钉钉机器人的token
        msg是要报警的内容
        参考文档:
        https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.43964a97im55WS&treeId=257&articleId=105735&docType=1
       '''
       token=self.confg['dindin_token']
       dindinapi = 'https://oapi.dingtalk.com/robot/send?access_token=%s' % token #token钉钉机器人认证的token
       headers = {'content-type': 'application/json','charset':'utf-8'} #http头内存,钉钉必填项
       data  = {
         "msgtype": "text",
         "text": {
             "content": msg
         },
         "at": {
             "atMobiles": [
             ],
             "isAtAll": True
         }
       }
       try:
        req=requests.post(dindinapi,json=data,headers=headers)
       except:
           return  0
    def send_email(self,subject,content):
      mail_host=self.confg['mail_host']
      #mail_host='smtp.yunzongnet.com'
      mail_user = self.confg["mail_user"]
      user = self.confg['user']
      mail_pass = self.confg["mail_pass"]
      to_addr=self.confg['to_addr'].split(',')
      cc_addr=self.confg['cc_addr'].split(',')
      print (to_addr,cc_addr)
      msg = MIMEMultipart()
      msg['From'] = mail_user
      msg['To'] = ','.join(to_addr)
      if cc_addr:
        msg['Cc'] = ','.join(cc_addr)
      msg['Subject'] = subject
      msg.attach(MIMEText(content, 'plain'))
      smtp = smtplib.SMTP_SSL(mail_host, 994)
      smtp.ehlo()
      smtp.login(mail_user, mail_pass)
      smtp.sendmail(mail_user,to_addr+cc_addr,msg.as_string())

if __name__ == '__main__':
    cm='>dns.log'
    i=os.popen(cm)
    validlist=[]
    normal_list=[]
    d_err_list=[]
    domainlist = ali_operation().dnslists()
    for i in domainlist:
        for j in i:
            if TelnetClient().login_host(j.split('|')[1]):
                #TelnetClient().logout_host()
                j='%s' % j
                f = https_certificate_verification().https_ca_verification(j)
                if "False" in f:
                    validlist.append(f)
                elif "True" in f:
                    normal_list.append(f)
            else:
                d_err_list.append(j)
                continue
    ca_normalmsg=''
    ca_warmsg=''
    ca_crimsg=''
    no_camsg=''
    not_is_match_ca=''
    for i in normal_list:
        if i:
            domain=i.split(',')[0]
            ca_time=int(i.split(',')[2])
            # if https_certificate_verification().ca_is_match(domain):
            #     pass
            # else:
            #    not_is_match_ca += '%s' % domain +'\n'
            if ca_time > 29:
                ca_normalmsg+='%s %d 天' % (domain,ca_time) +'\n'
            elif ca_time <= 28 and ca_time>=1 :
                ca_warmsg+='%s %d 天' % (domain,ca_time) +'\n'
            elif ca_time < 1 :
                ca_crimsg+='%s %d 天' % (domain,ca_time) +'\n'
    if validlist:
        for i in validlist:
            no_camsg+='%s' % i.split(',')[0] +'\n'
        no_camsg='未获取到证书域名如下:'+'\n'+'%s'  %  no_camsg
        #call().dindin(no_camsg)
    if ca_normalmsg:
        ca_normalmsg='域名证书正常有效期如下:'+'\n'+'%s'%  ca_normalmsg
        #call().dindin(ca_normalmsg)
        # call().send_email('域名证书有效期报警',ca_normalmsg)
    if ca_warmsg:
        ca_warmsg='域名证书小于28天有效期如下:'+'\n'+'%s'  %  ca_warmsg
        #call().dindin(ca_warmsg)
        with open('dns.log', mode='w+',encoding='utf-8') as f:
            f.seek(0)
            f.writelines(ca_warmsg)
        # call().send_email('证书小于28天有效期域名',ca_normalmsg)
    if ca_crimsg:
        ca_crimsg='证书已过期域名如下:'+'\n'+'%s' %  ca_crimsg
        #call().dindin(ca_crimsg)
        # call().send_email('证书已过期域名',ca_normalmsg)
EOF

06-云厂商产品证书自动更新

SLB证书自动更新

环境 python3 pip包

cat >request.txt<<EOF
aliyun-python-sdk-core-v3==2.13.11
aliyun-python-sdk-slb==3.2.16
requests==2.22.0
EOF
pip install -r request.txt

计划任务

# update xiaoqian ssl
0 0 * * * /root/.pyenv/versions/py367/bin/python  /home/scripts/py/xiaoqian-slb.py &>/dev/null

脚本内容

#!/usr/bin/env python
#coding=utf-8
import json
import requests
import datetime
import subprocess
import sys
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkslb.request.v20140515.UploadServerCertificateRequest import UploadServerCertificateRequest
from aliyunsdkslb.request.v20140515.SetLoadBalancerHTTPSListenerAttributeRequest import SetLoadBalancerHTTPSListenerAttributeRequest
from aliyunsdkslb.request.v20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest import DescribeLoadBalancerHTTPSListenerAttributeRequest
from aliyunsdkslb.request.v20140515.DeleteServerCertificateRequest import DeleteServerCertificateRequest

'''Judge the validity of the certificate'''
def CheckDomain(domain):
    cmd = "echo | openssl s_client -connect %s:443 2>/dev/null | openssl x509 -noout -dates | awk -F= '{print $2}'|tail -n 1" %domain
    cer_time = subprocess.getoutput(cmd)
    cmd1 = "date -d '%s' +%%s" %cer_time
    end_time, now_time = subprocess.getoutput(cmd1), subprocess.getoutput('date +%s')
    time = int((int(end_time) - int(now_time))/24/3600)
    info = "机房:%s 域名:%s 剩余天数%s\n" %('社餐华东1',domain,time)
    print(info)
    return time

def GetNewCertInfo(domain):
    cmd =r'''
    cat >tmp.sh <<\EOF
#!/usr/bin/env bash
export LANG=en_US.UTF8
[ -z "$(which rsync)" ] && yum install -y rsync
[ -z "$(which jq)" ] && wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo && yum install -y jq
export RSYNC_PASSWORD=wowonetwork
tmpfile=$(mktemp -u zhengshu.XXXX)
tmp_path=/tmp/.${tmpfile}
now_seconds=$(date '+%s')
ssl_pem=fullchain.cer

domain=$1
[ -z "$domain" ] && echo "err, please: .sh domain" && exit 1
[ ! -e  $tmp_path ] && mkdir -p $tmp_path
rsync -r [email protected]::zhengshu/$domain/ $tmp_path/$domain
cert_info=$(cat $tmp_path/$domain/${ssl_pem}|grep -vE '^$'|sed ':a;N;$!ba;s/\n/\\n/g')
key_info=$(cat $tmp_path/$domain/${domain}.key|grep -vE '^$'|sed ':a;N;$!ba;s/\n/\\n/g')

end_date=$(openssl x509 -noout -dates -in $tmp_path/$domain/${ssl_pem} | sed -n 's@notAfter=@@p')
last_time=$((($(date '+%s' --date "$end_date") - $now_seconds)/24/3600))
rm -fr $tmp_path
[ $last_time -lt 30 ] && echo "" && exit 0
echo  '{"cert_info":"'$cert_info'","key_info":"'$key_info'"}'
EOF
    '''
    subprocess.getoutput(cmd)
    cmd1=r'bash tmp.sh %s|jq .cert_info' %(domain)
    cmd2=r'bash tmp.sh %s|jq .key_info && rm -f tmp.sh' %(domain)
    cert_info, key_info =subprocess.getoutput(cmd1), subprocess.getoutput(cmd2)
    return {"cert_info":cert_info, "key_info":key_info}

'''upload a new certificate''' 
def UpLoadCert(cert_info,key_info,now=None):
    request = UploadServerCertificateRequest()
    request.set_accept_format('json')
    request.set_ServerCertificate(cert_info)
    request.set_PrivateKey(key_info)
    request.set_ServerCertificateName("xq"+now)

    response = client.do_action_with_exception(request)
    print(str(response, encoding='utf-8'))
    return str(response, encoding='utf-8')

'''delete  certificate'''
def DelCert(id):
    request = DeleteServerCertificateRequest()
    request.set_accept_format('json')
    print(id)
    request.set_ServerCertificateId(id)

    response = client.do_action_with_exception(request)
    print(str(response, encoding='utf-8'))
    return str(response, encoding='utf-8')

'''get an old certificate id'''
def DesSlbhttps(slb_id):
    request = DescribeLoadBalancerHTTPSListenerAttributeRequest()
    request.set_accept_format('json')

    request.set_ListenerPort(443)
    request.set_LoadBalancerId(slb_id)

    response = client.do_action_with_exception(request)
    print(str(response, encoding='utf-8'))
    return str(response, encoding='utf-8')

'''replace slb certificate'''
def SetSlbHttps(slb_id,cert_id):
    request = SetLoadBalancerHTTPSListenerAttributeRequest()
    request.set_accept_format('json')

    request.set_ListenerPort(443)
    request.set_LoadBalancerId(slb_id)
    request.set_ServerCertificateId(cert_id)

    response = client.do_action_with_exception(request)
    print(str(response, encoding='utf-8'))
    return str(response, encoding='utf-8')

'''get aliyun RAM key'''
def GetAliRamKey():
    sw_url = "http://10.200.41.37:10435/api/v1/sw_decode_passwd"
    headers = {'content-type': 'application/json'}
    data = {"pass_key":"bbf79631a4de0836c1b2b97887359a50"}
    parameters = {"url": sw_url,"headers": headers,"timeout":10,"data": json.dumps(data)}
    res_discache = requests.post(**parameters)
    res_discache = json.loads(res_discache.text)
    return res_discache["username"], res_discache["passwd"],

'''push dingding'''
def dingding(token,msg):
    try:
        dindinapi = 'https://oapi.dingtalk.com/robot/send?access_token=%s' % token
        headers = {'content-type': 'application/json','charset':'utf-8'}
        data  = {
         "msgtype": "text",
         "text": {
         "content": msg
         },
         "at": {
         "atMobiles": [
         ],
         "isAtAll": True
         }
        }
        req=requests.post(dindinapi,json=data,headers=headers)
    except Exception as e:
        print(str(e))

if __name__ == '__main__':
    now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
    username,passwd = GetAliRamKey()
    client = AcsClient(username, passwd, 'cn-hangzhou')
    slb_id ='lb-bp10x2hqas6pv5cfliiab'
    domain ='xqapic.zizi.com'
    domain1 = 'cici..com'
    token = 'a6184389010ffc418aa13057c'

    #1. Judge the validity of the certificate, 27day remaining
    days=CheckDomain(domain)
    if days < 28:
        print("the certificate will be expired, continue...")
    else:
        try:  
            sys.exit(0)  
        except:  
            print('the certificate is not expired') 
            sys.exit(0)

    #2. upload a new certificate
    #2.1 get a new certificate
    # cert_info,key_info = GetNewCertInfo(domain1)
    # 此处为了解决字符串加r问题。如r"a\n" 变成"a\n",使用"%s"%r"a\n".replace('\\n',"\n")
    cert_info = "%s"% GetNewCertInfo(domain1)["cert_info"].replace('\\n',"\n").replace('"',"")
    key_info =  "%s"% GetNewCertInfo(domain1)["key_info"].replace('\\n',"\n").replace('"',"")
    print(cert_info)
    print(key_info)
    # cert_info_origin="<CERTIFICATE>" 
    # key_info_origin="<PRIVATE KEY>"

    #2.2 upload a new certificate
    upload_cert_info=json.loads(UpLoadCert(cert_info,key_info,now))
    cert_name,cert_id=upload_cert_info['ServerCertificateName'],upload_cert_info['ServerCertificateId']
    #print(cert_name,cert_id)

    #3. replace slb certificate
    #3.1 get an old certificate id
    old_cert_id=json.loads(DesSlbhttps(slb_id))['ServerCertificateId']
    # print(old_cert_id)
    #3.2 replace slb certificate
    SetSlbHttps(slb_id,cert_id)
    #3.3 delete old certificate
    DelCert(old_cert_id)

    #4. dingding
    newdays=CheckDomain(domain)
    msg = '证书更新通知:\n机房:%s, 小千SLB: %s 域名:%s,  新证书有效期:%s天, 证书名:%s' % ('1',slb_id, domain, newdays, cert_name)
    print(msg)
    dingding(token,msg)

07-检查-nginx有哪些443域名

需求: 统计所有证书情况,区分域名不在116.213.103段的证书。

统计结果格式:配置文件位置,域名,证书名

cat  >check_ssl.sh << \EOF
#!/bin/bash
ng_p=$(ps -ef|grep nginx|grep master |awk '{print $2}')
[  -z "$ng_p" ] && exit 1
ng_dir=$(ls -l /proc/${ng_p}/exe| awk '{print $11}' | awk -F'/sbin/nginx' '{print $1}')
#/usr/local/nginx/sbin/nginx -V 2>&1 |sed -rn '/conf-path/s@.*conf-path=([[:graph:]]{1,}).*@\1@gp'
#如果没有,则编译用手默认--prefix目录下的conf
ng_main_conf=$(echo ${ng_dir}/conf/nginx.conf)
ng_main_path=$(echo ${ng_main_conf%/nginx.conf*})
ng_conf=$(grep '^[^#][[:space:]\t]\+include' ${ng_main_conf}|grep -v mime.types|awk -F'[\t ;]+' '{print $3}')
ng_conf_dir=$(echo ${ng_conf%/*})

cd ${ng_main_path}/${ng_conf_dir}
for f in *.conf; do
        if grep -A 11 '[^#]listen.*443' $f &>/dev/null; then
        for server_name in $(grep -A 11 -w '[^#]listen.*443' $f  | grep -EA 11 '^[^#][[:space:]\t]+listen.*443' | grep -E '^[^#][[:space:]\t]+server_name' | awk -F'server_name[ ]+' '{print $2}' |awk -F';' '{print $1}'); do
                #server_name=$(grep -A 4 '[^#]listen.*443' $f | grep -E 'server_name' | awk -F'[ ;]+' '{print $3}')
                pc_ip=$(ping  -c1 -W 1 $server_name | grep -o '\([0-9]\{1,3\}\.\)\{3\}' 2>/dev/null)

                if [ "210.14.157." == "$pc_ip" ]; then
                    key=$(grep -A 11 '^[^#][[:space:]\t]\+listen.*443' $f|grep -A 11 -E '^[^#][[:space:]\t]+server_name'| grep -A 9   -E "${server_name}" | grep -E "^[^#][[:space:]\t]+ssl_certificate_key"| awk -F'[ ;]+' /ssl/'{print "key="$(NF-1)}')
                    echo  "HLGconf=$f,server_name=${server_name},$key"
                else
                    key=$(grep -A 11 '^[^#][[:space:]\t]\+listen.*443' $f| grep -A 11 -E '^[^#][[:space:]\t]+server_name'|grep -A 9  -E "${server_name}" | grep -E "^[^#][[:space:]\t]+ssl_certificate_key" | awk -F'[ ;]+' /ssl/'{print "key="$(NF-1)}')
                    echo  "otherconf=$f,server_name=${server_name},$key"

                fi
        done
        fi
done
EOF

sh   check_ssl.sh  | sort |grep -v  '^key' >sc-$(hostname).csv
sed -nr 's@.*server_name=(.*),.*@\1@gp' sc-$(hostname).csv

08-ansible批量修改

role ssl

# 执行命令
ansible-playbook change_ssl.yml
# 创建基础环境
mkdir ansible/
cd ansible

cat >ansible.cfg<<EOF
[defaults]
inventory = ./hosts
forks          = 5
remote_port    = 22
roles_path    = ./roles
host_key_checking = False
timeout = 30
log_path = ./logs/ansible.log
private_key_file = /home/oap/.ssh/id_rsa
[inventory]
[privilege_escalation]
become=True
become_method=sudo
become_user=root
become_ask_pass=False
[paramiko_connection]
record_host_keys=False
[ssh_connection]
[persistent_connection]
[accelerate]
[selinux]
[colors]
[diff]
EOF

mkdir {logs,roles,files}
# 创建证书role环境
mkdir  -pv roles/cron_ssl/{defaults,files,handlers,tasks,templates}

# 生成执行yml
cat >change_ssl.yml<< \EOF
---
- name: Change cron for ssl manage
  hosts: ssl
  vars:
    - script_name: "ssl_monitor.sh"
    - script_dest_path: "/home/sh"
    - script_type: "sh"
    - parameters: "--days 26"
    #- state: "absent"
    - state: "present"
    - {minute: "0", hour: "23", day: "*", month: "*", weekday: "*"}

  roles:
    - cron_ssl
EOF

# 生成证书环境变量
cat >roles/cron_ssl/defaults/main.yml<<\EOF
ssl_script: "ssl_monitor.sh"
script_name: ""
script_source_path: ""
script_dest_path: "/home/sh"
script_type: "sh"
state: "present"

user: "root"
minute: "*"
hour: "*"
day: "*"
month: "*"
weekday: "*"
EOF

# 生成证书任务
cat >roles/cron_ssl/tasks/main.yml<< EOF
---
- include: cron.yml
EOF

cat >roles/cron_ssl/tasks/cron.yml<< \EOF
---
- name: Copy {{ script_name }} to
  copy: src={{ item.src }} dest={{ item.dest }}
  with_items:
    - { src: "./files/{{ script_name  }}", dest: "{{ script_dest_path }}/{{ script_name }}" }

- name: Backup {{ script_name  }}
  shell: "cp {{ script_dest_path }}/{{ script_name }} /data/backup/{{ script_name }}.$(date +'%F-%H')"
  ignore_errors: yes

- name: Cat cron
  shell: crontab -l
  register: cat_cron
  ignore_errors: yes

- name: Cat cron
  debug: msg="{{ cat_cron.stdout_lines }}"

- name: Del cron that which is "{{ script_name }}"
  cron: name={{ script_name }} state="absent"
  when: state  == "absent"

- name: Del notcron that which is "{{ script_name }}"
  shell: "[ $(crontab -l |grep {{ script_name }}|wc -l) != $(crontab -l |grep -A 2 Ansible| grep {{ script_name }}|wc -l) ] && sed -ri '/ssl_monitor.sh/d' /var/spool/cron/root"
  ignore_errors: yes
  when: state  == "present"

- name: Cron bash "{{ script_name }}" to "{{ state }}"
  cron: name={{ script_name }} minute={{ minute }} hour={{ hour }} day={{ day }} month={{ month }} weekday={{ weekday }} job='/bin/bash {{ script_dest_path }}/{{ script_name }} {{ parameters }} &>/dev/null'
  when: script_type == "sh" and state == "present"

- name: Cat cron
  shell: crontab -l
  register: cat_cron

- name: Cat cron
  debug: msg="{{ cat_cron.stdout_lines }}"
EOF

09-客户私有化部署

目标:

  1. 自动申请证书
  2. 证书管理,到期天数

计划:

使用Let's Encrypt 免费泛域名证书 [https://letsencrypt.org/](https://letsencrypt.org/)

  1. 自动申请证书

1.0 安装rsync 服务端

服务器地址后面证书自动更新脚本要用到。

# 安装rsync服务
yum -y install rsync
systemctl enable rsyncd
systemctl start rsyncd

# 修改配置
cat >/etc/rsyncd.conf <<\EOF
pid file = /var/run/rsyncd.pid
port = 873
address = 0.0.0.0
uid = root
gid = root
use chroot = yes
read only = no
write only = no
#host allow = 172.16.1.0/24
#hosts deny = 0.0.0.0/32
max connections = 10
list = false
log file = /var/log/rsyncd.log
transfer logging = yes
log format = %t %a %m %f %b
syslog facility = local3
timeout = 300
[zhengshu]
comment = "certificate dir"
path=/data/zhengshu
list=yes
auth users = sa
secrets file = /etc/rsyncd.secrets
EOF

# 认证文件
cat >/etc/rsyncd.secrets <<\EOF
sa:helloworld
EOF

chmod 600 /etc/rsyncd.secrets
systemctl restart rsyncd

1.1 安装客户端

curl  https://get.acme.sh | sh
alias acme.sh=~/.acme.sh/acme.sh
echo 'alias acme.sh=~/.acme.sh/acme.sh' >>/etc/profile

自动为你创建 cronjob, 每天 0:00 点自动检测所有的证书, 如果快过期了, 需要更新, 则会自动更新证书.

1.2 申请阿里云证书-dns方式

定义全局变量

# 定义全局变量
export Ali_Key="LTAI4Fqxb" 
export Ali_Secret="whTtVJmhiEjg"
# 申请证书
~/.acme.sh/acme.sh --issue --dnssleep 10 --dns dns_ali -d qxhcloud.com -d "*.qxd.com" 
# 等待...
# 查看证书具体内容
openssl x509 -in /root/.acme.sh/qxhcloud.com/fullchain.cer  -noout -text

1.3 上传自动申请证书脚本并配置定时任务

脚本update_cer.sh上传到/home/script/目录下。

mkdir /home/script/
cat >/home/script/update_cer.sh <<\EOF 
#!/bin/sh
###############################
#system variable
timestamp=`date +%s`
cer_dir="/root/.acme.sh/"
domain_name="
cici.com
"
renew_flag=1

###############################
#qxhcloud domain name info
Qxhcloud_Key () {
    export Ali_Key="LTAI3Wcxb" 
    export Ali_Secret="wmhiEjg"
}

#application certificate
App_Cer () {
    ~/.acme.sh/acme.sh --force --renew-all --dnssleep 10
}

#copy certificate to directory
Copy_Cer () {
    mkdir -p /data/zhengshu/
    /bin/cp -dR /root/.acme.sh/qxhcloud.com /data/zhengshu
}

#check certificate endtime
declare -A apply_certs
declare -A certs_days
Check_Cer () {
    for domain in ${domain_name}; do
    cd ${cer_dir}${domain}
    check_key=`/bin/openssl x509 -in fullchain.cer -noout -dates|head -n 2|awk -F= '{print $2}'|tail -n 1`
    key_time=$[(`date -d "${check_key}" +%s`-${timestamp})/24/3600]
    if [ ${key_time} -le 28 ];then
            apply_certs[${#apply_certs[*]}]="`pwd` Certificate time remaining ${key_time} day,this applying\n"
            renew_flag=0
    else
            certs_days[${#certs_days[*]}]="`pwd` ${key_time} day \n"
    fi
    done

    if [ $renew_flag -eq 0 ]; then
        Qxhcloud_Key
        App_Cer
        Copy_Cer
    fi
}


main (){
    Check_Cer
    #[ ${#apply_certs[@]} -gt 0 ] && echo -e "${apply_certs[@]}" | mail -s 'Ali Certificate Details' [email protected]
    #echo -e "${certs_days[@]}" | mail  -s 'Ali Certificate Details' [email protected]
}

main
EOF
```
```bash
# 配置定时任务
echo -e "# update certificate\n30 09 * * * /bin/sh /home/script/update_cer.sh > /dev/null" >>/var/spool/cron/root 

2 上传证书自动更新脚本并配置定时任务

2.1 上传证书自动更新脚本

脚本ssl_monitor.sh上传到/home/script/目录下,目录没有刚创建

注意:

  1. 脚本配置在nginx主机上。
  2. nginx配置证书目录必须为/usr/local/zhengshu

2.2 配置定时任务

# 配置定时任务
echo -e "# ssl_monitor 剩余28天更新\n0 23 * * * /bin/bash /home/script/ssl_monitor.sh -days 28 &>/dev/null" >>/var/spool/cron/root 

注意:

  1. 修改脚本中rsync服务端地址

3 nginx ssl配置

server {
    #listen 80;
    listen 443 ssl http2;
    server_name *.qxd.com;
    ssl                  on;
    ssl_certificate      /usr/local/zhengshu/qxd.com/fullchain.cer;
    ssl_certificate_key  /usr/local/zhengshu/qxd.com/nfin.com.key;
    ssl_ciphers                EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers  on;
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_session_cache          shared:SSL:1024m;
    ssl_session_timeout        10m;
    ssl_session_tickets        on;
    ssl_stapling               on;
    ssl_stapling_verify        on;

    ssl_trusted_certificate   /usr/local/zhengshu/qxd.com/fullchain.cer;
... ...