Skip to content

Releases

v1.48.0: Touch ID-protected macOS Keychain secrets and streaming proxy responses

Choose a tag to compare

@jdx jdx released this 09 Nov 20:46
Immutable release. Only release title and notes can be modified.
dbb3072

The macOS keychain provider can now require Touch ID for each read, and fnox proxy run streams rule-domain responses so SSE and LLM clients get events as they arrive. Secrets can record the URL where a new value is issued. This release also fixes several 1Password problems: secrets lost their trailing newline, and environment:// references failed on CLI 2.42.0.

Added

  • Touch ID protection for macOS Keychain secrets (#1140, #1146) by @jdalton and @jdx. Set touch_id = true on a keychain provider:
    [providers.keychain]
    type = "keychain"
    service = "fnox"
    touch_id = true
    • Protected items are stored under a separate <service>.touch-id Keychain service. Before each read, fnox shows the invoking command, project path and secret name, then asks macOS for Touch ID. Canceling or failing the check blocks access, and there is no password fallback.
    • fnox asks for Touch ID on every protected read. With touch_id = true, fnox provider test creates a temporary protected item and reads it, so it checks real protected access.
    • The protected Keychain operation runs through a signed Fnox.app helper. Official releases publish it as a separate fnox-touch-id-<target>.zip asset. Install it in /Applications or next to the fnox binary. Without the helper, Touch ID operations fail with an Apple missing-entitlement error.
    • Source builds on macOS need Node.js 22.18.0 or later to build the approval UI. They also need a Developer ID provisioning profile, and the CLI and helper must be signed with the same team and identifier. Builds made with an SDK older than macOS 26 use an opaque approval panel instead of the glass style. See the keychain docs for helper packaging.
  • Secret URLs and fnox set --open (#1145) by @jdx. A secret can now include a url field for the page where you create a new value, such as a service's token settings page. The URL must use http or https. fnox set KEY --open prints that URL, opens it in your browser, and then shows the usual hidden prompt for the value:
    fnox set GITHUB_TOKEN --url <URL>   # record the URL only
    fnox set GITHUB_TOKEN --open        # open the page, then enter the new value
    --open needs an interactive terminal and can't be combined with VALUE, --from-file or --generate. fnox list shows a URL column only when at least one secret has a URL, and fnox tui shows the URL in the secret details.
  • Find 1Password items by tag (#1138) by @jdalton. fnox provider discover lists item metadata (ID, title, tags and vault) without reading any secret fields, so you can look up references before adding them to your config. It uses the provider's configured account and vault. If no vault is configured, it searches every vault the account can access. Tags can be comma-separated or passed more than once, and archived items are skipped.
    fnox provider discover op --tags project/example --json
  • Opt-in sudo Touch ID setup script (#1139) by @jdalton. scripts/enable-sudo-touch-id.sh can check, preview or enable Touch ID for sudo on macOS, using the --check, --dry-run and --apply modes. --json gives structured output. The script never elevates itself and only creates /etc/pam.d/sudo_local when that file doesn't exist. It refuses to change existing policies it doesn't recognize.

Changed

  • Streaming proxy responses (#1113) by @jdx. fnox proxy run no longer waits for a rule domain's full upstream response before forwarding it. It sends the headers right away and then sends the body in chunks as it arrives. Secret values are still redacted, including values split across two chunks. Before this change, streaming APIs such as SSE-based LLM endpoints delivered no events until the upstream finished, and agent clients could time out.
    • Responses no longer have a 10 MiB limit. Request bodies are still limited to 10 MiB.
    • If the upstream fails partway through a response, the client receives an incomplete chunked body instead of a 502.

Fixed

  • fnox proxy run no longer exits when a client disconnects mid-response (#1112) by @jdx. Previously, the proxy was killed by SIGPIPE (exit 141). The child command then pointed at a closed port and the proxy's CA file was left in the temp directory. proxy run and daemon serve now drop only the affected connection and keep running. Pipelines such as fnox get FOO | head behave as before.
  • 1Password values keep their trailing newline (#1131) by @jdx. Values read from 1Password now match the stored value exactly. This fixes SSH private keys in [files] that OpenSSH rejected with invalid format. Batched reads also no longer change multi-line values or drop empty ones. As a result, a stored value that really ends in whitespace now keeps it in environment variables too. Sync cache entries written by earlier versions keep the trimmed value until you refresh them.
  • 1Password environment:// references work with CLI 2.42.0+ (#1144) by @jdx. fnox now runs op environment secret list. It falls back to op environment read only when the CLI doesn't know the new command, which is the case for older betas.
  • fnox get and fnox sync resolve provider tokens stored as secrets (#1125) by @jdx. A provider token kept as its own fnox secret, such as BWS_ACCESS_TOKEN, is now resolved for get and sync as it already was for exec and export. sync still writes only the secrets you selected.
  • Shell hook reports the actual config error (#1130) by @jdx. When fnox.toml has a schema error, hook-env now warns with the parse error. Previously it reported a missing .config/fnox.local.toml, or showed no warning at all when that file existed.
  • Vault leases send ttl only when duration is set (#1129) by @jdx. This fixes unknown field: ttl errors from strict engines such as the GitHub secrets plugin on OpenBao. If you don't set duration, engines that honor ttl now use their role's default TTL instead of the 15 minutes fnox used to request. Set duration to keep the old behavior.
  • Daemon shell hooks pick up FNOX_AGE_KEY changes (#1137) by @Aschenbath. Adding, replacing or removing the inline age key in the calling shell now takes effect on the next hook request, without restarting the daemon.
  • fnox sync --local-file warns before removing stale local cache (#1135) by @jdx. When entries are marked for removal, --dry-run and the confirmation prompt now show a hint. Personal secrets synced by v1.36.0 or older lack local_source = true and look the same as cache copies, so you need to add that flag to keep them. The sync guide now documents local_source and how to recover those entries.

New Contributors

Full Changelog: v1.47.0...v1.48.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.

v1.47.0: Named and passphrase-protected age identities, Azure Container Apps push target

Choose a tag to compare

@jdx jdx released this 03 Nov 17:57
Immutable release. Only release title and notes can be modified.
3ec91cc

The age provider can now pick one identity per machine from a shared config, read passphrase-protected identity files, and use a named table of recipients. fnox push adds an Azure Container Apps target that writes all of a target's variables in one request. This release also fixes op signin detection for 1Password and fnox sync caching for secrets that use a profile's default_provider.

Added

  • Named age identities (#1099, #1100) by @jdx. One shared fnox.toml can list an identity for each machine, for example one per YubiKey. Choose one with default_identity, or set FNOX_AGE_IDENTITY to override it on a given machine. An entry can be a key file path or a { provider, value } reference, such as an age identity unlocked through the fido2 provider:
    [providers.age]
    type = "age"
    recipients = ["age1...", "age1...", "age1..."]
    default_identity = "yk1"
    
    [providers.age.identities]
    yk1 = { provider = "fido2-yk1", value = "..." }
    yk2 = { provider = "fido2-yk2", value = "..." }
    recovery = "~/.config/fnox/recovery.txt"
    • The selected identity replaces key_file, or identity for provider-backed entries. FNOX_AGE_KEY still takes precedence over everything.
    • fnox only calls the provider of the selected entry, so a machine never asks for another machine's key.
    • An unknown name is an error that lists the available names. A table that mixes secret with provider/value is rejected.
    • Configs that don't set identities, default_identity or FNOX_AGE_IDENTITY work as before.
  • Passphrase-encrypted age identity files (#1097) by @jdx. key_file can now point at an identity created with age-keygen | age -p -o identity.age, in binary or armored form. When fnox decrypts a secret, it prompts for the passphrase once per file in each run. fnox set only uses the public recipients, so it never prompts. Without a terminal, fnox fails with an auth error that suggests decrypting the identity and setting FNOX_AGE_KEY. The daemon does not cache the passphrase, and no environment variable can supply it.
  • Named age recipients (#1104) by @jdx. recipients can now be a table, so you can see whose key each entry is:
    [providers.age.recipients]
    desktop = "age1..."
    laptop = "age1..."
    recovery = "age1..."
    Secrets are encrypted to every key, in the order written. The names are only labels, so renaming one doesn't require fnox reencrypt. The array form still works.
  • azure-container-apps push target (#1098) by @jdx. fnox push can now set environment variables on an Azure Container App through the ARM REST API. It uses the same credentials as azure-sm and azure-ac: service principal environment variables, an az/azd login, or a managed identity. Values never appear on a command line.
    [targets.api]
    type = "azure-container-apps"
    subscription = "00000000-0000-0000-0000-000000000000"
    resource_group = "rg-dev"
    app = "ca-api-dev"
    # container = "api"   # required if the app has several containers
    • Every env change creates a new revision, so fnox writes all of a target's variables in one request. If every value already matches, it writes nothing.
    • Variables that fnox doesn't push, and the app's secrets, are left alone. A pushed variable is stored as a plain value and replaces any existing variable with the same name, including one that used a secretRef.
    • When Azure provides an ETag, the write is conditional, so a concurrent change to the app causes the push to fail instead of being overwritten. fnox polls long-running updates for up to five minutes but doesn't wait for the new revision to become ready. Only the public cloud is supported.
    • fnox push now groups writes by target, so output lines are ordered by target. If a batch fails, every variable in it is reported as failed.

Fixed

  • 1Password: When op reports "You are not currently signed in", fnox now treats it as an authentication failure. In a terminal, it offers to run op signin and then retries. Otherwise, it shows the op signin hint. Item, vault or field names containing words like "authorization" no longer cause unrelated failures to be reported as sign-in errors. (#1102) by @djgoku
  • Sync: fnox sync now caches secrets that have a value but no provider and are read through the profile's default_provider. Before, --local-file skipped them, so they kept calling the remote provider and failed offline. The same rule now applies to --source, the "from" labels, and the cleanup of stale cache entries. (#1106) by @jdx

New Contributors

Full Changelog: v1.46.0...v1.47.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.

v1.46.0: Push secrets to CI and hosting platforms with fnox push

Choose a tag to compare

@jdx jdx released this 01 Nov 10:29
Immutable release. Only release title and notes can be modified.
6373fec

fnox 1.46.0 adds fnox push, which copies secrets into CI and hosting secret stores such as GitHub Actions, Vercel and Fly. Proxy rules can now inject short-lived lease credentials, [files] entries can be cached locally with fnox sync, and allow_private entries can be limited to one port. This release also fixes a macOS sandbox escape through open.

Added

  • fnox push and push targets (experimental) (#1079) by @jdx. CI secrets and hosting environment variables accept a value but never return it, so fnox can't read them as providers. Instead, a secret can now list the targets it should be sent to. fnox push gets each value through the secret's own provider and writes it to every listed target, using the fnox key as the name.
    [targets.gh-prod]
    type = "github-actions"
    repo = "acme/api"
    environment = "production"
    
    [targets.vercel]
    type = "vercel"
    environments = ["production", "preview"]
    sensitive = true
    
    [secrets]
    STRIPE_KEY = { provider = "op", value = "Stripe/secret key", push = ["gh-prod", "vercel"] }
    fnox push --dry-run          # preview the writes
    fnox push STRIPE_KEY -t gh-prod
    fnox -P production push      # use the production profile's targets
    • Supported target types: github-actions (via gh), gitlab-ci (via glab), vercel, cloudflare-workers (via wrangler), fly, forgejo-actions (Forgejo/Gitea/Codeberg REST API, token from FORGEJO_TOKEN/GITEA_TOKEN), and hcp-terraform (token from TF_TOKEN_<host>, TFE_TOKEN, or terraform login).
    • CLI-based targets receive the value on stdin, not as an argument, and use whatever login that CLI already has.
    • fnox push asks for confirmation unless you pass --force. --non-interactive only works together with --force. fnox checks every target name and target config before it resolves or writes anything. If one write fails, the remaining writes still run, and the command exits non-zero with a list of the failures.
    • A profile can define its own targets or override a top-level target that has the same name.
    • Pushing keeps a value out of terminals, clipboards and chat transcripts. It doesn't hide the value from anyone who can change code that runs with the secret.
  • Lease credentials in proxy rules (#1078) by @jdx. A [[proxy.rules]] entry can now set lease instead of secret. The proxy then mints a short-lived credential from that lease (for example gitlab, github-app, vault or cloudflare) and injects it only into matching requests. The command sees a placeholder and never sees the minted credential or the parent credential it was minted from.
    [leases.forge]
    type = "gitlab"
    # url, project, scopes, duration ...
    
    [[proxy.rules]]
    lease = "forge"
    domain = "forge.example.com"
    header = "private-token"
    methods = ["POST"]
    • Each rule sets exactly one of secret and lease. env defaults to the variable the lease produces. Leases that produce several variables, and command leases, need credential = "NAME" to pick one. aws-sts leases are rejected.
    • The proxy mints a lease the first time a matching request needs it, and concurrent requests share that lease. A new lease is minted shortly before the old one expires. Old values stay redacted from responses.
    • When the command exits, fnox revokes every lease it minted, if the backend supports revocation. Minted leases are recorded in the lease ledger, so fnox lease list and fnox lease revoke can clean them up if fnox was killed.
    • If the parent credential is missing at startup, fnox proxy run won't start. If minting fails, that request is refused, and a later request tries again.
  • Sync cache for [files] (#1081) by @jdx. fnox sync now caches [files] entries as well as secrets. A file stored in a slow remote provider such as 1Password can then be read from a local encrypted copy by fnox get --file, fnox files write and fnox edit --file. The sync target must be an encryption provider defined in the global config, and the cache is always written to the global config. fnox set --file, fnox edit --file and fnox reencrypt drop the old cache. Run fnox sync again whenever the file changes at its source.
    fnox sync -p sync-age                 # or: fnox sync ~/.kube/config
  • Port-specific allow_private entries (#1088) by @jdx. Under fnox proxy run --sandbox, you can now give the command access to one local service without opening every port on that address. Entries take the forms ip:port, [ipv6]:port, or a CIDR block followed by a port. A bare IP or CIDR still covers every port. Port 0, hostnames and malformed entries are rejected when the config loads.
    [proxy]
    allow_private = ["127.0.0.1:4790", "[::1]:4790"]

Changed

  • Signal handling in fnox proxy run (#1078) by @jdx. Outside the sandbox, fnox proxy run no longer exits as soon as it gets a signal. It passes SIGTERM and SIGHUP on to the command. It also passes on SIGINT, unless fnox is in the terminal's foreground process group, because Ctrl-C has already reached the command in that case. fnox then waits for the command to exit, so it can shut down the proxy and revoke leases.

Fixed

  • macOS sandbox escape through open (#1090) by @jdx. A command run under fnox proxy run --sandbox on macOS could use open to launch an app through Launch Services. That app ran outside the sandbox, where it could read hidden paths and write outside the working directory (GHSA-38m4-h5j8-8fq6). The sandbox now blocks these launches. Linux is not affected.

Full Changelog: v1.45.0...v1.46.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.

v1.45.0: Agents can ask you to enter a missing secret, and fnox proxy run serves MCP tools with placeholders

Choose a tag to compare

@jdx jdx released this 29 Oct 19:47
Immutable release. Only release title and notes can be modified.
48f328b

With fnox 1.45.0, an AI agent can ask you for a missing secret through a one-time local page, so the value never goes through the chat. fnox proxy run can now start before every secret is set, and it serves fnox's MCP tools using placeholders instead of real values. fnox env --describe also reports which file defines each key, and symlinked global configs now work with [files].

Added

  • request_secret MCP tool (#1065, #1074, #1075) by @jdx. When an agent needs a credential you haven't stored yet, it can ask fnox for it by name. fnox serves a one-time page on 127.0.0.1 and sends the link to the MCP client through URL-mode elicitation. You type the value on that page and fnox stores it the same way fnox set does. The agent is told only that the secret was stored, and exec injects it from then on. The tool is off by default:
    default_provider = "age"
    
    [mcp]
    tools = ["exec", "request_secret"]
    • If mcp.secrets is set, the agent can only ask for names on that list. The plain provider is refused. If the secret already has a value, fnox doesn't ask for it again unless the agent passes replace: true.
    • The link works once and expires after 10 minutes. The page rejects cross-site form posts. After you save, a reload shows the result for 60 seconds and doesn't store the value a second time.
    • If the client doesn't support URL-mode elicitation, no page opens. Instead, the agent is told to ask you to run fnox set NAME in your own terminal. If the client rejects or cancels the prompt, or the request times out, the agent gets a message that is different from the one sent when you click Decline. That message also tells the agent not to reopen the prompt or ask for the secret in chat.
    • The input is a single line, so multi-line values such as PEM keys still need fnox set --from-file. The page can't prove that you are the one filling it in. Another program running as your user could submit a value.
  • MCP tools under fnox proxy run (#1072) by @jdx. When an agent runs fnox mcp inside fnox proxy run (including --sandbox), the MCP server now relays to the proxy, and you don't need to change the agent's MCP config. get_secret returns the secret's placeholder instead of its real value. request_secret opens its page from the proxy process, outside the sandbox. After you enter the value, the proxy starts injecting it into matching requests. exec isn't offered in this mode. [mcp] tools and secrets still apply. Only secrets that have a proxy rule can be used this way. Once the child starts, fnox proxy run no longer prompts for provider authentication, so authenticate before you start it.
  • Pending proxy secrets (#1071) by @jdx. Before, fnox proxy run exited if any rule's secret had no value. Now, if a secret is declared but not set, fnox prints a warning, starts anyway, and still gives the command a placeholder. Requests that need the secret get a 403 with a "not set yet" message. When you set the secret, for example with fnox set STRIPE_KEY in another terminal, the next matching request picks it up without a restart. The proxy checks for the value at most once every 3 seconds per secret. A rule that names an undeclared secret is still an error.
    [secrets]
    STRIPE_KEY = { description = "Stripe test key" }
    
    [[proxy.rules]]
    secret = "STRIPE_KEY"
    domain = "api.stripe.com"
  • Key source in fnox env --json --describe (#1059) by @jdx. Each key now has a source field with path, the file whose definition wins, and global, which is true for the global config and the files it imports. Tools such as mise can use this to tell a project's own keys from your personal global ones. The field is left out when fnox can't trace a key to a file. The describe schema version is still 1. In fnox-client, KeyInfo gains source and a with_source builder.
    "source": { "path": "/home/me/.config/fnox/config.toml", "global": true }

Fixed

  • Symlinked global config and source files (#1063) by @jdx. fnox 1.44.0 refused [files] when ~/.config/fnox/config.toml was a symlink, as dotfiles managers such as stow create. Because fnox set --file had already stored the entry, every later command failed. fnox also rejected source files linked into ~/.config/fnox/files/. Both now work. fnox set and fnox reencrypt write through a source link instead of replacing it. They report an error, before storing anything, if the link is broken or points to the config file or to another entry's file. One case is now stricter: a project fnox.toml that is a symlink into the config directory can no longer declare [files].

Full Changelog: v1.44.0...v1.45.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.

v1.44.0: Store whole files by path and edit a single secret

Choose a tag to compare

@jdx jdx released this 28 Oct 21:37
Immutable release. Only release title and notes can be modified.
252a61a

fnox 1.44.0 adds secret files: whole files such as ~/.kube/config stored in the global config under the path they belong at, and written there with fnox files write. fnox edit can now edit one secret, accepts editor commands that include arguments, and saves the change to the config file that defines the secret.

Added

  • Secret files (#1056) by @jdx and @owpac. Some secrets need to exist as files at a fixed path, such as ~/.kube/config, ~/.aws/credentials or ~/.npmrc. You can now declare them in a [files] table, using the path as the key. Files use the same providers, profiles and fnox reencrypt as secrets, but fnox never turns them into environment variables.
    # ~/.config/fnox/config.toml
    [files]
    "~/.kube/config" = { provider = "age", value = "YWdl..." }
    
    [profiles.work.files]
    "~/.aws/credentials" = { provider = "age", source = "files/aws-credentials.age" }
    fnox set --file ~/.kube/config      # store the file at that path
    fnox get --file ~/.kube/config      # print it, byte for byte
    fnox edit --file ~/.kube/config     # edit it in $EDITOR
    fnox remove --file ~/.kube/config   # remove the entry
    fnox files                          # list entries without decrypting them
    fnox files write                    # write every file of the active profiles
    • [files] is only allowed in the global config. If a project fnox.toml declares it, fnox reports an error, so a cloned repository can't make fnox overwrite paths such as ~/.ssh/authorized_keys.
    • A key must be an absolute path or start with ~/. Content is kept byte for byte, including the final newline, and must be UTF-8 text.
    • You can keep the encrypted content in a separate file with source instead of value. The path is relative to the config directory and can't point outside it. The plain provider can't be used for files.
    • fnox files write resolves every file before it writes any of them. It writes each file with mode 0600 and replaces a symlink at the path instead of writing through it. Files whose content already matches are left unchanged. --dry-run lists what would be written, and path arguments limit the write to those files.
    • Files are written only when you run fnox files write. fnox exec and shell integration don't write them.
  • Edit a single secret (#1045) by @jdx. fnox edit KEY opens only that secret's resolved value in your editor, as raw text in a private temporary file. No other secrets are decrypted. When you save, fnox stores the new value with the secret's provider and keeps env, description and its other settings. If the only change is a final newline added by the editor, fnox ignores it. Secrets from read-only providers can't be edited this way. Running fnox edit with no key works the same as before.
    fnox -P work edit KUBECONFIG

Fixed

  • Editor commands with arguments (#1045) by @jdx. Setting EDITOR="code --wait" or EDITOR="vim -n" used to fail with Failed to launch editor. fnox now splits the value using shell quoting rules. If EDITOR is empty, fnox uses VISUAL, then vi (notepad on Windows). fnox now also reports an error when the editor exits with a nonzero status.
  • fnox edit KEY saving to the wrong file (#1048) by @jdx. Editing a secret defined in the global config or a parent project used to create a new ./fnox.toml in the current directory. The original definition kept the old value. The new value is now written to the file that defines the secret, encrypted with that file's providers. If a fnox sync --local-file cache holds the secret, fnox updates the shared definition and removes the outdated cache entry.
  • fnox set on synced and inherited secrets (#1045) by @jdx. Setting a new value now clears the secret's fnox sync cache, so an outdated cached value no longer takes precedence. Setting a value for a secret inherited from another profile or config layer now keeps its as_file, env, description and other settings.

Full Changelog: v1.43.0...v1.44.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.

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

Choose a tag to compare

@jdx jdx released this 26 Oct 10:38
Immutable release. Only release title and notes can be modified.
2627b55

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 p...
Read more

v1.42.1: Shell hooks survive mise upgrades and work with starship in PowerShell

Choose a tag to compare

@jdx jdx released this 23 Oct 10:31
Immutable release. Only release title and notes can be modified.
4381218

fnox 1.42.1 fixes two shell-integration problems: hooks that broke after a mise upgrade, and PowerShell hooks that didn't work correctly alongside starship.

Fixed

  • Shell hooks breaking after a mise upgrade (#1016) by @jdx. fnox activate used to put the fully resolved binary path, such as .../installs/fnox/1.42.0/fnox, into the shell hook. When mise upgrade deleted that directory, every prompt printed _fnox_hook: no such file or directory, and fnox deactivate failed too. These changes apply:
    • fnox now writes the path you ran it from into the hook, keeping symlinks. To get hooks that keep working after upgrades, activate through a stable path:
      eval "$(~/.local/share/mise/installs/fnox/latest/fnox activate zsh)"
      A plain eval "$(fnox activate zsh)" that finds fnox through a versioned PATH entry still writes that versioned path into the hook.
    • In bash, zsh and fish, fnox deactivate now removes the hooks and the fnox function even if the binary is gone. To recover, run fnox deactivate and then activate again. When you activate again, fnox still unsets the secrets that were loaded before.
    • Binary paths in the generated bash, zsh and fish hooks are now quoted, so paths containing spaces work.
    • Nushell and PowerShell hooks still use the old behavior.
  • PowerShell hooks with starship (#1020) by @jdx. fnox activate pwsh now works whether it's loaded before or after starship:
    • If fnox loaded first, starship replaced the prompt function and cd stopped loading and unloading secrets. The hook now also runs when the current location changes. It passes the event on to any existing handler, and fnox deactivate puts the previous handler back.
    • If starship loaded first, the prompt showed failed commands as successful. The hook now keeps $? and $LASTEXITCODE intact.
    • Each change summary (for example fnox: +1 KEY) is now printed once instead of twice.

Full Changelog: v1.42.0...v1.42.1

💚 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.

v1.42.0: Export selected secrets by name, managed identity for azure-token leases

Choose a tag to compare

@jdx jdx released this 20 Oct 19:29
Immutable release. Only release title and notes can be modified.
315acbd

In fnox 1.42.0, fnox export can export only the secrets you name, and the azure-token lease can authenticate with an Azure managed identity. This release also fixes the AWS SSO login prompt for templated profiles, stops fnox sync --local-file from deleting personal secrets, and updates the bundled TLS root certificates after the eMudhra CA compromise.

Added

  • Export only named secrets (#1002) by @jdx. fnox export now takes optional KEY arguments. When you pass keys, fnox resolves and exports only those secrets, so it doesn't need to authenticate to providers for the rest of the profile. It still resolves the secrets they depend on for interpolation, but doesn't export them. A named key is exported whatever its env setting, so you don't need --all. An unknown key fails with the same "did you mean" suggestions as fnox get. Format, --output, --dry-run and as_file work the same as before, and running fnox export without keys behaves as it did.
    fnox export API_KEY
    fnox export --format json DATABASE_URL API_KEY
  • Managed identity for the azure-token lease (#1003) by @jdx. The lease now accepts the same auth and managed_identity_client_id settings as azure-sm, azure-kms and azure-ac. CI jobs on Azure-hosted runners no longer need to install the Azure CLI and run az login --identity just for the lease. You have to turn managed identity on with auth = "managed-identity"; fnox never tries it on its own. Without it, fnox still uses service principal environment variables and then az/azd. In managed identity mode, fnox doesn't check for az on PATH. Setting managed_identity_client_id without auth = "managed-identity", or using an unknown auth value, is a config error.
    [leases.azure-openai-access-token]
    type = "azure-token"
    scope = "https://ai.azure.com/.default"
    env_var = "AI_ACCESS_TOKEN"
    auth = "managed-identity"
    managed_identity_client_id = "00000000-0000-0000-0000-000000000000"  # omit for system-assigned

Fixed

  • AWS SSO login prompt with templated or secret-based profiles (#998) by @balintant. When profile was a template such as "{{ fnox.profile }}", the aws-kms, aws-ps and aws-sm providers offered to run aws sso login --profile '{{ fnox.profile }}', and that command failed. The prompt now uses the provider's resolved profile, for example aws sso login --profile production. A profile set with profile = { secret = "..." } also gets its resolved name now; before, fnox ran a bare aws sso login. An auth_command you configure still takes precedence.
  • fnox sync --local-file deleting personal secrets (#1004) by @jdx. When you ran the sync a second time, fnox mistook secrets defined only in fnox.local.toml for cached copies of shared secrets and deleted their source references. Synced entries for these secrets now record local_source = true in their sync block. fnox keeps and refreshes those entries on every sync. Cached copies of shared fnox.toml secrets are still removed when the shared secret is deleted. Existing files work without changes. If this bug already removed one of your secrets, add it back to fnox.local.toml once and it will be kept from then on.
  • Revoked eMudhra intermediate certificates (#1005) by @jdx. The private keys of eight eMudhra intermediate CA certificates were compromised. fnox now uses rustls-webpki 0.103.16 and webpki-root-certs/webpki-roots 1.0.10, which stop trusting those certificates for HTTPS connections.

Full Changelog: v1.41.0...v1.42.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.

v1.41.0: gopass provider and profile-aware config templates

Choose a tag to compare

@jdx jdx released this 19 Oct 10:28
Immutable release. Only release title and notes can be modified.
4530878

fnox 1.41.0 adds a gopass provider and lets secret defaults and provider fields use the active profile through Tera templates. fnox exec no longer decrypts env = false secrets that nothing uses, and the AWS SSO login prompt now logs in to the profile your provider is configured with.

Added

  • gopass provider (#980) by @jdx. Teams that already keep secrets in gopass can now read them through fnox. fnox reads values with gopass cat, writes them with gopass insert -f (fnox set --provider gopass), and tests the connection with gopass ls --flat. The optional mount setting is added to the front of every secret path. fnox removes one trailing newline from each value and leaves other whitespace alone. Errors tell you whether gopass is missing, the GPG key is locked or unavailable, or the secret path doesn't exist. You can also add the provider with fnox provider add <name> gopass.
    [providers]
    gopass = { type = "gopass", mount = "company" }  # mount is optional
    
    [secrets]
    # read with: gopass cat company/apps/myapp/database_url
    DATABASE_URL = { provider = "gopass", value = "apps/myapp/database_url" }
  • Profile-aware templates in config (#993) by @jdx. A secret default and literal string provider fields are now rendered as Tera templates if they contain {{, {% or {#. Two variables are available: fnox.profile (the selected profile with the highest priority, or default if none is selected) and fnox.profiles (all selected profiles, lowest priority first). This means you can write values that follow the profile, such as AWS profiles or SST stages, once instead of repeating them for every profile. FNOX_PROFILE is still not exported to child processes.
    [providers.aws]
    type = "aws-ps"
    region = "eu-central-1"
    profile = "{{ fnox.profile }}"
    
    [secrets]
    SST_STAGE = { default = "{{ fnox.profile }}" }
    • Templates are rendered before ${NAME} interpolation. A default is only rendered when it's actually used, and only the ${NAME} references left after rendering must exist. So a branch of an {% if %} that isn't taken requires nothing.
    • An undefined variable or a syntax error fails with a message that names the field. To keep a marker as literal text, wrap it in {% raw %}...{% endraw %}.
    • These fields are not templated: value, description, list fields such as age recipients, and credential_command, which keeps its own template context.
    • Check existing configs: a default or provider field that already contains {{, {% or {# as plain text will now be treated as a template. Wrap it in {% raw %} to keep the old value.
  • More detailed --help (#990) by @jdx. Every public command now shows a description, examples and links to the relevant guides when you run fnox <command> --help. This is the same content as the CLI reference at fnox.jdx.dev/cli. Flags and behavior are unchanged.

Fixed

  • fnox exec no longer resolves unused env = false secrets (#984) by @andersonkrs. Previously, fnox exec resolved every secret in the profile and then removed the env = false ones from the child's environment. With a PIN-protected age identity, each extra decrypt asked for the PIN again. fnox exec now selects secrets the same way as fnox env --json. It resolves only the secrets it injects plus what they depend on: ${NAME} references in defaults, provider secret references such as token = { secret = ... }, and the inputs of selected leases. If a command lease is selected, it still resolves the whole profile. The aws-sts lease's list of inputs now also includes the environment variables for web identity, container credentials and EC2 instance metadata, so those can still be supplied as env = false secrets. The child's environment is the same as before.
  • AWS SSO login prompt uses the provider's profile (#991) by @balintant. When authentication fails for an aws-ps, aws-sm or aws-kms provider that sets profile, the default prompt now runs aws sso login --profile <name> instead of a bare aws sso login. Before, the bare command logged in to the default profile and failed if your AWS config had none. The profile is only added when it's a literal value and no auth_command is set. On Windows, it's only added when the name uses ASCII letters, digits, -, _ and ..

Documentation

  • The guides, landing page, README, provider pages, lease pages and configuration and environment variable references were reorganized and rewritten (#987, #988, #990) by @jdx. This fixes some wrong statements, for example: an earlier import takes precedence over a later one, and imports are not recursive. The sync docs now cover TPM PIN protection and hardware-backed service tokens (#986) by @andersonkrs.

New Contributors

Full Changelog: v1.40.0...v1.41.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.

v1.40.0: Lua plugin providers, Kubernetes Secrets, and Windows daemon support

Choose a tag to compare

@jdx jdx released this 16 Oct 14:46
Immutable release. Only release title and notes can be modified.
fb004bd

fnox 1.40.0 adds custom providers written as Lua plugins, a read-only Kubernetes Secrets provider, 1Password Environments, file-backed plain providers and Bitwarden folder scoping. The daemon now runs on Windows. This release also fixes Azure service principal authentication, repeated hardware PIN prompts for age identities, and fnox set dropping piped values.

Added

  • Custom providers as Lua plugins (#968) by @jdx. You can now write a secrets backend that fnox doesn't support as a Lua 5.4 plugin and share it as a git repository. Declare the plugin under [plugins], then use it with type = "lua":
    [plugins]
    corp = "acme/fnox-plugin-corp#v1.2.0"   # git URL or owner/repo, optional #ref
    
    [providers.corp]
    type = "lua"
    plugin = "corp"                          # or a path such as ./plugins/corp.lua
    
    [secrets]
    DATABASE_URL = { provider = "corp", value = "prod/database-url" }
    • A plugin is a provider.lua that returns hooks. get is required. get_batch, put, encrypt and test are optional, and the hooks you define decide whether fnox set and fnox import can write to the provider. A provider's options table (string values, which can be secret references) is passed to the plugin as ctx.options. Plugins can use the fnox.http, fnox.json, fnox.cmd, fnox.log and fnox.base64 modules. fnox.auth_error(msg) triggers the provider's auth_command flow.
    • New commands: fnox plugin install, update, link, ls and uninstall, plus fnox provider add NAME lua --plugin ....
    • fnox installs a declared plugin the first time a provider uses it (git clone). Plugins go in $FNOX_DATA_DIR/plugins, which defaults to $XDG_DATA_HOME/fnox or ~/.local/share/fnox. Set FNOX_PLUGINS_DIR to use a different location. If a project declares a different source or ref than the installed one, fnox won't load the plugin until you run fnox plugin update <name>.
    • Plugins run with your user's permissions. Only install plugins from sources you trust, and pin a ref. See docs/providers/lua.md for details.
  • Kubernetes Secrets provider (#953) by @jdx. A read-only kubernetes provider reads existing Secrets through the Kubernetes API. It only makes named GET requests and never lists, watches or writes Secrets. A reference can be secret, secret/key or namespace/secret/key. The optional context, namespace, kubeconfig and prefix settings control where it looks, and the FNOX_K8S_CONTEXT environment variable can also select the context. Daemon caching is always disabled for this provider, even with daemon_cache = true.
    [providers.kubernetes]
    type = "kubernetes"
    context = "production"
    namespace = "payments"
    
    [secrets]
    DATABASE_URL = { provider = "kubernetes", value = "database/url" }
  • 1Password Environments (#955) by @jdx. The 1password provider can read variables from a 1Password Environment using environment://<environment-id>/<VARIABLE> references. You can mix these with vault item references in the same config. fnox reads each Environment once per provider instance, and a variable missing from the Environment is an error. This needs the beta op CLI with the op environment command.
  • Read values from a plaintext file with the plain provider (#954) by @joshbode. The new file and format settings make each secret's value a key to look up in that file. format can be env, shell, json, yaml or toml, and you must set it whenever file is set. A relative path resolves against the config file that declares the provider. The file is read-only, and a missing key counts as a missing secret, so default and if_missing still apply.
    [providers.dotenv]
    type = "plain"
    file = "secrets.env"
    format = "env"
    
    [secrets]
    API_TOKEN = { provider = "dotenv", value = "SERVICE_TOKEN" }
  • Bitwarden folder scoping (#965) by @andersonkrs. The new optional folder setting limits lookups to one Bitwarden folder. For nested folders, use the full path, such as folder = "MyApp/Production". This lets items with the same name exist in different folders. It works with both bw and rbw.
  • Azure managed identity (#964) by @jdx. azure-sm, azure-kms and azure-ac accept auth = "managed-identity", with an optional managed_identity_client_id for a user-assigned identity. fnox never tries managed identity on its own; you have to set auth.
  • Daemon on Windows (#974) by @jdx. On Windows, the daemon now uses a per-user named pipe where other platforms use a Unix socket, so Windows gets cached secrets too. Only the current user can access the pipe, and both the client and the daemon check the user on the other end. The wire protocol and the fnox-client API are unchanged.

Fixed

  • Azure service principal environment variables (#964) by @jdx. Azure providers authenticate as a service principal again when AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET are all set. Without them, fnox falls back to the az/azd login. For sovereign clouds, set AZURE_AUTHORITY_HOST. These variables can also be fnox secrets resolved by fnox exec.
  • Hardware PIN prompts for age identities (#978) by @andersonkrs and @jdx. With PIN-protected age identities (age-plugin-tpm --pin, age-plugin-yubikey), a value used by several providers made one command ask for the PIN up to three times. Now a successful decrypt is reused for the rest of the request, so you get one prompt per fnox get, exec or export, or per daemon request. Nothing is kept between requests, and a failed decrypt can be retried.
  • fnox set with piped stdin and metadata flags (#976) by @jdx. echo val | fnox set A -d "desc" (or with --default or --if-missing) said it worked but saved no value. fnox now stores the piped value along with the metadata. If stdin is empty, only the metadata is updated, as before.

Changed

  • fnox import parsing (#954) by @joshbode. fnox import now accepts the {secrets, metadata} document that fnox export --format json|yaml|toml produces. The env parser now follows Docker Compose dotenv rules for inline comments. For example, PORT=8080 # default now imports as 8080, where it used to keep the comment as part of the value. # without whitespace before it, as in ID=val#ue, and # inside quotes are still part of the value.

New Contributors

Full Changelog: v1.39.0...v1.40.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.