Read mounted DB credentials from a Kubernetes snapshot

Secret volume rotation swaps ..data. Sequential reads of username then
password can tear across two leases. Resolve the snapshot once.

Also document why ESO AppRole login cannot parent database/creds leases:
the token discard DROP ROLEs the role ESO just stored.
This commit is contained in:
tegwick 2026-08-13 12:25:32 +02:00
parent 52d8545952
commit c404c910cd
3 changed files with 103 additions and 33 deletions

View file

@ -55,25 +55,50 @@ class CredentialDirectory:
Called per connection attempt, so a rotated password is picked up by
the next connection the pool opens without any restart.
Kubernetes Secret volumes update by swapping the ``..data`` directory.
Reading ``username`` then ``password`` from the mount point can tear
across that swap and pair a new user with an old password. Resolve
``..data`` once and read every file from that snapshot.
"""
last_error: Exception | None = None
for _ in range(5):
try:
params = self._read_snapshot()
except (FileNotFoundError, ValueError) as exc:
last_error = exc
continue
if "dsn" in params:
self._note_rotation(params["dsn"])
return params
if "user" not in params:
last_error = ValueError(f"no username in credential directory {self.path}")
continue
self._note_rotation(params.get("password", ""))
return params
if last_error is not None:
raise last_error
raise ValueError(f"no username in credential directory {self.path}")
def _snapshot_dir(self) -> pathlib.Path:
data = self.path / "..data"
if data.exists():
return data.resolve()
return self.path
def _read_snapshot(self) -> dict[str, str]:
root = self._snapshot_dir()
params: dict[str, str] = {}
dsn_file = self.path / "dsn"
dsn_file = root / "dsn"
if dsn_file.exists():
params["dsn"] = dsn_file.read_text().strip()
self._note_rotation(params["dsn"])
return params
for name, keyword in _FIELDS.items():
candidate = self.path / name
candidate = root / name
if candidate.exists():
value = candidate.read_text().strip()
if value:
params.setdefault(keyword, value)
if "user" not in params:
raise ValueError(f"no username in credential directory {self.path}")
# Fingerprint the password so rotation is visible in logs without the
# password itself ever being logged.
self._note_rotation(params.get("password", ""))
return params
def _note_rotation(self, secret: str) -> None: