diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce82a11 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "acme.sh"] + path = acme.sh + url = https://github.com/acmesh-official/acme.sh diff --git a/README.md b/README.md index 6b16b8d..f734123 100644 --- a/README.md +++ b/README.md @@ -14,90 +14,47 @@ using lighttpd. ## Key features -* Uses [Acme.sh](https://github.com/acmesh-official/acme.sh) client for free TLS certificates from [Let's Encrypt](https://letsencrypt.org/) +* Uses the [acme.sh](https://github.com/acmesh-official/acme.sh) client to + obtain free TLS certificates from [Let's Encrypt](https://letsencrypt.org/) * Uses hook scripts to simplify issue and renewal process * Opportunistically opens and closes firewall port 80 * Restarts lighttpd to deploy certificates * Configures lighttpd for TLSv1.3 only following the [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/). -* Disables lighttpd from running insecurely on port 80 - - * HSTS handles the odd case where you forget or are too lazy to type in the - `https://` at the start. Just load the `https://` URL once and your browser - will remember for you forever. +* Configures lighttpd to upgrade unencrypted connections. +* Configures the [HSTS +header](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security). ## Installation This installs the project and files in `/srv`, which is the default path for external storage on a Turris device, but you can install wherever you'd like. -1. Download this project: +1. Download this project, including the `acme.sh` submodule: opkg install git-http - git clone https://github.com/davidjb/turris-omnia-tls.git /srv/turris-omnia-tls - -1. Determine the latest version of `acme.sh` by checking - https://github.com/acmesh-official/acme.sh/releases. Note the release - version (which is the tag name); you'll use it in the next step, - substituting for `[VERSION]`. - -1. Install `acme.sh` client and its dependency, `socat`; taking care to - substitute `[VERSION]` and `[YOUREMAIL]` with correct values: - - opkg install socat - git clone https://github.com/acmesh-official/acme.sh -b [VERSION] /srv/acme.sh - cd /srv/acme.sh - ./acme.sh --install --home /srv/.acme.sh --nocron --email [YOUREMAIL] --set-default-ca --server letsencrypt - -1. Disable the existing SSL configuration by removing the - `lighttpd-https-cert` package: - - opkg remove lighttpd-https-cert - -1. Stop `updater` from automatically reinstalling the `lighttpd-https-cert` - package: - - cp /srv/turris-omnia-tls/updater_custom.lua /etc/updater/conf.d/no-upstream-ssl.lua - -1. Make sure the `lighttpd-mod-openssl` package is installed: - - opkg install lighttpd-mod-openssl + git clone --recurse-submodules https://github.com/davidjb/turris-omnia-tls.git /srv/turris-omnia-tls -1. Lighttpd needs to stop listening on port 80 so modify - `/etc/lighttpd/conf.d/90-turris-root.conf` to comment out these lines: +1. Run the `install.sh` script and answer the questions: - $SERVER["socket"] == "*:80" { } - $SERVER["socket"] == "[::]:80" { } + /srv/turris-omnia-tls/install.sh -1. Stop lighttpd; we will enable it again shortly: +1. Alternatively, the answer to the questions can be provided via environment + variables for non-interactive/scripted use (check the source of `install.sh` + for a current list of supported variables): - /etc/init.d/lighttpd stop + TOT_EMAIL="foo@example.com" TOT_FQDN="turris.example.com" /srv/turris-omnia-tls/install.sh -1. Issue the certificate, taking care to specify your FQDN in place of - `[YOUR.DOMAIN.COM]`: +## Uninstallation - /srv/turris-omnia-tls/cert-issue.sh [YOUR.DOMAIN.COM] +Note that this will not touch issued certificates, which will be left in place +under `/etc/lighttpd/certs`. Also, `acme.sh` related state information will be +left untouched under the `/srv/turris-omnia-tls/var/` hierarchy. -1. Reconfigure lighttpd with the supplied custom configuration: +1. Run the `uninstall.sh` script to uninstall modifications performed by the + `install.sh` script: - cp /srv/turris-omnia-tls/lighttpd_custom.conf /etc/lighttpd/conf.d/40-ssl-acme-enable.conf - - Inside this file, replace the `domain.example.com` placeholders with your - FQDN. You can do this automatically by running the following command, - again taking care to specify your FQDN in place of `[YOUR.DOMAIN.COM]`: - - sed -i 's/domain.example.com/[YOUR.DOMAIN.COM]/g' /etc/lighttpd/conf.d/40-ssl-acme-enable.conf - -1. Restart `lighttpd`: - - /etc/init.d/lighttpd start - -1. Add crontab entry for renewal; pick a random minute and hour: - - echo '34 0 * * * /srv/turris-omnia-tls/cert-renew.sh > /dev/null' >> /etc/crontabs/root - - The renewal process will automatically re-use the settings for certificates - that were issued. + /srv/turris-omnia-tls/uninstall.sh ## Issuing more certificates @@ -111,15 +68,14 @@ inside `cert-issue.sh` before you run it the first time or go and modify the con that `acme.sh` generates in `/etc/lighttpd/certs/extra.example.com/extra.example.com.conf`, where `extra.example.com` is the name of your domain. -## Upgrading acme.sh +## Upgrading turris-omnia-tls and acme.sh -Run the following; after `fetch`ing, you'll see the latest version tag: - - cd /srv/acme.sh - git fetch - git checkout [VERSION] - ./acme.sh --install --home /srv/.acme.sh --nocron +Run the following: + cd /srv/turris-omnia-tls + git pull --recurse-submodules + ./install.sh + ## License MIT. See LICENSE.txt. diff --git a/acme.sh b/acme.sh new file mode 160000 index 0000000..e6959f0 --- /dev/null +++ b/acme.sh @@ -0,0 +1 @@ +Subproject commit e6959f093c4e147b4a206f0b5d027ff3d0a59b80 diff --git a/cert-issue.sh b/cert-issue.sh index 198f820..f0dc65d 100755 --- a/cert-issue.sh +++ b/cert-issue.sh @@ -1,18 +1,34 @@ -#!/bin/sh +#!/usr/bin/env bash +# +# Copyright (C) 2018-2022 David Beitey +# Copyright (C) 2022 David Härdeman +# +# SPDX-License-Identifier: MIT +# +# This script is used once for the initial issuance of a certificate. + +set -o nounset -o pipefail -o errexit -o errtrace + +cd "${0%/*}" +tothome="$(pwd)" certhome="/etc/lighttpd/certs" +acmehome="${tothome}/var/acme" ca_path="/etc/ssl/certs" +webroot="${tothome}/var/webroot" domain="$1" mkdir -p "$certhome" -/srv/.acme.sh/acme.sh \ - --home "/srv/.acme.sh" \ +mkdir -p "$webroot" + +"${acmehome}/acme.sh" \ + --home "${acmehome}" \ --issue \ - --standalone \ - --domain "$domain" \ + --webroot "${webroot}" \ + --domain "${domain}" \ --keylength 4096 \ - --certhome "$certhome" \ - --ca-path "$ca_path" \ - --pre-hook "/srv/turris-omnia-tls/pre-hook.sh '$domain'" \ - --post-hook "/srv/turris-omnia-tls/post-hook.sh '$domain'" \ - --renew-hook "/srv/turris-omnia-tls/renew-hook.sh '$domain'" \ - --reloadcmd "/srv/turris-omnia-tls/reloadcmd.sh '$domain'" + --certhome "${certhome}" \ + --ca-path "${ca_path}" \ + --pre-hook "${tothome}/pre-hook.sh '$domain'" \ + --post-hook "${tothome}/post-hook.sh '$domain'" \ + --renew-hook "${tothome}/renew-hook.sh '$domain'" \ + --reloadcmd "${tothome}/reloadcmd.sh '$domain'" diff --git a/cert-renew.sh b/cert-renew.sh index 75d4380..98e743f 100755 --- a/cert-renew.sh +++ b/cert-renew.sh @@ -1,8 +1,23 @@ -#!/bin/sh +#!/usr/bin/env bash +# +# Copyright (C) 2018-2022 David Beitey +# Copyright (C) 2022 David Härdeman +# +# SPDX-License-Identifier: MIT +# +# This script renews already issued certs and is meant to be executed +# periodically via e.g. cron. + +set -o nounset -o pipefail -o errexit -o errtrace + +cd "${0%/*}" +tothome="$(pwd)" certhome="/etc/lighttpd/certs" +acmehome="${tothome}/var/acme" ca_path="/etc/ssl/certs" -/srv/.acme.sh/acme.sh \ - --home "/srv/.acme.sh" \ + +"${acmehome}/acme.sh" \ + --home "${tothome}" \ --cron \ - --certhome "$certhome" \ - --ca-path "$ca_path" + --certhome "${certhome}" \ + --ca-path "${ca_path}" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c4c2ce2 --- /dev/null +++ b/install.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 David Härdeman +# +# SPDX-License-Identifier: MIT +# +# This script automates the installation of the Turris Omnia TLS project +# on a Turris Omnia router by asking a few questions (or obtaining them +# from environment variables). + +set -o nounset -o pipefail -o errexit -o errtrace + +cd "${0%/*}" + +TOT_SCRIPT="$(basename "${0}")" +readonly TOT_SCRIPT +TOT_BASEDIR="$(pwd)" +readonly TOT_BASEDIR +readonly TOT_ACMEDIR="${TOT_BASEDIR}/acme.sh" +readonly TOT_ACMEHOME="${TOT_BASEDIR}/var/acme" + +# Supported environment variables +TOT_EMAIL="${TOT_EMAIL:-}" +TOT_FQDN="${TOT_FQDN:-}" +TOT_HOSTNAME="${TOT_HOSTNAME:-}" + +log_message() { + # Display a log message if the script is used interactively, otherwise + # write the log message to syslog. + local msg="${1:-}" + + if [ -n "${msg}" ]; then + if [ -t 0 ]; then + echo "${TOT_SCRIPT}: ${msg}" 1>&2 + else + if type logger > /dev/null 2>&1; then + logger -t "${TOT_SCRIPT}[${PID}]" "${msg}" + fi + fi + fi +} + +template_install() { + # Install a template file to a given location, replacing variables + # formatted as %VARIABLE% with the proper value + local src="${1:-}" + local dst="${2:-}" + + if [ -n "${dst}" ] && [ -n "${src}" ]; then + sed \ + -e "s|%TOT_EMAIL%|${TOT_EMAIL}|g" \ + -e "s|%TOT_FQDN%|${TOT_FQDN}|g" \ + -e "s|%TOT_HOSTNAME%|${TOT_HOSTNAME}|g" \ + -e "s|%TOT_BASEDIR%|${TOT_BASEDIR}|g" \ + "${src}" > "${dst}" + fi +} + +add_crontab() { + local crontab="${1:-}" + local cmd="${TOT_BASEDIR}/cert-renew.sh" + + if [ -n "${crontab}" ]; then + echo 'MAILTO=""' > "${crontab}" + echo "$(( RANDOM % 60 )) $(( RANDOM % 24 )) * * * root ${cmd} > /dev/null" >> "${crontab}" + fi +} + +prompt_input() { + # Ask the user for some input, unless a given variable is + # already defined (e.g. via environment variables) + local varname="${1:-}" + local msg="${2:-}" + local value="" + + if [ -n "${varname}" ] && [ -z "${!varname}" ] && [ -n "${msg}" ]; then + if [ ! -t 0 ]; then + log_message "non-interactive mode failed, missing options" + exit 1 + fi + + read -r -p "${msg}: " value + + if [ -z "${value}" ]; then + log_message "missing value for $varname" + exit 1 + fi + declare -g "${varname}"="${value}" + fi +} + +prompt_input "TOT_EMAIL" "Enter your email address" +prompt_input "TOT_FQDN" "Enter the FQDN of your router" +if [ -n "${TOT_FQDN}" ] && [ -z "${TOT_HOSTNAME}" ]; then + TOT_HOSTNAME="$(echo "${TOT_FQDN}" | cut -d '.' -f1)" +fi + +log_message "Installing the socat package" +opkg install socat > /dev/null + +log_message "Installing the acme.sh script" +mkdir -p "${TOT_ACMEHOME}" +# Note that acme.sh fails to perform the installation if we don't chdir +cd "${TOT_ACMEDIR}" + +./acme.sh \ + --home "${TOT_ACMEHOME}" \ + --install \ + --no-profile \ + --nocron \ + --email "${TOT_EMAIL}" + +log_message "Setting the default CA" +./acme.sh \ + --home "${TOT_ACMEHOME}" \ + --set-default-ca \ + --server letsencrypt + +cd "${TOT_BASEDIR}" + +log_message "Removing the lighttpd-https-cert package" +opkg remove lighttpd-https-cert > /dev/null + +log_message "Stop updater from automatically reinstalling lighttpd-https-cert" +cp updater_custom.lua /etc/updater/conf.d/no-upstream-ssl.lua + +log_message "Installing the lighttpd-mod-openssl package" +opkg install lighttpd-mod-openssl > /dev/null + +log_message "Installing custom lighttp webroot configuration" +template_install "lighttpd_webroot.conf" "/etc/lighttpd/conf.d/39-acme-webroot.conf" + +log_message "Restarting lighttpd" +/etc/init.d/lighttpd restart +sleep 3 + +log_message "Issuing certificate" +./cert-issue.sh "${TOT_FQDN}" + +log_message "Installing custom lighttp TLS configuration" +template_install "lighttpd_tls.conf" "/etc/lighttpd/conf.d/40-acme-tls.conf" + +log_message "Restarting lighttpd again" +/etc/init.d/lighttpd restart +sleep 3 + +log_message "Adding a cron job for certificate renewal" +add_crontab "/etc/cron.d/turris-omnia-tls" diff --git a/lighttpd_custom.conf b/lighttpd_tls.conf similarity index 65% rename from lighttpd_custom.conf rename to lighttpd_tls.conf index 72e216a..0a0e4f5 100644 --- a/lighttpd_custom.conf +++ b/lighttpd_tls.conf @@ -2,11 +2,12 @@ # https://ssl-config.mozilla.org/ # Last verified: 22 October 2022 -server.port = 443 - -# Port 80 is disabled, but this doesn't hurt... $HTTP["scheme"] == "http" { - url.redirect = ("" => "https://${url.authority}${url.path}${qsa}") + $HTTP["host"] == "%TOT_HOSTNAME%" { + url.redirect = ("" => "http://%TOT_FQDN%${url.path}${qsa}") + } else $HTTP["url"] !~ "^/.well-known/acme-challenge/" { + url.redirect = ("" => "https://${url.authority}${url.path}${qsa}") + } } $HTTP["scheme"] == "https" { @@ -24,7 +25,8 @@ $HTTP["scheme"] == "https" { # (to avoid having to repeat ssl.* directives in both ":443" and "[::]:443") $SERVER["socket"] == ":443" { ssl.engine = "enable" } $SERVER["socket"] == "[::]:443" { ssl.engine = "enable" } -ssl.privkey = "/etc/lighttpd/certs/domain.example.com/domain.example.com.key" -ssl.pemfile = "/etc/lighttpd/certs/domain.example.com/domain.example.com.cer" +ssl.privkey = "/etc/lighttpd/certs/%TOT_FQDN%/%TOT_FQDN%.key" +ssl.pemfile = "/etc/lighttpd/certs/%TOT_FQDN%/%TOT_FQDN%.cer" +ssl.ca-file = "/etc/lighttpd/certs/%TOT_FQDN%/fullchain.cer" ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.3") ssl.openssl.ssl-conf-cmd += ("Options" => "-ServerPreference") diff --git a/lighttpd_webroot.conf b/lighttpd_webroot.conf new file mode 100644 index 0000000..53f2075 --- /dev/null +++ b/lighttpd_webroot.conf @@ -0,0 +1,6 @@ +# Handle the acme.sh webroot + +alias.url = ( + "/.well-known/acme-challenge" => + "%TOT_BASEDIR%/var/webroot/.well-known/acme-challenge" +) diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..24fd1a7 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 David Härdeman +# +# SPDX-License-Identifier: MIT +# +# This script automates the uninstallation of the Turris Omnia TLS project +# on a Turris Omnia router. Certificates will not be deleted from the +# file system. + +set -o nounset -o pipefail -o errexit -o errtrace + +log_message() { + # Display a log message if the script is used interactively, otherwise + # write the log message to syslog. + local msg="${1:-}" + + if [ -n "${msg}" ]; then + if [ -t 0 ]; then + echo "${TOT_SCRIPT}: ${msg}" 1>&2 + else + if type logger > /dev/null 2>&1; then + logger -t "${TOT_SCRIPT}[${PID}]" "${msg}" + fi + fi + fi +} + +log_message "Removing cron job for certificate renewal" +rm -f "/etc/cron.d/turris-omnia-tls" + +log_message "Removing custom lighttp webroot configuration" +rm -f "/etc/lighttpd/conf.d/39-acme-webroot.conf" + +log_message "Removing custom lighttp TLS configuration" +rm -f "/etc/lighttpd/conf.d/40-acme-tls.conf" + +log_message "Removing updater configuration" +rm -f "/etc/updater/conf.d/no-upstream-ssl.lua" + +log_message "Reinstalling the lighttpd-https-cert package" +opkg install lighttpd-https-cert > /dev/null + +log_message "Restarting lighttpd" +/etc/init.d/lighttpd restart diff --git a/var/.gitignore b/var/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/var/.gitignore @@ -0,0 +1 @@ +* diff --git a/var/webroot/.gitignore b/var/webroot/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/var/webroot/.gitignore @@ -0,0 +1 @@ +*