fnox 1.43.0 makes fnox proxy run harder to misuse: it now has an OS sandbox, a per-session password and blocking of private network destinations. It also adds output redaction and secret filtering to fnox exec, four new lease backends, a local audit log, value-based secret scanning, generated secrets and KeePass field references. Some proxy setups need a config change; see Breaking Changes.
Highlights
- Credential proxy: the proxy can now confine the command it runs, refuses unauthenticated clients and private network destinations, and can inject secrets into query parameters on wildcard domains.
- Keeping secrets out of logs and repos:
fnox execcan redact secret values from piped output,fnox scancan search for your actual secret values (including in a pre-commit hook), and an opt-in audit log records which secrets were read, without recording their values. - Leases: new OAuth2, GitLab, Tailscale and Kubernetes backends, plus easier revocation and JSON listing.
Added
Proxy
fnox proxy run --sandbox(#1039) by @jdx. Runs the command in an OS sandbox where the proxy is the only network access. Inside the sandbox, the command can't read fnox config, state and data dirs,fnox.toml/profile files and their imports, age key files, or common credential dirs (~/.ssh,~/.aws,~/.kube,~/.gnupg, ...). On Linux it also can't reach local credential sockets. It can write only to the working directory and temp files, and it can't create or change.git/hooks,.git/configor fnox config files. The rest of the filesystem stays readable: fnox hides only the paths on its list.On Linux this requires[proxy] sandbox = true # project default; --no-sandbox overrides it for one run sandbox_allow_write = ["~/.cache"] sandbox_deny_read = ["~/.config/x"] sandbox_allow_read = ["fnox.toml"]
bwrap; on macOS fnox usessandbox-exec. Windows isn't supported. If the sandbox can't start, fnox stops before resolving any secrets and never runs the command unsandboxed. The macOS path hasn't been tested on real hardware yet. On macOS the sandbox can't hide other processes or the Keychain service.- Query-parameter injection and wildcard domains (#1026) by @jdx. A proxy rule can set
query = "<name>"instead ofheaderto substitute a secret into a URL query parameter. The value is percent-encoded, and fnox refuses the request (403) if the placeholder appears anywhere else or more than once.domainaccepts a leading*.that matches exactly one subdomain level. The apex domain and deeper subdomains don't match, and patterns like*.comare rejected.headeris now optional and still defaults toauthorization.[[proxy.rules]] secret = "MAPS_KEY" domain = "*.maps.example.com" query = "key" methods = ["GET"]
allow_domainsandallow_private(#1038) by @jdx.allow_domainslets the proxy tunnel the hosts you list without inspection or credentials while egress stays strict.allow_privatelists the internal networks the proxy may reach (see Breaking Changes). The proxy audit output now also logsrefused requestlines with the destination and the reason.
Exec
- Redact secrets from piped output (#1028) by @jdx. With
fnox exec --redact, secret values in the command's stdout and stderr are replaced with[REDACTED], but only for streams that are piped or redirected; terminal output is left alone. Redaction is off by default. To turn it on everywhere, setredact_exec = trueinfnox.tomlorFNOX_EXEC_REDACT=1.--no-redactturns it off for one run, and the flag takes precedence over the environment variable, which takes precedence over config. Values shorter than 3 characters aren't redacted, and you can't combine--redactwith--replace. Redaction matches exact values only, so it won't catch a secret that has been encoded or split before printing.fnox exec --redact -- sh -c 'echo "token=$API_TOKEN"' | cat # token=[REDACTED]
--onlyand--except(#1030) by @jdx. These flags limitfnox execto the secrets you select. Unselected secrets are never resolved, so their providers aren't called and can't prompt for login or fail the run. Patterns are exact names or case-sensitive globs, and you can repeat the flag or separate patterns with commas. fnox still resolves the secrets a selected secret depends on, but passes them to the command only if they're selected too. Leases follow the same filter. An unknown name, or an--onlyglob that matches nothing, is an error.fnox exec --only DATABASE_URL,'AWS_*' -- ./migrate.sh fnox exec --except LEGACY_API_KEY -- npm start
Leases
-
OAuth2, GitLab, Tailscale and Kubernetes backends (#1037) by @jdx:
oauth2-client-credentials: access tokens from any OAuth2 token endpoint. They're revoked only if you setrevocation_url.gitlab: project or group access tokens on gitlab.com or self-hosted GitLab, deleted on revoke. GitLab expiry is date-only, so a token can stay valid for up to a day after its lease ends unless you revoke it.tailscale: tagged, ephemeral, pre-authorized auth keys. With the defaultreusable = false, every command gets a new key and fnox doesn't cache it.kubernetes: ServiceAccount tokens from the TokenRequest API, without needingkubectl. These can't be revoked and expire on their own. The minimum duration is 10 minutes.
Credential URLs must use https; plain http is accepted only for localhost.
fnox lease revokeandfnox lease cleanupnow load the parent credentials a backend needs to revoke a lease, so revocation also works when the parent token is stored in[secrets].[leases.gl] type = "gitlab" project = "platform/api" scopes = ["read_api"] duration = "1h"
-
Easier lease revocation and JSON listing (#1036) by @jdx.
fnox lease revokeaccepts a full lease ID, a unique ID prefix (including the shortened ID thatlease listprints) or a backend name, which revokes all of that backend's active leases.--allrevokes every active lease in the project. If a prefix matches more than one lease, fnox lists the matches and revokes nothing.fnox lease listgains aLABELcolumn and a--jsonflag that prints full IDs and timestamps but never cached credentials.fnox lease revoke aws-1a2b3c fnox lease revoke aws fnox lease revoke --all fnox lease list --json --active
Other additions
- Local access audit log (#1031) by @jdx. The log is opt-in. Each time a secret is read, fnox appends a JSON line to a
0600file with the time, command, profile, config file, secret name, provider and whether the value came from the daemon cache. Values and command lines are never recorded. The log coversexec,get,export,env --json,list --values,check, shell integration,mcp,proxy runand the TUI. It doesn't coverlease,sync,editorreencrypt. Usefnox auditto view entries, with--secret,--command,--provider,-n/--limit,--jsonand--path.FNOX_AUDIT=on|offandFNOX_AUDIT_PATHoverride the config. Any process running as your user can edit the log, so it isn't tamper-proof. Set[audit]in your global config so a projectfnox.tomlcan't choose the log path.# ~/.config/fnox/config.toml [audit] enabled = true
- Find real secret values with
fnox scan(#1029) by @jdx.fnox scan --valuesresolves the active profile and searches for the actual secret values instead of text that merely looks like a secret, so every match is a real leak. Output names the secret but never prints its value.--stagedscans what's staged in the git index.--install-hookadds a pre-commit hook that runsfnox scan --values --staged. It won't overwrite an existing hook unless you pass--append. fnox skips secrets shorter than 8 characters, binary files, files over 5 MiB and gitignored files (unless you pass--include-ignored). Iffnox.tomlitself contains plain values, it will match; use--ignore fnox.tomlif that's intended. - Generate secret values with
fnox set --generate(#1032) by @jdx. fnox creates the value with the OS random number generator and stores it through the selected provider.--generate-typecan bepassword(the default),hex,base64(URL-safe) oruuid.--lengthdefaults to 32 and accepts 1 to 4096. fnox doesn't print the value unless you pass--show, and it won't replace an existing secret without--force.fnox set API_TOKEN --generate fnox set SESSION_KEY --generate --generate-type hex --length 64
- KeePass field references and custom fields (#1022, #1025) by @jdx. The
keepassprovider now resolves{REF:...}references such as{REF:P@I:<uuid>}instead of returning the raw text. References can be embedded in longer strings and nested up to 10 levels deep. A path such aswork/github/ApiTokencan now read a custom field (ApiToken) of an entry (work/github). If an entry is titled with the full path, that entry takes precedence. Custom field names are case-sensitive.fnox setdoesn't write custom fields.
Changed
- FIDO2 prompts match how the key verifies you (#1027) by @jdx. Instead of always asking for an optional PIN, fnox checks whether the key uses a fingerprint, a PIN or touch only, and prompts to match. If a fingerprint read fails in a terminal, fnox offers the PIN as a fallback.
fnox provider addasks for a PIN only if the key has one. If a PIN-protected key is used without a terminal, the error now suggestspin = { secret = "NAME" }.
Fixed
- Multiple daemons when
TMPDIRchanges (#1042) by @jdx. WhenXDG_RUNTIME_DIRwas unset, fnox put the daemon socket under$TMPDIR. Tools that set their ownTMPDIR(nix-shell, build systems, CI runners, agents) each started a separate daemon. The fallback no longer usesTMPDIR: sockets go to/tmp/fnox-<uid>/fnox, or to the per-user temp directory on macOS. SetFNOX_DAEMON_SOCKET_DIRto an absolute path to choose the directory yourself; it takes precedence overXDG_RUNTIME_DIR. Fixes #1041. fnox editsaving secrets in plaintext (#1034) by @jdx. If a secret's provider was defined in an imported config,fnox editwrote the edited value back as plaintext but still reported that it had re-encrypted secrets. Edits are now re-encrypted with providers from imported configs. If a required provider is missing,fnox editnow fails instead of saving plaintext. Fixes #1033.
Breaking Changes
- Proxy rules for private networks need
allow_private(#1038).fnox proxy runnow refuses loopback, private, link-local (including the cloud metadata endpoint169.254.169.254), CGNAT and other reserved addresses. This applies to rules,allow_domainsandegress = "permissive". fnox checks every address a hostname resolves to. Requests to a host that resolves to one of these addresses, such as an internal API on10.x, now get403 destination resolves to a non-public address. To allow them, list the networks:[proxy] allow_private = ["10.20.0.0/16", "192.168.1.5"]
- Proxy clients must send the session password (#1038). Each
proxy runcreates a random password and includes it as the user credentials (userfnox) in theHTTPS_PROXYURL it gives the command. curl, wget, Node, Python, Go and git all send it automatically. A client that removes credentials from proxy URLs gets407 Proxy Authentication Required. The password keeps out other users on a shared host, but not other processes running as you. - Daemon socket location (#1042). If
XDG_RUNTIME_DIRisn't set, fnox won't reuse daemons started at the oldTMPDIR-based path; they exit after theiridle_timeout. Separately, the daemon protocol version is now 7 (#1031), so a daemon started by an older fnox is replaced on the next read that uses the daemon. Neither change needs action. If you use thefnox_clientcrate and buildRuntimeEnvby hand, addsocket_dir: None.
Full Changelog: v1.42.1...v1.43.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.