Claude
Skills
Sign in
Back

secret-handling-runtime

Included with Lifetime
$97 forever

Decision aid for runtime secret hygiene — fd passing, scratch surface, error-path safety, identifier hygiene, and avoiding the SECRETS_ENV aggregation anti-pattern.

Security

What this skill does


# secret-handling-runtime

Decision aid for how a system handles secrets **during** operation: in memory, in pipes, in scratch files, in error messages, in logs, in process tables. Use when designing or reviewing any code that touches secret material at runtime.

This skill complements the existing `addons/security/secure-token-load.md` (which covers tokens at rest — file modes, heredoc patterns, source locations). This skill covers **runtime** — what happens to those secrets between load and use, and what happens when something fails mid-operation.

## Triggers

- "secret in env" / "SECRETS_ENV"
- "shred" / "tmpfs" / "scratch surface"
- "set -e" / "error trap" / "ERR EXIT"
- "fd passing" / "named pipe for secrets"
- "log scrubbing" / "secret in error message"
- "ps aux" / "/proc/self/environ"
- "memory wipe" / "zeroize"

---

## Section 1: File-descriptor / named-pipe passing

### Suggested default — one secret per fd

```bash
# Caller
encrypt_program 3<<<"$SECRET_VALUE" 4<<<"$OTHER_SECRET" arg1 arg2

# Inside encrypt_program
secret=$(cat <&3)
other=$(cat <&4)
# fd 3 and fd 4 are open for the duration of the program
# never appear in /proc/self/environ, never in `ps aux`
```

Why this beats env vars:

- `env`, `printenv`, `/proc/<pid>/environ` are readable by other processes on most systems
- Env vars persist across `exec` chains (subprocess inherits)
- `set -x` (bash trace) prints env values for each subprocess invocation
- Crash dumps include env

### Anti-pattern: SECRETS_ENV aggregation (review M3)

```bash
# WRONG
SECRETS_ENV="api_key=$KEY1;db_pwd=$KEY2;jwt_secret=$KEY3"
some_program  # all three secrets visible to any process inspecting environ
```

Single variable = single leak vector. If `set -x` fires, all three leak. If a child process logs env, all three leak. Use one fd per secret OR one named pipe per secret.

### Named pipes (mkfifo) when fd passing isn't structural

```bash
mkfifo /tmp/secrets.fifo
chmod 600 /tmp/secrets.fifo
{ echo "$SECRET" > /tmp/secrets.fifo; } &
program /tmp/secrets.fifo
rm /tmp/secrets.fifo
```

Caveats:

- `/tmp/` should be tmpfs (Section 2)
- Named pipes are visible to other users via `ls -la /tmp/`; use a per-process directory in `/run/<uid>/` or `/dev/shm/` that's mode 700

---

## Section 2: Scratch surface — tmpfs only

### Suggested default — refuse to operate if `/tmp` is not tmpfs

```bash
require_tmpfs() {
    local path="${1:-/tmp}"
    if ! mountpoint -q "$path" || \
       [ "$(findmnt -no FSTYPE "$path")" != "tmpfs" ]; then
        echo "ERROR: $path is not tmpfs; refusing to operate" >&2
        exit 1
    fi
}
require_tmpfs /tmp
```

Why tmpfs only:

- tmpfs is RAM-resident; on shutdown, contents are gone
- Disk-backed `/tmp` (some servers, some embedded systems) leaves traces in flash wear-leveling, journal, and swap
- `shred -u` on flash storage is **worse than nothing** — it gives false assurance against wear-leveling reality (review M1)

### `shred` is not a substitute

`shred` overwrites file content multiple times then deletes. On flash storage (SSD, USB, eMMC), the wear-leveling layer remaps writes to fresh blocks; the original data sits in unmapped blocks until eventual reuse. `shred -u` deletes the visible file but the data remains recoverable via filesystem forensics.

`shred` IS appropriate for spinning disks where the OS guarantees in-place rewrites — uncommon in 2026.

For ephemeral scratch, tmpfs is the answer. For persistent encrypted storage, the encryption (LUKS, FileVault) is the answer; never write secrets to "ordinary" disk and try to delete them later.

### Documenting the scratch surface

```markdown
## Scratch surface

| Path | Backing | Acceptable for secrets? |
|------|---------|-------------------------|
| /tmp | tmpfs (verified at start) | yes |
| /var/tmp | disk | NO — refuse to write secrets |
| /dev/shm | tmpfs | yes |
| ~/.cache | disk | NO |
```

---

## Section 3: Error-path safety in shell

### Suggested default — `set -euo pipefail` + `trap cleanup`

```bash
#!/bin/bash
set -euo pipefail

cleanup() {
    # zeroize what we can; unmount tmpfs scratch
    [ -n "${SCRATCH_DIR:-}" ] && [ -d "$SCRATCH_DIR" ] && \
        shred -u "$SCRATCH_DIR"/*.tmp 2>/dev/null
    [ -n "${SECRET_VAR:-}" ] && unset SECRET_VAR
    [ -n "${MOUNTED:-}" ] && umount "$MOUNTED" 2>/dev/null
}
trap cleanup ERR EXIT INT TERM

# ... actual script logic
```

Without `set -e`, a failing `openssl enc` or `cat` mid-pipeline can produce a partial-state file the next step processes (review M2). Without the `trap`, an interrupted script leaves secret-bearing scratch files intact.

### Specific shell pitfalls

| Issue | Mitigation |
|---|---|
| `set -e` doesn't fire inside `if`/`while`/`&&`/`\|\|` chains | Test exit codes explicitly in those contexts: `if foo; then ...; else echo "foo failed"; exit 1; fi` |
| `set -e` ignores failures in subshells unless caller checks | `(...)` returns the subshell's exit; check it |
| `set -x` (debug) prints expanded variables — including secrets | Never enable `set -x` on secret-bearing code paths; use `set +x` defensively before secret handling |
| `cat /etc/secret \| openssl enc ...` — pipe failures partial-write | `set -o pipefail` to make the pipeline return any failure |

### Python / Node / Go equivalents

| Language | Pattern |
|---|---|
| Python | `try/finally` for cleanup; `subprocess.run(..., check=True)`; `secrets.compare_digest` for any constant-time compare |
| Node | `try/finally` + `process.on('exit', cleanup)`; pass-fd via `child_process.spawn(..., { stdio: [...] })` |
| Go | `defer cleanup()`; `os/exec.Cmd.ExtraFiles` for fd passing |

---

## Section 4: Memory hygiene

### When to bother

In long-running processes (daemons, sessions), wipe key material from memory after use:

```c
// C with libsodium
sodium_memzero(key, sizeof(key));
```

```python
# Python — limited; CPython doesn't guarantee zeroization but bytearrays help
import ctypes
key = bytearray(32)
# ... use key
ctypes.memset(ctypes.addressof(ctypes.c_char.from_buffer(key)), 0, len(key))
```

For short-lived programs (one-shot scripts), explicit zeroization is mostly performative — the process exits, OS reclaims memory. The bigger concern is:

- **Don't put secrets in long-lived globals**; make them function-local
- **Don't pass secrets as positional command-line args** (`ps aux` reveals them)
- **Don't log secrets, even to debug**

### `mlock` to prevent swap

```c
mlock(key_buffer, key_size);
// ... use
sodium_memzero(key_buffer, key_size);
munlock(key_buffer, key_size);
```

`mlock` prevents the page from being swapped to disk. Required when the process may run on a system with swap enabled. libsodium's `sodium_mlock`/`sodium_munlock` is portable.

---

## Section 5: Identifier hygiene

### Anti-pattern: serial numbers, IDs in metadata files (review L2)

```json
{
  "yk5_serial": "12345678",
  "bio_serial": "87654321"
}
```

Hardware serials in metadata expose:

- Operator identity (serials are often issued to specific people)
- Hardware revisions (different firmware = different attack surface)
- Inventory enumeration (attacker who recovers `.meta.json` learns what other devices the operator carries)

### Remediation: salted hash

```python
import hashlib, secrets
salt = secrets.token_bytes(16)  # per-USB, stored alongside
yk5_hash = hashlib.sha256(salt + yk5_serial.encode()).hexdigest()
# .meta.json stores yk5_hash and salt; serial is never persisted
```

The salt is stored *with* the metadata, so loss of the metadata file reveals nothing. The hash lets you verify "is this the right key?" without revealing which key.

### Apply broadly

The pattern applies to:

- Hardware serials (USB, YubiKey, smart card, TPM EK)
- Email addresses, usernames in stored audit
- IP addresses in long-term logs
- Build host fingerprints in artifact manifests

If you need to identify-without-revealing, salted-hash. If you need to compare-without-revealing, HMAC with a per-context key.

---

## Section 6

Related in Security