diff --git a/README.md b/README.md index ccbb111..08ba4ff 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Exposed ports: - HTTPS: `8446` Target hostname: - `nxt.bhatfamily.in` + ## What changed The stack now includes: - Fixed Nginx mount path (`nginx/nginx.conf` mapped correctly) @@ -12,11 +13,18 @@ The stack now includes: - 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) +- Nextcloud app startup fixes for Apache `ServerName` and writable Fontconfig cache +- Reverse-proxy trust configuration in Nextcloud (`trusted_proxies`, `forwarded_for_headers`) +- Nginx hardening (`server_tokens off`, stronger HSTS, hide `X-Powered-By`, TLS session hardening) +- Brute-force protection explicitly enabled and maintenance window configured +- New scripted Nextcloud Hub upgrade workflow: `scripts/update-nextcloud-hub.sh` + ## Prerequisites - 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 @@ -31,14 +39,47 @@ cp .env.example .env ```bash ./scripts/test.sh ``` + +## Update Nextcloud Hub (scripted) +Use the upgrade helper script to pull new images, apply the upgrade, run post-upgrade repairs, and validate endpoints. + +Run update: +```bash +./scripts/update-nextcloud-hub.sh +``` + +Optional flags: +- Skip app marketplace updates: +```bash +RUN_APP_UPDATES=0 ./scripts/update-nextcloud-hub.sh +``` +- Require strict TLS validation during smoke tests (no `-k`): +```bash +STRICT_TLS=1 ./scripts/update-nextcloud-hub.sh +``` + +What the script does: +- pulls latest `db`, `app`, and `web` images +- recreates services via Compose +- enables maintenance mode and runs `occ upgrade` +- runs `occ app:update --all` (unless disabled) +- runs schema and repair commands (`db:add-missing-*`, `maintenance:repair`) +- disables maintenance mode +- runs `occ status`, `occ setupchecks`, and `scripts/test.sh` + +> Recommendation: take a filesystem/database backup before major Hub upgrades. + ## Move Nextcloud data directory to external storage Use the migration helper to move existing data to a host path and switch the app to a bind mount. + Default target: - `/media/rbhat/DATA/nextcloud/NextCloudData` + Run migration: ```bash ./scripts/migrate-data-directory.sh /media/rbhat/DATA/nextcloud/NextCloudData ``` + What the script does: - enables maintenance mode - copies current `/var/www/html/data` content to target directory @@ -46,6 +87,7 @@ What the script does: - updates `docker-compose.yml` app volume with `...:/var/www/html/data` - recreates `app` and `web` services - disables maintenance mode and verifies mount + Rollback (if needed): 1. Remove the `:/var/www/html/data` bind mount line from `app` volumes in `docker-compose.yml`. 2. `docker compose up -d app web` @@ -72,6 +114,7 @@ docker compose restart web ```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` @@ -87,6 +130,7 @@ This script will: ```bash ./scripts/renew-production-tls.sh ``` + ## Admin password reset List existing users: ```bash @@ -102,6 +146,26 @@ NEW_NEXTCLOUD_PASSWORD={{NEW_NEXTCLOUD_PASSWORD}} ./scripts/reset-admin-password ``` You can target a different username by passing it as the first argument. +## Operational/security changes applied (Apr 2026) +Applied and validated in this deployment: +- `docker-compose.yml` (`app` service): + - startup command now ensures writable `/var/cache/fontconfig` + - sets Apache `ServerName nxt.bhatfamily.in` + - sets `XDG_CACHE_HOME=/tmp/.cache` +- `nginx/nginx.conf`: + - `server_tokens off` + - HSTS set to `max-age=63072000; includeSubDomains; preload` + - `proxy_hide_header X-Powered-By` + - `ssl_session_cache`, `ssl_session_timeout`, `ssl_session_tickets off` +- Nextcloud `occ` settings: + - `trusted_proxies` configured to Docker network subnet + - `forwarded_for_headers` set to `HTTP_X_FORWARDED_FOR` + - `auth.bruteforce.protection.enabled=true` + - `maintenance_window_start=1` + - `weather_status` app disabled to remove repeated PHP warning noise +- Host security hygiene: + - `.env` permission reduced to `600` + ## Useful commands Start/update containers: ```bash @@ -119,6 +183,8 @@ 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. +- Keep `.env` mode restricted (`chmod 600 .env`). - If secrets were ever committed earlier, rotate them. diff --git a/docker-compose.yml b/docker-compose.yml index 511416f..517eb6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,14 @@ services: image: nextcloud:29-apache container_name: nextcloud-app restart: unless-stopped + command: + - /bin/sh + - -c + - | + mkdir -p /var/cache/fontconfig + chown -R www-data:www-data /var/cache/fontconfig + printf "ServerName nxt.bhatfamily.in\n" > /etc/apache2/conf-enabled/servername.conf + exec apache2-foreground env_file: - .env environment: @@ -38,6 +46,7 @@ services: - NEXTCLOUD_TRUSTED_DOMAINS=nxt.bhatfamily.in - NEXTCLOUD_OVERWRITEHOST=nxt.bhatfamily.in:8446 - NEXTCLOUD_OVERWRITEPROTOCOL=https + - XDG_CACHE_HOME=/tmp/.cache depends_on: - db volumes: diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 6eb2015..afba31a 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,4 +1,6 @@ # nginx/nginx.conf +server_tokens off; + server { listen 80; server_name nxt.bhatfamily.in; @@ -20,9 +22,12 @@ server { ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers HIGH:!aNULL:!MD5; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; client_max_body_size 10240M; - add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; location / { proxy_pass http://nextcloud-app:80; @@ -35,5 +40,6 @@ server { proxy_set_header X-Forwarded-Port 8446; proxy_read_timeout 3600; proxy_send_timeout 3600; + proxy_hide_header X-Powered-By; } } diff --git a/scripts/update-nextcloud-hub.sh b/scripts/update-nextcloud-hub.sh new file mode 100755 index 0000000..fcc7647 --- /dev/null +++ b/scripts/update-nextcloud-hub.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# scripts/update-nextcloud-hub.sh +# Update Nextcloud Hub and supporting containers, run upgrade tasks, +# and execute post-update validation. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPOSE_FILE="${REPO_DIR}/docker-compose.yml" +APP_CONTAINER="${NEXTCLOUD_APP_CONTAINER:-nextcloud-app}" +RUN_APP_UPDATES="${RUN_APP_UPDATES:-1}" + +compose() { + if docker compose version >/dev/null 2>&1; then + docker compose "$@" + else + docker-compose "$@" + fi +} + +require_command() { + local cmd="$1" + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "ERROR: '${cmd}' is required but not installed." + exit 1 + fi +} + +wait_for_occ() { + local max_attempts=45 + local attempt=1 + + while [ "${attempt}" -le "${max_attempts}" ]; do + if docker exec --user www-data "${APP_CONTAINER}" php occ status >/dev/null 2>&1; then + return 0 + fi + + echo "Waiting for Nextcloud app container to become ready (${attempt}/${max_attempts})..." + sleep 2 + attempt=$((attempt + 1)) + done + + return 1 +} + +require_command docker +require_command curl + +if ! docker ps >/dev/null 2>&1; then + echo "ERROR: Docker daemon is not reachable." + exit 1 +fi + +if ! compose -f "${COMPOSE_FILE}" ps >/dev/null 2>&1; then + echo "ERROR: Unable to access compose project using ${COMPOSE_FILE}." + exit 1 +fi + +echo "==> Pulling latest images for db/app/web" +compose -f "${COMPOSE_FILE}" pull db app web + +echo "==> Recreating services" +compose -f "${COMPOSE_FILE}" up -d db app web + +if ! wait_for_occ; then + echo "ERROR: Nextcloud OCC did not become ready in time." + exit 1 +fi + +maintenance_enabled=0 +cleanup() { + if [ "${maintenance_enabled}" -eq 1 ]; then + docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:mode --off >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +echo "==> Enabling maintenance mode" +docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:mode --on +maintenance_enabled=1 + +echo "==> Running Nextcloud upgrade" +docker exec --user www-data "${APP_CONTAINER}" php occ upgrade + +if [ "${RUN_APP_UPDATES}" = "1" ]; then + echo "==> Updating installed apps" + docker exec --user www-data "${APP_CONTAINER}" php occ app:update --all +else + echo "==> Skipping app:update --all (RUN_APP_UPDATES=${RUN_APP_UPDATES})" +fi + +echo "==> Running database/schema remediation commands" +docker exec --user www-data "${APP_CONTAINER}" php occ db:add-missing-columns || true +docker exec --user www-data "${APP_CONTAINER}" php occ db:add-missing-indices || true +docker exec --user www-data "${APP_CONTAINER}" php occ db:add-missing-primary-keys || true + +echo "==> Running maintenance repair" +docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:repair + +echo "==> Disabling maintenance mode" +docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:mode --off +maintenance_enabled=0 + +echo "==> Running post-update checks" +docker exec --user www-data "${APP_CONTAINER}" php occ status +docker exec --user www-data "${APP_CONTAINER}" php occ setupchecks || true + +if [ -x "${REPO_DIR}/scripts/test.sh" ]; then + echo "==> Running endpoint smoke tests" + STRICT_TLS="${STRICT_TLS:-0}" "${REPO_DIR}/scripts/test.sh" +else + echo "WARN: scripts/test.sh is not executable; skipping smoke tests." +fi + +echo "==> Nextcloud Hub update workflow completed successfully"