Add Hub update script and document hardening changes
Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
66
README.md
66
README.md
@ -5,6 +5,7 @@ Exposed ports:
|
|||||||
- HTTPS: `8446`
|
- HTTPS: `8446`
|
||||||
Target hostname:
|
Target hostname:
|
||||||
- `nxt.bhatfamily.in`
|
- `nxt.bhatfamily.in`
|
||||||
|
|
||||||
## What changed
|
## What changed
|
||||||
The stack now includes:
|
The stack now includes:
|
||||||
- Fixed Nginx mount path (`nginx/nginx.conf` mapped correctly)
|
- 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)
|
- Nginx reverse proxy mode for `nextcloud:apache` (no FastCGI mismatch)
|
||||||
- Production TLS provisioning using Let's Encrypt DNS-01 with Cloudflare
|
- Production TLS provisioning using Let's Encrypt DNS-01 with Cloudflare
|
||||||
- Automated TLS renewal job support (cron)
|
- 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
|
## Prerequisites
|
||||||
- Ubuntu host with Docker + Docker Compose plugin (or `docker-compose`)
|
- Ubuntu host with Docker + Docker Compose plugin (or `docker-compose`)
|
||||||
- Domain `nxt.bhatfamily.in` in Cloudflare DNS
|
- Domain `nxt.bhatfamily.in` in Cloudflare DNS
|
||||||
- DNS A record for `nxt` pointing to your server public IP (DNS-only)
|
- DNS A record for `nxt` pointing to your server public IP (DNS-only)
|
||||||
- Router/firewall forwarding for ports `8082` and `8446`
|
- Router/firewall forwarding for ports `8082` and `8446`
|
||||||
|
|
||||||
## Initial setup
|
## Initial setup
|
||||||
1. Create runtime env file:
|
1. Create runtime env file:
|
||||||
```bash
|
```bash
|
||||||
@ -31,14 +39,47 @@ cp .env.example .env
|
|||||||
```bash
|
```bash
|
||||||
./scripts/test.sh
|
./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
|
## 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.
|
Use the migration helper to move existing data to a host path and switch the app to a bind mount.
|
||||||
|
|
||||||
Default target:
|
Default target:
|
||||||
- `/media/rbhat/DATA/nextcloud/NextCloudData`
|
- `/media/rbhat/DATA/nextcloud/NextCloudData`
|
||||||
|
|
||||||
Run migration:
|
Run migration:
|
||||||
```bash
|
```bash
|
||||||
./scripts/migrate-data-directory.sh /media/rbhat/DATA/nextcloud/NextCloudData
|
./scripts/migrate-data-directory.sh /media/rbhat/DATA/nextcloud/NextCloudData
|
||||||
```
|
```
|
||||||
|
|
||||||
What the script does:
|
What the script does:
|
||||||
- enables maintenance mode
|
- enables maintenance mode
|
||||||
- copies current `/var/www/html/data` content to target directory
|
- 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`
|
- updates `docker-compose.yml` app volume with `...:/var/www/html/data`
|
||||||
- recreates `app` and `web` services
|
- recreates `app` and `web` services
|
||||||
- disables maintenance mode and verifies mount
|
- disables maintenance mode and verifies mount
|
||||||
|
|
||||||
Rollback (if needed):
|
Rollback (if needed):
|
||||||
1. Remove the `:/var/www/html/data` bind mount line from `app` volumes in `docker-compose.yml`.
|
1. Remove the `:/var/www/html/data` bind mount line from `app` volumes in `docker-compose.yml`.
|
||||||
2. `docker compose up -d app web`
|
2. `docker compose up -d app web`
|
||||||
@ -72,6 +114,7 @@ docker compose restart web
|
|||||||
```bash
|
```bash
|
||||||
echo | openssl s_client -connect nxt.bhatfamily.in:8446 -servername nxt.bhatfamily.in 2>/dev/null | openssl x509 -noout -subject -issuer -dates
|
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)
|
## Automated renewal job (cron)
|
||||||
1. Ensure your Cloudflare token export script exists (default path used by renewal wrapper):
|
1. Ensure your Cloudflare token export script exists (default path used by renewal wrapper):
|
||||||
- `~/bin/cloudflare-api-usertoken.sh`
|
- `~/bin/cloudflare-api-usertoken.sh`
|
||||||
@ -87,6 +130,7 @@ This script will:
|
|||||||
```bash
|
```bash
|
||||||
./scripts/renew-production-tls.sh
|
./scripts/renew-production-tls.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Admin password reset
|
## Admin password reset
|
||||||
List existing users:
|
List existing users:
|
||||||
```bash
|
```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.
|
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
|
## Useful commands
|
||||||
Start/update containers:
|
Start/update containers:
|
||||||
```bash
|
```bash
|
||||||
@ -119,6 +183,8 @@ Stop and remove containers/volumes:
|
|||||||
```bash
|
```bash
|
||||||
./scripts/uninstall.sh
|
./scripts/uninstall.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
- `.env`, `.tls-renewal.env`, and runtime cert material under `nginx/ssl` are intentionally ignored by Git.
|
- `.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.
|
- If secrets were ever committed earlier, rotate them.
|
||||||
|
|||||||
@ -26,6 +26,14 @@ services:
|
|||||||
image: nextcloud:29-apache
|
image: nextcloud:29-apache
|
||||||
container_name: nextcloud-app
|
container_name: nextcloud-app
|
||||||
restart: unless-stopped
|
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_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@ -38,6 +46,7 @@ services:
|
|||||||
- NEXTCLOUD_TRUSTED_DOMAINS=nxt.bhatfamily.in
|
- NEXTCLOUD_TRUSTED_DOMAINS=nxt.bhatfamily.in
|
||||||
- NEXTCLOUD_OVERWRITEHOST=nxt.bhatfamily.in:8446
|
- NEXTCLOUD_OVERWRITEHOST=nxt.bhatfamily.in:8446
|
||||||
- NEXTCLOUD_OVERWRITEPROTOCOL=https
|
- NEXTCLOUD_OVERWRITEPROTOCOL=https
|
||||||
|
- XDG_CACHE_HOME=/tmp/.cache
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
# nginx/nginx.conf
|
# nginx/nginx.conf
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name nxt.bhatfamily.in;
|
server_name nxt.bhatfamily.in;
|
||||||
@ -20,9 +22,12 @@ server {
|
|||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
client_max_body_size 10240M;
|
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 / {
|
location / {
|
||||||
proxy_pass http://nextcloud-app:80;
|
proxy_pass http://nextcloud-app:80;
|
||||||
@ -35,5 +40,6 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Port 8446;
|
proxy_set_header X-Forwarded-Port 8446;
|
||||||
proxy_read_timeout 3600;
|
proxy_read_timeout 3600;
|
||||||
proxy_send_timeout 3600;
|
proxy_send_timeout 3600;
|
||||||
|
proxy_hide_header X-Powered-By;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
scripts/update-nextcloud-hub.sh
Executable file
115
scripts/update-nextcloud-hub.sh
Executable file
@ -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"
|
||||||
Reference in New Issue
Block a user