diff --git a/docker-compose.publish.yml b/docker-compose.publish.yml index 2022064..58a80e0 100644 --- a/docker-compose.publish.yml +++ b/docker-compose.publish.yml @@ -1,6 +1,6 @@ # Bind-mount-free compose for GHCR Compose OCI publish. # Operators: docker compose -f oci://ghcr.io/tagbase/tagbase-stack:vX.Y.Z up -d -# Required: POSTGRES_PASSWORD, NGINX_PASS, PGADMIN_DEFAULT_PASSWORD +# Required: POSTGRES_PASSWORD, NGINX_PASS # Optional Let's Encrypt: --profile letsencrypt plus TLS_DOMAIN and ACME_EMAIL x-syslog: &syslog driver: syslog @@ -104,13 +104,47 @@ services: - acme-www:/var/www/acme pgadmin4: + configs: + - source: pgadmin-servers + target: /pgadmin4/servers.json + - source: pgadmin-pgpass + target: /run/secrets/pgpass + uid: "5050" + gid: "0" + mode: 0400 depends_on: postgis: condition: service_healthy + # The image copies PGPASS_FILE only while pgadmin4.db is first created. + # Its /var/lib/pgadmin volume survives recreate, so refresh .pgpass here. + # Desktop mode resolves PassFile at /var/lib/pgadmin/.pgpass. + entrypoint: + - /bin/sh + - -c + - | + set -eu + if [ -n "$${PGPASS_FILE:-}" ] && [ -f "$$PGPASS_FILE" ]; then + cp "$$PGPASS_FILE" /var/lib/pgadmin/.pgpass + chown 5050:0 /var/lib/pgadmin/.pgpass + chmod 600 /var/lib/pgadmin/.pgpass + fi + exec /entrypoint.sh environment: - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-admin@example.com} - - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:?set PGADMIN_DEFAULT_PASSWORD} + - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-tagbase} - PGADMIN_LISTEN_PORT=5434 + # The image compares this value to the string True. + - PGADMIN_REPLACE_SERVERS_ON_STARTUP=True + - PGADMIN_SERVER_JSON_FILE=/pgadmin4/servers.json + # Desktop mode: skip the pgAdmin login form behind nginx basic auth. + # Use PGADMIN_CONFIG_SERVER_MODE (not PGADMIN_SERVER_MODE) so DATA_DIR + # stays /var/lib/pgadmin. Quotes make DESKTOP_USER a Python string. + - PGADMIN_CONFIG_SERVER_MODE=False + - PGADMIN_CONFIG_USE_OS_SECRET_STORAGE=False + - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False + - PGADMIN_CONFIG_DESKTOP_USER='${PGADMIN_DEFAULT_EMAIL:-admin@example.com}' + # Desktop mode copies this to /var/lib/pgadmin/.pgpass. + - PGPASS_FILE=/run/secrets/pgpass expose: - 5434 hostname: pgadmin4 @@ -329,3 +363,26 @@ volumes: fswatch-state: dbbackups: tagbase-logs: + +configs: + # Keep in sync with services/pgadmin/servers.json. Inline content avoids a + # host bind, which Compose OCI artifacts cannot include. + pgadmin-servers: + content: | + { + "Servers": { + "1": { + "Name": "tagbase", + "Group": "Servers", + "Host": "postgis", + "Port": 5432, + "MaintenanceDB": "tagbase", + "Username": "tagbase", + "SSLMode": "prefer", + "PassFile": "/var/lib/pgadmin/.pgpass" + } + } + } + pgadmin-pgpass: + content: | + postgis:5432:*:tagbase:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} diff --git a/docker-compose.yml b/docker-compose.yml index a2001f1..973bb14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -116,13 +116,45 @@ services: - nginx-certs:/etc/nginx/certs - acme-www:/var/www/acme pgadmin4: + configs: + - source: pgadmin-pgpass + target: /run/secrets/pgpass + uid: "5050" + gid: "0" + mode: 0400 depends_on: postgis: condition: service_healthy + # The image copies PGPASS_FILE only while pgadmin4.db is first created. + # Its /var/lib/pgadmin volume survives recreate, so refresh .pgpass here. + # Desktop mode resolves PassFile at /var/lib/pgadmin/.pgpass. + entrypoint: + - /bin/sh + - -c + - | + set -eu + if [ -n "$${PGPASS_FILE:-}" ] && [ -f "$$PGPASS_FILE" ]; then + cp "$$PGPASS_FILE" /var/lib/pgadmin/.pgpass + chown 5050:0 /var/lib/pgadmin/.pgpass + chmod 600 /var/lib/pgadmin/.pgpass + fi + exec /entrypoint.sh environment: - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-admin@example.com} - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-tagbase} - PGADMIN_LISTEN_PORT=5434 + # The image compares this value to the string True. + - PGADMIN_REPLACE_SERVERS_ON_STARTUP=True + - PGADMIN_SERVER_JSON_FILE=/pgadmin4/servers.json + # Desktop mode: skip the pgAdmin login form behind nginx basic auth. + # Use PGADMIN_CONFIG_SERVER_MODE (not PGADMIN_SERVER_MODE) so DATA_DIR + # stays /var/lib/pgadmin. Quotes make DESKTOP_USER a Python string. + - PGADMIN_CONFIG_SERVER_MODE=False + - PGADMIN_CONFIG_USE_OS_SECRET_STORAGE=False + - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False + - PGADMIN_CONFIG_DESKTOP_USER='${PGADMIN_DEFAULT_EMAIL:-admin@example.com}' + # Desktop mode copies this to /var/lib/pgadmin/.pgpass. + - PGPASS_FILE=/run/secrets/pgpass expose: - 5434 hostname: pgadmin4 @@ -136,6 +168,8 @@ services: networks: - internal-network restart: unless-stopped + volumes: + - ./services/pgadmin/servers.json:/pgadmin4/servers.json:ro # postgres: # build: # context: ./services/postgres @@ -386,3 +420,8 @@ volumes: nginx-certs: acme-www: lego-data: + +configs: + pgadmin-pgpass: + content: | + postgis:5432:*:tagbase:${POSTGRES_PASSWORD:-tagbase} diff --git a/services/pgadmin/servers.json b/services/pgadmin/servers.json new file mode 100644 index 0000000..19e0aea --- /dev/null +++ b/services/pgadmin/servers.json @@ -0,0 +1,14 @@ +{ + "Servers": { + "1": { + "Name": "tagbase", + "Group": "Servers", + "Host": "postgis", + "Port": 5432, + "MaintenanceDB": "tagbase", + "Username": "tagbase", + "SSLMode": "prefer", + "PassFile": "/var/lib/pgadmin/.pgpass" + } + } +} diff --git a/wiki/Installation.md b/wiki/Installation.md index 1a5b276..9c36643 100644 --- a/wiki/Installation.md +++ b/wiki/Installation.md @@ -46,7 +46,6 @@ Set secrets in the environment (or a `.env` file in the directory where you run ```bash export POSTGRES_PASSWORD='choose-a-strong-password' export NGINX_PASS='choose-a-strong-password' -export PGADMIN_DEFAULT_PASSWORD='choose-a-strong-password' # optional export NGINX_USER=tagbase export PGADMIN_DEFAULT_EMAIL=admin@example.com @@ -74,9 +73,8 @@ Expect a browser **certificate warning**. Nginx creates a **self-signed** cert o | --------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `POSTGRES_PASSWORD` | yes | Database and app | | `NGINX_PASS` | yes | HTTP basic auth on all UIs | -| `PGADMIN_DEFAULT_PASSWORD` | yes | pgAdmin login (after nginx auth) | | `NGINX_USER` | no | Default `tagbase` | -| `PGADMIN_DEFAULT_EMAIL` | no | Default `admin@example.com` | +| `PGADMIN_DEFAULT_EMAIL` | no | Bootstrap email for pgAdmin’s config database (default `admin@example.com`); not a browser login | | `SYSLOG_ADDRESS` | no | Default `host.docker.internal:514`. Docker Desktop: leave default. Linux engine: try `127.0.0.1:514` if logs never reach Loki | | `TLS_DOMAIN` / `ACME_EMAIL` | only with `--profile letsencrypt` | See [Trusted HTTPS](#trusted-https-lets-encrypt) | @@ -125,17 +123,20 @@ docker compose -f oci://ghcr.io/tagbase/tagbase-stack:vX.Y.Z down -v ## Using the UIs -All browser routes need **nginx basic auth** (`NGINX_USER` / `NGINX_PASS`). Grafana is anonymous **behind** that gateway; do not expose Grafana on the host. +All browser routes need **nginx basic auth** (`NGINX_USER` / `NGINX_PASS`). Grafana and pgAdmin are anonymous **behind** that gateway; do not expose them on the host. **OpenAPI** is the ingest contract (GET/POST, `file` / `ftp` / `http` / `https`, `.txt` / archives). Start at [https://localhost/docs](https://localhost/docs). -**pgAdmin:** [https://localhost/pgadmin4/](https://localhost/pgadmin4/) — nginx auth first, then `PGADMIN_DEFAULT_EMAIL` / `PGADMIN_DEFAULT_PASSWORD`. Register a server: +**pgAdmin:** [https://localhost/pgadmin4/](https://localhost/pgadmin4/) — nginx auth only (no second email/password prompt). The saved `tagbase` connection is already registered: -- Host: **`postgis`** (Docker DNS name, not `postgres` and not `localhost`) +- Host: `postgis` (Docker DNS name on the Compose network) - Port: `5432` - Database: `tagbase` - Username: `tagbase` -- Password: the same `POSTGRES_PASSWORD` + +Expand **tagbase** in the object explorer to connect. The password is the configured `POSTGRES_PASSWORD` from a [pgpass](https://www.postgresql.org/docs/current/libpq-pgpass.html) file. + +Keep `:`, `\`, and line breaks out of `POSTGRES_PASSWORD`. The preloaded connection reads that password from pgpass, which treats those characters as syntax. **Materialized views** (once, after you have ingested tags you want in “application ready” form): run [`tagbase_materialized_views.sql`](https://github.com/tagbase/tagbase-server/blob/main/services/postgis/tagbase_materialized_views.sql) in the pgAdmin query tool. Do not re-run that script on every ingest. diff --git a/wiki/Release-Management.md b/wiki/Release-Management.md index 1915a97..a7211ed 100644 --- a/wiki/Release-Management.md +++ b/wiki/Release-Management.md @@ -34,7 +34,6 @@ Requires Docker Engine and Compose **2.34+**. No git clone. Digest-pinned images ```bash export POSTGRES_PASSWORD=... export NGINX_PASS=... -export PGADMIN_DEFAULT_PASSWORD=... docker compose -f oci://ghcr.io/tagbase/tagbase-stack:vX.Y.Z up -d ``` diff --git a/wiki/Security.md b/wiki/Security.md index d299e57..9bd0df8 100644 --- a/wiki/Security.md +++ b/wiki/Security.md @@ -29,7 +29,7 @@ Do not bake credentials into an image build. Old `--build-arg NGINX_USER` / `NGI Grafana is **anonymous Admin** _behind_ nginx. Nginx **clears** the `Authorization` header on `/grafana/` (and other observability paths) so browser basic auth is not forwarded (that used to cause a second 401 prompt). -pgAdmin has its **own** login (`PGADMIN_DEFAULT_EMAIL` / `PGADMIN_DEFAULT_PASSWORD`) **after** nginx basic auth. Treat both as secrets on any shared host. +pgAdmin runs in **desktop mode** behind the same nginx gate: there is no second email/password form. Anyone who passes nginx basic auth can use the query tool and the preloaded `tagbase` connection (password from `POSTGRES_PASSWORD`). Database password is `POSTGRES_PASSWORD` (required on the OCI install). You do not edit SQL files to change it.