__
___ ___ __ ______/ /
/ _ \/ _ \/ // / __/ _ \
/ .__/\___/\_,_/\__/_//_/
/_/
Pouch is a process runner for Linux and Unix-like systems that delivers secrets to your app over a dedicated file descriptor—gloriously over-engineered to implement every form of secure hardening imaginable, regardless of utility.
It’s for people who wouldn’t trust corporate cloud vaults like AWS Secrets Manager or Google Secret Manager, and who roll their eyes at hardware like TPMs and HSMs. For a detailed walkthrough of how Pouch delivers secrets and the reasoning behind each safeguard see Execution Flow.
Install via Cargo:
cargo install pouch-run --lockedThe binary will be installed at $CARGO_HOME/bin/pouch.
--from <SOURCE>selects the secret source; see Sources.--fd <N>chooses the secret FD number (≥3) to avoid stdio and reserved descriptors; otherwise a safe descriptor is auto-selected.--max-bytes <N>bounds the accepted size (default262144) to limit memory usage and denial-of-service risk.--consume-timeout <seconds>waits for a validated ACK after delivery (default3.0) to prove receipt;0disables the wait and requires--i-accept-no-ack.--i-accept-no-ackexplicitly opts out of ACK enforcement and prints a warning, reducing guarantees to fire-and-forget.--keep-fd <N>keeps additional FDs (≥3) open in the child besides{0,1,2,secret,ack}; use sparingly since extra FDs widen the attack surface.--no-containmentdisables Linux cgroup v2 containment used for teardown isolation, trading compatibility for weaker cleanup guarantees.-v/--verboseemits structural diagnostics to stderr only and never logs secret content, avoiding accidental leaks.--keep-envpreserves the child's environment instead of a clean allowlist; Pouch still scrubsLD_*/DYLD_*and requires an absolute command path to reduce hijacking risk.
The child command and its arguments follow -- and are passed verbatim. The command must be an absolute path (no PATH lookup) to avoid PATH hijacking and ensure deterministic execution.
- (stdin) reads from stdin (must not be a TTY) with size capped by --max-bytes; use prompt for interactive input to avoid echoing secrets.
file:/ABSOLUTE/PATH reads from an absolute path only, rejects .., allows only regular files, and rejects symlinks with O_NOFOLLOW plus fstat verification; group/world‑writable files are refused and size is checked before reading to enforce --max-bytes, all to prevent path/symlink attacks and tampering.
fd:N reads from an already‑open FD N, rejects 0, 1, and 2, duplicates the FD with CLOEXEC, and reads up to --max-bytes, avoiding stdio confusion, handle leaks, and unbounded reads.
prompt reads interactively from /dev/tty and requires a controlling TTY so a human can enter the secret securely.
cred:NAME or cred:UNIT/NAME is shorthand for systemd credentials: it reads from ${CREDENTIALS_DIRECTORY}/NAME or /run/credentials/UNIT.cred/NAME as applicable, and applies the same file: safety checks to preserve file‑source hardening.
0indicates success (validated ACK observed and child exit0; or fire‑and‑forget mirrors child0).1indicates a usage or validation error (invalid source, TTY on stdin, invalid path, or--consume-timeout 0without the guard flag).2indicates delivery failure when validated ACK fails (EOF before 48 bytes, timeout, oversized/truncated payload, nonce/digest mismatch, or premature child exit). It also occurs on EPIPE while writing the secret; with ACK enforcement, pouch tears down the subtree (process group or cgroup) before exiting.3indicates an I/O or system error (open/dup/pipe failures, etc.).- If the child exits non‑zero, Pouch propagates that exact code after ACK; if the child is signaled, Pouch exits with
128 + signal.
When enforcement is enabled, the parent generates a 16‑byte random nonce and exposes it to the child as a 32‑character lowercase hex string via POUCH_ACK_NONCE to bind the ACK to this delivery and prevent replay.
The child must then write exactly 48 bytes to POUCH_ACK_FD: the raw 16‑byte nonce followed by a 32‑byte BLAKE2s digest keyed with that nonce over the exact secret bytes, which proves receipt of the exact data and prevents tampering.
The parent verifies both values in constant time and rejects any deviation (size errors, extra bytes, mismatch, or premature EOF) to avoid timing and truncation/extension attacks.
+-----------------------+ write secret +-------------+
| Parent (pouch runner) | ------------------------> | Secret Pipe |
+-----------------------+ +-------------+
| ^ |
| | 48-byte ACK (nonce + digest) v POUCH_SECRET_FD
| +----------------------+ +---------------+
| | | Child |
| read from | | (exec target) |
+------------------------- ACK Pipe <---------+---------------+
(POUCH_ACK_FD)
A validated ACK proves only that the child has read the complete secret. It does not guarantee how the secret is used afterward. The child should close the ACK descriptor immediately after sending the 48‑byte payload. Extra data beyond this is ignored.
import os, hashlib, binascii
fd = int(os.environ["POUCH_SECRET_FD"]) # secret FD number
buf = []
while True:
chunk = os.read(fd, 65536)
if not chunk:
break
buf.append(chunk)
os.close(fd)
data = b"".join(buf)
nonce = binascii.unhexlify(os.environ["POUCH_ACK_NONCE"]) # 16-byte raw nonce
digest = hashlib.blake2s(data, digest_size=32, key=nonce).digest()
os.write(int(os.environ["POUCH_ACK_FD"]), nonce + digest) # 48-byte ACK
os.close(int(os.environ["POUCH_ACK_FD"]))Logging in to a container registry in GitHub Actions without exposing the token in environment variables or logs. This example uses the drop‑in Python helper from examples/example.py to emit the secret on stdout and send a validated ACK over POUCH_ACK_FD, then pipes the secret into docker login:
name: ci
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install pouch (Cargo)
run: cargo install pouch-run --locked
- name: Registry login via FD (no env)
env:
REGISTRY: ghcr.io
USERNAME: ${{ github.actor }}
run: |
printf '%s' '${{ secrets.REGISTRY_TOKEN }}' \
| pouch --from - -- /usr/bin/python3 examples/example.py \
| docker login -u "$USERNAME" --password-stdin "$REGISTRY"Notes:
- The token is never placed in the child’s environment because Pouch starts from a clean env, preventing leaks via
ps, crash dumps, or inherited subprocesses. - The secret never appears on stdout and diagnostics go to stderr only, keeping pipelines safe to consume without credential exposure.
- Replace
docker loginwith any tool that accepts credentials on stdin to avoid writing secrets to files or environment variables. - If the consumer cannot ACK,
--consume-timeout 0 --i-accept-no-ackdisables enforcement, trading delivery assurance for compatibility.
For other languages, see the examples directory.
-
The parent ignores SIGPIPE so writes to a closed pipe return EPIPE instead of killing the runner, preventing a child from terminating the parent by closing the pipe.
-
Core dumps are disabled and the secret buffer is page‑locked and marked no‑core‑dump, keeping plaintext out of crash dumps and swap to reduce accidental leakage while the buffer is live.
-
The delivery pipe is created with FD_CLOEXEC on both ends so unintended exec’d processes cannot inherit the handles, preventing secret leaks to unrelated children.
-
A safe target file descriptor ≥3 is chosen (or honored from
--fd) while avoiding stdio, systemd sockets and runtime IPC FDs, preventing accidental collisions or hijacking. -
The pipe’s read end is duplicated onto the chosen descriptor with CLOEXEC so the secret exists only on the intended FD, confining access to a predictable, non‑leaking handle.
-
All redundant read ends in the parent are closed so EOF is unambiguous and cannot be masked, ensuring the child can reliably detect completion.
-
Memory is pre‑allocated up to
--max-bytesto bound usage and prevent unbounded allocation or fragmentation attacks. -
The secret is read only after validating the source (rejecting TTY stdin, symlinks, and relative paths) to block TOCTTOU tricks and path hijacks.
-
On any read error the buffer is immediately wiped so partial plaintext cannot be recovered later.
-
Diagnostics in verbose mode include only structural metadata on stderr, never secret bytes, to prevent exfiltration via logs.
-
The child starts from a cleared environment to neutralize dangerous variables (e.g.,
LD_*,DYLD_*, andPATH) that could hijack execution. -
Only an allowlisted set of env vars is restored and
PATHis sanitized to a root‑owned, non‑writable set of directories, reducing the risk of spoofed binaries. -
The child receives only
POUCH_SECRET_FD,POUCH_ACK_FD, andPOUCH_ACK_NONCE(as 32‑char hex) so it has the capability to read and acknowledge without seeing the secret in env. -
Just before exec, all descriptors except
{0,1,2,POUCH_SECRET_FD,POUCH_ACK_FD}are closed (preferringclose_rangeor/proc/self/fdon Linux andclosefromor a bounded loop on BSD/macOS) to prevent unintended handle inheritance. -
The process is spawned only after environment and descriptor cleanup so no race or leakage occurs during the exec transition.
-
All duplicate parent handles to the pipe are closed so EOF cannot be masked, ensuring readers see end‑of‑data promptly.
-
The secret is written and flushed fully so delivery either completes or fails clearly, avoiding partial reads.
-
Any EPIPE on write is treated as failure and triggers zeroization, making early child closure a safe, detectable error (rc=2).
-
The in‑memory buffer is wiped immediately after sending to minimize the lifetime of plaintext.
-
The write end is closed to signal EOF to the child, allowing deterministic completion and ACK computation.
-
When enforced, the child must write exactly 48 bytes (raw 16‑byte nonce + 32‑byte BLAKE2s digest keyed with the nonce over the secret), and the parent verifies both in constant time; this proves the exact bytes were received and defends against truncation, tampering, and timing attacks. Timeouts, premature EOF, size mismatches, or digest failures trigger logging, subtree teardown, and rc=2. With
--consume-timeout 0 --i-accept-no-ack, the wait and teardown are skipped, but EPIPE during send still yields rc=2.
-
Pouch exits with the child’s status (or
128+signalif signaled) so the runner preserves application semantics after enforcing delivery policy. -
Core‑dump rlimits are restored after secret handling to avoid surprising global settings outside the secret’s lifetime.
-
When ACK is enforced, a missing or invalid ACK yields rc=2 and best‑effort teardown of the launched subtree (process group or cgroup v2) so failed deliveries don’t leave stray processes; with
--consume-timeout 0 --i-accept-no-ack, enforcement and teardown are skipped by design. -
Secrets are never passed via argv, environment, stdout, or stderr to avoid discovery by
ps, logs, or inheritance across subprocesses. -
File sources require absolute paths, reject
..and symlinks, and must be regular files to prevent path tricks and time‑of‑check/time‑of‑use races. -
All diagnostics are restricted to stderr so stdout can be safely piped to downstream tools without secret contamination.
-
Input size is bounded by
--max-bytesto reject oversized data early and limit memory usage and denial‑of‑service risk.
In September 2021, Travis CI suffered a vulnerability that exposed sensitive information from public repositories. During a one-week window (2021-09-03 through 2021-09-10), secure environment variables including API tokens, signing keys, and other credentials were injected into builds triggered by pull requests from untrusted forks. Attackers could submit a pull request and gain access to secrets never intended to leave trusted builds.
The root cause was Travis CI's handling of pull request builds and secret injection. Normally, CI systems prevent secrets from being available to code from outside contributors. Here, the protection failed, and "secure environment variables" were passed along despite coming from forks.
This type of exposure is exactly what Pouch is designed to avoid. Because Pouch never delivers secrets via environment variables or temporary files, the Travis CI class of failure cannot occur.