refactor(secrets): every private-credential file is written by utils.atomic_json_write(mode=0o600)
Ten hand-rolled "write a token file safely" routines each carried a
different subset of {0600-on-create, fsync, atomic_replace, parent-0700
guard, BaseException cleanup}. Two of them (iron_proxy state files,
the exchanged-JWT store) still opened the temp file at process umask
and chmod'ed afterwards - the exact TOCTOU window the others document
as fixed. None of the bare-os.replace copies got atomic_replace's
Windows-contention retry or EXDEV fallback.
utils gains fsync_dir= (absorbs auth.py's dir fsync), atomic_write_bytes
(vault blob) and mode= on atomic_write_text; the ten sites become 1-3
line callers. mkstemp creates the temp file O_EXCL at 0600 regardless of
umask, so the payload is never umask-readable.
Behavior change: iron_proxy proxy.yaml/mappings.json and the exchanged-JWT
store are now 0600 from creation and fsync'd; every credential write goes
through atomic_replace (symlink-preserving, Windows retry, EXDEV copy).
auth_nous shared store now uses atomic_replace too (it forced os.replace
with no recorded reason). secret_sources cache parent-0700 goes through
the guarded secure_parent_dir instead of an unguarded chmod.
This commit is contained in:
@@ -19,6 +19,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags
|
||||
from utils import atomic_json_write
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -270,15 +271,6 @@ def _read_jwt_store(path: Path) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def _write_jwt_store(path: Path, store: dict) -> None:
|
||||
"""Atomically write the JWT store (tmp + os.replace), best-effort 0o600."""
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(store), encoding="utf-8")
|
||||
with contextlib.suppress(Exception):
|
||||
os.chmod(tmp, 0o600)
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def _jwt_disk_path() -> Optional[Path]:
|
||||
"""Path to the on-disk exchanged-JWT cache (profile-aware), or None."""
|
||||
try:
|
||||
@@ -313,7 +305,7 @@ def evict_cached_exchanged_token(raw_token: str) -> None:
|
||||
def _evict(path, store):
|
||||
if store is not None and fp in store:
|
||||
del store[fp]
|
||||
_write_jwt_store(path, store)
|
||||
atomic_json_write(path, store, indent=None, mode=0o600)
|
||||
|
||||
_with_jwt_store("evict cached", _evict)
|
||||
|
||||
@@ -339,7 +331,7 @@ def _save_jwt_to_disk(fp: str, api_token: str, expires_at: float, base_url: Opti
|
||||
k: v for k, v in (store or {}).items()
|
||||
if isinstance(v, dict) and float(v.get("expires_at", 0) or 0) > now}
|
||||
kept[fp] = {"api_token": api_token, "expires_at": expires_at, "base_url": base_url}
|
||||
_write_jwt_store(path, kept)
|
||||
atomic_json_write(path, kept, indent=None, mode=0o600)
|
||||
|
||||
_with_jwt_store("persist", _save)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user