Skip to content
Snippets Groups Projects
Commit b254b1bd authored by Jeffrey Phillips Freeman's avatar Jeffrey Phillips Freeman :boom:
Browse files

Initial working version of swarm-proxy.

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 1570 additions and 0 deletions
FROM modjular/modjular-nginx:1.19.3
LABEL maintainer="Jeffrey Phillips Freeman the@jeffreyfreeman.me"
# Install needed tools
RUN apt-get update && \
apt-get upgrade -y --no-install-recommends && \
apt-get dist-upgrade -y --no-install-recommends && \
apt-get install -y --no-install-recommends \
ca-certificates \
sed \
curl \
jq \
software-properties-common \
gnupg \
lsb-release && \
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - && \
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" && \
apt-get update && \
apt-get -y --no-install-recommends install \
docker-ce \
docker-ce-cli \
containerd.io && \
apt-get purge -y \
curl \
lsb-release && \
apt-get clean && \
rm -r /var/lib/apt/lists/*
RUN mkdir -p /etc/nginx/vhost.d/
RUN mkdir -p /etc/swarm-gen/templates/
COPY swarm-gen.conf /etc/swarm-gen/
COPY swarm-gen-parse-conf /usr/bin
COPY swarm-gen-parse-swarm /usr/bin
COPY swarm-gen-update-state /usr/bin
COPY swarm-gen-check-state /usr/bin
COPY swarm-gen-watch-directories /usr/bin
COPY swarm-gen /usr/bin
COPY swarm-gen-run.sh /docker-run.d/
COPY swarm-gen-watch-directories-run.sh /docker-run.d/
#!/bin/bash
source swarm-gen-parse-conf
SERVICE_BLOCK_REGEX='(### BEGIN SERVICE ###.*### END SERVICE ###)'
while test -f "/etc/swarm-gen/swarm-gen.conf"
do
if [[ $(swarm-gen-check-state) == "true" ]]; then
echo "state updated, waiting ${CONF_WAIT} seconds to regen"
sleep $CONF_WAIT
source swarm-gen-parse-swarm
#echo "entries parsed:"
#echo "${SERVICE_ENTRIES}"
#echo "-------------------"
for (( CONF_INDEX=0; $CONF_INDEX < ${#CONF_TEMPLATES[@]}; CONF_INDEX++ ))
do
CONF_TEMPLATE="${CONF_TEMPLATES[$CONF_INDEX]}"
CONF_DEST="${CONF_TEMPLATES_DESTS[$CONF_INDEX]}"
CONF_NOTIFYCMD="${CONF_TEMPLATES_NOTIFYCMDS[$CONF_INDEX]}"
#process the template
TEMPLATE_TEXT=$(cat $CONF_TEMPLATE)
TEMPLATE_DONE="false"
#echo "processing template:"
#echo "$TEMPLATE_TEXT"
#echo "------------------"
unset TEMPLATE_DONE
while [ -z "${TEMPLATE_DONE}" ]
do
#echo "replacing..."
if [[ "$TEMPLATE_TEXT" =~ $SERVICE_BLOCK_REGEX ]]; then
SERVICE_BLOCK="${BASH_REMATCH[1]}"
#echo "Service block is:"
#echo "${SERVICE_BLOCK}"
#echo "processing service names..."
BLOCKS_PROCESSED=""
for SERVICE_NAME in ${SERVICE_NAMES[@]};
do
SERVICE_ENTRY=$(echo "$SERVICE_ENTRIES" | jq -r ".[\"${SERVICE_NAME}\"]")
#echo "${SERVICE_ENTRY}"
#echo "========="
PROXY_ACTIVE=$(echo "$SERVICE_ENTRY" | jq -r ".[\"swarm-proxy\"]")
PROXY_HOST=$(echo "$SERVICE_ENTRY" | jq -r ".[\"swarm-proxy-host\"]")
PROXY_PORT=$(echo "$SERVICE_ENTRY" | jq -r ".[\"swarm-proxy-port\"]")
PROXY_UPSTREAM=$(echo "$SERVICE_ENTRY" | jq -r ".[\"swarm-proxy-upstream\"]")
PROXY_EMAIL=$(echo "$SERVICE_ENTRY" | jq -r ".[\"swarm-proxy-email\"]")
#echo "$PROXY_HOST"
if [ ! -z "${PROXY_ACTIVE}" ] && [[ "${PROXY_ACTIVE}" == "true" ]]; then
BLOCK_PROCESSED=$(echo "$SERVICE_BLOCK" | sed '/\#\#\# BEGIN SERVICE \#\#\#/d' | sed '/\#\#\# END SERVICE \#\#\#/d')
BLOCK_PROCESSED=$(echo "$BLOCK_PROCESSED" | sed "s/\${HOST}/${PROXY_HOST}/g" | sed "s/\${PORT}/${PROXY_PORT}/g" | sed "s/\${UPSTREAM}/${PROXY_UPSTREAM}/g" | sed "s/\${EMAIL}/${PROXY_EMAIL}/g" )
BLOCKS_PROCESSED="${BLOCK_PROCESSED}"$'\n'"${BLOCKS_PROCESSED}"
#echo "Processed:"
#echo "$BLOCK_PROCESSED"
#echo "-------------------"
fi
done
#echo "Processed blocks are:"
#echo "${BLOCKS_PROCESSED}"
#echo "--------------------"
TEMPLATE_TEXT=${TEMPLATE_TEXT/"${SERVICE_BLOCK}"/"$BLOCKS_PROCESSED"}
#echo "${firstString/Suzi/$secondString}"
else
#echo "template done"
TEMPLATE_DONE="true"
fi
done
echo "Template ${CONF_TEMPLATE} updated:"
echo "$TEMPLATE_TEXT"
echo
echo "------------------------"
echo
if [ ! -z "${TEMPLATE_TEXT}" ]; then
echo "${TEMPLATE_TEXT}" > ${CONF_DEST}
fi
if [ ! -z "${CONF_NOTIFYCMD}" ]; then
echo "Executing notify command: ${CONF_NOTIFYCMD}"
$CONF_NOTIFYCMD
fi
done
swarm-gen-update-state
else
sleep "$CONF_INTERVAL"
fi
done
#!/bin/bash
source swarm-gen-parse-swarm
LAST_STATE_FILE="/tmp/swarm-gen-last-state"
if test -f "$LAST_STATE_FILE"; then
STATE_CHANGE=$(echo "${SERVICE_ENTRIES}" | jq -r --argfile last ${LAST_STATE_FILE} '. as $current | $current != $last')
else
STATE_CHANGE="true"
fi
echo "$STATE_CHANGE"
#!/bin/bash
if [ ! -z "${1}" ]; then
CONFIG_FILE="${1}"
else
CONFIG_FILE="/etc/swarm-gen/swarm-gen.conf"
fi
CONFIG_TEXT=$(cat ${CONFIG_FILE} | sed '/^[[:space:]]*$/d' | sed '/^#/d' | sed -re '/\[\[[a-z]*\]\]/{N;/\[\[[a-z]*\]\]/!b};/\[\[[a-z]*\]\].*\[\[[a-z]*\]\]/d')
# | sed 's/^\[\[/\'$'\n\[\[/g' | sed 's/^\[\[/\'$'\n\[\[/g')
#echo "$CONFIG_TEXT"
######################################################
## Seperate each section of the conf into blocks
######################################################
GENERAL_BLOCK=""
DIRECTORY_BLOCKS=()
TEMPLATE_BLOCKS=()
TEMPLATE_BLOCK_REGEX='\[\[template\]\][\s]*
([^\[]*)'
GENERAL_BLOCK_REGEX='\[\[general\]\][\s]*
([^\[]*)'
DIRECTORY_BLOCK_REGEX='\[\[directory\]\][\s]*
([^\[]*)'
while [ ! -z "${CONFIG_TEXT}" ]
do
if [[ "$CONFIG_TEXT" =~ $TEMPLATE_BLOCK_REGEX ]]; then
TEMPLATE_BLOCK=$(echo "${BASH_REMATCH[1]}" | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*//g' | sed 's/[[:space:]]*$//g' | sed 's/[[:space:]]*=[[:space:]]*/=/g')
TEMPLATE_BLOCKS+=( "${TEMPLATE_BLOCK}" )
CONFIG_TEXT=${CONFIG_TEXT//"${BASH_REMATCH}"/}
elif [[ "$CONFIG_TEXT" =~ $DIRECTORY_BLOCK_REGEX ]]; then
DIRECTORY_BLOCK=$(echo "${BASH_REMATCH[1]}" | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*//g' | sed 's/[[:space:]]*$//g' | sed 's/[[:space:]]*=[[:space:]]*/=/g')
DIRECTORY_BLOCKS+=( "${DIRECTORY_BLOCK}" )
CONFIG_TEXT=${CONFIG_TEXT//"${BASH_REMATCH}"/}
elif [[ "$CONFIG_TEXT" =~ $GENERAL_BLOCK_REGEX ]]; then
GENERAL_BLOCK_MATCH=$(echo "${BASH_REMATCH[1]}" | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*//g' | sed 's/[[:space:]]*$//g' | sed 's/[[:space:]]*=[[:space:]]*/=/g')
GENERAL_BLOCK="${GENERAL_BLOCK_MATCH}"$'\n'"${GENERAL_BLOCK}"
CONFIG_TEXT=${CONFIG_TEXT//"${BASH_REMATCH}"/}
else
unset CONFIG_TEXT
fi
done
#echo "General:"
#echo "$GENERAL_BLOCK"
#echo
#echo "DIRECTORY BLOCKS:"
#echo "${DIRECTORY_BLOCKS[@]}"
#echo
#echo "TEMPLATES BLOCKS:"
#echo "${TEMPLATE_BLOCKS[@]}"
#echo
######################################################
## Parse all the template blocks
######################################################
CONFIG_INDEX=0
CONF_TEMPLATES=()
CONF_TEMPLATES_DESTS=()
CONF_TEMPLATES_NOTIFYCMDS=()
for TEMPLATE_BLOCK in "${TEMPLATE_BLOCKS[@]}"
do
TEMPLATE=$(echo "$TEMPLATE_BLOCK" | grep "^template" | sed 's/^template=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
DEST=$(echo "$TEMPLATE_BLOCK" | grep "^dest" | sed 's/^dest=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
NOTIFYCMD=$(echo "$TEMPLATE_BLOCK" | grep "^notifycmd" | sed 's/^notifycmd=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
WAIT=$(echo "$TEMPLATE_BLOCK" | grep "^wait" | sed 's/^wait=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
if [ ! -z "${TEMPLATE}" ] && [ ! -z "${DEST}" ] ; then
CONF_TEMPLATES[${CONFIG_INDEX}]=${TEMPLATE}
CONF_TEMPLATES_DESTS[${CONFIG_INDEX}]=${DEST}
CONF_TEMPLATES_NOTIFYCMDS[${CONFIG_INDEX}]=${NOTIFYCMD}
CONFIG_INDEX=$(( CONFIG_INDEX + 1 ))
else
echo "Bad config file, either template or dest is missing from template section, will attempt to recover, skipping section, had the following values:"
echo "${TEMPLATE_BLOCK}"
echo "===================="
fi
done
######################################################
## Parse all the directory blocks
######################################################
CONFIG_INDEX=0
CONF_DIRECTORIES=()
CONF_DIRECTORIES_NOTIFYCMDS=()
for DIRECTORY_BLOCK in "${DIRECTORY_BLOCKS[@]}"
do
DIR=$(echo "$DIRECTORY_BLOCK" | grep "^dir" | sed 's/^dir=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
NOTIFYCMD=$(echo "$DIRECTORY_BLOCK" | grep "^notifycmd" | sed 's/^notifycmd=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
WAIT=$(echo "$DIRECTORY_BLOCK" | grep "^wait" | sed 's/^wait=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
if [ ! -z "${DIR}" ] && [ ! -z "${NOTIFYCMD}" ] ; then
CONF_DIRECTORIES[${CONFIG_INDEX}]=${DIR}
CONF_DIRECTORIES_NOTIFYCMDS[${CONFIG_INDEX}]=${NOTIFYCMD}
if [ ! -z "${WAIT}" ]; then
if [[ $WAIT =~ ^[0-9]+$ ]]; then
CONF_DIRECTORIES_WAITS[${CONFIG_INDEX}]=${WAIT}
else
echo "Directory block had invalid wait setting value of '$WAIT', ignoring and not setting"
CONF_DIRECTORIES_WAITS[${CONFIG_INDEX}]="0"
fi
else
CONF_TEMPLATES_WAITS[${CONFIG_INDEX}]="0"
fi
CONFIG_INDEX=$(( CONFIG_INDEX + 1 ))
else
echo "Bad config file, either dir or notifycmd is missing from directory section, will attempt to recover, skipping section, had the following values:"
echo "${DIRECTORY_BLOCK}"
echo "===================="
fi
done
############################################################
## Parse the general block (only one, combined if multiple)
############################################################
INTERVAL=$(echo "$GENERAL_BLOCK" | grep "^interval" | sed 's/^interval=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
if [ ! -z "${INTERVAL}" ]; then
if [[ $INTERVAL =~ ^[1-9][0-9]*$ ]] && (( $INTERVAL > 0)); then
CONF_INTERVAL="${INTERVAL}"
else
echo "General section's interval setting has invalid value of '$INTERVAL', ignoring and setting to default of 5 instead"
CONF_INTERVAL="5"
fi
else
echo "General section has no interval set, using default value of 5"
CONF_INTERVAL="5"
fi
WAIT=$(echo "$GENERAL_BLOCK" | grep "^wait" | sed 's/^wait=["]\{0,1\}//g' | sed 's/["]\{0,1\}$//g' | head -1)
if [ ! -z "${WAIT}" ]; then
if [[ $WAIT =~ ^[0-9]+$ ]]; then
CONF_WAIT="${WAIT}"
else
echo "General section wait setting has invalid value of '$WAIT', ignoring and setting to default of 5 instead"
CONF_WAIT="5"
fi
else
echo "General section has no wait set, using default value of 5"
CONF_WAIT="5"
fi
######################################################
## Export all the collected conf variables
######################################################
export CONF_TEMPLATES
export CONF_TEMPLATES_DESTS
export CONF_TEMPLATES_NOTIFYCMDS
export CONF_DIRECTORIES
export CONF_DIRECTORIES_NOTIFYCMDS
export CONF_DIRECTORIES_WAITS
export CONF_INTERVAL
export CONF_WAIT
#echo "==============="
#echo "results:"
#echo "${CONF_TEMPLATES[@]}"
#echo
#echo "${CONF_TEMPLATES_DESTS[@]}"
#echo
#echo "${CONF_TEMPLATES_NOTIFYCMDS[@]}"
#echo
#echo "${CONF_DIRECTORIES[@]}"
#echo
#echo "${CONF_DIRECTORIES_NOTIFYCMDS[@]}"
#echo
#echo "${CONF_DIRECTORIES_WAITS[@]}"
#echo
#echo "${CONF_INTERVAL}"
#echo
#echo "${CONF_WAIT}"
#echo
#echo "=============="
#!/bin/bash
SERVICE_ENTRIES=""
SERVICE_NAMES=($(docker service ls | sed -n '1!p' | sed 's/^[^[:space:]]* [[:space:]]*\([^[:space:]]*\) .*/\1/g'))
for SERVICE_NAME in "${SERVICE_NAMES[@]}";
do
SERVICE_ENTRY="\"${SERVICE_NAME}\": $(docker service inspect "${SERVICE_NAME}" | jq -r '.[].Spec.Labels')"
SERVICE_ENTRIES="${SERVICE_ENTRIES}${SERVICE_ENTRY},"
done
SERVICE_ENTRIES="{ ${SERVICE_ENTRIES%?} }"
# Sort and format it so we can more easily compare down the line
SERVICE_ENTRIES=$(echo "$SERVICE_ENTRIES" | jq -r '. | (.. | arrays) |= sort')
export SERVICE_NAMES
export SERVICE_ENTRIES
echo "Running swarm gen..."
swarm-gen &
export PID="$!"
echo "...swarm-gen is now running: ${PID}"
#!/bin/bash
source swarm-gen-parse-swarm
LAST_STATE_FILE="/tmp/swarm-gen-last-state"
echo "${SERVICE_ENTRIES}" > ${LAST_STATE_FILE}
#/bin/bash
source swarm-gen-parse-conf
DIR_CHECKSUMS=()
while test -f "/etc/swarm-gen/swarm-gen.conf"
do
for (( DIR_INDEX=0; $DIR_INDEX < ${#CONF_DIRECTORIES[@]}; DIR_INDEX++ ))
do
DIR_DIRECTORY="${CONF_DIRECTORIES[$DIR_INDEX]}"
DIR_NOTIFYCMD="${CONF_DIRECTORIES_NOTIFYCMDS[$DIR_INDEX]}"
DIR_WAIT="${CONF_DIRECTORIES_WAITS[$DIR_INDEX]}"
if [ -z "${DIR_DIRECTORY}" ]; then
echo "Error blank DIR_DIRECTORY value"
exit 1
fi
if [ -z "${DIR_NOTIFYCMD}" ]; then
echo "Error blank DIR_NOTIFYCMD value"
exit 1
fi
if [ -z "${DIR_WAIT}" ]; then
echo "Error blank DIR_WAIT value"
exit 1
fi
DIR_NEW_CHECKSUM=`find ${DIR_DIRECTORY} -type f -exec md5sum {} \;`
if [[ ${DIR_CHECKSUMS[$DIR_INDEX]} != $DIR_NEW_CHECKSUM ]]; then
sleep $DIR_WAIT
echo "Directory contents changed, running ${DIR_NOTIFYCMD}"
${DIR_NOTIFYCMD}
DIR_CHECKSUMS[$DIR_INDEX]=$DIR_NEW_CHECKSUM
fi
done
sleep ${CONF_INTERVAL}
done
echo "Running swarm-gen-watch-directories..."
swarm-gen-watch-directories &
export PID="$!"
echo "...swarm-gen-watch-directories is now running: ${PID}"
#[[general]]
#interval = 5
#wait = 5
#[[directory]]
#dir = "/etc/nginx/conf.d/"
#notifycmd = nginx -s reload
#wait = 10
#[[directory]]
#dir = "/etc/nginx/vhost.d/"
#notifycmd = nginx -s reload
#[[template]]
#template=/etc/swarm-gen/templates/swarm-gen.tmpl
#dest=/etc/nginx/conf.d/swarm-gen.conf
#notifycmd = nginx -s reload
#[[template]]
#template=/etc/swarm-gen/templates/custom.tmpl
#dest=/etc/nginx/conf.d/custom.conf
#notifycmd = nginx -s reload
[[template]]
template = "templates/nginx.tmpl"
dest = "/tmp/nginx.conf"
notifycmd = "/etc/init.d/nginx reload"
[[template]]
template = "templates/fluentd.conf.tmpl"
dest = "/tmp/fluentd.conf"
watch = true
notifycmd = "echo test"
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = true
notifycmd = /bin/bash /tmp/etcd.sh
interval = 10
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
wait = 10
[[template]]
template = "templates/etcd.tmpl"
dest = /tmp/etcd.sh
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
interval = 10
[[directory]]
dir = /etc/nginx/conf.d/
notifycmd = some thing we do
wait = 10
wait = 9
#[[template]]
#template = "templates/etcd.tmplllll"
#dest = /tmp/etcd.shhhhh
#watch = true
#notifycmd = "/bin/bash /tmp/etcd.sh"
#interval = 100
[[directory]]
#dir = /etc/nginx/conf.dddd/
#notifycmd = some thing we doooo
#wait = 100
#wait = 90
[[directory]]
dir = /etc/nginx/conf.d/stuff/
notifycmd = some thing we do
wait = 9
[[directory]]
dir = /etc/nginx/vhost.d/
[[directory]]
[[directory]]
[[template]]
[[template]]
[[template]]
dest = /tmp/etcd.sh
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
wait = 10
[[template]]
template = "templates/etcd.tmpl"
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
wait = 10
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = burgers
notifycmd = "/bin/bash /tmp/etcd.sh"
wait = 10
[[general]]
interval = -2
intercal = meat
interval =2
wait = 5
[[general]]
interval = -2
intercal = meat
interval =2
[[general]]
interval = -2
intercal = meat
interval =2
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
interval = 0
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
interval = -3
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
wait = 6.7
[[template]]
template = "templates/etcd.tmpl"
dest = "/tmp/etcd.sh"
watch = true
notifycmd = "/bin/bash /tmp/etcd.sh"
interval = cheese
[[template]]
template = templates/etcd.tmpl
dest = "/tmp/etcd.sh"
watch = true
notifycmd =/bin/bash /tmp/etcd.sh
interval = 10
#!/bin/bash
set -u
# shellcheck source=functions.sh
source /app/functions.sh
DEBUG="$(lc "$DEBUG")"
function check_deprecated_env_var {
if [[ -n "${ACME_TOS_HASH:-}" ]]; then
echo "Info: the ACME_TOS_HASH environment variable is no longer used by simp_le and has been deprecated."
echo "simp_le now implicitly agree to the ACME CA ToS."
fi
}
function check_docker_socket {
if [[ $DOCKER_HOST == unix://* ]]; then
socket_file=${DOCKER_HOST#unix://}
if [[ ! -S $socket_file ]]; then
echo "Error: you need to share your Docker host socket with a volume at $socket_file" >&2
echo "Typically you should run your container with: '-v /var/run/docker.sock:$socket_file:ro'" >&2
exit 1
fi
fi
}
function check_writable_directory {
local dir="$1"
if [[ $(get_self_cid) ]]; then
if ! docker_api "/containers/$(get_self_cid)/json" | jq ".Mounts[].Destination" | grep -q "^\"$dir\"$"; then
echo "Warning: '$dir' does not appear to be a mounted volume."
fi
else
echo "Warning: can't check if '$dir' is a mounted volume without self container ID."
fi
if [[ ! -d "$dir" ]]; then
echo "Error: can't access to '$dir' directory !" >&2
echo "Check that '$dir' directory is declared as a writable volume." >&2
exit 1
fi
if ! touch "$dir/.check_writable" 2>/dev/null ; then
echo "Error: can't write to the '$dir' directory !" >&2
echo "Check that '$dir' directory is export as a writable volume." >&2
exit 1
fi
rm -f "$dir/.check_writable"
}
function check_dh_group {
# Credits to Steve Kamerman for the background Diffie-Hellman creation logic.
# https://github.com/jwilder/nginx-proxy/pull/589
local DHPARAM_BITS="${DHPARAM_BITS:-2048}"
re='^[0-9]*$'
if ! [[ "$DHPARAM_BITS" =~ $re ]] ; then
echo "Error: invalid Diffie-Hellman size of $DHPARAM_BITS !" >&2
exit 1
fi
# If a dhparam file is not available, use the pre-generated one and generate a new one in the background.
local PREGEN_DHPARAM_FILE="/app/dhparam.pem.default"
local DHPARAM_FILE="/etc/nginx/certs/dhparam.pem"
local GEN_LOCKFILE="/tmp/le_companion_dhparam_generating.lock"
# The hash of the pregenerated dhparam file is used to check if the pregen dhparam is already in use
local PREGEN_HASH; PREGEN_HASH=$(sha256sum "$PREGEN_DHPARAM_FILE" | cut -d ' ' -f1)
if [[ -f "$DHPARAM_FILE" ]]; then
local CURRENT_HASH; CURRENT_HASH=$(sha256sum "$DHPARAM_FILE" | cut -d ' ' -f1)
if [[ "$PREGEN_HASH" != "$CURRENT_HASH" ]]; then
# There is already a dhparam, and it's not the default
set_ownership_and_permissions "$DHPARAM_FILE"
echo "Info: Custom Diffie-Hellman group found, generation skipped."
return 0
fi
if [[ -f "$GEN_LOCKFILE" ]]; then
# Generation is already in progress
return 0
fi
fi
echo "Info: Creating Diffie-Hellman group in the background."
echo "A pre-generated Diffie-Hellman group will be used for now while the new one
is being created."
# Put the default dhparam file in place so we can start immediately
cp "$PREGEN_DHPARAM_FILE" "$DHPARAM_FILE"
set_ownership_and_permissions "$DHPARAM_FILE"
touch "$GEN_LOCKFILE"
# Generate a new dhparam in the background in a low priority and reload nginx when finished (grep removes the progress indicator).
(
(
nice -n +5 openssl dhparam -out "${DHPARAM_FILE}.new" "$DHPARAM_BITS" 2>&1 \
&& mv "${DHPARAM_FILE}.new" "$DHPARAM_FILE" \
&& echo "Info: Diffie-Hellman group creation complete, reloading nginx." \
&& set_ownership_and_permissions "$DHPARAM_FILE" \
) | grep -vE '^[\.+]+'
rm "$GEN_LOCKFILE"
) & disown
}
function check_default_cert_key {
local cn='letsencrypt-nginx-proxy-companion'
if [[ -e /etc/nginx/certs/default.crt && -e /etc/nginx/certs/default.key ]]; then
default_cert_cn="$(openssl x509 -noout -subject -in /etc/nginx/certs/default.crt)"
# Check if the existing default certificate is still valid for more
# than 3 months / 7776000 seconds (60 x 60 x 24 x 30 x 3).
check_cert_min_validity /etc/nginx/certs/default.crt 7776000
cert_validity=$?
[[ "$DEBUG" == true ]] && echo "Debug: a default certificate with $default_cert_cn is present."
fi
# Create a default cert and private key if:
# - either default.crt or default.key are absent
# OR
# - the existing default cert/key were generated by the container
# and the cert validity is less than three months
if [[ ! -e /etc/nginx/certs/default.crt || ! -e /etc/nginx/certs/default.key ]] || [[ "${default_cert_cn:-}" =~ $cn && "${cert_validity:-}" -ne 0 ]]; then
openssl req -x509 \
-newkey rsa:4096 -sha256 -nodes -days 365 \
-subj "/CN=$cn" \
-keyout /etc/nginx/certs/default.key.new \
-out /etc/nginx/certs/default.crt.new \
&& mv /etc/nginx/certs/default.key.new /etc/nginx/certs/default.key \
&& mv /etc/nginx/certs/default.crt.new /etc/nginx/certs/default.crt
echo "Info: a default key and certificate have been created at /etc/nginx/certs/default.key and /etc/nginx/certs/default.crt."
elif [[ "$DEBUG" == true && "${default_cert_cn:-}" =~ $cn ]]; then
echo "Debug: the self generated default certificate is still valid for more than three months. Skipping default certificate creation."
elif [[ "$DEBUG" == true ]]; then
echo "Debug: the default certificate is user provided. Skipping default certificate creation."
fi
set_ownership_and_permissions "/etc/nginx/certs/default.key"
set_ownership_and_permissions "/etc/nginx/certs/default.crt"
}
#########################################################################
## Code Entry
#########################################################################
acmev1_r='acme-(v01\|staging)\.api\.letsencrypt\.org'
if [[ "${ACME_CA_URI:-}" =~ $acmev1_r ]]; then
echo "Error: the ACME v1 API is no longer supported by simp_le."
echo "See https://github.com/zenhack/simp_le/pull/119"
echo "Please use one of Let's Encrypt ACME v2 endpoints instead."
exit 1
fi
check_docker_socket
check_writable_directory '/etc/nginx/certs'
check_writable_directory '/etc/nginx/vhost.d'
check_writable_directory '/usr/share/nginx/html'
[[ -f /app/letsencrypt_user_data ]] && check_writable_directory '/etc/nginx/conf.d'
check_deprecated_env_var
check_default_cert_key
check_dh_group
cat > "/etc/nginx/vhost.d/default-letsencrypt" << EOF
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
auth_request off;
allow all;
root /usr/share/nginx/html;
try_files \$uri =404;
break;
}
EOF
mkdir -p /usr/share/nginx/html/.well-known/acme-challenge/
cat > "/usr/share/nginx/html/.well-known/acme-challenge/active.html" << EOF
<!DOCTYPE html>
<html>
<head>
<title>I am alive!</title>
</head>
<body>
<h1>I like being alive!</h1>
</body>
</html>
EOF
FROM modjular/swarm-gen:latest
LABEL maintainer="Jeffrey Phillips Freeman the@jeffreyfreeman.me"
RUN apt-get -y --no-install-recommends purge nginx
ENV DEBUG=false \
DOCKER_HOST=unix:///var/run/docker.sock
RUN sync && \
apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
git \
gcc \
musl-dev \
libffi-dev \
python3-dev \
libssl-dev \
procps \
curl \
python3-pip && \
mkdir -p /src && \
git -C /src clone --depth=1 --branch "0.18.1" https://github.com/zenhack/simp_le.git && \
cd /src/simp_le && \
python3 -m pip install -U \
pip \
setuptools \
wheel && \
python3 -m pip install . && \
cd / && \
rm -rf /src && \
apt-get purge -y \
git \
gcc \
musl-dev \
libffi-dev \
python3-dev \
libssl-dev && \
apt-get clean && \
rm -r /var/lib/apt/lists/*
COPY /app/ /app/
COPY swarm-gen.conf /etc/swarm-gen/swarm-gen.conf
COPY swarm-proxy-letsencrypt /usr/bin/
COPY swarm-proxy-letsencrypt-run.sh /docker-run.d/
COPY 99-swarm-proxy-letsencrypt-entry.sh /docker-entrypoint.d/
RUN rm -f /docker-run.d/nginx-run.sh && \
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh && \
mkdir -p /etc/nginx/certs && \
mkdir -p /etc/nginx/dhparam && \
mkdir -p /etc/nginx/conf.d && \
mkdir -p /etc/nginx/vhost.d && \
mkdir -p /usr/share/nginx/html/.well-known/acme-challenge/
VOLUME ["/etc/nginx/certs", "/etc/nginx/dhparam", "/etc/nginx/conf.d", "/etc/nginx/vhost.d", "/usr/share/nginx/html"]
WORKDIR /app
#!/bin/bash
function print_cert_info {
local enddate
local subject
local san_str
# Get the wanted informations with OpenSSL.
issuer="$(openssl x509 -noout -issuer -in "$1" | sed -n 's/.*CN = \(.*\)/\1/p')"
enddate="$(openssl x509 -noout -enddate -in "$1" | sed -n 's/notAfter=\(.*$\)/\1/p')"
subject="$(openssl x509 -noout -subject -in "$1" | sed -n 's/.*CN = \([a-z0-9.-]*\)/- \1/p')"
san_str="$(openssl x509 -text -in "$1" | grep 'DNS:')"
echo "Certificate was issued by $issuer"
if [[ "$2" == "expired" ]]; then
echo "Certificate was valid until $enddate"
else
echo "Certificate is valid until $enddate"
fi
echo "Subject Name:"
echo "$subject"
# Display the SAN info only if there is more than one SAN domain.
while IFS=',' read -ra SAN; do
if [[ ${#SAN[@]} -gt 1 ]]; then
echo "Subject Alternative Name:"
for domain in "${SAN[@]}"; do
echo "$domain" | sed -n 's/.*DNS:\([a-z0-9.-]*\)/- \1/p'
done
fi
done <<< "$san_str"
}
echo '##### Certificate status #####'
for cert in /etc/nginx/certs/*/fullchain.pem; do
[[ -e "$cert" ]] || continue
if [[ -e "${cert%fullchain.pem}chain.pem" ]]; then
# Verify the certificate with OpenSSL.
if verify=$(openssl verify -CAfile "${cert%fullchain.pem}chain.pem" "$cert" 2>&1); then
echo "$verify"
# Print certificate info.
print_cert_info "$cert"
else
echo "${cert}: EXPIRED"
# Print certificate info.
print_cert_info "$cert" "expired"
fi
else
echo "${cert}: no corresponding chain.pem file, unable to verify certificate"
# Print certificate info.
print_cert_info "$cert"
fi
# Find the .crt files in /etc/nginx/certs which are
# symlinks pointing to the current certificate.
unset symlinked_domains
for symlink in /etc/nginx/certs/*.crt; do
[[ -e "$symlink" ]] || continue
if [[ "$(readlink -f "$symlink")" == "$cert" ]]; then
domain="${symlink%.crt}"
domain="${domain//\/etc\/nginx\/certs\//}"
symlinked_domains+=("$domain")
fi
done
# Display symlinks pointing to the current cert if there is any.
if [[ ${#symlinked_domains[@]} -gt 0 ]]; then
echo "Certificate is used by the following domain(s):"
for domain in "${symlinked_domains[@]}"; do
echo "- $domain"
done
fi
echo '##############################'
done
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAwpR+yYapElMV4DiO+BwKK2N8Ur4giZtga+dslyDMuhY+U4t/97Eq
gdFg2RD5nqrgWCRWEYcbh1kPBOAPWXZ4+N8mZL8pJXaNi2XFA8IxQex283Sz7CX+
qr/zb+piJLx+/6JB/NNTZtKurM3ZQgwdGqSHqeWgvRIgCQAykC1oz7muCsev1IMc
rLig1kyvhg3L1t+uKYV0OtiXONmPglPm9pXRqMQ53Rg/D3CpUpyyTSugOFjVhLrP
Ow+kO6qXBQSDhrL2L0UjprbcVMPHv9bFmWNoTCtC8OYA1OuiA368PWhgeH/76Yu8
4an6/vt3HowDZHKfB3Vb1VwTI+k6hzwhkwIBAg==
-----END DH PARAMETERS-----
#!/bin/bash
# shellcheck source=letsencrypt_service
source /app/letsencrypt_service --source-only
update_certs --force-renew
#!/bin/bash
# Convert argument to lowercase (bash 4 only)
function lc {
echo "${@,,}"
}
DEBUG="$(lc "$DEBUG")"
[[ -z "${VHOST_DIR:-}" ]] && \
declare -r VHOST_DIR=/etc/nginx/vhost.d
[[ -z "${START_HEADER:-}" ]] && \
declare -r START_HEADER='## Start of configuration add by letsencrypt container'
[[ -z "${END_HEADER:-}" ]] && \
declare -r END_HEADER='## End of configuration add by letsencrypt container'
function check_nginx_proxy_container_run {
# TODO make the load balancer address configurable.
if curl --head --silent --fail http://lb.qoto.org/.well-known/acme-challenge/active.html > /dev/null;
then
return 0
else
echo "Could not reach load-balancer, is it listening on port 80?"
exit 1
fi
}
function ascending_wildcard_locations {
# Given foo.bar.baz.example.com as argument, will output:
# - *.bar.baz.example.com
# - *.baz.example.com
# - *.example.com
local domain="${1:?}"
local first_label
regex="^[[:alnum:]_\-]+(\.[[:alpha:]]+)?$"
until [[ "$domain" =~ $regex ]]; do
first_label="${domain%%.*}"
domain="${domain/${first_label}./}"
echo "*.${domain}"
done
}
function descending_wildcard_locations {
# Given foo.bar.baz.example.com as argument, will output:
# - foo.bar.baz.example.*
# - foo.bar.baz.*
# - foo.bar.*
# - foo.*
local domain="${1:?}"
local last_label
regex="^[[:alnum:]_\-]+$"
until [[ "$domain" =~ $regex ]]; do
last_label="${domain##*.}"
domain="${domain/.${last_label}/}"
echo "${domain}.*"
done
}
function enumerate_wildcard_locations {
# Goes through ascending then descending wildcard locations for a given FQDN
local domain="${1:?}"
ascending_wildcard_locations "$domain"
descending_wildcard_locations "$domain"
}
function add_standalone_configuration {
local domain="${1:?}"
cat > "/etc/nginx/conf.d/standalone-cert-${domain}.conf" << EOF
server {
server_name ${domain};
listen 80 ;
location /.well-known/acme-challenge/ {
auth_basic off;
allow all;
root /usr/share/nginx/html;
try_files \$uri =404;
break;
}
include /opt/nginx/vhost.d/${domain}*;
include /opt/nginx/vhost.d/default*;
location / {
return 301 https://\$host\$request_uri;
}
}
EOF
}
function remove_all_standalone_configurations {
local old_shopt_options; old_shopt_options=$(shopt -p) # Backup shopt options
shopt -s nullglob
for file in "/etc/nginx/conf.d/standalone-cert-"*".conf"; do
rm -f "$file"
done
eval "$old_shopt_options" # Restore shopt options
}
function check_cert_min_validity {
# Check if a certificate ($1) is still valid for a given amount of time in seconds ($2).
# Returns 0 if the certificate is still valid for this amount of time, 1 otherwise.
local cert_path="$1"
local min_validity="$(( $(date "+%s") + $2 ))"
local cert_expiration
cert_expiration="$(openssl x509 -noout -enddate -in "$cert_path" | cut -d "=" -f 2)"
cert_expiration="$(date --utc --date "${cert_expiration% GMT}" "+%s")"
[[ $cert_expiration -gt $min_validity ]] || return 1
}
function get_self_cid {
local self_cid=""
# Try the /proc files methods first then resort to the Docker API.
if [[ -f /proc/1/cpuset ]]; then
self_cid="$(grep -Eo '[[:alnum:]]{64}' /proc/1/cpuset)"
fi
if [[ ( ${#self_cid} != 64 ) && ( -f /proc/self/cgroup ) ]]; then
self_cid="$(grep -Eo -m 1 '[[:alnum:]]{64}' /proc/self/cgroup)"
fi
if [[ ( ${#self_cid} != 64 ) ]]; then
self_cid="$(docker_api "/containers/$(hostname)/json" | jq -r '.Id')"
fi
# If it's not 64 characters long, then it's probably not a container ID.
if [[ ${#self_cid} == 64 ]]; then
echo "$self_cid"
else
echo "$(date "+%Y/%m/%d %T"), Error: can't get my container ID !" >&2
return 1
fi
}
## Docker API
function docker_api {
local scheme
local curl_opts=(-s)
local method=${2:-GET}
# data to POST
if [[ -n "${3:-}" ]]; then
curl_opts+=(-d "$3")
fi
if [[ -z "$DOCKER_HOST" ]];then
echo "Error DOCKER_HOST variable not set" >&2
return 1
fi
if [[ $DOCKER_HOST == unix://* ]]; then
curl_opts+=(--unix-socket "${DOCKER_HOST#unix://}")
scheme='http://localhost'
else
scheme="http://${DOCKER_HOST#*://}"
fi
[[ $method = "POST" ]] && curl_opts+=(-H 'Content-Type: application/json')
curl "${curl_opts[@]}" -X "${method}" "${scheme}$1"
}
function docker_exec {
local id="${1?missing id}"
local cmd="${2?missing command}"
local data; data=$(printf '{ "AttachStdin": false, "AttachStdout": true, "AttachStderr": true, "Tty":false,"Cmd": %s }' "$cmd")
exec_id=$(docker_api "/containers/$id/exec" "POST" "$data" | jq -r .Id)
if [[ -n "$exec_id" && "$exec_id" != "null" ]]; then
docker_api "/exec/${exec_id}/start" "POST" '{"Detach": false, "Tty":false}'
else
echo "$(date "+%Y/%m/%d %T"), Error: can't exec command ${cmd} in container ${id}. Check if the container is running." >&2
return 1
fi
}
function labeled_cid {
docker_api "/containers/json" | jq -r '.[] | select(.Labels["'"$1"'"])|.Id'
}
function is_docker_gen_container {
local id="${1?missing id}"
if [[ $(docker_api "/containers/$id/json" | jq -r '.Config.Env[]' | grep -c -E '^DOCKER_GEN_VERSION=') = "1" ]]; then
return 0
else
return 1
fi
}
function get_docker_gen_container {
# First try to get the docker-gen container ID from the container label.
local docker_gen_cid; docker_gen_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen)"
# If the labeled_cid function dit not return anything and the env var is set, use it.
if [[ -z "$docker_gen_cid" ]] && [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
docker_gen_cid="$NGINX_DOCKER_GEN_CONTAINER"
fi
# If a container ID was found, output it. The function will return 1 otherwise.
[[ -n "$docker_gen_cid" ]] && echo "$docker_gen_cid"
}
function set_ownership_and_permissions {
local path="${1:?}"
# The default ownership is root:root, with 755 permissions for folders and 644 for files.
local user="${FILES_UID:-root}"
local group="${FILES_GID:-$user}"
local f_perms="${FILES_PERMS:-644}"
local d_perms="${FOLDERS_PERMS:-755}"
if [[ ! "$f_perms" =~ ^[0-7]{3,4}$ ]]; then
echo "Warning : the provided files permission octal ($f_perms) is incorrect. Skipping ownership and permissions check."
return 1
fi
if [[ ! "$d_perms" =~ ^[0-7]{3,4}$ ]]; then
echo "Warning : the provided folders permission octal ($d_perms) is incorrect. Skipping ownership and permissions check."
return 1
fi
[[ "$DEBUG" == true ]] && echo "Debug: checking $path ownership and permissions."
# Find the user numeric ID if the FILES_UID environment variable isn't numeric.
if [[ "$user" =~ ^[0-9]+$ ]]; then
user_num="$user"
# Check if this user exist inside the container
elif id -u "$user" > /dev/null 2>&1; then
# Convert the user name to numeric ID
local user_num; user_num="$(id -u "$user")"
[[ "$DEBUG" == true ]] && echo "Debug: numeric ID of user $user is $user_num."
else
echo "Warning: user $user not found in the container, please use a numeric user ID instead of a user name. Skipping ownership and permissions check."
return 1
fi
# Find the group numeric ID if the FILES_GID environment variable isn't numeric.
if [[ "$group" =~ ^[0-9]+$ ]]; then
group_num="$group"
# Check if this group exist inside the container
elif getent group "$group" > /dev/null 2>&1; then
# Convert the group name to numeric ID
local group_num; group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
[[ "$DEBUG" == true ]] && echo "Debug: numeric ID of group $group is $group_num."
else
echo "Warning: group $group not found in the container, please use a numeric group ID instead of a group name. Skipping ownership and permissions check."
return 1
fi
# Check and modify ownership if required.
if [[ -e "$path" ]]; then
if [[ "$(stat -c %u:%g "$path" )" != "$user_num:$group_num" ]]; then
[[ "$DEBUG" == true ]] && echo "Debug: setting $path ownership to $user:$group."
if [[ -L "$path" ]]; then
chown -h "$user_num:$group_num" "$path"
else
chown "$user_num:$group_num" "$path"
fi
fi
# If the path is a folder, check and modify permissions if required.
if [[ -d "$path" ]]; then
if [[ "$(stat -c %a "$path")" != "$d_perms" ]]; then
[[ "$DEBUG" == true ]] && echo "Debug: setting $path permissions to $d_perms."
chmod "$d_perms" "$path"
fi
# If the path is a file, check and modify permissions if required.
elif [[ -f "$path" ]]; then
# Use different permissions for private files (private keys and ACME account files) ...
if [[ "$path" =~ ^.*(default\.key|key\.pem|\.json)$ ]]; then
if [[ "$(stat -c %a "$path")" != "$f_perms" ]]; then
[[ "$DEBUG" == true ]] && echo "Debug: setting $path permissions to $f_perms."
chmod "$f_perms" "$path"
fi
# ... and for public files (certificates, chains, fullchains, DH parameters).
else
if [[ "$(stat -c %a "$path")" != "644" ]]; then
[[ "$DEBUG" == true ]] && echo "Debug: setting $path permissions to 644."
chmod "644" "$path"
fi
fi
fi
else
echo "Warning: $path does not exist. Skipping ownership and permissions check."
return 1
fi
}
#!/bin/bash
# shellcheck source=functions.sh
source /app/functions.sh
DEBUG="$(lc "$DEBUG")"
seconds_to_wait=3600
ACME_CA_URI="${ACME_CA_URI:-https://acme-v02.api.letsencrypt.org/directory}"
DEFAULT_KEY_SIZE=4096
REUSE_ACCOUNT_KEYS="$(lc "${REUSE_ACCOUNT_KEYS:-true}")"
REUSE_PRIVATE_KEYS="$(lc "${REUSE_PRIVATE_KEYS:-false}")"
MIN_VALIDITY_CAP=7603200
DEFAULT_MIN_VALIDITY=2592000
function create_link {
local -r source=${1?missing source argument}
local -r target=${2?missing target argument}
if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
set_ownership_and_permissions "$target"
[[ "$DEBUG" == true ]] && echo "$target already linked to $source"
return 1
else
ln -sf "$source" "$target" \
&& set_ownership_and_permissions "$target"
fi
}
function create_links {
local -r base_domain=${1?missing base_domain argument}
local -r domain=${2?missing base_domain argument}
if [[ ! -f "/etc/nginx/certs/$base_domain/fullchain.pem" || \
! -f "/etc/nginx/certs/$base_domain/key.pem" ]]; then
return 1
fi
local return_code=1
create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/$domain.crt"
return_code=$(( return_code & $? ))
create_link "./$base_domain/key.pem" "/etc/nginx/certs/$domain.key"
return_code=$(( return_code & $? ))
if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
create_link ./dhparam.pem "/etc/nginx/certs/$domain.dhparam.pem"
return_code=$(( return_code & $? ))
fi
if [[ -f "/etc/nginx/certs/$base_domain/chain.pem" ]]; then
create_link "./$base_domain/chain.pem" "/etc/nginx/certs/$domain.chain.pem"
return_code=$(( return_code & $? ))
fi
return $return_code
}
function cleanup_links {
local -a LE_HOST
local -a ENABLED_DOMAINS
local -a SYMLINKED_DOMAINS
local -a DISABLED_DOMAINS
# Create an array containing domains for which a
# symlinked private key exists in /etc/nginx/certs.
for symlinked_domain in /etc/nginx/certs/*.crt; do
[[ -L "$symlinked_domain" ]] || continue
symlinked_domain="${symlinked_domain##*/}"
symlinked_domain="${symlinked_domain%*.crt}"
SYMLINKED_DOMAINS+=("$symlinked_domain")
done
[[ "$DEBUG" == true ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"
# Create an array containing domains that are considered
# enabled (ie present on /app/letsencrypt_service_data or /app/letsencrypt_user_data).
# First the dynamic data
[[ -f /app/letsencrypt_service_data ]] && source /app/letsencrypt_service_data
for (( CONT_INDEX=0; $CONT_INDEX < ${#LETSENCRYPT_HOSTS[@]}; CONT_INDEX++ ))
do
LE_HOST="${LETSENCRYPT_HOSTS[$CONT_INDEX]}"
ENABLED_DOMAINS+=("$LE_HOST")
done
# Now the user data
[[ -f /app/letsencrypt_user_data ]] && source /app/letsencrypt_user_data
for (( CONT_INDEX=0; $CONT_INDEX < ${#LETSENCRYPT_USER_HOSTS[@]}; CONT_INDEX++ ))
do
LE_HOST="${LETSENCRYPT_USER_HOSTS[$CONT_INDEX]}"
ENABLED_DOMAINS+=("$LE_HOST")
done
[[ "$DEBUG" == true ]] && echo "Enabled domains: ${ENABLED_DOMAINS[*]}"
# Create an array containing only domains for which a symlinked private key exists
# in /etc/nginx/certs but that no longer have a corresponding LETSENCRYPT_HOST set
# on an active container or on /app/letsencrypt_user_data
if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
"${ENABLED_DOMAINS[@]}" \
"${ENABLED_DOMAINS[@]}" \
| tr ' ' '\n' | sort | uniq -u)
fi
[[ "$DEBUG" == true ]] && echo "Disabled domains: ${DISABLED_DOMAINS[*]}"
# Remove disabled domains symlinks if present.
# Return 1 if nothing was removed and 0 otherwise.
if [[ ${#DISABLED_DOMAINS[@]} -gt 0 ]]; then
[[ "$DEBUG" == true ]] && echo "Some domains are disabled :"
for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
[[ "$DEBUG" == true ]] && echo "Checking domain ${disabled_domain}"
cert_folder="$(readlink -f /etc/nginx/certs/"${disabled_domain}".crt)"
# If the dotfile is absent, skip domain.
if [[ ! -e "${cert_folder%/*}/.companion" ]]; then
[[ "$DEBUG" == true ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by letsencrypt-nginx-proxy-companion. Skipping domain."
continue
else
[[ "$DEBUG" == true ]] && echo "${disabled_domain} is managed by letsencrypt-nginx-proxy-companion. Removing unused symlinks."
fi
for extension in .crt .key .dhparam.pem .chain.pem; do
file="${disabled_domain}${extension}"
if [[ -n "${file// }" ]] && [[ -L "/etc/nginx/certs/${file}" ]]; then
[[ "$DEBUG" == true ]] && echo "Removing /etc/nginx/certs/${file}"
rm -f "/etc/nginx/certs/${file}"
fi
done
rm -f "/etc/nginx/conf.d/${disabled_domain}-letsencrypt.conf"
done
return 0
else
return 1
fi
}
function update_certs {
local -a LE_HOST
local -a LE_EMAIL
check_nginx_proxy_container_run || return
# Load relevant container settings
if [[ -f /app/letsencrypt_service_data ]]; then
source /app/letsencrypt_service_data
else
echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
fi
# Load settings for standalone certs
if [[ -f /app/letsencrypt_user_data ]]; then
if source /app/letsencrypt_user_data; then
for (( CONT_INDEX=0; $CONT_INDEX < ${#LETSENCRYPT_USER_HOSTS[@]}; CONT_INDEX++ ))
do
add_standalone_configuration "${LETSENCRYPT_USER_HOSTS[$CONT_INDEX]}"
done
else
echo "Warning: could not source /app/letsencrypt_user_data, skipping user data"
fi
fi
unset FOR_USER
INDEX_MAX="${#LETSENCRYPT_HOSTS[@]}"
for (( CONT_INDEX=0; $CONT_INDEX < ${INDEX_MAX}; CONT_INDEX++ ))
do
if [ -z "${FOR_USER}" ]; then #if FOR_USER is not set
echo "for user not set"
LE_HOST="${LETSENCRYPT_HOSTS[$CONT_INDEX]}"
LE_EMAIL="${LETSENCRYPT_EMAILS[$CONT_INDEX]}"
else # FOR_USER is set
echo "for suer set"
LE_HOST="${LETSENCRYPT_USER_HOSTS[$CONT_INDEX]}"
LE_EMAIL="${LETSENCRYPT_USER_EMAILS[$CONT_INDEX]}"
fi
if [ -z "${LE_HOST}" ]; then
echo "Error blank HOST value"
exit 1
fi
if [ -z "${LE_EMAIL}" ]; then
echo "Error blank EMAIL value"
exit 1
fi
base_domain="${LE_HOST}"
params_d_arr=()
# Use container's LETSENCRYPT_EMAIL if set, fallback to DEFAULT_EMAIL
email_address="${LE_EMAIL:-"<no value>"}"
if [[ "$email_address" != "<no value>" ]]; then
params_d_arr+=(--email "$email_address")
elif [[ -n "${DEFAULT_EMAIL:-}" ]]; then
params_d_arr+=(--email "$DEFAULT_EMAIL")
fi
cert_keysize="${LE_KEYSIZE:-"<no value>"}"
if [[ "$cert_keysize" == "<no value>" ]]; then
cert_keysize=$DEFAULT_KEY_SIZE
fi
le_staging_uri="https://acme-staging-v02.api.letsencrypt.org/directory"
if [[ $(lc "${LE_TEST:-}") == true ]] || \
[[ "$ACME_CA_URI" == "$le_staging_uri" ]]; then
# Use staging Let's Encrypt ACME end point
acme_ca_uri="$le_staging_uri"
# Prefix test certificate directory with _test_
certificate_dir="/etc/nginx/certs/_test_$base_domain"
else
# Use default or user provided ACME end point
acme_ca_uri="$ACME_CA_URI"
certificate_dir="/etc/nginx/certs/$base_domain"
fi
account_alias="${LE_ACCOUNT_ALIAS:-"<no value>"}"
if [[ "$account_alias" == "<no value>" ]]; then
account_alias=default
fi
[[ "$DEBUG" == true ]] && params_d_arr+=(-v)
[[ $REUSE_PRIVATE_KEYS == true ]] && params_d_arr+=(--reuse_key)
min_validity="${LE_MIN_VALIDITY:-"<no value>"}"
if [[ "$min_validity" == "<no value>" ]]; then
min_validity=$DEFAULT_MIN_VALIDITY
fi
# Sanity Check
# Upper Bound
if [[ $min_validity -gt $MIN_VALIDITY_CAP ]]; then
min_validity=$MIN_VALIDITY_CAP
fi
# Lower Bound
if [[ $min_validity -lt $((seconds_to_wait * 2)) ]]; then
min_validity=$((seconds_to_wait * 2))
fi
if [[ "${1}" == "--force-renew" ]]; then
# Manually set to highest certificate lifetime given by LE CA
params_d_arr+=(--valid_min 7776000)
else
params_d_arr+=(--valid_min "$min_validity")
fi
# Create directory for the first domain,
# make it root readable only and make it the cwd
mkdir -p "$certificate_dir"
set_ownership_and_permissions "$certificate_dir"
pushd "$certificate_dir" || return
params_d_arr+=(-d "$LE_HOST")
if [[ -e "./account_key.json" ]] && [[ ! -e "./account_reg.json" ]]; then
# If there is an account key present without account registration, this is
# a leftover from the ACME v1 version of simp_le. Remove this account key.
rm -f ./account_key.json
[[ "$DEBUG" == true ]] \
&& echo "Debug: removed ACME v1 account key $certificate_dir/account_key.json"
fi
# The ACME account key and registration full path are derived from the
# endpoint URI + the account alias (set to 'default' if no alias is provided)
account_dir="../accounts/${acme_ca_uri#*://}"
if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
for type in "key" "reg"; do
file_full_path="${account_dir}/${account_alias}_${type}.json"
simp_le_file="./account_${type}.json"
if [[ -f "$file_full_path" ]]; then
# If there is no symlink to the account file, create it
if [[ ! -L "$simp_le_file" ]]; then
ln -sf "$file_full_path" "$simp_le_file" \
&& set_ownership_and_permissions "$simp_le_file"
# If the symlink target the wrong account file, replace it
elif [[ "$(readlink -f "$simp_le_file")" != "$file_full_path" ]]; then
ln -sf "$file_full_path" "$simp_le_file" \
&& set_ownership_and_permissions "$simp_le_file"
fi
fi
done
fi
echo "Creating/renewal $base_domain certificates... (${LE_HOST})"
simp_le \
-f account_key.json -f account_reg.json \
-f key.pem -f chain.pem -f fullchain.pem -f cert.pem \
"${params_d_arr[@]}" \
--cert_key_size="$cert_keysize" \
--server="$acme_ca_uri" \
--default_root /usr/share/nginx/html/
simp_le_return=$?
if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
mkdir -p "$account_dir"
for type in "key" "reg"; do
file_full_path="${account_dir}/${account_alias}_${type}.json"
simp_le_file="./account_${type}.json"
# If the account file to be reused does not exist yet, copy it
# from the CWD and replace the file in CWD with a symlink
if [[ ! -f "$file_full_path" && -f "$simp_le_file" ]]; then
cp "$simp_le_file" "$file_full_path"
ln -sf "$file_full_path" "$simp_le_file"
fi
done
fi
popd || return
if [[ $simp_le_return -ne 2 ]]; then
if [[ "$acme_ca_uri" == "$le_staging_uri" ]]; then
create_links "_test_$base_domain" "$LE_HOST"
else
create_links "$base_domain" "$LE_HOST"
fi
touch "${certificate_dir}/.companion"
# Set ownership and permissions of the files inside $certificate_dir
for file in .companion cert.pem key.pem chain.pem fullchain.pem account_key.json account_reg.json; do
file_path="${certificate_dir}/${file}"
[[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
done
account_path="/etc/nginx/certs/accounts/${acme_ca_uri#*://}"
account_key_perm_path="${account_path}/${account_alias}_key.json"
account_reg_perm_path="${account_path}/${account_alias}_reg.json"
# Account key and registration files do not necessarily exists after
# simp_le exit code 1. Check if they exist before perm check (#591).
[[ -f "$account_key_perm_path" ]] && set_ownership_and_permissions "$account_key_perm_path"
[[ -f "$account_reg_perm_path" ]] && set_ownership_and_permissions "$account_reg_perm_path"
# Set ownership and permissions of the ACME account folder and its
# parent folders (up to /etc/nginx/certs/accounts included)
until [[ "$account_path" == /etc/nginx/certs ]]; do
set_ownership_and_permissions "$account_path"
account_path="$(dirname "$account_path")"
done
fi
if [[ -e "/etc/nginx/certs/${LE_HOST}.crt" ]] && [[ -e "/etc/nginx/certs/${LE_HOST}.key" ]] && [[ -e "/etc/nginx/certs/${LE_HOST}.dhparam.pem" ]] && [[ -e "/etc/nginx/certs/${LE_HOST}.chain.pem" ]]; then
cat > "/etc/nginx/conf.d/${LE_HOST}-letsencrypt.conf" << EOF
server {
server_name ${LE_HOST};
listen 443 ssl http2 ;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_certificate /etc/nginx/certs/${LE_HOST}.crt;
ssl_certificate_key /etc/nginx/certs/${LE_HOST}.key;
ssl_dhparam /etc/nginx/certs/${LE_HOST}.dhparam.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/certs/${LE_HOST}.chain.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
include /etc/nginx/vhost.d/${LE_HOST}*;
include /etc/nginx/vhost.d/default*;
location / {
proxy_pass http://${LE_HOST}_upstream;
}
}
EOF
fi
if [[ -f "/etc/nginx/conf.d/standalone-cert-${LE_HOST}.conf" ]]; then
[[ $DEBUG == true ]] && echo "Debug: removing standalone configuration file /etc/nginx/conf.d/standalone-cert-${LE_HOST}.conf"
rm -f "/etc/nginx/conf.d/standalone-cert-${domain}.conf"
fi
if (( ${CONT_INDEX}+1 >= ${INDEX_MAX} )) && [ -z "${FOR_USER}" ]; then
FOR_USER="true"
CONT_INDEX=-1
INDEX_MAX="${#LETSENCRYPT_USER_HOSTS[@]}"
fi
done
cleanup_links
}
# Allow the script functions to be sourced without starting the Service Loop.
if [ "${1}" == "--source-only" ]; then
return 0
fi
pid=
# Service Loop: When this script exits, start it again.
trap '[[ $pid ]] && kill $pid; exec $0' EXIT
trap 'trap - EXIT' INT TERM
update_certs "$@"
# Wait some amount of time
echo "Sleep for ${seconds_to_wait}s"
sleep $seconds_to_wait & pid=$!
wait
pid=
#!/bin/bash
LETSENCRYPT_HOSTS=()
LETSENCRYPT_EMAILS=()
### BEGIN SERVICE ###
LETSENCRYPT_HOSTS+=( "${HOST}" )
LETSENCRYPT_EMAILS+=( "${EMAIL}" )
### END SERVICE ###
#!/bin/bash
# Using busybox pkill
pkill -USR1 -f /app/letsencrypt_service
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment