diff --git a/Dockerfile b/Dockerfile index 3ad0a32..9e16b7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Multi-stage build - See https://docs.docker.com/engine/userguide/eng-image/multistage-build -FROM ubnt/unms:0.11.0 as unms +FROM ubnt/unms:0.11.1 as unms -FROM oznu/s6-node:8.9.1 +FROM oznu/s6-node:8.9.4 # Copy UNMS app from offical image since the source code is not published at this time COPY --from=unms /home/app/unms /app @@ -27,7 +27,8 @@ RUN devDeps="musl-dev gcc python python-dev py-pip libffi-dev openssl-dev" \ && apk del ${devDeps} \ && echo "abc ALL=(ALL) NOPASSWD: /usr/sbin/nginx -s *" >> /etc/sudoers -ENV PATH=/app/node_modules/.bin:$PATH \ +ENV NODE_ENV=production \ + PATH=/app/node_modules/.bin:$PATH \ PGDATA=/config/postgres \ POSTGRES_DB=unms \ HOME=/var/lib/rabbitmq \ diff --git a/Dockerfile.raspberry-pi b/Dockerfile.raspberry-pi index 771f3ee..dbf145a 100644 --- a/Dockerfile.raspberry-pi +++ b/Dockerfile.raspberry-pi @@ -1,7 +1,7 @@ # Multi-stage build - See https://docs.docker.com/engine/userguide/eng-image/multistage-build -FROM ubnt/unms:0.11.0 as unms +FROM ubnt/unms:0.11.1 as unms -FROM oznu/s6-node:8.9.1-armhf +FROM oznu/s6-node:8.9.4-armhf # Copy UNMS app from offical image since the source code is not published at this time COPY --from=unms /home/app/unms /app @@ -27,7 +27,8 @@ RUN devDeps="musl-dev gcc python python-dev py-pip libffi-dev openssl-dev" \ && apk del ${devDeps} \ && echo "abc ALL=(ALL) NOPASSWD: /usr/sbin/nginx -s *" >> /etc/sudoers -ENV PATH=/app/node_modules/.bin:$PATH \ +ENV NODE_ENV=production \ + PATH=/app/node_modules/.bin:$PATH \ PGDATA=/config/postgres \ POSTGRES_DB=unms \ HOME=/var/lib/rabbitmq \ diff --git a/root/cert.sh b/root/cert.sh index 50a01b6..9dea235 100755 --- a/root/cert.sh +++ b/root/cert.sh @@ -5,9 +5,29 @@ set -e echo "Running cert.sh $*" domain=$1 -# don't do anything if user provides a custom certificate +# if custom certificate is used, make sure that it is up to date if [ ! -z "${SSL_CERT}" ]; then - echo "Custom certificate is set up, exiting" + CERT_FILE="/config/unms/cert/live.crt" + KEY_FILE="/config/unms/cert/live.key" + if ! [ "${CERT_FILE}" -ot "/config/usercert/${SSL_CERT}" ] \ + && ! [ "${KEY_FILE}" -ot "/config/usercert/${SSL_CERT_KEY}" ] \ + && ([ -z "${SSL_CERT_CA}" ] || ! [ "${CERT_FILE}" -ot "/config/usercert/${SSL_CERT_CA}" ]); + then + echo "Custom SSL certificate not changed, exiting" + exit 0 + fi + + if [ ! -z "${SSL_CERT_CA}" ]; then + echo "Joining '/config/usercert/${SSL_CERT}' and '/config/usercert/${SSL_CERT_CA}' into '${CERT_FILE}'" + cat "/config/usercert/${SSL_CERT}" "/config/usercert/${SSL_CERT_CA}" > /cert/live.crt + else + echo "Copying '/config/usercert/${SSL_CERT}' to '${CERT_FILE}'" + cp -a "/config/usercert/${SSL_CERT}" ${CERT_FILE} + fi + cp -a "/config/usercert/${SSL_CERT_KEY}" ${KEY_FILE} + + echo "Reloading Nginx configuration" + sudo /usr/sbin/nginx -s reload exit 0 fi @@ -23,7 +43,15 @@ if [ -f "/config/unms/cert/${domain}.crt" ] && [ -f "/config/unms/cert/${domain} echo "Found existing self-signed certificate for ${domain}" else echo "Generating self-signed certificate for ${domain}" - SAN="DNS:${domain}" openssl req -nodes -x509 -newkey rsa:4096 -subj "/CN=${domain}" -keyout "/config/unms/cert/${domain}.key" -out "/config/unms/cert/${domain}.crt" -days "36500" -batch -config "/defaults/openssl.cnf" + + # determine subjectAltName - IP addressess need both IP and DNS, domains just need DNS + case "${domain}" in + *:*) SAN="IP:${domain},DNS:${domain}" ;; # contains ":" - IPv6 address + *[0-9]) SAN="IP:${domain},DNS:${domain}" ;; # ends with a digit - IPv4 address + *) SAN="DNS:${domain}" ;; # else domain name + esac + + SAN="${SAN}" openssl req -nodes -x509 -newkey rsa:4096 -subj "/CN=${domain}" -keyout "/config/unms/cert/${domain}.key" -out "/config/unms/cert/${domain}.crt" -days "36500" -batch -config "/defaults/openssl.cnf" fi ln -fs "./${domain}.crt" "/config/unms/cert/live.crt" ln -fs "./${domain}.key" "/config/unms/cert/live.key" diff --git a/root/defaults/combined.conf.template b/root/defaults/combined.conf.template index f12515b..a92af20 100644 --- a/root/defaults/combined.conf.template +++ b/root/defaults/combined.conf.template @@ -6,9 +6,8 @@ map $http_upgrade $upstream { server { listen ##HTTPS_PORT##; - ##SSL_CERTIFICATE## - ##SSL_CERTIFICATE_KEY## - ##SSL_CERTIFICATE_CA## + ssl_certificate /config/unms/cert/live.crt; + ssl_certificate_key /config/unms/cert/live.key; ssl on; ssl_session_cache builtin:1000 shared:SSL:10m; diff --git a/root/defaults/https.conf.template b/root/defaults/https.conf.template index 5c42c3d..b90c8bd 100644 --- a/root/defaults/https.conf.template +++ b/root/defaults/https.conf.template @@ -1,9 +1,8 @@ server { listen ##HTTPS_PORT##; - ##SSL_CERTIFICATE## - ##SSL_CERTIFICATE_KEY## - ##SSL_CERTIFICATE_CA## + ssl_certificate /config/unms/cert/live.crt; + ssl_certificate_key /config/unms/cert/live.key; ssl on; ssl_session_cache builtin:1000 shared:SSL:10m; diff --git a/root/defaults/nginx.conf.template b/root/defaults/nginx.conf.template index 29d30d2..6bb8781 100644 --- a/root/defaults/nginx.conf.template +++ b/root/defaults/nginx.conf.template @@ -28,6 +28,18 @@ http { proxy_temp_path /tmp 1 2; sendfile on; + set_real_ip_from ##LOCAL_NETWORK##; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + # limit device connection rate + map $http_upgrade $limit { + default device_connecting; + '' ""; + } + limit_req_zone $limit zone=throttle:1m rate=50r/s; + limit_req zone=throttle burst=500; + map $http_upgrade $connection_upgrade { default upgrade; '' close; @@ -78,16 +90,25 @@ http { -- allowed characters are [a-z A-Z 0-9 . : -] if not string.find(domain, "^[%w%.:-]+$") then print('/letsencrypt called with invalid domain "' .. domain .. '"'); - ngx.status = 500 + ngx.status = 400 ngx.say('Invalid domain') - return ngx.exit(500) + return ngx.exit(400) + end + + function execute(command) + -- returns success, error code, output. + local f = io.popen(command..' 2>&1 && echo " $?"') + local output = f:read"*a" + local begin, finish, code = output:find" (%d+)\n$" + output, code = output:sub(1, begin, -1), tonumber(code) + return code == 0 and true or false, code, output end print('Calling letsencrypt.sh "' .. domain .. '"'); - result=os.execute('/letsencrypt.sh "' .. domain .. '"') - if result ~= 0 then + success, code, output = execute('/letsencrypt.sh "' .. domain .. '"') + if code ~= 0 then ngx.status = 500 - ngx.say('Failed to create SSL certificate using Let\'s Encrypt') + ngx.say(output) return ngx.exit(500) else ngx.say('OK') diff --git a/root/etc/cont-init.d/70-nginx b/root/etc/cont-init.d/70-nginx index 577a02c..4e06a28 100644 --- a/root/etc/cont-init.d/70-nginx +++ b/root/etc/cont-init.d/70-nginx @@ -5,6 +5,9 @@ echo "Creating /www directory" mkdir /www chown -R abc:abc /www +# determine local network address +export LOCAL_NETWORK=$(ip route | tail -1 | cut -d' ' -f1) || true + # create Nginx config files from templates echo "Creating Nginx config files" rm -f /etc/nginx/conf.d/* @@ -26,6 +29,21 @@ if [ -z "${SSL_CERT}" ] && [ ! -f "/config/unms/cert/live.crt" ] && [ -d "/confi rm -rf /config/unms/cert/accounts fi +# If a self signed certificate exists from UNMS versins without integrated nginx, reuse it. This is necessary, +# because UNMS UI will report an update failure if the certificate changes after the update. +# This requires determining the Common Name and renaming the certificate files. +if [ -z "${SSL_CERT}" ] && [ -f "/config/unms/cert/self-signed.crt" ] && [ -f "/config/unms/cert/self-signed.key" ]; then + echo "Found old certificate files, extracting Common Name..." + commonName=$(openssl x509 -noout -subject -in /config/unms/cert/self-signed.crt 2>/dev/null | sed -n '/^subject/s/^.*CN=//p' || true) + if [ ! -z "${commonName}" ]; then + echo "Renaming old certificate files from 'self-signed' to '${commonName}'" + mv -f "/config/unms/cert/self-signed.crt" "/config/unms/cert/${commonName}.crt" || echo "Failed to rename self-signed.crt to ${commonName}.crt" + mv -f "/config/unms/cert/self-signed.key" "/config/unms/cert/${commonName}.key" || echo "Failed to rename self-signed.key to ${commonName}.key" + else + echo "Failed to extract Common Name from old certificate file, will not reuse" + fi +fi + # generate self-signed SSL certificate if none is provided or existing if [ -z "${SSL_CERT}" ]; then if [ -f /config/unms/cert/live.crt ] && [ -f /config/unms/cert/live.key ]; then @@ -33,8 +51,18 @@ if [ -z "${SSL_CERT}" ]; then else echo "Generating self-signed certificate without domain names" SAN="DNS:localhost" openssl req -nodes -x509 -newkey "rsa:2048" -subj "/CN=localhost" -keyout "/config/unms/cert/live.key" -out "/config/unms/cert/live.crt" -days "36500" -batch -config "/defaults/openssl.cnf" - chown -R abc:abc /config/unms/cert/* + chown -R abc /config/unms/cert/* fi else echo "Will use custom SSL certificate" + cp -a "/config/usercert/${SSL_CERT_KEY}" /config/unms/cert/live.key + if [ -z "${SSL_CERT_CA}" ]; then + cp -a "/config/usercert/${SSL_CERT}" /config/unms/cert/live.crt + else + # Unlike previous nodejs implementation, nginx needs certificate and chain + # in one file. + echo "Joining '/config/usercert/${SSL_CERT}' and '/config/usercert/${SSL_CERT_CA}' into '/config/unms/cert/live.crt'" + cat "/config/usercert/${SSL_CERT}" "/config/usercert/${SSL_CERT_CA}" > /cert/live.crt + fi + chown -R abc /config/unms/cert/* fi diff --git a/root/fill-template.sh b/root/fill-template.sh index 5207a9c..538bf0f 100755 --- a/root/fill-template.sh +++ b/root/fill-template.sh @@ -10,21 +10,10 @@ echo "Running fill-template.sh $*" cp -f "${in}" "${out}" +sed -i -- "s|##LOCAL_NETWORK##|${LOCAL_NETWORK}|g" "${out}" sed -i -- "s|##HTTP_PORT##|${HTTP_PORT}|g" "${out}" sed -i -- "s|##HTTPS_PORT##|${HTTPS_PORT}|g" "${out}" sed -i -- "s|##WS_PORT##|${WS_PORT}|g" "${out}" sed -i -- "s|##UNMS_HTTP_PORT##|${UNMS_HTTP_PORT}|g" "${out}" sed -i -- "s|##UNMS_WS_PORT##|${UNMS_WS_PORT}|g" "${out}" sed -i -- "s|##PUBLIC_HTTPS_PORT##|${PUBLIC_HTTPS_PORT}|g" "${out}" - -if [ -z "${SSL_CERT}" ]; then - sed -i -- "s|##SSL_CERTIFICATE##|ssl_certificate /config/unms/cert/live.crt;|g" "${out}" - sed -i -- "s|##SSL_CERTIFICATE_KEY##|ssl_certificate_key /config/unms/cert/live.key;|g" "${out}" - sed -i -- "s|##SSL_CERTIFICATE_CA##||g" "${out}" -else - sed -i -- "s|##SSL_CERTIFICATE##|ssl_certificate /config/unms/cert/${SSL_CERT};|g" "${out}" - sed -i -- "s|##SSL_CERTIFICATE_KEY##|ssl_certificate_key /config/unms/cert/${SSL_CERT_KEY};|g" "${out}" - if [ ! -z "${SSL_CERT_CA}" ]; then - sed -i -- "s|##SSL_CERTIFICATE_CA##|ssl_certificate_ca /config/unms/cert/${SSL_CERT_CA};|g" "${out}" - fi -fi diff --git a/root/letsencrypt.sh b/root/letsencrypt.sh index 295ceb8..4d6e95c 100755 --- a/root/letsencrypt.sh +++ b/root/letsencrypt.sh @@ -21,7 +21,7 @@ if echo "${domain}" | grep "[0-9]$" &>/dev/null \ || echo "${domain}" | grep "^[^.]*$" &>/dev/null \ || echo "${domain}" | grep ":" &>/dev/null then - echo "Cannot use Let's Encrypt for ${domain}" + echo "Let's Encrypt can only be used for fully qualified domain names." else echo "Generating certificate for ${domain} using Let's Encrypt" if certbot certonly \