From 52922b8e7f22c607477f912c74a083963cc5e711 Mon Sep 17 00:00:00 2001 From: b0txec Date: Thu, 20 Aug 2026 19:22:03 +0300 Subject: [PATCH] Prepare isolated VPS UAT deployment bundle --- .gitignore | 4 ++ deploy/vps/Dockerfile | 32 +++++++++ deploy/vps/README.md | 53 ++++++++++++++ deploy/vps/compose.yml | 70 +++++++++++++++++++ deploy/vps/config/authelia/configuration.yml | 64 +++++++++++++++++ .../authelia/users_database.example.yml | 11 +++ deploy/vps/environment.example | 8 +++ deploy/vps/nginx/laikapstak.li.conf.example | 45 ++++++++++++ .../weathertool-authelia-authrequest.conf | 14 ++++ .../weathertool-authelia-location.conf | 25 +++++++ .../vps/nginx/snippets/weathertool-proxy.conf | 12 ++++ docs/DEVELOPMENT_AND_STAGING.md | 7 +- docs/README.md | 4 +- docs/VPS_STAGING_PLAN.md | 47 +++++++++---- 14 files changed, 379 insertions(+), 17 deletions(-) create mode 100644 deploy/vps/Dockerfile create mode 100644 deploy/vps/README.md create mode 100644 deploy/vps/compose.yml create mode 100644 deploy/vps/config/authelia/configuration.yml create mode 100644 deploy/vps/config/authelia/users_database.example.yml create mode 100644 deploy/vps/environment.example create mode 100644 deploy/vps/nginx/laikapstak.li.conf.example create mode 100644 deploy/vps/nginx/snippets/weathertool-authelia-authrequest.conf create mode 100644 deploy/vps/nginx/snippets/weathertool-authelia-location.conf create mode 100644 deploy/vps/nginx/snippets/weathertool-proxy.conf diff --git a/.gitignore b/.gitignore index 5c527f4..b783064 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ logs/ .DS_Store .env .env-fly +deploy/vps/.env.staging +deploy/vps/config/authelia/users_database.yml +deploy/vps/secrets/ +deploy/vps/data/ data/* !project/assembly.sbt !src/main/scala/data diff --git a/deploy/vps/Dockerfile b/deploy/vps/Dockerfile new file mode 100644 index 0000000..cbde825 --- /dev/null +++ b/deploy/vps/Dockerfile @@ -0,0 +1,32 @@ +FROM node:22.14.0-alpine AS web-build + +WORKDIR /build/web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/ ./ +RUN npm run build -- --mode production + +FROM hseeberger/scala-sbt:17.0.2_1.8.2_2.13.10 AS scala-build + +WORKDIR /build +COPY build.sbt ./ +COPY project/ ./project/ +COPY src/ ./src/ +RUN sbt clean assembly + +FROM amazoncorretto:17-alpine + +RUN addgroup -S weather && adduser -S -G weather weather +WORKDIR /app +RUN mkdir -p /app/data/tmp /app/data/grids /app/web/dist && chown -R weather:weather /app + +COPY --from=scala-build --chown=weather:weather /build/target/scala-2.13/app.jar /app/app.jar +COPY --from=web-build --chown=weather:weather /build/web/dist/ /app/web/dist/ + +USER weather +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1 + +CMD ["java", "-Xms128m", "-Xmx512m", "-XX:+UseG1GC", "-jar", "/app/app.jar"] diff --git a/deploy/vps/README.md b/deploy/vps/README.md new file mode 100644 index 0000000..3d8a7bc --- /dev/null +++ b/deploy/vps/README.md @@ -0,0 +1,53 @@ +# WeatherTool VPS deployment bundle + +This directory contains the reviewed templates for the temporary UAT deployment. +It is intentionally separate from the Rocky development Compose project. + +## Boundaries + +- Compose project: `weathertool-uat` +- WeatherTool host binding: `127.0.0.1:8002` +- Authelia host binding: `127.0.0.1:9091` +- PostgreSQL: private Docker network only; no host port +- Scheduled provider jobs: disabled +- Existing VPS applications, networks, volumes, and Nginx sites: untouched + +The committed configuration contains no live password, password hash, or +cryptographic secret. Runtime material belongs only under `/srv/weathertool`. + +## Runtime layout + +Copy the committed bundle into `/srv/weathertool`, then create these untracked +paths before the first start: + +```text +/srv/weathertool/ + .env.staging + compose.yml + config/authelia/configuration.yml + config/authelia/users_database.yml + data/app/ + data/authelia/ + data/postgres/ + secrets/jwt_secret + secrets/session_secret + secrets/storage_encryption_key +``` + +The Nginx files remain examples until the domain, Cloudflare origin certificate, +trusted client-address configuration, and private upstream tests are complete. +Do not enable the virtual host merely because the containers start successfully. + +## Release image + +Build the exact committed revision on Rocky: + +```bash +git archive HEAD | docker build \ + -f deploy/vps/Dockerfile \ + -t "weathertool:$(git rev-parse HEAD)" - +``` + +Export the image, calculate its SHA-256 checksum, transfer both files, verify the +checksum on the VPS, and only then load it. The detailed operator sequence is in +`docs/VPS_STAGING_PLAN.md`. diff --git a/deploy/vps/compose.yml b/deploy/vps/compose.yml new file mode 100644 index 0000000..a3f10a8 --- /dev/null +++ b/deploy/vps/compose.yml @@ -0,0 +1,70 @@ +name: weathertool-uat + +services: + postgres: + image: postgres:16.1 + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env.staging} + POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env.staging} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env.staging} + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - backend + security_opt: + - no-new-privileges:true + + app: + image: ${WEATHERTOOL_IMAGE:?set WEATHERTOOL_IMAGE in .env.staging} + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB} + DB_USER: ${POSTGRES_USER} + DB_PASSWORD: ${POSTGRES_PASSWORD} + ENABLE_SCHEDULED_JOBS: "false" + DEBUG: "false" + ports: + - "127.0.0.1:${WEATHERTOOL_PORT:-8002}:8080" + volumes: + - ./data/app:/app/data + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + networks: + - backend + security_opt: + - no-new-privileges:true + + authelia: + image: authelia/authelia:4.39.20 + restart: unless-stopped + environment: + AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: /run/secrets/jwt_secret + AUTHELIA_SESSION_SECRET_FILE: /run/secrets/session_secret + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /run/secrets/storage_encryption_key + ports: + - "127.0.0.1:${AUTHELIA_PORT:-9091}:9091" + volumes: + - ./config/authelia:/config:ro + - ./data/authelia:/var/lib/authelia + - ./secrets/jwt_secret:/run/secrets/jwt_secret:ro + - ./secrets/session_secret:/run/secrets/session_secret:ro + - ./secrets/storage_encryption_key:/run/secrets/storage_encryption_key:ro + networks: + - backend + security_opt: + - no-new-privileges:true + +networks: + backend: + driver: bridge + internal: true diff --git a/deploy/vps/config/authelia/configuration.yml b/deploy/vps/config/authelia/configuration.yml new file mode 100644 index 0000000..3fdc632 --- /dev/null +++ b/deploy/vps/config/authelia/configuration.yml @@ -0,0 +1,64 @@ +server: + address: tcp4://0.0.0.0:9091 + endpoints: + authz: + auth-request: + implementation: AuthRequest + +log: + level: info + format: text + +identity_validation: + reset_password: + disable: true + +authentication_backend: + password_reset: + disable: true + refresh_interval: 5m + file: + path: /config/users_database.yml + watch: true + password: + algorithm: argon2 + argon2: + variant: argon2id + iterations: 3 + memory: 65536 + parallelism: 4 + key_length: 32 + salt_length: 16 + +access_control: + default_policy: deny + rules: + - domain: laikapstak.li + policy: one_factor + +session: + name: weathertool_session + cookies: + - domain: laikapstak.li + authelia_url: https://auth.laikapstak.li + default_redirection_url: https://laikapstak.li + same_site: lax + inactivity: 1h + expiration: 12h + remember_me: -1 + +regulation: + modes: + - ip + max_retries: 3 + find_time: 2m + ban_time: 15m + +storage: + local: + path: /var/lib/authelia/db.sqlite3 + +notifier: + disable_startup_check: false + filesystem: + filename: /var/lib/authelia/notification.txt diff --git a/deploy/vps/config/authelia/users_database.example.yml b/deploy/vps/config/authelia/users_database.example.yml new file mode 100644 index 0000000..7d63e7f --- /dev/null +++ b/deploy/vps/config/authelia/users_database.example.yml @@ -0,0 +1,11 @@ +# Generate the password hash on the VPS; never place a plaintext password here. +# Then copy this file to users_database.yml, replace the placeholders, and set +# mode 0640. The real users_database.yml is excluded from Git. +users: + newsroom: + disabled: false + displayname: Newsroom tester + password: REPLACE_WITH_ARGON2ID_HASH + email: newsroom@invalid.local + groups: + - testers diff --git a/deploy/vps/environment.example b/deploy/vps/environment.example new file mode 100644 index 0000000..5b4cd07 --- /dev/null +++ b/deploy/vps/environment.example @@ -0,0 +1,8 @@ +# Copy to /srv/weathertool/.env.staging and set mode 0600. +# Do not commit the real file. +WEATHERTOOL_IMAGE=weathertool:REPLACE_WITH_FULL_GIT_SHA +WEATHERTOOL_PORT=8002 +AUTHELIA_PORT=9091 +POSTGRES_DB=weather +POSTGRES_USER=weather +POSTGRES_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_VALUE diff --git a/deploy/vps/nginx/laikapstak.li.conf.example b/deploy/vps/nginx/laikapstak.li.conf.example new file mode 100644 index 0000000..37822ea --- /dev/null +++ b/deploy/vps/nginx/laikapstak.li.conf.example @@ -0,0 +1,45 @@ +# Do not enable this file until DNS, origin certificates, and private upstream +# checks are complete. Adapt certificate/include paths to the VPS conventions. + +server { + listen 80; + listen [::]:80; + server_name laikapstak.li auth.laikapstak.li; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name auth.laikapstak.li; + + ssl_certificate /etc/nginx/tls/laikapstak.li/origin.pem; + ssl_certificate_key /etc/nginx/tls/laikapstak.li/origin.key; + + location / { + include /etc/nginx/snippets/weathertool-proxy.conf; + proxy_pass http://127.0.0.1:9091; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name laikapstak.li; + + ssl_certificate /etc/nginx/tls/laikapstak.li/origin.pem; + ssl_certificate_key /etc/nginx/tls/laikapstak.li/origin.key; + + # Start short during UAT. Increase only after TLS behavior is proven. + add_header Strict-Transport-Security "max-age=3600" always; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy no-referrer always; + + include /etc/nginx/snippets/weathertool-authelia-location.conf; + + location / { + include /etc/nginx/snippets/weathertool-proxy.conf; + include /etc/nginx/snippets/weathertool-authelia-authrequest.conf; + proxy_pass http://127.0.0.1:8002; + } +} diff --git a/deploy/vps/nginx/snippets/weathertool-authelia-authrequest.conf b/deploy/vps/nginx/snippets/weathertool-authelia-authrequest.conf new file mode 100644 index 0000000..9bf1a8f --- /dev/null +++ b/deploy/vps/nginx/snippets/weathertool-authelia-authrequest.conf @@ -0,0 +1,14 @@ +auth_request /internal/authelia/authz; + +auth_request_set $user $upstream_http_remote_user; +auth_request_set $groups $upstream_http_remote_groups; +auth_request_set $name $upstream_http_remote_name; +auth_request_set $email $upstream_http_remote_email; +auth_request_set $redirection_url $upstream_http_location; + +proxy_set_header Remote-User $user; +proxy_set_header Remote-Groups $groups; +proxy_set_header Remote-Name $name; +proxy_set_header Remote-Email $email; + +error_page 401 =302 $redirection_url; diff --git a/deploy/vps/nginx/snippets/weathertool-authelia-location.conf b/deploy/vps/nginx/snippets/weathertool-authelia-location.conf new file mode 100644 index 0000000..fe35e2f --- /dev/null +++ b/deploy/vps/nginx/snippets/weathertool-authelia-location.conf @@ -0,0 +1,25 @@ +set $upstream_authelia http://127.0.0.1:9091/api/authz/auth-request; + +location /internal/authelia/authz { + internal; + proxy_pass $upstream_authelia; + + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$host$request_uri; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Content-Length ""; + proxy_set_header Connection ""; + + proxy_pass_request_body off; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; + proxy_redirect http:// $scheme://; + proxy_http_version 1.1; + proxy_cache_bypass $cookie_weathertool_session; + proxy_no_cache $cookie_weathertool_session; + proxy_buffers 4 32k; + client_body_buffer_size 128k; + send_timeout 5m; + proxy_read_timeout 240s; + proxy_send_timeout 240s; + proxy_connect_timeout 240s; +} diff --git a/deploy/vps/nginx/snippets/weathertool-proxy.conf b/deploy/vps/nginx/snippets/weathertool-proxy.conf new file mode 100644 index 0000000..fa50812 --- /dev/null +++ b/deploy/vps/nginx/snippets/weathertool-proxy.conf @@ -0,0 +1,12 @@ +proxy_set_header Host $host; +proxy_set_header X-Original-URL $scheme://$host$request_uri; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; +proxy_set_header X-Forwarded-URI $request_uri; +proxy_set_header X-Forwarded-Ssl on; +proxy_set_header X-Forwarded-For $remote_addr; + +proxy_http_version 1.1; +proxy_connect_timeout 15s; +proxy_read_timeout 240s; +proxy_send_timeout 240s; diff --git a/docs/DEVELOPMENT_AND_STAGING.md b/docs/DEVELOPMENT_AND_STAGING.md index e645c9f..caa0639 100644 --- a/docs/DEVELOPMENT_AND_STAGING.md +++ b/docs/DEVELOPMENT_AND_STAGING.md @@ -6,7 +6,7 @@ |---|---|---| | Windows | Source editing and Git workflow only | Not applicable | | Rocky Linux | Docker development and production-like staging | `http://192.168.1.101:9190` | -| Temporary VPS | Proposed month-long user-acceptance staging behind Cloudflare, Nginx, and Authelia | Not provisioned; hostname pending | +| Temporary VPS | Month-long user-acceptance staging behind Cloudflare, Nginx, and Authelia | `laikapstak.li` pending domain delivery and activation; services not started | | Workplace production | Out of scope until reviewed and approved | Not documented here | Rocky checkout: @@ -124,3 +124,8 @@ Windows source and Git -> Rocky build and verification -> immutable image + chec ``` Releases are manual during user acceptance and are identified by the source commit SHA. The VPS keeps WeatherTool in its own `/srv/weathertool` tree, publishes the application only to a loopback port, and relies on Nginx plus Authelia at the public boundary. See `VPS_STAGING_PLAN.md` before provisioning or changing the VPS. + +The committed templates live under `deploy/vps/`. They are not a second local +development environment. Validate them on Rocky, build the exact Git revision, +and transfer a checksummed image plus the minimal deployment files. Operational +environment files, password hashes, and Authelia secrets must remain VPS-only. diff --git a/docs/README.md b/docs/README.md index 2ee6e84..89d72c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ This directory contains the working documentation for the WeatherTool modernizat - Faktiskā symbol placement is automatic after manual image selection and is anchored to each rendered temperature badge. - The first **Ūdens temperatūra** workspace is implemented with manual Latvian-named fields and required 1920×1080 and 3840×1440 exports; visual calibration remains in progress. - Confirmed local Monda Regular/Bold files provide interface and generated-graphic typography; weather symbols use normalized transparent image assets. -- The next proposed environment is a temporary, isolated VPS user-acceptance deployment built on Rocky, protected by Cloudflare, Nginx, and removable Authelia authentication. It has not yet been provisioned. +- A version-controlled bundle is being prepared for a temporary, isolated VPS user-acceptance deployment built on Rocky and protected by Cloudflare, Nginx, and removable Authelia authentication. VPS directories exist, but no WeatherTool service has been started. - This is not yet approved or hardened for workplace production. ## Documents @@ -23,7 +23,7 @@ This directory contains the working documentation for the WeatherTool modernizat - [Product workflows](PRODUCT_WORKFLOWS.md) — the intended purpose and current status of each visible workspace. - [Update roadmap](UPDATE_ROADMAP.md) — phased technical, security, dependency, testing, and UI work. - [Third-party notices](THIRD_PARTY_NOTICES.md) — licenses and attribution for adapted interface components. -- [Temporary VPS staging plan](VPS_STAGING_PLAN.md) — proposed isolation, authentication, release, backup, verification, and rollback model for external user testing. +- [Temporary VPS staging plan](VPS_STAGING_PLAN.md) — isolation, authentication, prepared deployment bundle, release, backup, verification, and rollback model for external user testing. ## Documentation rules diff --git a/docs/VPS_STAGING_PLAN.md b/docs/VPS_STAGING_PLAN.md index 62d6477..8a97c0c 100644 --- a/docs/VPS_STAGING_PLAN.md +++ b/docs/VPS_STAGING_PLAN.md @@ -1,6 +1,6 @@ # Temporary VPS staging plan -Status: **Proposed; WeatherTool has not been provisioned on the VPS** +Status: **Deployment bundle prepared; runtime directories created; services not started** This document defines the next deployment stage for approximately one month of newsroom-user testing. It records the intended boundary before any VPS changes @@ -57,6 +57,10 @@ Temporary VPS Cloudflare -> Nginx -> Authelia authorization -> WeatherTool ``` +The host root plus `releases/` and `backups/` now exist with owner +`ubuntu:ubuntu` and mode `0750`. No Compose file, secret, image, database, or +running WeatherTool service has been placed there yet. + The VPS receives a built release artifact, not a development checkout. It gets no credential or route for the Rocky Git repository, the private Forgejo instance, or the home LAN. @@ -91,9 +95,10 @@ WeatherTool will use: - no shared Docker volumes, database, or application network with HOP or any other service. -Only Nginx may expose WeatherTool publicly. PostgreSQL and Authelia's internal -service port must not be published to the internet. Docker port bindings must -not be allowed to bypass the intended firewall boundary. +Only Nginx may expose WeatherTool publicly. The committed bundle binds the app +to `127.0.0.1:8002`, binds the Authelia portal to `127.0.0.1:9091`, and gives +PostgreSQL no host port. Docker port bindings must not be allowed to bypass the +intended firewall boundary. ## Authentication boundary @@ -123,7 +128,9 @@ include and Authelia service. The application remains unchanged. ## Domain, TLS, and Nginx -The WeatherTool hostname is not yet selected. Before DNS publication: +The selected application hostname is `laikapstak.li`; the authentication portal +will use `auth.laikapstak.li`. The OVH order and Cloudflare zone activation are +still pending. Before DNS publication: 1. Add an isolated Nginx virtual host without changing existing hosts. 2. Install a certificate valid for the exact WeatherTool/Authelia hostname or @@ -138,9 +145,24 @@ The WeatherTool hostname is not yet selected. Before DNS publication: 9. Confirm TLS, static assets, login, logout, API calls, PNG downloads, real client addresses, and direct-origin rejection. -Whether Authelia uses a dedicated `auth.` hostname or a subpath remains -an explicit deployment decision. A dedicated hostname is conventional; a -same-host subpath reduces DNS and certificate entries for this single app. +The dedicated authentication hostname is an explicit decision. Both hostnames +must be covered by the origin certificate and proxied through Cloudflare. + +## Version-controlled deployment bundle + +`deploy/vps/` now contains: + +- a multi-stage production Dockerfile that builds both the Vite frontend and + Scala assembly, then runs as a non-root user; +- an isolated `weathertool-uat` Compose definition; +- an environment example with no operational secret; +- pinned Authelia `4.39.20` configuration and an example file-user database; +- official-style Nginx AuthRequest integration snippets; and +- a disabled Nginx virtual-host example for the app and auth portal. + +The bundle is a template until it passes Compose rendering, Authelia validation, +image build, container health, and private loopback smoke tests. Nginx activation +and public DNS remain later checkpoints. ## Manual release procedure @@ -202,13 +224,10 @@ Proposed temporary-staging minimum: ## Decisions and prerequisites still open -- Final public hostname and Cloudflare zone. -- Dedicated Authelia hostname versus same-host subpath. -- Confirmed VPS capacity and unused loopback port. +- Completion of the OVH order, nameserver delegation, and Cloudflare activation. +- Cloudflare origin certificate covering both selected hostnames. - Deployment account and whether normal releases use public SSH or Tailscale. - Final session and backup-retention values. - Secure operator-side generation and delivery of the shared credential. -- A production Dockerfile/Compose definition validated independently from the - Rocky development Compose file. +- Validation of the prepared production Dockerfile/Compose bundle on Rocky. - Health/readiness behavior suitable for automated release verification. -