From 08990f6420003843ac8cfda044e93605578a4362 Mon Sep 17 00:00:00 2001 From: Raghav <1858838+quantumrag@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:44:28 +0530 Subject: [PATCH] Add automated TLS renewal and deployment documentation Co-Authored-By: Oz --- .env | 10 --- .env.example | 9 +++ .gitignore | 12 +++ .tls-renewal.env.example | 5 ++ README.md | 116 ++++++++++++++++++++-------- docker-compose.yml | 2 - nginx/ssl/.gitkeep | 0 nginx/ssl/dhparam.pem | 8 -- nginx/ssl/nxt.bhatfamily.in.crt | 30 ------- nginx/ssl/nxt.bhatfamily.in.key | 52 ------------- scripts/install.sh | 56 ++++++++------ scripts/provision-production-tls.sh | 73 +++++++++++++++++ scripts/renew-production-tls.sh | 69 +++++++++++++++++ scripts/setup-renewal-cron.sh | 52 +++++++++++++ scripts/test.sh | 25 +++--- 15 files changed, 347 insertions(+), 172 deletions(-) delete mode 100644 .env create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .tls-renewal.env.example create mode 100644 nginx/ssl/.gitkeep delete mode 100644 nginx/ssl/dhparam.pem delete mode 100644 nginx/ssl/nxt.bhatfamily.in.crt delete mode 100644 nginx/ssl/nxt.bhatfamily.in.key create mode 100755 scripts/provision-production-tls.sh create mode 100755 scripts/renew-production-tls.sh create mode 100755 scripts/setup-renewal-cron.sh diff --git a/.env b/.env deleted file mode 100644 index 28097cd..0000000 --- a/.env +++ /dev/null @@ -1,10 +0,0 @@ -# .env -# Database -NEXTCLOUD_DB_NAME=nextcloud -NEXTCLOUD_DB_USER=nextcloud -NEXTCLOUD_DB_PASSWORD=u@ZFN8y8WzwVPgE -NEXTCLOUD_DB_ROOT_PASSWORD=FNEyZG6pzPv*uf9 - -# Nextcloud admin -NEXTCLOUD_ADMIN_USER=admin -NEXTCLOUD_ADMIN_PASSWORD=7VZxFLJd-8Lb*pp diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..38d104e --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Database +NEXTCLOUD_DB_NAME=nextcloud +NEXTCLOUD_DB_USER=nextcloud +NEXTCLOUD_DB_PASSWORD=change-me +NEXTCLOUD_DB_ROOT_PASSWORD=change-me + +# Nextcloud admin +NEXTCLOUD_ADMIN_USER=admin +NEXTCLOUD_ADMIN_PASSWORD=change-me diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f41e0ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Local runtime configuration +.env +.tls-renewal.env + +# Runtime TLS artifacts (never commit private keys/certs) +nginx/ssl/*.crt +nginx/ssl/*.key +nginx/ssl/*.pem +nginx/ssl/letsencrypt/ + +# Runtime logs +logs/ diff --git a/.tls-renewal.env.example b/.tls-renewal.env.example new file mode 100644 index 0000000..864ce4b --- /dev/null +++ b/.tls-renewal.env.example @@ -0,0 +1,5 @@ +# Used by scripts/renew-production-tls.sh in non-interactive contexts (e.g. cron) +# Copy to .tls-renewal.env and adjust values. +LETSENCRYPT_EMAIL=you@example.com +CLOUDFLARE_TOKEN_SCRIPT=$HOME/bin/cloudflare-api-usertoken.sh +CF_DNS_PROPAGATION_SECONDS=60 diff --git a/README.md b/README.md index a196f6f..da36d1b 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,86 @@ # Nextcloud on Ubuntu via Docker for nxt.bhatfamily.in - -This repo deploys Nextcloud using Docker on Ubuntu, fronted by Nginx on: - -- HTTP: 8082 -- HTTPS: 8446 - -It is intended to serve the hostname **nxt.bhatfamily.in**. - +This repository deploys Nextcloud behind Nginx using Docker Compose. +Exposed ports: +- HTTP: `8082` +- HTTPS: `8446` +Target hostname: +- `nxt.bhatfamily.in` +## What changed +The stack now includes: +- Fixed Nginx mount path (`nginx/nginx.conf` mapped correctly) +- Fixed MariaDB command (`mariadbd`) +- Nginx reverse proxy mode for `nextcloud:apache` (no FastCGI mismatch) +- Production TLS provisioning using Let's Encrypt DNS-01 with Cloudflare +- Automated TLS renewal job support (cron) ## Prerequisites - -- Ubuntu host with Docker and Docker Compose installed -- Static public IP (or Cloudflare Tunnel) -- Domain `bhatfamily.in` managed in Cloudflare -- Basic familiarity with UFW and router port forwarding - -## DNS (Cloudflare) - -1. In Cloudflare DNS for `bhatfamily.in`, create: - - - Type: A - - Name: `nxt` - - IPv4: your Ubuntu server public IP - - Proxy: DNS only (grey cloud) - -2. Forward ports 8082 and 8446 from your router to the Ubuntu host. - -## First-time setup - -1. Clone this repo and enter directory: - - ```bash - git clone nextcloud-docker-nxt.bhatfamily.in - cd nextcloud-docker-nxt.bhatfamily.in +- Ubuntu host with Docker + Docker Compose plugin (or `docker-compose`) +- Domain `nxt.bhatfamily.in` in Cloudflare DNS +- DNS A record for `nxt` pointing to your server public IP (DNS-only) +- Router/firewall forwarding for ports `8082` and `8446` +## Initial setup +1. Create runtime env file: +```bash +cp .env.example .env +``` +2. Edit `.env` with strong values. +3. Start stack with bootstrap TLS: +```bash +./scripts/install.sh +``` +4. Validate: +```bash +./scripts/test.sh +``` +## Production TLS (Let's Encrypt + Cloudflare DNS-01) +1. Export credentials in shell: +```bash +export CF_DNS_API_TOKEN={{CF_DNS_API_TOKEN}} +export LETSENCRYPT_EMAIL={{LETSENCRYPT_EMAIL}} +``` +2. Issue/renew and install production cert: +```bash +./scripts/provision-production-tls.sh +``` +3. Reload Nginx container: +```bash +docker compose restart web +``` +4. Verify cert: +```bash +echo | openssl s_client -connect nxt.bhatfamily.in:8446 -servername nxt.bhatfamily.in 2>/dev/null | openssl x509 -noout -subject -issuer -dates +``` +## Automated renewal job (cron) +1. Ensure your Cloudflare token export script exists (default path used by renewal wrapper): +- `~/bin/cloudflare-api-usertoken.sh` +2. Install/update renewal cron entry: +```bash +./scripts/setup-renewal-cron.sh +``` +This script will: +- create/update `.tls-renewal.env` (local only, not committed) +- install a daily cron job (`03:17` by default) +- write logs to `logs/tls-renew.log` +3. Manual renewal run (same path cron uses): +```bash +./scripts/renew-production-tls.sh +``` +## Useful commands +Start/update containers: +```bash +docker compose up -d +``` +Restart all services: +```bash +docker compose restart +``` +Restart web only: +```bash +docker compose restart web +``` +Stop and remove containers/volumes: +```bash +./scripts/uninstall.sh +``` +## Security notes +- `.env`, `.tls-renewal.env`, and runtime cert material under `nginx/ssl` are intentionally ignored by Git. +- If secrets were ever committed earlier, rotate them. diff --git a/docker-compose.yml b/docker-compose.yml index 22fa304..7dabfdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,4 @@ # docker-compose.yml -version: "3.8" - services: db: image: mariadb:11 diff --git a/nginx/ssl/.gitkeep b/nginx/ssl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nginx/ssl/dhparam.pem b/nginx/ssl/dhparam.pem deleted file mode 100644 index 167ef3b..0000000 --- a/nginx/ssl/dhparam.pem +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN DH PARAMETERS----- -MIIBCAKCAQEA6URKKHp177eP2+UP//DCEmAP63DpZNtVQeC0PO3x14Jomg6bicVa -ySBkEGtSHw2WMtcGe/4P0AyrPuVL8VgIUvFXzLO2j8XeHZ/Gu5LWZw3bAwONBUC2 -N4l2msyNMRoPBnu/BjjlYEkDEsqo5VX38ZdaCSa7ZgseYAWV524jW2VewB58ox6s -c0iw5aoq1XrJp5+mgj/XG2i0xO5DvdLfDa/Yvslhi8MT7GLvfoHziLcybB949ZK4 -PSCPvvZE17P5oQhRgLhCWe2VW7TchRLy4FTk8Iv2HV7ndDzZEsXIGjy8MgyDQNCd -YxIIa1Fkw90Bzw8htyNX3AcakTlWy8d/ZwIBAg== ------END DH PARAMETERS----- diff --git a/nginx/ssl/nxt.bhatfamily.in.crt b/nginx/ssl/nxt.bhatfamily.in.crt deleted file mode 100644 index c85bf6d..0000000 --- a/nginx/ssl/nxt.bhatfamily.in.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFGTCCAwGgAwIBAgIUHAA348XpRrN8g/F5MM2n1PRwGOQwDQYJKoZIhvcNAQEL -BQAwHDEaMBgGA1UEAwwRbnh0LmJoYXRmYW1pbHkuaW4wHhcNMjYwNDE3MDIzMDU4 -WhcNMjcwNDE3MDIzMDU4WjAcMRowGAYDVQQDDBFueHQuYmhhdGZhbWlseS5pbjCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKygVD9ln901VJIhiq7cDu5Q -mxvCN+jlfMzSDeIri7SDqBKFgLh5l+c0SddNpKmRqnSLWAFPrJbKRXuwmE5sAD6t -wPDKiwxpX09IP6lhBojjKAPRpHNCWw0JRJJgOYixm7/dsDIenTK55AuQkIeM9Iic -R6ohzoeJ+zDF+etGedh0O+UI7XDhZJECUOGUJBxSPepeETww436FNJfoLx7VxS+4 -VF65Zq7uqv18FhqO1gab4q/ydwZ9AbIILfUdZXWbd6z3DbmKug0FPyFOASwfNCFD -mJ7dIFBsOu6mhRpV881Gjitn3O5RwrIC0QJ4R2FQ/SKhat1GUlmNNx9ELt+VH6VB -+jyNk9/e3pEHKlweKki6HW8tB/MT7ZZkk0/qD0Kg/SEqLsxblmnLv5s6GXLPfMNR -yqV7Gs0ZlXbCkFSYrMSh5A4NACJUtGpq5rTZNKtn9taMHGuO3l0h8K88Su0t2nRM -yxBgL6WizvdsQvqHowzjQVnBds6cTUwR7QLxUUbKr4uuZoo0lleUlHGvubNDdOIE -6NKuJBB1rcENO5B6tITPRrOYrurDwVPVvHBfLlYHEQvvaypWQtDpcf39GTHCIekK -C4NysXrDGbshcK9fH30VwiR78kkvXLShbytGL4aq6yf9Gp77iMSt69GUCR4wIGMu -ck7H96LmA82vx8Q+bZhDAgMBAAGjUzBRMB0GA1UdDgQWBBTV67Dyn+iaubohmUDq -OBp78vgKJTAfBgNVHSMEGDAWgBTV67Dyn+iaubohmUDqOBp78vgKJTAPBgNVHRMB -Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA4t0pSJViW66Ue+KX5YD3GWW5Z -RQy8XwrPqHqFHU8yjf5jeK2bsBXQC5Ovr/dQnlLs2GdGvHFmthcWFlpzF4zRzVFK -jy2ZgqDYV+ET0rPqpGkz3iThTRLWAEm7q6gwZtmGQSdpuKK5Ei47TqfOtq/r0Juy -cg12PQs3wUuavNXWVz/uRis+RZiWZhn9+xz6XRhEPZjTOyIoS65frm1uvFv96Sw+ -0Lrxn2e/oFRuV0yrnaIV/bJjPvf0/VGVrQ0DmWfrJ1ywXwl+Ys/Ib9LmVTU0tdlL -vVR5a/6Lg5iSNiQqm8VrLspFj5iv6x65ygb8klbUxHz7zPmwGRrG8UxscEg9pF5u -TaZfCk+V6VSp+VjdLUUdmn4UGSN2tPMBdIwQjZAsBHN6pPaTE9EpxOqBQG64rUzt -jR8damZ3X+qHaS8Lz3wvRFYr1NNVC2oTx5+1neM0zfSEcGkmNlkONcmkWghzggI/ -g7xTm3oSlrUKqw+cSoksMaa8bCuXMR0CtM8x/sPZgwiDwQVQOGBVQKmnMbMq9eGh -QkGHAERUeGUNArdYeYxzjFWQ1EmWIy9kjmVIePt4qNpGQWQ/lk2XTkScsG4GrQ1q -JVqFP1RwTkZfgo1KPTT1FrxVawuIiSemaqmTw8y08zKYw3UJxwVhgo50cmiSiCh7 -UunNLvfJ2QgFnV0yYw== ------END CERTIFICATE----- diff --git a/nginx/ssl/nxt.bhatfamily.in.key b/nginx/ssl/nxt.bhatfamily.in.key deleted file mode 100644 index 9b7e2e5..0000000 --- a/nginx/ssl/nxt.bhatfamily.in.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -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 -6RSKsmjGRIZba+3ia3R8g8Z2rwhv ------END PRIVATE KEY----- diff --git a/scripts/install.sh b/scripts/install.sh index 7a35383..d55006c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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=" +echo " 2) export LETSENCRYPT_EMAIL=" +echo " 3) ./scripts/provision-production-tls.sh" +echo " 4) docker compose restart web" diff --git a/scripts/provision-production-tls.sh b/scripts/provision-production-tls.sh new file mode 100755 index 0000000..10804d7 --- /dev/null +++ b/scripts/provision-production-tls.sh @@ -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." diff --git a/scripts/renew-production-tls.sh b/scripts/renew-production-tls.sh new file mode 100755 index 0000000..a6d278e --- /dev/null +++ b/scripts/renew-production-tls.sh @@ -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." diff --git a/scripts/setup-renewal-cron.sh b/scripts/setup-renewal-cron.sh new file mode 100755 index 0000000..a9ba35c --- /dev/null +++ b/scripts/setup-renewal-cron.sh @@ -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}" </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}" diff --git a/scripts/test.sh b/scripts/test.sh index e8a18ea..fc58959 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -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."