前言

最近在给 APISIX 配置自动更新 SSL 证书的时候,发现了一些问题,本文记录以下发现问题的过程和解决方案。

步骤

我们先来看下原始的配置方法吧:

1 安装相应脚本

1
2
3
4
5
6
7
$ curl --output /root/.acme.sh/renew-hook-update-APISIX.sh --silent https://gist.githubusercontent.com/anjia0532/9ebf8011322f43e3f5037bc2af3aeaa6/raw/65b359a4eed0ae990f9188c2afa22bacd8471652/renew-hook-update-APISIX.sh

$ chmod +x /root/.acme.sh/renew-hook-update-APISIX.sh

$ /root/.acme.sh/renew-hook-update-APISIX.sh 

Usage : /root/.acme.sh/renew-hook-update-APISIX.sh -h <APISIX admin host> -p <certificate pem file> -k <certificate private key file> -a <admin api key> -t <print debug info switch off/on,default off>

2 安装 acme.sh

1
curl https://get.acme.sh | sh -s email=[email protected]

3 申请证书,并添加renew-hook

这里我采用的是 dns api的方式申请证书的

1
 ~/.acme.sh/acme.sh --issue  --dns dns_ali     -d *.xx.com   --renew-hook '~/.acme.sh/renew-hook-update-APISIX.sh  -h http://127.0.0.1:9280 -p ~/.acme.sh/"*.xx.com_ecc"/"fullchain.cer"  -k ~/.acme.sh/"*.xx.com_ecc"/"*.xx.com.key" -a {admin-key}' --log --debug 

这里的 http://127.0.0.1:9280 是你的 APISIX 的 admin 接口地址,admin-key 是你的 key。

问题

在执行以上步骤后,我以为能顺利申请证书,并添加至 APISIX, 但在允许命令后,提示以下错误: img.png

从报错信息可以看出是 jq 解析 json 出现错误。

解决

我们先来看看原来的脚本内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env bash

# author [email protected]
# blog https://anjia0532.github.io/
# github https://github.com/anjia0532

# this script depend on jq,check it first
RED='\033[0;31m'
NC='\033[0m' # No Color

if ! [ -x "$(command -v jq)" ]; then
  echo  -e "${RED}Error: jq is not installed.${NC}" >&2
  exit 1
fi

if ! [ -x "$(command -v openssl)" ]; then
  echo  -e "${RED}Error: openssl is not installed.${NC}" >&2
  exit 1
fi

if ! [ -x "$(command -v ~/.acme.sh/acme.sh)" ]; then
  echo  -e "${RED}Error: acme.sh is not installed.(doc https://github.com/acmesh-official/acme.sh/wiki/How-to-install)${NC}" >&2
  exit 1
fi

usage () { echo "Usage : $0 -h <apisix admin host> -p <certificate pem file> -k <certificate private key file> -a <admin api key> -t <print debug info switch off/on,default off>"; }

# parse args
while getopts "h:p:k:a:t:" opts; do
   case ${opts} in
      h) HOST=${OPTARG} ;;
      p) PEM=${OPTARG} ;;
      k) KEY=${OPTARG} ;;
      a) API_KEY=${OPTARG} ;;
      t) DEBUG=${OPTARG} ;;
      *) usage; exit;;
   esac
done

# those args must be not null
if [ ! "$HOST" ] || [ ! "$PEM" ] || [ ! "$KEY" ] || [ ! "$API_KEY" ]
then
    usage
    exit 1
fi

# optional args,set default value

[ -z "$DEBUG" ] && DEBUG=off

# print vars key and value when DEBUG eq on
[[ "on" == "$DEBUG" ]] && echo -e "HOST:${HOST} API_KEY:${API_KEY} PEM FILE:${PEM} KEY FILE:${KEY} DEBUG:${DEBUG}"


