Merge remote-tracking branch 'upstream/master' into ssh-deploy

This commit is contained in:
David Kerr 2017-03-15 18:37:46 -04:00
commit 070a141601
9 changed files with 430 additions and 12 deletions

View File

@ -18,6 +18,18 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
# [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E) # [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
# Who are using **acme.sh**
- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
- [ruby-china.org](https://ruby-china.org/topics/31983)
- [Proxmox](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x_and_newer))
- [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89)
- [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt)
- [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty)
- [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709)
- [Centminmod](http://centminmod.com/letsencrypt-acmetool-https.html)
- [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297)
- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/)
- [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials)
# Tested OS # Tested OS
@ -295,6 +307,8 @@ You don't have to do anything manually!
1. cyon.ch 1. cyon.ch
1. Domain-Offensive/Resellerinterface/Domainrobot API 1. Domain-Offensive/Resellerinterface/Domainrobot API
1. Gandi LiveDNS API 1. Gandi LiveDNS API
1. Knot DNS API
1. DigitalOcean API (native)
**More APIs coming soon...** **More APIs coming soon...**

20
acme.sh
View File

@ -299,6 +299,16 @@ _secure_debug3() {
fi fi
} }
_upper_case() {
# shellcheck disable=SC2018,SC2019
tr 'a-z' 'A-Z'
}
_lower_case() {
# shellcheck disable=SC2018,SC2019
tr 'A-Z' 'a-z'
}
_startswith() { _startswith() {
_str="$1" _str="$1"
_sub="$2" _sub="$2"
@ -2462,7 +2472,7 @@ _setNginx() {
fi fi
_debug "Start detect nginx conf for $_d from:$_start_f" _debug "Start detect nginx conf for $_d from:$_start_f"
if ! _checkConf "$_d" "$_start_f"; then if ! _checkConf "$_d" "$_start_f"; then
"Can not find conf file for domain $d" _err "Can not find conf file for domain $d"
return 1 return 1
fi fi
_info "Found conf file: $FOUND_REAL_NGINX_CONF" _info "Found conf file: $FOUND_REAL_NGINX_CONF"
@ -2546,7 +2556,7 @@ _checkConf() {
if [ ! -f "$2" ] && ! echo "$2" | grep '*$' >/dev/null && echo "$2" | grep '*' >/dev/null; then if [ ! -f "$2" ] && ! echo "$2" | grep '*$' >/dev/null && echo "$2" | grep '*' >/dev/null; then
_debug "wildcard" _debug "wildcard"
for _w_f in $2; do for _w_f in $2; do
if _checkConf "$1" "$_w_f"; then if [ -f "$_w_f"] && _checkConf "$1" "$_w_f"; then
return 0 return 0
fi fi
done done
@ -2559,9 +2569,9 @@ _checkConf() {
FOUND_REAL_NGINX_CONF="$2" FOUND_REAL_NGINX_CONF="$2"
return 0 return 0
fi fi
if grep "^ *include *.*;" "$2" >/dev/null; then if cat "$2" | tr "\t" " " | grep "^ *include *.*;" >/dev/null; then
_debug "Try include files" _debug "Try include files"
for included in $(grep "^ *include *.*;" "$2" | sed "s/include //" | tr -d " ;"); do for included in $(cat "$2" | tr "\t" " " | grep "^ *include *.*;" | sed "s/include //" | tr -d " ;"); do
_debug "check included $included" _debug "check included $included"
if _checkConf "$1" "$included"; then if _checkConf "$1" "$included"; then
return 0 return 0
@ -4618,7 +4628,7 @@ install() {
#Modify shebang #Modify shebang
if _exists bash; then if _exists bash; then
_info "Good, bash is found, so change the shebang to use bash as preferred." _info "Good, bash is found, so change the shebang to use bash as preferred."
_shebang='#!/usr/bin/env bash' _shebang='#!'"$(env bash -c "command -v bash")"
_setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang" _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang"
for subf in $_SUB_FOLDERS; do for subf in $_SUB_FOLDERS; do
if [ -d "$LE_WORKING_DIR/$subf" ]; then if [ -d "$LE_WORKING_DIR/$subf" ]; then

View File

@ -210,3 +210,9 @@ export DEPLOY_EXIM4_RELOAD="/etc/init.d/exim4 restart"
acme.sh --deploy -d ftp.example.com --deploy-hook exim4 acme.sh --deploy -d ftp.example.com --deploy-hook exim4
``` ```
## 6. Deploy the cert to OSX Keychain
```sh
acme.sh --deploy -d ftp.example.com --deploy-hook keychain
```

31
deploy/keychain.sh Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env sh
#Here is a sample custom api script.
#This file name is "myapi.sh"
#So, here must be a method myapi_deploy()
#Which will be called by acme.sh to deploy the cert
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
keychain_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
/usr/bin/security import "$_ckey" -k "/Library/Keychains/System.keychain"
/usr/bin/security import "$_ccert" -k "/Library/Keychains/System.keychain"
/usr/bin/security import "$_cca" -k "/Library/Keychains/System.keychain"
/usr/bin/security import "$_cfullchain" -k "/Library/Keychains/System.keychain"
return 0
}

View File

@ -349,6 +349,64 @@ Ok, let's issue a cert now:
acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com
``` ```
## 19. Use Knot (knsupdate) DNS API to automatically issue cert
First, generate a TSIG key for updating the zone.
```
keymgr tsig generate acme_key algorithm hmac-sha512 > /etc/knot/acme.key
```
Include this key in your knot configuration file.
```
include: /etc/knot/acme.key
```
Next, configure your zone to allow dynamic updates.
Dynamic updates for the zone are allowed via proper ACL rule with the `update` action. For in-depth instructions, please see [Knot DNS's documentation](https://www.knot-dns.cz/documentation/).
```
acl:
- id: acme_acl
address: 192.168.1.0/24
key: acme_key
action: update
zone:
- domain: example.com
file: example.com.zone
acl: acme_acl
```
Finally, make the DNS server and TSIG Key available to `acme.sh`
```
export KNOT_SERVER="dns.example.com"
export KNOT_KEY=`grep \# /etc/knot/acme.key | cut -d' ' -f2`
```
Ok, let's issue a cert now:
```
acme.sh --issue --dns dns_knot -d example.com -d www.example.com
```
The `KNOT_SERVER` and `KNOT_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
## 20. Use DigitalOcean API (native)
You need to obtain a read and write capable API key from your DigitalOcean account. See: https://www.digitalocean.com/help/api/
```
export DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
```
Ok, let's issue a cert now:
```
acme.sh --issue --dns dns_dgon -d example.com -d www.example.com
```
# Use custom API # Use custom API
If your API is not supported yet, you can write your own DNS API. If your API is not supported yet, you can write your own DNS API.

View File

@ -143,7 +143,7 @@ aws_rest() {
CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n" CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n"
SignedHeaders="host;x-amz-date" SignedHeaders="host;x-amz-date"
if [ -n "$AWS_SESSION_TOKEN" ]; then if [ -n "$AWS_SESSION_TOKEN" ]; then
export _H2="x-amz-security-token: $AWS_SESSION_TOKEN" export _H3="x-amz-security-token: $AWS_SESSION_TOKEN"
CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n" CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n"
SignedHeaders="${SignedHeaders};x-amz-security-token" SignedHeaders="${SignedHeaders};x-amz-security-token"
fi fi
@ -204,8 +204,8 @@ aws_rest() {
Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
_debug2 Authorization "$Authorization" _debug2 Authorization "$Authorization"
_H3="Authorization: $Authorization" _H2="Authorization: $Authorization"
_debug _H3 "$_H3" _debug _H2 "$_H2"
url="$AWS_URL/$ep" url="$AWS_URL/$ep"

View File

@ -209,8 +209,7 @@ _rest() {
return 1 return 1
fi fi
_debug2 response "$response" _debug2 response "$response"
if ! _contains "$response" '"message":"success"'; then
return 1 _contains "$response" '"code":1'
fi
return 0
} }

205
dnsapi/dns_dgon.sh Executable file
View File

@ -0,0 +1,205 @@
#!/usr/bin/env sh
## Will be called by acme.sh to add the txt record to your api system.
## returns 0 means success, otherwise error.
## Author: thewer <github at thewer.com>
## GitHub: https://github.com/gitwer/acme.sh
##
## Environment Variables Required:
##
## DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
##
##################### Public functions #####################
## Create the text record for validation.
## Usage: fulldomain txtvalue
## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs"
dns_dgon_add() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
_info "Using digitalocean dns validation - add record"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
## save the env vars (key and domain split location) for later automated use
_saveaccountconf DO_API_KEY "$DO_API_KEY"
## split the domain for DO API
if ! _get_base_domain "$fulldomain"; then
_err "domain not found in your account for addition"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
## Set the header with our post type and key auth key
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $DO_API_KEY"
PURL='https://api.digitalocean.com/v2/domains/'$_domain'/records'
PBODY='{"type":"TXT","name":"'$_sub_domain'","data":"'$txtvalue'"}'
_debug PURL "$PURL"
_debug PBODY "$PBODY"
## the create request - post
## args: BODY, URL, [need64, httpmethod]
response="$(_post "$PBODY" "$PURL")"
## check response
if [ "$?" != "0" ]; then
_err "error in response: $response"
return 1
fi
_debug2 response "$response"
## finished correctly
return 0
}
## Remove the txt record after validation.
## Usage: fulldomain txtvalue
## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs"
dns_dgon_rm() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
_info "Using digitalocean dns validation - remove record"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
## split the domain for DO API
if ! _get_base_domain "$fulldomain"; then
_err "domain not found in your account for removal"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
## Set the header with our post type and key auth key
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $DO_API_KEY"
## get URL for the list of domains
## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}}
GURL="https://api.digitalocean.com/v2/domains/$_domain/records"
## while we dont have a record ID we keep going
while [ -z "$record" ]; do
## 1) get the URL
## the create request - get
## args: URL, [onlyheader, timeout]
domain_list="$(_get "$GURL")"
## 2) find record
## check for what we are looing for: "type":"A","name":"$_sub_domain"
record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*\d+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")"
## 3) check record and get next page
if [ -z "$record" ]; then
## find the next page if we dont have a match
nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=\d+")"
if [ -z "$nextpage" ]; then
_err "no record and no nextpage in digital ocean DNS removal"
return 1
fi
_debug2 nextpage "$nextpage"
GURL="$nextpage"
fi
## we break out of the loop when we have a record
done
## we found the record
rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*\d+" | _egrep_o "\d+")"
_debug rec_id "$rec_id"
## delete the record
## delete URL for removing the one we dont want
DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id"
## the create request - delete
## args: BODY, URL, [need64, httpmethod]
response="$(_post "" "$DURL" "" "DELETE")"
## check response (sort of)
if [ "$?" != "0" ]; then
_err "error in remove response: $response"
return 1
fi
_debug2 response "$response"
## finished correctly
return 0
}
##################### Private functions below #####################
## Split the domain provided into the "bade domain" and the "start prefix".
## This function searches for the longest subdomain in your account
## for the full domain given and splits it into the base domain (zone)
## and the prefix/record to be added/removed
## USAGE: fulldomain
## EG: "_acme-challenge.two.three.four.domain.com"
## returns
## _sub_domain="_acme-challenge.two"
## _domain="three.four.domain.com" *IF* zone "three.four.domain.com" exists
## if only "domain.com" exists it will return
## _sub_domain="_acme-challenge.two.three.four"
## _domain="domain.com"
_get_base_domain() {
# args
fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
_debug fulldomain "$fulldomain"
# domain max legal length = 253
MAX_DOM=255
## get a list of domains for the account to check thru
## Set the headers
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $DO_API_KEY"
_debug DO_API_KEY "$DO_API_KEY"
## get URL for the list of domains
## havent seen this request paginated, tested with 18 domains (more requres manual requests with DO)
DOMURL="https://api.digitalocean.com/v2/domains"
## get the domain list (DO gives basically a full XFER!)
domain_list="$(_get "$DOMURL")"
## check response
if [ "$?" != "0" ]; then
_err "error in domain_list response: $domain_list"
return 1
fi
_debug2 domain_list "$domain_list"
## for each shortening of our $fulldomain, check if it exists in the $domain_list
## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge"
i=2
while [ $i -gt 0 ]; do
## get next longest domain
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
## check we got something back from our cut (or are we at the end)
if [ -z "$_domain" ]; then
## we got to the end of the domain - invalid domain
_err "domain not found in DigitalOcean account"
return 1
fi
## we got part of a domain back - grep it out
found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")"
## check if it exists
if [ ! -z "$found" ]; then
## exists - exit loop returning the parts
sub_point=$(_math $i - 1)
_sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
return 0
fi
## increment cut point $i
i=$(_math $i + 1)
done
## we went through the entire domain zone list and dint find one that matched
## doesnt look like we can add in the record
_err "domain not found in DigitalOcean account, but we should never get here"
return 1
}

95
dnsapi/dns_knot.sh Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env sh
######## Public functions #####################
#Usage: dns_knot_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_knot_add() {
fulldomain=$1
txtvalue=$2
_checkKey || return 1
[ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost"
# save the dns server and key to the account.conf file.
_saveaccountconf KNOT_SERVER "${KNOT_SERVER}"
_saveaccountconf KNOT_KEY "${KNOT_KEY}"
if ! _get_root "$fulldomain"; then
_err "Domain does not exist."
return 1
fi
_info "Adding ${fulldomain}. 60 TXT \"${txtvalue}\""
knsupdate -y "${KNOT_KEY}" <<EOF
server ${KNOT_SERVER}
zone ${_domain}.
update add ${fulldomain}. 60 TXT "${txtvalue}"
send
quit
EOF
if [ $? -ne 0 ]; then
_err "Error updating domain."
return 1
fi
_info "Domain TXT record successfully added."
return 0
}
#Usage: dns_knot_rm _acme-challenge.www.domain.com
dns_knot_rm() {
fulldomain=$1
_checkKey || return 1
[ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost"
if ! _get_root "$fulldomain"; then
_err "Domain does not exist."
return 1
fi
_info "Removing ${fulldomain}. TXT"
knsupdate -y "${KNOT_KEY}" <<EOF
server ${KNOT_SERVER}
zone ${_domain}.
update del ${fulldomain}. TXT
send
quit
EOF
if [ $? -ne 0 ]; then
_err "error updating domain"
return 1
fi
_info "Domain TXT record successfully deleted."
return 0
}
#################### Private functions below ##################################
# _acme-challenge.www.domain.com
# returns
# _domain=domain.com
_get_root() {
domain=$1
i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
i=$(_math "$i" - 1)
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
return 1
fi
_domain="$h"
return 0
done
_debug "$domain not found"
return 1
}
_checkKey() {
if [ -z "${KNOT_KEY}" ]; then
_err "You must specify a TSIG key to authenticate the request."
return 1
fi
}