Skip to content

Read secrets from Docker secrets, falling back to the environment - #197

Merged
adamjohnwright merged 3 commits into
mainfrom
feat/docker-secrets
Sep 10, 2026
Merged

adamjohnwright merged 3 commits into
mainfrom
feat/docker-secrets

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

Harvested from security-improvements, which is marked [WIP] and pairs this with a Vault deployment. Only the Docker Secrets half is taken — it works with the compose file that exists today, needs no new services, and changes nothing for a deployment that mounts nothing.

An environment variable is visible in docker inspect, in the process environment of every child, and in a .env on disk that has to be chmod'ed by hand — which is what we did to this host on 2026-09-04. A Docker secret is a file mounted only into the containers that declare it.

Two behaviours worth stating, both tested:

  • A mounted file beats the environment. A deployment that mounted a secret meant it; preferring a stale env var would make the secret look applied when it was not, surfacing as an auth error somewhere else entirely.
  • A blank file counts as absent. init-docker-secrets on that branch creates placeholders containing a single space, and a blank password reaching Postgres fails somewhere far less obvious than here.

What I did not take, and why

The Vault half of that branch is not mergeable as it stands:

  • it reaches Postgres over a unix socket the current compose does not mount (?host=/sockets/postgres/), so it cannot connect on today's topology
  • it loops forever on hvac.exceptions.VaultDown waiting for an unseal — startup never fails, it just never finishes
  • it switches chainlit's data layer (SQLAlchemyDataLayer → ChainlitDataLayer) and adds a git submodule
  • it logs the credentials Vault issues it: logging.warning(response), where response["data"] is the generated username and password

That is a deployment migration to plan and test, not a merge. Happy to do it as focused work — it needs Vault running, the socket topology, and a beta rehearsal.

The other two branches

analysis and safetycheck have nothing left to harvest — verified file by file, not assumed:

expert survey R pipeline already on main
safety checker prompt main's has the userguide relevance rule and five worked examples the branch lacks, and builds the prompt once at module level rather than per call
unsafe_answer.py main's unsafe_question.py is the same plus multi-language support
tools/preprocessing/ main runs rephrase → (safety ∥ language) concurrently in base.py; the branch runs rephrase → safety sequentially as a compiled subgraph, has no language detection, and imports the private langgraph.utils.runnable.RunnableLike that 1.x removed
analysis/ragas_evaluation/ duplicate of src/evaluation/, including the test_generator.py deleted in #195

They can be closed with credit and deleted.

Harvested from the security-improvements branch, which is marked WIP and
pairs this with a Vault deployment. Only the Docker Secrets half is
taken: it works with the compose file that exists today, needs no new
services, and changes nothing for a deployment that mounts nothing.

An environment variable is visible in `docker inspect`, in the process
environment of every child, and in a .env file on disk that has to be
chmod'ed by hand -- which is what was done to this host on 2026-09-04. A
Docker secret is a file mounted only into the containers that declare it.

Two behaviours worth stating, both tested:

- A mounted file beats the environment. A deployment that mounted a
  secret meant it; preferring a stale environment variable would make
  the secret look applied when it was not, and surface as an auth error
  somewhere else entirely.
- A blank file counts as absent. init-docker-secrets on that branch
  creates placeholders containing a single space, and a blank password
  reaching Postgres fails somewhere far less obvious than here.

The Vault half of the branch is deliberately not included. It reaches
Postgres over a unix socket the current compose does not mount, loops
forever on `hvac.exceptions.VaultDown` waiting for an unseal, switches
chainlit's data layer and adds a git submodule -- and it logs the
credentials Vault issues it, at warning level. That is a deployment
migration to plan, not a merge.
Comment thread bin/chat-chainlit.py Fixed
CodeQL flagged the startup log as "Clear-text logging of sensitive
information", and it was right to. The line logged only names, but they
came back from load_secrets_to_environ -- a function that had read every
secret body -- so nothing about the value said it was safe to log.

Suppressing that would have been the same mistake the Vault code on
security-improvements makes with logging.warning(response), which this
PR's description criticises.

mounted_secrets() answers "which of these were supplied" from st_size
alone and never opens a file, so it is what the log is built from.
load_secrets_to_environ() now returns nothing at all.
Comment thread bin/chat-chainlit.py Fixed
CodeQL still flagged the startup log after splitting the stat-only
listing from the read. Its clear-text-logging rule matches on the shape
of the strings -- OPENAI_API_KEY, CLOUDFLARE_SECRET_KEY -- not on where
they came from, so mounted_secrets() not opening a file does not settle
it.

The names are genuinely not secret values, but dismissing a security
alert to keep a convenience is not a trade worth making, and `ls
/run/secrets` answers the same question. The log now carries a count.
@adamjohnwright
adamjohnwright merged commit f81c387 into main Sep 10, 2026
10 checks passed
@adamjohnwright
adamjohnwright deleted the feat/docker-secrets branch September 10, 2026 13:52
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