# get all ssl and filter this one by sni name
cert_content=$(curl --silent --location --request GET "${HOST}/apisix/admin/ssl/" \
--header "X-API-KEY: ${API_KEY}" \
--header 'Content-Type: application/json' | jq "first(.node.nodes[]| select(.value.snis[] | contains(\"$(openssl x509 -in $PEM -noout -text|grep -oP '(?<=DNS:|IP Address:)[^,]+'|sort|head -n1)\")))")


validity_start=$(date --date="$(openssl x509 -startdate -noout -in $PEM|cut -d= -f 2)" +"%s")
validity_end=$(date --date="$(openssl x509 -enddate -noout -in $PEM|cut -d= -f 2)" +"%s")
  
# create a new ssl when it not exist
if [ -z "$cert_content" ]
then
  cert_content="{\"snis\":[],\"status\": 1}"

  # read domains from pem file by openssl
  snis=$(openssl x509 -in $PEM -noout -text|grep -oP '(?<=DNS:|IP Address:)[^,]+'|sort)
  for sni in ${snis[@]} ; do
    cert_content=$(echo $cert_content | jq ".snis += [\"$sni\"]")
  done

  cert_content=$(echo $cert_content | jq ".|.cert = \"$(cat $PEM)\"|.key = \"$(cat $KEY)\"|.validity_start=${validity_start}|.validity_end=${validity_end}")

  cert_update_result=$(curl --silent --location --request POST "${HOST}/apisix/admin/ssl/" \
  --header "X-API-KEY: ${API_KEY}" \
  --header 'Content-Type: application/json' \
  --data "$cert_content" )

  [[ "on" == "$DEBUG" ]] && echo -e "cert_content: \n${cert_content}\n\ncreate result json:\n\n${cert_update_result}"
else
  # get exist ssl id
  URI=$(echo $cert_content | jq -r ".key")
  ID=$(echo ${URI##*/})
  # get exist  ssl certificate json , modify cert and key value
  cert_content=$(echo $cert_content | jq ".value|.cert = \"$(cat $PEM)\"|.key = \"$(cat $KEY)\"|.id=\"${ID}\"|.update_time=$(date +'%s')|.validity_start=${validity_start}|.validity_end=${validity_end}")

  # update apisix ssl
  cert_update_result=$(curl --silent --location --request PUT "${HOST}/apisix/admin/ssl/${ID}" \
  --header "X-API-KEY: ${API_KEY}" \
  --header 'Content-Type: application/json' \
  --data "$cert_content" )

  [[ "on" == "$DEBUG" ]] && echo -e "cert_content: \n${cert_content}\n\nupdate result json:\n\n${cert_update_result}"
fi

exit 0

通过简单分析脚本可以看出功能是解析申请的证书,通过 APISIX admin API 添加更新证书至 APISIX。通过简单的调试可以发现是调研 APISIX admin API 时解析json响应时出现问题,通过这里我才想起来,APISIX 3.x版本后 admin API 进行了比较大的更新,接口和相应的响应不兼容2.X版本的接口,于是这里就需要通过对脚本中APISIX 相关的接口进行调整。

img_1.png

img_2.png

修改后的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env bash

# author [email protected]
# blog https://anjia0532.github.io/
# github https://github.com/anjia0532

# this script depend on jq,check it first
RED='\033[0;31m'
NC='\033[0m' # No Color

if ! [ -x "$(command -v jq)" ]; then
  echo  -e "${RED}Error: jq is not installed.${NC}" >&2
  exit 1
fi

if ! [ -x "$(command -v openssl)" ]; then
  echo  -e "${RED}Error: openssl is not installed.${NC}" >&2
  exit 1
fi

if ! [ -x "$(command -v ~/.acme.sh/acme.sh)" ]; then
  echo  -e "${RED}Error: acme.sh is not installed.(doc https://github.com/acmesh-official/acme.sh/wiki/How-to-install)${NC}" >&2
  exit 1
fi

usage () { echo "Usage : $0 -h <apisix admin host> -p <certificate pem file> -k <certificate private key file> -a <admin api key> -t <print debug info switch off/on,default off>"; }

# parse args
while getopts "h:p:k:a:t:" opts; do
   case ${opts} in
      h) HOST=${OPTARG} ;;
      p) PEM=${OPTARG} ;;
      k) KEY=${OPTARG} ;;
      a) API_KEY=${OPTARG} ;;
      t) DEBUG=${OPTARG} ;;
      *) usage; exit;;
   esac
