Add automated TLS renewal and deployment documentation

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
Raghav
2026-04-17 08:44:28 +05:30
parent 249bed66e2
commit 08990f6420
15 changed files with 347 additions and 172 deletions

View File

@ -6,60 +6,68 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SSL_DIR="${REPO_DIR}/nginx/ssl"
DOMAIN="nxt.bhatfamily.in"
echo "==> Ensuring required packages are installed (docker, docker-compose, ufw, openssl)..."
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is not installed. Install Docker and rerun this script."
echo "ERROR: Docker is not installed. Install Docker and rerun this script."
exit 1
fi
if ! command -v docker compose >/dev/null 2>&1 && ! command -v docker-compose >/dev/null 2>&1; then
echo "docker compose / docker-compose is not installed. Install Docker Compose and rerun."
if ! docker compose version >/dev/null 2>&1 && ! command -v docker-compose >/dev/null 2>&1; then
echo "ERROR: docker compose / docker-compose is not installed."
exit 1
fi
if ! command -v openssl >/dev/null 2>&1; then
echo "ERROR: openssl is not installed."
exit 1
fi
if ! command -v ufw >/dev/null 2>&1; then
echo "ufw not found. Installing ufw requires root and internet access."
echo "INFO: ufw not found. Ensure ports 8082 and 8446 are open in your firewall/router."
fi
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
else
docker-compose "$@"
fi
}
mkdir -p "${SSL_DIR}"
echo "==> Generating self-signed TLS certificate for ${DOMAIN} (valid 365 days)..."
echo "==> Ensuring TLS files exist for nginx startup..."
if [ ! -f "${SSL_DIR}/${DOMAIN}.crt" ] || [ ! -f "${SSL_DIR}/${DOMAIN}.key" ]; then
echo "==> Generating bootstrap self-signed certificate for ${DOMAIN} (valid 365 days)..."
openssl req -x509 -nodes -newkey rsa:4096 \
-keyout "${SSL_DIR}/${DOMAIN}.key" \
-out "${SSL_DIR}/${DOMAIN}.crt" \
-days 365 \
-subj "/CN=${DOMAIN}"
else
echo "Certificate already exists, skipping generation."
echo "==> Existing certificate/key found; skipping bootstrap certificate generation."
fi
if [ ! -f "${SSL_DIR}/dhparam.pem" ]; then
echo "==> Generating dhparam (this may take a while)..."
echo "==> Generating dhparam (one-time, may take a while)..."
openssl dhparam -out "${SSL_DIR}/dhparam.pem" 2048
fi
echo "==> Configuring UFW firewall rules (allow 8082/tcp and 8446/tcp)..."
if command -v ufw >/dev/null 2>&1; then
echo "==> Configuring UFW firewall rules (8082/tcp, 8446/tcp)..."
sudo ufw allow 8082/tcp comment "Nextcloud HTTP"
sudo ufw allow 8446/tcp comment "Nextcloud HTTPS"
else
echo "ufw not installed; ensure ports 8082 and 8446 are open in your firewall/router."
fi
echo "==> Starting Nextcloud stack via Docker Compose..."
cd "${REPO_DIR}"
echo "==> Pulling and starting containers..."
compose -f "${REPO_DIR}/docker-compose.yml" pull
compose -f "${REPO_DIR}/docker-compose.yml" up -d
if command -v docker compose >/dev/null 2>&1; then
docker compose pull
docker compose up -d
else
docker-compose pull
docker-compose up -d
fi
echo "==> Nextcloud should now be reachable at:"
echo "==> Stack started"
echo " http://${DOMAIN}:8082 (redirects to HTTPS)"
echo " https://${DOMAIN}:8446"
echo ""
echo "NOTE: Browser will warn about self-signed certificate. Replace with a valid cert for production."
echo
echo "For production TLS with Let's Encrypt DNS-01 (Cloudflare):"
echo " 1) export CF_DNS_API_TOKEN=<token>"
echo " 2) export LETSENCRYPT_EMAIL=<email>"
echo " 3) ./scripts/provision-production-tls.sh"
echo " 4) docker compose restart web"

View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# scripts/provision-production-tls.sh
# Issue/renew Let's Encrypt certificate via DNS-01 (Cloudflare), then install
# cert/key into nginx/ssl paths used by the existing Nginx config.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DOMAIN="${DOMAIN:-nxt.bhatfamily.in}"
SSL_DIR="${REPO_DIR}/nginx/ssl"
LE_DIR="${SSL_DIR}/letsencrypt"
PROPAGATION_SECONDS="${CF_DNS_PROPAGATION_SECONDS:-60}"
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: docker is required but not found."
exit 1
fi
if [ -z "${CF_DNS_API_TOKEN:-}" ]; then
echo "ERROR: CF_DNS_API_TOKEN is not set."
echo "Export it first, then rerun this script."
exit 1
fi
if [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
echo "ERROR: LETSENCRYPT_EMAIL is not set."
echo "Export it first, then rerun this script."
exit 1
fi
mkdir -p "${LE_DIR}" "${SSL_DIR}"
CF_CREDS_FILE="$(mktemp)"
cleanup() {
rm -f "${CF_CREDS_FILE}"
}
trap cleanup EXIT
chmod 600 "${CF_CREDS_FILE}"
printf 'dns_cloudflare_api_token = %s\n' "${CF_DNS_API_TOKEN}" > "${CF_CREDS_FILE}"
echo "==> Requesting/renewing Let's Encrypt cert for ${DOMAIN} via Cloudflare DNS-01..."
docker run --rm \
-v "${LE_DIR}:/etc/letsencrypt" \
-v "${CF_CREDS_FILE}:/run/secrets/cloudflare.ini:ro" \
certbot/dns-cloudflare:latest certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /run/secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds "${PROPAGATION_SECONDS}" \
--non-interactive \
--agree-tos \
--email "${LETSENCRYPT_EMAIL}" \
--keep-until-expiring \
--preferred-challenges dns-01 \
--rsa-key-size 4096 \
-d "${DOMAIN}"
echo "==> Installing production certificate into nginx/ssl..."
docker run --rm \
-v "${LE_DIR}:/etc/letsencrypt:ro" \
-v "${SSL_DIR}:/work" \
alpine:3.20 sh -lc " test -f /etc/letsencrypt/live/${DOMAIN}/fullchain.pem && \
test -f /etc/letsencrypt/live/${DOMAIN}/privkey.pem && \
cp /etc/letsencrypt/live/${DOMAIN}/fullchain.pem /work/${DOMAIN}.crt && \
cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem /work/${DOMAIN}.key && \
chmod 0644 /work/${DOMAIN}.crt && \
chmod 0600 /work/${DOMAIN}.key "
echo "==> Production TLS material installed:"
echo " ${SSL_DIR}/${DOMAIN}.crt"
echo " ${SSL_DIR}/${DOMAIN}.key"
echo ""
echo "Next step: restart web service (or full stack) to pick up the new certificate."

69
scripts/renew-production-tls.sh Executable file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# scripts/renew-production-tls.sh
# Non-interactive renewal wrapper:
# - loads renewal env defaults
# - loads Cloudflare token export script
# - runs certificate provisioning
# - restarts only nextcloud-web when certificate changed
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DOMAIN="${DOMAIN:-nxt.bhatfamily.in}"
SSL_DIR="${REPO_DIR}/nginx/ssl"
TLS_ENV_FILE="${TLS_ENV_FILE:-${REPO_DIR}/.tls-renewal.env}"
CLOUDFLARE_TOKEN_SCRIPT_DEFAULT="${HOME}/bin/cloudflare-api-usertoken.sh"
if [ -f "${TLS_ENV_FILE}" ]; then
# shellcheck disable=SC1090
set -a
source "${TLS_ENV_FILE}"
set +a
fi
CLOUDFLARE_TOKEN_SCRIPT="${CLOUDFLARE_TOKEN_SCRIPT:-${CLOUDFLARE_TOKEN_SCRIPT_DEFAULT}}"
if [ -z "${CF_DNS_API_TOKEN:-}" ] && [ -f "${CLOUDFLARE_TOKEN_SCRIPT}" ]; then
# shellcheck disable=SC1090
source "${CLOUDFLARE_TOKEN_SCRIPT}"
fi
if [ -z "${CF_DNS_API_TOKEN:-}" ]; then
echo "ERROR: CF_DNS_API_TOKEN is not set and could not be loaded from ${CLOUDFLARE_TOKEN_SCRIPT}."
exit 1
fi
if [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
echo "ERROR: LETSENCRYPT_EMAIL is not set."
echo "Set it in ${TLS_ENV_FILE} or export it in the shell before running this script."
exit 1
fi
mkdir -p "${REPO_DIR}/logs"
CERT_FILE="${SSL_DIR}/${DOMAIN}.crt"
BEFORE_SHA=""
if [ -f "${CERT_FILE}" ]; then
BEFORE_SHA="$(sha256sum "${CERT_FILE}" | awk '{print $1}')"
fi
echo "==> Running production TLS provisioning/renewal..."
"${REPO_DIR}/scripts/provision-production-tls.sh"
AFTER_SHA=""
if [ -f "${CERT_FILE}" ]; then
AFTER_SHA="$(sha256sum "${CERT_FILE}" | awk '{print $1}')"
fi
if [ "${BEFORE_SHA}" != "${AFTER_SHA}" ]; then
echo "==> Certificate changed; restarting nextcloud-web..."
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
docker compose -f "${REPO_DIR}/docker-compose.yml" restart web
else
docker-compose -f "${REPO_DIR}/docker-compose.yml" restart web
fi
else
echo "==> Certificate unchanged; no container restart required."
fi
echo "==> Renewal workflow complete."

52
scripts/setup-renewal-cron.sh Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# scripts/setup-renewal-cron.sh
# Installs/updates a daily cron entry for automated TLS renewal.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TLS_ENV_FILE="${REPO_DIR}/.tls-renewal.env"
LOG_DIR="${REPO_DIR}/logs"
LOG_FILE="${LOG_DIR}/tls-renew.log"
CRON_MARKER="# nextcloud-docker tls renewal"
CRON_SCHEDULE="${RENEW_CRON_SCHEDULE:-17 3 * * *}"
DEFAULT_TOKEN_SCRIPT="${HOME}/bin/cloudflare-api-usertoken.sh"
if [ -f "${TLS_ENV_FILE}" ]; then
# shellcheck disable=SC1090
set -a
source "${TLS_ENV_FILE}"
set +a
fi
if [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
echo "ERROR: LETSENCRYPT_EMAIL is not set."
echo "Export it in your shell before running this script."
exit 1
fi
CLOUDFLARE_TOKEN_SCRIPT="${CLOUDFLARE_TOKEN_SCRIPT:-${DEFAULT_TOKEN_SCRIPT}}"
CF_DNS_PROPAGATION_SECONDS="${CF_DNS_PROPAGATION_SECONDS:-60}"
mkdir -p "${LOG_DIR}"
chmod 700 "${LOG_DIR}"
cat > "${TLS_ENV_FILE}" <<ENVFILE
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
CLOUDFLARE_TOKEN_SCRIPT=${CLOUDFLARE_TOKEN_SCRIPT}
CF_DNS_PROPAGATION_SECONDS=${CF_DNS_PROPAGATION_SECONDS}
ENVFILE
chmod 600 "${TLS_ENV_FILE}"
CRON_COMMAND="cd ${REPO_DIR} && /usr/bin/env bash ${REPO_DIR}/scripts/renew-production-tls.sh >> ${LOG_FILE} 2>&1"
CRON_LINE="${CRON_SCHEDULE} ${CRON_COMMAND} ${CRON_MARKER}"
{
crontab -l 2>/dev/null | grep -v "${CRON_MARKER}" || true
echo "${CRON_LINE}"
} | crontab -
echo "==> Installed cron renewal job:"
echo " ${CRON_LINE}"
echo "==> Stored renewal defaults in ${TLS_ENV_FILE}"
echo "==> Logs will be written to ${LOG_FILE}"

View File

@ -5,23 +5,20 @@ set -euo pipefail
DOMAIN="nxt.bhatfamily.in"
HTTP_PORT=8082
HTTPS_PORT=8446
STRICT_TLS="${STRICT_TLS:-0}"
CURL_TLS_ARGS=("-k")
if [ "${STRICT_TLS}" = "1" ]; then
CURL_TLS_ARGS=()
fi
echo "==> Testing HTTP redirect..."
curl -I "http://${DOMAIN}:${HTTP_PORT}" || {
echo "HTTP test failed."
exit 1
}
curl -I "http://${DOMAIN}:${HTTP_PORT}" >/dev/null
echo "==> Testing HTTPS endpoint (ignoring self-signed cert errors)..."
curl -k -I "https://${DOMAIN}:${HTTPS_PORT}" || {
echo "HTTPS test failed."
exit 1
}
echo "==> Testing HTTPS endpoint..."
curl "${CURL_TLS_ARGS[@]}" -I "https://${DOMAIN}:${HTTPS_PORT}" >/dev/null
echo "==> Quick application-level check (Nextcloud status.php)..."
curl -k "https://${DOMAIN}:${HTTPS_PORT}/status.php" || {
echo "status.php test failed."
exit 1
}
echo "==> Testing Nextcloud status.php..."
curl "${CURL_TLS_ARGS[@]}" "https://${DOMAIN}:${HTTPS_PORT}/status.php" >/dev/null
echo "All tests passed."