From 2aa10b17112ce7864d8fa2924eb7bf0689b56547 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Tue, 2 Jun 2026 22:05:47 +0200 Subject: [PATCH] fix(auth): front CouchDB with nginx to mark AuthSession cookie Secure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CouchDB 3.5 derives the Set-Cookie Secure flag from the raw mochiweb socket scheme and ignores X-Forwarded-Proto/Ssl for cookie attribution. Behind a TLS-terminating proxy the cookie was emitted without Secure, so browsers dropped the SameSite=None AuthSession cookie on cross-origin requests — POST /_session returned 200 but the next GET /_session showed userCtx.name: null and PouchDB stayed unauthenticated. Add an nginx sidecar that adds Secure via proxy_cookie_flags, route the public domain through it, and make couchdb internal. Also persist same_site=none in couchdb-init so the setting survives container recreation (local.d/ is not in the data volume). --- README.md | 29 +++++++++++++++++++++++++++-- docker-compose.yml | 33 +++++++++++++++++++++++++++------ nginx/default.conf | 29 +++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 nginx/default.conf diff --git a/README.md b/README.md index 2b6a5b4..a1a1ed0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # coolcouch -CouchDB 3 on Coolify. The `couchdb-init` sidecar waits for the server to come up, creates the three system databases, and enables CORS via the config API — no file mounts required. +CouchDB 3 on Coolify, fronted by an nginx sidecar. The `couchdb-init` sidecar waits for the server to come up, creates the three system databases, and applies CORS + cookie config via the config API — no file mounts on CouchDB itself. + +## Architecture + +``` +client ──https──> Traefik ──http──> nginx ──http──> couchdb (internal) +``` + +nginx exists for one reason: to add `Secure` to the `AuthSession` cookie that CouchDB emits. CouchDB only marks cookies `Secure` when its embedded mochiweb sees a true HTTPS socket; behind a TLS-terminating proxy it never does, and it ignores `X-Forwarded-Proto`/`X-Forwarded-Ssl` for cookie attribution. Without `Secure`, browsers reject `SameSite=None` cookies, and cross-origin PouchDB logins silently fail (login returns 200, the next `_session` shows `userCtx.name: null`). The fix is `proxy_cookie_flags AuthSession secure;` in `nginx/default.conf`. ## Deploy @@ -8,7 +16,8 @@ CouchDB 3 on Coolify. The `couchdb-init` sidecar waits for the server to come up 2. In the resource's **Environment Variables** tab, set: - `COUCHDB_PASSWORD` — required - `COUCHDB_USER` — optional, defaults to `admin` -3. In the resource's **Domains** tab, assign a domain to the `couchdb` service on port `5984`. Coolify fills `SERVICE_FQDN_COUCHDB_5984` from this. + - `INVITE_CODE` — required for the signup service +3. In the resource's **Domains** tab, assign your CouchDB domain to the **`nginx`** service on port `5984` (not `couchdb` directly — that's now internal). Coolify fills `SERVICE_FQDN_NGINX_5984` from this. Assign the signup domain to the `signup` service on port `8080`. 4. **Deploy.** `couchdb-init` runs once, exits, and won't restart. ## Verify @@ -26,6 +35,21 @@ curl -fsS -I -X OPTIONS https://$DOMAIN/ \ Expected: `_up` returns `{"status":"ok",...}`; `/` returns the CouchDB welcome JSON; `_all_dbs` returns `["_global_changes","_replicator","_users"]`; the preflight returns 2xx with an `Access-Control-Allow-Origin` header. +Verify the cookie has both `SameSite=None` and `Secure`: + +```bash +curl -i -X POST https://$DOMAIN/_session \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d "name=admin&password=$PASS" | grep -i set-cookie +``` + +Expected: +``` +set-cookie: AuthSession=...; Version=1; Expires=...; Max-Age=600; Path=/; HttpOnly; SameSite=None; Secure +``` + +If `Secure` is missing, the nginx sidecar isn't fronting the request — check that the Coolify domain is assigned to `nginx:5984` and not to `couchdb` directly. + If the init logs show CORS errors, you can re-trigger only that step by redeploying — the PUTs against `/_node/_local/_config/...` are idempotent. ## CORS @@ -87,4 +111,5 @@ The frontend should then `POST https://couch.apoena.dev/_session` with the same ## Files - `docker-compose.yml` — services, volume, Traefik labels, init sidecar +- `nginx/default.conf` — reverse-proxy config that adds `Secure` to AuthSession cookies - `signup/` — Gleam signup service (Dockerfile, gleam.toml, src/) diff --git a/docker-compose.yml b/docker-compose.yml index 982e0a5..67c244b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,6 @@ services: image: couchdb:3 restart: unless-stopped environment: - SERVICE_FQDN_COUCHDB_5984: COUCHDB_USER: ${COUCHDB_USER:-admin} COUCHDB_PASSWORD: ${COUCHDB_PASSWORD} expose: @@ -12,6 +11,24 @@ services: - couchdb_data:/opt/couchdb/data networks: - default + + # nginx fronts CouchDB to add `Secure` to the AuthSession cookie. CouchDB only + # marks the cookie Secure when mochiweb sees a true HTTPS socket, which never + # happens behind a TLS-terminating proxy — so SameSite=None cookies would be + # dropped by browsers on cross-origin requests. See nginx/default.conf. + nginx: + image: nginx:alpine + restart: unless-stopped + depends_on: + - couchdb + environment: + SERVICE_FQDN_NGINX_5984: + expose: + - "5984" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - default - coolify labels: - "traefik.enable=true" @@ -59,11 +76,15 @@ services: "$$base/_node/_local/_config/$$1/$$2" -d "\"$$3\"" >/dev/null echo "cfg $$1/$$2 = $$3" } - put_config chttpd enable_cors true - put_config cors origins 'http://localhost:8080,http://localhost:5173,http://localhost:3000,http://localhost:4173,https://vaquant.at' - put_config cors credentials true - put_config cors methods 'GET, PUT, POST, HEAD, DELETE' - put_config cors headers 'accept, authorization, content-type, origin, referer, x-csrf-token' + put_config chttpd enable_cors true + put_config cors origins 'http://localhost:8080,http://localhost:5173,http://localhost:3000,http://localhost:4173,https://vaquant.at' + put_config cors credentials true + put_config cors methods 'GET, PUT, POST, HEAD, DELETE' + put_config cors headers 'accept, authorization, content-type, origin, referer, x-csrf-token' + # SameSite=None is required for PouchDB cross-origin cookie sessions. + # Browsers also require Secure on SameSite=None cookies — that flag is + # added by the nginx sidecar via proxy_cookie_flags. + put_config couch_httpd_auth same_site none signup: build: diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..02e5ad8 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,29 @@ +server { + listen 5984; + server_name _; + + # PouchDB attachments and _bulk_docs can be large. + client_max_body_size 256m; + + # CouchDB's _changes?feed=continuous needs HTTP/1.1, no buffering, long timeouts. + proxy_http_version 1.1; + proxy_buffering off; + proxy_read_timeout 1h; + + location / { + proxy_pass http://couchdb:5984; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Forwarded-Host $http_x_forwarded_host; + proxy_set_header Connection ""; + + # CouchDB never marks AuthSession cookies Secure behind a TLS-terminating + # proxy — mochiweb only checks the raw socket scheme, and ignores + # x_forwarded_proto/x_forwarded_ssl for cookie attribution. Add Secure + # here so SameSite=None cookies survive cross-origin requests. + proxy_cookie_flags AuthSession secure; + } +}