Files
five/tests/pgserver/run.sh
CharlesKWON 3b2dd365ad feat(pgserver): Phase 6 — TLS + source-IP allowlist
Closes the v1.0 hardening surface: encrypted transport + a
coarse pg_hba.conf-equivalent CIDR allowlist. Together with the
Phase 5 auth flows, this is the security-baseline an internet-
exposed PostgreSQL-wire server needs.

TLS subsystem
-------------

`hbrtl/pgserver/tls.go`:

* `LoadTLSFromFiles(certPath, keyPath)` — cert/key PEM pair load
  with tls.VersionTLS12 floor. Installed as the *pending* config
  that the next PG_SERVER_START consumes (matches PG's
  "must-set-before-pg_ctl-start" semantics).

* `GenerateSelfSignedCert(certPath, keyPath, hostname)` — ECDSA
  P-256 + 365-day validity + DNSNames+IPAddresses SANs covering
  the hostname plus 127.0.0.1 / ::1. Dev/CI helper; production
  ships a CA-signed cert via the loader.

* `upgradeToTLS()` wraps `tls.Server(conn, cfg).Handshake()` so
  pgproto3 reads plaintext on top of the encrypted stream.

Source-IP allowlist
-------------------

* `AllowIP(cidr)` parses a CIDR and appends it to a per-server
  list snapshotted at PG_SERVER_START time.
* `peerAllowed(remote, list)` runs at accept() — empty list →
  accept any, otherwise drop connections whose RemoteAddr falls
  outside every registered range.
* `ClearAllowList()` resets to allow-all.

Coarse but compatible with the "host alice 10.0.0.0/8 md5"-style
entries every pg_hba.conf author already knows; a fuller per-
role/per-database matcher is Phase 6.1+.

PRG bindings (register.go)
--------------------------

New HB_FUNCs, all idempotent and composable in any order before
PG_SERVER_START:

  pg_tls_load( certPath, keyPath )           → .T. | cErr
  pg_tls_self_signed( cert, key, hostname )  → .T. | cErr
  pg_allow_ip( cidr )                        → .T. | cErr
  pg_clear_allowlist()                       → NIL

Bootstrap idiom:

  PROCEDURE Main()
     PG_TLS_SELF_SIGNED( "/tmp/cert.pem", "/tmp/key.pem", "localhost" )
     PG_ADD_ROLE( "alice", "swordfish" )
     PG_ALLOW_IP( "127.0.0.1/32" )
     PG_ALLOW_IP( "10.0.0.0/8" )
     PG_SERVER_START( ":5432", "md5" )

The startup banner now reports TLS + allowlist state so the PRG
operator sees the security posture at a glance:

  pgserver: listening on :5432 (auth=md5 tls=on allowlist=2)

Verification
------------

End-to-end via real psql against a self-signed server:

  $ PGPASSWORD=swordfish psql \
        "postgres://alice@127.0.0.1:15432/alice?sslmode=require" \
        -c "SELECT 'tls-works' AS x" -At
  tls-works

  $ # off-allowlist source (192.168.x.x mock) → connection refused
  $ # (verified manually; psql can't easily spoof src IP for CI)

Integration script gates expanded to 6/6:
  PASS  Simple Query
  PASS  Multi-statement Simple Query
  PASS  Transaction control
  PASS  MD5 auth: wrong password rejected
  PASS  MD5 auth: correct password accepted
  PASS  TLS handshake + MD5 auth via sslmode=require

All six release gates green:
  go test ./...               ✓
  FiveSql2 SQL:1999 43/43     ✓
  Harbour compat 56/56        ✓
  std.ch 17/17                ✓
  FRB 7/7                     ✓
  pgserver integration 6/6    ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:07:19 +09:00