done

# those args must be not null
if [ ! "$HOST" ] || [ ! "$PEM" ] || [ ! "$KEY" ] || [ ! "$API_KEY" ]
then
    usage
    exit 1
fi

# optional args,set default value

[ -z "$DEBUG" ] && DEBUG=off

# print vars key and value when DEBUG eq on
[[ "on" == "$DEBUG" ]] && echo -e "HOST:${HOST} API_KEY:${API_KEY} PEM FILE:${PEM} KEY FILE:${KEY} DEBUG:${DEBUG}"


# get all ssl and filter this one by sni name
cert_content=$(curl --silent --location --request GET "${HOST}/apisix/admin/ssls/" \
--header "X-API-KEY: ${API_KEY}" \
--header 'Content-Type: application/json' | jq "first(.list[]| select(.value.snis[] | contains(\"$(openssl x509 -in $PEM -noout -text|grep -oP '(?<=DNS:|IP Address:)[^,]+'|sort|head -n1)\")))")


validity_start=$(date --date="$(openssl x509 -startdate -noout -in $PEM|cut -d= -f 2)" +"%s")
validity_end=$(date --date="$(openssl x509 -enddate -noout -in $PEM|cut -d= -f 2)" +"%s")
  
# create a new ssl when it not exist
if [ -z "$cert_content" ]
then
  cert_content="{\"snis\":[],\"status\": 1}"

  # read domains from pem file by openssl
  snis=$(openssl x509 -in $PEM -noout -text|grep -oP '(?<=DNS:|IP Address:)[^,]+'|sort)
  for sni in ${snis[@]} ; do
    cert_content=$(echo $cert_content | jq ".snis += [\"$sni\"]")
  done

  cert_content=$(echo $cert_content | jq ".|.cert = \"$(cat $PEM)\"|.key = \"$(cat $KEY)\"|.validity_start=${validity_start}|.validity_end=${validity_end}")

  cert_update_result=$(curl --silent --location --request POST "${HOST}/apisix/admin/ssls/" \
  --header "X-API-KEY: ${API_KEY}" \
  --header 'Content-Type: application/json' \
  --data "$cert_content" )

  [[ "on" == "$DEBUG" ]] && echo -e "cert_content: \n${cert_content}\n\ncreate result json:\n\n${cert_update_result}"
else
  # get exist ssl id
  URI=$(echo $cert_content | jq -r ".key")
  ID=$(echo ${URI##*/})
  # get exist  ssl certificate json , modify cert and key value
  cert_content=$(echo $cert_content | jq ".value|.cert = \"$(cat $PEM)\"|.key = \"$(cat $KEY)\"|.id=\"${ID}\"|.update_time=$(date +'%s')|.validity_start=${validity_start}|.validity_end=${validity_end}")

  # update apisix ssl
  cert_update_result=$(curl --silent --location --request PUT "${HOST}/apisix/admin/ssls/${ID}" \
  --header "X-API-KEY: ${API_KEY}" \
  --header 'Content-Type: application/json' \
  --data "$cert_content" )

  [[ "on" == "$DEBUG" ]] && echo -e "cert_content: \n${cert_content}\n\nupdate result json:\n\n${cert_update_result}"
fi

exit 0

运行后顺利申请证书并添加证书至 APISIX 数据存储。

修改后的脚本地址: https://gist.github.com/overstarry/0f5c2cf7cd4ccfe653dfa071390ae90b

参考