openvibers/openvibe.ai Commits

Add the ai.openvibe.services reference nginx vhost (same allow-list as ai.openvibe.network) (#19)

OP OpenVibers committed 5 Oct 2026 · 5d6c8993ba · 2 files, +109 −2

* Add the ai.openvibe.services reference nginx vhost (same allow-list as ai.openvibe.network)

- `deploy/nginx/ai.openvibe.services.conf:1` — new vhost file, modelled line by line on `ai.openvibe.network.conf` (verified identical after domain substitution except the intentional comment rewording); header names the new domain/file, redirect URI, and says it replaces the placeholder once its certificate exists.
- `deploy/nginx/ai.openvibe.services.conf:23,26` — HTTP server: `server_name ai.openvibe.services`, acme-challenge location, `return 301 https://ai.openvibe.services$request_uri`.
- `deploy/nginx/ai.openvibe.services.conf:33,35-36,39-40` — HTTPS server: `server_name ai.openvibe.services`, certs `/etc/letsencrypt/live/ai.openvibe.services/{fullchain,privkey}.pem`, matching access/error logs.
- `deploy/nginx/ai.openvibe.services.conf:49-100` — copied location set exactly: proxied `/api/health`, `/api/ready`, `/console`, `/auth/`, `/robots.txt`, `/stats`, `/` to `127.0.0.1:4700`; `/metrics` → 404 and `/api/` → 403 still blocked; headers from `$remote_addr` only (no `$proxy_add_x_forwarded_for`); trailing `location /` → 404.
- `README.md:389` — deploy section names `deploy/nginx/ai.openvibe.services.conf`, says it mirrors the existing vhost and replaces the Sites placeholder for `ai.openvibe.services` once its certificate exists, and that it is not installed or live yet.

Test results:
- `ov test test/console.test.js` (the only test reading `deploy/nginx`): **1 passed** in 13 s, exit 0.
- nginx syntax check (Docker `nginx:stable-alpine`, throwaway `/tmp` prefix, `/etc` untouched): expected `[emerg] cannot load certificate "/etc/letsencrypt/live/ai.openvibe.services/fullchain.pem" … No such file or directory`; a re-check with real self-signed cert paths reported `syntax is ok` / `test is successful` (exit 0), confirming no other error.

Not committed or pushed.

* Use the *.openvibe.services wildcard certificate the host holds; note the .services console redirect URI (review)

Changes

ddiff --git a/README.md b/README.md
iindex cc56348..6a52935 100644
---- a/README.md
++++ b/README.md
@@@ -386,10 +386,15 @@ Deployed: `/opt/openvibe.ai`, env `/etc/openvibe/ai.env` (0600), unit `deploy/sy
(`StateDirectory=openvibe-ai`; database `ov_ai` on the host's data role), principal `ai`. The nginx vhost
`deploy/nginx/ai.openvibe.network.conf` (health/ready, `/`, `/robots.txt`, the operator console
`/console` and its sign-in `/auth/` public; the API is host-local) is not installed:
--`ai.openvibe.network` still serves the Sites placeholder. The console also needs
++`ai.openvibe.network` still serves the Sites placeholder. `deploy/nginx/ai.openvibe.services.conf`
++mirrors that vhost for `ai.openvibe.services` (the same proxied locations to the same upstream,
++`/metrics` and the API still blocked, on the `*.openvibe.services` certificate the host already holds) and
++replaces the Sites placeholder for `ai.openvibe.services` when installed; it is not installed or live yet. The console also needs
`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file
(without them it answers 503 in production), and the Network must list
--`https://ai.openvibe.network/auth/callback` among client `ai`'s redirect URIs.
++`https://ai.openvibe.network/auth/callback` among client `ai`'s redirect URIs (and
++`https://ai.openvibe.services/auth/callback` once that vhost serves the console, since the callback is built from
++the base URL).
## Not done yet
ddiff --git a/deploy/nginx/ai.openvibe.services.conf b/deploy/nginx/ai.openvibe.services.conf
nnew file mode 100644
iindex 0000000..de6a112
---- /dev/null
++++ b/deploy/nginx/ai.openvibe.services.conf
@@@ -0,0 +1,102 @@
++## ═══════════════════════════════════════════════════════════════
++## ai.openvibe.services — Nginx Configuration (REFERENCE)
++##
++## Deployed to /etc/nginx/sites-available/ai.openvibe.services.conf — keep them in sync, and reload
++## nginx after changing it. It uses the *.openvibe.services certificate the host already holds
++## (/etc/letsencrypt/live/openvibe.services/); installed in place of the OpenVibe.Sites placeholder
++## vhost of the same name, it replaces that placeholder.
++##
++## The Node service (port 4700, systemd unit openvibe-ai) serves:
++## /api/health, /api/ready
++## /metrics Prometheus text, answered only to direct loopback callers (never proxied)
++## /api/v1/* service-to-service API (Network service tokens, audience openvibe.ai). Services on
++## this host call 127.0.0.1:4700 directly, so the public vhost refuses it; open the
++## location below only when a service on another host needs it (it still needs a token).
++## /console the operator console (server-rendered, OpenVibe.Network staff only) and /auth/* its
++## Network sign-in (redirect URI https://ai.openvibe.services/auth/callback, OAuth client
++## `ai`); both noindex. /robots.txt keeps crawlers out of them.
++## /stats public price and latency per provider and model (server-rendered, no sign-in)
++## ═══════════════════════════════════════════════════════════════
++
++server {
++ listen 80;
++ listen [::]:80;
++ server_name ai.openvibe.services;
++
++ location /.well-known/acme-challenge/ { root /var/www/certbot; }
++ location / { return 301 https://ai.openvibe.services$request_uri; }
++}
++
++server {
++ listen 443 ssl;
++ listen [::]:443 ssl;
++ http2 on;
++ server_name ai.openvibe.services;
++
++ ssl_certificate /etc/letsencrypt/live/openvibe.services/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/openvibe.services/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
++
++ access_log /var/log/nginx/ai.openvibe.services.access.log;
++ error_log /var/log/nginx/ai.openvibe.services.error.log;
++
++ # Inline images arrive as data URLs inside run inputs.
++ client_max_body_size 12m;
++
++ add_header X-Content-Type-Options nosniff always;
++ add_header Referrer-Policy strict-origin-when-cross-origin always;
++ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
++
++ location = /api/health { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++ location = /api/ready { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++
++ # Metrics are for host-local scrapers only (http://127.0.0.1:4700/metrics).
++ location = /metrics { return 404; }
++
++ # ── Service API: host-local only (see header) ───────────
++ location /api/ {
++ return 403;
++ # To expose it (token-guarded) to off-host services instead:
++ # proxy_pass http://127.0.0.1:4700;
++ # proxy_http_version 1.1;
++ # proxy_set_header Host $host;
++ # proxy_set_header X-Real-IP $remote_addr; # client address from $remote_addr only (realip),
++ # proxy_set_header X-Forwarded-For $remote_addr; # never appended to or passed through
++ # proxy_set_header CF-Connecting-IP $remote_addr;
++ # proxy_set_header X-Forwarded-Proto $scheme;
++ # proxy_read_timeout 90s; # ?wait= holds a request up to 60 s
++ }
++
++ # ── Operator console and its Network sign-in (staff only; the app refuses everyone else) ──
++ location /console {
++ proxy_pass http://127.0.0.1:4700;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++ location /auth/ {
++ proxy_pass http://127.0.0.1:4700;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++ location = /robots.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++
++ location = /stats { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++
++ location = / {
++ proxy_pass http://127.0.0.1:4700;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ }
++
++ location / {
++ return 404;
++ }
++}