Files
five/hbrtl/pgserver/server.go
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

6.0 KiB