github jdx/fnox v1.43.0
v1.43.0: Proxy sandbox and egress hardening, exec output redaction, and new lease backends

3 hours ago

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 exec can redact secret values from piped output, fnox scan can 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/config or fnox config files. The rest of the filesystem stays readable: fnox hides only the paths on its list.
    [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"]
    On Linux this requires bwrap; on macOS fnox uses sandbox-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 of header to 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. domain accepts a leading *. that matches exactly one subdomain level. The apex domain and deeper subdomains don't match, and patterns like *.com are rejected. header is now optional and still defaults to authorization.
    [[proxy.rules]]
    secret = "MAPS_KEY"
    domain = "*.maps.example.com"
    query = "key"
    methods = ["GET"]
  • allow_domains and allow_private (#1038) by @jdx. allow_domains lets the proxy tunnel the hosts you list without inspection or credentials while egress stays strict. allow_private lists the internal networks the proxy may reach (see Breaking Changes). The proxy audit output now also logs refused request lines 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, set redact_exec = true in fnox.toml or FNOX_EXEC_REDACT=1. --no-redact turns 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 --redact with --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]
  • --only and --except (#1030) by @jdx. These flags limit fnox exec to 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 --only glob 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 set revocation_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 default reusable = false, every command gets a new key and fnox doesn't cache it.
    • kubernetes: ServiceAccount tokens from the TokenRequest API, without needing kubectl. 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 revoke and fnox lease cleanup now 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 revoke accepts a full lease ID, a unique ID prefix (including the shortened ID that lease list prints) or a backend name, which revokes all of that backend's active leases. --all revokes every active lease in the project. If a prefix matches more than one lease, fnox lists the matches and revokes nothing. fnox lease list gains a LABEL column and a --json flag 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 0600 file 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 covers exec, get, export, env --json, list --values, check, shell integration, mcp, proxy run and the TUI. It doesn't cover lease, sync, edit or reencrypt. Use fnox audit to view entries, with --secret, --command, --provider, -n/--limit, --json and --path. FNOX_AUDIT=on|off and FNOX_AUDIT_PATH override 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 project fnox.toml can't choose the log path.
    # ~/.config/fnox/config.toml
    [audit]
    enabled = true
  • Find real secret values with fnox scan (#1029) by @jdx. fnox scan --values resolves 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. --staged scans what's staged in the git index. --install-hook adds a pre-commit hook that runs fnox 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). If fnox.toml itself contains plain values, it will match; use --ignore fnox.toml if 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-type can be password (the default), hex, base64 (URL-safe) or uuid. --length defaults 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 keepass provider 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 as work/github/ApiToken can 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 set doesn'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 add asks for a PIN only if the key has one. If a PIN-protected key is used without a terminal, the error now suggests pin = { secret = "NAME" }.

Fixed

  • Multiple daemons when TMPDIR changes (#1042) by @jdx. When XDG_RUNTIME_DIR was unset, fnox put the daemon socket under $TMPDIR. Tools that set their own TMPDIR (nix-shell, build systems, CI runners, agents) each started a separate daemon. The fallback no longer uses TMPDIR: sockets go to /tmp/fnox-<uid>/fnox, or to the per-user temp directory on macOS. Set FNOX_DAEMON_SOCKET_DIR to an absolute path to choose the directory yourself; it takes precedence over XDG_RUNTIME_DIR. Fixes #1041.
  • fnox edit saving secrets in plaintext (#1034) by @jdx. If a secret's provider was defined in an imported config, fnox edit wrote 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 edit now fails instead of saving plaintext. Fixes #1033.

Breaking Changes

  • Proxy rules for private networks need allow_private (#1038). fnox proxy run now refuses loopback, private, link-local (including the cloud metadata endpoint 169.254.169.254), CGNAT and other reserved addresses. This applies to rules, allow_domains and egress = "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 on 10.x, now get 403 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 run creates a random password and includes it as the user credentials (user fnox) in the HTTPS_PROXY URL it gives the command. curl, wget, Node, Python, Go and git all send it automatically. A client that removes credentials from proxy URLs gets 407 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_DIR isn't set, fnox won't reuse daemons started at the old TMPDIR-based path; they exit after their idle_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 the fnox_client crate and build RuntimeEnv by hand, add socket_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.

Don't miss a new fnox release

NewReleases is sending notifications on new releases.