2ce68cb098
- Move db/schema.sql to supabase/migrations/ as the first supabase CLI migration (manual `db push` only, no automated runner); re-add the gebos_ingest role + grants there since init.sql never re-runs - Add gebos-postgres-passwords oneshot on db-host: syncs role passwords from sops (LoadCredential, journal-safe), makes supabase_admin SUPERUSER and hands the auth schema to supabase_auth_admin to match the upstream supabase/postgres image; add pg_hba rule for 10.0.0.0/8 - Vendor the official docker-compose (studio/kong/auth/rest/meta only, external Postgres, loopback Studio with no Kong dashboard route) plus kong.yml (trimmed) and kong-entrypoint.sh (verbatim); tested: compose config, Kong config parse, migration applied on TimescaleDB pg17 - Document decisions as ADRs 0001-0004 (migrations, passwords, vendored stack, JWT API keys incl. verify/mint procedure) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
5.8 KiB
Nix
135 lines
5.8 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.gebos.postgres;
|
|
in
|
|
{
|
|
options.services.gebos.postgres = {
|
|
enable = lib.mkEnableOption "Gebos Postgres 17 + TimescaleDB";
|
|
listenAddresses = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ "127.0.0.1" ];
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# TimescaleDB ships under the non-OSI "Timescale License" (TSL), which
|
|
# nixpkgs flags as unfree. Allow just this package rather than opening the
|
|
# whole config to unfree.
|
|
nixpkgs.config.allowUnfreePredicate = pkg:
|
|
lib.elem (lib.getName pkg) [ "timescaledb" ];
|
|
|
|
services.postgresql = {
|
|
enable = true;
|
|
package = pkgs.postgresql_17;
|
|
# listen_addresses is managed explicitly below. The upstream postgresql
|
|
# module also defines it (via its own default / enableTCPIP), so mkForce
|
|
# makes this module the single source of truth and avoids the collision.
|
|
settings = {
|
|
listen_addresses = lib.mkForce (lib.concatStringsSep "," cfg.listenAddresses);
|
|
shared_preload_libraries = "timescaledb,pg_stat_statements";
|
|
};
|
|
extensions = ps: with ps; [
|
|
timescaledb
|
|
pgjwt
|
|
pg_graphql
|
|
pgsodium
|
|
# supabase_vault — TODO: package or vendor
|
|
];
|
|
# Init script creates Supabase's `auth`, `storage`, `_analytics`, `_supavisor`
|
|
# schemas and the Supabase admin/API roles. It only runs at first initdb;
|
|
# everything else arrives via manual supabase migrations (see
|
|
# supabase/README.md) and the password oneshot below.
|
|
initialScript = ../supabase/init.sql;
|
|
|
|
# The firewall limits 5432 to 10.0.0.0/8, but NixOS's default pg_hba
|
|
# only covers local sockets and loopback — without this line every
|
|
# connection from app-host / mqtt-ingest is rejected before password
|
|
# auth even starts.
|
|
authentication = ''
|
|
host all all 10.0.0.0/8 scram-sha-256
|
|
'';
|
|
};
|
|
|
|
# Role passwords are data, not configuration: NixOS has no declarative
|
|
# option for them (anything reachable from the config lands world-readable
|
|
# in /nix/store, and initialScript only runs at first initdb). This
|
|
# oneshot re-applies them from the sops-rendered env file on every boot /
|
|
# deploy, idempotently. It also enforces two attributes the Supabase
|
|
# services expect because the upstream supabase/postgres image ships them:
|
|
# supabase_admin is SUPERUSER (Studio/postgres-meta connect as it) and
|
|
# supabase_auth_admin owns the auth schema (GoTrue runs its own migrations
|
|
# there on startup).
|
|
#
|
|
# After rotating a password in secrets.yaml, re-run with
|
|
# `systemctl restart gebos-postgres-passwords` (deploys re-run it only
|
|
# when the unit definition itself changed).
|
|
systemd.services.gebos-postgres-passwords =
|
|
lib.mkIf config.services.gebos.secrets.postgresAdmin {
|
|
description = "Gebos — sync Postgres role passwords from sops secrets";
|
|
after = [ "postgresql.service" ];
|
|
requires = [ "postgresql.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [ config.services.postgresql.package ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "postgres";
|
|
# LoadCredential hands the root-owned 0400 env file to the postgres
|
|
# user without widening its permissions.
|
|
LoadCredential = "gebos-env:${config.services.gebos.secrets.envFile}";
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
set -a; . "$CREDENTIALS_DIRECTORY/gebos-env"; set +a
|
|
|
|
# All three come from the gebos-env template; the toggles in
|
|
# db-host.nix (postgresAdmin + supabase + ingester) must be on.
|
|
# --output=/dev/null: the set_config() SELECTs would otherwise echo
|
|
# the passwords into the journal.
|
|
psql -v ON_ERROR_STOP=1 --output=/dev/null \
|
|
-v admin_pw="''${POSTGRES_ADMIN_PASSWORD:?}" \
|
|
-v supabase_pw="''${POSTGRES_PASSWORD:?}" \
|
|
-v ingest_pw="''${GEBOS_POSTGRES_PASSWORD:?}" \
|
|
--dbname postgres <<'SQL'
|
|
-- psql :'var' interpolation does not reach inside DO bodies, so
|
|
-- stash the values in session GUCs first.
|
|
SELECT set_config('gebos.supabase_pw', :'supabase_pw', false);
|
|
SELECT set_config('gebos.ingest_pw', :'ingest_pw', false);
|
|
|
|
ALTER ROLE postgres PASSWORD :'admin_pw';
|
|
|
|
DO $do$
|
|
DECLARE r text;
|
|
BEGIN
|
|
-- One shared password for the roles the compose services log in
|
|
-- as, mirroring upstream's POSTGRES_PASSWORD convention.
|
|
FOREACH r IN ARRAY ARRAY['supabase_admin','supabase_auth_admin','authenticator'] LOOP
|
|
IF EXISTS (SELECT FROM pg_roles WHERE rolname = r) THEN
|
|
EXECUTE format('ALTER ROLE %I PASSWORD %L',
|
|
r, current_setting('gebos.supabase_pw'));
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_admin') THEN
|
|
EXECUTE 'ALTER ROLE supabase_admin SUPERUSER';
|
|
END IF;
|
|
|
|
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_auth_admin')
|
|
AND EXISTS (SELECT FROM pg_namespace WHERE nspname = 'auth') THEN
|
|
EXECUTE 'ALTER SCHEMA auth OWNER TO supabase_auth_admin';
|
|
END IF;
|
|
|
|
-- Created by the schema-v1 migration, so tolerate its absence on
|
|
-- a cluster the migration has not reached yet.
|
|
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'gebos_ingest') THEN
|
|
EXECUTE format('ALTER ROLE gebos_ingest PASSWORD %L',
|
|
current_setting('gebos.ingest_pw'));
|
|
END IF;
|
|
END
|
|
$do$;
|
|
SQL
|
|
'';
|
|
};
|
|
};
|
|
}
|