put the postgres credentials somewhere findable
The password now lands in ~/.zshenv as PGHOST/PGPORT/PGUSER/PGPASSWORD, as well as in
~/.pgpass. Asked for on the grounds that it is an easier place to remember, which is a
real requirement — a credential you cannot find is one you will ask about every time.
.zshenv rather than the .zshrc that was asked for, for two reasons, neither about
secrecy:
- zsh sources .zshrc for INTERACTIVE shells only. Verified: `zsh -c` prints an empty
PGUSER when it is set there, and the right one from .zshenv. A script, a cron entry
or an agent turn running psql would silently get nothing.
- .zshrc is a shared template and seedShellConfig only updates it while it still
matches byte-for-byte, so members keep their edits. A per-member password in it
would strand every member on the template they were created with — a silent
maintenance break rather than a tradeoff.
Both files are still written because they are not redundant: .pgpass is what libpq
reads with no shell involved, so it is the one that works for psycopg, a systemd unit
or a compiled binary. The rotate condition now covers both — either missing means we
cannot reconstruct it from the other, so we regenerate.
Also drops the `head -1 ~/.pgpass` parsing I had put in the zshrc template. It was a
hack, and the values are in the environment before that file is read now anyway.
Verified: zsh sourcing order and both shell types, transpiles. Still no tsgo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,13 @@ import { osUserHome, runAs } from './os-user';
|
||||
//
|
||||
// CREATEDB as many databases as they like, owned by them. Not one database provisioned for them:
|
||||
// the ask was to create databases in their own name, not to be handed one.
|
||||
// ~/.pgpass so `psql` never prompts. Mode 600, owned by them, written the same way as their SSH
|
||||
// keys — see os-user-ssh.ts for why every write into a 700 home goes through
|
||||
// `sudo install`.
|
||||
// ~/.zshenv PGHOST/PGPORT/PGUSER/PGPASSWORD, so every pg command works with no flags — and an
|
||||
// obvious place to read your own password back from.
|
||||
// ~/.pgpass the same credential in the form libpq reads by itself, for everything that is not a
|
||||
// zsh process.
|
||||
//
|
||||
// Both 600, owned by them, written the same way as their SSH keys — see os-user-ssh.ts for why every
|
||||
// write into a 700 home goes through `sudo install`.
|
||||
//
|
||||
// ── What keeps them out of everything else ──
|
||||
//
|
||||
@@ -264,6 +268,7 @@ export async function provisionPostgresRole(params: {
|
||||
|
||||
const home = osUserHome(params.email);
|
||||
const pgpass = join(home, '.pgpass');
|
||||
const zshenv = join(home, '.zshenv');
|
||||
|
||||
try {
|
||||
const existing = await db.execute<{ rolname: string }>(
|
||||
@@ -274,10 +279,14 @@ export async function provisionPostgresRole(params: {
|
||||
// Tested AS THE MEMBER, through runAs — a 700 home is unreadable to the service user, so a `test -f`
|
||||
// run as ourselves reports "missing" for a file sitting right there, and would rotate their password on
|
||||
// every single reprovision.
|
||||
const probe = runAs(params.osUser, ['test', '-f', pgpass]);
|
||||
const hasPgpass = (await probe.exited) === 0;
|
||||
//
|
||||
// BOTH files, because the password is not stored anywhere else: if either is missing we cannot rewrite
|
||||
// it from the other, and rotating is the only route back to a state where the two agree.
|
||||
const present = await Promise.all(
|
||||
[pgpass, zshenv].map(async (path) => (await runAs(params.osUser, ['test', '-f', path]).exited) === 0),
|
||||
);
|
||||
|
||||
const passwordSet = created || !hasPgpass;
|
||||
const passwordSet = created || present.some((found) => !found);
|
||||
const password = passwordSet ? generatePassword() : null;
|
||||
|
||||
if (created) {
|
||||
@@ -291,18 +300,64 @@ export async function provisionPostgresRole(params: {
|
||||
|
||||
if (passwordSet && password) {
|
||||
const { host, port } = pgpassTarget(process.env.POSTGRES_URL);
|
||||
|
||||
// Written twice, on purpose, and they are not redundant.
|
||||
//
|
||||
// ~/.zshenv the readable one. Asked for by name: somewhere obvious to look when you want to know
|
||||
// your own credentials, rather than a dotfile you have to be told about. `.zshenv`
|
||||
// rather than `.zshrc` for two reasons — zsh sources it for NON-interactive shells too,
|
||||
// so a script, a cron entry or an agent turn gets it, and `.zshrc` is a shared template
|
||||
// that `seedShellConfig` only updates while it still matches byte-for-byte. Writing a
|
||||
// per-member password into that file would strand every member on the template they
|
||||
// were created with.
|
||||
//
|
||||
// ~/.pgpass the one libpq reads on its own, with no shell involved at all. Anything that is not a
|
||||
// zsh process — a systemd unit, python's psycopg, a compiled binary — finds this and
|
||||
// nothing else.
|
||||
//
|
||||
// Both 600. psql ignores a .pgpass that is group- or world-readable, exactly like ssh and
|
||||
// authorized_keys, so there it is a requirement rather than caution.
|
||||
const files: Array<{ content: string; dest: string; label: string }> = [
|
||||
{
|
||||
// `*` for the database field: they may create as many as they like and every one of them is theirs.
|
||||
// psql ignores a .pgpass that is group- or world-readable, exactly like ssh and authorized_keys, so
|
||||
// 600 is a requirement rather than caution.
|
||||
const written = await installFile({
|
||||
content: `${host}:${port}:*:${params.osUser}:${password}\n`,
|
||||
dest: pgpass,
|
||||
label: '~/.pgpass',
|
||||
},
|
||||
{
|
||||
content: [
|
||||
'# Officer — your Postgres credentials.',
|
||||
'#',
|
||||
'# Written when your account was created. Officer rewrites this file, so put your own settings',
|
||||
'# in ~/.zshrc.local instead.',
|
||||
'#',
|
||||
'# With these set, no flags are needed: createdb myapp • psql myapp • dropdb myapp',
|
||||
'# Make as many databases as you like. They are yours, and nobody else can reach them.',
|
||||
`export PGHOST=${host}`,
|
||||
`export PGPORT=${port}`,
|
||||
`export PGUSER=${params.osUser}`,
|
||||
`export PGPASSWORD=${password}`,
|
||||
'',
|
||||
].join('\n'),
|
||||
dest: zshenv,
|
||||
label: '~/.zshenv',
|
||||
},
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const written = await installFile({
|
||||
content: file.content,
|
||||
dest: file.dest,
|
||||
uid: params.uid,
|
||||
gid: params.gid,
|
||||
mode: '600',
|
||||
});
|
||||
if (!written.ok) {
|
||||
return { ok: false, error: `role ${params.osUser} exists but ~/.pgpass could not be written: ${written.out}` };
|
||||
return {
|
||||
ok: false,
|
||||
error: `role ${params.osUser} exists but ${file.label} could not be written: ${written.out}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,16 +115,13 @@ fi
|
||||
#
|
||||
# Nobody else can read your data, and you cannot reach Officer's own database.
|
||||
#
|
||||
# PGHOST is set because Postgres runs in a container published on loopback: there is no unix socket on
|
||||
# this machine, and without this psql fails with "No such file or directory", which reads like Postgres
|
||||
# is not installed rather than like it is one flag away.
|
||||
# Nothing is set here. PGHOST, PGPORT, PGUSER and PGPASSWORD are in ~/.zshenv, which zsh reads before
|
||||
# this file and for non-interactive shells too — so scripts and agents get them as well. That is also
|
||||
# where to look if you want to read your own password; `cat ~/.zshenv`.
|
||||
#
|
||||
# Read out of ~/.pgpass rather than hard-coded, for the same reason the PATH lines above are $HOME-relative
|
||||
# — one fact, in one place, that cannot come to disagree with itself.
|
||||
if [ -f "$HOME/.pgpass" ]; then
|
||||
export PGHOST="${$(head -1 "$HOME/.pgpass")%%:*}"
|
||||
export PGPORT="$(head -1 "$HOME/.pgpass" | cut -d: -f2)"
|
||||
fi
|
||||
# PGHOST matters more than it looks: Postgres runs in a container published on loopback, so there is no
|
||||
# unix socket on this machine, and without it psql fails with "No such file or directory" — which reads
|
||||
# like Postgres is not installed rather than like it is one flag away.
|
||||
|
||||
# ── Docker ──
|
||||
# Officer no longer provisions a rootless daemon per account (2026-08-13) — a Postgres role covers the
|
||||
|
||||
Reference in New Issue
Block a user