178 lines
5.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# tests/pgserver/run.sh — integration harness for the pgserver wire
# layer. Builds a bootstrap PRG that starts the server, then drives
# it from a Go-side pgx client (located alongside this script).
#
# Verified scope (Phase 3):
# * CREATE TABLE / INSERT / UPDATE / DELETE over Simple Query
# * BEGIN / COMMIT / ROLLBACK from the wire
# * Two-connection cross-visibility on shared DBF
# * Per-session ROLLBACK doesn't affect other connection
#
# Known limitation (tracked for Phase 7):
# * ≥3 concurrent connections doing in-flight INSERT/SELECT in
# their own transactions can race at the hbrdd workarea layer
# — surfaces as one worker's just-inserted row missing from its
# own SELECT. Two-connection serial use, and N-connection use
# where each goroutine completes its txn before the next starts,
# are both reliable. Multi-way append-time WA arbitration is
# deferred until the audit's "WorkArea collision under multi-
# session" Top-Risk #2 fix lands.
set -e
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
FIVE="$ROOT/five"
PORT="${PGSERVER_TEST_PORT:-15432}"
if [ ! -x "$FIVE" ]; then
echo "five binary not found at $FIVE — run 'go build -o five ./cmd/five' first" >&2
exit 2
fi
if ! command -v psql >/dev/null 2>&1; then
echo "psql not in PATH — install PostgreSQL client tools to run this suite" >&2
exit 2
fi
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
# Bootstrap PRG — opens nothing, just stands up the server.
cat > "$work/boot.prg" <<EOF
PROCEDURE Main()
PG_SERVER_START( ":$PORT" )
RETURN
EOF
"$FIVE" build "$work/boot.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/boot" >/dev/null 2>&1
"$work/boot" &
SERVER_PID=$!
sleep 1
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
pass=0
total=0
ok() {
pass=$((pass+1))
total=$((total+1))
echo "PASS $1"
}
fail() {
total=$((total+1))
echo "FAIL $1"
echo "$2" | sed 's/^/ /'
}
# 1) Simple Query via psql.
out="$(psql "postgres://alice:any@127.0.0.1:$PORT/alice?sslmode=disable" \
-c "SELECT 1 AS one, 'hello' AS greet" -At 2>&1 || true)"
if echo "$out" | grep -q "1|hello"; then
ok "Simple Query: SELECT 1, 'hello'"
else
fail "Simple Query: SELECT 1, 'hello'" "$out"
fi
# 2) Multi-statement Simple Query — each ';'-separated stmt rolls
# through the engine independently. Verifies wire reuses one
# session across statements without bleed.
out="$(psql "postgres://alice:any@127.0.0.1:$PORT/alice?sslmode=disable" -At <<SQL 2>&1 || true
SELECT 'first';
SELECT 2 AS n;
SQL
)"
if echo "$out" | grep -q "first" && echo "$out" | grep -q "^2$"; then
ok "Multi-statement Simple Query"
else
fail "Multi-statement Simple Query" "$out"
fi
# Note on Extended Protocol coverage:
# psql can't drive raw Parse/Bind/Execute from -c invocations
# (PG's SQL-level PREPARE/EXECUTE is a separate feature that
# FiveSql2 doesn't parse). The Extended Protocol path is instead
# covered by hbrtl/pgserver/wire_test.go (Go unit) plus the
# pgx-driven manual sanity script in /tmp/pgs_test/. Adding a
# self-contained Go integration that bootstraps the server +
# drives pgx in one process is Phase 7 work.
# 3) Transaction control via simple query — BEGIN/COMMIT round-trip
# must leave ReadyForQuery in 'I' state so psql doesn't hang on
# the next command.
out="$(psql "postgres://alice:any@127.0.0.1:$PORT/alice?sslmode=disable" -At <<SQL 2>&1 || true
BEGIN;
SELECT 'in-txn';
COMMIT;
SELECT 'post-commit';
SQL
)"
if echo "$out" | grep -q "in-txn" && echo "$out" | grep -q "post-commit"; then
ok "Transaction control: BEGIN/COMMIT round-trip"
else
fail "Transaction control: BEGIN/COMMIT round-trip" "$out"
fi
# 4) MD5 authentication — kill the trust-mode server, restart with
# md5 + a known role, then verify both the rejection and success
# paths.
kill $SERVER_PID 2>/dev/null
wait 2>/dev/null
cat > "$work/auth.prg" <<EOF
PROCEDURE Main()
PG_ADD_ROLE( "alice", "swordfish" )
PG_SERVER_START( ":$PORT", "md5" )
RETURN
EOF
"$FIVE" build "$work/auth.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/auth" >/dev/null 2>&1
"$work/auth" &
SERVER_PID=$!
sleep 1
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
bad="$(PGPASSWORD=wrong psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=disable" \
-c "SELECT 1" 2>&1 | head -1 || true)"
if echo "$bad" | grep -qi "md5 authentication failed"; then
ok "MD5 auth: wrong password rejected"
else
fail "MD5 auth: wrong password rejected" "$bad"
fi
good="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=disable" \
-c "SELECT 'ok' AS x" -At 2>&1 || true)"
if echo "$good" | grep -q "^ok$"; then
ok "MD5 auth: correct password accepted"
else
fail "MD5 auth: correct password accepted" "$good"
fi
# 5) TLS — restart server with self-signed cert + allowlist and
# connect via psql sslmode=require.
kill $SERVER_PID 2>/dev/null
wait 2>/dev/null
cat > "$work/tls.prg" <<EOF
PROCEDURE Main()
PG_TLS_SELF_SIGNED( "$work/cert.pem", "$work/key.pem", "localhost" )
PG_ADD_ROLE( "alice", "swordfish" )
PG_ALLOW_IP( "127.0.0.1/32" )
PG_SERVER_START( ":$PORT", "md5" )
RETURN
EOF
"$FIVE" build "$work/tls.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/tls" >/dev/null 2>&1
"$work/tls" &
SERVER_PID=$!
sleep 1
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
tls_out="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=require" \
-c "SELECT 'tls-ok' AS x" -At 2>&1 || true)"
if echo "$tls_out" | grep -q "^tls-ok$"; then
ok "TLS handshake + MD5 auth via sslmode=require"
else
fail "TLS handshake + MD5 auth via sslmode=require" "$tls_out"
fi
echo "================================================================"
echo " pgserver integration: $pass / $total passed"
echo "================================================================"
[ $pass -eq $total ]