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:
parent
52d8545952
commit
c404c910cd
3 changed files with 103 additions and 33 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue