Implement exec-file delivery
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Write the selected field to a mode-0600 temp file, inject FIELD_FILE for
the child only, then overwrite and unlink on every exit path. The value
is not copied into the child environment.

Assistant: grok
Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
This commit is contained in:
tegwick 2026-09-02 08:59:56 +02:00
parent afd1c8e593
commit ce1790f267
7 changed files with 126 additions and 19 deletions

View file

@ -10,7 +10,7 @@ Command surface (FR7):
verify <catalog-id> [--positive] [--negative] [--field NAME] [--negative-token-file F]
handoff <catalog-id> --stage <stage> --role-id-file F --secret-id-file F
wrap <catalog-id> --out F [--ttl 15m]
exec --catalog <catalog-id> [--field NAME] [--mode auto|npm-config|exec-env] -- CMD...
exec --catalog <catalog-id> [--field NAME] [--mode auto|npm-config|exec-env|exec-file] -- CMD...
route <catalog-id> [--json]
revoke <catalog-id>
session revoke --accessor-file F [--stage stage]
@ -840,7 +840,7 @@ def build_parser() -> argparse.ArgumentParser:
ex = sub.add_parser("exec", help="run a command with the secret injected for the child only")
ex.add_argument("--catalog", required=True)
ex.add_argument("--field", default=None)
ex.add_argument("--mode", default="auto", choices=("auto", "npm-config", "exec-env"))
ex.add_argument("--mode", default="auto", choices=("auto", "npm-config", "exec-env", "exec-file"))
add_token_arg(ex)
ex.add_argument("command", nargs=argparse.REMAINDER,
help="command after '--'")

View file

@ -8,6 +8,7 @@ Supported here:
- npm-config: write a temporary .npmrc with the auth token and point the child
at it via NPM_CONFIG_USERCONFIG. Preferred for `npm publish`.
- exec-env: inject the value as an environment variable for the child only.
- exec-file: write the value to a mode-0600 temp file and inject its path.
The parent shell never sees the value; the value is never logged. Child stdout/
stderr is streamed through a redactor as a backstop.
@ -119,10 +120,47 @@ def _npm_userconfig(registry: str, scope: str, token_env: str) -> Iterator[Path]
fh.write(f"{authkey}:_authToken=${{{token_env}}}\n")
yield path
finally:
_unlink_secret_file(path)
def _unlink_secret_file(path: Path) -> None:
"""Overwrite then unlink a secret file. Best-effort; never raises."""
try:
if path.is_file():
size = path.stat().st_size
with path.open("r+b") as fh:
fh.write(b"\0" * max(size, 1))
fh.flush()
os.fsync(fh.fileno())
path.unlink()
except FileNotFoundError:
return
except OSError:
try:
path.unlink()
except FileNotFoundError:
pass
except OSError:
return
@contextmanager
def _secret_file(value: str) -> Iterator[Path]:
"""Write the value to a mode-0600 temp file; overwrite and unlink on exit."""
fd, name = tempfile.mkstemp(prefix="se-exec-", suffix=".tmp")
path = Path(name)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fd = -1
fh.write(value)
fh.write("\n")
yield path
finally:
if fd >= 0:
try:
os.close(fd)
except OSError:
pass
_unlink_secret_file(path)
def _stream_redacted(proc: subprocess.Popen, secret: str) -> None:
@ -156,9 +194,14 @@ def exec_with_secret(
declared = set(entry.delivery_modes)
if mode == "auto":
mode = "npm-config" if "npm-config" in declared else (
"exec-env" if "exec-env" in declared else ""
)
if "npm-config" in declared:
mode = "npm-config"
elif "exec-env" in declared:
mode = "exec-env"
elif "exec-file" in declared:
mode = "exec-file"
else:
mode = ""
if not mode:
raise DeliveryError(
f"lane '{entry.id}' declares no exec-capable delivery mode "
@ -200,6 +243,12 @@ def exec_with_secret(
child_env[env_name] = value
return _spawn(command, child_env, value)
if mode == "exec-file":
env_name = f"{field.upper()}_FILE"
with _secret_file(value) as secret_path:
child_env[env_name] = str(secret_path)
return _spawn(command, child_env, value)
raise DeliveryError(f"unsupported delivery mode '{mode}'")