Skip to content

Harden the deployment: Postgres on a socket, credentials from Vault - #198

Merged
adamjohnwright merged 3 commits into
mainfrom
feat/vault-postgres-hardening
Sep 10, 2026
Merged

adamjohnwright merged 3 commits into
mainfrom
feat/vault-postgres-hardening

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

Harvested from security-improvements, rebuilt on current main, with the pieces that would have broken production left out.

What it does

Postgres no longer listens on TCP at all. It starts with --auth-host reject and POSTGRES_HOST_AUTH_METHOD=reject; every consumer reaches it over a shared unix-socket volume.

Verified against a real container, not reasoned about:

psql -h 127.0.0.1  ->  connection to server at "127.0.0.1", port 5432 failed: Connection refused
psql (socket, no pw) ->  fe_sendauth: no password supplied
psql (socket, auth)  ->  chainlit|postgres          # and initdb created chainlit + langgraph

Vault, where deployed, issues short-lived Postgres credentials (1h default, 24h max) over its own unix socket, and rotates the bootstrap POSTGRES_PASSWORD out of use once configured. Without a Vault token mounted, get_db_uri falls back to POSTGRES_PASSWORD — the development path.

The container stops running as root — it builds and runs as appuser:appgroup with application files copied read-only.

Three defects in the original, fixed rather than carried

logged the credentials Vault issued — logging.warning(response), where response["data"] is the generated username and password nothing is logged now but the exception type
unbounded wait on VaultDown — a Vault that never got unsealed left the container running and silent forever gives up after ~5 minutes with a message saying what to do
Dockerfile dropped nltk.downloader punkt_tab — BM25 tokenises with word_tokenize(language="english") restored at build time

Two more found while writing the tests

  • get_db_uri was called twice, once inside the data-layer factory chainlit invokes per session — under Vault that mints a fresh lease per visitor. Built once now.
  • urllib.parse.quote leaves / unescaped by default, and Vault passwords are random punctuation — an unescaped slash truncates the URI at the database name. safe="" now. The test caught this, not review.

Deliberately not included

  • the ChainlitDataLayer switch and its chainlit-datalayer git submodule — a persistence change, not a security one
  • the survey message added to config_default.yml
  • the chainlit.md branding edit — that needs the per-deployment flag we discussed, not a silent overwrite

203 tests pass; ruff and mypy clean.

GFJHogue and others added 3 commits April 17, 2025 18:43
Harvested from security-improvements, rebuilt on current main and with
the pieces that would have broken production left out.

Postgres no longer listens on TCP at all. It starts with
--auth-host reject and POSTGRES_HOST_AUTH_METHOD=reject, and every
consumer reaches it over a shared unix socket volume. Verified against
a real container: `psql -h 127.0.0.1` gives "Connection refused", the
socket refuses an unauthenticated connection with fe_sendauth, and an
authenticated socket connection returns a row from the chainlit
database that initdb created.

Vault, where deployed, issues short-lived Postgres credentials (1h
default, 24h max) over its own unix socket, and rotates the bootstrap
POSTGRES_PASSWORD out of use as soon as it is configured. Without a
Vault token mounted, get_db_uri falls back to POSTGRES_PASSWORD, which
is the development path.

The container also stops running as root: it builds and runs as
appuser:appgroup, with application files copied read-only.

Three defects in the original are fixed rather than carried:

- It logged the credentials Vault issued it -- logging.warning(response)
  where response["data"] is the generated username and password.
  Nothing is logged now but the exception type.
- It waited on `hvac.exceptions.VaultDown` in an unbounded loop, so a
  Vault that never got unsealed left the container running and silent
  forever. It now gives up after ~5 minutes and says what to do.
- Its Dockerfile dropped `nltk.downloader punkt_tab`. BM25 tokenises
  with word_tokenize(language="english"), so retrieval would have
  downloaded it at runtime or failed.

Two more found while writing the tests:

- get_db_uri called twice in chat-chainlit.py, once inside the data
  layer factory chainlit invokes per session -- under Vault that mints
  a fresh lease per visitor. Built once now.
- urllib.parse.quote leaves "/" unescaped by default, and Vault
  passwords are random punctuation; an unescaped slash truncates the
  URI at the database name. safe="" now.

Deliberately not included: the ChainlitDataLayer switch and its
chainlit-datalayer git submodule (a persistence change, not a security
one), the survey message in config_default.yml, and the chainlit.md
branding edit -- that one needs the per-deployment flag discussed
separately, not a silent overwrite.
CI's "can every entry point be imported" check failed: the export
scripts resolved their database URI at module scope and raised
SystemExit when none was configured, and the runner has no Postgres.

My change, and the wrong shape for it. Importing a script should do
nothing; running it should fail loudly. The resolution now lives in a
function the script calls from main().

Verified both halves: verify_imports.py passes, and calling the helper
without POSTGRES_LANGGRAPH_DB still exits with the message naming what
to set.
@adamjohnwright
adamjohnwright merged commit 6c57fe0 into main Sep 10, 2026
10 checks passed
@adamjohnwright
adamjohnwright deleted the feat/vault-postgres-hardening branch September 10, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants