fix(gateway): overwrite stale env vars on keystore-backed refresh
The gateway refresh path now calls keystore injection with force=True so rotated secrets replace stale in-process env vars without requiring a restart. Startup paths still keep the default non-overwriting behavior so shell exports and explicitly supplied env vars win on boot. Also tighten the regression test to require the rotated keystore secret to replace a stale env value during refresh, instead of accepting either old or new values.
This commit is contained in:
+8
-3
@@ -87,9 +87,14 @@ _env_path = _hermes_home / '.env'
|
||||
load_hermes_dotenv(hermes_home=_hermes_home, project_env=Path(__file__).resolve().parents[1] / '.env')
|
||||
|
||||
|
||||
def _inject_keystore_env() -> None:
|
||||
def _inject_keystore_env(force: bool = False) -> None:
|
||||
"""Inject keystore-backed secrets into os.environ when available.
|
||||
|
||||
Args:
|
||||
force: If True, overwrite already-present env vars with the latest
|
||||
keystore values. This is used by the gateway refresh path so
|
||||
secret rotation takes effect without restart.
|
||||
|
||||
Runs independently of config.yaml so gateway/headless deployments using
|
||||
only a keystore (with stubbed .env) still receive credentials.
|
||||
"""
|
||||
@@ -97,7 +102,7 @@ def _inject_keystore_env() -> None:
|
||||
from keystore.client import get_keystore
|
||||
_ks = get_keystore()
|
||||
if _ks.is_initialized and _ks.ensure_unlocked(interactive=False):
|
||||
_ks.inject_env()
|
||||
_ks.inject_env(force=force)
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as _e:
|
||||
@@ -5252,7 +5257,7 @@ class GatewayRunner:
|
||||
# Re-inject keystore secrets too so rotated values take effect
|
||||
# without requiring a gateway restart.
|
||||
try:
|
||||
_inject_keystore_env()
|
||||
_inject_keystore_env(force=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+16
-8
@@ -153,27 +153,35 @@ class KeystoreClient:
|
||||
|
||||
raise PassphraseMismatch("Too many incorrect passphrase attempts")
|
||||
|
||||
def inject_env(self) -> Dict[str, bool]:
|
||||
def inject_env(self, force: bool = False) -> Dict[str, bool]:
|
||||
"""Inject all ``injectable`` secrets into ``os.environ``.
|
||||
|
||||
Returns a dict of {secret_name: True} for each injected secret.
|
||||
Existing env vars are NOT overwritten (shell exports take priority).
|
||||
Args:
|
||||
force: If True, overwrite existing env vars with keystore values.
|
||||
Use this only for explicit refresh flows in long-lived processes
|
||||
(e.g. gateway credential reload). Startup paths should usually
|
||||
keep the default ``False`` so shell exports/Docker env vars win.
|
||||
|
||||
Returns:
|
||||
Dict of ``{secret_name: injected_or_overwritten}``.
|
||||
"""
|
||||
secrets = self._store.get_injectable_secrets()
|
||||
injected = {}
|
||||
for name, value in secrets.items():
|
||||
if name not in os.environ:
|
||||
if force or name not in os.environ:
|
||||
os.environ[name] = value
|
||||
injected[name] = True
|
||||
else:
|
||||
# Already set in environment (shell export or Docker env)
|
||||
injected[name] = False
|
||||
self._injected = injected
|
||||
count_new = sum(1 for v in injected.values() if v)
|
||||
count_existing = sum(1 for v in injected.values() if not v)
|
||||
count_written = sum(1 for v in injected.values() if v)
|
||||
count_skipped = sum(1 for v in injected.values() if not v)
|
||||
logger.info(
|
||||
"Keystore: injected %d secrets (%d already in env)",
|
||||
count_new, count_existing,
|
||||
"Keystore: %s %d secrets (%d skipped)",
|
||||
"refreshed" if force else "injected",
|
||||
count_written,
|
||||
count_skipped,
|
||||
)
|
||||
return injected
|
||||
|
||||
|
||||
@@ -72,12 +72,8 @@ def test_gateway_refresh_reinjects_keystore_secret(monkeypatch, tmp_path):
|
||||
gateway_run = _reload_gateway_run(monkeypatch, home)
|
||||
assert os.environ.get("OPENAI_API_KEY") == "sk-old"
|
||||
|
||||
# Rotate secret in keystore; .env stays empty.
|
||||
# Rotate secret in keystore; refresh must overwrite the stale in-process env var.
|
||||
ks.set_secret("OPENAI_API_KEY", "sk-new")
|
||||
os.environ["OPENAI_API_KEY"] = "stale"
|
||||
gateway_run._inject_keystore_env()
|
||||
assert os.environ.get("OPENAI_API_KEY") == "stale" or os.environ.get("OPENAI_API_KEY") == "sk-new"
|
||||
# More important guarantee: if env is cleared, reinjection restores the keystore value.
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
gateway_run._inject_keystore_env()
|
||||
gateway_run._inject_keystore_env(force=True)
|
||||
assert os.environ.get("OPENAI_API_KEY") == "sk-new"
|
||||
|
||||
Reference in New Issue
Block a user