acme: Support running in webroot mode, detect other daemons on port 80

For configurations where another web server is running on port 80, running
acme.sh in standalone mode fails. Try to detect this and refuse to run; and
allow the user to configure a webroot directory to use the running webserver for
certificate verification.

This also updates acme.sh to the latest version.

Signed-off-by: Toke Høiland-Jørgensen <toke@toke.dk>
This commit is contained in:
Toke Høiland-Jørgensen 2017-04-09 18:43:34 +02:00
parent 424f4e2c63
commit 34ed7a9f2c
4 changed files with 101 additions and 31 deletions

View file

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=acme PKG_NAME:=acme
PKG_SOURCE_VERSION:=7b40cbe8c1a52041351524bcde4b37665a7cdf79 PKG_SOURCE_VERSION:=7b40cbe8c1a52041351524bcde4b37665a7cdf79
PKG_VERSION:=1.5 PKG_VERSION:=1.6
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_LICENSE:=GPLv3 PKG_LICENSE:=GPLv3
@ -47,6 +47,7 @@ define Build/Compile
endef endef
define Package/acme/install define Package/acme/install
$(INSTALL_DIR) $(1)/etc/acme
$(INSTALL_DIR) $(1)/etc/config $(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/acme.config $(1)/etc/config/acme $(INSTALL_CONF) ./files/acme.config $(1)/etc/config/acme
$(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_DIR) $(1)/etc/init.d

View file

@ -25,11 +25,12 @@ s.anonymous = true
st = s:option(Value, "state_dir", translate("State directory"), st = s:option(Value, "state_dir", translate("State directory"),
translate("Where certs and other state files are kept.")) translate("Where certs and other state files are kept."))
st.rmempty = false st.rmempty = false
st.datatype = "string" st.datatype = "directory"
ae = s:option(Value, "account_email", translate("Account email"), ae = s:option(Value, "account_email", translate("Account email"),
translate("Email address to associate with account key.")) translate("Email address to associate with account key."))
ae.rmempty = false ae.rmempty = false
ae.datatype = "minlength(1)"
d = s:option(Flag, "debug", translate("Enable debug logging")) d = s:option(Flag, "debug", translate("Enable debug logging"))
d.rmempty = false d.rmempty = false
@ -56,6 +57,12 @@ u = cs:option(Flag, "update_uhttpd", translate("Use for uhttpd"),
"(only select this for one certificate).")) "(only select this for one certificate)."))
u.rmempty = false u.rmempty = false
wr = cs:option(Value, "webroot", translate("Webroot directory"),
translate("Webserver root directory. Set this to the webserver " ..
"document root to run Acme in webroot mode. The web " ..
"server must be accessible from the internet on port 80."))
wr.rmempty = false
dom = cs:option(DynamicList, "domains", translate("Domain names"), dom = cs:option(DynamicList, "domains", translate("Domain names"),
translate("Domain names to include in the certificate. " .. translate("Domain names to include in the certificate. " ..
"The first name will be the subject name, subsequent names will be alt names. " .. "The first name will be the subject name, subsequent names will be alt names. " ..

View file

@ -5,7 +5,8 @@ config acme
config cert 'example' config cert 'example'
option enabled 0 option enabled 0
option use_staging 0 option use_staging 1
option keylength 2048 option keylength 2048
option update_uhttpd 1 option update_uhttpd 1
option webroot ""
list domains example.org list domains example.org

View file

@ -27,45 +27,85 @@ check_cron()
/etc/init.d/cron start /etc/init.d/cron start
} }
log()
{
logger -t acme -s -p daemon.info "$@"
}
err()
{
logger -t acme -s -p daemon.err "$@"
}
debug() debug()
{ {
[ "$DEBUG" -eq "1" ] && echo "$@" >&2 [ "$DEBUG" -eq "1" ] && logger -t acme -s -p daemon.debug "$@"
}
get_listeners()
{
netstat -nptl 2>/dev/null | awk 'match($4, /:80$/){split($7, parts, "/"); print parts[2];}' | uniq | tr "\n" " "
} }
pre_checks() pre_checks()
{ {
echo "Running pre checks." main_domain="$1"
check_cron
[ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR" log "Running pre checks for $main_domain."
if [ -e /etc/init.d/uhttpd ]; then listeners="$(get_listeners)"
debug "port80 listens: $listeners"
UHTTPD_LISTEN_HTTP=$(uci get uhttpd.main.listen_http) case "$listeners" in
"uhttpd")
debug "Found uhttpd listening on port 80; trying to disable."
uci set uhttpd.main.listen_http='' UHTTPD_LISTEN_HTTP=$(uci get uhttpd.main.listen_http)
uci commit uhttpd
/etc/init.d/uhttpd reload || return 1
fi
iptables -I input_rule -p tcp --dport 80 -j ACCEPT || return 1 if [ -z "$UHTTPD_LISTEN_HTTP" ]; then
ip6tables -I input_rule -p tcp --dport 80 -j ACCEPT || return 1 err "$main_domain: Unable to find uhttpd listen config."
err "Manually disable uhttpd or set webroot to continue."
return 1
fi
uci set uhttpd.main.listen_http=''
uci commit uhttpd || return 1
if ! /etc/init.d/uhttpd reload ; then
uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
uci commit uhttpd
return 1
fi
;;
"")
debug "Nothing listening on port 80."
;;
*)
err "$main_domain: Cannot run in standalone mode; another daemon is listening on port 80."
err "Disable other daemon or set webroot to continue."
return 1
;;
esac
iptables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
ip6tables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
debug "v4 input_rule: $(iptables -nvL input_rule)" debug "v4 input_rule: $(iptables -nvL input_rule)"
debug "v6 input_rule: $(ip6tables -nvL input_rule)" debug "v6 input_rule: $(ip6tables -nvL input_rule)"
debug "port80 listens: $(netstat -ntpl | grep :80)"
return 0 return 0
} }
post_checks() post_checks()
{ {
echo "Running post checks (cleanup)." log "Running post checks (cleanup)."
iptables -D input_rule -p tcp --dport 80 -j ACCEPT # The comment ensures we only touch our own rules. If no rules exist, that
ip6tables -D input_rule -p tcp --dport 80 -j ACCEPT # is fine, so hide any errors
iptables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
ip6tables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
if [ -e /etc/init.d/uhttpd ]; then if [ -e /etc/init.d/uhttpd ] && [ -n "$UHTTPD_LISTEN_HTTP" ]; then
uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP" uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
uci commit uhttpd uci commit uhttpd
/etc/init.d/uhttpd reload /etc/init.d/uhttpd reload
UHTTPD_LISTEN_HTTP=
fi fi
} }
@ -102,12 +142,14 @@ issue_cert()
local main_domain local main_domain
local moved_staging=0 local moved_staging=0
local failed_dir local failed_dir
local webroot
config_get_bool enabled "$section" enabled 0 config_get_bool enabled "$section" enabled 0
config_get_bool use_staging "$section" use_staging config_get_bool use_staging "$section" use_staging
config_get_bool update_uhttpd "$section" update_uhttpd config_get_bool update_uhttpd "$section" update_uhttpd
config_get domains "$section" domains config_get domains "$section" domains
config_get keylength "$section" keylength config_get keylength "$section" keylength
config_get webroot "$section" webroot
[ "$enabled" -eq "1" ] || return [ "$enabled" -eq "1" ] || return
@ -116,13 +158,17 @@ issue_cert()
set -- $domains set -- $domains
main_domain=$1 main_domain=$1
[ -n "$webroot" ] || pre_checks "$main_domain" || return 1
log "Running ACME for $main_domain"
if [ -e "$STATE_DIR/$main_domain" ]; then if [ -e "$STATE_DIR/$main_domain" ]; then
if [ "$use_staging" -eq "0" ] && is_staging "$main_domain"; then if [ "$use_staging" -eq "0" ] && is_staging "$main_domain"; then
echo "Found previous cert issued using staging server. Moving it out of the way." log "Found previous cert issued using staging server. Moving it out of the way."
mv "$STATE_DIR/$main_domain" "$STATE_DIR/$main_domain.staging" mv "$STATE_DIR/$main_domain" "$STATE_DIR/$main_domain.staging"
moved_staging=1 moved_staging=1
else else
echo "Found previous cert config. Issuing renew." log "Found previous cert config. Issuing renew."
$ACME --home "$STATE_DIR" --renew -d "$main_domain" $acme_args || return 1 $ACME --home "$STATE_DIR" --renew -d "$main_domain" $acme_args || return 1
return 0 return 0
fi fi
@ -130,17 +176,28 @@ issue_cert()
acme_args="$acme_args $(for d in $domains; do echo -n "-d $d "; done)" acme_args="$acme_args $(for d in $domains; do echo -n "-d $d "; done)"
acme_args="$acme_args --standalone"
acme_args="$acme_args --keylength $keylength" acme_args="$acme_args --keylength $keylength"
[ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL" [ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL"
[ "$use_staging" -eq "1" ] && acme_args="$acme_args --staging" [ "$use_staging" -eq "1" ] && acme_args="$acme_args --staging"
if [ -z "$webroot" ]; then
log "Using standalone mode"
acme_args="$acme_args --standalone"
else
if [ ! -d "$webroot" ]; then
err "$main_domain: Webroot dir '$webroot' does not exist!"
return 1
fi
log "Using webroot dir: $webroot"
acme_args="$acme_args --webroot \"$webroot\""
fi
if ! $ACME --home "$STATE_DIR" --issue $acme_args; then if ! $ACME --home "$STATE_DIR" --issue $acme_args; then
failed_dir="$STATE_DIR/${main_domain}.failed-$(date +%s)" failed_dir="$STATE_DIR/${main_domain}.failed-$(date +%s)"
echo "Issuing cert for $main_domain failed. Moving state to $failed_dir" >&2 err "Issuing cert for $main_domain failed. Moving state to $failed_dir"
[ -d "$STATE_DIR/$main_domain" ] && mv "$STATE_DIR/$main_domain" "$failed_dir" [ -d "$STATE_DIR/$main_domain" ] && mv "$STATE_DIR/$main_domain" "$failed_dir"
if [ "$moved_staging" -eq "1" ]; then if [ "$moved_staging" -eq "1" ]; then
echo "Restoring staging certificate" >&2 err "Restoring staging certificate"
mv "$STATE_DIR/${main_domain}.staging" "$STATE_DIR/${main_domain}" mv "$STATE_DIR/${main_domain}.staging" "$STATE_DIR/${main_domain}"
fi fi
return 1 return 1
@ -152,6 +209,7 @@ issue_cert()
# commit and reload is in post_checks # commit and reload is in post_checks
fi fi
post_checks
} }
load_vars() load_vars()
@ -163,19 +221,22 @@ load_vars()
DEBUG=$(config_get "$section" debug) DEBUG=$(config_get "$section" debug)
} }
if [ -n "$CHECK_CRON" ]; then check_cron
check_cron [ -n "$CHECK_CRON" ] && exit 0
exit 0
fi
config_load acme config_load acme
config_foreach load_vars acme config_foreach load_vars acme
pre_checks || exit 1 if [ -z "$STATE_DIR" ] || [ -z "$ACCOUNT_EMAIL" ]; then
err "state_dir and account_email must be set"
exit 1
fi
[ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR"
trap err_out HUP TERM trap err_out HUP TERM
trap int_out INT trap int_out INT
config_foreach issue_cert cert config_foreach issue_cert cert
post_checks
exit 0 exit 0