feat: add Database-Gateway Cloudron package (Infrastructure) [#639]

Database Gateway 0.24.0 as the 12th package: multi-stage Go build
(CGO required by the libpg_query parser, hence an alpine:3.23 runtime
matching upstream), native OIDC wired to the Cloudron platform identity
provider, postgresql addon storage with goose migrations applied at
start, jq-generated config + OPA policy seeded to /app/data. Verified
end-to-end against a throwaway postgres (migrations, policy compile,
startup to the OIDC handoff). Docs gardened to 12 packages.

Ticket: https://projects.knownelement.com/issues/639
This commit is contained in:
2026-09-01 19:28:06 -05:00
parent f918a90c3b
commit 3d38c507e8
10 changed files with 431 additions and 13 deletions
@@ -0,0 +1,30 @@
# Environment knobs for the Database Gateway Cloudron package.
#
# The app itself is configured through /app/data/config.json (seeded on
# first start; see README.md). The variables below are consumed by
# start.sh at seed time and are provided automatically by Cloudron —
# do NOT set them by hand.
# --- Cloudron platform OIDC provider (injected by Cloudron) -------------------
# Written into users.* of /app/data/config.json on first start.
#CLOUDRON_OIDC_ISSUER=https://my.example.com/openid
#CLOUDRON_OIDC_CLIENT_ID=
#CLOUDRON_OIDC_CLIENT_SECRET=
#CLOUDRON_OIDC_TOKEN_SIGNATURE_ALGORITHM=RS256
# --- App origin (injected by Cloudron) ----------------------------------------
# Redirect URL is derived as ${CLOUDRON_APP_ORIGIN}/auth/callback.
#CLOUDRON_APP_ORIGIN=https://dbgw.example.com
# --- PostgreSQL addon (injected by Cloudron) ----------------------------------
# Gateway storage: profiles, bookmarks, query results, migrations.
#CLOUDRON_POSTGRESQL_HOST=
#CLOUDRON_POSTGRESQL_PORT=5432
#CLOUDRON_POSTGRESQL_DATABASE=
#CLOUDRON_POSTGRESQL_USERNAME=
#CLOUDRON_POSTGRESQL_PASSWORD=
# --- Operator-editable files in /app/data -------------------------------------
# config.json database targets + role mapping (restart after edit)
# opa/simple.rego query authorization policy (restart after edit)
# .cookie_secret session cookie secret (rotate = logout everyone)
@@ -0,0 +1,18 @@
# Changelog
## [0.24.0] - 2026-09-01
Initial Cloudron package for
[Database Gateway](https://github.com/kazhuravlev/database-gateway) v0.24.0
(ticket [#639](https://projects.knownelement.com/issues/639)).
- Multi-stage Go build (golang:1.26-alpine → cloudron/base:3.2.0), frontend
embedded by upstream via `go:embed`.
- Native OIDC login wired to the Cloudron platform identity provider
(`CLOUDRON_OIDC_ISSUER` / `CLIENT_ID` / `CLIENT_SECRET`); roles mapped from
the `groups` claim (`admins` → admin, `users` → user).
- `postgresql` addon (16) for the gateway's own storage; migrations applied
on every start via `migrate-up`.
- `/app/data/config.json` + `/app/data/opa/simple.rego` seeded on first run;
operators add database targets and OPA policy rules via the file manager.
- Single HTTP listener on port 8080 (`httpPort`).
@@ -0,0 +1,24 @@
{
"manifestVersion": 2,
"type": "app",
"id": "io.cloudron.dbgateway",
"title": "Database Gateway",
"description": "Web gateway for secure, policy-controlled access to PostgreSQL databases. Users authenticate via OIDC (Cloudron single sign-on) and run queries that are parsed and authorized by embedded OPA policies per user, target, operation and table. Query results are stored with shareable links for debugging and auditing.",
"author": "Kirill Zhuravlev",
"website": "https://github.com/kazhuravlev/database-gateway",
"contactEmail": "cloudron@tsys.dev",
"tagline": "Policy-checked SQL gateway for your databases",
"version": "0.24.0",
"healthCheckPath": "/",
"httpPort": 8080,
"memoryLimit": 512,
"addons": {
"localstorage": true,
"postgresql": {
"version": "16"
}
},
"mediaLinks": [],
"changelog": "Initial Cloudron package for Database Gateway 0.24.0. Native OIDC login wired to the Cloudron platform identity provider (admins group -> gateway admin, users group -> gateway user); local PostgreSQL addon stores profiles, bookmarks and query results; OPA policies and database targets are configured from /app/data. Single HTTP listener on port 8080.",
"icon": "file://logo.png"
}
@@ -0,0 +1,65 @@
# Database Gateway Cloudron Package
#
# Database Gateway (dbgw) is a web gateway for secure, policy-controlled
# access to PostgreSQL databases: users log in via OIDC and every query is
# parsed and authorized by embedded OPA policies (per user, target,
# operation and table) before it reaches a remote database. Query results
# are stored in a local PostgreSQL storage with shareable links.
#
# Upstream: https://github.com/kazhuravlev/database-gateway (v0.24.0, Go 1.26, Echo)
# - Single Go binary; the frontend is already built and embedded in the
# repo (internal/facade/ui/dist via go:embed), so no Node stage needed
# - Listens on 0.0.0.0:8080; config file passed with `-c config.json`
# - Storage schema applied via the `migrate-up` subcommand (embedded SQL)
#
# Authentication: NATIVE OIDC (preferred). start.sh wires the Cloudron
# platform OIDC provider (CLOUDRON_OIDC_ISSUER / CLIENT_ID / CLIENT_SECRET)
# into /app/data/config.json on first run; roles map from the Cloudron
# `groups` claim (admins -> admin, users -> user).
#
# NOTE on base images: the SQL parser is a cgo binding (libpg_query), so the
# binary MUST be built with CGO_ENABLED=1 — a static CGO_ENABLED=0 build
# fails with `undefined: pg.Parse`. A musl-linked binary then needs a musl
# runtime, hence alpine:3.23 (same as upstream's own image) instead of the
# usual cloudron/base (Ubuntu/glibc).
FROM golang:1.26-alpine AS builder
ARG VERSION=v0.24.0
WORKDIR /src
# Mirrors the upstream Dockerfile build dependencies (git for module
# fetches, gcc/musl-dev for the CGO-enabled build).
RUN apk add --no-cache ca-certificates git gcc musl-dev
# Cache dependency downloads separately from source changes.
COPY repo/go.mod repo/go.sum ./
RUN go mod download
COPY repo/ .
RUN CGO_ENABLED=1 go build \
-ldflags "-s -w -X github.com/dev-services42/version.version=${VERSION}" \
-o /out/database-gateway \
./cmd/gateway
# Same runtime base as the upstream image. The tools back start.sh:
# bash (script), postgresql16-client (wait for the Cloudron PG addon),
# jq (seed config.json), openssl (cookie secret).
FROM alpine:3.23
RUN apk add --no-cache bash ca-certificates jq openssl postgresql16-client
COPY --from=builder /out/database-gateway /usr/local/bin/database-gateway
# start.sh waits for the postgresql addon, seeds /app/data/config.json and
# /app/data/opa/ on first run, applies migrations, then execs the gateway.
# Made executable on the host, not at build time (Cloudron builds hit
# permission errors on RUN chmod).
COPY start.sh /app/start.sh
ENV WORKDIR=/app/data
WORKDIR ${WORKDIR}
EXPOSE 8080
CMD ["/bin/bash", "/app/start.sh"]
@@ -0,0 +1,85 @@
# Database Gateway — Cloudron Package
[Database Gateway](https://github.com/kazhuravlev/database-gateway) (dbgw) is
a web gateway for secure, controlled access to PostgreSQL databases: users
log in via OIDC and run SQL through a unified web UI, with every query
parsed and authorized by embedded **OPA policies** (per user, target,
operation and table). Results are stored in a local PostgreSQL with
shareable links for debugging and auditing.
## Packaging overview
| Aspect | Choice |
|--------|--------|
| Pattern | Multi-stage Go build (JOURNAL pattern #2) |
| Builder | `golang:1.26-alpine` (upstream's own build recipe, CGO on) |
| Base image | `alpine:3.23` (musl runtime required — see below) |
| Upstream | v0.24.0 (Go 1.26.1, Echo; frontend pre-built and embedded via `go:embed`) |
| Addons | `localstorage` + `postgresql` (16) — storage for profiles, bookmarks, query results |
| Auth | **Native OIDC (preferred)** wired to the Cloudron platform identity provider |
| Runtime | `start.sh` waits for Postgres, seeds config + OPA policy, `migrate-up`, execs `run` |
Why alpine instead of `cloudron/base`: the SQL parser is a cgo binding
(libpg_query) — the binary must be built with `CGO_ENABLED=1` (a static
build fails with `undefined: pg.Parse`) and therefore links musl, which
does not exec on Ubuntu/glibc bases. Alpine runtime matches upstream's own
image; bash/psql/jq/openssl are apk-installed for `start.sh`.
Why not the official image: upstream publishes `ghcr.io/kazhuravlev/
database-gateway:latest` but tagging is `:latest`-only; building v0.24.0
from the cloned source pins the version and lets the ldflags carry the
release tag into `/healthcheck`-style version output.
## Authentication (auth gate verdict: OIDC, preferred)
Database Gateway authenticates **only** via OIDC — no local user database,
no LDAP. This is the preferred row of the auth matrix:
- `CLOUDRON_OIDC_ISSUER`, `CLOUDRON_OIDC_CLIENT_ID`,
`CLOUDRON_OIDC_CLIENT_SECRET` (injected by Cloudron's platform OIDC
provider, manifestVersion 2) are written into `/app/data/config.json` on
first start.
- Redirect URL: `${CLOUDRON_APP_ORIGIN}/auth/callback`.
- Scopes: `openid profile email groups`; `role_claim` is `groups`, mapped
`admins``admin`, `users``user` (Cloudron's built-in groups).
- Access policy is then enforced per query by OPA (see below), not just at
login.
## Ports
| Port | Type | Purpose |
|------|------|---------|
| 8080 | `httpPort` | Web UI + LRPC API (only listener) |
## Configuration (persistent, in `/app/data`)
- `config.json` — seeded once on first start (OIDC + storage from Cloudron
env). **Add your database targets here** with the Cloudron file manager;
restart the app after editing. See
[upstream example](https://github.com/kazhuravlev/database-gateway/blob/master/example/config.json)
for the full target format (connection, `default_schema`,
`tables[].table` must be schema-qualified, `fields` allowlist).
- `opa/simple.rego` — seeded default: Cloudron `admins` see all targets and
may run any op; everyone else denied until an operator extends the policy
(input subjects contain `user:<email>` and `role:<role>`).
- `.cookie_secret` — random 32-byte hex, generated once; rotating it
invalidates sessions.
The **postgresql addon** database is the gateway's own storage (users,
bookmarks, cached results) — not a query target. Targets point at the
*other* PostgreSQL instances you want to reach.
## Build & test
```bash
docker build --cgroup-parent ukrrs-batch.slice -t database-gateway-cloudron:test Package-Workspace/Infrastructure/database-gateway/
docker run --rm --entrypoint /usr/local/bin/database-gateway database-gateway-cloudron:test --version
```
## Files
- `Dockerfile` — multi-stage Go build (golang:1.26-alpine → alpine:3.23, musl)
- `CloudronManifest.json` — manifestVersion 2, httpPort 8080, localstorage + postgresql addons
- `start.sh` — DB wait, config/policy seeding, migrations, exec gateway
- `.env.example` — environment knobs consumed at seed time
- `logo.png` — from upstream `frontend/src/favicon-96x96.png`
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
set -euo pipefail
# Database Gateway runtime setup:
# 1. wait for the Cloudron postgresql addon (storage for profiles/bookmarks/
# query results)
# 2. seed /app/data/config.json + /app/data/opa/ on first run
# 3. apply the embedded schema migrations (migrate-up)
# 4. exec the gateway (web UI + LRPC API on 0.0.0.0:8080)
CONFIG_PATH="/app/data/config.json"
OPA_DIR="/app/data/opa"
COOKIE_SECRET_PATH="/app/data/.cookie_secret"
# --- 1. wait for PostgreSQL -------------------------------------------------
DB_HOST=${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}
DB_PORT=${CLOUDRON_POSTGRESQL_PORT:-5432}
DB_NAME=${CLOUDRON_POSTGRESQL_DATABASE:-dbgw}
DB_USER=${CLOUDRON_POSTGRESQL_USERNAME:-dbgw}
DB_PASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD}
echo "Waiting for PostgreSQL at ${DB_HOST}:${DB_PORT} ..."
until PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -c '\q' >/dev/null 2>&1; do
echo "PostgreSQL is unavailable - sleeping"
sleep 2
done
echo "PostgreSQL is up"
# --- 2. seed persistent configuration ---------------------------------------
# Persistent cookie secret (rotating it would invalidate live sessions).
if [[ ! -f "${COOKIE_SECRET_PATH}" ]]; then
umask 077
openssl rand -hex 32 > "${COOKIE_SECRET_PATH}"
echo "Generated new cookie secret"
fi
COOKIE_SECRET=$(cat "${COOKIE_SECRET_PATH}")
# Default OPA policy: Cloudron "admins" see every target and may run any op;
# everyone else is denied until an operator extends the policy. Edit with the
# Cloudron file manager; changes apply on restart.
mkdir -p "${OPA_DIR}"
if [[ ! -f "${OPA_DIR}/simple.rego" ]]; then
cat > "${OPA_DIR}/simple.rego" <<'EOF'
package gateway
# Default Cloudron policy: the admins group is unrestricted; everyone else
# is denied until an operator edits this file (input.subjects contains
# user:<email> and role:<role> principals).
default allow_target := false
default allow_query := false
allow_target if {
"role:admin" in input.subjects
}
allow_query if {
"role:admin" in input.subjects
}
EOF
echo "Seeded default OPA policy at ${OPA_DIR}/simple.rego"
fi
# Seed the app config once (jq handles escaping of secrets into JSON). OIDC
# comes from the Cloudron platform identity provider; targets (the databases
# this gateway may reach) are added by the operator by editing
# ${CONFIG_PATH} with the Cloudron file manager.
if [[ ! -f "${CONFIG_PATH}" ]]; then
jq -n \
--arg db_host "${DB_HOST}" \
--arg db_name "${DB_NAME}" \
--arg db_user "${DB_USER}" \
--arg db_password "${DB_PASSWORD}" \
--argjson db_port "${DB_PORT}" \
--arg issuer "${CLOUDRON_OIDC_ISSUER}" \
--arg client_id "${CLOUDRON_OIDC_CLIENT_ID}" \
--arg client_secret "${CLOUDRON_OIDC_CLIENT_SECRET}" \
--arg origin "${CLOUDRON_APP_ORIGIN}" \
--arg cookie_secret "${COOKIE_SECRET}" \
--arg opa_dir "${OPA_DIR}" \
'{
targets: [],
users: {
client_id: $client_id,
client_secret: $client_secret,
issuer_url: $issuer,
redirect_url: ($origin + "/auth/callback"),
scopes: ["openid", "profile", "email", "groups"],
role_claim: "groups",
role_mapping: {
admins: "admin",
users: "user"
}
},
policy: { path: $opa_dir },
facade: {
port: 8080,
cookie_secret: $cookie_secret,
unsafe_cors_allow_all: false
},
storage: {
host: $db_host,
port: $db_port,
database: $db_name,
username: $db_user,
password: $db_password,
use_ssl: false,
max_pool_size: 16
}
}' > "${CONFIG_PATH}"
chmod 600 "${CONFIG_PATH}"
echo "Seeded config at ${CONFIG_PATH}"
fi
# --- 3. migrations -----------------------------------------------------------
echo "Applying database migrations ..."
/usr/local/bin/database-gateway -c "${CONFIG_PATH}" migrate-up
# --- 4. run --------------------------------------------------------------------
echo "Starting Database Gateway on :8080 ..."
exec /usr/local/bin/database-gateway -c "${CONFIG_PATH}" run