From c15bac1728c893b6911f58e871d69d536f6120d3 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:11:12 -0500 Subject: [PATCH 1/2] docs: overhaul guides and redesign the documentation site Entire-Checkpoint: 01M1TJ1CYCXYDWDZZQ1DFBZ1GZ --- AGENTS.md | 21 +- CONTRIBUTING.md | 54 +- README.md | 246 +++---- docs/.vitepress/SOCIAL-IMAGES.md | 2 + docs/.vitepress/check-links.mjs | 56 ++ docs/.vitepress/cli-examples.json | 207 ++++++ docs/.vitepress/config.mjs | 147 +---- docs/.vitepress/enrich-cli.mjs | 96 +++ docs/.vitepress/fonts/README.md | 4 +- docs/.vitepress/navigation.mjs | 205 ++++++ docs/.vitepress/theme/EndevSponsors.vue | 5 + docs/.vitepress/theme/HomePage.vue | 256 ++++++++ docs/.vitepress/theme/style.css | 811 +++++++++++++++++++----- docs/README.md | 79 ++- docs/cli/activate.md | 23 + docs/cli/check.md | 23 + docs/cli/completion.md | 22 + docs/cli/config-files.md | 23 + docs/cli/configuration.md | 12 + docs/cli/daemon.md | 23 + docs/cli/daemon/clear.md | 22 + docs/cli/daemon/start.md | 22 + docs/cli/daemon/status.md | 22 + docs/cli/daemon/stop.md | 21 + docs/cli/deactivate.md | 21 + docs/cli/doctor.md | 23 + docs/cli/edit.md | 22 + docs/cli/exec.md | 24 + docs/cli/export.md | 25 + docs/cli/get.md | 23 + docs/cli/import.md | 23 + docs/cli/index.md | 20 + docs/cli/init.md | 22 + docs/cli/lease.md | 22 + docs/cli/lease/cleanup.md | 21 + docs/cli/lease/create.md | 22 + docs/cli/lease/list.md | 22 + docs/cli/lease/revoke.md | 22 + docs/cli/list.md | 24 + docs/cli/mcp.md | 22 + docs/cli/profiles.md | 22 + docs/cli/provider.md | 22 + docs/cli/provider/add.md | 23 + docs/cli/provider/list.md | 22 + docs/cli/provider/remove.md | 21 + docs/cli/provider/test.md | 23 + docs/cli/proxy.md | 22 + docs/cli/proxy/rules.md | 22 + docs/cli/proxy/run.md | 21 + docs/cli/reencrypt.md | 23 + docs/cli/remove.md | 23 + docs/cli/scan.md | 23 + docs/cli/set.md | 24 + docs/cli/sponsors.md | 21 + docs/cli/sync.md | 23 + docs/cli/tui.md | 22 + docs/cli/version.md | 22 + docs/contributing.md | 85 ++- docs/guide/daemon.md | 28 +- docs/guide/golden-path.md | 40 +- docs/guide/hierarchical-config.md | 42 +- docs/guide/how-it-works.md | 103 ++- docs/guide/import-export.md | 131 ++-- docs/guide/installation.md | 58 +- docs/guide/leases.md | 94 +-- docs/guide/mcp.md | 33 +- docs/guide/mise-integration.md | 229 ++----- docs/guide/missing-secrets.md | 46 +- docs/guide/profiles.md | 50 +- docs/guide/proxy.md | 25 +- docs/guide/quick-start.md | 142 ++--- docs/guide/real-world-example.md | 386 +++-------- docs/guide/shell-integration.md | 32 +- docs/guide/sync.md | 130 ++-- docs/guide/troubleshooting.md | 107 ++++ docs/guide/tui.md | 35 +- docs/guide/what-is-fnox.md | 67 +- docs/index.md | 102 +-- docs/leases/aws-sts.md | 18 +- docs/leases/azure-token.md | 24 +- docs/leases/cloudflare.md | 20 +- docs/leases/command.md | 28 +- docs/leases/gcp-iam.md | 22 +- docs/leases/github-app.md | 14 +- docs/leases/github-oauth.md | 12 +- docs/leases/vault.md | 18 +- docs/providers/1password.md | 374 +++-------- docs/providers/age.md | 132 ++-- docs/providers/aws-kms.md | 98 ++- docs/providers/aws-ps.md | 102 ++- docs/providers/aws-sm.md | 124 ++-- docs/providers/azure-ac.md | 46 +- docs/providers/azure-kms.md | 43 +- docs/providers/azure-sm.md | 52 +- docs/providers/bitwarden-sm.md | 58 +- docs/providers/bitwarden.md | 146 ++--- docs/providers/doppler.md | 58 +- docs/providers/fido2.md | 27 +- docs/providers/foks.md | 47 +- docs/providers/gcp-kms.md | 41 +- docs/providers/gcp-sm.md | 51 +- docs/providers/infisical.md | 99 ++- docs/providers/keepass.md | 112 ++-- docs/providers/keeper-sm.md | 15 +- docs/providers/keychain.md | 103 ++- docs/providers/overview.md | 151 +++-- docs/providers/password-store.md | 133 ++-- docs/providers/passwordstate.md | 73 +++ docs/providers/plain.md | 172 +---- docs/providers/proton-pass.md | 38 +- docs/providers/vault.md | 28 +- docs/providers/yubikey.md | 21 +- docs/reference/configuration.md | 255 ++++---- docs/reference/environment.md | 113 ++-- mise.toml | 1 + package.json | 2 +- test/BITWARDEN_TESTING.md | 239 ++----- test/README.md | 92 +-- test/VAULT_TESTING.md | 341 ++-------- test/fixtures/README.md | 17 +- test/fixtures/bitwarden-certs/README.md | 4 +- tmp/README.md | 26 +- 122 files changed, 4867 insertions(+), 3827 deletions(-) create mode 100644 docs/.vitepress/check-links.mjs create mode 100644 docs/.vitepress/cli-examples.json create mode 100644 docs/.vitepress/enrich-cli.mjs create mode 100644 docs/.vitepress/navigation.mjs create mode 100644 docs/.vitepress/theme/HomePage.vue create mode 100644 docs/guide/troubleshooting.md create mode 100644 docs/providers/passwordstate.md diff --git a/AGENTS.md b/AGENTS.md index aa45c7ca..984f11e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## mbx build cache -`mise install` installs mbx 1.4. `mise run` activates the project's transparent +`mise install` installs the mbx version pinned in `mise.toml`. `mise run` activates the project's transparent Cargo wrapper, so compilation-heavy mise tasks and hk checks use ordinary `cargo` commands. Standalone Cargo commands require an activated mise shell. If the wrapper fails or creates a development papercut, rerun the exact equivalent @@ -57,7 +57,7 @@ will fail), pin that dependency to its last MSRV-compatible version instead. mise run build # Build (debug mode, never use --release) mise run test # Run all tests (cargo + bats) mise run test:cargo # Cargo tests only -mise run test:bats # Bats tests only (depends on build) +mise run test:bats # Bats tests only (run build first) mise run test:bats -- test/init.bats # Specific bats test file mise run ci # Full CI: build + test + lint mise run lint # Lint (hk) @@ -81,12 +81,11 @@ mise run lint-fix # Auto-fix lint issues ## Code Organization -``` -src/commands/ # One file per command -src/providers/ # Implement Provider trait -src/encryption/ # Encryption methods -src/config.rs # Config parsing -src/env.rs # Centralized env var handling (LazyLock, FNOX_* prefix) +```text +src/commands/ # One file per command +crates/fnox-core/src/providers/ # Provider implementations and encryption +crates/fnox-core/src/config.rs # Config parsing and layering +crates/fnox-core/src/env.rs # Centralized FNOX_* environment handling ``` - Use `mod.rs` for module exports @@ -110,6 +109,8 @@ src/env.rs # Centralized env var handling (LazyLock, FNOX_* prefix) 4. `fnox.$FNOX_PROFILE.toml` (profile-specific, if not "default") 5. `fnox.local.toml` (local overrides, gitignored) +Steps 3-5 apply at each discovered directory, from outermost to innermost. A closer directory overrides its parent, including parent-local values. + An explicit `-c/--config` path skips steps 2-5 (no directory recursion, no local overrides) but still loads the global config and the file's own `import`s. @@ -122,7 +123,7 @@ overrides) but still loads the global config and the file's own `import`s. ## Provider Types -All providers follow the same pattern: config in `fnox.toml` stores references/names, actual secrets live in the provider. See `src/providers/` for implementations. +Encryption providers store ciphertext in `fnox.toml`; remote and local storage providers store references there. The plain provider returns unencrypted values. See `crates/fnox-core/src/providers/` for implementations and `docs/providers/overview.md` for the complete provider catalog. | Type | Config `type` | Storage | Key crate/CLI | | ------------------- | ---------------- | ------------------------- | ------------------------ | @@ -140,7 +141,7 @@ All providers follow the same pattern: config in `fnox.toml` stores references/n | password-store | `password-store` | GPG files | `pass` CLI | | Proton Pass | `proton-pass` | Proton Pass vault | `pass-cli` CLI | -**Common provider config fields:** `type` (required), `prefix` (optional namespace), `region` (AWS providers). Most providers support `value` as item name, `item/field` for specific fields. +**Provider fields:** `type` is required. Fields such as `prefix`, `region`, and `vault` depend on the provider type; use its schema and guide for supported fields and reference formats. ## GitHub Interactions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4da17bbf..e2086a35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,53 @@ -# Contributing +# Contributing to fnox -See the [contributing guide](https://fnox.jdx.dev/contributing). +Read the [contribution expectations](https://fnox.jdx.dev/contributing) before starting a substantial change. For repository conventions, see [AGENTS.md](AGENTS.md). + +## Set up a checkout + +```sh +mise install +mise run build +``` + +Development uses the debug build in `target/debug`. An activated mise shell puts the project tools on `PATH`. + +## Run the relevant checks + +```sh +mise run test:cargo # Rust tests +mise run build # Build before Bats tests +mise run test:bats -- test/init.bats # One end-to-end test file +mise run test:bats # All Bats tests +mise run lint # Formatting and lint checks +mise run ci # Build, tests, and lint +``` + +Provider tests may require credentials or local services. See the [test guide](test/README.md) for setup and skip behavior. + +## Work on documentation + +```sh +aube install +aube run docs:dev +aube run docs:build +``` + +The build checks internal links and social previews as well as rendering the site. See [docs/README.md](docs/README.md) for layout, generated CLI pages, and visual checks. Markdown-only changes do not require the full Rust test suite. ## mbx build cache -`mise install` installs [mbx](https://mr-boxington.jdx.dev) 1.8. The normal -`mise run build`, `mise run test:cargo`, and `mise run lint` workflows activate -its transparent Cargo wrapper and therefore use the cache while invoking Cargo -normally. Standalone Cargo commands require an activated mise shell. To bypass -mbx without skipping or weakening a check, prefix the -equivalent Cargo command with `MBX_DISABLE=1`: +`mise install` installs the mbx version pinned in [mise.toml](mise.toml). `mise run` activates the transparent Cargo wrapper for compilation tasks and lint checks. Standalone Cargo commands require an activated mise shell. + +If the wrapper fails, bypass it for the equivalent check without weakening that check: ```sh MBX_DISABLE=1 cargo build MBX_DISABLE=1 cargo test MBX_DISABLE=1 cargo check --workspace MBX_DISABLE=1 cargo clippy -q -- -D warnings -# CI also runs this broader clippy check: +# CI's broader clippy check: MBX_DISABLE=1 cargo clippy --workspace --all-targets -- -D warnings MBX_DISABLE=1 cargo msrv verify ``` -If bypassed Cargo succeeds where the wrapper fails, or mbx introduces a papercut, please start a -[mr-boxington Discussion](https://github.com/jdx/mr-boxington/discussions). -Include the repository and commit, operating system, `mbx --version`, -`mbx doctor`, and both commands and their output. Before posting, redact -secrets, absolute cache paths, remote URLs, namespaces, and other sensitive or -identifying details. +If bypassed Cargo succeeds where the wrapper fails, report the mismatch in a [mr-boxington Discussion](https://github.com/jdx/mr-boxington/discussions). Include the repository and commit, OS, `mbx --version`, `mbx doctor`, and both commands and their output. Redact secrets, absolute cache paths, remote URLs, namespaces, and other sensitive or identifying details before posting. Keep the wrapper enabled in project configuration. diff --git a/README.md b/README.md index 62c033cd..e6941778 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,121 @@ -# πŸ” fnox +# fnox **Fort Knox for your secrets.** [![CI](https://github.com/jdx/fnox/actions/workflows/ci.yml/badge.svg)](https://github.com/jdx/fnox/actions/workflows/ci.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Manage secrets with encryption or cloud providersβ€”or both! fnox gives you a unified interface to work with secrets across development, CI, and production. +fnox loads secrets into your commands from encrypted files, password managers, and cloud services. Keep the configuration in `fnox.toml`, choose where each secret lives, and use the same command in development and CI: -## Sponsors - -

- Sponsored by

- - - - Entire - - -     - - - - Omacom Foundation - - -

- View all sponsors -

- -## Quick Start - -```bash -# Install via mise (recommended) -mise use -g fnox - -# Initialize in your project -fnox init - -# Set a secret (encrypted by default) -fnox set DATABASE_URL "postgresql://localhost/mydb" - -# Get a secret -fnox get DATABASE_URL - -# Run commands with secrets loaded +```sh fnox exec -- npm start - -# Enable shell integration (auto-load on cd) -eval "$(fnox activate bash)" # or zsh, fish β€” see docs for Nushell ``` -## What is fnox? - -fnox lets you store secrets in two ways: - -1. **Encrypted in git** - Using age, AWS KMS, Azure KMS, or GCP KMS -2. **Remote in cloud** - Using AWS Secrets Manager, AWS Parameter Store, Azure Key Vault, GCP Secret Manager, 1Password, Bitwarden, Bitwarden Secrets Manager, Infisical, or HashiCorp Vault - -Your `fnox.toml` config file either contains encrypted secrets or references to remote secrets. Use `fnox exec` to run commands with secrets loaded, or enable shell integration to auto-load secrets when you `cd` into a directory. - -## Supported Providers - -### πŸ” Encryption (secrets in git, encrypted) - -- [**age**](https://fnox.jdx.dev/providers/age) - Modern encryption (works with SSH keys!) -- [**fido2**](https://fnox.jdx.dev/providers/fido2) - FIDO2 hmac-secret hardware-backed encryption -- [**yubikey**](https://fnox.jdx.dev/providers/yubikey) - YubiKey HMAC-SHA1 hardware-backed encryption -- [**aws-kms**](https://fnox.jdx.dev/providers/aws-kms) - AWS Key Management Service -- [**azure-kms**](https://fnox.jdx.dev/providers/azure-kms) - Azure Key Vault encryption -- [**gcp-kms**](https://fnox.jdx.dev/providers/gcp-kms) - Google Cloud KMS - -### ☁️ Cloud Secret Storage (remote, centralized) - -- [**aws-ps**](https://fnox.jdx.dev/providers/aws-ps) - AWS Parameter Store -- [**aws-sm**](https://fnox.jdx.dev/providers/aws-sm) - AWS Secrets Manager -- [**azure-ac**](https://fnox.jdx.dev/providers/azure-ac) - Azure App Configuration -- [**azure-sm**](https://fnox.jdx.dev/providers/azure-sm) - Azure Key Vault Secrets -- [**gcp-sm**](https://fnox.jdx.dev/providers/gcp-sm) - Google Cloud Secret Manager -- [**bitwarden-sm**](https://fnox.jdx.dev/providers/bitwarden-sm) - Bitwarden Secrets Manager -- [**vault**](https://fnox.jdx.dev/providers/vault) - HashiCorp Vault -- [**doppler**](https://fnox.jdx.dev/providers/doppler) - Doppler -- [**foks**](https://fnox.jdx.dev/providers/foks) - FOKS (Federated Open Key Service) -- [**keeper-sm**](https://fnox.jdx.dev/providers/keeper-sm) - Keeper Secrets Manager - -### πŸ”‘ Password Managers & Secret Services - -- [**1password**](https://fnox.jdx.dev/providers/1password) - 1Password CLI -- [**bitwarden**](https://fnox.jdx.dev/providers/bitwarden) - Bitwarden/Vaultwarden -- [**infisical**](https://fnox.jdx.dev/providers/infisical) - Infisical secrets management -- [**proton-pass**](https://fnox.jdx.dev/providers/proton-pass) - Proton Pass CLI +[Documentation](https://fnox.jdx.dev) Β· [Quick start](https://fnox.jdx.dev/guide/quick-start) Β· [Providers](https://fnox.jdx.dev/providers/overview) Β· [CLI reference](https://fnox.jdx.dev/cli/) -### πŸ’» Local Storage +## Get started -- [**keychain**](https://fnox.jdx.dev/providers/keychain) - OS Keychain (macOS/Windows/Linux) -- [**keepass**](https://fnox.jdx.dev/providers/keepass) - KeePass database files (.kdbx) -- [**password-store**](https://fnox.jdx.dev/providers/password-store) - GPG-encrypted password store (Unix pass) -- [**plain**](https://fnox.jdx.dev/providers/plain) - Plain text (for defaults only!) +Install with [mise](https://mise.jdx.dev): -## Documentation - -**πŸ“š [Complete Documentation](https://fnox.jdx.dev/)** +```sh +mise use -g fnox +``` -### Quick Links +Or with Rust: `cargo install fnox --locked`. See [installation](https://fnox.jdx.dev/guide/installation) for other methods and updates. -- [Installation](https://fnox.jdx.dev/guide/installation) -- [Quick Start Guide](https://fnox.jdx.dev/guide/quick-start) -- [How It Works](https://fnox.jdx.dev/guide/how-it-works) -- [Shell Integration](https://fnox.jdx.dev/guide/shell-integration) -- [Providers Overview](https://fnox.jdx.dev/providers/overview) -- [Real-World Example](https://fnox.jdx.dev/guide/real-world-example) +In your project, run the setup wizard and configure a provider before storing a secret: -### Provider Guides +```sh +fnox init -- [Age Encryption](https://fnox.jdx.dev/providers/age) - Simple, free, works with SSH keys -- [AWS Secrets Manager](https://fnox.jdx.dev/providers/aws-sm) - Centralized AWS secret management -- [AWS Parameter Store](https://fnox.jdx.dev/providers/aws-ps) - Simple, cost-effective AWS secret storage -- [1Password](https://fnox.jdx.dev/providers/1password) - Integrate with 1Password CLI -- [Bitwarden](https://fnox.jdx.dev/providers/bitwarden) - Open source password manager +# Use the default provider selected in the wizard; the value is prompted +fnox set DATABASE_URL -[**View all providers β†’**](https://fnox.jdx.dev/providers/overview) +# Check access, then run your application +fnox check --all +fnox exec -- npm start +``` -### Reference +The provider determines where the value is stored. **Without a provider, `fnox set` writes a plaintext default.** Follow the [age quick start](https://fnox.jdx.dev/guide/quick-start) for a complete encrypted setup, or [connect an existing vault](https://fnox.jdx.dev/guide/golden-path). -- [CLI Reference](https://fnox.jdx.dev/cli/) -- [Environment Variables](https://fnox.jdx.dev/reference/environment) -- [Configuration File](https://fnox.jdx.dev/reference/configuration) +## One config, your choice of storage -## Example +With a remote provider, `value` is a reference. This example reads an existing 1Password item: ```toml # fnox.toml - -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } +[providers.op] +type = "1password" +vault = "Engineering" [secrets] -# Development secrets (encrypted in git) -DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } # ← encrypted, safe to commit -API_KEY = { default = "dev-key-12345" } # ← plain default for local dev +DATABASE_URL = { provider = "op", value = "Database/url" } +LOG_LEVEL = { default = "info" } # Non-sensitive configuration -[profiles.production.providers] -aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp/" } +[profiles.production.providers.aws] +type = "aws-sm" +region = "us-east-1" +prefix = "myapp/" [profiles.production.secrets] -DATABASE_URL = { provider = "aws", value = "database-url" } # ← reference to AWS secret +DATABASE_URL = { provider = "aws", value = "database-url", if_missing = "error" } ``` -```bash -# Development (uses encrypted secrets) +```sh fnox exec -- npm start - -# Production (uses AWS Secrets Manager) fnox exec --profile production -- ./deploy.sh ``` -## Why fnox? - -- **Flexible** - Mix and match encryption and cloud providers -- **Team-friendly** - Encrypted secrets in git, everyone can decrypt -- **Multi-environment** - Different providers for dev, staging, prod -- **Shell integration** - Auto-load secrets on directory change -- **Developer-focused** - Simple config, powerful features -- **No vendor lock-in** - Switch providers anytime - -## Installation - -### Using mise (recommended) - -```bash -mise use -g fnox -``` - -### Using Cargo +With an encryption provider, `fnox set` writes ciphertext into `fnox.toml`. Commit the ciphertext and public recipients; keep private keys outside the repository. Teammates need a matching private key or access to the configured vault. -```bash -cargo install fnox -``` +## Fit secrets into your workflow -### From Source +- **[Load on directory change](https://fnox.jdx.dev/guide/shell-integration).** Shell hooks load and unload secrets as you move between projects. Bash, Zsh, Fish, Nushell, and PowerShell are supported. +- **[Cache a vault locally](https://fnox.jdx.dev/guide/sync).** `fnox sync` encrypts a personal cache for offline use with a local provider such as age. Refresh it when vault values change. +- **[Cache in memory](https://fnox.jdx.dev/guide/daemon).** The optional daemon reuses resolved values during your session. +- **[Separate environments](https://fnox.jdx.dev/guide/profiles).** Compose profiles, share settings across a monorepo, and keep personal overrides in `fnox.local.toml`. +- **[Use temporary credentials](https://fnox.jdx.dev/guide/leases).** Create short-lived credentials with AWS STS, GitHub Apps, Vault, and other lease backends. +- **[Scope agent access](https://fnox.jdx.dev/guide/proxy).** Pass placeholders to a command and inject credentials into matching HTTPS requests. An [MCP server](https://fnox.jdx.dev/guide/mcp) also exposes selected secrets and command execution. -```bash -git clone https://github.com/jdx/fnox -cd fnox -cargo install --path . -``` +## Providers -## Development +Mix providers in the same project. Each provider guide covers authentication, configuration, and reference formats. -See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CLAUDE.md](./CLAUDE.md) for development guidelines. +| Where values live | Providers | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Encrypted in your config | [age](https://fnox.jdx.dev/providers/age), [FIDO2](https://fnox.jdx.dev/providers/fido2), [YubiKey](https://fnox.jdx.dev/providers/yubikey), [AWS KMS](https://fnox.jdx.dev/providers/aws-kms), [Azure KMS](https://fnox.jdx.dev/providers/azure-kms), [GCP KMS](https://fnox.jdx.dev/providers/gcp-kms) | +| Cloud and hosted stores | [AWS Secrets Manager](https://fnox.jdx.dev/providers/aws-sm), [AWS Parameter Store](https://fnox.jdx.dev/providers/aws-ps), [Azure Key Vault](https://fnox.jdx.dev/providers/azure-sm), [Azure App Configuration](https://fnox.jdx.dev/providers/azure-ac), [GCP Secret Manager](https://fnox.jdx.dev/providers/gcp-sm), [Vault](https://fnox.jdx.dev/providers/vault), [Doppler](https://fnox.jdx.dev/providers/doppler), [FOKS](https://fnox.jdx.dev/providers/foks), [Bitwarden Secrets Manager](https://fnox.jdx.dev/providers/bitwarden-sm), [Keeper Secrets Manager](https://fnox.jdx.dev/providers/keeper-sm) | +| Password managers and secret services | [1Password](https://fnox.jdx.dev/providers/1password), [Bitwarden](https://fnox.jdx.dev/providers/bitwarden), [Infisical](https://fnox.jdx.dev/providers/infisical), [Passwordstate](https://fnox.jdx.dev/providers/passwordstate), [Proton Pass](https://fnox.jdx.dev/providers/proton-pass) | +| Local stores | [OS keychain](https://fnox.jdx.dev/providers/keychain), [KeePass](https://fnox.jdx.dev/providers/keepass), [password-store](https://fnox.jdx.dev/providers/password-store) | +| Non-sensitive configuration | [Plaintext defaults](https://fnox.jdx.dev/providers/plain) | -```bash -# Build -mise run build +See the [provider comparison](https://fnox.jdx.dev/providers/overview) to choose a storage model. Cloud KMS providers require network access even though their ciphertext lives locally. -# Run tests -mise run test +## Contribute -# Run specific tests -mise run test:cargo -mise run test:bats +Start with [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, tests, and documentation changes. Report bugs in the [issue tracker](https://github.com/jdx/fnox/issues). -# Lint -mise run lint +## Sponsors -# Full CI check -mise run ci -``` +

+ Sponsored by

+ + + + Entire + + +     + + + + Omacom Foundation + + +

+ View all sponsors +

## License -MIT License - see [LICENSE](LICENSE) for details. - -## Links - -- [Documentation](https://fnox.jdx.dev/) -- [GitHub Repository](https://github.com/jdx/fnox) -- [Issue Tracker](https://github.com/jdx/fnox/issues) -- [mise](https://mise.jdx.dev) - Recommended installation method +[MIT](LICENSE) diff --git a/docs/.vitepress/SOCIAL-IMAGES.md b/docs/.vitepress/SOCIAL-IMAGES.md index 5fe57857..fe09a363 100644 --- a/docs/.vitepress/SOCIAL-IMAGES.md +++ b/docs/.vitepress/SOCIAL-IMAGES.md @@ -11,3 +11,5 @@ new URL. for matching Open Graph/Twitter metadata and the exact image expected from each page title. Regression tests reject swapped images and empty alt text. Edit `social-images.mjs` to adjust colors or layout. + +The homepage layout lives in `theme/HomePage.vue`; the social image remains a static title-and-logo composition. Check both when changing product messaging. See [the documentation guide](../README.md) for build and preview commands. diff --git a/docs/.vitepress/check-links.mjs b/docs/.vitepress/check-links.mjs new file mode 100644 index 00000000..9b4a46a2 --- /dev/null +++ b/docs/.vitepress/check-links.mjs @@ -0,0 +1,56 @@ +// Check rendered links, including fragments and links inside Vue components. +import { readFileSync, readdirSync, existsSync, statSync } from "node:fs"; +import { join, resolve, relative } from "node:path"; + +const root = resolve(process.argv[2] || "docs/.vitepress/dist"); +const walk = (dir) => + readdirSync(dir, { withFileTypes: true }).flatMap((entry) => + entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)], + ); +const files = walk(root).filter((file) => file.endsWith(".html")); +const pages = new Map(files.map((file) => [file, readFileSync(file, "utf8")])); +const errors = new Set(); +const checked = new Set(); +const decode = (s) => + s + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'"); +let links = 0; +for (const [file, html] of pages) { + const base = new URL(relative(root, file), "https://fnox.jdx.dev/"); + for (const [, raw] of html.matchAll(/]*\bhref="([^"]+)"/g)) { + const href = decode(raw); + const url = new URL(href, base); + if (url.origin !== base.origin) continue; + links++; + const key = `${file}:${url.pathname}${url.hash}`; + if (checked.has(key)) continue; + checked.add(key); + let path = join(root, decodeURIComponent(url.pathname)); + if (existsSync(path) && statSync(path).isDirectory()) + path = join(path, "index.html"); + if (!existsSync(path) && !/\.[^/]+$/.test(path)) path += ".html"; + if (!existsSync(path)) { + errors.add(`${relative(root, file)}: missing page ${href}`); + continue; + } + if (url.hash && pages.has(path)) { + const ids = new Set( + [...pages.get(path).matchAll(/\bid="([^"]+)"/g)].map(([, id]) => + decode(id), + ), + ); + if (!ids.has(decodeURIComponent(url.hash.slice(1)))) + errors.add(`${relative(root, file)}: missing anchor ${href}`); + } + } +} +if (errors.size) { + console.error([...errors].sort().join("\n")); + process.exitCode = 1; +} else { + console.log( + `Checked ${links} internal links across ${files.length} built pages, including anchors.`, + ); +} diff --git a/docs/.vitepress/cli-examples.json b/docs/.vitepress/cli-examples.json new file mode 100644 index 00000000..f37b2e98 --- /dev/null +++ b/docs/.vitepress/cli-examples.json @@ -0,0 +1,207 @@ +{ + "activate": { + "note": "Enable the hook for your shell. Fish, Nushell, and PowerShell use different activation syntax; follow the shell guide for those commands.", + "commands": "eval \"$(fnox activate bash)\"\n# Zsh:\neval \"$(fnox activate zsh)\"", + "links": ["/guide/shell-integration"] + }, + "deactivate": { + "note": "After shell activation, the fnox shell function applies this command to the current session. Remove the activation line from your startup file to disable it in future shells.", + "commands": "fnox deactivate", + "links": ["/guide/shell-integration"] + }, + "check": { + "note": "By default, check validates required secrets. Use --all to also check secrets whose missing-value policy is warn or ignore. The command validates resolution without printing resolved values.", + "commands": "fnox check --all\nfnox check --all --profile production", + "links": ["/guide/troubleshooting", "/guide/missing-secrets"] + }, + "completion": { + "note": "Generate completions for the named shell. Install or source the output according to that shell\u2019s completion setup; generating it alone does not change your shell.", + "commands": "fnox completion bash > fnox.bash\nfnox completion zsh > _fnox", + "links": ["/guide/shell-integration"] + }, + "config-files": { + "note": "Inspect the effective file stack before debugging an unexpected value. An explicit config path skips directory discovery and adjacent local overrides; the global config still loads.", + "commands": "fnox config-files\nfnox --profile staging config-files\nfnox -c ./fnox.toml config-files", + "links": ["/guide/hierarchical-config"] + }, + "doctor": { + "note": "Check the local fnox setup, then test a specific provider or resolve all configured secrets to narrow down a failure.", + "commands": "fnox doctor\nfnox provider test op\nfnox check --all", + "links": ["/guide/troubleshooting"] + }, + "edit": { + "note": "Open configuration in your editor. Configure EDITOR (or VISUAL) first. Use fnox set for an individual value so the selected provider handles storage.", + "commands": "EDITOR=vim fnox edit", + "links": ["/reference/environment#editor", "/cli/set"] + }, + "exec": { + "note": "Put fnox options before --. Arguments after -- belong to the child command. To expand an injected variable in a shell expression, defer expansion to a child shell with single quotes.", + "commands": "fnox exec -- npm start\nfnox exec --profile production --if-missing error -- ./deploy.sh\nfnox exec -- sh -c 'test -n \"$DATABASE_URL\"'", + "links": ["/guide/how-it-works", "/guide/profiles"] + }, + "export": { + "note": "Exports contain real resolved values. JSON, YAML, and TOML wrap them in a secrets object; see the import/export guide before migrating. By default, export follows shell injection settings; `--all` also includes exec-only and non-injected secrets. `--dry-run` only suppresses a file write when `--output` is supplied: output to stdout still contains values.", + "commands": "fnox export --format json\n# Create a private plaintext file (POSIX shell)\numask 077\nfnox export --output .env", + "links": ["/guide/import-export", "/reference/configuration#env-1"] + }, + "get": { + "note": "Print one resolved value to stdout. Use check --all when you only need to verify access without displaying the secret.", + "commands": "fnox get DATABASE_URL\nfnox get DATABASE_URL --profile staging", + "links": ["/guide/how-it-works", "/cli/check"] + }, + "import": { + "note": "Configure an encryption provider first. Remote storage providers are not import targets. When stdin carries the import data, use --force because stdin cannot also answer the confirmation prompt.", + "commands": "fnox import --input .env --provider age --dry-run\nfnox import --input .env --provider age\ncat secrets.json | fnox import json --provider age --force", + "links": ["/guide/import-export"] + }, + "init": { + "note": "In a terminal, the wizard can configure a provider and select it as the default. --skip-wizard creates a minimal config; it does not enable encryption. Configure a provider before storing sensitive values.", + "commands": "fnox init\nfnox init --skip-wizard", + "links": ["/guide/quick-start"] + }, + "list": { + "note": "Lists the secrets configured in fnox. It does not enumerate every remote vault item. Values are hidden unless you request --values.", + "commands": "fnox list\nfnox list --sources\nfnox list --profile production", + "links": ["/guide/hierarchical-config", "/cli/tui"] + }, + "mcp": { + "note": "Run a stdio MCP server from the project directory. Configure its secret allowlist and authenticate to providers before starting the client.", + "commands": "fnox mcp\nfnox --profile staging mcp", + "links": ["/guide/mcp"] + }, + "profiles": { + "note": "Inspect the available profile names before selecting or composing them. Use --write-profile when writing with multiple active profiles.", + "commands": "fnox profiles\nfnox -P aws,staging list", + "links": ["/guide/profiles"] + }, + "provider": { + "note": "Manage configured provider instances. The instance name, such as op, is distinct from the provider type, such as 1password.", + "commands": "fnox provider list\nfnox provider test op", + "links": ["/providers/overview"] + }, + "provider/add": { + "note": "Creates a provider configuration. Some types require interactive setup; others create placeholders that you must replace before use. For this command, the aws and gcp choices correspond to the aws-sm and gcp-sm config types.", + "commands": "fnox provider add op 1password\nfnox provider add sync-age age --global", + "links": ["/providers/overview", "/guide/golden-path"] + }, + "provider/list": { + "note": "List configured provider instances for the selected profile. To find the supported provider types, use the provider catalog.", + "commands": "fnox provider list\nfnox --profile production provider list", + "links": ["/providers/overview"] + }, + "provider/remove": { + "note": "Remove a provider definition from the configuration. Update secrets that reference it before removing it; this command does not delete the remote vault or its items.", + "commands": "fnox provider remove old-vault", + "links": ["/providers/overview"] + }, + "provider/test": { + "note": "Use a configured instance name, not its type. A successful connection test does not guarantee that every referenced item exists; check --all validates those references too.", + "commands": "fnox provider test op\nfnox provider test --all\nfnox check --all", + "links": ["/guide/troubleshooting"] + }, + "reencrypt": { + "note": "After changing recipients, re-encrypt with an identity that can still decrypt the existing ciphertext. Repeat for each affected profile. This changes the encryption, not the underlying secret value.", + "commands": "fnox reencrypt --provider age --dry-run\nfnox reencrypt --provider age\nfnox reencrypt --provider age --profile staging", + "links": ["/providers/age#adding-a-new-team-member"] + }, + "remove": { + "note": "Remove a secret definition from fnox configuration. The command does not delete or revoke the original value in a remote provider.", + "commands": "fnox remove OLD_TOKEN --dry-run\nfnox remove OLD_TOKEN", + "links": ["/cli/set", "/guide/profiles"] + }, + "scan": { + "note": "Scan files for potential secrets using heuristics. Findings need review; a clean scan does not prove that files or git history contain no secrets.", + "commands": "fnox scan .\nfnox scan . --format json\nfnox scan . --ignore \"vendor/**\"", + "links": ["/providers/plain"] + }, + "set": { + "note": "Configure a provider first. Omitting VALUE prompts with hidden input in a terminal or reads piped stdin. Without a selected provider, set writes a plaintext default. --from-file preserves file contents, including trailing newlines.", + "commands": "fnox set DATABASE_URL --provider age\nfnox set SSH_PRIVATE_KEY --provider age --from-file ~/.ssh/id_ed25519\nfnox set DATABASE_URL --profile staging --provider age", + "links": ["/guide/quick-start", "/reference/configuration#as-file"] + }, + "sponsors": { + "note": "Show project sponsors in the terminal.", + "commands": "fnox sponsors", + "links": ["/contributing"] + }, + "sync": { + "note": "Use a configured encryption provider as the target. --local-file writes a personal cache next to the project config; add that file to .gitignore. Run sync again after source values change.", + "commands": "fnox sync --provider sync-age --local-file --dry-run\nfnox sync --provider sync-age --local-file\nfnox sync --provider sync-age --local-file DATABASE_URL", + "links": ["/guide/sync"] + }, + "tui": { + "note": "Browse and copy values in the terminal. Dashboard edits are held in memory only; use set or remove to persist changes.", + "commands": "fnox tui\nfnox --profile staging tui", + "links": ["/guide/tui"] + }, + "version": { + "note": "Include the version when reporting a bug or comparing local and CI behavior.", + "commands": "fnox version\nfnox --version", + "links": ["/guide/troubleshooting"] + }, + "daemon": { + "note": "The daemon is opt-in and caches resolved values in memory. Enable it in config or with FNOX_DAEMON=on to use it for supported reads.", + "commands": "fnox daemon status\nfnox daemon start\nFNOX_DAEMON=on fnox check --all", + "links": ["/guide/daemon"] + }, + "daemon/start": { + "note": "Start the per-user daemon. Enable daemon mode in configuration or the environment for subsequent reads to use it.", + "commands": "fnox daemon start\nfnox daemon status", + "links": ["/guide/daemon"] + }, + "daemon/status": { + "note": "Inspect the current daemon state when diagnosing cache behavior.", + "commands": "fnox daemon status", + "links": ["/guide/daemon", "/guide/troubleshooting"] + }, + "daemon/clear": { + "note": "Clear cached values after changing a secret in its source provider. This clears running daemon caches; it does not remove encrypted sync caches.", + "commands": "fnox daemon clear", + "links": ["/guide/daemon", "/guide/sync"] + }, + "daemon/stop": { + "note": "Stop the daemon and discard its in-memory cache. If daemon mode remains enabled, a later supported read may start it again.", + "commands": "fnox daemon stop", + "links": ["/guide/daemon"] + }, + "lease": { + "note": "Create and manage temporary credentials from configured lease backends. fnox exec can create or reuse a lease automatically.", + "commands": "fnox lease list --active\nfnox exec -- aws s3 ls", + "links": ["/guide/leases"] + }, + "lease/create": { + "note": "Create a lease for a configured backend. Without a backend name, all configured backends are selected. Output contains usable temporary credentials; avoid putting it in logs.", + "commands": "fnox lease create aws --duration 30m\nfnox lease create aws --interactive", + "links": ["/guide/leases"] + }, + "lease/list": { + "note": "Inspect the local lease ledger. This is a record of leases tracked by fnox, not a complete inventory of credentials at the service.", + "commands": "fnox lease list --active\nfnox lease list --expired", + "links": ["/guide/leases#how-caching-works"] + }, + "lease/revoke": { + "note": "Use an ID from lease list. Remote revocation depends on the backend; native-expiry backends cannot invalidate a token immediately through fnox.", + "commands": "fnox lease list --active\nfnox lease revoke LEASE_ID", + "links": ["/guide/leases#supported-backends"] + }, + "lease/cleanup": { + "note": "Ask backends to clean up expired tracked leases that require manual revocation. Backend credentials and network access may be needed.", + "commands": "fnox lease cleanup", + "links": ["/guide/leases"] + }, + "proxy": { + "note": "Configure destination rules before launching a command. The proxy controls credential injection; it is not an operating-system sandbox.", + "commands": "fnox proxy rules\nfnox proxy run -- node agent.js", + "links": ["/guide/proxy"] + }, + "proxy/rules": { + "note": "Inspect effective rules without resolving their secrets. Confirm domains, methods, and paths before launching the client.", + "commands": "fnox proxy rules\nfnox --profile staging proxy rules", + "links": ["/guide/proxy"] + }, + "proxy/run": { + "note": "The child receives placeholders and proxy/CA settings. Its HTTP client must honor those settings. Configure proxy rules before running this command.", + "commands": "fnox proxy run -- node agent.js", + "links": ["/guide/proxy"] + } +} diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 326e80bc..bc534174 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -1,3 +1,4 @@ +import { sidebar } from "./navigation.mjs"; import { socialCard, writeSocialCard } from "./social-images.mjs"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; @@ -42,145 +43,45 @@ export default defineConfig({ title: "fnox", description: siteDescription, base: "/", - appearance: "force-dark", + appearance: "dark", sitemap: { hostname: siteUrl }, + srcExclude: ["README.md"], themeConfig: { logo: "/logo.svg", + outline: { level: [2, 3], label: "On this page" }, + editLink: { + pattern: ({ relativePath }) => + relativePath.startsWith("cli/") + ? "https://github.com/jdx/fnox/blob/main/docs/README.md#generated-reference" + : `https://github.com/jdx/fnox/edit/main/docs/${relativePath}`, + text: "Improve this page", + }, nav: [ - { text: "Guide", link: "/guide/what-is-fnox" }, + { text: "Guide", link: "/guide/quick-start" }, { text: "Providers", link: "/providers/overview" }, - { text: "CLI Reference", link: "/cli/" }, - { text: "Reference", link: "/reference/environment" }, - { - text: `v${latestVersion}`, - link: "https://github.com/jdx/fnox/releases", - }, - ], - - sidebar: [ - { - text: "Introduction", - items: [ - { text: "What is fnox?", link: "/guide/what-is-fnox" }, - { text: "Installation", link: "/guide/installation" }, - { text: "Quick Start", link: "/guide/quick-start" }, - { text: "How It Works", link: "/guide/how-it-works" }, - { text: "Contributing", link: "/contributing" }, - ], - }, - { - text: "Features", - items: [ - { text: "Shell Integration", link: "/guide/shell-integration" }, - { text: "Syncing Secrets Locally", link: "/guide/sync" }, - { text: "Per-User Daemon", link: "/guide/daemon" }, - { text: "Credential Proxy", link: "/guide/proxy" }, - { text: "Mise Integration", link: "/guide/mise-integration" }, - { text: "TUI Dashboard", link: "/guide/tui" }, - { text: "Profiles", link: "/guide/profiles" }, - { text: "Hierarchical Config", link: "/guide/hierarchical-config" }, - { - text: "Handling Missing Secrets", - link: "/guide/missing-secrets", - }, - { text: "Import/Export", link: "/guide/import-export" }, - { text: "Credential Leases", link: "/guide/leases" }, - { text: "MCP Server", link: "/guide/mcp" }, - ], - }, - { - text: "Lease Backends", - collapsed: true, - items: [ - { text: "AWS STS", link: "/leases/aws-sts" }, - { text: "GCP IAM", link: "/leases/gcp-iam" }, - { text: "Azure Token", link: "/leases/azure-token" }, - { text: "HashiCorp Vault", link: "/leases/vault" }, - { text: "Custom Command", link: "/leases/command" }, - ], - }, - { - text: "Examples", - items: [ - { text: "Golden Path Setup", link: "/guide/golden-path" }, - { text: "Real-World Setup", link: "/guide/real-world-example" }, - ], - }, - { - text: "Providers", - items: [ - { text: "Overview", link: "/providers/overview" }, - { - text: "Encryption (in git)", - collapsed: true, - items: [ - { text: "Age Encryption", link: "/providers/age" }, - { text: "FIDO2", link: "/providers/fido2" }, - { text: "YubiKey", link: "/providers/yubikey" }, - { text: "AWS KMS", link: "/providers/aws-kms" }, - { text: "Azure Key Vault Keys", link: "/providers/azure-kms" }, - { text: "Google Cloud KMS", link: "/providers/gcp-kms" }, - ], - }, - { - text: "Cloud Secret Storage", - collapsed: true, - items: [ - { text: "AWS Parameter Store", link: "/providers/aws-ps" }, - { text: "AWS Secrets Manager", link: "/providers/aws-sm" }, - { text: "Azure App Configuration", link: "/providers/azure-ac" }, - { text: "Azure Key Vault Secrets", link: "/providers/azure-sm" }, - { text: "Doppler", link: "/providers/doppler" }, - { text: "FOKS", link: "/providers/foks" }, - { text: "GCP Secret Manager", link: "/providers/gcp-sm" }, - { - text: "Bitwarden Secrets Manager", - link: "/providers/bitwarden-sm", - }, - { text: "HashiCorp Vault", link: "/providers/vault" }, - ], - }, - { - text: "Password Managers & Secret Services", - collapsed: true, - items: [ - { text: "1Password", link: "/providers/1password" }, - { text: "Bitwarden", link: "/providers/bitwarden" }, - { text: "Infisical", link: "/providers/infisical" }, - { text: "Proton Pass", link: "/providers/proton-pass" }, - ], - }, - { - text: "Local Storage", - collapsed: true, - items: [ - { text: "OS Keychain", link: "/providers/keychain" }, - { text: "KeePass", link: "/providers/keepass" }, - { text: "password-store", link: "/providers/password-store" }, - { text: "Plain Text", link: "/providers/plain" }, - ], - }, - ], - }, { - text: "CLI Reference", - link: "/cli/", - items: commands.map((cmd) => ({ - text: cmd.join(" "), - link: `/cli/${cmd.join("/")}`, - })), + text: "Leases", + link: "/guide/leases", + activeMatch: "/leases/|/guide/leases", }, { text: "Reference", items: [ - { text: "Environment Variables", link: "/reference/environment" }, + { text: "Commands", link: "/cli/" }, { text: "Configuration", link: "/reference/configuration" }, + { text: "Environment variables", link: "/reference/environment" }, ], }, + { + text: `v${latestVersion}`, + link: "https://github.com/jdx/fnox/releases", + }, ], + sidebar: sidebar(commands), + socialLinks: [{ icon: "github", link: "https://github.com/jdx/fnox" }], footer: false, @@ -252,7 +153,7 @@ export default defineConfig({ }, ], ["link", { rel: "manifest", href: "/site.webmanifest" }], - ["meta", { name: "theme-color", content: "#0d0221" }], + ["meta", { name: "theme-color", content: "#0b1018" }], ["meta", { property: "og:site_name", content: "fnox" }], ["meta", { property: "og:type", content: "website" }], ["meta", { property: "og:locale", content: "en_US" }], diff --git a/docs/.vitepress/enrich-cli.mjs b/docs/.vitepress/enrich-cli.mjs new file mode 100644 index 00000000..0a24dec7 --- /dev/null +++ b/docs/.vitepress/enrich-cli.mjs @@ -0,0 +1,96 @@ +// Run after usage-cli generation, before formatting. Never edit generated pages by hand. +import { readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { join, relative } from "node:path"; +import examples from "./cli-examples.json" with { type: "json" }; + +const root = fileURLToPath(new URL("../cli/", import.meta.url)); +const start = ""; +const end = ""; +const walk = (dir) => + readdirSync(dir, { withFileTypes: true }).flatMap((entry) => + entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)], + ); +const labels = { + "/guide/shell-integration": "Shell integration", + "/guide/troubleshooting": "Troubleshooting", + "/guide/missing-secrets": "Missing secrets and defaults", + "/guide/hierarchical-config": "Config files and overrides", + "/reference/environment#editor": "Editor selection", + "/cli/set": "Set a secret", + "/guide/how-it-works": "How fnox works", + "/guide/profiles": "Profiles", + "/guide/import-export": "Import and export", + "/reference/configuration#env-1": "Secret injection settings", + "/cli/check": "Check secret access", + "/guide/quick-start": "Quick start", + "/cli/tui": "Terminal dashboard", + "/guide/mcp": "MCP server", + "/providers/overview": "Provider catalog", + "/guide/golden-path": "Connect a vault", + "/providers/age#adding-a-new-team-member": "Update age recipients", + "/providers/plain": "Plaintext defaults", + "/reference/configuration#as-file": "File secrets", + "/contributing": "Contributing", + "/guide/sync": "Sync a local cache", + "/guide/tui": "Terminal dashboard", + "/guide/daemon": "Daemon caching", + "/guide/leases": "Credential leases", + "/guide/leases#how-caching-works": "Lease caching", + "/guide/leases#supported-backends": "Lease backend capabilities", + "/guide/proxy": "Credential proxy", +}; +const seen = new Set(); +for (const file of walk(root).filter((file) => file.endsWith(".md"))) { + const slug = relative(root, file).replaceAll("\\", "/").replace(/\.md$/, ""); + let source = readFileSync(file, "utf8"); + const marker = source.indexOf(start); + if (marker !== -1) { + const finish = source.indexOf(end, marker); + if (finish === -1) throw new Error(`Unclosed examples block: ${file}`); + source = source.slice(0, marker) + source.slice(finish + end.length); + } + source = source.replace(/^---\n[\s\S]*?\n---\n\n/, ""); + let extra; + let description; + if (slug === "index") { + description = + "Find fnox commands for reading secrets, running applications, configuring providers, and managing caches and temporary credentials."; + extra = `## Choose a command\n\n| Task | Commands |\n| --- | --- |\n| Read and run | [get](/cli/get), [list](/cli/list), [exec](/cli/exec), [check](/cli/check) |\n| Store or migrate | [set](/cli/set), [import](/cli/import), [export](/cli/export), [sync](/cli/sync) |\n| Configure | [init](/cli/init), [provider](/cli/provider), [profiles](/cli/profiles), [config-files](/cli/config-files) |\n| Diagnose | [doctor](/cli/doctor), [scan](/cli/scan) |\n| Cache and broker | [daemon](/cli/daemon), [lease](/cli/lease), [proxy](/cli/proxy), [mcp](/cli/mcp) |\n\nNew to fnox? Follow the [quick start](/guide/quick-start). Put fnox options before the \`--\` separator in commands such as \`fnox exec -- npm start\`.`; + } else if (slug === "configuration") { + description = + "Generated runtime settings for fnox, including profile selection, missing-secret behavior, and key-file settings."; + extra = + "## Configuration file reference\n\nThis page describes generated runtime settings. For the structure of `fnox.toml`, see the [configuration reference](/reference/configuration). For authentication and runtime overrides, see [environment variables](/reference/environment)."; + } else { + const example = examples[slug]; + if (!example) + throw new Error(`Add documentation examples for fnox ${slug}`); + seen.add(slug); + description = `Usage and examples for fnox ${slug.replaceAll("/", " ")}. ${example.note.split(". ")[0].replace(/\.$/, "")}.`; + extra = `## Examples\n\n${example.note}\n\n\`\`\`sh\n${example.commands}\n\`\`\`\n\n## Related\n\n${example.links + .map((link) => { + if (!labels[link]) throw new Error(`Missing label for ${link}`); + return `- [${labels[link]}](${link})`; + }) + .join( + "\n", + )}\n- [Global options](/cli/#global-flags), including profile selection and non-interactive mode.`; + } + const block = `${start}\n\n${extra}\n\n${end}`; + const body = + slug === "index" + ? source.replace(/\n## /, `\n${block}\n\n## `) + : `${source.trimEnd()}\n\n${block}`; + writeFileSync( + file, + `---\ndescription: ${JSON.stringify(description)}\n---\n\n${body.trimEnd()}\n`, + ); +} +for (const slug of Object.keys(examples)) { + if (!seen.has(slug)) + throw new Error(`Examples reference a missing CLI page: ${slug}`); +} +console.log( + `Enriched ${seen.size} CLI command pages and two reference indexes.`, +); diff --git a/docs/.vitepress/fonts/README.md b/docs/.vitepress/fonts/README.md index f401eef3..3400699e 100644 --- a/docs/.vitepress/fonts/README.md +++ b/docs/.vitepress/fonts/README.md @@ -1,5 +1,7 @@ +# Bundled font + Space Grotesk is bundled for deterministic social image generation without -system fonts or network access during the build. +system fonts or network access during the build. The site also uses this file for display headings through a local `@font-face` rule with `font-display: swap`. Source: [Google Fonts](https://github.com/google/fonts/tree/main/ofl/spacegrotesk) License: SIL Open Font License 1.1, included in OFL.txt. diff --git a/docs/.vitepress/navigation.mjs b/docs/.vitepress/navigation.mjs new file mode 100644 index 00000000..d6d1cb04 --- /dev/null +++ b/docs/.vitepress/navigation.mjs @@ -0,0 +1,205 @@ +const page = (text, link) => ({ text, link }); + +const start = { + text: "Start here", + items: [ + page("What is fnox?", "/guide/what-is-fnox"), + page("Installation", "/guide/installation"), + page("Quick start", "/guide/quick-start"), + page("Connect a vault", "/guide/golden-path"), + page("How fnox works", "/guide/how-it-works"), + ], +}; +const reference = { + text: "Reference", + items: [ + page("Commands", "/cli/"), + page("Configuration", "/reference/configuration"), + page("Environment variables", "/reference/environment"), + page("Troubleshooting", "/guide/troubleshooting"), + page("Contributing", "/contributing"), + ], +}; +const guide = [ + start, + { + text: "Everyday use", + items: [ + page("Run commands", "/cli/exec"), + page("Shell integration", "/guide/shell-integration"), + page("Profiles", "/guide/profiles"), + page("Config files and overrides", "/guide/hierarchical-config"), + page("Missing secrets and defaults", "/guide/missing-secrets"), + page("Import and export", "/guide/import-export"), + page("Terminal dashboard", "/guide/tui"), + ], + }, + { + text: "Caching and credentials", + items: [ + page("Sync a local cache", "/guide/sync"), + page("Cache in memory", "/guide/daemon"), + page("Credential leases", "/guide/leases"), + page("Credential proxy", "/guide/proxy"), + page("MCP server", "/guide/mcp"), + ], + }, + { + text: "Recipes", + items: [ + page("mise tasks", "/guide/mise-integration"), + page("Development to production", "/guide/real-world-example"), + ], + }, + reference, +]; +const providerGroups = [ + [ + "Encryption in config", + [ + ["age", "age"], + ["FIDO2", "fido2"], + ["YubiKey", "yubikey"], + ["AWS KMS", "aws-kms"], + ["Azure Key Vault Keys", "azure-kms"], + ["Google Cloud KMS", "gcp-kms"], + ], + ], + [ + "Cloud and hosted stores", + [ + ["AWS Parameter Store", "aws-ps"], + ["AWS Secrets Manager", "aws-sm"], + ["Azure App Configuration", "azure-ac"], + ["Azure Key Vault Secrets", "azure-sm"], + ["Google Cloud Secret Manager", "gcp-sm"], + ["Bitwarden Secrets Manager", "bitwarden-sm"], + ["Doppler", "doppler"], + ["FOKS", "foks"], + ["HashiCorp Vault", "vault"], + ["Keeper Secrets Manager", "keeper-sm"], + ], + ], + [ + "Password managers and services", + [ + ["1Password", "1password"], + ["Bitwarden", "bitwarden"], + ["Infisical", "infisical"], + ["Passwordstate", "passwordstate"], + ["Proton Pass", "proton-pass"], + ], + ], + [ + "Local stores and defaults", + [ + ["OS keychain", "keychain"], + ["KeePass", "keepass"], + ["password-store", "password-store"], + ["Plaintext defaults", "plain"], + ], + ], +]; +const providers = [ + { + text: "Choose a provider", + items: [page("Provider catalog", "/providers/overview")], + }, + ...providerGroups.map(([text, entries]) => ({ + text, + collapsed: false, + items: entries.map(([name, slug]) => page(name, `/providers/${slug}`)), + })), + reference, +]; +const leases = [ + { + text: "Credential leases", + items: [page("Overview and setup", "/guide/leases")], + }, + { + text: "Backends", + items: [ + ["AWS STS", "aws-sts"], + ["Azure Token", "azure-token"], + ["Cloudflare", "cloudflare"], + ["GCP IAM", "gcp-iam"], + ["GitHub App", "github-app"], + ["GitHub OAuth", "github-oauth"], + ["HashiCorp Vault", "vault"], + ["Custom command", "command"], + ].map(([name, slug]) => page(name, `/leases/${slug}`)), + }, + reference, +]; + +export function sidebar(commands) { + const commandGroups = [ + ["Read and run", ["get", "list", "exec", "check", "export", "tui"]], + [ + "Configure and write", + [ + "init", + "set", + "remove", + "import", + "sync", + "reencrypt", + "edit", + "provider", + "profiles", + "config-files", + ], + ], + [ + "Shell and diagnostics", + [ + "activate", + "deactivate", + "completion", + "doctor", + "scan", + "version", + "sponsors", + ], + ], + ["Caching and agents", ["daemon", "lease", "proxy", "mcp"]], + ]; + const cli = [ + { + text: "CLI reference", + items: [ + page("Command overview", "/cli/"), + page("Global settings", "/cli/configuration"), + ], + }, + ...commandGroups.map(([text, names]) => ({ + text, + items: names.map((name) => { + const children = commands.filter( + (cmd) => cmd[0] === name && cmd.length > 1, + ); + return { + ...page(`fnox ${name}`, `/cli/${name}`), + ...(children.length + ? { + collapsed: true, + items: children.map((cmd) => + page(cmd.join(" "), `/cli/${cmd.join("/")}`), + ), + } + : {}), + }; + }), + })), + reference, + ]; + return { + "/guide/": guide, + "/providers/": providers, + "/leases/": leases, + "/cli/": cli, + "/reference/": [reference, start], + "/": guide, + }; +} diff --git a/docs/.vitepress/theme/EndevSponsors.vue b/docs/.vitepress/theme/EndevSponsors.vue index 6c9a61f4..6a59f4f9 100644 --- a/docs/.vitepress/theme/EndevSponsors.vue +++ b/docs/.vitepress/theme/EndevSponsors.vue @@ -157,6 +157,11 @@ onMounted(async () => { max-width: 120px; object-fit: contain; width: auto; + filter: brightness(0) opacity(0.82); +} + +:global(.dark) .EndevSponsorsLogo img { + filter: none; } .EndevSponsorsCta { diff --git a/docs/.vitepress/theme/HomePage.vue b/docs/.vitepress/theme/HomePage.vue new file mode 100644 index 00000000..a644348c --- /dev/null +++ b/docs/.vitepress/theme/HomePage.vue @@ -0,0 +1,256 @@ + + + diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 656b1d3e..799bf2d8 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -1,207 +1,678 @@ -/** - * Customize default theme styling by overriding CSS variables: - * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css - */ -@import url("https://fonts.googleapis.com/css2?family=Black+Ops+One&display=swap"); - -/** - * Colors - Fort Knox theme (dark blue/gold) - * -------------------------------------------------------------------------- */ - -:root { - /* Brand colors - vault/security theme */ - --vp-c-brand-1: #d4af37; /* Gold */ - --vp-c-brand-2: #c5a028; - --vp-c-brand-3: #b8911a; - --vp-c-brand-soft: rgba(212, 175, 55, 0.14); - - /* Background - dark vault */ - --vp-c-bg: #0a0e1a; - --vp-c-bg-alt: #0f1420; - --vp-c-bg-elv: #161b2e; - --vp-c-bg-soft: #1a2035; - - /* Text */ - --vp-c-text-1: #e8e6e3; - --vp-c-text-2: #c9c6c1; - --vp-c-text-3: #8e8b86; - - /* Borders */ - --vp-c-border: #2d3548; - --vp-c-divider: #232938; - --vp-c-gutter: #1a2035; - - /* Component colors */ - --vp-c-default-1: #2d3548; - --vp-c-default-2: #232938; - --vp-c-default-3: #1a2035; - --vp-c-default-soft: rgba(45, 53, 72, 0.14); - - /* Sidebar */ - --vp-sidebar-bg-color: var(--vp-c-bg-alt); - - /* Code blocks */ - --vp-code-block-bg: #0d1117; - --vp-code-tab-divider: var(--vp-c-border); - --vp-code-copy-code-hover-bg: rgba(212, 175, 55, 0.1); +@font-face { + font-family: "Space Grotesk"; + src: url("../fonts/SpaceGrotesk.ttf") format("truetype"); + font-weight: 300 700; + font-style: normal; + font-display: swap; } -/** - * Component: Button - * -------------------------------------------------------------------------- */ - :root { + --vp-font-family-base: + -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --fnox-font-display: "Space Grotesk", var(--vp-font-family-base); + --vp-c-brand-1: #80600e; + --vp-c-brand-2: #967319; + --vp-c-brand-3: #d4af37; + --vp-c-brand-soft: rgba(160, 124, 24, 0.1); + --vp-c-bg: #fcfbf8; + --vp-c-bg-alt: #f3f1ea; + --vp-c-bg-elv: #fff; + --vp-c-bg-soft: #f2f0e9; + --vp-c-text-1: #222a35; + --vp-c-text-2: #515a65; + --vp-c-text-3: #666e77; + --vp-c-border: #d2d1c9; + --vp-c-divider: #e0ded6; + --vp-c-gutter: #e0ded6; + --vp-sidebar-bg-color: var(--vp-c-bg-alt); + --vp-code-block-bg: #f1efe8; --vp-button-brand-border: transparent; - --vp-button-brand-text: #1a1a1a; - --vp-button-brand-bg: var(--vp-c-brand-1); - --vp-button-brand-hover-border: transparent; - --vp-button-brand-hover-text: #1a1a1a; - --vp-button-brand-hover-bg: var(--vp-c-brand-2); - --vp-button-brand-active-border: transparent; - --vp-button-brand-active-text: #1a1a1a; - --vp-button-brand-active-bg: var(--vp-c-brand-3); + --vp-button-brand-text: #121820; + --vp-button-brand-bg: #e5bf52; + --vp-button-brand-hover-text: #121820; + --vp-button-brand-hover-bg: #f1cf70; + --vp-button-brand-active-text: #121820; + --vp-button-brand-active-bg: #d4af37; + --vp-custom-block-tip-border: var(--vp-c-brand-soft); + --vp-custom-block-tip-bg: var(--vp-c-brand-soft); + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); + --vp-layout-max-width: 1440px; } -/** - * Component: Home - * -------------------------------------------------------------------------- */ - -:root { - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: linear-gradient( - 135deg, - var(--vp-c-brand-1) 0%, - #8b7355 100% - ); - - --vp-home-hero-image-background-image: linear-gradient( - -45deg, - rgba(212, 175, 55, 0.3) 50%, - rgba(45, 53, 72, 0.3) 50% - ); - --vp-home-hero-image-filter: blur(44px); +.dark { + --vp-c-brand-1: #e5bf52; + --vp-c-brand-2: #edcc75; + --vp-c-brand-3: #d4af37; + --vp-c-brand-soft: rgba(229, 191, 82, 0.1); + --vp-c-bg: #0b1018; + --vp-c-bg-alt: #101620; + --vp-c-bg-elv: #151d29; + --vp-c-bg-soft: #151d29; + --vp-c-text-1: #eeeee9; + --vp-c-text-2: #b0b8c4; + --vp-c-text-3: #8c97a7; + --vp-c-border: #323c4a; + --vp-c-divider: #26303d; + --vp-c-gutter: #1c2531; + --vp-code-block-bg: #101620; } -/** - * Component: Custom Block - * -------------------------------------------------------------------------- */ - -:root { - --vp-custom-block-tip-border: transparent; - --vp-custom-block-tip-text: var(--vp-c-text-1); - --vp-custom-block-tip-bg: rgba(212, 175, 55, 0.1); - --vp-custom-block-tip-code-bg: rgba(212, 175, 55, 0.05); +.VPNavBarTitle .title { + font-family: var(--fnox-font-display); + font-size: 25px; + font-weight: 700; + letter-spacing: -1px; } -/** - * Component: Algolia (Search) - * -------------------------------------------------------------------------- */ - -.DocSearch { - --docsearch-primary-color: var(--vp-c-brand-1) !important; - --docsearch-highlight-color: var(--vp-c-brand-1) !important; - --docsearch-text-color: var(--vp-c-text-1) !important; - --docsearch-muted-color: var(--vp-c-text-2) !important; - --docsearch-container-background: rgba(10, 14, 26, 0.8) !important; - --docsearch-modal-background: var(--vp-c-bg-alt) !important; - --docsearch-searchbox-background: var(--vp-c-bg) !important; - --docsearch-searchbox-focus-background: var(--vp-c-bg) !important; - --docsearch-hit-color: var(--vp-c-text-2) !important; - --docsearch-hit-active-color: var(--vp-c-text-1) !important; - --docsearch-hit-background: var(--vp-c-bg) !important; - --docsearch-footer-background: var(--vp-c-bg) !important; +.VPNavBarTitle .logo { + width: 28px; + height: 28px; } - -/** - * Custom styles - * -------------------------------------------------------------------------- */ - -/* Add subtle texture to code blocks */ -.vp-code-group .tabs { - background: linear-gradient(to bottom, rgba(212, 175, 55, 0.05), transparent); +.VPNavBar { + border-bottom: 1px solid var(--vp-c-divider); +} +.VPDoc .content-container { + max-width: 780px !important; +} +.vp-doc { + line-height: 1.8; +} +.vp-doc h1, +.vp-doc h2, +.vp-doc h3 { + font-family: var(--fnox-font-display); +} +.vp-doc h1 { + letter-spacing: -1.1px; + line-height: 1.2; +} +.vp-doc h2 { + letter-spacing: -0.5px; + margin-top: 48px; + padding-top: 28px; +} +.vp-doc h3 { + line-height: 1.4; +} +.vp-doc p, +.vp-doc li { + line-height: 1.8; } - -/* Enhance link styling */ .vp-doc a { - text-decoration: underline; - text-decoration-color: rgba(212, 175, 55, 0.3); - text-underline-offset: 2px; - transition: text-decoration-color 0.2s; + text-underline-offset: 3px; + text-decoration-thickness: 1px; } - -.vp-doc a:hover { - text-decoration-color: var(--vp-c-brand-1); +.vp-doc :not(pre) > code { + overflow-wrap: anywhere; +} +.vp-doc table { + font-size: 14px; +} +.vp-doc th { + background: var(--vp-c-bg-alt); + text-align: left; +} +.vp-doc td, +.vp-doc th { + padding: 11px 16px; +} +.vp-doc div[class*="language-"] { + border: 1px solid var(--vp-c-divider); + border-radius: 10px; } - -/* Sidebar active item */ .VPSidebarItem.is-active > .item > .link { - color: var(--vp-c-brand-1); font-weight: 600; } - -/* Enhanced navbar */ -.VPNavBar { - border-bottom: 1px solid rgba(212, 175, 55, 0.1); - background: rgba(10, 14, 26, 0.8); - backdrop-filter: blur(8px); +:focus-visible { + outline: 2px solid var(--vp-c-brand-1); + outline-offset: 4px; } - -/* Black Ops One branding */ -.VPNavBarTitle .title { - font-family: "Black Ops One", cursive; - font-size: 1.5rem; - color: var(--vp-c-brand-1) !important; +::selection { + background: var(--vp-c-brand-soft); } -.VPHero .name { - font-family: "Black Ops One", cursive !important; - font-size: 4rem !important; - font-weight: 400 !important; +/* Product homepage. Keep these rules out of the documentation layout. */ +.VPHome { + margin-bottom: 0 !important; } - -.VPSocialLinks a[href*="github.com/jdx/fnox"] { - display: inline-flex !important; - flex-direction: column; +.VPHome .vp-doc.container { + max-width: 1200px; + padding-top: 0; +} +.fnox-home { + padding: 0 0 48px; +} +.fnox-home h1, +.fnox-home h2, +.fnox-home h3, +.fnox-home p { + margin: 0; + padding: 0; + border: 0; +} +.fnox-home a { + text-decoration: none; +} +.fnox-home .eyebrow { + color: var(--vp-c-brand-1); + font: 600 11px/1.5 var(--vp-font-family-mono); + letter-spacing: 1.6px; +} +.home-hero { + display: grid; + grid-template-columns: 1.05fr 1fr; + gap: 64px; + align-items: center; + padding: 88px 0 76px; +} +.hero-copy .eyebrow { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 25px; + text-transform: uppercase; +} +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--vp-c-brand-1); + box-shadow: 0 0 0 5px var(--vp-c-brand-soft); +} +.fnox-home h1 { + font-size: clamp(42px, 4.4vw, 62px); + line-height: 1.08; + letter-spacing: -2.8px; + font-weight: 600; +} +.fnox-home h1 span { + color: var(--vp-c-brand-1); +} +.fnox-home .hero-lead { + font-size: 18px; + line-height: 1.75; + color: var(--vp-c-text-2); + margin-top: 26px; + max-width: 470px; +} +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 30px; +} +.home-button { + display: inline-flex; align-items: center; justify-content: center; - gap: 0; - position: relative; - padding-bottom: 12px !important; - margin-bottom: -12px !important; + gap: 20px; + min-height: 48px; + padding: 10px 20px; + border: 1px solid var(--vp-c-border); + border-radius: 7px; + font-size: 14px; + font-weight: 600; + line-height: 1.5; + transition: + background 0.15s, + border-color 0.15s; } - -.VPSocialLinks a[href*="github.com/jdx/fnox"] svg { - display: block !important; - width: 20px; - height: 20px; - margin-top: 2px; +.fnox-home .home-button.primary { + background: var(--vp-button-brand-bg); + border-color: transparent; + color: var(--vp-button-brand-text); } - -.VPSocialLinks .star-count { +.fnox-home .home-button.primary:hover { + background: var(--vp-button-brand-hover-bg); +} +.fnox-home .home-button.secondary { + color: var(--vp-c-text-1); +} +.fnox-home .home-button.secondary:hover { + background: var(--vp-c-bg-soft); + border-color: var(--vp-c-brand-1); +} +.install-command { + display: flex; + align-items: center; + gap: 12px; + margin-top: 28px; + font-size: 13px; +} +.fnox-home .install-command code { + padding: 0; + color: var(--vp-c-text-2); + background: transparent; + font-size: 13px; +} +.install-command button { + border-left: 1px solid var(--vp-c-divider); + padding: 0 12px; + color: var(--vp-c-text-2); + font-size: 12px; + cursor: pointer; +} +.install-command button:hover { + color: var(--vp-c-brand-1); +} +.fnox-home .install-note { + min-height: 20px; + color: var(--vp-c-text-3); + font-size: 12px; + margin-top: 10px; +} +.prompt { + color: var(--vp-c-brand-1); + user-select: none; +} +.hero-demo { + min-width: 0; + border: 1px solid var(--vp-c-border); + border-radius: 12px; + background: var(--vp-c-bg-alt); + box-shadow: 0 24px 64px -32px rgba(0, 0, 0, 0.35); + position: relative; +} +.hero-demo::before { + content: ""; position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - font-size: 0.6rem; - font-weight: 600; + z-index: -1; + inset: -28px; + background-image: radial-gradient(var(--vp-c-border) 1px, transparent 1px); + background-size: 16px 16px; + mask-image: linear-gradient(135deg, transparent, #000 45%, transparent); +} +.demo-topbar { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + padding: 17px 22px; + border-bottom: 1px solid var(--vp-c-divider); +} +.demo-file { + display: flex; + align-items: center; + gap: 10px; + font: 12px var(--vp-font-family-mono); + color: var(--vp-c-text-1); +} +.demo-label { color: var(--vp-c-text-3); - font-family: var(--vp-font-family-mono); - white-space: nowrap; - line-height: 1; + font: 10px/1.4 var(--vp-font-family-mono); + letter-spacing: 1px; } - -.VPSocialLinks .star-count .star-glyph { - font-family: -apple-system, "Segoe UI Symbol", sans-serif; - line-height: 1; - margin-right: 0.2em; +.storage-switch { + display: flex; + gap: 4px; + margin: 18px 22px 0; +} +.storage-switch button { + padding: 5px 10px; + border: 1px solid transparent; + border-radius: 5px; + color: var(--vp-c-text-2); + font-size: 11px; + line-height: 1.7; + cursor: pointer; +} +.storage-switch button[aria-pressed="true"] { + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand-1); + border-color: var(--vp-c-divider); +} +.storage-switch button:hover { + border-color: var(--vp-c-brand-1); +} +.fnox-home .hero-demo pre { + margin: 0; + padding: 20px 22px 26px; + background: transparent; + overflow-x: auto; +} +.fnox-home .hero-demo code { + color: var(--vp-c-text-1); + font-size: 12px; + line-height: 1.95; + white-space: pre; +} +.code-comment { + color: var(--vp-c-text-3); +} +.code-section { + color: var(--vp-c-brand-1); +} +.code-string { + color: #38694d; +} +.dark .code-string { + color: #a7cbb1; +} +.demo-run { + border-top: 1px solid var(--vp-c-divider); + padding: 18px 22px; + background: var(--vp-c-bg); +} +.fnox-home .demo-run pre { + padding: 10px 0 4px; +} +.fnox-home .demo-run p { + color: var(--vp-c-text-2); + font-size: 11px; +} +.fnox-home .demo-run p span { + color: var(--vp-c-brand-1); + padding-right: 7px; +} +.fnox-home .demo-caption { + padding: 11px 22px; + font-size: 11px; + line-height: 1.65; + color: var(--vp-c-text-3); + border-top: 1px solid var(--vp-c-divider); +} +.provider-strip { + border-top: 1px solid var(--vp-c-divider); + border-bottom: 1px solid var(--vp-c-divider); + padding: 22px 0; +} +.strip-label { + font: 10px var(--vp-font-family-mono); + letter-spacing: 1.6px; + color: var(--vp-c-text-3); +} +.provider-strip > div { + display: flex; + flex-wrap: wrap; + gap: 12px 28px; + align-items: center; + margin-top: 12px; +} +.provider-strip a { + color: var(--vp-c-text-2); + font: 500 16px/1.6 var(--fnox-font-display); +} +.provider-strip a:hover { + color: var(--vp-c-brand-1); +} +.provider-strip .all-providers { + color: var(--vp-c-brand-1); + font: 12px var(--vp-font-family-base); + margin-left: auto; +} +.home-section { + padding-top: 80px; +} +.section-heading { + display: flex; + justify-content: space-between; + align-items: end; + gap: 32px; + margin-bottom: 32px; +} +.fnox-home h2 { + font-size: 34px; + line-height: 1.2; + letter-spacing: -1.2px; + font-weight: 500; +} +.fnox-home .eyebrow + h2 { + margin-top: 18px; +} +.section-heading > p { + max-width: 340px; + color: var(--vp-c-text-2); + font-size: 15px; +} +.workflow-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + border: 1px solid var(--vp-c-divider); + border-radius: 10px; + overflow: hidden; +} +.fnox-home .workflow { + display: flex; + flex-direction: column; + padding: 30px; + color: var(--vp-c-text-1); + background: var(--vp-c-bg-alt); +} +.workflow + .workflow { + border-left: 1px solid var(--vp-c-divider); +} +.workflow:hover { + background: var(--vp-c-bg-soft); +} +.workflow-number { + font: 11px/1.5 var(--vp-font-family-mono); + letter-spacing: 1px; + color: var(--vp-c-brand-1); +} +.fnox-home .workflow h3 { + margin: 32px 0 14px; + font-size: 21px; + line-height: 1.3; + letter-spacing: -0.5px; +} +.fnox-home .workflow p { + color: var(--vp-c-text-2); + font-size: 14px; + line-height: 1.8; + margin-bottom: 24px; +} +.fnox-home .text-link { + color: var(--vp-c-brand-1); + font-size: 13px; + font-weight: 600; +} +.text-link span { + margin-left: 8px; +} +.workflow .text-link { + margin-top: auto; +} +.everyday { + display: grid; + grid-template-columns: 1fr 1.3fr; + gap: 72px; +} +.fnox-home .everyday-intro > p:not(.eyebrow) { + color: var(--vp-c-text-2); + font-size: 15px; + margin: 22px 0; + max-width: 320px; +} +.capability-list > a { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 22px 0; + color: var(--vp-c-text-1); + border-bottom: 1px solid var(--vp-c-divider); +} +.capability-list > a:first-child { + padding-top: 0; +} +.fnox-home .capability-list h3 { + font-size: 18px; + font-weight: 500; +} +.fnox-home .capability-list p { + color: var(--vp-c-text-2); + font-size: 14px; + margin-top: 6px; +} +.capability-list > a > span, +.capability-list > a:hover h3 { + color: var(--vp-c-brand-1); +} +.home-start { + display: flex; + gap: 24px; + align-items: center; + margin-top: 80px; + padding: 32px; + background: var(--vp-c-brand-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 10px; +} +.fnox-home .home-start h2 { + font-size: 26px; +} +.fnox-home .home-start p { + margin-top: 7px; + font-size: 14px; + color: var(--vp-c-text-2); +} +.home-start .home-button { + margin-left: auto; + flex-shrink: 0; +} +.home-start img { + flex-shrink: 0; } -.VPSocialLinks a[href*="github.com/jdx/fnox"]:hover .star-count { +.VPSocialLinks .star-count { + font: 10px var(--vp-font-family-mono); + color: var(--vp-c-text-2); + margin-left: 5px; + white-space: nowrap; +} +.VPSocialLinks .star-glyph { + margin-right: 2px; color: var(--vp-c-brand-1); } +.VPSocialLinks a[href*="github.com/jdx/fnox"] { + width: auto; + min-width: 36px; +} -@media (max-width: 640px) { - .VPSocialLinks .star-count { +@media (max-width: 1100px) { + .home-hero { + gap: 32px; + } + .fnox-home h1 { + font-size: 48px; + } + .demo-label { + font-size: 9px; + } + .home-start { + flex-wrap: wrap; + } + .home-start .home-button { + margin-left: 88px; + } +} +@media (max-width: 800px) { + .home-hero { + grid-template-columns: 1fr; + padding-top: 52px; + gap: 38px; + } + .hero-copy { + max-width: 560px; + } + .fnox-home h1 { + font-size: clamp(40px, 7vw, 60px); + } + .hero-demo { + max-width: 600px; + width: 100%; + } + .hero-demo::before { + inset: -16px; + } + .provider-strip .all-providers { + margin-left: 0; + } + .section-heading { + display: block; + } + .section-heading > p { + margin-top: 20px; + max-width: 480px; + } + .workflow-grid { + grid-template-columns: 1fr; + } + .workflow + .workflow { + border-left: 0; + border-top: 1px solid var(--vp-c-divider); + } + .fnox-home .workflow h3 { + margin-top: 20px; + } + .everyday { + grid-template-columns: 1fr; + gap: 36px; + } + .home-section { + padding-top: 56px; + } + .home-start { + margin-top: 56px; + padding: 24px; + } +} +@media (max-width: 480px) { + .fnox-home h1 { + font-size: 40px; + letter-spacing: -1.8px; + } + .fnox-home h2 { + font-size: 29px; + } + .fnox-home .hero-lead { + font-size: 16px; + } + .hero-actions { + flex-direction: column; + align-items: stretch; + } + .demo-topbar { + padding: 14px 16px; + } + .demo-topbar .demo-label { display: none; } + .storage-switch { + margin-left: 12px; + margin-right: 12px; + } + .fnox-home .hero-demo > pre { + padding-left: 16px; + padding-right: 16px; + } + .demo-run { + padding: 16px; + } + .fnox-home .demo-caption { + padding: 12px 16px; + } + .fnox-home .workflow { + padding: 24px; + } + .home-start { + display: block; + } + .home-start img { + margin-bottom: 20px; + } + .home-start .home-button { + margin: 24px 0 0; + width: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + } } diff --git a/docs/README.md b/docs/README.md index d844216a..870fc6d6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,43 +1,70 @@ -# fnox Documentation +# Documentation contributor guide -This directory contains the VitePress documentation for fnox. +The public site is built with VitePress and deployed at [fnox.jdx.dev](https://fnox.jdx.dev). Run the commands below from the repository root. This README is for contributors and is excluded from the published site. -## Local Development +## Local development -```bash -# Install dependencies -aube install +Install the project tools with `mise install`, then: -# Start dev server +```sh +aube install aube run docs:dev +``` -# Build for production -aube run docs:build +Open the URL printed by VitePress. If the default port is occupied, VitePress chooses another port. -# Preview production build +## Build and preview + +```sh +aube run docs:build aube run docs:preview ``` -## Deployment +The production build validates local links and anchors, generates page-specific social images, and checks their metadata. Check the affected pages at desktop and mobile widths, in light and dark themes. For UI changes, verify keyboard focus, navigation, search, and code copying. + +## Where content belongs -The documentation is automatically deployed to GitHub Pages on every push to `main` via the `.github/workflows/docs.yml` workflow. +| Path | Purpose | +| ---------------------------------------------- | -------------------------------------------------------------- | +| `index.md` and `.vitepress/theme/HomePage.vue` | Product landing page and interactive example | +| `guide/` | Setup walkthroughs and task-oriented guides | +| `providers/` | Provider authentication, configuration, references, and limits | +| `leases/` | Temporary-credential backend setup and behavior | +| `reference/` | Configuration fields and environment variables | +| `cli/` | Generated command and runtime-settings reference | +| `.vitepress/navigation.mjs` | Section-specific sidebar organization | +| `.vitepress/theme/style.css` | Shared typography, themes, and responsive landing styles | +| `public/schema.json` | Generated configuration schema | -### First-Time Setup +## Writing conventions -To enable GitHub Pages for this repository: +Lead with the task or behavior, then prerequisites and a working example. Explain what a command reads, writes, or prints when that distinction matters. Link to shared explanations instead of repeating them on every provider page. -1. Go to **Settings** β†’ **Pages** in the GitHub repository -2. Under **Source**, select **GitHub Actions** -3. The workflow will automatically deploy on the next push to `main` +- Use sentence case for prose headings; preserve product names and literal config keys. +- Add a concise `description` in page frontmatter for search and social previews. +- Use root-relative site links such as `/guide/quick-start` in public pages. Keep established paths and anchors, or update every inbound link when changing them. +- Give every code fence a language (`sh`, `toml`, `json`, or `text` for output). +- Make TOML blocks valid syntax, including fragments. Do not repeat the same key to show alternatives in one block. +- Label abbreviated ciphertext and recipients. Prefer `fnox set KEY` with hidden input over examples containing realistic credentials. +- State setup prerequisites and distinguish complete workflows from excerpts. +- Link to vendor pricing instead of maintaining dated price tables. -The documentation will be available at: https://fnox.jdx.dev/ +## Generated reference + +`mise run render:usage` regenerates the CLI pages from the Rust command definitions and the usage specification. It then runs `.vitepress/enrich-cli.mjs` to add maintained examples and related links from `.vitepress/cli-examples.json`, followed by formatting. + +```sh +mise run render:usage +``` + +Edit command descriptions and flags in `src/commands/` or the corresponding usage source. Edit examples and context in `.vitepress/cli-examples.json`. Do not hand-edit generated Markdown: regeneration will replace it. New public commands need an examples entry and a place in the CLI sidebar. + +Regenerate the configuration schema with `mise run render:schema`, or both sets of artifacts with `mise run render`. CI checks that generation produces no diff. + +The changelog is also generated. Preserve published release history; update the release tooling when its format needs to change. + +## Deployment -## Structure +[The docs workflow](../.github/workflows/docs.yml) builds and deploys GitHub Pages on pushes to `main` or a manual workflow dispatch. Local builds do not publish the site. -- `docs/` - Documentation root - - `index.md` - Homepage - - `guide/` - User guides - - `providers/` - Provider-specific documentation - - `reference/` - Reference documentation - - `.vitepress/` - VitePress configuration - - `config.mjs` - Site configuration +For social image maintenance, see [.vitepress/SOCIAL-IMAGES.md](.vitepress/SOCIAL-IMAGES.md). The bundled font and license are described in [.vitepress/fonts/README.md](.vitepress/fonts/README.md). diff --git a/docs/cli/activate.md b/docs/cli/activate.md index aaa101a0..f96c99ed 100644 --- a/docs/cli/activate.md +++ b/docs/cli/activate.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox activate. Enable the hook for your shell." +--- + # `fnox activate` @@ -14,3 +18,22 @@ Output shell activation code to enable automatic secret loading - **`--no-hook-env`** β€” Don't automatically invoke hook-env (for testing) - **`-h --help`** β€” Print help + + + +## Examples + +Enable the hook for your shell. Fish, Nushell, and PowerShell use different activation syntax; follow the shell guide for those commands. + +```sh +eval "$(fnox activate bash)" +# Zsh: +eval "$(fnox activate zsh)" +``` + +## Related + +- [Shell integration](/guide/shell-integration) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/check.md b/docs/cli/check.md index 2e36b55f..d34e3f51 100644 --- a/docs/cli/check.md +++ b/docs/cli/check.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox check. By default, check validates required secrets." +--- + # `fnox check` @@ -11,3 +15,22 @@ Check if all required secrets are defined and configured - **`-a --all`** β€” Check all secrets including those with if_missing=warn or if_missing=ignore - **`-h --help`** β€” Print help + + + +## Examples + +By default, check validates required secrets. Use --all to also check secrets whose missing-value policy is warn or ignore. The command validates resolution without printing resolved values. + +```sh +fnox check --all +fnox check --all --profile production +``` + +## Related + +- [Troubleshooting](/guide/troubleshooting) +- [Missing secrets and defaults](/guide/missing-secrets) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/completion.md b/docs/cli/completion.md index bde04751..eaeef872 100644 --- a/docs/cli/completion.md +++ b/docs/cli/completion.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox completion. Generate completions for the named shell." +--- + # `fnox completion` @@ -13,3 +17,21 @@ Generate shell completions ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Generate completions for the named shell. Install or source the output according to that shell’s completion setup; generating it alone does not change your shell. + +```sh +fnox completion bash > fnox.bash +fnox completion zsh > _fnox +``` + +## Related + +- [Shell integration](/guide/shell-integration) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/config-files.md b/docs/cli/config-files.md index 346fca7e..e5cdb31a 100644 --- a/docs/cli/config-files.md +++ b/docs/cli/config-files.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox config-files. Inspect the effective file stack before debugging an unexpected value." +--- + # `fnox config-files` @@ -9,3 +13,22 @@ List all config files that would be loaded ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Inspect the effective file stack before debugging an unexpected value. An explicit config path skips directory discovery and adjacent local overrides; the global config still loads. + +```sh +fnox config-files +fnox --profile staging config-files +fnox -c ./fnox.toml config-files +``` + +## Related + +- [Config files and overrides](/guide/hierarchical-config) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index b5698b62..bcb600f2 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -1,3 +1,7 @@ +--- +description: "Generated runtime settings for fnox, including profile selection, missing-secret behavior, and key-file settings." +--- + # Configuration @@ -146,3 +150,11 @@ FNOX_SHELL_OUTPUT=none fnox activate bash ```sh FNOX_SHELL_OUTPUT=debug fnox activate zsh ``` + + + +## Configuration file reference + +This page describes generated runtime settings. For the structure of `fnox.toml`, see the [configuration reference](/reference/configuration). For authentication and runtime overrides, see [environment variables](/reference/environment). + + diff --git a/docs/cli/daemon.md b/docs/cli/daemon.md index 9f42c7e7..6293434d 100644 --- a/docs/cli/daemon.md +++ b/docs/cli/daemon.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox daemon. The daemon is opt-in and caches resolved values in memory." +--- + # `fnox daemon` @@ -16,3 +20,22 @@ Manage the per-user daemon - [`fnox daemon start`](/cli/daemon/start.md) - [`fnox daemon status`](/cli/daemon/status.md) - [`fnox daemon stop`](/cli/daemon/stop.md) + + + +## Examples + +The daemon is opt-in and caches resolved values in memory. Enable it in config or with FNOX_DAEMON=on to use it for supported reads. + +```sh +fnox daemon status +fnox daemon start +FNOX_DAEMON=on fnox check --all +``` + +## Related + +- [Daemon caching](/guide/daemon) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/daemon/clear.md b/docs/cli/daemon/clear.md index 066f0126..f5dec918 100644 --- a/docs/cli/daemon/clear.md +++ b/docs/cli/daemon/clear.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox daemon clear. Clear cached values after changing a secret in its source provider." +--- + # `fnox daemon clear` @@ -9,3 +13,21 @@ Clear the caches of all running daemons ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Clear cached values after changing a secret in its source provider. This clears running daemon caches; it does not remove encrypted sync caches. + +```sh +fnox daemon clear +``` + +## Related + +- [Daemon caching](/guide/daemon) +- [Sync a local cache](/guide/sync) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/daemon/start.md b/docs/cli/daemon/start.md index a8cb3e6f..24c8f387 100644 --- a/docs/cli/daemon/start.md +++ b/docs/cli/daemon/start.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox daemon start. Start the per-user daemon." +--- + # `fnox daemon start` @@ -9,3 +13,21 @@ Start the per-user daemon in the background ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Start the per-user daemon. Enable daemon mode in configuration or the environment for subsequent reads to use it. + +```sh +fnox daemon start +fnox daemon status +``` + +## Related + +- [Daemon caching](/guide/daemon) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/daemon/status.md b/docs/cli/daemon/status.md index a0aff00b..2c44dcc7 100644 --- a/docs/cli/daemon/status.md +++ b/docs/cli/daemon/status.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox daemon status. Inspect the current daemon state when diagnosing cache behavior." +--- + # `fnox daemon status` @@ -9,3 +13,21 @@ Show daemon status ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Inspect the current daemon state when diagnosing cache behavior. + +```sh +fnox daemon status +``` + +## Related + +- [Daemon caching](/guide/daemon) +- [Troubleshooting](/guide/troubleshooting) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/daemon/stop.md b/docs/cli/daemon/stop.md index 9706b608..a9774b2f 100644 --- a/docs/cli/daemon/stop.md +++ b/docs/cli/daemon/stop.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox daemon stop. Stop the daemon and discard its in-memory cache." +--- + # `fnox daemon stop` @@ -9,3 +13,20 @@ Stop the daemon ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Stop the daemon and discard its in-memory cache. If daemon mode remains enabled, a later supported read may start it again. + +```sh +fnox daemon stop +``` + +## Related + +- [Daemon caching](/guide/daemon) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/deactivate.md b/docs/cli/deactivate.md index 5016f590..2acaa40f 100644 --- a/docs/cli/deactivate.md +++ b/docs/cli/deactivate.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox deactivate. After shell activation, the fnox shell function applies this command to the current session." +--- + # `fnox deactivate` @@ -16,3 +20,20 @@ run the activation command again for your shell. ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +After shell activation, the fnox shell function applies this command to the current session. Remove the activation line from your startup file to disable it in future shells. + +```sh +fnox deactivate +``` + +## Related + +- [Shell integration](/guide/shell-integration) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index 96d4247f..4d97548c 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox doctor. Check the local fnox setup, then test a specific provider or resolve all configured secrets to narrow down a failure." +--- + # `fnox doctor` @@ -10,3 +14,22 @@ Show diagnostic information about the current fnox state ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Check the local fnox setup, then test a specific provider or resolve all configured secrets to narrow down a failure. + +```sh +fnox doctor +fnox provider test op +fnox check --all +``` + +## Related + +- [Troubleshooting](/guide/troubleshooting) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/edit.md b/docs/cli/edit.md index 31415626..571ee63d 100644 --- a/docs/cli/edit.md +++ b/docs/cli/edit.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox edit. Open configuration in your editor." +--- + # `fnox edit` @@ -9,3 +13,21 @@ Edit the configuration file ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Open configuration in your editor. Configure EDITOR (or VISUAL) first. Use fnox set for an individual value so the selected provider handles storage. + +```sh +EDITOR=vim fnox edit +``` + +## Related + +- [Editor selection](/reference/environment#editor) +- [Set a secret](/cli/set) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/exec.md b/docs/cli/exec.md index fea8dedb..42eaf2c8 100644 --- a/docs/cli/exec.md +++ b/docs/cli/exec.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox exec. Put fnox options before --." +--- + # `fnox exec` @@ -15,3 +19,23 @@ Execute a command with secrets as environment variables - **`--replace`** β€” Replace the fnox process with the command so it keeps the same PID and receives signals directly. Rejected when the command's environment would carry an as_file secret, or when the profile configures credential leases, since fnox must clean those up after the command exits. Unix only - **`-h --help`** β€” Print help + + + +## Examples + +Put fnox options before --. Arguments after -- belong to the child command. To expand an injected variable in a shell expression, defer expansion to a child shell with single quotes. + +```sh +fnox exec -- npm start +fnox exec --profile production --if-missing error -- ./deploy.sh +fnox exec -- sh -c 'test -n "$DATABASE_URL"' +``` + +## Related + +- [How fnox works](/guide/how-it-works) +- [Profiles](/guide/profiles) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/export.md b/docs/cli/export.md index 5b9c7c5d..aed03c76 100644 --- a/docs/cli/export.md +++ b/docs/cli/export.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox export. Exports contain real resolved values." +--- + # `fnox export` @@ -20,3 +24,24 @@ Export secrets in various formats - **`--all`** β€” Include secrets with env = false or env = "exec" (excluded by default) - **`--header`** β€” Include metadata comments in env and shell output - **`-h --help`** β€” Print help + + + +## Examples + +Exports contain real resolved values. JSON, YAML, and TOML wrap them in a secrets object; see the import/export guide before migrating. By default, export follows shell injection settings; `--all` also includes exec-only and non-injected secrets. `--dry-run` only suppresses a file write when `--output` is supplied: output to stdout still contains values. + +```sh +fnox export --format json +# Create a private plaintext file (POSIX shell) +umask 077 +fnox export --output .env +``` + +## Related + +- [Import and export](/guide/import-export) +- [Secret injection settings](/reference/configuration#env-1) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/get.md b/docs/cli/get.md index 055c139e..74450b8c 100644 --- a/docs/cli/get.md +++ b/docs/cli/get.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox get. Print one resolved value to stdout." +--- + # `fnox get` @@ -14,3 +18,22 @@ Get a secret value - **`--base64-decode`** β€” Base64 decode the secret - **`-h --help`** β€” Print help + + + +## Examples + +Print one resolved value to stdout. Use check --all when you only need to verify access without displaying the secret. + +```sh +fnox get DATABASE_URL +fnox get DATABASE_URL --profile staging +``` + +## Related + +- [How fnox works](/guide/how-it-works) +- [Check secret access](/cli/check) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/import.md b/docs/cli/import.md index ddd01699..2e5b843e 100644 --- a/docs/cli/import.md +++ b/docs/cli/import.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox import. Configure an encryption provider first." +--- + # `fnox import` @@ -25,3 +29,22 @@ Import secrets from various sources - **`--filter `** β€” Only import matching secrets (regex pattern) - **`--prefix `** β€” Prefix to add to imported secret names - **`-h --help`** β€” Print help + + + +## Examples + +Configure an encryption provider first. Remote storage providers are not import targets. When stdin carries the import data, use --force because stdin cannot also answer the confirmation prompt. + +```sh +fnox import --input .env --provider age --dry-run +fnox import --input .env --provider age +cat secrets.json | fnox import json --provider age --force +``` + +## Related + +- [Import and export](/guide/import-export) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/index.md b/docs/cli/index.md index 47e32791..3880608f 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -1,3 +1,7 @@ +--- +description: "Find fnox commands for reading secrets, running applications, configuring providers, and managing caches and temporary credentials." +--- + # `fnox` @@ -8,6 +12,22 @@ - **Usage:** `fnox [FLAGS] ` + + +## Choose a command + +| Task | Commands | +| ---------------- | ---------------------------------------------------------------------------------------------------------- | +| Read and run | [get](/cli/get), [list](/cli/list), [exec](/cli/exec), [check](/cli/check) | +| Store or migrate | [set](/cli/set), [import](/cli/import), [export](/cli/export), [sync](/cli/sync) | +| Configure | [init](/cli/init), [provider](/cli/provider), [profiles](/cli/profiles), [config-files](/cli/config-files) | +| Diagnose | [doctor](/cli/doctor), [scan](/cli/scan) | +| Cache and broker | [daemon](/cli/daemon), [lease](/cli/lease), [proxy](/cli/proxy), [mcp](/cli/mcp) | + +New to fnox? Follow the [quick start](/guide/quick-start). Put fnox options before the `--` separator in commands such as `fnox exec -- npm start`. + + + ## Global Flags - **`-c --config `** β€” Path to the configuration file (default: fnox.toml, searches parent directories) diff --git a/docs/cli/init.md b/docs/cli/init.md index 8d926d30..51bbfe62 100644 --- a/docs/cli/init.md +++ b/docs/cli/init.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox init. In a terminal, the wizard can configure a provider and select it as the default." +--- + # `fnox init` @@ -13,3 +17,21 @@ Initialize a new fnox configuration file - **`-g --global`** β€” Initialize the global config file (~/.config/fnox/config.toml) - **`--skip-wizard`** β€” Skip the interactive wizard and create a minimal config - **`-h --help`** β€” Print help + + + +## Examples + +In a terminal, the wizard can configure a provider and select it as the default. --skip-wizard creates a minimal config; it does not enable encryption. Configure a provider before storing sensitive values. + +```sh +fnox init +fnox init --skip-wizard +``` + +## Related + +- [Quick start](/guide/quick-start) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/lease.md b/docs/cli/lease.md index c503d7b0..bf888d70 100644 --- a/docs/cli/lease.md +++ b/docs/cli/lease.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox lease. Create and manage temporary credentials from configured lease backends." +--- + # `fnox lease` @@ -16,3 +20,21 @@ Manage ephemeral credential leases - [`fnox lease create [FLAGS] [BACKEND_NAME]`](/cli/lease/create.md) - [`fnox lease list [--active] [--expired]`](/cli/lease/list.md) - [`fnox lease revoke `](/cli/lease/revoke.md) + + + +## Examples + +Create and manage temporary credentials from configured lease backends. fnox exec can create or reuse a lease automatically. + +```sh +fnox lease list --active +fnox exec -- aws s3 ls +``` + +## Related + +- [Credential leases](/guide/leases) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/lease/cleanup.md b/docs/cli/lease/cleanup.md index d706fcc1..2e653e17 100644 --- a/docs/cli/lease/cleanup.md +++ b/docs/cli/lease/cleanup.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox lease cleanup. Ask backends to clean up expired tracked leases that require manual revocation." +--- + # `fnox lease cleanup` @@ -9,3 +13,20 @@ Revoke all expired leases that need manual cleanup ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Ask backends to clean up expired tracked leases that require manual revocation. Backend credentials and network access may be needed. + +```sh +fnox lease cleanup +``` + +## Related + +- [Credential leases](/guide/leases) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/lease/create.md b/docs/cli/lease/create.md index 2cf25ec5..294954cf 100644 --- a/docs/cli/lease/create.md +++ b/docs/cli/lease/create.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox lease create. Create a lease for a configured backend." +--- + # `fnox lease create` @@ -26,3 +30,21 @@ Create a short-lived credential lease from a secret **Default:** `fnox-lease` - **`-h --help`** β€” Print help + + + +## Examples + +Create a lease for a configured backend. Without a backend name, all configured backends are selected. Output contains usable temporary credentials; avoid putting it in logs. + +```sh +fnox lease create aws --duration 30m +fnox lease create aws --interactive +``` + +## Related + +- [Credential leases](/guide/leases) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/lease/list.md b/docs/cli/lease/list.md index f0ac0991..1c3d1b80 100644 --- a/docs/cli/lease/list.md +++ b/docs/cli/lease/list.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox lease list. Inspect the local lease ledger." +--- + # `fnox lease list` @@ -11,3 +15,21 @@ List tracked leases - **`--active`** β€” Show only active (non-expired, non-revoked) leases - **`--expired`** β€” Show only expired leases - **`-h --help`** β€” Print help + + + +## Examples + +Inspect the local lease ledger. This is a record of leases tracked by fnox, not a complete inventory of credentials at the service. + +```sh +fnox lease list --active +fnox lease list --expired +``` + +## Related + +- [Lease caching](/guide/leases#how-caching-works) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/lease/revoke.md b/docs/cli/lease/revoke.md index b482f5d5..fef0a1ce 100644 --- a/docs/cli/lease/revoke.md +++ b/docs/cli/lease/revoke.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox lease revoke. Use an ID from lease list." +--- + # `fnox lease revoke` @@ -13,3 +17,21 @@ Revoke a lease by ID ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Use an ID from lease list. Remote revocation depends on the backend; native-expiry backends cannot invalidate a token immediately through fnox. + +```sh +fnox lease list --active +fnox lease revoke LEASE_ID +``` + +## Related + +- [Lease backend capabilities](/guide/leases#supported-backends) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/list.md b/docs/cli/list.md index 47ba0408..9c93fc90 100644 --- a/docs/cli/list.md +++ b/docs/cli/list.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox list. Lists the secrets configured in fnox." +--- + # `fnox list` @@ -13,3 +17,23 @@ List all secrets - **`-s --sources`** β€” Show source file paths where secrets are defined - **`-V --values`** β€” Show secret values (if available) - **`-h --help`** β€” Print help + + + +## Examples + +Lists the secrets configured in fnox. It does not enumerate every remote vault item. Values are hidden unless you request --values. + +```sh +fnox list +fnox list --sources +fnox list --profile production +``` + +## Related + +- [Config files and overrides](/guide/hierarchical-config) +- [Terminal dashboard](/cli/tui) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index 62b45059..5e6f96ba 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox mcp. Run a stdio MCP server from the project directory." +--- + # `fnox mcp` @@ -9,3 +13,21 @@ Start an MCP server that brokers secrets to AI agents ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Run a stdio MCP server from the project directory. Configure its secret allowlist and authenticate to providers before starting the client. + +```sh +fnox mcp +fnox --profile staging mcp +``` + +## Related + +- [MCP server](/guide/mcp) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/profiles.md b/docs/cli/profiles.md index 4e343ac2..5b19b48b 100644 --- a/docs/cli/profiles.md +++ b/docs/cli/profiles.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox profiles. Inspect the available profile names before selecting or composing them." +--- + # `fnox profiles` @@ -9,3 +13,21 @@ List available profiles ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Inspect the available profile names before selecting or composing them. Use --write-profile when writing with multiple active profiles. + +```sh +fnox profiles +fnox -P aws,staging list +``` + +## Related + +- [Profiles](/guide/profiles) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/provider.md b/docs/cli/provider.md index d138d306..ed271a63 100644 --- a/docs/cli/provider.md +++ b/docs/cli/provider.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox provider. Manage configured provider instances." +--- + # `fnox provider` @@ -16,3 +20,21 @@ Manage providers (defaults to list) - [`fnox provider list`](/cli/provider/list.md) - [`fnox provider remove [-g --global] `](/cli/provider/remove.md) - [`fnox provider test [-a --all] [PROVIDER]`](/cli/provider/test.md) + + + +## Examples + +Manage configured provider instances. The instance name, such as op, is distinct from the provider type, such as 1password. + +```sh +fnox provider list +fnox provider test op +``` + +## Related + +- [Provider catalog](/providers/overview) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/provider/add.md b/docs/cli/provider/add.md index af025051..78e63e2e 100644 --- a/docs/cli/provider/add.md +++ b/docs/cli/provider/add.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox provider add. Creates a provider configuration." +--- + # `fnox provider add` @@ -19,3 +23,22 @@ Add a new provider - **`-g --global`** β€” Add to the global config file (~/.config/fnox/config.toml) - **`--vault `** β€” Default Proton Pass vault name (only valid with provider type proton-pass) - **`-h --help`** β€” Print help + + + +## Examples + +Creates a provider configuration. Some types require interactive setup; others create placeholders that you must replace before use. For this command, the aws and gcp choices correspond to the aws-sm and gcp-sm config types. + +```sh +fnox provider add op 1password +fnox provider add sync-age age --global +``` + +## Related + +- [Provider catalog](/providers/overview) +- [Connect a vault](/guide/golden-path) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/provider/list.md b/docs/cli/provider/list.md index ed895ce6..5da1f954 100644 --- a/docs/cli/provider/list.md +++ b/docs/cli/provider/list.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox provider list. List configured provider instances for the selected profile." +--- + # `fnox provider list` @@ -10,3 +14,21 @@ List available providers ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +List configured provider instances for the selected profile. To find the supported provider types, use the provider catalog. + +```sh +fnox provider list +fnox --profile production provider list +``` + +## Related + +- [Provider catalog](/providers/overview) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/provider/remove.md b/docs/cli/provider/remove.md index f8fbde75..4b12214c 100644 --- a/docs/cli/provider/remove.md +++ b/docs/cli/provider/remove.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox provider remove. Remove a provider definition from the configuration." +--- + # `fnox provider remove` @@ -15,3 +19,20 @@ Remove a provider - **`-g --global`** β€” Remove from the global config file (~/.config/fnox/config.toml) - **`-h --help`** β€” Print help + + + +## Examples + +Remove a provider definition from the configuration. Update secrets that reference it before removing it; this command does not delete the remote vault or its items. + +```sh +fnox provider remove old-vault +``` + +## Related + +- [Provider catalog](/providers/overview) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/provider/test.md b/docs/cli/provider/test.md index 470a7778..27cd5c69 100644 --- a/docs/cli/provider/test.md +++ b/docs/cli/provider/test.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox provider test. Use a configured instance name, not its type." +--- + # `fnox provider test` @@ -15,3 +19,22 @@ Test a provider connection - **`-a --all`** β€” Test all configured providers - **`-h --help`** β€” Print help + + + +## Examples + +Use a configured instance name, not its type. A successful connection test does not guarantee that every referenced item exists; check --all validates those references too. + +```sh +fnox provider test op +fnox provider test --all +fnox check --all +``` + +## Related + +- [Troubleshooting](/guide/troubleshooting) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/proxy.md b/docs/cli/proxy.md index 6dabf2f1..0df57031 100644 --- a/docs/cli/proxy.md +++ b/docs/cli/proxy.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox proxy. Configure destination rules before launching a command." +--- + # `fnox proxy` @@ -14,3 +18,21 @@ Broker credentials into destination-scoped HTTPS requests - [`fnox proxy rules`](/cli/proxy/rules.md) - [`fnox proxy run [COMMAND]…`](/cli/proxy/run.md) + + + +## Examples + +Configure destination rules before launching a command. The proxy controls credential injection; it is not an operating-system sandbox. + +```sh +fnox proxy rules +fnox proxy run -- node agent.js +``` + +## Related + +- [Credential proxy](/guide/proxy) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/proxy/rules.md b/docs/cli/proxy/rules.md index 8ab1c87c..2627b085 100644 --- a/docs/cli/proxy/rules.md +++ b/docs/cli/proxy/rules.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox proxy rules. Inspect effective rules without resolving their secrets." +--- + # `fnox proxy rules` @@ -9,3 +13,21 @@ Show the effective credential proxy rules ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Inspect effective rules without resolving their secrets. Confirm domains, methods, and paths before launching the client. + +```sh +fnox proxy rules +fnox --profile staging proxy rules +``` + +## Related + +- [Credential proxy](/guide/proxy) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/proxy/run.md b/docs/cli/proxy/run.md index 57709ce3..11187e05 100644 --- a/docs/cli/proxy/run.md +++ b/docs/cli/proxy/run.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox proxy run. The child receives placeholders and proxy/CA settings." +--- + # `fnox proxy run` @@ -13,3 +17,20 @@ Run a command with placeholder credentials through the local proxy ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +The child receives placeholders and proxy/CA settings. Its HTTP client must honor those settings. Configure proxy rules before running this command. + +```sh +fnox proxy run -- node agent.js +``` + +## Related + +- [Credential proxy](/guide/proxy) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/reencrypt.md b/docs/cli/reencrypt.md index c9b8acf6..0411abb6 100644 --- a/docs/cli/reencrypt.md +++ b/docs/cli/reencrypt.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox reencrypt. After changing recipients, re-encrypt with an identity that can still decrypt the existing ciphertext." +--- + # `fnox reencrypt` @@ -19,3 +23,22 @@ When you add or remove recipients from an encryption provider (e.g. age), existi - **`-p --provider `** β€” Only re-encrypt secrets from this provider - **`--filter `** β€” Only re-encrypt matching secrets (regex pattern) - **`-h --help`** β€” Print help + + + +## Examples + +After changing recipients, re-encrypt with an identity that can still decrypt the existing ciphertext. Repeat for each affected profile. This changes the encryption, not the underlying secret value. + +```sh +fnox reencrypt --provider age --dry-run +fnox reencrypt --provider age +fnox reencrypt --provider age --profile staging +``` + +## Related + +- [Update age recipients](/providers/age#adding-a-new-team-member) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/remove.md b/docs/cli/remove.md index 6f16a7fd..b001ad00 100644 --- a/docs/cli/remove.md +++ b/docs/cli/remove.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox remove. Remove a secret definition from fnox configuration." +--- + # `fnox remove` @@ -16,3 +20,22 @@ Remove a secret - **`-g --global`** β€” Remove from the global config file (~/.config/fnox/config.toml) - **`-n --dry-run`** β€” Show what would be removed without making changes - **`-h --help`** β€” Print help + + + +## Examples + +Remove a secret definition from fnox configuration. The command does not delete or revoke the original value in a remote provider. + +```sh +fnox remove OLD_TOKEN --dry-run +fnox remove OLD_TOKEN +``` + +## Related + +- [Set a secret](/cli/set) +- [Profiles](/guide/profiles) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/scan.md b/docs/cli/scan.md index b19706f0..efb5e1cb 100644 --- a/docs/cli/scan.md +++ b/docs/cli/scan.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox scan. Scan files for potential secrets using heuristics." +--- + # `fnox scan` @@ -23,3 +27,22 @@ Scan repository for potential secrets - **`-q --quiet`** β€” Show only files with potential secrets - **`-h --help`** β€” Print help + + + +## Examples + +Scan files for potential secrets using heuristics. Findings need review; a clean scan does not prove that files or git history contain no secrets. + +```sh +fnox scan . +fnox scan . --format json +fnox scan . --ignore "vendor/**" +``` + +## Related + +- [Plaintext defaults](/providers/plain) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/set.md b/docs/cli/set.md index 458b269c..79f5d7c4 100644 --- a/docs/cli/set.md +++ b/docs/cli/set.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox set. Configure a provider first." +--- + # `fnox set` @@ -31,3 +35,23 @@ Set a secret value **Choices:** `error`, `warn`, `ignore` - **`-h --help`** β€” Print help + + + +## Examples + +Configure a provider first. Omitting VALUE prompts with hidden input in a terminal or reads piped stdin. Without a selected provider, set writes a plaintext default. --from-file preserves file contents, including trailing newlines. + +```sh +fnox set DATABASE_URL --provider age +fnox set SSH_PRIVATE_KEY --provider age --from-file ~/.ssh/id_ed25519 +fnox set DATABASE_URL --profile staging --provider age +``` + +## Related + +- [Quick start](/guide/quick-start) +- [File secrets](/reference/configuration#as-file) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/sponsors.md b/docs/cli/sponsors.md index 72a551a5..b65b5a0d 100644 --- a/docs/cli/sponsors.md +++ b/docs/cli/sponsors.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox sponsors. Show project sponsors in the terminal." +--- + # `fnox sponsors` @@ -9,3 +13,20 @@ Show the companies sponsoring fnox and the jdx.dev open source tools ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Show project sponsors in the terminal. + +```sh +fnox sponsors +``` + +## Related + +- [Contributing](/contributing) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/sync.md b/docs/cli/sync.md index 367d24ae..b892ccec 100644 --- a/docs/cli/sync.md +++ b/docs/cli/sync.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox sync. Use a configured encryption provider as the target." +--- + # `fnox sync` @@ -20,3 +24,22 @@ Sync secrets from remote providers to a local encryption provider - **`--filter `** β€” Only sync matching secrets (regex pattern) - **`--local-file`** β€” Write synced secrets to the local override file next to the config file: fnox.local.toml, or .fnox.local.toml when that file exists or the config is .fnox.toml - **`-h --help`** β€” Print help + + + +## Examples + +Use a configured encryption provider as the target. --local-file writes a personal cache next to the project config; add that file to .gitignore. Run sync again after source values change. + +```sh +fnox sync --provider sync-age --local-file --dry-run +fnox sync --provider sync-age --local-file +fnox sync --provider sync-age --local-file DATABASE_URL +``` + +## Related + +- [Sync a local cache](/guide/sync) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/tui.md b/docs/cli/tui.md index b4b648c9..69b051ce 100644 --- a/docs/cli/tui.md +++ b/docs/cli/tui.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox tui. Browse and copy values in the terminal." +--- + # `fnox tui` @@ -9,3 +13,21 @@ Interactive TUI dashboard for managing secrets ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Browse and copy values in the terminal. Dashboard edits are held in memory only; use set or remove to persist changes. + +```sh +fnox tui +fnox --profile staging tui +``` + +## Related + +- [Terminal dashboard](/guide/tui) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/cli/version.md b/docs/cli/version.md index d7a496d8..28b4034e 100644 --- a/docs/cli/version.md +++ b/docs/cli/version.md @@ -1,3 +1,7 @@ +--- +description: "Usage and examples for fnox version. Include the version when reporting a bug or comparing local and CI behavior." +--- + # `fnox version` @@ -10,3 +14,21 @@ Show version information ## Flags - **`-h --help`** β€” Print help + + + +## Examples + +Include the version when reporting a bug or comparing local and CI behavior. + +```sh +fnox version +fnox --version +``` + +## Related + +- [Troubleshooting](/guide/troubleshooting) +- [Global options](/cli/#global-flags), including profile selection and non-interactive mode. + + diff --git a/docs/contributing.md b/docs/contributing.md index 89ccd932..4c759480 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,72 +1,61 @@ -# Contributing - -Thank you for your interest in contributing to fnox. +--- +description: "Set up a fnox development checkout, run checks, edit documentation, and prepare a contribution for review." +--- -## Contribution Expectations +# Contributing -Before opening a PR, unless it is something obvious, consider creating a -discussion or mentioning what you plan to do in -[Discord](https://discord.gg/UBa7pJUN7Z). The important part is to settle the -direction before much review happens. fnox has a specific scope and design -taste. I am comfortable saying no to changes that do not clearly fit. +Contributions should solve a clear problem within fnox's scope. For a substantial change, discuss the direction first in [GitHub Discussions](https://github.com/jdx/fnox/discussions) or [Discord](https://discord.gg/UBa7pJUN7Z). Small, obvious fixes can go straight to a pull request. -Before I review a PR, CI must be passing and all automated AI review comments -must be addressed. If those are still open, assume I will wait to look at the -PR. +## What to expect in review -If I am on the fence about a contribution, I will probably reject it for that -reason alone. If I did not do this, fnox would suffer from feature bloat. I -may also reject a PR if the quality is poor enough that I do not have confidence -the contributor can get it across the finish line. I do not have time to coach -contributors. +CI must pass and automated review comments must be addressed before maintainer review. Explain the problem, the resulting behavior, and how you verified it. -I get hundreds of PRs per week across my projects, so I do not have time to -respond to every PR with detailed context. A rejection may be brief. +fnox has a deliberate scope and design direction. A change may be declined because it does not fit, introduces too much complexity, or is not ready for review. Maintainer time is limited across many projects, so a rejection may be brief and detailed coaching may not be available. -## Code Style +## Development setup -fnox uses [hk](https://hk.jdx.dev) for linting and formatting. Run the checks -before opening a PR: +Clone the repository, install its tools, and build the debug binary: ```sh -hk check --all -hk fix --all +git clone https://github.com/jdx/fnox +cd fnox +mise install +mise run build ``` -fnox also exposes these as the wrapper tasks `mise run lint` and -`mise run lint-fix`; prefer those. +Use the repository's mise tasks so tool versions and the Cargo build wrapper are consistent. See [CONTRIBUTING.md](https://github.com/jdx/fnox/blob/main/CONTRIBUTING.md#mbx-build-cache) for the mbx cache and bypass procedure when the wrapper fails. -## Commit and PR Titles +## Checks -Use Conventional Commits for commit messages and PR titles. Examples: +```sh +mise run test:cargo +mise run build +mise run test:bats -- test/init.bats +mise run lint +``` -- `fix: handle missing config file` -- `docs: clarify installation steps` -- `feat: add quiet output mode` +Build before running Bats tests; `test:bats` uses the existing binary. Use `mise run test` for both test suites and `mise run ci` for the full set of build, test, and lint tasks. Some provider tests require credentials or a local service; see the [test guide](https://github.com/jdx/fnox/blob/main/test/README.md). -## Testing +Run `mise run lint-fix` to apply formatting fixes. Run tests appropriate to the behavior you changed. -Run the relevant tests for the code you changed, and the full CI-style task when -practical: +## Documentation changes ```sh -mise run test:cargo # Rust unit and integration tests -mise run test:bats # End-to-end CLI tests (builds first) -mise run test # Both -mise run ci # Build, test, and lint +aube install +aube run docs:dev +aube run docs:build ``` -Run `mise tasks` or check `mise.toml` for the complete list. +Review the affected page in a browser, including narrow layouts and both themes when changing styles. The production build checks internal links, anchors, and social metadata. -## Development +CLI pages are generated. Update their source help or the maintained examples, then regenerate them; see the [documentation contributor guide](https://github.com/jdx/fnox/blob/main/docs/README.md#generated-reference). -Install project tools with mise, then build: +## Commit and pull request titles -```sh -mise install -mise run build -``` +Use Conventional Commits with a lowercase, imperative description: + +- `fix(aws-sm): handle missing secrets` +- `docs: clarify installation steps` +- `feat(exec): add a command option` -Run the lint and test tasks above before opening a PR. See -[CONTRIBUTING.md](https://github.com/jdx/fnox/blob/main/CONTRIBUTING.md) in the -repository for notes on the mbx build cache. +Follow the [repository conventions](https://github.com/jdx/fnox/blob/main/AGENTS.md) for accepted types and scopes, MSRV, dependency changes, and AI assistance disclosure. Keep dependency updates focused and do not raise the MSRV to accommodate a dependency. diff --git a/docs/guide/daemon.md b/docs/guide/daemon.md index 72b4ff83..679c7f70 100644 --- a/docs/guide/daemon.md +++ b/docs/guide/daemon.md @@ -1,10 +1,14 @@ -# Per-User Daemon +--- +description: "Enable the optional fnox daemon to cache resolved secrets in memory, inspect its status, and clear stale values." +--- + +# Cache secrets in memory The fnox daemon keeps resolved secrets in memory for your user session. It is useful when your config points at remote providers such as 1Password, Bitwarden, AWS Secrets Manager, or Vault and repeated `fnox get`, `fnox exec`, or shell hook refreshes feel slow. The daemon is opt-in. fnox does not use it unless you enable it in config or set `FNOX_DAEMON=on`. -## Enable It +## Enable it Add a top-level `[daemon]` section: @@ -41,7 +45,7 @@ Or disable it for a shell/session: export FNOX_DAEMON=off ``` -## What Uses It +## What uses it Daemon-backed resolution applies to read-oriented commands: @@ -50,7 +54,7 @@ Daemon-backed resolution applies to read-oriented commands: - `fnox hook-env` - `fnox export` - `fnox list --values` -- `fnox check` +- `fnox check --all` - `fnox tui` - `fnox mcp` - `fnox proxy run` @@ -58,24 +62,24 @@ Daemon-backed resolution applies to read-oriented commands: Mutation and admin commands still resolve directly, including `sync`, `reencrypt`, `edit`, `set`, `remove`, `provider`, and `lease create`. -## Cache Behavior +## Cache behavior The daemon cache is memory-only. Secret values are not written to disk by the daemon. -Cached values are discarded when: +Remote changes do not automatically invalidate cached values. Run `fnox daemon clear` after changing a secret in its source provider. Cached values are also discarded when: - You run `fnox daemon clear`, which clears all running profile-scoped daemon caches - You run `fnox daemon stop` - The daemon exits after its idle timeout - Config files, profile settings, provider references, post-processing options, or relevant `FNOX_*` and provider environment variables change -`fnox check` uses the daemon connection when daemon mode is enabled, but it does not reuse cached secret values. It still contacts providers so it can validate the current state. +`fnox check --all` uses the daemon connection when daemon mode is enabled, but it does not reuse cached secret values. It still contacts providers so it can validate the current state. Secrets with `env = false` are not resolved during normal batch environment injection. They can still be resolved explicitly, such as with `fnox get SECRET_NAME`. -## Opt Out Per Secret Or Provider +## Opt out per secret or provider -Set `daemon_cache = false` on a secret that should always resolve directly: +Set `daemon_cache = false` on a secret that should be resolved again on each request: ```toml [secrets] @@ -93,7 +97,7 @@ daemon_cache = false This disables cache reuse for those values. If daemon mode is enabled, fnox still talks to the daemon for supported read commands, but those entries are resolved again for every request. -## Security Model +## Security model The daemon is Unix-first and uses a Unix domain socket. It does not listen on TCP. @@ -101,13 +105,13 @@ The socket is created in a user-owned runtime directory with strict permissions. On unsupported platforms, daemon mode returns a clear unsupported error. Use `--no-daemon` or `FNOX_DAEMON=off` to force direct resolution. -## Daemon Vs Sync +## Daemon vs sync Use the daemon when you want faster repeated reads during a session and are comfortable keeping resolved values in memory. Use [syncing secrets locally](/guide/sync) when you want an encrypted local cache that survives restarts and can work offline. -## Next Steps +## Next steps - [Shell Integration](/guide/shell-integration) - Auto-load secrets on `cd` - [Syncing Secrets Locally](/guide/sync) - Store an encrypted local cache diff --git a/docs/guide/golden-path.md b/docs/guide/golden-path.md index 2b9edbf5..56503c89 100644 --- a/docs/guide/golden-path.md +++ b/docs/guide/golden-path.md @@ -1,6 +1,10 @@ -# Golden Path Setup +--- +description: "Connect a 1Password vault, create a personal encrypted cache with age, and use it from your shell." +--- -A complete, zero-to-working walkthrough of [the golden path](/guide/what-is-fnox#the-golden-path): secrets live in 1Password, `fnox.toml` commits only references to them, and `fnox sync` caches everything locally under a personal age key so day-to-day loads are instant and offline. +# Connect a vault and cache locally + +Keep secrets in 1Password, commit their references in `fnox.toml`, and use `fnox sync` to create an encrypted personal cache. Daily reads then use your local age key without contacting 1Password. This is [the golden path](/guide/what-is-fnox#the-golden-path) for teams with an existing vault. The same recipe works with any remote provider β€” swap 1Password for [AWS Secrets Manager](/providers/aws-sm), [Bitwarden](/providers/bitwarden), [Doppler](/providers/doppler), or any other [remote provider](/providers/overview). @@ -10,7 +14,7 @@ The same recipe works with any remote provider β€” swap 1Password for [AWS Secre - The [1Password CLI](https://developer.1password.com/docs/cli/) installed and signed in (`op signin`) - `age` installed (`brew install age` / `apt install age`) -## Step 1: One-Time Machine Setup +## Step 1: one-time machine setup Create a personal age key and a machine-wide `sync-age` provider. You do this once per machine, then reuse it in every project: @@ -39,7 +43,7 @@ key_file = "~/.config/fnox/age.txt" Instead of a key file on disk, the age key can live in [Apple's Secure Enclave (Touch ID)](/guide/sync#apple-secure-enclave-touch-id), a [YubiKey](/guide/sync#yubikey), or a [TPM or FIDO2 token](/guide/sync#tpm-and-fido2). Only this step changes β€” everything below stays the same. ::: -## Step 2: Put Secrets in 1Password +## Step 2: put secrets in 1Password The vault is the single source of truth. Use existing items, or create them: @@ -50,13 +54,13 @@ op item create --category=login --vault=Engineering --title=Stripe \ 'secret-key=sk_live_...' ``` -## Step 3: Commit References in fnox.toml +## Step 3: commit references in fnox.TOML In the project, reference the 1Password items β€” no secret material goes into git: ```bash cd my-api -fnox init +fnox init --skip-wizard ``` ```toml @@ -75,10 +79,10 @@ Make sure the local cache never gets committed: ```bash echo "fnox.local.toml" >> .gitignore git add fnox.toml .gitignore -git commit -m "add fnox config" +git commit -m "chore: add fnox config" ``` -## Step 4: Sync +## Step 4: sync Pull every secret from 1Password once and cache it locally, re-encrypted to your personal age key: @@ -88,14 +92,14 @@ fnox sync --provider sync-age --local-file This writes the encrypted values into the gitignored `fnox.local.toml`. From now on fnox decrypts locally instead of calling 1Password β€” see [Syncing Secrets Locally](/guide/sync) for exactly what this looks like on disk. -## Step 5: Enable Shell Integration +## Step 5: enable shell integration ```bash # Add to your shell profile -eval "$(fnox activate zsh)" # or bash, fish +eval "$(fnox activate zsh)" ``` -Entering the project now loads secrets instantly, offline, with no 1Password calls: +For other shells and startup file locations, see [shell integration](/guide/shell-integration). Entering the project now loads secrets from the local cache: ```bash ~/projects $ cd my-api @@ -103,21 +107,21 @@ fnox: +2 DATABASE_URL, STRIPE_KEY ~/projects/my-api $ ``` -## Day-to-Day +## Keep the cache current -**A secret changed in 1Password?** Re-sync: +The cache does not refresh automatically. After a secret changes in 1Password, re-sync: ```bash fnox sync --provider sync-age --local-file --force ``` -**A teammate joins?** They do Step 1 once on their machine, clone the repo, and run Step 4. Their cache is encrypted to their own key β€” nothing is shared except the vault. +**Onboard a teammate:** grant vault access, then have them complete Step 1 on their machine, clone the repo, and run Step 4. Their cache is encrypted to their own key β€” nothing is shared except the vault. -**A new secret?** Add the item to 1Password, add its reference to `fnox.toml`, commit, and everyone re-syncs. +**Add a secret:** Add the item to 1Password, add its reference to `fnox.toml`, commit, and everyone re-syncs. -## What About CI? +## What about CI? -Don't sync in CI β€” the cache is a per-developer convenience, and a synced key sitting in CI defeats its purpose. Instead, let CI authenticate to the vault directly: +In CI, authenticate to the vault directly. Install fnox and the 1Password CLI before this workflow step: ```yaml # GitHub Actions @@ -129,7 +133,7 @@ Don't sync in CI β€” the cache is a per-developer convenience, and a synced key The committed `fnox.toml` references resolve straight from 1Password using the [service account token](https://developer.1password.com/docs/service-accounts/). Alternatively, keep a separate set of [age-encrypted secrets in git](/providers/age) for CI. -## Next Steps +## Next steps - [Syncing Secrets Locally](/guide/sync) - Everything `fnox sync` can do, including hardware-backed keys - [Real-World Setup](/guide/real-world-example) - An alternative workflow with encrypted secrets in git diff --git a/docs/guide/hierarchical-config.md b/docs/guide/hierarchical-config.md index a496f841..26fa103f 100644 --- a/docs/guide/hierarchical-config.md +++ b/docs/guide/hierarchical-config.md @@ -1,12 +1,16 @@ -# Hierarchical Configuration +--- +description: "Share configuration across directories and understand how global, project, profile, and local files override one another." +--- -fnox searches parent directories for `fnox.toml` (or `.fnox.toml`) files and merges them. This is perfect for monorepos and multi-service projects. +# Hierarchical configuration -## How It Works +fnox searches parent directories for `fnox.toml` (or `.fnox.toml`) files and merges them. Use this to share providers and common settings across a monorepo. + +## How it works fnox builds configuration by merging multiple sources, starting with the global config and walking up the directory tree: -``` +```text project/ β”œβ”€β”€ fnox.toml # Root config β”œβ”€β”€ fnox.local.toml # Root local overrides (optional) @@ -29,9 +33,9 @@ When you run fnox from `project/services/api/`, the merge order is (lowest to hi Each level merges the main config, any profile-specific file (`fnox..toml`), and local overrides, with child configs taking precedence over parent configs, and profile and local files taking precedence over the main config at the same level. Global config provides the base layer available to all projects. -## Example Setup +## Example setup -### Root Config (Common Secrets) +### Root config (common secrets) ```toml # project/fnox.toml @@ -45,7 +49,7 @@ ENVIRONMENT = { default = "development" } JWT_SECRET = { provider = "age", value = "encrypted-shared-jwt..." } ``` -### API Service Config +### API service config ```toml # project/services/api/fnox.toml @@ -56,7 +60,7 @@ DATABASE_URL = { provider = "age", value = "encrypted-api-db..." } LOG_LEVEL = { default = "debug" } # Override shared secret - more verbose for API during dev ``` -### Worker Service Config +### Worker service config ```toml # project/services/worker/fnox.toml @@ -66,12 +70,14 @@ QUEUE_URL = { provider = "age", value = "encrypted-queue-url..." } WORKER_CONCURRENCY = { default = "4" } ``` -## Resulting Secrets +## Resulting secrets + +The comments below summarize the resolved configuration; the CLI displays a table of names, providers, and sources. From `project/services/api/`: ```bash -fnox list +fnox list --sources # ENVIRONMENT=development (from root) # JWT_SECRET=*** (from root) # LOG_LEVEL=debug (from api, overrides root) @@ -82,7 +88,7 @@ fnox list From `project/services/worker/`: ```bash -fnox list +fnox list --sources # ENVIRONMENT=development (from root) # JWT_SECRET=*** (from root) # LOG_LEVEL=info (from root) @@ -90,7 +96,7 @@ fnox list # WORKER_CONCURRENCY=4 (from worker) ``` -## Imports vs Hierarchy +## Imports vs hierarchy **Hierarchy** (automatic): @@ -108,7 +114,7 @@ import = ["./shared/secrets.toml", "./envs/dev.toml"] Use hierarchy for location-based config (monorepos). Use imports for cross-cutting concerns (shared secret bundles). -## Local Overrides +## Local overrides Use `fnox.local.toml` for user-specific overrides without committing to version control: @@ -140,17 +146,17 @@ EOF - Use explicit paths to bypass parent configs and local overrides: `fnox -c ./fnox.toml get SECRET` (the file's own `import`s and the global config are still loaded) - `fnox sync --local-file` only supports `fnox.toml` and `.fnox.toml`. Other config filenames are rejected because adjacent local override files are not loaded. -## Global Configuration +## Global configuration -For machine-wide secrets that apply to all projects, use the global config: +For machine-wide secrets that apply to all projects, use the global config. Configure the `age` provider and its recipient before running these write commands: ```bash # Initialize global config fnox init --global # Add secrets to global config -fnox set GITHUB_TOKEN "ghp_..." --global -fnox set NPM_TOKEN "npm_..." --global +fnox set GITHUB_TOKEN --global --provider age +fnox set NPM_TOKEN --global --provider age # Add providers to global config fnox provider add age age --global @@ -176,7 +182,7 @@ fnox provider add age age --global - **Use `root = true` to stop recursion:** Prevents searching parent directories (but not global config) - **Use dotfiles to declutter:** `.fnox.toml` works the same as `fnox.toml` (same for `.fnox.local.toml`, `.fnox.staging.toml`, etc.) -## Next Steps +## Next steps - [Profiles](/guide/profiles) - Multi-environment management - [Real-World Example](/guide/real-world-example) - See it all together diff --git a/docs/guide/how-it-works.md b/docs/guide/how-it-works.md index f31f07c9..070b4804 100644 --- a/docs/guide/how-it-works.md +++ b/docs/guide/how-it-works.md @@ -1,85 +1,72 @@ -# How It Works +--- +description: "Understand configuration loading, secret resolution, environment injection, and the three kinds of fnox cache." +--- -fnox uses a simple TOML config file (`fnox.toml`) that you check into git. +# How fnox works -## Two Storage Modes +fnox maps a named secret in `fnox.toml` to a resolved value, then passes that value to a command, a shell, or stdout. Providers decide where values come from; profiles and config layering decide which definitions apply. -Secrets can be stored in two ways: +## From configuration to command -### 1. Encrypted Inline +When you run `fnox exec -- npm start`: -The encrypted ciphertext lives directly in the config file: +1. **Load configuration.** Merge the global configuration and project files, including applicable profile and local files. +2. **Select definitions.** Apply the selected profiles and their inheritance in order. +3. **Resolve values.** Decrypt ciphertext, fetch provider references, or use defaults and existing environment variables. +4. **Prepare the environment.** Apply field selectors, create temporary files for `as_file` secrets, and obtain configured credential leases. +5. **Run the command.** Start `npm start` with the resolved environment and clean up temporary resources when it exits. -```toml -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } - -[secrets] -DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC4uLg==" } # ← encrypted, safe to commit -``` - -**Providers:** age, aws-kms, azure-kms, gcp-kms +`fnox exec` does not change the parent shell. It inherits the existing environment and adds the configured values according to each secret's [`env` setting](/reference/configuration#env-1). -### 2. Remote References +## Providers and references -The config contains only a reference to a secret stored remotely: +A provider has an instance name, such as `op`, and a type, such as `1password`: ```toml -[providers] -aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp/" } +[providers.op] +type = "1password" +vault = "Engineering" [secrets] -DATABASE_URL = { provider = "aws", value = "database-url" } # ← Just a reference, actual secret in AWS +DATABASE_URL = { provider = "op", value = "Database/url" } +LOG_LEVEL = { default = "info" } ``` -**Providers:** aws-sm, azure-sm, gcp-sm, vault, 1password, bitwarden, keychain +`DATABASE_URL` is the environment variable name. `op` selects the configured provider. `Database/url` identifies the item and field in 1Password. -## Secret Resolution Order +For encryption providers such as age, the `value` field holds ciphertext generated by `fnox set`, rather than an item reference. You do not need to construct ciphertext by hand. -When fnox resolves a secret, it checks in this order: +Local stores such as the OS keychain and KeePass also use references; a reference does not necessarily imply a network request. Conversely, a cloud KMS provider needs network access even though its ciphertext lives in your config. -1. **Provider value** β€” either an encrypted value (`provider = "age"`, `value = "encrypted..."`) or a remote reference (`provider = "aws"`, `value = "secret-name"`) -2. **Default value** (`default = "fallback"`), also used as a fallback if the provider lookup fails -3. **Environment variable** (if no provider or default value is available) +## Resolution order -First match wins. Note that a provider error is only survivable when a `default` is -configured: without one, the error is returned immediately and fnox does **not** fall -back to the environment. +For an individual secret, fnox tries: -## Example Config +1. **Provider value.** When a [`sync` cache](/guide/sync) exists, its encryption provider and ciphertext take the place of the original provider lookup. +2. **Default.** A `default` value can also supply a fallback when a provider fails. Defaults can [reference other secrets](/reference/configuration#default). +3. **Existing environment variable.** Used when no provider value or default is available. -```toml -# Provider definitions -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } -aws = { type = "aws-sm", region = "us-east-1" } +An individual provider error without a default returns an error before the environment fallback. Commands that resolve multiple secrets apply each secret's [`if_missing` policy](/guide/missing-secrets) to decide whether to stop, warn, or skip it. -[secrets] -JWT_SECRET = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC4uLg==" } # Encrypted secret (in git) -DATABASE_URL = { provider = "aws", value = "prod-database-url" } # Remote secret (in AWS) -NODE_ENV = { default = "development" } # Default value (fallback) -``` +After resolution, `json_path` or `line` can select part of a provider value. These selectors are mutually exclusive. -## Execution Flow +## Three kinds of cache -When you run `fnox exec -- `: +| Cache | Stores | Lifetime | Refresh | +| ----------------------------------------------- | ---------------------------------------- | ----------------------------------------------- | -------------------------------- | +| [Sync](/guide/sync) | Encrypted values in a local config file | Survives restarts | Run `fnox sync` again | +| [Daemon](/guide/daemon) | Resolved values in process memory | Until cleared, invalidated, or the daemon exits | `fnox daemon clear` | +| [Lease ledger](/guide/leases#how-caching-works) | Temporary credentials and lease metadata | Until expiry or revocation | A new lease is created as needed | -1. fnox reads `fnox.toml` from the current directory (and parent directories) -2. Resolves all secrets based on the active profile -3. Decrypts encrypted secrets or fetches remote secrets -4. Exports all secrets as environment variables -5. Executes your command with those env vars +These serve different purposes. Sync supports offline use with a local encryption provider. The opt-in daemon avoids repeated reads during a session. Leases replace long-lived credentials with temporary ones when the backend supports expiry. -If the [per-user daemon](/guide/daemon) is enabled, supported read commands resolve through a same-user Unix socket and can reuse memory-only cached values. Direct resolution remains available with `--no-daemon`. +## Inspect a configuration -For agent workloads, the [credential proxy](/guide/proxy) can pass placeholders -to a child and inject real credentials only into destination-scoped HTTPS -requests. - -## Next Steps +```sh +fnox config-files # Which files are loaded? +fnox profiles # Which profiles exist? +fnox list --sources # Where was each secret defined? +fnox check --all # Can the configured secrets be resolved? +``` -- [Profiles](/guide/profiles) - Manage multiple environments -- [Providers](/providers/overview) - Choose the right provider for your needs -- [Per-User Daemon](/guide/daemon) - Speed up repeated provider reads -- [Credential Proxy](/guide/proxy) - Broker credentials for agent workloads -- [Shell Integration](/guide/shell-integration) - Auto-load secrets in your shell +See [troubleshooting](/guide/troubleshooting) when the result differs from what you expect. diff --git a/docs/guide/import-export.md b/docs/guide/import-export.md index c207c6a4..cd804c9d 100644 --- a/docs/guide/import-export.md +++ b/docs/guide/import-export.md @@ -1,15 +1,19 @@ -# Import / Export +--- +description: "Import secrets from environment files or structured data, export resolved values, and migrate between providers." +--- -fnox can import secrets from various formats and export them for use in other tools. +# Import and export + +Import existing values into an encryption provider, or export resolved secrets for another tool. Exported values are plaintext; fnox does not turn them into dummy examples. Import requires an encryption provider (`-p`/`--provider`), such as `age`, so that imported values are encrypted before they are written to the config file. Remote storage providers (1Password, AWS Secrets Manager, etc.) are not yet supported as import targets. -## Import from Files +## Import from files -### From .env Files +### From .env files ```bash # Import from .env file, encrypting with the "age" provider @@ -43,7 +47,7 @@ API_KEY=sk_test_abc123 EOF ``` -### From Different Formats +### From different formats ```bash # JSON @@ -72,9 +76,9 @@ DATABASE_URL: postgresql://localhost/mydb API_KEY: sk_test_abc123 ``` -## Import Options +## Import options -### With Provider +### With provider The provider encrypts secrets during import. It must be an encryption provider defined in your config (for example `age`, `aws-kms`, or a hardware-backed @@ -88,7 +92,7 @@ fnox import -i .env --provider age fnox import -i .env --provider kms ``` -### With Filters +### With filters Import only specific secrets: @@ -100,7 +104,7 @@ fnox import -i .env --provider age --filter "^DATABASE_" fnox import -i .env --provider age --filter "^API_" ``` -### With Prefix +### With prefix Add a prefix to all imported secrets: @@ -112,7 +116,7 @@ fnox import -i .env --provider age --prefix "MYAPP_" # API_KEY becomes MYAPP_API_KEY ``` -### Combining Options +### Combining options ```bash # Import DB secrets with encryption and prefix @@ -125,9 +129,9 @@ fnox import -i .env \ # DATABASE_PASSWORD β†’ PROD_DATABASE_PASSWORD (encrypted with age) ``` -## Export Secrets +## Export secrets -### Export Formats +### Export formats ```bash # Export as .env format (default) @@ -149,7 +153,21 @@ fnox export --format toml fnox export --header ``` -### Save to File +JSON, YAML, and TOML exports wrap the values in a `secrets` object and include `metadata` with the profile, export time, and count. Imports expect a flat name-to-value map instead. To produce JSON that can be imported again, extract the values with [jq](https://jqlang.org/manual/): + +```sh +fnox export --format json | jq '.secrets' +``` + +For `as_file = true` secrets, export returns paths to temporary files rather than their contents. Export is therefore not a complete backup of configuration or file secrets. + +### Save to file + +Create private output files and keep them out of version control. In a POSIX shell, `umask 077` restricts permissions on newly created files: + +```sh +umask 077 +``` ```bash # Export to file @@ -160,7 +178,9 @@ fnox export --format yaml > secrets.yaml fnox export --format toml > secrets.toml ``` -### Export with Profile +`--dry-run` suppresses writing only when you also provide `--output`. Without an output path, export still prints the resolved values to stdout. + +### Export with profile ```bash # Export production secrets @@ -170,72 +190,53 @@ fnox export --profile production > .env.production fnox export --profile staging --format json > staging.json ``` -## Migration Workflows +## Migration workflows -### From .env to fnox with Encryption +### From .env to fnox -```bash -# 1. Set up age provider -cat >> fnox.toml << 'EOF' -[providers.age] -type = "age" -recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] -EOF +First [configure an age provider](/guide/quick-start), then preview and import: -# 2. Import and encrypt all secrets +```sh +fnox import -i .env --provider age --dry-run fnox import -i .env --provider age - -# 3. Remove .env file (secrets now encrypted in fnox.toml) -rm .env +fnox check --all +fnox exec -- npm start ``` -### From fnox to .env (for legacy tools) - -```bash -# Export current secrets to .env -fnox export > .env -``` +Review the encrypted configuration before committing. Keep `.env` ignored, and remove the plaintext file when the application no longer needs it. -### Between Providers +### From a remote provider to local encryption -```bash -# 1. Export from AWS Secrets Manager -fnox export --profile production --format json > prod-secrets.json - -# 2. Switch to age provider -cat >> fnox.toml << 'EOF' -[providers.age] -type = "age" -recipients = ["age1..."] -EOF +To retain remote references and make a personal cache, use [`fnox sync`](/guide/sync). -# 3. Re-import with new provider -fnox import -i prod-secrets.json json --provider age +To migrate values into an encryption provider permanently, export and import through a pipe in Bash or Zsh: -# 4. Verify -fnox list +```sh +fnox export --profile production --format json | + jq '.secrets' | + fnox import json --provider age --force ``` -### Team Onboarding - -```bash -# 1. Export example secrets (with dummy values) -fnox export --format json > secrets.example.json +This example requires `jq` and an age provider in the destination profile. It writes into the import command's active profile. Select `--profile` explicitly on each command when source and destination differ. Export follows shell injection settings; add `--all` only when you intend to include `env = false` and `env = "exec"` secrets. -# 2. Team member fills in real values -cp secrets.example.json secrets.json -# Edit secrets.json with real credentials +### Create an onboarding template -# 3. Import with encryption -fnox import -i secrets.json json --provider age +Write dummy values explicitly. `fnox export` returns real resolved values, even if the output filename contains `example`: -# 4. Delete plaintext file -rm secrets.json +```json +{ + "DATABASE_URL": "postgresql://localhost/example", + "API_KEY": "replace-me" +} ``` -## CI/CD Integration +A teammate can fill in a private copy and import it with `fnox import -i secrets.json json --provider age`. -### GitHub Actions Secrets β†’ fnox +## CI/CD integration + +The examples below assume fnox, the target encryption provider, and the destination tools are already configured. + +### GitHub Actions secrets β†’ fnox ```yaml # .github/workflows/setup-secrets.yml @@ -259,6 +260,8 @@ jobs: ### fnox β†’ Docker Compose +For Compose environment interpolation, you can avoid a plaintext file with `fnox exec -- docker compose up`. If your service explicitly uses `env_file`, create that file with restricted permissions: + ```bash # Export for docker-compose fnox export > .env @@ -268,7 +271,7 @@ fnox export > .env # - .env ``` -### fnox β†’ Kubernetes Secrets +### fnox β†’ Kubernetes secrets ```bash # Create Kubernetes secret from .env-format output @@ -276,7 +279,7 @@ kubectl create secret generic app-secrets \ --from-env-file=<(fnox export) ``` -## Next Steps +## Next steps - [Providers](/providers/overview) - Choose providers for your secrets - [Profiles](/guide/profiles) - Organize secrets by environment diff --git a/docs/guide/installation.md b/docs/guide/installation.md index f7fc05fb..c9e52a1d 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -1,38 +1,64 @@ +--- +description: "Install fnox with mise, Cargo, or a prebuilt binary, then verify and update your installation." +--- + # Installation -## Using mise (recommended) +Install the fnox CLI, verify it is on your `PATH`, then [configure your first provider](/guide/quick-start). + +## With mise -The easiest way to install fnox is with [mise](https://mise.jdx.dev): +If you use [mise](https://mise.jdx.dev), install fnox globally: -```bash +```sh mise use -g fnox ``` -This installs fnox globally and keeps it up to date. +To manage the version in a project, run `mise use fnox` from that project instead. Commit the resulting mise configuration so teammates use the same version. + +To update a mise-managed installation: + +```sh +mise upgrade fnox +``` + +Installing fnox does not automatically upgrade it on every run. See [mise upgrade](https://mise.jdx.dev/cli/upgrade.html) for version constraints and update options. -## Using Cargo +## With cargo -If you have Rust installed: +With a supported Rust toolchain installed: -```bash -cargo install fnox +```sh +cargo install fnox --locked ``` -## From Source +This builds from source using the published lockfile. Ensure Cargo's binary directory (`~/.cargo/bin` on Unix) is on your `PATH`. Check the package's `rust-version` in [Cargo.toml](https://github.com/jdx/fnox/blob/main/Cargo.toml) for the minimum Rust version. -```bash +Run the same command again to install a newer published release. + +## Prebuilt binaries + +Download the archive for your operating system and architecture from [GitHub Releases](https://github.com/jdx/fnox/releases). Extract `fnox` into a directory on your `PATH`. + +Provider requirements are separate from fnox installation. For example, 1Password needs `op`, Bitwarden needs `bw`, and generating an age key needs `age-keygen`. Each [provider guide](/providers/overview) lists its prerequisites. + +## From a checkout + +```sh git clone https://github.com/jdx/fnox cd fnox -cargo install --path . +cargo install --path . --locked ``` -## Verify Installation +For development, use the debug build and project tasks in the [contributing guide](/contributing). + +## Verify the installation -```bash +```sh fnox --version +fnox --help ``` -## Next Steps +If the shell cannot find fnox, check that your install directory is on `PATH`, or that mise is [activated](https://mise.jdx.dev/getting-started.html). Open a new terminal after changing shell configuration. -- [Quick Start](/guide/quick-start) - Get started with fnox in 5 minutes -- [Shell Integration](/guide/shell-integration) - Set up automatic secret loading +Continue with the [quick start](/guide/quick-start) for local encryption or [connect a vault](/guide/golden-path). diff --git a/docs/guide/leases.md b/docs/guide/leases.md index 879d7bb3..397f3cca 100644 --- a/docs/guide/leases.md +++ b/docs/guide/leases.md @@ -1,10 +1,14 @@ -# Credential Leases +--- +description: "Create temporary credentials, connect lease backends, and understand caching, expiry, and revocation." +--- -Credential leases let you vend short-lived credentials from cloud providers like AWS, GCP, Azure, and HashiCorp Vault. Instead of storing long-lived access keys, fnox creates temporary credentials that expire automatically. +# Credential leases -## Why Leases? +Credential leases create temporary credentials from a configured backend. fnox uses an existing identity or bootstrap credential to request the lease, then injects the resulting credentials into `fnox exec` subprocesses. Expiry and revocation depend on the backend. -Long-lived credentials are a security risk. If they leak, an attacker has access until someone rotates them. Leases flip this model: credentials are created on demand, last minutes to hours, and expire on their own. +## Why leases? + +A lease limits how long a credential remains useful when the backend enforces an expiry. It does not remove the need to protect the identity that creates leases. Custom commands and some Vault responses can produce credentials without expiry; check the resulting lease metadata. fnox supports three approaches depending on your security requirements: @@ -12,13 +16,13 @@ fnox supports three approaches depending on your security requirements: 2. **Hardware-protected** β€” store master credentials encrypted on disk, requiring a physical security key (YubiKey or FIDO2) to decrypt 3. **Prompt-based** β€” never store master credentials on the machine; paste them in when needed -## Approach 1: Stored Master Credentials +## Approach 1: stored master credentials This is the simplest setup. You store the long-lived credentials (e.g., an AWS IAM user's access key) in a fnox provider, and fnox uses them to create short-lived leases automatically via `fnox exec`. Any provider works here. Choose based on your security requirements: -- **1Password / Bitwarden** β€” requires authentication (password, biometric, or service account token) to access secrets. Best when you want a gate on every session. +- **1Password / Bitwarden** β€” use the vault's authentication and access policy. Prompt frequency depends on its session settings. - **OS Keychain** β€” unlocked at login on most systems. Convenient but offers no additional prompt after login on Linux. macOS may prompt for Touch ID/password on first access. - **Age / KMS** β€” encrypted in git. Good for CI and shared team setups. @@ -33,8 +37,8 @@ vault = "Development" # Long-lived IAM credentials stored in 1Password [secrets] -AWS_ACCESS_KEY_ID = { provider = "op", value = "AWS IAM/access key" } -AWS_SECRET_ACCESS_KEY = { provider = "op", value = "AWS IAM/secret key" } +AWS_ACCESS_KEY_ID = { provider = "op", value = "AWS IAM/access key", env = false } +AWS_SECRET_ACCESS_KEY = { provider = "op", value = "AWS IAM/secret key", env = false } # Lease: use those credentials to assume a role and get temp creds [leases.aws] @@ -59,15 +63,16 @@ You can also use `keychain` if you prefer convenience over per-session authentic ```toml [providers.keychain] type = "keychain" +service = "fnox" [secrets] -AWS_ACCESS_KEY_ID = { provider = "keychain" } -AWS_SECRET_ACCESS_KEY = { provider = "keychain" } +AWS_ACCESS_KEY_ID = { provider = "keychain", env = false } +AWS_SECRET_ACCESS_KEY = { provider = "keychain", env = false } ``` ```bash -fnox set AWS_ACCESS_KEY_ID "AKIA..." -fnox set AWS_SECRET_ACCESS_KEY "wJalr..." +fnox set AWS_ACCESS_KEY_ID --provider keychain +fnox set AWS_SECRET_ACCESS_KEY --provider keychain ``` The temporary credentials are cached in the lease ledger and reused until they're close to expiring (within 5 minutes of expiry). When they expire, fnox automatically creates a new lease. @@ -82,7 +87,7 @@ type = "1password" vault = "Development" [secrets] -GOOGLE_APPLICATION_CREDENTIALS = { provider = "op", value = "GCP Service Account/key file", as_file = true } +GOOGLE_APPLICATION_CREDENTIALS = { provider = "op", value = "GCP Service Account/key file", as_file = true, env = false } [leases.gcp] type = "gcp-iam" @@ -105,7 +110,7 @@ type = "1password" vault = "Infrastructure" [secrets] -VAULT_TOKEN = { provider = "op", value = "Vault/token" } +VAULT_TOKEN = { provider = "op", value = "Vault/token", env = false } [leases.vault-aws] type = "vault" @@ -129,18 +134,18 @@ type = "1password" vault = "Development" [secrets] -AZURE_CLIENT_ID = { provider = "op", value = "Azure SP/client id" } -AZURE_CLIENT_SECRET = { provider = "op", value = "Azure SP/client secret" } -AZURE_TENANT_ID = { provider = "op", value = "Azure SP/tenant id" } +AZURE_CLIENT_ID = { provider = "op", value = "Azure SP/client id", env = false } +AZURE_CLIENT_SECRET = { provider = "op", value = "Azure SP/client secret", env = false } +AZURE_TENANT_ID = { provider = "op", value = "Azure SP/tenant id", env = false } [leases.azure] type = "azure-token" scope = "https://management.azure.com/.default" ``` -## Approach 2: Hardware-Protected Master Credentials +## Approach 2: hardware-protected master credentials -This approach stores master credentials encrypted on disk with a hardware security key required for decryption. It combines the convenience of Approach 1 (no manual paste step each session) with stronger security β€” decryption is physically impossible without the key. +This approach stores master credentials encrypted on disk with a hardware security key required for decryption. It combines the convenience of Approach 1 (no manual paste step each session) with stronger security β€” the hardware is needed to derive the decryption key. Derived keys and resolved values still enter process memory during use. fnox supports two hardware provider types: @@ -198,17 +203,20 @@ Key points: ```bash # 1. Create the hardware-backed provider (choose one) -fnox provider add secure yubikey # YubiKey HMAC-SHA1 -fnox provider add secure fido2 # Any FIDO2 key +fnox -c fnox.local.toml provider add secure yubikey +# Or, for a key with hmac-secret support: +fnox -c fnox.local.toml provider add secure fido2 # 2. Store your master credentials (requires key tap) -fnox set AWS_ACCESS_KEY_ID "AKIA..." --provider secure -fnox set AWS_SECRET_ACCESS_KEY "wJalr..." --provider secure +fnox -c fnox.local.toml set AWS_ACCESS_KEY_ID --provider secure +fnox -c fnox.local.toml set AWS_SECRET_ACCESS_KEY --provider secure ``` +Set `env = false` on both stored master secrets in `fnox.local.toml`, as shown above. The explicit `-c` paths in setup keep those definitions in the local file. + ### Daily workflow -```bash +```text $ fnox exec -- aws s3 ls Tap your YubiKey... # β†’ Derives encryption key from hardware device (one tap per session) @@ -220,7 +228,7 @@ Tap your YubiKey... The hardware key tap only happens once per `fnox exec` invocation, even when multiple secrets use the same provider. After the lease is cached, subsequent `fnox exec` calls reuse it without prompting until it's close to expiring. -## Approach 3: Prompt-Based (No Stored Credentials) +## Approach 3: prompt-based (no stored credentials) This approach is ideal for remote machines, shared servers, or environments where you don't want master credentials persisted to disk at all. Instead of storing credentials in a provider, you paste them in when `fnox lease create` prompts you. @@ -278,27 +286,29 @@ When the lease expires, run `fnox lease create aws -i` again and paste fresh cre If you run `fnox exec` without having created a lease and without stored master credentials, it skips the lease gracefully: -``` +```text Skipping lease 'aws': AWS credentials not found. Run 'aws sso login' or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. Run 'fnox lease create -i aws' to set up credentials interactively. ``` The subprocess still runs β€” just without the lease credentials. This means other secrets and leases that _are_ available will still be injected. -## Supported Backends +## Supported backends -| Backend | Type | Max Duration | Revocation | -| ------------------------------------ | -------------- | ------------ | ----------------------- | -| [AWS STS](/leases/aws-sts) | `aws-sts` | 12 hours | No-op (native TTL) | -| [GCP IAM](/leases/gcp-iam) | `gcp-iam` | 1 hour | No-op (native TTL) | -| [Azure Token](/leases/azure-token) | `azure-token` | ~1 hour | No-op (native TTL) | -| [HashiCorp Vault](/leases/vault) | `vault` | 24 hours | Vault lease revocation | -| [Cloudflare](/leases/cloudflare) | `cloudflare` | 24 hours | Token deletion | -| [GitHub App](/leases/github-app) | `github-app` | 1 hour | No-op (native TTL) | -| [GitHub OAuth](/leases/github-oauth) | `github-oauth` | ~8 hours | No-op (native TTL) | -| [Custom Command](/leases/command) | `command` | 24 hours | Optional revoke command | +These are the backend limits documented by fnox. Account policies or the service can impose a shorter lifetime. Revoking a ledger entry does not necessarily invalidate the remote credential immediately. -## Managing Leases +| Backend | Type | Max Duration | Revocation | +| ------------------------------------ | -------------- | ------------ | ----------------------------- | +| [AWS STS](/leases/aws-sts) | `aws-sts` | 12 hours | No-op (native TTL) | +| [GCP IAM](/leases/gcp-iam) | `gcp-iam` | 1 hour | No-op (native TTL) | +| [Azure Token](/leases/azure-token) | `azure-token` | ~1 hour | No-op (native TTL) | +| [HashiCorp Vault](/leases/vault) | `vault` | 24 hours | Vault lease revocation | +| [Cloudflare](/leases/cloudflare) | `cloudflare` | 24 hours | Token deletion | +| [GitHub App](/leases/github-app) | `github-app` | 1 hour | Installation token revocation | +| [GitHub OAuth](/leases/github-oauth) | `github-oauth` | ~8 hours | No-op (native TTL) | +| [Custom Command](/leases/command) | `command` | 24 hours | Optional revoke command | + +## Managing leases ```bash # List active leases @@ -314,7 +324,7 @@ fnox lease revoke fnox lease cleanup ``` -## How Caching Works +## How caching works fnox caches lease credentials in a per-project ledger file (`~/.local/state/fnox/leases/.toml`). Cached leases are reused until: @@ -323,3 +333,9 @@ fnox caches lease credentials in a per-project ledger file (`~/.local/state/fnox - They're explicitly revoked The ledger automatically prunes entries that have been expired or revoked for more than 24 hours. + +## Next steps + +- [Lease commands](/cli/lease): create, inspect, revoke, and clean up leases. +- [Hardware providers](/providers/fido2): protect bootstrap credentials at rest. +- [Configuration reference](/reference/configuration#lease-backend-settings): place leases in profiles. diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md index 1f678a87..10be3ab1 100644 --- a/docs/guide/mcp.md +++ b/docs/guide/mcp.md @@ -1,26 +1,33 @@ -# MCP Server +--- +description: "Expose selected secrets and command execution through the fnox MCP server, with explicit tool and secret allowlists." +--- + +# MCP server `fnox mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio, allowing AI agents like Claude Code to access secrets without having them directly in the environment. -## Why? +## Access model -When you give an AI agent `GITHUB_TOKEN` as an environment variable, it can use that token however it wants. The MCP server acts as a **session-scoped secret broker** β€” secrets are resolved on first access and cached in memory for the session. +The server resolves selected secrets on first access and caches them in memory for the session. `get_secret` returns values directly. `exec` runs a command with the allowed secrets and returns its output. Neither tool is an operating-system sandbox; configure the allowlist for the task. -## Quick Setup +## Quick setup ### 1. Configure secrets normally ```toml # fnox.toml -[providers] -age = { type = "age" } +[providers.op] +type = "1password" +vault = "Engineering" [secrets] -GITHUB_TOKEN = { provider = "age", value = "AGE-SECRET-KEY-..." } -API_KEY = { provider = "age", value = "AGE-SECRET-KEY-..." } +GITHUB_TOKEN = { provider = "op", value = "GitHub/agent-token", env = "exec" } +API_KEY = { provider = "op", value = "Service/api-key", env = "exec" } ``` -### 2. (Optional) Configure which tools to expose +Authenticate to the provider before starting the MCP server; it runs non-interactively. The values above are references to existing vault items. + +### 2. Configure which tools to expose ```toml [mcp] @@ -84,7 +91,9 @@ Retrieves a single secret by name. The agent provides the secret name (must matc Executes a command with all secrets injected as environment variables. The agent provides a command and arguments, and receives stdout/stderr output. Note that the agent controls the command, so it could run `printenv` or `echo $SECRET` to read injected values β€” `exec` provides **audit visibility** (you can see what commands were run), not secret isolation. -## How It Works +Start the MCP client from the project directory so fnox can discover its configuration, or pass an explicit `-c` path in the server arguments. + +## How it works 1. The MCP server starts in non-interactive mode (no stdin prompts) 2. On the **first tool call**, all env-injectable profile secrets (`env = true` or `env = "exec"`) are resolved in a single batch β€” this amortizes the cost of YubiKey taps or SSO prompts. Secrets configured with `env = false` are resolved on-demand when individually requested via `get_secret`. @@ -92,13 +101,13 @@ Executes a command with all secrets injected as environment variables. The agent 4. Subsequent tool calls use the cache 5. When the agent disconnects (EOF), the process exits and all secrets are cleared from memory -## Security Considerations +## Security considerations - Secrets live only in process memory β€” except for `as_file = true` secrets, which are written to ephemeral temp files for subprocess injection and deleted when the command completes - The `exec` tool captures stdout/stderr (does not inherit stdio, which would corrupt the JSON-RPC stream) and caps output at 1 MiB to prevent unbounded memory usage - Non-interactive mode prevents provider auth prompts from interfering with the protocol - The `exec` tool redacts resolved secret values from stdout/stderr before returning output to the agent β€” commands like `printenv` or `echo $SECRET` will show `[REDACTED]` instead of the raw value. Redaction performs literal string matching and does not detect base64-encoded or otherwise transformed values. To disable (not recommended): `mcp.redact_output = false` -- With `tools = ["exec"]` and redaction enabled (default), agents cannot retrieve raw secret values through either `get_secret` or subprocess output +- With `tools = ["exec"]`, direct `get_secret` access is disabled. Output redaction reduces accidental disclosure, but an agent-controlled command can transform or transmit a value; it does not provide secret isolation - Use `mcp.secrets` to limit which secrets the agent can access β€” unlisted secrets are never resolved or injected - Disabled tools are not advertised in `tools/list` β€” agents only see tools they can actually call - The MCP allowlist only controls the MCP channel β€” secrets injected into your shell by shell integration are still visible to any agent running there. Set the top-level `env = "exec"` default (see the [configuration reference](/reference/configuration#env)) to keep secrets out of the interactive shell entirely; they remain available through `fnox exec` and the MCP tools diff --git a/docs/guide/mise-integration.md b/docs/guide/mise-integration.md index 18f99c7a..0c82671a 100644 --- a/docs/guide/mise-integration.md +++ b/docs/guide/mise-integration.md @@ -1,225 +1,78 @@ -# Mise Integration +--- +description: "Run mise tasks with fnox-managed secrets and understand the limitations of the fnox environment plugin." +--- -fnox works well with [mise](https://mise.jdx.dev) as a tool installer and task -runner. The recommended setup is to install the fnox CLI with mise, then use -fnox directly through shell integration, `fnox exec`, or mise tasks. +# mise integration -::: warning Experimental plugin -We do not recommend using fnox through the -[`jdx/mise-env-fnox`](https://github.com/jdx/mise-env-fnox) env plugin. It is an -incomplete experiment and does not track every fnox feature. -::: +Use [mise](https://mise.jdx.dev) to install fnox and run project tasks. Put `fnox exec` in tasks that need secrets so those tasks work from a shell, an editor, or CI. -## Installation +## Install fnox for the project -Install fnox globally with mise: - -```bash -mise use -g fnox -``` - -Then enable fnox shell integration if you want secrets to load automatically when -you enter a project directory: - -```bash -eval "$(fnox activate bash)" +```sh +mise use fnox ``` -See [Shell Integration](/guide/shell-integration) for zsh, fish, Nushell, and -PowerShell setup. +This adds fnox to the project's mise configuration. To install it for all projects, use `mise use -g fnox` instead. -## Using fnox in mise Tasks +## Run tasks with secrets -For commands launched through mise, run them through `fnox exec`: +Add tasks to `mise.toml`: ```toml [tasks.dev] run = "fnox exec -- npm run dev" -[tasks.deploy] -run = "fnox exec --profile production -- ./deploy.sh" -``` - -This keeps secret resolution inside fnox, so options such as `env = false`, -`as_file`, leases, profiles, and provider-specific behavior all work the same as -they do outside mise. - -## Experimental Env Plugin - -The `jdx/mise-env-fnox` env plugin is documented here only for existing users. -For new setups, prefer shell integration or `fnox exec`. - -Add the plugin to your project's `mise.toml`: - -```toml -[plugins] -fnox-env = "https://github.com/jdx/mise-env-fnox" - -[tools] -fnox = "latest" - -[env] -_.fnox-env = { tools = true } -``` - -> [!IMPORTANT] -> `tools = true` is required so the plugin can access the mise-managed fnox -> binary. Without it, the plugin runs before mise tools are added to PATH and -> won't be able to find fnox. - -## How It Works - -When mise activates your environment, the experimental fnox plugin: - -1. Searches for `fnox.toml` in the current directory and parent directories -2. Resolves secrets using your configured providers -3. Exports the secrets as environment variables -4. Watches `fnox.toml` for changes to invalidate the cache - -## Configuration Options - -| Option | Description | Default | -| ---------- | --------------------------------------------------------------- | --------- | -| `tools` | Use mise-managed tools (required if fnox is installed via mise) | `false` | -| `profile` | fnox profile to use | `default` | -| `fnox_bin` | Path to fnox binary | `fnox` | - -### Examples - -```toml -[plugins] -fnox-env = "https://github.com/jdx/mise-env-fnox" - -[env] -# Use default profile -_.fnox-env = { tools = true } -``` - -```toml -[plugins] -fnox-env = "https://github.com/jdx/mise-env-fnox" - -[env] -# Use production profile -_.fnox-env = { tools = true, profile = "production" } -``` - -```toml -[plugins] -fnox-env = "https://github.com/jdx/mise-env-fnox" - -[env] -# Custom fnox binary path (tools = true not needed when specifying fnox_bin) -_.fnox-env = { fnox_bin = "/usr/local/bin/fnox" } -``` - -## Environment-Specific Configuration - -Combine with [mise's environment system](https://mise.jdx.dev/configuration/environments.html) for different profiles per environment. mise uses separate config files for each environment: - -**`mise.toml`** (default/dev): - -```toml -[plugins] -fnox-env = "https://github.com/jdx/mise-env-fnox" - -[tools] -fnox = "latest" - -[env] -_.fnox-env = { tools = true, profile = "dev" } -``` - -**`mise.production.toml`**: - -```toml -[env] -_.fnox-env = { tools = true, profile = "production" } -``` - -**`mise.staging.toml`**: +[tasks.test] +run = "fnox exec -- npm test" -```toml -[env] -_.fnox-env = { tools = true, profile = "staging" } +[tasks.deploy] +run = "fnox exec --profile production --if-missing error -- ./deploy.sh" ``` -Then activate different environments: +Run them normally: -```bash -# Development (default) -mise env - -# Production -MISE_ENV=production mise env - -# Staging -MISE_ENV=staging mise env +```sh +mise run dev +mise run test +mise run deploy ``` -## Caching - -The fnox plugin supports mise's environment caching (when `MISE_ENV_CACHE=1`). Secrets are: +fnox owns secret resolution, including profiles, file secrets, lease creation, and cache settings. CI must still authenticate to the configured provider before running these tasks. -- Cached encrypted on disk for fast subsequent loads -- Automatically refreshed when `fnox.toml` changes -- Scoped to your shell session for security +## Load secrets in the interactive shell -To enable caching: +[fnox shell integration](/guide/shell-integration) can run alongside mise activation. Add the appropriate fnox activation command to your shell's startup file: ```bash -export MISE_ENV_CACHE=1 +# Bash: ~/.bashrc +eval "$(fnox activate bash)" ``` -## Comparison with Shell Integration - -| Feature | Shell Integration | Experimental mise env plugin | -| ------------------------- | ----------------- | ---------------------------- | -| Automatic loading on `cd` | Yes | Yes (via mise) | -| Works without mise | Yes | No | -| Caching | No | Yes (with env cache) | -| Task integration | No | Yes | -| Tool version management | No | Yes | -| Full fnox feature support | Yes | No | - -Use shell integration or `fnox exec` for the maintained fnox behavior. Use the -mise env plugin only when its current feature set is enough for your project. - -## Troubleshooting +For tasks, keep the explicit `fnox exec` wrapper even when shell integration is enabled. It makes the task's secret requirements independent of the calling shell. -### Secrets not loading +Use top-level `env = "exec"` in `fnox.toml` if you want secrets available only to commands launched through fnox, rather than every process in the interactive shell. -1. Ensure `fnox.toml` exists in your project: +## Choose a cache - ```bash - ls fnox.toml - ``` +If remote reads are slow, configure caching in fnox: -2. Test fnox directly: +- [Sync](/guide/sync) creates a persistent encrypted local snapshot. Use age or another local provider for offline access. +- The [daemon](/guide/daemon) keeps resolved values in memory during a session. - ```bash - fnox export --format json - ``` +Both work when a mise task invokes `fnox exec`. -3. Check mise is loading the plugin: - ```bash - mise env - ``` +## Experimental environment plugin -### Cache not invalidating +The [`jdx/mise-env-fnox`](https://github.com/jdx/mise-env-fnox) environment plugin is an incomplete experiment and does not track all fnox features. Existing users can consult its repository for its current configuration and limitations. -If secrets aren't updating after changes to `fnox.toml`: +For a maintained setup, remove the `_.fnox-env` entry from mise's `[env]` configuration and use the task wrappers or shell activation shown above. Remove the plugin registration only if nothing else in your mise configuration uses it. -```bash -# Clear mise's env cache -mise cache clear +## Troubleshooting -# Or use fresh flag -mise exec --fresh-env -- your-command +```sh +mise exec -- fnox --version +mise exec -- fnox config-files +mise exec -- fnox check --all ``` -## Next Steps - -- [Shell Integration](/guide/shell-integration) - Alternative direct shell integration -- [Profiles](/guide/profiles) - Managing multiple environments -- [Hierarchical Config](/guide/hierarchical-config) - Organizing secrets across directories +These checks distinguish tool installation, config discovery, and secret resolution problems. Continue with [fnox troubleshooting](/guide/troubleshooting) if the CLI is available but secrets do not resolve. diff --git a/docs/guide/missing-secrets.md b/docs/guide/missing-secrets.md index a6e3333f..15739fc9 100644 --- a/docs/guide/missing-secrets.md +++ b/docs/guide/missing-secrets.md @@ -1,14 +1,20 @@ -# Handling Missing Secrets +--- +description: "Choose whether missing secrets fail, warn, or stay silent, and supply non-sensitive defaults when appropriate." +--- + +# Handling missing secrets Use the `if_missing` setting to control what happens when a secret can't be resolved. This is especially useful for CI environments or when some secrets are optional. -## Available Modes +## Available modes + +These policies apply when a command resolves multiple secrets. An explicit `fnox get KEY` may still return a provider error directly. A configured `default` is tried as a fallback before a value is considered missing. - **`error`** - Fail the command if a secret cannot be resolved (strictest) - **`warn`** - Print a warning and continue (default) - **`ignore`** - Silently skip missing secrets -## Priority Chain +## Priority chain You can set `if_missing` at multiple levels. fnox uses the first match: @@ -19,7 +25,7 @@ You can set `if_missing` at multiple levels. fnox uses the first match: 5. **Base default environment variable**: `FNOX_IF_MISSING_DEFAULT=error` 6. **Default**: `warn` (lowest priority) -## Per-Secret Configuration +## Per-secret configuration Set different behaviors for different secrets: @@ -35,7 +41,7 @@ ANALYTICS_KEY = { provider = "aws", value = "analytics-key", if_missing = "ignor CACHE_URL = { provider = "aws", value = "cache-url", if_missing = "warn" } # Print warning if missing ``` -## Top-Level Default +## Top-level default Set a default for all secrets: @@ -49,7 +55,7 @@ API_KEY = { provider = "age", value = "encrypted..." } # Inherits if_missing = OPTIONAL_FEATURE_FLAG = { default = "false", if_missing = "ignore" } # Override - this one can be missing ``` -## Runtime Override with CLI +## Runtime override with CLI Override config settings at runtime: @@ -64,7 +70,7 @@ fnox exec --if-missing error -- ./deploy.sh fnox exec --if-missing warn -- npm start ``` -## Runtime Override with Environment Variable +## Runtime override with environment variable ```bash # Set globally for a session @@ -75,7 +81,7 @@ fnox exec -- npm start FNOX_IF_MISSING=error fnox exec -- ./critical-task.sh ``` -## Base Default Behavior +## Base default behavior Set a default behavior when `if_missing` is not configured anywhere: @@ -100,9 +106,11 @@ This is useful for: - Secret-level config - Top-level config -## CI/CD Examples +## CI/CD examples + +These workflow excerpts assume fnox and any provider CLI have been installed. Supply provider credentials for tests that require real secrets. -### Forked PRs (Secrets Unavailable) +### Forked PRs (secrets unavailable) ```yaml # .github/workflows/test.yml @@ -122,7 +130,7 @@ jobs: fnox exec -- npm test ``` -### Production Deployment (Strict) +### Production deployment (strict) ```yaml # .github/workflows/deploy.yml @@ -147,7 +155,7 @@ jobs: fnox exec --profile production -- ./deploy.sh ``` -### Staging (Warn on Missing) +### Staging (warn on missing) ```yaml # .github/workflows/staging.yml @@ -162,9 +170,9 @@ jobs: fnox exec --profile staging -- ./deploy.sh ``` -## Use Cases +## Use cases -### Optional Analytics/Monitoring +### Optional analytics/monitoring ```toml [secrets] @@ -173,21 +181,21 @@ SENTRY_DSN = { provider = "aws", value = "sentry-dsn", if_missing = "ignore" } DATADOG_API_KEY = { provider = "aws", value = "datadog-key", if_missing = "ignore" } ``` -### Required Database +### Required database ```toml [secrets] DATABASE_URL = { provider = "aws", value = "database-url", if_missing = "error" } # Must exist or fail ``` -### Development Defaults +### Development defaults ```toml [secrets] -REDIS_URL = { provider = "aws", value = "redis-url", default = "redis://localhost:6379", if_missing = "warn" } # Warn if the provider lookup fails, then fall back to the default +REDIS_URL = { provider = "aws", value = "redis-url", default = "redis://localhost:6379", if_missing = "warn" } # Fall back to the local URL if the provider lookup fails ``` -## Behavior Summary +## Behavior summary | Mode | Behavior | Use Case | | -------- | ----------------------- | ----------------------------------------- | @@ -195,7 +203,7 @@ REDIS_URL = { provider = "aws", value = "redis-url", default = "redis://localhos | `warn` | Print warning, continue | Optional but recommended secrets | | `ignore` | Silent skip | Truly optional features (analytics, etc.) | -## Next Steps +## Next steps - [Profiles](/guide/profiles) - Different secrets per environment - [Import/Export](/guide/import-export) - Migrate secrets between systems diff --git a/docs/guide/profiles.md b/docs/guide/profiles.md index fbf1f196..b7fd067f 100644 --- a/docs/guide/profiles.md +++ b/docs/guide/profiles.md @@ -1,10 +1,14 @@ +--- +description: "Define development and production secrets, inherit shared settings, compose profiles, and choose a write target." +--- + # Profiles Profiles let you manage secrets for different environments (dev, staging, production) in a single `fnox.toml` file. -## Basic Usage +## Basic usage -Define environment-specific secrets using profiles: +Define environment-specific secrets using profiles. The examples that follow assume the named providers are configured; ciphertext is abbreviated. See the [complete setup](/guide/real-world-example) for provider definitions. ```toml # Default profile (development) @@ -23,9 +27,9 @@ API_URL = { default = "https://api.example.com" } DATABASE_URL = { provider = "aws", value = "prod-database-url" } # Stored in AWS Secrets Manager ``` -## Using Profiles +## Using profiles -### Via Command Line +### Via command line ```bash # Use default profile @@ -36,7 +40,7 @@ fnox get API_URL --profile staging fnox exec --profile production -- ./deploy.sh ``` -### Via Environment Variable +### Via environment variable ```bash # Set once for the session @@ -47,7 +51,7 @@ fnox get DATABASE_URL fnox exec -- node server.js ``` -### With Shell Integration +### With shell integration ```bash # Enable shell integration @@ -61,7 +65,7 @@ export FNOX_PROFILE=staging # fnox detects the change on the next prompt automatically ``` -## Composing Multiple Profiles +## Composing multiple profiles You can activate multiple profiles at the same time as an ordered overlay stack. Later profiles override earlier ones on key conflicts, with the @@ -88,7 +92,7 @@ fnox -P aws -P prod --write-profile prod set DATABASE_URL "value" With a single active profile, the write target defaults to that profile and `--write-profile` is not needed. -### When to Use Composition +### When to use composition Composition is useful when concerns are split across profiles: @@ -97,7 +101,7 @@ Composition is useful when concerns are split across profiles: - `ci` adds CI-only secrets - `local` overrides a few values for local development -## Profile Inheritance +## Profile inheritance Profiles automatically inherit secrets from the top level: @@ -152,9 +156,9 @@ provider. Nested inheritance is supported; cycles and unknown inherited profiles are reported as configuration errors. `--no-defaults` still controls whether top-level secrets are included. -This reduces duplication for secrets shared across environments. +Use `--no-defaults` to exclude top-level secrets while keeping the selected profiles and their inheritance. It does not remove top-level provider definitions. -## Profile-Specific Providers +## Profile-specific providers Each profile can have its own providers: @@ -173,7 +177,7 @@ aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp/" } DATABASE_URL = { provider = "aws", value = "database-url" } ``` -## Secret References in Provider Config +## Secret references in provider config Provider configuration properties can reference secrets using `{ secret = "NAME" }`. This enables bootstrap scenarios where provider credentials are themselves managed as secrets: @@ -184,17 +188,17 @@ recipients = ["age1..."] [providers.vault] type = "vault" -address = "http://vault.example.com:8200" +address = "https://vault.example.com:8200" token = { secret = "VAULT_TOKEN" } # Resolved from secrets or env var [secrets] VAULT_TOKEN = { provider = "age", value = "AGE-ENCRYPTED-TOKEN..." } -DATABASE_URL = { provider = "vault", value = "database/creds/myapp" } +DATABASE_URL = { provider = "vault", value = "database/password" } ``` Resolution order: config secrets first, then environment variables. fnox detects circular dependencies and errors if found. -## List Profiles +## List profiles See all available profiles: @@ -204,16 +208,16 @@ fnox profiles Output: -``` +```text Available profiles: default (2 secrets) staging (2 secrets) production (2 secrets) ``` -## Common Patterns +## Common patterns -### Development + Production +### Development + production ```toml # Development (default): encrypted in git @@ -231,7 +235,7 @@ aws = { type = "aws-sm", region = "us-east-1" } DATABASE_URL = { provider = "aws", value = "database-url" } ``` -### Multi-Region Production +### Multi-region production ```toml [profiles.production-us.providers] @@ -241,7 +245,7 @@ aws = { type = "aws-sm", region = "us-east-1" } aws = { type = "aws-sm", region = "eu-west-1" } ``` -### Per-Developer Profiles +### Per-developer profiles ```toml [profiles.alice] @@ -260,7 +264,9 @@ export FNOX_PROFILE=alice fnox exec -- npm start ``` -## CI/CD Example +## CI/CD example + +These job excerpts assume checkout, fnox installation, and provider authentication steps are already in place. ```yaml # .github/workflows/deploy.yml @@ -277,7 +283,7 @@ jobs: - run: fnox exec --profile production -- ./deploy.sh ``` -## Next Steps +## Next steps - [Hierarchical Config](/guide/hierarchical-config) - Organize configs across directories (includes local overrides) - [Real-World Example](/guide/real-world-example) - Complete multi-environment setup diff --git a/docs/guide/proxy.md b/docs/guide/proxy.md index 00925b9a..2deba466 100644 --- a/docs/guide/proxy.md +++ b/docs/guide/proxy.md @@ -1,4 +1,8 @@ -# Credential Proxy +--- +description: "Inject credentials into matching HTTPS requests while passing placeholders to a command. Configure rules and understand the access model." +--- + +# Credential proxy The fnox credential proxy lets a command use API credentials without receiving their real values. The child process receives placeholders, and fnox substitutes @@ -7,7 +11,7 @@ the real values only in approved HTTPS requests. This is useful for AI agents and other untrusted or highly automated programs that need to call external APIs. -## Configure Rules +## Configure rules Proxy rules refer to secrets in the active profile: @@ -40,7 +44,7 @@ Inspect the effective rules without resolving secrets: fnox proxy rules ``` -## Run a Command +## Run a command ```bash fnox proxy run -- codex @@ -64,7 +68,7 @@ fnox: The CA private key and real secret values remain in fnox process memory. -## Egress Modes +## Egress modes `egress = "strict"` is the default. Destinations without proxy rules are rejected. @@ -79,9 +83,9 @@ egress = "permissive" Strict mode is recommended for agent workloads. -## Current Limits +## Current limits -This first version intentionally has a narrow protocol surface: +The proxy currently supports the following request shapes: - Credential substitution is supported in HTTP headers. - Plain `http://` proxy requests are rejected. @@ -92,13 +96,13 @@ This first version intentionally has a narrow protocol surface: - Domains are exact names; wildcard domains are not supported. - Client software must honor the standard proxy and CA environment variables. -## Security Model +## Security model The credential proxy prevents the child from receiving the real values through its configured environment and proxy traffic. Rules also restrict where fnox will inject each credential. -The proxy is not yet an operating-system sandbox. A determined process running +The proxy is not an operating-system sandbox. A determined process running as the same user may bypass proxy environment variables, read accessible fnox configuration or provider state, or invoke fnox directly. Run untrusted agents in a container, VM, or other sandbox that blocks direct egress and access to @@ -106,3 +110,8 @@ credential sources. Request auditing logs method, domain, path, and injected secret names through fnox tracing. It never logs request headers, bodies, or secret values. + +## Next steps + +- [MCP server](/guide/mcp): expose selected secrets and command execution over stdio. +- [Proxy commands](/cli/proxy): inspect rules and launch a command. diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md index c48462b3..f47e8679 100644 --- a/docs/guide/quick-start.md +++ b/docs/guide/quick-start.md @@ -1,106 +1,106 @@ -# Quick Start +--- +description: "Encrypt your first secret with age, check access, and run an application with fnox. No cloud account required." +--- -Get started with fnox in 5 minutes. +# Quick start -## 1. Initialize fnox +Store your first secret with age encryption, then make it available to a command. This walkthrough uses a local key file and needs no cloud account. -```bash -cd your-project -fnox init -``` - -This creates a `fnox.toml` configuration file. In a terminal, `fnox init` runs an interactive wizard that can also set up a provider; answer "No" (or pass `--skip-wizard`) to create a minimal config and follow the steps below. +Already have a vault? Use [1Password with a local cache](/guide/golden-path), or choose another [provider](/providers/overview). -## 2. Set a Secret +## 1. Install fnox and age -```bash -# Set a secret (prompts for value) -fnox set DATABASE_URL +With [mise](https://mise.jdx.dev): -# Or provide the value directly -fnox set DATABASE_URL "postgresql://localhost/mydb" +```sh +mise use -g fnox age +fnox --version ``` -Without a provider configured, secrets are stored as plain text `default` values in `fnox.toml`. For encryption, see [Add Encryption](#add-encryption-recommended) below. +See [installation](/guide/installation) for other fnox installation methods. The age CLI supplies `age-keygen`; fnox handles encryption itself. -## 3. Get a Secret +## 2. Create your encryption key -```bash -fnox get DATABASE_URL +```sh +mkdir -p ~/.config/fnox +age-keygen -o ~/.config/fnox/age.txt +age-keygen -y ~/.config/fnox/age.txt ``` -## 4. Run Commands with Secrets +The last command prints your public recipient, beginning with `age1`. Copy it for the next step. If you already have `age.txt`, reuse it and run only the last command. -```bash -# Secrets are loaded as environment variables -fnox exec -- npm start -fnox exec -- python app.py -fnox exec -- ./my-script.sh -``` +Keep `age.txt` private and back it up. The public recipient can go in git; the private key file cannot. fnox automatically reads `age.txt` from its [configuration directory](/reference/environment#fnox-config-dir). If you use a different directory, set the provider's [`key_file`](/providers/age#configuration). + +## 3. Configure the project + +From your project directory: -## 5. Enable Shell Integration (Optional) +```sh +fnox init --skip-wizard +``` -Automatically load secrets when you `cd` into a directory: +Edit the new `fnox.toml` to contain the following, replacing `age1...` with the public recipient you copied: -```bash -# Enable for your shell -eval "$(fnox activate bash)" # or zsh, fish +```toml +#:schema https://fnox.jdx.dev/schema.json +default_provider = "age" -# Add to your shell profile for persistence -echo 'eval "$(fnox activate bash)"' >> ~/.bashrc +[providers.age] +type = "age" +recipients = ["age1..."] # Replace with your public recipient ``` -To enable integration in other shells (Nushell, PowerShell), see the [Shell Integration](/guide/shell-integration) guide. +`--skip-wizard` creates an empty configuration. These provider settings are what enable encryption; initialization alone does not encrypt secrets. -Now secrets auto-load: +## 4. Store and check a secret -```bash -~/projects $ cd my-app -fnox: +3 DATABASE_URL, API_KEY, JWT_SECRET -~/projects/my-app $ +```sh +fnox set DATABASE_URL ``` -## Add Encryption (Recommended) +At the hidden prompt, enter a value such as `postgresql://localhost/mydb`. fnox uses `default_provider = "age"` and writes encrypted ciphertext into `fnox.toml`. -For production use, encrypt your secrets: +```sh +# Verify that configured secrets can be resolved +fnox check --all -### Using age encryption +# Inspect names and providers without displaying resolved values +fnox list +``` -```bash -# 1. Generate an age key -age-keygen -o ~/.config/fnox/age.txt +Use `fnox get DATABASE_URL` when you need the value itself. It prints the decrypted secret to stdout. -# 2. Get your public key -grep "public key:" ~/.config/fnox/age.txt -# Output: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p +## 5. Run a command -# 3. Configure the age provider in fnox.toml -cat >> fnox.toml << 'EOF' -[providers.age] -type = "age" -recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] -EOF +```sh +fnox exec -- npm start +# Or: +fnox exec -- python app.py +``` -# 4. Set your decryption key -export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt) +The command receives `DATABASE_URL` as an environment variable. Your parent shell is unchanged. To verify injection without an application or printing the value: -# 5. Encrypt a secret -fnox set DATABASE_URL "postgresql://prod.example.com/db" --provider age +```sh +fnox exec -- sh -c 'test -n "$DATABASE_URL" && printf "Database secret is available\n"' ``` -The secret is now encrypted in `fnox.toml` and safe to commit to git! +Put fnox options before `--`; everything after it belongs to the command. + +## 6. Commit the configuration + +Add these entries to your existing `.gitignore`: + +```text +fnox.local.toml +.fnox.local.toml +.env +``` -::: tip Using a password manager or cloud vault? -Encrypted-in-git is great for solo and open source projects. If your team keeps -secrets in a vault like 1Password, follow [the golden path](/guide/golden-path) -instead: commit references to the vault and cache secrets locally with -[`fnox sync`](/guide/sync). -::: +Review `fnox.toml`, then commit it with `.gitignore`. The age secret's `value` is ciphertext. Any `default` values remain plaintext, so reserve them for non-sensitive configuration. -## Next Steps +## Where to go next -- [Golden Path Setup](/guide/golden-path) - The recommended workflow: a remote vault plus a local encrypted cache -- [How It Works](/guide/how-it-works) - Understand fnox's architecture -- [Providers](/providers/overview) - Explore all available providers -- [Shell Integration](/guide/shell-integration) - Deep dive into shell integration -- [Real-World Example](/guide/real-world-example) - See a complete setup with multiple environments +- [Shell integration](/guide/shell-integration): load secrets automatically when you enter a project. +- [Profiles](/guide/profiles): use separate development, staging, and production values. +- [Age team setup](/providers/age#team-workflow): add teammates and re-encrypt for their keys. +- [Troubleshooting](/guide/troubleshooting): diagnose configuration and authentication problems. diff --git a/docs/guide/real-world-example.md b/docs/guide/real-world-example.md index 362967c9..aa86c623 100644 --- a/docs/guide/real-world-example.md +++ b/docs/guide/real-world-example.md @@ -1,365 +1,165 @@ -# Real-World Example +--- +description: "Build an example development, staging, and production setup using age encryption, AWS Secrets Manager, and profiles." +--- -Let's build a complete setup for a typical web application with development, staging, and production environments. +# From development to production -This example keeps dev/staging secrets encrypted in git with age. If your team keeps secrets in a vault like 1Password instead, see the [Golden Path Setup](/guide/golden-path). +Use age-encrypted development and staging values in git, then switch to AWS Secrets Manager for production. This example shows how the pieces fit together without requiring the application to know which provider it uses. -## The Scenario +If all environments use an existing vault, start with [connect a vault](/guide/golden-path) instead. -You're building an API that needs: +## Prerequisites -- Database URL -- API keys (Stripe, SendGrid) -- JWT secret -- External service URLs +- fnox and age installed; see the [quick start](/guide/quick-start). +- A project with a development command, such as `npm start`. +- An age identity for each teammate and for CI. +- AWS credentials, an AWS region, and permission to read the production secrets. -**Requirements:** +The example uses two secrets, `DATABASE_URL` and `JWT_SECRET`, plus the non-sensitive `LOG_LEVEL` setting. Replace names and commands with those used by your application. -- **Development:** Secrets in git (encrypted) so the team can clone and run -- **Staging:** Secrets in git (encrypted) with staging values -- **Production:** Secrets in AWS Secrets Manager (never in git) +## 1. Configure development encryption -## Step 1: Initialize - -```bash -cd my-api -fnox init -git init -``` - -## Step 2: Set Up Age Encryption (for Dev/Staging) - -```bash -# Generate age key -age-keygen -o ~/.config/fnox/age.txt - -# Get your public key -grep "public key:" ~/.config/fnox/age.txt -# Output: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p - -# For teams: collect everyone's public keys and add them all -``` - -Add to `fnox.toml`: +Create the project configuration with `fnox init --skip-wizard`, then edit `fnox.toml`: ```toml -# Shared age provider for dev and staging +#:schema https://fnox.jdx.dev/schema.json +default_provider = "age" + [providers.age] type = "age" -recipients = [ - "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p", # alice - "age1pr3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqabc123", # bob - "age1zr3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqdxf456" # ci -] -``` - -Set decryption key: - -```bash -# Add to ~/.bashrc or ~/.zshrc -export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt) -``` - -## Step 3: Add Development Secrets - -```bash -# Encrypt development secrets -fnox set DATABASE_URL "postgresql://localhost/mydb" --provider age -fnox set JWT_SECRET "$(openssl rand -hex 32)" --provider age -fnox set STRIPE_KEY "sk_test_abc123" --provider age -fnox set SENDGRID_KEY "SG.test123" --provider age -``` - -Your `fnox.toml` now contains encrypted secrets: - -```toml -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } +recipients = ["age1..."] # Replace with the public recipients of your team and CI [secrets] -DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } -JWT_SECRET = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } -STRIPE_KEY = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } -SENDGRID_KEY = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } +LOG_LEVEL = { default = "debug" } ``` -**Commit this!** It's encrypted and safe. - -```bash -git add fnox.toml -git commit -m "Add encrypted development secrets" -``` +Each teammate keeps their own private identity outside the repository. fnox reads `age.txt` from its configuration directory by default. See [age team setup](/providers/age#team-workflow) for other identity locations and multiple recipients. -## Step 4: Add Staging Profile +Store the development values using hidden prompts: -Add staging secrets (also encrypted): +```sh +fnox set DATABASE_URL +fnox set JWT_SECRET +fnox check --all +fnox exec -- npm start +``` -```bash -# Target the staging profile -fnox set DATABASE_URL "postgresql://staging.db.example.com/mydb" \ - --provider age \ - --profile staging +`fnox set` adds encrypted values to `fnox.toml`. Review the file, then commit it. `LOG_LEVEL` remains plaintext because it is a default. -fnox set JWT_SECRET "$(openssl rand -hex 32)" \ - --provider age \ - --profile staging +## 2. Add staging values -fnox set STRIPE_KEY "sk_test_staging_xyz" \ - --provider age \ - --profile staging +Select the staging profile when writing: -fnox set SENDGRID_KEY "SG.staging456" \ - --provider age \ - --profile staging +```sh +fnox set DATABASE_URL --profile staging --provider age +fnox set JWT_SECRET --profile staging --provider age ``` -Your `fnox.toml` now has a staging profile: - -```toml -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } +fnox writes under `[profiles.staging.secrets]`. Staging inherits `LOG_LEVEL` and the age provider from the top level. -# Development (default profile) -[secrets] -DATABASE_URL = { provider = "age", value = "..." } -JWT_SECRET = { provider = "age", value = "..." } -# ... other dev secrets ... - -# Staging profile -[profiles.staging.secrets] -DATABASE_URL = { provider = "age", value = "..." } -JWT_SECRET = { provider = "age", value = "..." } -# ... other staging secrets ... +```sh +fnox check --all --profile staging +fnox exec --profile staging -- ./deploy.sh ``` -## Step 5: Add Production Profile (AWS Secrets Manager) +A profile changes configuration; it does not restrict who can decrypt. Use separate encryption providers and recipients if staging requires a different access boundary. -Add production configuration (secrets stored in AWS): +## 3. Reference production secrets -```toml -# Add to fnox.toml +Create `myapi/database-url` and `myapi/jwt-secret` in AWS Secrets Manager using your normal provisioning process. Add the provider and references: -[profiles.production.providers] -aws = { type = "aws-sm", region = "us-east-1", prefix = "myapi/" } +```toml +[profiles.production.providers.aws] +type = "aws-sm" +region = "us-east-1" +prefix = "myapi/" [profiles.production.secrets] -DATABASE_URL = { provider = "aws", value = "database-url", if_missing = "error" } # Critical secret +DATABASE_URL = { provider = "aws", value = "database-url", if_missing = "error" } JWT_SECRET = { provider = "aws", value = "jwt-secret", if_missing = "error" } -STRIPE_KEY = { provider = "aws", value = "stripe-key", if_missing = "error" } -SENDGRID_KEY = { provider = "aws", value = "sendgrid-key", if_missing = "error" } +LOG_LEVEL = { default = "info" } ``` -Create secrets in AWS: +These values are names, not ciphertext. No production plaintext goes into the config. -```bash -aws secretsmanager create-secret \ - --name "myapi/database-url" \ - --secret-string "postgresql://prod.rds.amazonaws.com/mydb" +After authenticating to AWS: -aws secretsmanager create-secret \ - --name "myapi/jwt-secret" \ - --secret-string "$(openssl rand -base64 64)" - -aws secretsmanager create-secret \ - --name "myapi/stripe-key" \ - --secret-string "sk_live_REAL_KEY_HERE" - -aws secretsmanager create-secret \ - --name "myapi/sendgrid-key" \ - --secret-string "SG.REAL_KEY_HERE" +```sh +fnox check --all --profile production +fnox exec --profile production --if-missing error -- ./deploy.sh ``` -Commit the production references: - -```bash -git add fnox.toml -git commit -m "Add production profile (AWS Secrets Manager)" -``` +Use [`--no-defaults`](/guide/profiles#profile-inheritance) when you want production to include only its selected profile secrets. Otherwise it inherits any top-level secrets it does not override. -## Step 6: Local Overrides +## 4. Keep personal overrides local -Create `.gitignore`: +Add these patterns to the project's existing `.gitignore`: -```bash -cat > .gitignore << 'EOF' +```text fnox.local.toml +.fnox.local.toml .env -EOF ``` -Each developer can create personal overrides: +A developer can create `fnox.local.toml`: ```toml -# fnox.local.toml (not committed) - [secrets] -DATABASE_URL = { default = "postgresql://localhost/alice_db" } # Personal DB -DEBUG_MODE = { default = "true" } # Enable debugging +DATABASE_URL = { default = "postgresql://localhost/alice_db" } ``` -## Step 7: Use It - -### Development - -```bash -# Enable shell integration -eval "$(fnox activate bash)" -echo 'eval "$(fnox activate bash)"' >> ~/.bashrc +This local example has no password. Use an encryption provider or vault reference for a sensitive override. -# Navigate to project (secrets auto-load) -cd my-api -# fnox: +4 DATABASE_URL, JWT_SECRET, STRIPE_KEY, SENDGRID_KEY +Inspect which files and definitions are active: -# Run the app -npm run dev +```sh +fnox config-files +fnox list --sources ``` -Or explicitly: - -```bash -fnox exec -- npm run dev -``` - -### Staging - -```bash -# Deploy to staging -fnox exec --profile staging -- ./deploy.sh - -# Or set profile for session -export FNOX_PROFILE=staging -fnox exec -- ./deploy.sh -``` +## 5. Run in CI -### Production +Install fnox and the application toolchain in the workflow before these steps. If mise manages the project tools, include fnox in `mise.toml` so the installation step knows to install it. -```bash -# Ensure AWS credentials are set -export AWS_REGION=us-east-1 +For development tests, supply the dedicated CI age identity through the CI secret store: -# Deploy to production -fnox exec --profile production -- ./deploy.sh +```yaml +- name: Test + env: + FNOX_AGE_KEY: ${{ secrets.FNOX_AGE_KEY }} + run: fnox --non-interactive exec --if-missing error -- npm test ``` -## Step 8: CI/CD Setup +The CI public recipient must have been included when the development secrets were encrypted. Adding it later requires re-encryption by someone who can already decrypt. -### GitHub Actions +For production, authenticate the runner to AWS first, then run: ```yaml -# .github/workflows/ci.yml -name: CI -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v4 # Installs fnox via mise - - # Decrypt dev secrets for testing with the CI age key - - name: Run tests - env: - FNOX_AGE_KEY: ${{ secrets.FNOX_AGE_KEY }} - run: | - fnox exec -- npm test - - deploy-staging: - if: github.ref == 'refs/heads/develop' - needs: test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v4 - - - name: Deploy to staging - env: - FNOX_AGE_KEY: ${{ secrets.FNOX_AGE_KEY }} - run: | - fnox exec --profile staging -- ./deploy.sh - - deploy-production: - if: github.ref == 'refs/heads/main' - needs: test - runs-on: ubuntu-latest - environment: production - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v4 - - - name: Deploy to production - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: us-east-1 - FNOX_IF_MISSING: error # Fail if any secret is missing - run: | - fnox exec --profile production -- ./deploy.sh +- name: Deploy + run: fnox --non-interactive exec --profile production --if-missing error -- ./deploy.sh ``` -### Set GitHub Secrets - -1. Go to your repo β†’ Settings β†’ Secrets β†’ Actions -2. Add `FNOX_AGE_KEY`: - ```bash - # Copy the CI age secret key (from the CI recipient's age.txt) - grep "AGE-SECRET-KEY" ~/.config/fnox/ci-age.txt - ``` -3. Add `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` for production - -## Step 9: Team Onboarding - -New team member joins: - -```bash -# 1. Clone the repo -git clone https://github.com/myorg/my-api -cd my-api - -# 2. Install fnox (via mise) -mise install +The [AWS Secrets Manager guide](/providers/aws-sm) covers credentials and read permissions. Never assume that setting `AWS_REGION` alone authenticates a runner. -# 3. Generate age key -age-keygen -o ~/.config/fnox/age.txt +## 6. Onboard a teammate -# 4. Share public key with team -grep "public key:" ~/.config/fnox/age.txt -# Send to team lead to add to fnox.toml recipients +1. The teammate installs fnox and creates a personal age identity. +2. They share only the public recipient, from `age-keygen -y ~/.config/fnox/age.txt`. +3. A teammate with existing decryption access adds the recipient and re-encrypts each affected profile: -# 5. Set decryption key -echo 'export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt)' >> ~/.bashrc -source ~/.bashrc - -# 6. Enable shell integration -echo 'eval "$(fnox activate bash)"' >> ~/.bashrc - -# 7. Team lead updates fnox.toml with new recipient -# Then re-encrypts all secrets: -fnox reencrypt -p age - -# 8. New team member pulls and re-enters the directory -git pull -cd . -# fnox: +4 DATABASE_URL, JWT_SECRET, STRIPE_KEY, SENDGRID_KEY -npm run dev # Just works! -``` + ```sh + fnox reencrypt --provider age --profile default + fnox reencrypt --provider age --profile staging + ``` -## File Structure +4. Commit the new recipients and ciphertext. The new teammate pulls the changes and runs `fnox check --all`. +5. Grant production vault access separately if their role requires it. -``` -my-api/ -β”œβ”€β”€ .gitignore # fnox.local.toml, .env -β”œβ”€β”€ fnox.toml # Committed (encrypted dev/staging, AWS refs for prod) -β”œβ”€β”€ fnox.local.toml # Gitignored (personal overrides) -β”œβ”€β”€ package.json -β”œβ”€β”€ src/ -└── .github/ - └── workflows/ - └── ci.yml # CI/CD with fnox -``` +Removing an age recipient affects future ciphertext only. Previously accessible secrets in git history remain decryptable, so rotate the underlying values when access must end. -## Next Steps +## Next steps -- [Providers](/providers/overview) - Explore other providers -- [Shell Integration](/guide/shell-integration) - Advanced shell setup -- [Hierarchical Config](/guide/hierarchical-config) - Organize larger projects +- [Shell integration](/guide/shell-integration): load development secrets on directory change. +- [Hierarchical configuration](/guide/hierarchical-config): split common and service-specific settings. +- [Import and export](/guide/import-export): migrate existing `.env` values. +- [Credential leases](/guide/leases): use temporary credentials for supported services. diff --git a/docs/guide/shell-integration.md b/docs/guide/shell-integration.md index af1ae8a9..bd1ba99f 100644 --- a/docs/guide/shell-integration.md +++ b/docs/guide/shell-integration.md @@ -1,8 +1,12 @@ -# Shell Integration +--- +description: "Load and unload project secrets automatically in Bash, Zsh, Fish, Nushell, and PowerShell." +--- -fnox can automatically load secrets when you `cd` into directories with a `fnox.toml` file. +# Shell integration -## Enable Shell Integration +Shell integration loads secrets when you enter a project and restores the previous environment when you leave. Enable it for interactive work, or use `fnox exec -- ` when a single subprocess needs secrets. + +## Enable shell integration Add this to your shell profile: @@ -39,7 +43,7 @@ fnox activate nu | save -f ($nu.data-dir | path join "vendor/autoload/fnox.nu") ::: -## How It Works +## How it works Once enabled, fnox installs a hook that runs before each prompt. When you enter a directory with `fnox.toml`: @@ -57,7 +61,11 @@ fnox: -3 DATABASE_URL, API_KEY, JWT_SECRET ~/projects $ ``` -## Output Control +## Limit shell injection + +Set top-level `env = "exec"` in `fnox.toml` to keep secrets out of the interactive shell while retaining subprocess injection. Individual secrets can override this with their own `env` setting. See [injection settings](/reference/configuration#env). + +## Output control Control what gets printed with `FNOX_SHELL_OUTPUT`: @@ -72,7 +80,7 @@ export FNOX_SHELL_OUTPUT=normal export FNOX_SHELL_OUTPUT=debug ``` -## Using Profiles +## Using profiles Switch environments with `FNOX_PROFILE`: @@ -88,11 +96,11 @@ export FNOX_PROFILE=staging # fnox: +3 -3 DATABASE_URL, API_KEY, JWT_SECRET (from staging profile) ``` -## Hierarchical Loading +## Hierarchical loading fnox searches parent directories for `fnox.toml` files and merges them: -``` +```text project/ β”œβ”€β”€ fnox.toml # Common secrets (age provider, shared keys) └── services/ @@ -107,11 +115,11 @@ When you `cd services/api/`, fnox loads: 1. Secrets from `project/fnox.toml` 2. Secrets from `project/services/api/fnox.toml` (overrides parent) -## Manual Reload +## Manual reload fnox's shell hook runs on every prompt and automatically detects changes to config files and environment variables like `FNOX_PROFILE`. In most cases, no manual reload is needed. -To force a full reload, temporarily disable and re-enable: +With fnox activated, its shell function handles the deactivation output. To reset the integration in Bash: ```bash # Disable @@ -121,7 +129,9 @@ fnox deactivate eval "$(fnox activate bash)" ``` -## Next Steps +If a value changed in the remote vault, refresh the [sync cache](/guide/sync) or clear the [daemon cache](/guide/daemon) as appropriate. Re-enabling the hook does not refresh those caches. + +## Next steps - [Per-User Daemon](/guide/daemon) - Cache resolved secrets in memory for faster refreshes - [Profiles](/guide/profiles) - Manage multiple environments diff --git a/docs/guide/sync.md b/docs/guide/sync.md index fb204a99..e3af56d6 100644 --- a/docs/guide/sync.md +++ b/docs/guide/sync.md @@ -1,12 +1,16 @@ -# Syncing Secrets Locally +--- +description: "Cache vault secrets with a personal encryption provider. Learn where the cache lives, how to refresh it, and when it works offline." +--- -`fnox sync` fetches secrets from remote providers (1Password, AWS Secrets Manager, etc.) and re-encrypts them with a local encryption provider (age, YubiKey via age plugin, AWS KMS, etc.). The encrypted values are stored in `fnox.local.toml` (gitignored) so that subsequent access is instant and offline β€” no remote calls needed. +# Syncing secrets locally + +`fnox sync` fetches secrets from their source providers and re-encrypts them with a target encryption provider. Use `--local-file` to write the cache to `fnox.local.toml`, and add that file to `.gitignore`. A local target such as age supports offline reads; a cloud KMS target still needs its cloud API. ::: tip The golden path This is the recommended way to use fnox: secrets live in a remote vault like 1Password, `fnox.toml` only holds references to them, and `fnox sync` caches them locally under a personal age key. The vault stays the single source of -truth, but day-to-day loads are instant and work offline. To go further, keep +truth, but day-to-day reads use local decryption and work offline. To go further, keep the age key in hardware: [Apple's Secure Enclave (Touch ID)](#apple-secure-enclave-touch-id), a [YubiKey](#yubikey), or a [TPM or FIDO2 token](#tpm-and-fido2). @@ -15,7 +19,7 @@ For a zero-to-working walkthrough of the whole setup, see [Golden Path Setup](/guide/golden-path). ::: -## Why Sync? +## Why sync? A typical team setup stores secrets in a shared provider like 1Password: @@ -31,7 +35,7 @@ STRIPE_KEY = { provider = "op", value = "Stripe/secret-key" } SENDGRID_KEY = { provider = "op", value = "SendGrid/api-key" } ``` -This works, but every time you `cd` into the project (with [shell integration](/guide/shell-integration)), fnox calls 1Password to fetch each secret. This is slow and requires network access. +Without a fnox cache, reads query the provider. Its own CLI may cache data or require a network connection and an authentication prompt. Sync gives fnox a local encrypted copy that it can read independently of the source provider. With `fnox sync`, you pull those values once and cache them locally with a fast, offline encryption provider: @@ -39,9 +43,9 @@ With `fnox sync`, you pull those values once and cache them locally with a fast, fnox sync --provider sync-age --local-file ``` -Now entering the directory is instant β€” secrets are decrypted locally from age without any remote calls. +Subsequent reads decrypt with age locally. The cache stays unchanged until you run sync again. -## How It Works +## How it works 1. fnox reads all secrets from your merged config 2. It resolves each secret's plaintext value from the original remote provider @@ -50,7 +54,7 @@ Now entering the directory is instant β€” secrets are decrypted locally from age When fnox resolves secrets, it checks for a `sync` field first and uses that instead of calling the original provider. -## Basic Usage +## Basic usage ```bash # Set up a personal age provider if you haven't already. Replace the generated @@ -67,6 +71,8 @@ provider is machine-scoped and can be reused across checkouts. You can instead put the personal provider in `fnox.local.toml` if each project needs different settings. +`--local-file` requires normal config discovery: run from the project directory with the default config name. It rejects explicit paths such as `-c ./fnox.toml`, because those paths would not load the adjacent local cache on later reads. + ### Preview what would be synced ```bash @@ -91,7 +97,7 @@ fnox sync --provider sync-age --local-file --source op fnox sync --provider sync-age --local-file --filter "^DB_" ``` -## What It Looks Like +## What it looks like If you keep the personal provider in the project-local override, your files look like this after syncing. With the global setup above, the @@ -117,34 +123,32 @@ SENDGRID_KEY = { provider = "op", value = "SendGrid/api-key" } type = "age" recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] -[secrets] -DATABASE_URL = { - provider = "op", - value = "Database/url", - sync = { - provider = "sync-age", - value = "YWdlLWVuY3J5cHRpb24...", - }, -} -STRIPE_KEY = { - provider = "op", - value = "Stripe/secret-key", - sync = { - provider = "sync-age", - value = "YWdlLWVuY3J5cHRpb24...", - }, -} -SENDGRID_KEY = { - provider = "op", - value = "SendGrid/api-key", - sync = { - provider = "sync-age", - value = "YWdlLWVuY3J5cHRpb24...", - }, -} +[secrets.DATABASE_URL] +provider = "op" +value = "Database/url" + +[secrets.DATABASE_URL.sync] +provider = "sync-age" +value = "YWdlLWVuY3J5cHRpb24..." + +[secrets.STRIPE_KEY] +provider = "op" +value = "Stripe/secret-key" + +[secrets.STRIPE_KEY.sync] +provider = "sync-age" +value = "YWdlLWVuY3J5cHRpb24..." + +[secrets.SENDGRID_KEY] +provider = "op" +value = "SendGrid/api-key" + +[secrets.SENDGRID_KEY.sync] +provider = "sync-age" +value = "YWdlLWVuY3J5cHRpb24..." ``` -When you `cd` into the project, fnox sees the `sync` field and decrypts with age locally β€” no 1Password calls. +The ciphertext above is abbreviated; let `fnox sync` generate the actual values. The original provider references remain in `fnox.toml`. When you `cd` into the project, fnox sees the `sync` field and decrypts with age locally β€” no 1Password calls. ::: tip Sync cache vs. encrypted secrets in git A sync cache is personal: its recipient belongs in the global config or @@ -160,7 +164,7 @@ Provider definitions are replaced as a unit when configs are merged. A local name, so use a distinct name such as `sync-age` for the cache. ::: -## Hardware-Backed Decryption +## Hardware-backed decryption The sync cache is only as secure as the age key that decrypts it. Rather than keeping that key in a plaintext file on disk, you can hold it in hardware @@ -267,7 +271,7 @@ Add the resulting recipient to the provider's `recipients`, point `key_file` at the identity file, and sync as usual. If the identity must never touch host memory, use a YubiKey rather than FIDO2-HMAC. -### Age Plugins vs. Native Hardware Providers +### Age plugins vs. native hardware providers fnox also ships native [`yubikey`](/providers/yubikey) and [`fido2`](/providers/fido2) providers that skip age entirely: they derive a @@ -290,7 +294,7 @@ For a personal sync cache either works well; pick the native providers if you don't want to install age plugins, and the age route if you want backup recipients or hardware-free syncing. -## Refreshing the Cache +## Refreshing the cache When secrets change in the remote provider, re-run sync to update the local cache: @@ -300,55 +304,13 @@ fnox sync --provider sync-age --local-file --force The `--force` flag skips the confirmation prompt. fnox re-fetches from the original provider and re-encrypts. -## What About CI? - -The sync cache is a per-developer convenience β€” don't sync in CI. Let CI -authenticate to the remote provider directly (e.g. a 1Password -[service account token](https://developer.1password.com/docs/service-accounts/) -or an AWS role), or give it [age-encrypted secrets committed to -git](/providers/age#ci-cd-setup) with its own key. The committed `fnox.toml` -references resolve the same way in both cases. +## What about CI? -## Full Workflow Example +CI can read the committed references directly using a service account token or cloud identity. Install fnox and any required provider CLI, then supply authentication through your CI secret store. -Configure the personal provider once per machine: - -```bash -# 1. Set up your age key if it does not already exist -mkdir -p ~/.config/fnox -if [ ! -f ~/.config/fnox/age.txt ]; then - age-keygen -o ~/.config/fnox/age.txt -fi -export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt) - -# 2. Read the recipient, failing before changing any config if the key is invalid -recipient=$(age-keygen -y ~/.config/fnox/age.txt 2>/dev/null) -if [ -z "$recipient" ]; then - echo "Could not find an age public key in ~/.config/fnox/age.txt" >&2 - exit 1 -fi - -# 3. Add the machine-wide provider, then replace its age1... placeholder -# with $recipient in the file opened by the second command -fnox provider add sync-age age --global -"${EDITOR:-vi}" "${FNOX_CONFIG_DIR:-$HOME/.config/fnox}/config.toml" -``` - -Then reuse that provider in every checkout: - -```bash -# 1. Clone a project with 1Password secrets in fnox.toml -git clone https://github.com/myorg/my-api && cd my-api - -# 2. Sync all 1Password secrets to local age encryption -fnox sync --provider sync-age --local-file --force - -# 3. Done β€” entering the directory is now instant -cd .. && cd my-api -# Secrets load from local age cache, no 1Password calls -``` +A personal sync cache is usually unnecessary on an ephemeral runner. If you intentionally cache in CI, manage its encryption key, expiry, and refresh policy separately from developers' caches. -## Next Steps +## Next steps - [Golden Path Setup](/guide/golden-path) - Zero-to-working walkthrough of this workflow - [Per-User Daemon](/guide/daemon) - Cache resolved secrets in memory for a session diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md new file mode 100644 index 00000000..f60598bd --- /dev/null +++ b/docs/guide/troubleshooting.md @@ -0,0 +1,107 @@ +--- +description: "Diagnose configuration overrides, stale caches, authentication failures, age keys, and missing environment variables." +--- + +# Troubleshooting + +Start by checking which configuration fnox loaded, then test the provider involved. These commands help narrow the problem without printing resolved secret values: + +```sh +fnox --version +fnox config-files +fnox profiles +fnox list --sources +fnox doctor +``` + +## The wrong value is being loaded + +Inspect the active profile and the files in the configuration stack: + +```sh +fnox --profile staging config-files +fnox --profile staging list --sources +``` + +Closer directories override parent directories. At each level, profile-specific and local files can override the main file. A global config is also loaded. See [configuration layering](/guide/hierarchical-config). + +To read only a particular project file and its imports, pass an explicit path: + +```sh +fnox -c ./fnox.toml list --sources +``` + +This skips directory discovery and adjacent local overrides, but still loads the global config. See [explicit config paths](/reference/configuration#explicit-config-paths). + +## A secret changed in the vault but fnox still returns the old value + +Check both kinds of optional cache: + +- **Encrypted local cache:** refresh it with `fnox sync --provider sync-age --local-file`. Use the name of your own sync provider. +- **Daemon memory cache:** clear it with `fnox daemon clear`, or bypass the daemon for one invocation with `fnox --no-daemon check --all`. + +Bypassing the daemon does not remove a `sync` value from your config. Refresh the sync cache to fetch the current remote value. For a direct project-file check without local overrides, use `fnox -c ./fnox.toml --no-daemon check --all`. + +## Authentication fails + +Test a single configured provider by its instance name: + +```sh +fnox provider test op +``` + +Then follow that [provider's authentication instructions](/providers/overview). CLI-backed providers need their CLI on `PATH`; API-backed providers need the appropriate credentials and permissions. + +In a terminal, fnox can offer to run the provider's authentication command. `prompt_auth = false`, `FNOX_PROMPT_AUTH=false`, and `--non-interactive` affect prompting. For CI, supply non-interactive credentials before running fnox. + +A provider connection test checks access to the service. `fnox check --all` also checks the configured secret references. + +## Age cannot decrypt a secret + +Check the [identity selection order](/providers/age#set-decryption-key). An exported `FNOX_AGE_KEY` takes precedence over the provider's `identity` and `key_file` settings. + +Compare the public recipient derived from your private key with the provider's recipients: + +```sh +age-keygen -y ~/.config/fnox/age.txt +``` + +Adding a recipient to the config does not update existing ciphertext. Someone who can already decrypt it must run `fnox reencrypt --provider age` for the affected profiles. See [adding a team member](/providers/age#adding-a-new-team-member). + +## A command does not see a secret + +Check the secret's [`env` setting](/reference/configuration#env-1): + +- `true`: shell integration and `fnox exec`. +- `"exec"`: subprocess injection, excluded from the interactive shell. +- `false`: no normal environment injection; explicit reads and internal dependencies can still use it. + +Shell expansion happens before fnox starts. To expand a secret inside the child, use a child shell and single quotes: + +```sh +fnox exec -- sh -c 'test -n "$DATABASE_URL"' +``` + +An `as_file = true` secret supplies a file path, not the raw contents. Configure your application to read the file. + +## Shell integration is not loading + +Confirm you used the activation command for your actual shell and placed it in the right startup file. Fish, Nushell, and PowerShell do not use Bash's `eval` syntax. See [shell integration](/guide/shell-integration). + +Try the same command through `fnox exec`. If that works, the problem is in shell activation or injection settings. Enable hook diagnostics with `FNOX_SHELL_OUTPUT=debug` while investigating. + +## A write command asks for a profile + +When multiple profiles are active, select a write target explicitly: + +```sh +fnox -P aws,staging --write-profile staging set DATABASE_URL +``` + +Read commands compose the profiles in order. Write commands need one destination. See [profile composition](/guide/profiles#composing-multiple-profiles). + +## Report an issue + +Include the fnox version, OS, failing command, first substantive error, and a minimal configuration using dummy values. Describe the provider type and whether sync or daemon caching is enabled. + +Review logs and config before attaching them: `fnox get`, `fnox export`, `fnox list --values`, and verbose provider tools may print secrets. Open an issue in the [GitHub tracker](https://github.com/jdx/fnox/issues). diff --git a/docs/guide/tui.md b/docs/guide/tui.md index 470e55da..b98d53c3 100644 --- a/docs/guide/tui.md +++ b/docs/guide/tui.md @@ -1,6 +1,10 @@ -# TUI Dashboard +--- +description: "Browse secrets and profiles in the fnox terminal dashboard, inspect values, copy secrets, and understand editing limits." +--- -fnox includes an interactive terminal user interface (TUI) for browsing and managing your secrets visually. +# Terminal dashboard + +Use `fnox tui` to browse, filter, and copy secrets in a terminal. Values are masked by default. Changes made in the dashboard are temporary; use `fnox set` to persist a value. ## Launch the TUI @@ -10,7 +14,7 @@ fnox tui ## Features -### Secret List +### Secret list The main view shows all secrets in the current profile with their status: @@ -20,15 +24,15 @@ The main view shows all secrets in the current profile with their status: Use arrow keys or `j`/`k` to navigate through the list, `g`/`G` to jump to the top or bottom, and `Tab` to switch focus between the providers and secrets panes. -### Search Filtering +### Search filtering Press `/` to enter search mode. Type to filter secrets by name. The list updates in real time as you type. Press `Enter` to leave search mode and keep the filter, or `Esc` to clear the search and return to the full list. -### Profile Switching +### Profile switching Press `P` to open the profile picker. Select a different profile to view its secrets. This allows you to quickly compare secrets across environments (dev, staging, production). -### Secret Details +### Secret details Press `Enter` on any secret to view its details: @@ -41,11 +45,11 @@ Press `Enter` on any secret to view its details: Press `c` in the detail view to copy the value, or any other key to close it. -### Copy to Clipboard +### Copy to clipboard Press `c` to copy the currently selected secret's value to your clipboard. A confirmation message appears briefly at the bottom of the screen. -### Edit Secrets +### Edit secrets Press `e` to edit the selected secret's value, or `s` to add a new secret. This opens an input field where you can modify the value. Press `Enter` to confirm or `Esc` to cancel. @@ -53,7 +57,7 @@ Press `e` to edit the selected secret's value, or `s` to add a new secret. This Edits and new secrets created in the TUI are temporary and stored in memory only. They are **not** persisted to your config file, and `d` (delete) is not yet implemented. To permanently change a secret, use `fnox set` or `fnox remove`. ::: -## Keyboard Shortcuts +## Keyboard shortcuts | Key | Action | | ------------ | -------------------------------------- | @@ -72,7 +76,7 @@ Edits and new secrets created in the TUI are temporary and stored in memory only | `P` | Open profile picker | | `?` | Show help | -## Mouse Support +## Mouse support The TUI supports mouse interactions: @@ -82,18 +86,23 @@ The TUI supports mouse interactions: ## Tips -### Quickly Find a Secret +### Quickly find a secret 1. Press `/` to search 2. Type part of the secret name 3. Press `Enter` to leave search mode, then `Enter` again to view the selected match -### Compare Environments +### Compare environments 1. Press `P` to open the profile picker 2. Switch between profiles to see how secrets differ 3. Use `c` to copy values you need -### Secure Viewing +### Secure viewing The TUI masks values in the main list by default and never prints the full value on screen. Press `V` to reveal truncated previews when you need them, and use `c` to copy a value to the clipboard without displaying it. + +## Next steps + +- [Set a secret](/cli/set): save a value to a configured provider. +- [Profiles](/guide/profiles): choose which environment the dashboard displays. diff --git a/docs/guide/what-is-fnox.md b/docs/guide/what-is-fnox.md index 75ef4b8d..dd69a430 100644 --- a/docs/guide/what-is-fnox.md +++ b/docs/guide/what-is-fnox.md @@ -1,53 +1,52 @@ -# What is fnox? - -fnox is a secrets management tool that works with both encrypted secrets in git and remote cloud providers. - -## The Problem +--- +description: "Learn how fnox connects encrypted files and secret stores to your shell, applications, and CI jobs." +--- -Secrets are typically managed in one of two ways: - -1. **In git, encrypted** (hopefully) -2. **Remote**, typically a cloud provider like AWS Secrets Manager - -## The Solution +# What is fnox? -fnox works with eitherβ€”or both! Each has its pros and cons. Either way, fnox gives you a nice front-end to manage secrets and make them easy to work with in dev/CI/prod. +fnox is a command-line tool that loads secrets from encrypted files, password managers, and cloud services. It gives your application environment variables while keeping the storage and authentication choices in a versioned `fnox.toml` file. -fnox's config file, `fnox.toml`, will either contain the encrypted secrets, or a reference to a secret in a cloud provider. You can either use `fnox exec -- ` to run a command with the secrets, or you can use the [shell integration](/guide/shell-integration) to automatically load the secrets into your shell environment when you `cd` into a directory with a `fnox.toml` file. +```sh +fnox exec -- npm start +``` -## The Golden Path +Your application reads its usual environment variables. fnox resolves them before starting the command. -fnox supports a lot of provider combinations, but there's one workflow we recommend β€” the golden path: +## Choose where secrets live -1. **Keep secrets in a remote vault** like [1Password](/providers/1password) and commit only references to them in `fnox.toml`. The vault stays the single source of truth and nothing sensitive goes into git. -2. **Cache them locally with [`fnox sync`](/guide/sync)**, which re-encrypts each secret to your personal [age](/providers/age) key in the gitignored `fnox.local.toml`. -3. **Load them from the cache** via [shell integration](/guide/shell-integration) or `fnox exec`. Decryption is local, so it's instant and works offline. +| Storage model | What goes in `fnox.toml` | What you need to read it | +| ------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------- | +| Encrypted in the config | Ciphertext and public encryption settings | A matching age key, hardware token, or access to a KMS key | +| In a vault or local store | The provider configuration and item reference | Access to that provider | +| Non-sensitive defaults | Plaintext values such as `LOG_LEVEL = { default = "info" }` | No credentials | -You can also keep the age key in hardware: [Apple's Secure Enclave (Touch ID)](/guide/sync#apple-secure-enclave-touch-id), a [YubiKey](/guide/sync#yubikey), or a [TPM or FIDO2 token](/guide/sync#tpm-and-fido2). A Secure Enclave or TPM key only decrypts on the machine that created it, while a YubiKey travels with you. See [Syncing Secrets Locally](/guide/sync) for the walkthroughs and what each option actually protects against, or follow the complete [Golden Path Setup](/guide/golden-path) example from start to finish. +You can mix these models within a project. A development profile might use age while production reads AWS Secrets Manager. Cloning the repository gives you the configuration; access still depends on your keys or provider permissions. -## Why Choose fnox? +See [how resolution works](/guide/how-it-works) and the [provider catalog](/providers/overview). -### Works with Your Existing Infrastructure +## The golden path -Already using AWS Secrets Manager? 1Password? age encryption? fnox integrates with all of them. Mix and match providers based on your needs. +For a team that already uses a remote vault, a useful workflow is: -### Secrets in Version Control (Done Right) +1. Keep secrets in the vault and commit their references in `fnox.toml`. +2. Run [`fnox sync`](/guide/sync) to encrypt a personal copy into the gitignored `fnox.local.toml` using a local provider such as age. +3. Use `fnox exec` or [shell integration](/guide/shell-integration) to read that copy without contacting the vault. -Store encrypted secrets in git using age, AWS KMS, Azure KMS, or GCP KMS. Your team can clone the repo and immediately have access to development secrets. +The vault remains the source of truth. The cache is a snapshot: run sync again after a secret changes. A personal age key can also use [hardware-backed decryption](/guide/sync#hardware-backed-decryption). -### Multi-Environment Made Easy +Follow the [vault and local cache walkthrough](/guide/golden-path). If you want to start without a vault, follow the [age quick start](/guide/quick-start). -Use profiles to manage different secrets for dev, staging, and productionβ€”all in the same config file. +## Choose how commands receive secrets -### Developer Experience First +- **One command:** `fnox exec -- ` resolves secrets for that subprocess. +- **Your shell:** [shell integration](/guide/shell-integration) loads and unloads values as you change directories. +- **A file-based tool:** [`as_file = true`](/reference/configuration#as-file) supplies a temporary file path instead of the value. +- **An API client or agent:** the [credential proxy](/guide/proxy) supplies placeholders and injects credentials into matching requests. The [MCP server](/guide/mcp) offers selected secret retrieval and command execution. -- Simple TOML configuration -- Shell integration for automatic secret loading -- Works offline (with encrypted secrets) -- No vendor lock-in +Use [profiles](/guide/profiles) for environment-specific settings and [hierarchical configuration](/guide/hierarchical-config) to share configuration across directories. -## Why is this a standalone CLI and not part of mise? +## Why a standalone CLI? -[mise](https://mise.jdx.dev) has support for [encrypted secrets](https://mise.jdx.dev/environments/secrets/) but mise's design makes it a poor fit for remote secrets. mise reloads its environment too frequentlyβ€”whenever a directory is changed, `mise x` is run, a shim is called, etc. For any other use case like this, mise leverages caching, but secrets are an area where caching is a bad idea for obvious reasons. It might be possible to change mise's design to retain its environment in part to better support something like this but that's a huge challenge. +Secret resolution has its own lifecycle: authentication prompts, remote reads, local decryption, cache refreshes, and credential expiry. Keeping that lifecycle in fnox lets it work with any shell, task runner, or CI system. -Basically it's just too hard to get remote secrets to work effectively with mise so I made this a standalone tool. +[mise](/guide/mise-integration) can install fnox and run tasks through `fnox exec`. fnox handles secret resolution, including its optional [memory cache](/guide/daemon) and [encrypted sync cache](/guide/sync). diff --git a/docs/index.md b/docs/index.md index fa990fdf..9609e60f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,101 +1,11 @@ --- layout: home -title: Secure secrets for development workflows - -hero: - name: fnox - text: Fort Knox for your secrets - tagline: Manage secrets with encryption or cloud providers - or both! - image: - src: /logo.svg - alt: fnox - actions: - - theme: brand - text: Get Started - link: /guide/what-is-fnox - - theme: alt - text: View on GitHub - link: https://github.com/jdx/fnox - -features: - - icon: πŸ” - title: Multiple Provider Support - details: Works with age, AWS KMS/SM, Azure, GCP, 1Password, Bitwarden, Bitwarden Secrets Manager, Infisical, password-store, HashiCorp Vault, and more. - - icon: πŸ“ - title: Secrets in Git (Encrypted) - details: Store encrypted secrets in version control with age, AWS KMS, Azure KMS, or GCP KMS. - - icon: ☁️ - title: Cloud Secret Storage - details: Reference secrets stored in AWS Secrets Manager, AWS Parameter Store, Azure Key Vault, GCP Secret Manager, or Vault. - - icon: πŸ”„ - title: Shell Integration - details: Automatically load secrets when you cd into a directory with a fnox.toml file. - - icon: ⚑ - title: Per-User Daemon - details: Optionally cache resolved secrets in memory for faster repeated reads. - - icon: 🎯 - title: Multi-Environment Support - details: Use profiles to manage different secrets for dev, staging, and production. - - icon: πŸ› οΈ - title: Developer Friendly - details: Simple TOML config, easy CLI, and smooth integration with your existing workflow. +title: Your secrets, where you need them +description: Load secrets from encrypted files, password managers, and cloud services into your commands. One configuration for development and CI. --- -## Quick Example - -```bash -# Initialize fnox in your project -fnox init - -# Set a secret (stores it encrypted in fnox.toml) -fnox set DATABASE_URL "postgresql://localhost/mydb" - -# Get a secret -fnox get DATABASE_URL - -# Run commands with secrets loaded as env vars -fnox exec -- npm start - -# Enable shell integration (auto-load secrets on cd) -eval "$(fnox activate bash)" # or zsh, fish β€” see docs for Nushell -``` - -## The Golden Path - -The recommended setup: keep secrets in a vault like 1Password, commit only references to them in `fnox.toml`, and cache them locally with [`fnox sync`](/guide/sync) under a personal age key β€” which can even live in [hardware like a Secure Enclave, YubiKey, or TPM](/guide/sync#hardware-backed-decryption). - -```bash -fnox sync --provider sync-age --local-file -``` - -The vault stays the source of truth, but secrets load instantly and offline on every `cd`. See [The Golden Path](/guide/what-is-fnox#the-golden-path) for details, or jump straight to the [setup walkthrough](/guide/golden-path). - -## How It Works - -fnox uses a simple TOML config file (`fnox.toml`) that you check into git. Secrets are either: - -1. **Encrypted inline** - The encrypted ciphertext lives in the config file -2. **Remote references** - The config contains a reference (like "my-db-password") that points to a secret in AWS/1Password/etc. - -You configure providers (encryption methods or cloud services), then assign each secret to a provider. fnox handles the rest. - -```toml -# fnox.toml -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } - -[secrets] -DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24uLi4=" } # ← encrypted ciphertext, safe to commit -API_KEY = { default = "dev-key-12345" } # ← plain default value for local dev -``` - -## Supported Providers - -fnox works with over 20 providers across four categories: - -- **πŸ” Encryption (secrets in git)** β€” age (with SSH keys and hardware plugins), FIDO2, YubiKey, AWS/Azure/GCP KMS -- **☁️ Cloud secret storage** β€” AWS Secrets Manager & Parameter Store, Azure Key Vault, GCP Secret Manager, HashiCorp Vault, Doppler, and more -- **πŸ”‘ Password managers** β€” 1Password, Bitwarden, Proton Pass, Infisical -- **πŸ’» Local storage** β€” OS keychain, KeePass, password-store + -See the [Providers Overview](/providers/overview) for the full list and a comparison of trade-offs. + diff --git a/docs/leases/aws-sts.md b/docs/leases/aws-sts.md index bd385617..4977c438 100644 --- a/docs/leases/aws-sts.md +++ b/docs/leases/aws-sts.md @@ -1,3 +1,7 @@ +--- +description: "Create temporary AWS role credentials with fnox, including session duration, external IDs, and SSO-backed authentication." +--- + # AWS STS The `aws-sts` lease backend calls [AWS STS AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) to create short-lived AWS credentials from a long-lived IAM user or role. @@ -30,11 +34,11 @@ The backend needs AWS credentials to call `sts:AssumeRole`. Before creating a le The AWS SDK's default credential chain then decides which of them is used. If none are found, fnox prints: -``` +```text AWS credentials not found. Run 'aws sso login' or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. ``` -## Credentials Produced +## Credentials produced | Environment Variable | Description | | ----------------------- | -------------------- | @@ -59,8 +63,8 @@ type = "1password" vault = "Development" [secrets] -AWS_ACCESS_KEY_ID = { provider = "op", value = "AWS IAM/access key" } -AWS_SECRET_ACCESS_KEY = { provider = "op", value = "AWS IAM/secret key" } +AWS_ACCESS_KEY_ID = { provider = "op", value = "AWS IAM/access key", env = false } +AWS_SECRET_ACCESS_KEY = { provider = "op", value = "AWS IAM/secret key", env = false } [leases.aws] type = "aws-sts" @@ -94,11 +98,11 @@ If you use AWS SSO, no stored credentials are needed β€” just log in first: ```bash aws sso login --profile my-sso-profile -# fnox picks up the SSO session automatically -fnox exec -- aws s3 ls +# Select the same profile used for login +AWS_PROFILE=my-sso-profile fnox exec -- aws s3 ls ``` -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches - [AWS Secrets Manager provider](/providers/aws-sm) β€” for storing secrets in AWS diff --git a/docs/leases/azure-token.md b/docs/leases/azure-token.md index bc65b1e2..0db1ca0d 100644 --- a/docs/leases/azure-token.md +++ b/docs/leases/azure-token.md @@ -1,4 +1,8 @@ -# Azure Token +--- +description: "Acquire temporary Azure access tokens for a resource scope and pass them to commands that accept bearer tokens." +--- + +# Azure token The `azure-token` lease backend obtains a short-lived OAuth2 bearer token from Microsoft Entra ID (formerly Azure Active Directory) using either a service principal or the Azure CLI session. @@ -25,11 +29,11 @@ The backend needs Azure credentials. fnox looks for them in this order: If none are found, fnox prints: -``` +```text Azure credentials not found. Run 'az login' or set AZURE_CLIENT_ID/AZURE_CLIENT_SECRET/AZURE_TENANT_ID. ``` -## Credentials Produced +## Credentials produced | Environment Variable | Description | | -------------------- | ------------------- | @@ -52,9 +56,9 @@ type = "1password" vault = "Development" [secrets] -AZURE_CLIENT_ID = { provider = "op", value = "Azure SP/client id" } -AZURE_CLIENT_SECRET = { provider = "op", value = "Azure SP/client secret" } -AZURE_TENANT_ID = { provider = "op", value = "Azure SP/tenant id" } +AZURE_CLIENT_ID = { provider = "op", value = "Azure SP/client id", env = false } +AZURE_CLIENT_SECRET = { provider = "op", value = "Azure SP/client secret", env = false } +AZURE_TENANT_ID = { provider = "op", value = "Azure SP/tenant id", env = false } [leases.azure] type = "azure-token" @@ -62,16 +66,18 @@ scope = "https://management.azure.com/.default" ``` ```bash -fnox exec -- az resource list +fnox exec -- sh -c 'curl -fsS -H "Authorization: Bearer $AZURE_ACCESS_TOKEN" "https://management.azure.com/subscriptions?api-version=2022-12-01"' ``` +The example explicitly passes the leased token to the [Azure subscriptions API](https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list?view=rest-resources-2022-12-01). Client tools must consume the configured token variable; creating `AZURE_ACCESS_TOKEN` does not make every Azure tool use it automatically. + ### With Azure CLI login ```bash az login # fnox picks up the CLI session automatically -fnox exec -- az resource list +fnox exec -- sh -c 'curl -fsS -H "Authorization: Bearer $AZURE_ACCESS_TOKEN" "https://management.azure.com/subscriptions?api-version=2022-12-01"' ``` ### Custom env var name @@ -92,7 +98,7 @@ env_var = "GRAPH_TOKEN" | `https://database.windows.net/.default` | Azure SQL Database | | `https://storage.azure.com/.default` | Azure Storage | -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches - [Azure Key Vault Secrets provider](/providers/azure-sm) β€” for storing secrets in Azure diff --git a/docs/leases/cloudflare.md b/docs/leases/cloudflare.md index 628b595c..b0ff3dd9 100644 --- a/docs/leases/cloudflare.md +++ b/docs/leases/cloudflare.md @@ -1,8 +1,12 @@ +--- +description: "Create temporary Cloudflare API tokens with fnox, using user or account ownership, scoped policies, and expiry." +--- + # Cloudflare The `cloudflare` lease backend creates short-lived, scoped Cloudflare API tokens using the [Cloudflare API Tokens API](https://developers.cloudflare.com/api/resources/user/subresources/tokens/methods/create/). A parent token with the **API Tokens: Edit** permission creates child tokens that automatically expire. -By default, the child token inherits the same policies (permissions and resource scopes) as the parent token. You can override this by specifying explicit `policies` in the configuration. +By default, the child token inherits the parent's policies except the API Tokens permission groups that cannot be delegated. You can override this by specifying explicit `policies` in the configuration. Set `token_type = "account"` to use [account-owned tokens](https://developers.cloudflare.com/fundamentals/api/get-started/account-owned-tokens/) (`/accounts/{id}/tokens`) instead of user tokens. Account tokens are ideal for CI/CD and team workflows since they aren't tied to an individual user. @@ -16,7 +20,7 @@ duration = "1h" [[leases.cf.policies]] effect = "allow" -resources = { "com.cloudflare.api.account.abc123def456" = "*" } +resources = { "com.cloudflare.api.account.zone.*" = "*" } [[leases.cf.policies.permission_groups]] id = "c8fed203ed3043cba015a93ad1616f1f" @@ -57,11 +61,11 @@ The backend needs a parent API token that can create other tokens. fnox looks fo The parent token must have the **API Tokens: Edit** permission. If not found, fnox prints: -``` +```text Cloudflare API token not found. Set CLOUDFLARE_API_TOKEN with a token that has 'API Tokens: Edit' permission. ``` -## Credentials Produced +## Credentials produced | Environment Variable | Description | | ---------------------- | ---------------------------- | @@ -104,7 +108,7 @@ type = "1password" vault = "Development" [secrets] -CLOUDFLARE_API_TOKEN = { provider = "op", value = "Cloudflare/api token" } +CLOUDFLARE_API_TOKEN = { provider = "op", value = "Cloudflare/api token", env = false } [leases.cf] type = "cloudflare" @@ -113,7 +117,7 @@ duration = "1h" [[leases.cf.policies]] effect = "allow" -resources = { "com.cloudflare.api.account.abc123def456" = "*" } +resources = { "com.cloudflare.api.account.zone.*" = "*" } [[leases.cf.policies.permission_groups]] id = "c8fed203ed3043cba015a93ad1616f1f" @@ -125,7 +129,7 @@ name = "DNS Read" ``` ```bash -fnox exec -- wrangler deploy +fnox exec -- sh -c 'curl -fsS -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" https://api.cloudflare.com/client/v4/zones' ``` ### Using {account_id} placeholder @@ -175,6 +179,6 @@ name = "Zone Read" Use the permission groups API to find the full list for your account. -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches diff --git a/docs/leases/command.md b/docs/leases/command.md index 9fb1c49b..57074414 100644 --- a/docs/leases/command.md +++ b/docs/leases/command.md @@ -1,4 +1,8 @@ -# Custom Command +--- +description: "Use a custom command to create credentials, return expiry metadata, and optionally revoke a fnox lease." +--- + +# Custom command The `command` lease backend runs an arbitrary script or command to create (and optionally revoke) credentials. Use this for systems that fnox doesn't natively support. @@ -23,7 +27,7 @@ duration = "1h" None β€” fnox can't validate prerequisites without running the command. -## Create Command +## Create command Your script receives these environment variables: @@ -32,7 +36,7 @@ Your script receives these environment variables: | `FNOX_LEASE_DURATION` | Requested duration in seconds | | `FNOX_LEASE_LABEL` | Lease label (default: `fnox-lease`) | -The script must output JSON on stdout: +The script must output JSON on stdout and exit successfully. Send diagnostics to stderr. Ensure `expires_at` matches the real credential expiry; omitting it produces a lease with no tracked expiry, even when `duration` was requested: ```json { @@ -40,7 +44,7 @@ The script must output JSON on stdout: "MY_TOKEN": "tok-abc123", "MY_SECRET": "sec-xyz789" }, - "expires_at": "2024-01-15T10:00:00Z", + "expires_at": "2030-01-15T10:00:00Z", "lease_id": "my-custom-lease-1" } ``` @@ -51,7 +55,9 @@ The script must output JSON on stdout: | `expires_at` | No | Expiry timestamp (RFC3339). Omit for never-expiring leases. | | `lease_id` | No | Unique lease ID. Auto-generated if omitted. | -## Revoke Command +The timestamp above is illustrative. Generate it from the actual credential response, rather than copying a fixed date into a script. + +## Revoke command If `revoke_command` is set, it's called when you run `fnox lease revoke` or `fnox lease cleanup`. It receives: @@ -70,10 +76,11 @@ If `revoke_command` is set, it's called when you run `fnox lease revoke` or `fno ```bash #!/bin/bash +set -euo pipefail # scripts/get-creds.sh # Call your internal API -RESPONSE=$(curl -s https://creds.internal/api/token \ +RESPONSE=$(curl -fsS https://creds.internal/api/token \ --header "Authorization: Bearer $INTERNAL_AUTH" \ --data "ttl=$FNOX_LEASE_DURATION") @@ -87,6 +94,7 @@ Many CLIs output credentials in non-JSON formats. Use `jq` to reshape the output ```bash #!/bin/bash +set -euo pipefail # scripts/get-k8s-token.sh TOKEN=$(kubectl create token my-service-account \ @@ -115,6 +123,7 @@ duration = "1h" ```bash #!/bin/bash +set -euo pipefail # scripts/get-creds.sh LEASE_ID="custom-$(date +%s)" @@ -131,6 +140,7 @@ jq -n \ ```bash #!/bin/bash +set -euo pipefail # scripts/revoke-creds.sh my-tool revoke-token "$FNOX_LEASE_ID" @@ -140,6 +150,7 @@ my-tool revoke-token "$FNOX_LEASE_ID" ```bash #!/bin/bash +set -euo pipefail # scripts/assume-role.sh aws sts assume-role \ @@ -160,6 +171,7 @@ aws sts assume-role \ ```bash #!/bin/bash +set -euo pipefail # scripts/get-creds.sh # Some tools output key=value pairs @@ -167,11 +179,11 @@ OUTPUT=$(my-tool get-creds --format=env) # Parse into JSON with jq echo "$OUTPUT" | jq -Rn ' - [inputs | split("=") | {(.[0]): .[1]}] | add | + [inputs | capture("^(?[^=]+)=(?.*)$") | {(.key): .value}] | add | { credentials: . } ' ``` -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches diff --git a/docs/leases/gcp-iam.md b/docs/leases/gcp-iam.md index 09c83930..aad1bf73 100644 --- a/docs/leases/gcp-iam.md +++ b/docs/leases/gcp-iam.md @@ -1,3 +1,7 @@ +--- +description: "Impersonate a Google Cloud service account to create a temporary access token for a command." +--- + # GCP IAM The `gcp-iam` lease backend calls the [IAM Credentials API](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken) to generate a short-lived OAuth2 access token by impersonating a service account. @@ -28,29 +32,29 @@ The backend needs GCP credentials to call the IAM Credentials API. fnox looks fo If none are found, fnox prints: -``` +```text GCP credentials not found. Run 'gcloud auth application-default login' or set GOOGLE_APPLICATION_CREDENTIALS. ``` -## Credentials Produced +## Credentials produced | Environment Variable | Description | | ---------------------------- | ------------------- | | `CLOUDSDK_AUTH_ACCESS_TOKEN` | OAuth2 access token | -The default `CLOUDSDK_AUTH_ACCESS_TOKEN` is read by the `gcloud` CLI. GCP client libraries (Python, Java, Go, Node.js) typically read `GOOGLE_OAUTH_ACCESS_TOKEN` instead. Set `env_var` to match your target tool: +The default `CLOUDSDK_AUTH_ACCESS_TOKEN` is used by the `gcloud` CLI. For a custom application, configure an environment variable and explicitly pass its value to the application's authentication library; do not assume every Google Cloud SDK reads an OAuth token from the environment. ```toml -# For GCP SDKs (not gcloud CLI): -env_var = "GOOGLE_OAUTH_ACCESS_TOKEN" +# Field within [leases.gcp] +env_var = "MY_GCP_ACCESS_TOKEN" ``` ## Limits -- **Max duration:** 1 hour (can be extended to 12h with an [org policy](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth)) +- **Max duration:** 1 hour for this backend - **Revocation:** No-op β€” tokens expire automatically -## IAM Setup +## IAM setup The calling identity needs the **Service Account Token Creator** role on the target service account: @@ -77,7 +81,7 @@ type = "1password" vault = "Development" [secrets] -GOOGLE_APPLICATION_CREDENTIALS = { provider = "op", value = "GCP SA/key file", as_file = true } +GOOGLE_APPLICATION_CREDENTIALS = { provider = "op", value = "GCP SA/key file", as_file = true, env = false } [leases.gcp] type = "gcp-iam" @@ -110,7 +114,7 @@ scopes = [ ] ``` -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches - [GCP Secret Manager provider](/providers/gcp-sm) β€” for storing secrets in GCP diff --git a/docs/leases/github-app.md b/docs/leases/github-app.md index f762df2a..37bbaf95 100644 --- a/docs/leases/github-app.md +++ b/docs/leases/github-app.md @@ -1,3 +1,7 @@ +--- +description: "Create scoped GitHub App installation tokens with fnox using an app ID, installation ID, and private key." +--- + # GitHub App The `github-app` lease backend creates short-lived [GitHub App installation access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app) using a GitHub App's private key. Installation tokens expire after 1 hour (GitHub's hard limit) and can be scoped to specific permissions and repositories. @@ -55,7 +59,7 @@ The backend needs a GitHub App private key. fnox looks for it in order: If neither is found, fnox prints: -``` +```text GitHub App private key not found. Set FNOX_GITHUB_APP_PRIVATE_KEY or configure private_key_file pointing to a PEM file. ``` @@ -75,7 +79,7 @@ curl -H "Authorization: Bearer $JWT" \ In your GitHub App settings, scroll to "Private keys" and click "Generate a private key". Save the downloaded PEM file to a secure location. -## Credentials Produced +## Credentials produced | Environment Variable | Description | | -------------------- | ------------------------------------- | @@ -145,7 +149,7 @@ type = "1password" vault = "Infrastructure" [secrets] -FNOX_GITHUB_APP_PRIVATE_KEY = { provider = "op", value = "GitHub App/private key" } +FNOX_GITHUB_APP_PRIVATE_KEY = { provider = "op", value = "GitHub App/private key", env = false } [leases.github] type = "github-app" @@ -153,7 +157,7 @@ app_id = "12345" installation_id = "67890" ``` -fnox resolves the secret first, then the lease backend picks it up from the environment. +fnox resolves the private key for the lease backend internally. `env = false` keeps it out of normal subprocess injection; only the generated installation token is passed to the command. ### Custom env var @@ -195,6 +199,6 @@ api_base = "https://github.example.com/api/v3" See the [GitHub API docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app) for the full list. -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches diff --git a/docs/leases/github-oauth.md b/docs/leases/github-oauth.md index 6a4678a0..a13b40be 100644 --- a/docs/leases/github-oauth.md +++ b/docs/leases/github-oauth.md @@ -1,3 +1,7 @@ +--- +description: "Create GitHub OAuth access tokens with fnox through the device flow and cache them for subsequent commands." +--- + # GitHub OAuth The `github-oauth` lease backend creates GitHub App user access tokens with OAuth device flow. It is useful for local automation where you want `GITHUB_TOKEN` or `GH_TOKEN` to be short-lived and tied to the signed-in GitHub user, without storing a personal access token in `fnox.toml`. @@ -38,7 +42,11 @@ fnox exec -- gh pr list Approve the device prompt in your browser. Subsequent runs reuse the cached token while it remains valid. In non-interactive mode (`--non-interactive` or `FNOX_NON_INTERACTIVE`), fnox fails instead of starting the device flow, so authorize once from an interactive terminal first. -## Credentials Produced +## Expiry and revocation + +GitHub controls the returned token lifetime. Inspect the tracked lease rather than assuming the requested `duration` is enforced. This backend relies on expiry; revoking its fnox ledger entry does not provide immediate remote token revocation. + +## Credentials produced | Environment Variable | Description | | -------------------- | ------------------------ | @@ -72,7 +80,7 @@ keyring_cache = false With keyring caching disabled, fnox still caches active lease credentials in its lease ledger for the current project. -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches - [GitHub App](/leases/github-app) β€” installation access tokens for automation diff --git a/docs/leases/vault.md b/docs/leases/vault.md index 50f15196..614f1ce8 100644 --- a/docs/leases/vault.md +++ b/docs/leases/vault.md @@ -1,3 +1,7 @@ +--- +description: "Request dynamic credentials from HashiCorp Vault and configure field mapping, lease duration, and revocation." +--- + # HashiCorp Vault The `vault` lease backend reads dynamic secrets from a HashiCorp Vault secret engine. This works with any Vault dynamic secret backend β€” AWS, database, PKI, etc. You configure which Vault response fields map to which environment variables. @@ -37,7 +41,7 @@ The backend needs a Vault address and token. fnox resolves them in this order: If the address or token is missing, fnox prints one of: -``` +```text Vault address and token not found. Set VAULT_ADDR and VAULT_TOKEN. Vault address not found. Set VAULT_ADDR. Vault token not found. Set VAULT_TOKEN. @@ -45,7 +49,7 @@ Vault token not found. Set VAULT_TOKEN. When `credential_command` is configured, fnox runs it through the platform shell and uses its trimmed stdout as the token. The command is rendered as a [Tera](https://keats.github.io/tera/) template with `address`, `secret_path`, and `namespace` variables, and fnox sets `VAULT_ADDR` and `VAULT_NAMESPACE` in the command's environment from the lease config. Output is cached for five minutes within the current fnox process so repeated lease operations do not repeat the login, and the cache is cleared if Vault rejects the token. The command must finish within 30 seconds. -## Credentials Produced +## Credentials produced Determined by the `env_map` configuration. The keys are field names from the Vault response, and the values are the environment variable names to inject. @@ -85,11 +89,13 @@ password = "DB_PASSWORD" ``` ```bash -fnox exec -- psql -h db.example.com -U "$DB_USER" mydb +fnox exec -- sh -c 'PGPASSWORD="$DB_PASSWORD" psql -h db.example.com -U "$DB_USER" mydb' ``` ### PKI certificates +This backend selects the method and maps response fields, but does not expose arbitrary request-body parameters. Use it only with a role that can issue with these defaults; use a [custom command](/leases/command) if your request needs fields such as `common_name`. + PKI and some other engines require POST requests. Set `method = "post"`: ```toml @@ -113,7 +119,7 @@ type = "1password" vault = "Infrastructure" [secrets] -VAULT_TOKEN = { provider = "op", value = "Vault/token" } +VAULT_TOKEN = { provider = "op", value = "Vault/token", env = false } [leases.vault-aws] type = "vault" @@ -126,7 +132,7 @@ secret_key = "AWS_SECRET_ACCESS_KEY" security_token = "AWS_SESSION_TOKEN" ``` -### With namespace (Enterprise / HCP) +### With namespace (enterprise / HCP) ```toml [leases.vault-db] @@ -161,7 +167,7 @@ password = "DB_PASSWORD" - **Static KV secrets never expire.** KV v2 responses (`data.data`) are unwrapped automatically, and a `lease_duration` of `0` is treated as "no expiry", so the lease stays active until you revoke it. - **GET vs POST.** Most Vault dynamic secret engines use GET (e.g., `aws/creds`, `database/creds`). Some engines like `pki/issue` require POST β€” set `method = "post"` for those. -## See Also +## See also - [Credential Leases](/guide/leases) β€” overview and approaches - [HashiCorp Vault provider](/providers/vault) β€” for reading static KV secrets diff --git a/docs/providers/1password.md b/docs/providers/1password.md index f0dbc1e1..8e475372 100644 --- a/docs/providers/1password.md +++ b/docs/providers/1password.md @@ -1,358 +1,162 @@ +--- +description: "Read 1Password vault items with fnox. Set up local or service-account authentication, field references, and an optional local cache." +--- + # 1Password -Integrate with 1Password to retrieve secrets from your vaults using the 1Password CLI. +Read vault items with the 1Password CLI (`op`). Your `fnox.toml` contains item references; values remain in 1Password. -## Quick Start +For a complete team setup with offline reads, follow [connect a vault and cache locally](/guide/golden-path). -```bash -# 1. Install 1Password CLI -brew install 1password-cli +## Prerequisites -# 2. Create service account and get token -# (via 1Password web interface) +- A 1Password account with access to the vault and items you need. +- The [1Password CLI](https://developer.1password.com/docs/cli/) installed and available as `op`. -# 3. Store token (bootstrap with age) -fnox set OP_SERVICE_ACCOUNT_TOKEN "ops_YOUR_TOKEN" --provider age +## Quick start -# 4. Configure 1Password provider -cat >> fnox.toml << 'EOF' -[providers] -onepass = { type = "1password", vault = "Development" } -EOF +Authenticate with the CLI's interactive or desktop-app integration, then add an existing vault item to your config: -# 5. Add secrets to 1Password (via app or CLI) -op item create --category=login \ - --title="Database" \ - --vault="Development" \ - password="super-secret-password" +```toml +[providers.op] +type = "1password" +vault = "Engineering" -# 6. Reference in fnox -cat >> fnox.toml << 'EOF' [secrets] -DATABASE_PASSWORD = { provider = "onepass", value = "Database" } -EOF - -# 7. Use it -export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) -fnox get DATABASE_PASSWORD +DATABASE_URL = { provider = "op", value = "Database/url" } ``` -## Prerequisites - -- [1Password account](https://1password.com) -- [1Password CLI](https://developer.1password.com/docs/cli) installed - -## Installation - -```bash -# macOS -brew install 1password-cli - -# Linux -curl -sS https://downloads.1password.com/linux/keys/1password.asc | \ - sudo gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" | \ - sudo tee /etc/apt/sources.list.d/1password.list -sudo apt update && sudo apt install 1password-cli - -# Windows (via Scoop) -scoop install 1password-cli +```sh +fnox provider test op +fnox get DATABASE_URL +fnox exec -- npm start ``` -## Setup - -### 1. Create a Service Account +`fnox get` prints the value. Use `fnox check --all` when you only need to verify that it resolves. -1. Go to your [1Password account](https://my.1password.com) -2. Navigate to Settings β†’ Integrations β†’ Service Accounts -3. Click "Create Service Account" -4. Give it a name (e.g., "fnox-dev") -5. Grant access to your vault -6. Copy the `OP_SERVICE_ACCOUNT_TOKEN` (starts with `ops_`) +## Authentication -### 2. Store the Token (Bootstrap) +### Local development -Use age encryption to store the token: +Use the [CLI's supported sign-in methods](https://developer.1password.com/docs/cli/get-started/), including integration with the desktop app. Verify access with `op vault list` before running fnox. -```bash -# First, set up age provider (if not already done) -cat >> fnox.toml << 'EOF' -[providers] -age = { type = "age", recipients = ["age1..."] } -EOF +A service account is not required for an interactive developer session. Authentication prompts and session lifetime depend on your 1Password setup. -# Store the 1Password token encrypted in fnox -fnox set OP_SERVICE_ACCOUNT_TOKEN "ops_YOUR_TOKEN_HERE" --provider age -``` - -Now you can bootstrap the token: - -```bash -export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) -``` - -### 3. Configure 1Password Provider - -```toml -[providers] -onepass = { type = "1password", vault = "Development", account = "my.1password.com" } # account is optional -``` - -## Adding Secrets to 1Password - -### Via 1Password App - -1. Open 1Password app -2. Select your vault (e.g., "Development") -3. Click + to create new item -4. Choose category (Login, Password, etc.) -5. Fill in details -6. Save - -### Via 1Password CLI - -```bash -# Export token first -export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) - -# Create a login item -op item create --category=login \ - --title="Database" \ - --vault="Development" \ - username="admin" \ - password="super-secret-password" - -# Create an API credential -op item create --category=password \ - --title="Stripe API Key" \ - --vault="Development" \ - password="sk_live_abc123xyz789" - -# Create with custom fields -op item create --category=login \ - --title="AWS Credentials" \ - --vault="Development" \ - "Access Key=AKIAIOSFODNN7EXAMPLE" \ - "Secret Key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" -``` +### CI and automation -## Referencing Secrets +Create a [1Password service account](https://developer.1password.com/docs/service-accounts/) with access to the required vaults. Supply its token through your CI secret store as `OP_SERVICE_ACCOUNT_TOKEN`. fnox also accepts `FNOX_OP_SERVICE_ACCOUNT_TOKEN`, which takes precedence. -Add references to `fnox.toml`: +This workflow step assumes fnox and `op` are installed: -```toml -[secrets] -DATABASE_PASSWORD = { provider = "onepass", value = "Database" } # Item name (fetches 'password' field) -DB_USERNAME = { provider = "onepass", value = "Database/username" } # Specific field -API_KEY = { provider = "onepass", value = "op://Development/API Keys/credential" } # Full op:// URI +```yaml +- name: Run tests with secrets + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: fnox --non-interactive exec --if-missing error -- npm test ``` -## Reference Formats - -fnox supports multiple ways to reference 1Password items: +Scope the service account to the vaults and operations the job needs. Do not put the token in committed plaintext configuration. -### 1. Item Name (Gets Password Field) +## Configuration ```toml -[secrets] -MY_SECRET = { provider = "onepass", value = "My Item" } # β†’ Gets the 'password' field +[providers.op] +type = "1password" +vault = "Engineering" +account = "my.1password.com" # Optional account selector ``` -### 2. Item Name + Field - -```toml -[secrets] -USERNAME = { provider = "onepass", value = "Database/username" } # β†’ Gets 'username' field -PASSWORD = { provider = "onepass", value = "Database/password" } # β†’ Gets 'password' field -``` +`op` is the fnox provider instance name. Use it in secret definitions and commands such as `fnox provider test op`. -Common fields: `username`, `password`, `url`, `notes` +## Reference formats -### 3. Full op:// URI +| Reference | What fnox reads | +| ------------------------------------ | ------------------------------------- | +| `Database` | The item's password field | +| `Database/username` | A named field in the configured vault | +| `op://Engineering/Database/password` | A full 1Password secret reference | ```toml [secrets] -API_KEY = { provider = "onepass", value = "op://Development/API Keys/credential" } +DB_PASSWORD = { provider = "op", value = "Database" } +DB_USER = { provider = "op", value = "Database/username" } +API_KEY = { provider = "op", value = "op://Engineering/Service/api-key" } ``` -Format: `op://VAULT/ITEM/FIELD` +Use the field names in your items. Full `op://` references can be copied from 1Password and are useful when a project reads from several vaults. -## Usage +## Create and update items -```bash -# Export token (once per session) -export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) +Create or update items in the 1Password app or with the `op` CLI, then add their references to `fnox.toml`. For custom fields, use the exact field name in the reference. -# Get secrets -fnox get DATABASE_PASSWORD -fnox get DB_USERNAME +The configuration can be committed because it contains references. Vault names, item names, and field names are still visible to anyone who can read the repository. -# Run commands -fnox exec -- npm start -``` +## Separate environments -## Multi-Environment Example +Override the provider and references with a profile: ```toml -# Bootstrap token (encrypted in git) -[providers] -age = { type = "age", recipients = ["age1..."] } -onepass = { type = "1password", vault = "Development" } +[providers.op] +type = "1password" +vault = "Development" [secrets] -OP_SERVICE_ACCOUNT_TOKEN = { provider = "age", value = "encrypted-token..." } -DATABASE_URL = { provider = "onepass", value = "Dev Database" } +DATABASE_URL = { provider = "op", value = "Database/url" } -# Production: Different 1Password vault -[profiles.production.providers] -onepass = { type = "1password", vault = "Production" } +[profiles.production.providers.op] +type = "1password" +vault = "Production" [profiles.production.secrets] -DATABASE_URL = { provider = "onepass", value = "Prod Database" } +DATABASE_URL = { provider = "op", value = "Database/url", if_missing = "error" } ``` -## CI/CD Example - -### GitHub Actions - -```yaml -name: Deploy -on: [push] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v4 - - - name: Setup fnox age key - env: - FNOX_AGE_KEY: ${{ secrets.FNOX_AGE_KEY }} - run: echo "Age key configured" - - - name: Deploy with 1Password secrets - run: | - # Bootstrap 1Password token from fnox - export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) - - # Now we can access 1Password secrets - fnox exec --profile production -- ./deploy.sh +```sh +fnox exec --profile production -- ./deploy.sh ``` -## Team Workflow - -1. **Admin creates service account** in 1Password -2. **Admin stores token** encrypted in fnox: - ```bash - fnox set OP_SERVICE_ACCOUNT_TOKEN "ops_..." --provider age - git add fnox.toml && git commit -m "Add 1Password token" - ``` -3. **Admin creates items** in 1Password vault -4. **Admin adds references** to fnox.toml: - ```toml - [secrets] - DATABASE_URL = { provider = "onepass", value = "Database" } - ``` -5. **Team members pull and use**: - ```bash - git pull - export FNOX_AGE_KEY=... - export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) - fnox exec -- npm start - ``` - -## Service Account vs Personal Token - -### Service Account (Recommended) - -- βœ… Designed for CI/CD and automation -- βœ… Doesn't expire -- βœ… No MFA required -- βœ… Scoped access to specific vaults - -```bash -# Use service account token -export OP_SERVICE_ACCOUNT_TOKEN="ops_..." -``` +Profiles choose configuration, not authorization. The identity used by `op` must have access to the selected vault. -### Personal Token (Not Recommended) +## Store a bootstrap token with age -- ❌ Requires interactive login -- ❌ Subject to MFA -- ❌ Session expires +If you need to keep a service account token locally, first configure [age](/providers/age), then store the token through the hidden prompt: -```bash -# Personal login (interactive) -eval $(op signin) +```sh +fnox set OP_SERVICE_ACCOUNT_TOKEN --provider age ``` -::: warning -Always use service accounts for fnox, not personal tokens. -::: +Before resolving 1Password references in a new session: -## Pros +```sh +export OP_SERVICE_ACCOUNT_TOKEN="$(fnox get OP_SERVICE_ACCOUNT_TOKEN)" +``` -- βœ… Beautiful UI and mobile apps -- βœ… Excellent audit logs and access control -- βœ… No encryption key management -- βœ… Team-friendly -- βœ… Multi-factor authentication -- βœ… Service accounts for CI/CD +For interactive work, using your own 1Password sign-in avoids sharing a service account token across the team. -## Cons +## Cache for local use -- ❌ Requires 1Password subscription -- ❌ Requires network access -- ❌ Service account token management -- ❌ Not free (starts at $7.99/user/month for teams) +Use [`fnox sync`](/guide/sync) to store an encrypted snapshot under a personal age key, or enable the [daemon](/guide/daemon) for in-memory caching. Refresh caches when the vault value changes. ## Troubleshooting -### "Authentication required" - -Set the token: - -```bash -export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) -``` - -### "Item not found" +### Authentication required -Check: +Check the CLI's sign-in state and the selected account. For automation, confirm the service account token is available in the environment of the fnox process. -- Vault name is correct in fnox.toml -- Item exists in that vault -- Service account has access to the vault +### Item or vault not found -```bash -# List items in vault -op item list --vault "Development" +Check the vault name, item name, field name, and account. Verify that the authenticated user or service account has vault access: -# Get item details -op item get "Database" --vault "Development" -``` - -### "Vault not found" - -Verify vault name: - -```bash -# List all vaults +```sh op vault list +op item list --vault Engineering +fnox provider test op ``` -## Best Practices - -1. **Use service accounts** - Not personal tokens -2. **One service account per environment** - Separate dev, staging, prod -3. **Grant minimal access** - Only vaults the service account needs -4. **Store token encrypted** - Use age provider to encrypt `OP_SERVICE_ACCOUNT_TOKEN` -5. **Rotate tokens periodically** - Create new service account, update fnox.toml -6. **Use descriptive item names** - Makes referencing easier +Item listings expose names and metadata. Review output before including it in an issue. -## Next Steps +## Next steps -- [Bitwarden](/providers/bitwarden) - Open source alternative -- [Real-World Example](/guide/real-world-example) - Complete setup -- [Profiles](/guide/profiles) - Multi-environment configuration +- [Connect a vault](/guide/golden-path): set up a personal age cache. +- [Profiles](/guide/profiles): compose environment settings. +- [1Password CLI documentation](https://developer.1password.com/docs/cli/): account setup and reference syntax. diff --git a/docs/providers/age.md b/docs/providers/age.md index 7306d2b7..3b051fd5 100644 --- a/docs/providers/age.md +++ b/docs/providers/age.md @@ -1,30 +1,23 @@ -# Age Encryption +--- +description: "Encrypt secrets in fnox.toml with age recipients, SSH keys, or plugins. Configure identities and share access with teammates." +--- -Age is a modern encryption tool that's simple, secure, and works beautifully with SSH keys. +# Age encryption -## Quick Start +The `age` provider encrypts values into `fnox.toml`. Decryption uses an age identity, a supported SSH private key, or an age plugin. Standard age keys work offline and do not require a cloud account. -```bash -# 1. Generate age key -age-keygen -o ~/.config/fnox/age.txt +## Quick start -# 2. Get public key -grep "public key:" ~/.config/fnox/age.txt -# Output: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p +Follow the [age quick start](/guide/quick-start) for a complete setup. If you already have a key and a configured provider: -# 3. Configure fnox -cat >> fnox.toml << 'EOF' -[providers] -age = { type = "age", recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"] } -EOF - -# 4. Set private key -export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt) - -# 5. Encrypt a secret -fnox set DATABASE_URL "postgresql://localhost/mydb" --provider age +```sh +fnox set DATABASE_URL --provider age +fnox check --all +fnox exec -- npm start ``` +Omitting the value from `fnox set` prompts with hidden input. Never paste a private key into `recipients`: that field takes public recipients only. + ## Installation Install the age CLI: @@ -41,7 +34,7 @@ sudo apt install age ## Setup -### Option 1: Generate Age Key +### Option 1: generate age key ```bash # Create config directory @@ -50,19 +43,17 @@ mkdir -p ~/.config/fnox # Generate age key age-keygen -o ~/.config/fnox/age.txt -# View the generated key -cat ~/.config/fnox/age.txt +# Print only the public recipient +age-keygen -y ~/.config/fnox/age.txt ``` Output: -``` -# created: 2024-01-15T10:30:45-08:00 -# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p -AGE-SECRET-KEY-1ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRS +```text +age1... ``` -### Option 2: Use SSH Key +### Option 2: use SSH key Age has first-class SSH key support β€” no key generation needed. Your existing SSH public key becomes the recipient and your private key decrypts; see [SSH Key Support](#ssh-key-support) below. @@ -99,19 +90,26 @@ keychain = { type = "keychain", service = "fnox" } age = { type = "age", recipients = ["age1..."], identity = { provider = "keychain", value = "age-key" } } ``` -### Set Decryption Key +### Set decryption key {#set-decryption-key} -#### Using Age Key +fnox selects an identity in this order: -```bash -# Export the secret key -export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt) +1. `FNOX_AGE_KEY` (inline identity contents). +2. The provider's `identity` reference. +3. The provider's `key_file`. +4. The key-file setting (`FNOX_AGE_KEY_FILE` or the deprecated CLI flag). +5. `age.txt` in the fnox configuration directory. -# Add to shell profile -echo 'export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt)' >> ~/.bashrc +For local use, prefer `key_file` or the default file. `FNOX_AGE_KEY` is useful when CI supplies the identity directly. An exported inline key overrides the provider-specific settings. + +#### Using age key + +```bash +# Optional when the key is already in the default location +export FNOX_AGE_KEY_FILE=~/.config/fnox/age.txt ``` -#### Using SSH Key +#### Using SSH key ```bash # Point to SSH private key @@ -123,7 +121,7 @@ echo 'export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519' >> ~/.bashrc ## Usage -### Encrypt and Store a Secret +### Encrypt and store a secret ```bash fnox set DATABASE_URL "postgresql://localhost/mydb" --provider age @@ -136,28 +134,28 @@ The resulting `fnox.toml`: DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } # ← Encrypted, safe to commit! ``` -### Decrypt and Get a Secret +### Decrypt and get a secret ```bash fnox get DATABASE_URL ``` -### Run Commands with Secrets +### Run commands with secrets ```bash fnox exec -- npm run dev ``` -## SSH Key Support +## SSH key support -Age natively supports SSH keysβ€”no conversion needed! +Use a supported SSH public key as a recipient and the matching private key for decryption. -### Supported SSH Key Types +### Supported SSH key types -- **`ssh-ed25519`** - Ed25519 keys (recommended, most secure) +- **`ssh-ed25519`** - Ed25519 keys - **`ssh-rsa`** - RSA keys (2048-bit minimum, 4096-bit recommended) -### Using SSH Keys +### Using SSH keys ```toml [providers.age] @@ -176,10 +174,10 @@ export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519 ``` ::: warning Password-Protected SSH Keys -Password-protected SSH keys are NOT supported. If your SSH key has a passphrase, you must create a copy without a passphrase for use with fnox/age. +Password-protected SSH keys are not supported by this integration. Generate a dedicated age identity or use a supported age plugin instead of removing the passphrase from your SSH key. ::: -### Get Your SSH Public Key +### Get your SSH public key ```bash # Ed25519 key @@ -189,7 +187,7 @@ cat ~/.ssh/id_ed25519.pub cat ~/.ssh/id_rsa.pub ``` -## Plugin Support +## Plugin support Age plugins extend age with hardware-backed and alternative keys. fnox supports any [age plugin](https://github.com/FiloSottile/awesome-age#plugins), for example [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) (YubiKey / PIV) or [age-plugin-se](https://github.com/remko/age-plugin-se) (Apple's Secure Enclave). @@ -207,9 +205,9 @@ Refer to each plugin's docs for setup instructions. The sync guide also has full [hardware-backed decryption](/guide/sync#hardware-backed-decryption) walkthroughs for Secure Enclave, YubiKey, TPM, and FIDO2. -## Team Workflow +## Team workflow -### 1. Collect Public Keys +### 1. Collect public keys Each team member shares their public key: @@ -221,7 +219,7 @@ grep "public key:" ~/.config/fnox/age.txt cat ~/.ssh/id_ed25519.pub ``` -### 2. Add All Recipients +### 2. Add all recipients ```toml [providers.age] @@ -233,14 +231,14 @@ recipients = [ ] ``` -### 3. Encrypt Secrets +### 3. Encrypt secrets ```bash fnox set DATABASE_URL "postgresql://dev.example.com/db" --provider age -fnox set API_KEY "secret-key" --provider age +fnox set API_KEY --provider age ``` -### 4. Commit to Git +### 4. Commit to git ```bash git add fnox.toml @@ -248,7 +246,7 @@ git commit -m "Add encrypted development secrets" git push ``` -### 5. Everyone Can Decrypt +### 5. Decrypt with a matching identity Each team member sets their private key: @@ -263,13 +261,13 @@ export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519 export FNOX_AGE_KEY="AGE-SECRET-KEY-1..." ``` -Now everyone can decrypt: +A teammate whose public recipient was included when the secret was encrypted can now decrypt: ```bash fnox get DATABASE_URL # Works for all recipients! ``` -## Adding a New Team Member +## Adding a new team member 1. **New member generates/shares public key**: @@ -325,7 +323,7 @@ fnox get DATABASE_URL # Works for all recipients! fnox get DATABASE_URL # Works! ``` -## CI/CD Setup +## CI/CD setup ### GitHub Actions @@ -369,23 +367,9 @@ jobs: 4. Add to GitHub Secrets as `FNOX_AGE_KEY` -## Pros - -- βœ… Secrets live in git (version control, code review) -- βœ… Works offline -- βœ… Zero runtime dependencies (after initial setup) -- βœ… Free forever -- βœ… Works with SSH keys you already have -- βœ… Simple and secure -- βœ… Team-friendly (multiple recipients) - -## Cons +## Usage notes -- ❌ Key rotation requires re-encrypting all secrets -- ❌ No audit logs -- ❌ No centralized access control -- ❌ Manual key management -- ❌ Adding new team members requires re-encryption +Age decrypts locally with a matching identity. Changing recipients does not update existing ciphertext: run `fnox reencrypt` for each affected profile. Removing a recipient cannot revoke that person's access to old ciphertext in git history; rotate the underlying secret if access must end. ## Troubleshooting @@ -414,7 +398,7 @@ grep recipients fnox.toml - Check that the private key file path is correct - Ensure the private key is NOT password-protected -## Next Steps +## Next steps - [Real-World Example](/guide/real-world-example) - Complete project setup with age - [Profiles](/guide/profiles) - Multi-environment configuration diff --git a/docs/providers/aws-kms.md b/docs/providers/aws-kms.md index 8b1eb9c8..5c565c05 100644 --- a/docs/providers/aws-kms.md +++ b/docs/providers/aws-kms.md @@ -1,40 +1,46 @@ +--- +description: "Encrypt values in fnox.toml with AWS KMS. Configure credentials, IAM permissions, keys, and key migration." +--- + # AWS KMS AWS Key Management Service (KMS) encrypts secrets using AWS-managed keys. The encrypted ciphertext is stored in your `fnox.toml` file. ## Comparison: KMS vs Secrets Manager -| Feature | AWS KMS | AWS Secrets Manager | -| -------------- | -------------------------------------- | -------------------- | -| Storage | Local (encrypted in fnox.toml) | Remote (in AWS) | -| Secrets in git | Yes (encrypted) | No (references only) | -| Pricing | $1/key/month (one key for all secrets) | $0.40/secret/month | -| Rotation | Manual | Automatic | -| Offline | No (needs AWS API) | No (needs AWS API) | +| Feature | AWS KMS | AWS Secrets Manager | +| -------------- | ------------------------------ | -------------------- | +| Storage | Local (encrypted in fnox.toml) | Remote (in AWS) | +| Secrets in git | Yes (encrypted) | No (references only) | +| Rotation | Manual | Automatic | +| Offline | No (needs AWS API) | No (needs AWS API) | **Use KMS when:** You want secrets in git with AWS-managed keys. **Use Secrets Manager when:** You want centralized storage without secrets in git. -## Quick Start +## Quick start -```bash -# 1. Create KMS key +```sh +# Create KMS key aws kms create-key --description "fnox secrets encryption" # Note the KeyId +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 2. Configure provider -cat >> fnox.toml << 'EOF' +```toml [providers.kms] type = "aws-kms" key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" region = "us-east-1" -EOF +``` -# 3. Encrypt a secret +```sh +# Encrypt a secret fnox set DATABASE_URL "postgresql://prod.example.com/db" --provider kms -# 4. Get secret (decrypts via KMS) +# Get secret (decrypts via KMS) fnox get DATABASE_URL ``` @@ -45,7 +51,7 @@ fnox get DATABASE_URL - KMS key created - IAM permissions -## IAM Permissions +## IAM permissions ```json { @@ -62,7 +68,7 @@ fnox get DATABASE_URL ## Setup -### 1. Create KMS Key +### 1. Create KMS key Via AWS CLI: @@ -76,11 +82,11 @@ aws kms create-key \ Or use [AWS Console](https://console.aws.amazon.com/kms/) β†’ KMS β†’ Create Key. -### 2. Configure AWS Credentials +### 2. Configure AWS credentials Same as [AWS Secrets Manager](/providers/aws-sm#configure-aws-credentials). -### 3. Configure fnox Provider +### 3. Configure fnox provider ```toml [providers.kms] @@ -115,7 +121,7 @@ role_arn = "arn:aws:iam::123456789012:role/kms-user" ## Usage -### Encrypt and Store +### Encrypt and store ```bash fnox set DATABASE_URL "postgresql://prod.example.com/db" --provider kms @@ -128,13 +134,13 @@ Result in `fnox.toml`: DATABASE_URL = { provider = "kms", value = "AQICAHhw...base64...ciphertext..." } # ← Encrypted, safe to commit! ``` -### Decrypt and Get +### Decrypt and get ```bash fnox get DATABASE_URL ``` -## How It Works +## How it works 1. **Encryption (`fnox set`):** - Calls AWS KMS `Encrypt` API @@ -144,7 +150,7 @@ fnox get DATABASE_URL - Calls AWS KMS `Decrypt` API - Returns plaintext -## Multi-Environment Example +## Multi-environment example ```toml # Development: age encryption (free) @@ -162,49 +168,27 @@ kms = { type = "aws-kms", key_id = "arn:aws:kms:us-east-1:123456789012:key/...", DATABASE_URL = { provider = "kms", value = "AQICAHhw..." } # ← KMS encrypted ciphertext ``` -## Key Rotation +## Key rotation -When rotating KMS keys: +Rotating key material within the same KMS key is different from switching to a new key ID. The fnox provider supplies `key_id` when decrypting, so changing it before migrating values can prevent the old ciphertext from decrypting. -1. Create new KMS key -2. Update fnox.toml with new `key_id` -3. Re-encrypt all secrets with the new key: - ```bash - fnox reencrypt -p kms - ``` +To move to another KMS key, keep the old provider configured and add a second provider for the new key. Resolve each value through the old definition and store it with the new provider: -## Costs - -AWS KMS pricing (as of 2024): - -- **$1.00 per key per month** -- **$0.03 per 10,000 operations** - -Example: - -- 1 KMS key = $1.00/month -- 1,000 deployments Γ— 10 secrets Γ— 10 decrypt calls = $0.30/month -- **Total: ~$1.30/month** +```sh +fnox get DATABASE_URL | fnox set DATABASE_URL --provider new-kms +``` -Much cheaper than Secrets Manager for many secrets! +Repeat for the affected secrets and profiles, verify access, then remove the old provider when it is no longer used. Rotate the underlying application secret separately if its plaintext may have been exposed. -## Pros +## Costs -- βœ… Secrets in git (version control) -- βœ… AWS-managed encryption keys -- βœ… IAM access control -- βœ… CloudTrail audit logs -- βœ… Cheaper than Secrets Manager (one key for all secrets) -- βœ… No per-secret charges +Charges depend on region, storage, key type or tier, and API usage. Consult the [service pricing](https://aws.amazon.com/kms/pricing/) for current rates. fnox does not change the provider's billing model. -## Cons +## Usage notes -- ❌ Requires AWS account and network access -- ❌ Costs money ($1/key/month) -- ❌ More complex than age encryption -- ❌ Manual rotation (vs automatic in Secrets Manager) +Ciphertext lives in your config, but both encryption and decryption require AWS access. IAM policies and the KMS key policy must permit the operation. Rotating the KMS key material is distinct from changing the application secret. -## Next Steps +## Next steps - [AWS Secrets Manager](/providers/aws-sm) - Remote storage alternative - [Age Encryption](/providers/age) - Free local encryption diff --git a/docs/providers/aws-ps.md b/docs/providers/aws-ps.md index d97cbbf5..66c2fa1a 100644 --- a/docs/providers/aws-ps.md +++ b/docs/providers/aws-ps.md @@ -1,29 +1,37 @@ +--- +description: "Read and write AWS Systems Manager Parameter Store secrets with fnox, including path prefixes and environment-specific profiles." +--- + # AWS Parameter Store -AWS Systems Manager Parameter Store provides hierarchical secret storage with path-based organization. It's a cost-effective alternative to AWS Secrets Manager for simpler use cases. +AWS Systems Manager Parameter Store stores values under hierarchical paths. Use the `aws-ps` provider to read and write parameters, including encrypted `SecureString` values. -## Quick Start +## Quick start -```bash -# 1. Configure provider in fnox.toml -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] ps = { type = "aws-ps", region = "us-east-1", prefix = "/myapp/prod/" } -EOF +``` -# 2. Create parameter in AWS +```sh +# Create parameter in AWS aws ssm put-parameter \ --name "/myapp/prod/database-url" \ --value "postgresql://prod.example.com/db" \ --type "SecureString" +``` -# 3. Reference in fnox.toml -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [secrets] DATABASE_URL = { provider = "ps", value = "database-url" } # With prefix, fetches "/myapp/prod/database-url" -EOF +``` -# 4. Fetch secret +```sh +# Fetch secret fnox get DATABASE_URL ``` @@ -33,9 +41,9 @@ fnox get DATABASE_URL - AWS credentials configured (CLI, environment variables, or IAM role) - IAM permissions (see below) -## IAM Permissions +## IAM permissions -### Read-Only Access (Minimum) +### Read-only access (minimum) ```json { @@ -61,7 +69,7 @@ fnox get DATABASE_URL The `ssm:DescribeParameters` action **must** use `"Resource": "*"` and cannot be scoped to specific ARNs. ::: -### Full Access (For Testing) +### Full access (for testing) ```json { @@ -90,11 +98,11 @@ The `ssm:DescribeParameters` action **must** use `"Resource": "*"` and cannot be ## Configuration -### Configure AWS Credentials +### Configure AWS credentials Choose one: -#### Option 1: Environment Variables +#### Option 1: environment variables ```bash export AWS_ACCESS_KEY_ID="AKIA..." @@ -102,7 +110,7 @@ export AWS_SECRET_ACCESS_KEY="..." export AWS_REGION="us-east-1" ``` -#### Option 2: AWS CLI Profile +#### Option 2: AWS CLI profile ```bash aws configure @@ -111,7 +119,7 @@ aws configure export AWS_PROFILE=myapp ``` -#### Option 3: IAM Role (Automatic on AWS) +#### Option 3: IAM role (automatic on AWS) If running on EC2, ECS, Lambda, or other AWS services: @@ -120,13 +128,11 @@ If running on EC2, ECS, Lambda, or other AWS services: # Credentials are automatic via instance metadata ``` -### Configure fnox Provider +### Configure fnox provider ```toml [providers] -ps = { type = "aws-ps", region = "us-east-1" } # minimal config - -# With optional fields: +# Include only the optional fields you need. ps = { type = "aws-ps", region = "us-east-1", profile = "my-aws-profile", prefix = "/myapp/prod/" } ``` @@ -146,7 +152,7 @@ Set `role_arn` to have fnox call `sts:AssumeRole` and use the resulting credenti ps = { type = "aws-ps", region = "eu-west-1", profile = "sso-dev", role_arn = "arn:aws:iam::123456789012:role/param-reader" } ``` -## Creating Parameters +## Creating parameters ### Via AWS CLI @@ -179,7 +185,7 @@ aws ssm put-parameter \ fnox set DATABASE_URL "postgresql://prod.db.example.com/mydb" --provider ps ``` -### Via AWS Console +### Via AWS console 1. Go to [AWS Systems Manager Console](https://console.aws.amazon.com/systems-manager/parameters) 2. Click "Create parameter" @@ -188,7 +194,7 @@ fnox set DATABASE_URL "postgresql://prod.db.example.com/mydb" --provider ps 5. Enter the value 6. Create -## Referencing Parameters +## Referencing parameters Add references to `fnox.toml`: @@ -201,27 +207,27 @@ API_KEY = { provider = "ps", value = "api-key" } # β†’ Fetches "/myapp/prod/api ## Usage -### Get a Secret +### Get a secret ```bash fnox get DATABASE_URL ``` -### Run Commands +### Run commands ```bash # Fetches all secrets from Parameter Store fnox exec -- ./start-server.sh ``` -### Use Different Profiles +### Use different profiles ```bash # Different profile for different environments fnox exec --profile production -- ./deploy.sh ``` -## Prefix Behavior +## Prefix behavior The `prefix` is prepended to the `value`: @@ -244,11 +250,11 @@ ps = { type = "aws-ps", region = "us-east-1" } # No prefix DATABASE_URL = { provider = "ps", value = "/myapp/prod/database-url" } # β†’ Full path ``` -## Hierarchical Organization +## Hierarchical organization Parameter Store supports path-based organization: -``` +```text /myapp/ prod/ database/ @@ -275,7 +281,7 @@ DATABASE_URL = { provider = "prod", value = "database/url" } DATABASE_URL = { provider = "staging", value = "database/url" } ``` -## Multi-Environment Example +## Multi-environment example ```toml # Development: age encryption @@ -313,21 +319,12 @@ fnox get DATABASE_URL --profile production ## Costs -AWS Parameter Store pricing: - -- **Standard parameters**: Free (up to 10,000 parameters) -- **Advanced parameters**: $0.05 per parameter per month -- **API calls**: Free for standard tier - -::: tip Cost Optimization -Parameter Store standard tier is free for most use cases. Use it for configuration values and simple secrets. Reserve AWS Secrets Manager for secrets that need automatic rotation. -::: +Charges depend on region, storage, key type or tier, and API usage. Consult the [service pricing](https://aws.amazon.com/systems-manager/pricing/) for current rates. fnox does not change the provider's billing model. ## Comparison: Parameter Store vs Secrets Manager | Feature | Parameter Store | Secrets Manager | | ------------- | ----------------------------- | ------------------------- | -| Cost | Free (standard tier) | $0.40/secret/month | | Max Size | 4KB (8KB advanced) | 64KB | | Rotation | Manual | Automatic | | Versioning | Limited | Full versioning | @@ -348,7 +345,7 @@ Parameter Store standard tier is free for most use cases. Use it for configurati - You have complex JSON secrets - You need full versioning history -## CI/CD Example +## CI/CD example ### GitHub Actions @@ -376,22 +373,9 @@ jobs: fnox exec --profile production -- ./deploy.sh ``` -## Pros - -- βœ… Free for standard tier (up to 10,000 parameters) -- βœ… Hierarchical path-based organization -- βœ… IAM access control -- βœ… CloudTrail audit logs -- βœ… Secrets never in git -- βœ… Simple and straightforward - -## Cons +## Usage notes -- ❌ No automatic rotation (use Secrets Manager for that) -- ❌ Limited versioning -- ❌ Smaller size limit (4KB standard, 8KB advanced) -- ❌ Requires AWS account and network access -- ❌ AWS vendor lock-in +Use `SecureString` for sensitive parameter values. The provider applies its prefix to the configured parameter name; include the leading and trailing separators you need. Refresh any fnox cache after updating a parameter. ## Troubleshooting @@ -429,7 +413,7 @@ grep region fnox.toml echo $AWS_REGION ``` -## Next Steps +## Next steps - [AWS Secrets Manager](/providers/aws-sm) - For automatic rotation and complex secrets - [AWS KMS](/providers/aws-kms) - For encrypting secrets in git diff --git a/docs/providers/aws-sm.md b/docs/providers/aws-sm.md index f788b9a0..b25b71cb 100644 --- a/docs/providers/aws-sm.md +++ b/docs/providers/aws-sm.md @@ -1,28 +1,36 @@ +--- +description: "Read AWS Secrets Manager values with fnox. Configure authentication, IAM permissions, prefixes, and profiles." +--- + # AWS Secrets Manager AWS Secrets Manager provides centralized secret management with IAM access control, audit logs, and automatic rotation. -## Quick Start +## Quick start -```bash -# 1. Configure provider in fnox.toml -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp/" } -EOF +``` -# 2. Create secret in AWS +```sh +# Create secret in AWS aws secretsmanager create-secret \ --name "myapp/database-url" \ --secret-string "postgresql://prod.example.com/db" +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 3. Reference in fnox.toml -cat >> fnox.toml << 'EOF' +```toml [secrets] DATABASE_URL = { provider = "aws", value = "database-url" } # With prefix, fetches "myapp/database-url" -EOF +``` -# 4. Fetch secret +```sh +# Fetch secret fnox get DATABASE_URL ``` @@ -32,9 +40,9 @@ fnox get DATABASE_URL - AWS credentials configured (CLI, environment variables, or IAM role) - IAM permissions (see below) -## IAM Permissions +## IAM permissions -### Read-Only Access (Minimum) +### Read-only access (minimum) ```json { @@ -66,7 +74,7 @@ fnox get DATABASE_URL The `secretsmanager:ListSecrets` and `secretsmanager:BatchGetSecretValue` actions **must** use `"Resource": "*"` and cannot be scoped to specific ARNs. ::: -### Full Access (For Testing) +### Full access (for testing) ```json { @@ -100,11 +108,11 @@ The `secretsmanager:ListSecrets` and `secretsmanager:BatchGetSecretValue` action ## Configuration -### Configure AWS Credentials +### Configure AWS credentials Choose one: -#### Option 1: Environment Variables +#### Option 1: environment variables ```bash export AWS_ACCESS_KEY_ID="AKIA..." @@ -112,7 +120,7 @@ export AWS_SECRET_ACCESS_KEY="..." export AWS_REGION="us-east-1" ``` -#### Option 2: AWS CLI Profile +#### Option 2: AWS CLI profile ```bash aws configure @@ -121,7 +129,7 @@ aws configure export AWS_PROFILE=myapp ``` -#### Option 3: IAM Role (Automatic on AWS) +#### Option 3: IAM role (automatic on AWS) If running on EC2, ECS, Lambda, or other AWS services: @@ -130,13 +138,11 @@ If running on EC2, ECS, Lambda, or other AWS services: # Credentials are automatic via instance metadata ``` -### Configure fnox Provider +### Configure fnox provider ```toml [providers] -aws = { type = "aws-sm", region = "us-east-1" } # minimal config - -# With optional fields: +# Include only the optional fields you need. aws = { type = "aws-sm", region = "us-east-1", profile = "my-aws-profile", prefix = "myapp/" } ``` @@ -149,7 +155,7 @@ aws = { type = "aws-sm", region = "us-east-1", profile = "my-aws-profile", prefi The `profile` field is useful when you have multiple AWS accounts or roles configured and want to pin a provider to a specific one without relying on `AWS_PROFILE` in the environment. -### Assuming a Role +### Assuming a role Set `role_arn` to have fnox call `sts:AssumeRole` and use the resulting credentials for every request. The credentials from `profile` (or the default chain) are the source credentials for that call, so an SSO profile plus a cross-account role works in one step: @@ -162,7 +168,7 @@ This mirrors the `role` option in SOPS. If your `~/.aws/config` profile already The session name is always `fnox`, and the role must trust the source identity for `sts:AssumeRole`. -## Creating Secrets +## Creating secrets ### Via AWS CLI @@ -184,7 +190,7 @@ aws secretsmanager create-secret \ --secret-string '{"username":"admin","password":"secret123"}' ``` -### Via AWS Console +### Via AWS console 1. Go to [AWS Secrets Manager Console](https://console.aws.amazon.com/secretsmanager/) 2. Click "Store a new secret" @@ -194,7 +200,7 @@ aws secretsmanager create-secret \ 6. Configure rotation (optional) 7. Store -## Referencing Secrets +## Referencing secrets Add references to `fnox.toml`: @@ -207,27 +213,27 @@ API_KEY = { provider = "aws", value = "api-key" } # β†’ Fetches "myapp/api-key" ## Usage -### Get a Secret +### Get a secret ```bash fnox get DATABASE_URL ``` -### Run Commands +### Run commands ```bash # Fetches all secrets from AWS fnox exec -- ./start-server.sh ``` -### Use Different Profiles +### Use different profiles ```bash # Different profile for different environments fnox exec --profile production -- ./deploy.sh ``` -## Prefix Behavior +## Prefix behavior The `prefix` is prepended to the `value`: @@ -250,7 +256,7 @@ aws = { type = "aws-sm", region = "us-east-1" } # No prefix DATABASE_URL = { provider = "aws", value = "myapp/database-url" } # β†’ Fetches "myapp/database-url" ``` -## Multi-Environment Example +## Multi-environment example ```toml # Development: age encryption @@ -286,7 +292,7 @@ fnox get DATABASE_URL --profile staging fnox get DATABASE_URL --profile production ``` -## JSON Secrets +## JSON secrets AWS Secrets Manager supports JSON secrets: @@ -337,7 +343,7 @@ DB_HOST = { provider = "aws", value = "config", json_path = "database.host" } DB_CACHE_KEY = { provider = "aws", value = "config", json_path = 'database.cache\.key' } ``` -## Secret Rotation +## Secret rotation AWS Secrets Manager supports automatic rotation: @@ -348,26 +354,13 @@ aws secretsmanager rotate-secret \ --rotation-lambda-arn "arn:aws:lambda:..." ``` -fnox always fetches the current version, so rotation is transparent. +Direct reads fetch the current version. Refresh an encrypted sync cache or clear the daemon cache to pick up a rotated value. ## Costs -AWS Secrets Manager pricing (as of 2024): - -- **$0.40 per secret per month** -- **$0.05 per 10,000 API calls** - -Example: - -- 10 secrets Γ— $0.40 = $4.00/month -- 1,000 deployments Γ— 10 secrets Γ— $0.05/10k = $0.50/month -- **Total: ~$4.50/month** +Charges depend on region, storage, key type or tier, and API usage. Consult the [service pricing](https://aws.amazon.com/secrets-manager/pricing/) for current rates. fnox does not change the provider's billing model. -::: tip Cost Optimization -Use age encryption for development/staging secrets to reduce AWS Secrets Manager costs. Reserve AWS SM for production-only secrets. -::: - -## CI/CD Example +## CI/CD example ### GitHub Actions @@ -395,34 +388,19 @@ jobs: fnox exec --profile production -- ./deploy.sh ``` -## Pros - -- βœ… Centralized secret management -- βœ… IAM access control -- βœ… CloudTrail audit logs -- βœ… Automatic rotation support -- βœ… Secrets never in git -- βœ… Easy key rotation (no re-encryption needed) -- βœ… Versioning included - -## Cons +## Usage notes -- ❌ Requires AWS account and network access -- ❌ Costs money ($0.40/secret/month + API calls) -- ❌ More complex setup than encryption -- ❌ Slower (network latency) -- ❌ AWS vendor lock-in +fnox reads the current secret value from AWS when it resolves directly. If you use sync or daemon caching, refresh that cache after rotation. `json_path` can select a field from a JSON secret without creating separate AWS secrets. ## Comparison: AWS Secrets Manager vs AWS KMS -| Feature | AWS Secrets Manager | AWS KMS | -| -------------- | -------------------- | -------------------------------------- | -| Storage | Remote (AWS) | Local (encrypted in fnox.toml) | -| Secrets in git | No (references only) | Yes (encrypted ciphertext) | -| Pricing | $0.40/secret/month | $1/key/month (all secrets use one key) | -| Rotation | Automatic | Manual | -| Offline | No | No (needs AWS to encrypt/decrypt) | -| Access Control | IAM policies | IAM policies | +| Feature | AWS Secrets Manager | AWS KMS | +| -------------- | -------------------- | --------------------------------- | +| Storage | Remote (AWS) | Local (encrypted in fnox.toml) | +| Secrets in git | No (references only) | Yes (encrypted ciphertext) | +| Rotation | Automatic | Manual | +| Offline | No | No (needs AWS to encrypt/decrypt) | +| Access Control | IAM policies | IAM policies | **Use AWS SM when:** You want centralized storage, rotation, and don't want secrets in git. @@ -464,7 +442,7 @@ grep region fnox.toml echo $AWS_REGION ``` -## Next Steps +## Next steps - [AWS KMS](/providers/aws-kms) - Alternative with secrets in git - [Real-World Example](/guide/real-world-example) - Complete AWS setup diff --git a/docs/providers/azure-ac.md b/docs/providers/azure-ac.md index cc78f63e..540e82d0 100644 --- a/docs/providers/azure-ac.md +++ b/docs/providers/azure-ac.md @@ -1,31 +1,41 @@ +--- +description: "Load Azure App Configuration values with fnox, including label selection, prefixes, and Azure authentication." +--- + # Azure App Configuration Azure App Configuration is the Azure store for non-secret configuration: endpoints, feature toggles, tuning values. This provider reads key-values from it, so configuration your infrastructure owns can be resolved instead of hardcoded in `fnox.toml`. Read-only. Use [Azure Key Vault Secrets](/providers/azure-sm) for anything sensitive. -## Quick Start +## Quick start -```bash -# 1. Create the store +```sh +# Create the store az appconfig create --name "myapp-config" --resource-group "myapp-rg" --location westeurope +``` -# 2. Configure provider -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] appconfig = { type = "azure-ac", endpoint = "https://myapp-config.azconfig.io" } -EOF +``` -# 3. Set a key-value +```sh +# Set a key-value az appconfig kv set --name "myapp-config" --key "api-url" --value "https://api.example.com" +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 4. Reference in fnox -cat >> fnox.toml << 'EOF' +```toml [secrets] API_URL = { provider = "appconfig", value = "api-url" } -EOF +``` -# 5. Get value +```sh +# Get value fnox get API_URL ``` @@ -71,19 +81,11 @@ Without `label`, the key-value with no label is returned. The `endpoint` must be an HTTPS App Configuration domain: `*.azconfig.io`, or `*.azconfig.azure.us` and `*.azconfig.azure.cn` for Azure Government and Azure China. Anything else is rejected, since the endpoint is where fnox sends your Entra token. The audience follows the domain, so sovereign stores work without extra configuration. -## Pros - -- βœ… Keeps non-secret configuration out of Key Vault -- βœ… One key serves every environment via labels -- βœ… Integrated with Azure RBAC - -## Cons +## Usage notes -- ❌ Read-only: write key-values with `az appconfig kv set`, not `fnox set` -- ❌ Values are not secrets: anyone with Data Reader sees them -- ❌ Requires Azure subscription +This provider is read-only and intended for non-secret configuration. Use labels to select environment-specific values and Azure Key Vault Secrets for sensitive values. -## Next Steps +## Next steps - [Azure Key Vault Secrets](/providers/azure-sm) - For actual secrets - [Profiles](/guide/profiles) - Per-environment labels diff --git a/docs/providers/azure-kms.md b/docs/providers/azure-kms.md index fec59469..d8956143 100644 --- a/docs/providers/azure-kms.md +++ b/docs/providers/azure-kms.md @@ -1,23 +1,32 @@ -# Azure Key Vault Keys +--- +description: "Encrypt values in fnox.toml with Azure Key Vault keys and decrypt them through the Azure API." +--- + +# Azure Key Vault keys Azure Key Vault Keys encrypts secrets using Azure-managed keys. The encrypted ciphertext is stored in your `fnox.toml` file. -## Quick Start +## Quick start -```bash -# 1. Create Key Vault with key +Authenticate to Azure and create a Key Vault before following this example. The identity running fnox needs permission to encrypt and decrypt with the key. + +```sh +# Create a key in an existing Key Vault az keyvault key create --vault-name "myapp-vault" --name "encryption-key" --protection software +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 2. Configure provider -cat >> fnox.toml << 'EOF' +```toml [providers] azurekms = { type = "azure-kms", vault_url = "https://myapp-vault.vault.azure.net/", key_name = "encryption-key" } -EOF +``` -# 3. Encrypt a secret +```sh +# Encrypt a secret fnox set DATABASE_URL "postgresql://prod.example.com/db" --provider azurekms -# 4. Get secret (decrypts via Azure) +# Get secret (decrypts via Azure) fnox get DATABASE_URL ``` @@ -39,26 +48,18 @@ az role assignment create \ azurekms = { type = "azure-kms", vault_url = "https://myapp-vault.vault.azure.net/", key_name = "encryption-key" } ``` -## How It Works +## How it works Similar to [AWS KMS](/providers/aws-kms): 1. **Encryption:** Calls Azure Key Vault, stores ciphertext in fnox.toml 2. **Decryption:** Calls Azure Key Vault to recover plaintext -## Pros - -- βœ… Secrets in git (version control) -- βœ… Azure-managed keys -- βœ… Azure RBAC integration - -## Cons +## Usage notes -- ❌ Requires Azure subscription -- ❌ Costs money -- ❌ Network access required +Ciphertext lives in your config. Decryption still requires network access and permission to use the Azure key. Keep the key available for all ciphertext that depends on it. -## Next Steps +## Next steps - [Azure Key Vault Secrets](/providers/azure-sm) - Remote storage alternative - [Age Encryption](/providers/age) - Free local encryption diff --git a/docs/providers/azure-sm.md b/docs/providers/azure-sm.md index 4bedbc5f..5b719f1c 100644 --- a/docs/providers/azure-sm.md +++ b/docs/providers/azure-sm.md @@ -1,29 +1,39 @@ -# Azure Key Vault Secrets +--- +description: "Read Azure Key Vault secrets with fnox. Configure vault access, authentication, prefixes, and profiles." +--- + +# Azure Key Vault secrets Azure Key Vault Secrets provides centralized secret management for Azure workloads. -## Quick Start +## Quick start -```bash -# 1. Create Key Vault +```sh +# Create Key Vault az keyvault create --name "myapp-vault" --resource-group "myapp-rg" +``` -# 2. Configure provider -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] -azure = { type = "azure-sm", vault_url = "https://myapp-vault.vault.azure.net/", prefix = "myapp/" } -EOF +azure = { type = "azure-sm", vault_url = "https://myapp-vault.vault.azure.net/", prefix = "myapp-" } +``` -# 3. Create secret +```sh +# Create secret az keyvault secret set --vault-name "myapp-vault" --name "myapp-database-url" --value "postgresql://..." +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 4. Reference in fnox -cat >> fnox.toml << 'EOF' +```toml [secrets] DATABASE_URL = { provider = "azure", value = "database-url" } -EOF +``` -# 5. Get secret +```sh +# Get secret fnox get DATABASE_URL ``` @@ -59,22 +69,14 @@ az role assignment create \ ```toml [providers] -azure = { type = "azure-sm", vault_url = "https://myapp-vault.vault.azure.net/", prefix = "myapp/" } # prefix is optional +azure = { type = "azure-sm", vault_url = "https://myapp-vault.vault.azure.net/", prefix = "myapp-" } # prefix is optional ``` -## Pros - -- βœ… Integrated with Azure RBAC -- βœ… Audit logs -- βœ… Managed rotation - -## Cons +## Usage notes -- ❌ Requires Azure subscription -- ❌ Costs money -- ❌ Network access required +The config stores a secret name. Keep the configured prefix consistent with the actual name in Key Vault, and refresh any fnox cache after a value changes. -## Next Steps +## Next steps - [Azure Key Vault Keys](/providers/azure-kms) - Encryption alternative - [AWS Secrets Manager](/providers/aws-sm) - AWS equivalent diff --git a/docs/providers/bitwarden-sm.md b/docs/providers/bitwarden-sm.md index 8de84e91..b28dc007 100644 --- a/docs/providers/bitwarden-sm.md +++ b/docs/providers/bitwarden-sm.md @@ -1,29 +1,39 @@ +--- +description: "Read Bitwarden Secrets Manager values with the bws CLI, a machine-account access token, and secret UUIDs." +--- + # Bitwarden Secrets Manager Integrate with [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/) to retrieve secrets via the `bws` CLI. This is a separate product from Bitwarden Password Manager β€” it's designed for DevOps and infrastructure secrets. -## Quick Start +## Quick start -```bash -# 1. Install bws CLI (see Installation below) +Create a project and access token in Bitwarden Secrets Manager first. This provider uses `bws`, not the password manager's `bw` CLI. + +```sh +# Install bws CLI (see Installation below) brew install bws -# 2. Set access token -export BWS_ACCESS_TOKEN= +# Set access token +export BWS_ACCESS_TOKEN="your-access-token" +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 3. Configure provider -cat >> fnox.toml << 'EOF' +```toml [providers] bws = { type = "bitwarden-sm", project_id = "your-project-id" } -EOF +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 4. Reference secrets by name -cat >> fnox.toml << 'EOF' +```toml [secrets] DATABASE_URL = { provider = "bws", value = "database-url" } -EOF +``` -# 5. Use it +```sh +# Use it fnox get DATABASE_URL fnox exec -- npm start ``` @@ -48,7 +58,7 @@ brew install bws ## Setup -### 1. Create an Access Token +### 1. Create an access token In the Bitwarden Secrets Manager web console: @@ -57,17 +67,17 @@ In the Bitwarden Secrets Manager web console: 3. Generate an access token 4. Note the project ID you want to access -### 2. Set the Access Token +### 2. Set the access token ```bash # Set directly -export BWS_ACCESS_TOKEN= +export BWS_ACCESS_TOKEN="your-access-token" # Or store encrypted with age for bootstrap fnox set BWS_ACCESS_TOKEN "" --provider age ``` -### 3. Configure the Provider +### 3. Configure the provider ```toml [providers] @@ -76,7 +86,7 @@ bws = { type = "bitwarden-sm", project_id = "your-project-id" } The `project_id` can also be provided via the `BWS_PROJECT_ID` environment variable instead of in the config file. -## Referencing Secrets +## Referencing secrets Secrets are referenced by their key name in Bitwarden Secrets Manager: @@ -86,7 +96,7 @@ DATABASE_URL = { provider = "bws", value = "database-url" } API_KEY = { provider = "bws", value = "stripe-api-key" } ``` -### Field Access +### Field access By default, the secret's `value` field is returned. You can also access `key` and `note` fields: @@ -104,7 +114,7 @@ MY_KEY = { provider = "bws", value = "my-secret-name/key" } Supported fields: `value` (default), `key`, `note` -## Provider Configuration +## Provider configuration ```toml [providers] @@ -116,7 +126,7 @@ bws = { type = "bitwarden-sm", project_id = "...", profile = "..." } | `project_id` | No | BSM project ID (or set `BWS_PROJECT_ID` env var) | | `profile` | No | bws CLI profile (for self-hosted or multiple servers) | -## Environment Variables +## Environment variables | Variable | Description | | ----------------------- | -------------------------------------- | @@ -140,7 +150,7 @@ fnox list fnox set NEW_SECRET "secret-value" --provider bws --key-name "my-new-secret" ``` -## Multi-Environment Example +## Multi-environment example ```toml [providers] @@ -158,7 +168,7 @@ bws = { type = "bitwarden-sm", project_id = "prod-project-id" } DATABASE_URL = { provider = "bws", value = "prod-database-url" } ``` -## CI/CD Example +## CI/CD example ### GitHub Actions @@ -180,7 +190,7 @@ jobs: fnox exec -- ./deploy.sh ``` -## Bitwarden SM vs Bitwarden Password Manager +## Bitwarden sm vs Bitwarden password manager | Feature | Bitwarden SM (`bitwarden-sm`) | Bitwarden PM (`bitwarden`) | | ------- | ----------------------------- | -------------------------- | @@ -227,7 +237,7 @@ bws secret list --output json | jq '.[].key' brew install bws ``` -## Next Steps +## Next steps - [Bitwarden Password Manager](/providers/bitwarden) - For personal vault secrets - [AWS Secrets Manager](/providers/aws-sm) - AWS alternative diff --git a/docs/providers/bitwarden.md b/docs/providers/bitwarden.md index 000c7539..f6375294 100644 --- a/docs/providers/bitwarden.md +++ b/docs/providers/bitwarden.md @@ -1,43 +1,37 @@ +--- +description: "Read Bitwarden vault items with bw or rbw. Configure authentication, item fields, and an optional local cache." +--- + # Bitwarden Integrate with Bitwarden (or self-hosted Vaultwarden) to retrieve secrets from your vault. -## Quick Start +## Quick start -```bash -# 1. Install Bitwarden CLI (see Installation below) -brew install bitwarden-cli +Install the [Bitwarden CLI](https://bitwarden.com/help/cli/), sign in, and unlock the vault: -# 2. Login to Bitwarden +```sh bw login +export BW_SESSION="$(bw unlock --raw)" +``` -# 3. Unlock and get session token -export BW_SESSION=$(bw unlock --raw) - -# 4. Store session token (optional, for bootstrap) -fnox set BW_SESSION "$(bw unlock --raw)" --provider age - -# 5. Configure Bitwarden provider -cat >> fnox.toml << 'EOF' -[providers] -bitwarden = { type = "bitwarden" } -EOF +Create a login item named `Database` in the Bitwarden app, or use an existing item. Add its reference to `fnox.toml`: -# 6. Add secrets to Bitwarden -bw create item --name "Database" \ - --username "admin" \ - --password "secret-password" +```toml +[providers.bitwarden] +type = "bitwarden" -# 7. Reference in fnox -cat >> fnox.toml << 'EOF' [secrets] -DATABASE_PASSWORD = { provider = "bitwarden", value = "Database" } -EOF +DATABASE_PASSWORD = { provider = "bitwarden", value = "Database/password" } +``` -# 8. Use it -fnox get DATABASE_PASSWORD +```sh +fnox provider test bitwarden +fnox exec -- npm start ``` +This is the password manager integration. For Bitwarden Secrets Manager and machine-account tokens, use [`bitwarden-sm`](/providers/bitwarden-sm). + ## Prerequisites - [Bitwarden account](https://bitwarden.com) (or self-hosted Vaultwarden) @@ -71,7 +65,7 @@ bw config server https://vault.example.com bw login ``` -### 2. Unlock and Get Session Token +### 2. Unlock and get session token ```bash # Unlock vault @@ -82,28 +76,28 @@ bw unlock # Copy the session token from output ``` -### 3. Store Session Token (Bootstrap) +### 3. Store session token (bootstrap) Optionally, store the session encrypted for easy bootstrap: ```bash # Store token encrypted with age -fnox set BW_SESSION "$(bw unlock --raw)" --provider age +bw unlock --raw | fnox set BW_SESSION --provider age # Next time, bootstrap from fnox: export BW_SESSION=$(fnox get BW_SESSION) ``` -### 4. Configure Bitwarden Provider +### 4. Configure Bitwarden provider ```toml [providers] bitwarden = { type = "bitwarden", collection = "my-collection-id", organization_id = "my-org-id" } # both optional ``` -## Adding Secrets to Bitwarden +## Adding secrets to Bitwarden -### Via Bitwarden Web Vault +### Via Bitwarden web Vault 1. Go to [vault.bitwarden.com](https://vault.bitwarden.com) 2. Click + Add Item @@ -113,31 +107,9 @@ bitwarden = { type = "bitwarden", collection = "my-collection-id", organization_ ### Via Bitwarden CLI -```bash -# Unlock first -export BW_SESSION=$(bw unlock --raw) - -# Create a login item -bw create item \ - --name "Database" \ - --username "admin" \ - --password "secret-password" \ - --url "https://db.example.com" - -# Create with JSON -echo '{ - "type": 1, - "name": "API Key", - "login": { - "password": "sk_live_abc123xyz789" - } -}' | bw encode | bw create item - -# List items -bw list items -``` +`bw create item` accepts an encoded JSON item, rather than `--name` and `--password` flags. Follow the [Bitwarden CLI creation instructions](https://bitwarden.com/help/cli/#create) for the current item schema. -## Referencing Secrets +## Referencing secrets Add references to `fnox.toml`: @@ -148,16 +120,16 @@ DB_USERNAME = { provider = "bitwarden", value = "Database/username" } # Specifi API_KEY = { provider = "bitwarden", value = "API Key" } ``` -## Reference Formats +## Reference formats -### 1. Item Name (Gets Password Field) +### 1. Item name (gets password field) ```toml [secrets] MY_SECRET = { provider = "bitwarden", value = "My Item" } # β†’ Gets the 'password' field ``` -### 2. Item Name + Field +### 2. Item name + field ```toml [secrets] @@ -186,7 +158,7 @@ fnox get DATABASE_PASSWORD fnox exec -- npm start ``` -## Multi-Environment Example +## Multi-environment example ```toml # Bootstrap session token (encrypted in git) @@ -206,7 +178,7 @@ bitwarden = { type = "bitwarden", organization_id = "prod-org-id" } DATABASE_URL = { provider = "bitwarden", value = "Prod Database" } ``` -## Multi-profile Example +## Multi-profile example `bw` supports multiple accounts, as per the [official documentation](https://bitwarden.com/help/cli/#log-in-to-multiple-accounts). fnox can access secrets in a specific profile by supplying an optional `profile` attribute to the provider: @@ -242,7 +214,7 @@ The `auth_command` override ensures fnox prompts with `rbw unlock` instead of th NB: you must have set up the `rbw` CLI independently from fnox using `rbw login`. -## Self-Hosted Vaultwarden +## Self-hosted Vaultwarden Vaultwarden is a lightweight, open-source Bitwarden-compatible server: @@ -260,7 +232,7 @@ export BW_SESSION=$(bw unlock --raw) fnox get DATABASE_PASSWORD ``` -## CI/CD Example +## CI/CD example ### GitHub Actions @@ -289,11 +261,11 @@ jobs: fnox exec -- npm test ``` -## Session Token Management +## Session token management -The `BW_SESSION` token expires after a period of inactivity. +The `BW_SESSION` value represents an unlocked vault session. If the session is no longer valid, unlock again and update the environment variable. -### Option 1: Unlock Each Time +### Option 1: unlock each time ```bash #!/bin/bash @@ -301,19 +273,19 @@ export BW_SESSION=$(bw unlock --raw) fnox exec -- npm start ``` -### Option 2: Store Encrypted (Bootstrap) +### Option 2: store encrypted (bootstrap) ```bash # Store once -fnox set BW_SESSION "$(bw unlock --raw)" --provider age +bw unlock --raw | fnox set BW_SESSION --provider age # Use repeatedly export BW_SESSION=$(fnox get BW_SESSION) fnox exec -- npm start ``` -::: warning Token Expiration -Bitwarden session tokens expire. You'll need to unlock periodically: +::: tip Refresh an unavailable session +If the vault is locked or the session is no longer usable, unlock it again: ```bash export BW_SESSION=$(bw unlock --raw) @@ -321,7 +293,7 @@ export BW_SESSION=$(bw unlock --raw) ::: -## Collections and Organizations +## Collections and organizations Filter secrets by collection or organization: @@ -353,29 +325,19 @@ For local development without a Bitwarden account: source ./test/setup-bitwarden-test.sh # Follow on-screen instructions: -# 1. Create account at https://localhost:8080 (accept self-signed certificate) -# 2. Login: export NODE_TLS_REJECT_UNAUTHORIZED=0 && bw login -# 3. Unlock: export BW_SESSION=$(bw unlock --raw) +# Create account at https://localhost:8080 (accept self-signed certificate) +# Login: export NODE_TLS_REJECT_UNAUTHORIZED=0 && bw login +# Unlock: export BW_SESSION=$(bw unlock --raw) # Run tests mise run test:bats -- test/bitwarden.bats ``` -See `test/BITWARDEN_TESTING.md` for details. - -## Pros +See the [local testing guide](https://github.com/jdx/fnox/blob/main/test/BITWARDEN_TESTING.md) for details. -- βœ… Open source -- βœ… Free for personal use -- βœ… Self-hosting option (Vaultwarden) -- βœ… Good audit logs -- βœ… Cross-platform +## Usage notes -## Cons - -- ❌ UI less polished than 1Password -- ❌ Session token expires (need to unlock regularly) -- ❌ Requires network access (unless self-hosted locally) +The `bw` backend needs an unlocked vault session. A self-hosted Vaultwarden server uses the same reference formats. For machine credentials in Bitwarden Secrets Manager, use the separate `bitwarden-sm` provider. ## Troubleshooting @@ -407,15 +369,7 @@ Re-unlock: export BW_SESSION=$(bw unlock --raw) ``` -## Best Practices - -1. **Store session token encrypted** - Use age to encrypt `BW_SESSION` -2. **Use collections for organization** - Group secrets logically -3. **Self-host for full control** - Consider Vaultwarden -4. **Unlock before long operations** - Session won't expire mid-operation -5. **Use organizations for teams** - Better access control - -## Next Steps +## Next steps - [1Password](/providers/1password) - Commercial alternative - [OS Keychain](/providers/keychain) - Local alternative diff --git a/docs/providers/doppler.md b/docs/providers/doppler.md index e57226af..8343a5f8 100644 --- a/docs/providers/doppler.md +++ b/docs/providers/doppler.md @@ -1,26 +1,33 @@ +--- +description: "Load Doppler secrets with fnox using a project, config, and local login or service token." +--- + # Doppler Integrate with [Doppler](https://www.doppler.com/) to retrieve secrets from your Doppler projects and configs. -## Quick Start +## Quick start -```bash -# 1. Install Doppler CLI +```sh +# Install Doppler CLI brew install dopplerhq/cli/doppler -# 2. Login to Doppler +# Login to Doppler doppler login +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 3. Configure Doppler provider -cat >> fnox.toml << 'EOF' +```toml [providers] doppler = { type = "doppler", project = "my-project", config = "prd" } [secrets] DATABASE_URL = { provider = "doppler", value = "DATABASE_URL" } -EOF +``` -# 4. Use it +```sh +# Use it fnox get DATABASE_URL ``` @@ -48,13 +55,13 @@ mise use -g "github:DopplerHQ/cli" ### 1. Authentication -#### Option A: Interactive Login (Local Development) +#### Option A: interactive login (local development) ```bash doppler login ``` -#### Option B: Service Token (CI/CD) +#### Option B: service token (CI/CD) Create a service token in the Doppler dashboard scoped to a specific project and config: @@ -62,7 +69,7 @@ Create a service token in the Doppler dashboard scoped to a specific project and export DOPPLER_TOKEN="dp.st.prd.xxxx" ``` -### 2. Configure Doppler Provider +### 2. Configure Doppler provider ```toml [providers] @@ -77,7 +84,7 @@ All fields are optional. If not specified, the Doppler CLI will use its own defa - `config` - Doppler config (environment) name (e.g., "dev", "stg", "prd"). If omitted, uses the config configured via `doppler setup`. - `token` - Service token for authentication. If omitted, uses the `FNOX_DOPPLER_TOKEN` or `DOPPLER_TOKEN` environment variable (in that order), or the interactive login session. -## Referencing Secrets +## Referencing secrets ```toml [secrets] @@ -97,7 +104,7 @@ fnox get DATABASE_URL fnox exec -- npm start ``` -## Multi-Environment Example +## Multi-environment example Use named provider instances to pull secrets from different Doppler projects or configs: @@ -142,7 +149,7 @@ fnox exec --profile staging -- npm start fnox exec --profile production -- ./deploy.sh ``` -## CI/CD Example +## CI/CD example ### GitHub Actions @@ -169,9 +176,9 @@ jobs: 1. Create a service token in the Doppler dashboard for the target project/config 2. Add the token to GitHub Secrets as `DOPPLER_TOKEN` -## Token Management +## Token management -### Environment Variables +### Environment variables fnox checks for tokens in this order: @@ -180,7 +187,7 @@ fnox checks for tokens in this order: 3. `DOPPLER_TOKEN` environment variable 4. Interactive login session (from `doppler login`) -### Bootstrap Pattern +### Bootstrap pattern Store the Doppler token encrypted for easy bootstrap: @@ -193,20 +200,9 @@ export DOPPLER_TOKEN=$(fnox get DOPPLER_TOKEN) fnox exec -- npm start ``` -## Pros - -- βœ… Developer-friendly dashboard and CLI -- βœ… Simple project/config/environment model -- βœ… Automatic secret syncing across environments -- βœ… Good integrations (GitHub, Vercel, AWS, etc.) -- βœ… Secret referencing and inheritance between configs -- βœ… Audit logs and access controls -- βœ… Free tier available - -## Cons +## Usage notes -- ❌ Requires network access (cloud-only, no self-hosted option) -- ❌ No open source option +A provider instance selects a Doppler project and config. Use separate instances or fnox profiles when environments differ. For automation, scope the service token to the intended config. ## Troubleshooting @@ -237,7 +233,7 @@ Check the secret exists in the correct project/config: doppler secrets --project my-project --config prd ``` -## Next Steps +## Next steps - [Infisical](/providers/infisical) - Alternative cloud secrets manager - [HashiCorp Vault](/providers/vault) - Self-hosted alternative diff --git a/docs/providers/fido2.md b/docs/providers/fido2.md index 0beea043..cf81e638 100644 --- a/docs/providers/fido2.md +++ b/docs/providers/fido2.md @@ -1,10 +1,14 @@ +--- +description: "Encrypt secrets with a FIDO2 authenticator that supports hmac-secret. Learn setup, touch requirements, and recovery limits." +--- + # FIDO2 The `fido2` provider uses the FIDO2 hmac-secret extension to derive an AES-256-GCM encryption key from a hardware security key. Secrets are encrypted symmetrically β€” decryption requires the same physical FIDO2 key. -## Why? +## When to use it -The `fido2` provider ties encryption to a physical hardware device using the CTAP2 hmac-secret extension. Any FIDO2-compatible security key that supports hmac-secret can be used (YubiKey 5, SoloKeys, Nitrokey, etc.). +The `fido2` provider ties encryption to a physical hardware device using the CTAP2 hmac-secret extension. Use a security key and firmware that support the hmac-secret extension; FIDO2 support alone is not sufficient. The config is fully portable: move your `fnox.toml` to any machine, plug in the same FIDO2 key, and it works. @@ -47,7 +51,7 @@ fnox get MY_SECRET Within a single `fnox exec` invocation, the key is only touched once. The hmac-secret response is cached in memory for the duration of the process. -## With Credential Leases +## With credential leases The `fido2` provider works well with [credential leases](/guide/leases) and the `env = false` secret option: @@ -68,7 +72,7 @@ role_arn = "arn:aws:iam::123456789012:role/dev-role" region = "us-east-1" ``` -## How It Works +## How it works 1. **Setup:** A FIDO2 credential is created with hmac-secret extension; credential ID and a random 32-byte salt are stored in config 2. **hmac-secret:** The salt is sent to the authenticator during assertion, which returns a 32-byte HMAC derived from an internal device secret @@ -77,19 +81,19 @@ region = "us-east-1" The HMAC response is never stored on disk. It exists only in process memory after a key touch. -## Important Notes +## Important notes ::: warning Renaming providers invalidates cached credentials -The provider name is used in key derivation (HKDF context). Renaming a provider (e.g., from `secure` to `my_fido2`) will change the derived encryption key, making all previously encrypted secrets and cached lease credentials undecryptable. If you need to rename, re-encrypt all secrets after renaming. +The provider name is used in key derivation (HKDF context). Renaming a provider (e.g., from `secure` to `my_fido2`) will change the derived encryption key, making all previously encrypted secrets and cached lease credentials undecryptable. To migrate, keep the old provider available, create a new provider under the new name, and read values through the old provider before storing them with the new one. Verify the new values before removing the old configuration. Renaming first prevents decryption. ::: ## Requirements - A FIDO2-compatible security key with hmac-secret extension support -- Most modern security keys support this: YubiKey 5 series, SoloKeys, Nitrokey FIDO2, Google Titan (v2+) +- Confirm hmac-secret support for your device and firmware before setup - PIN may be required depending on your key's configuration -## FIDO2 vs YubiKey Provider +## FIDO2 vs YubiKey provider | Feature | FIDO2 | YubiKey | | ----------- | ----------------- | ---------------------------- | @@ -99,4 +103,9 @@ The provider name is used in key derivation (HKDF context). Renaming a provider | Key output | 32 bytes | 20 bytes | | Slot config | N/A | Slot 1 or 2 | -Choose `fido2` if you have any FIDO2-compatible key. Choose `yubikey` if you specifically use YubiKey's HMAC-SHA1 challenge-response (configured via `ykman otp chalresp`). +Choose `fido2` if your key supports the hmac-secret extension. Choose `yubikey` if you specifically use YubiKey's HMAC-SHA1 challenge-response (configured via `ykman otp chalresp`). + +## Next steps + +- [Sync a local cache](/guide/sync): use the hardware provider as an encryption target. +- [Credential leases](/guide/leases): protect credentials used to create temporary access. diff --git a/docs/providers/foks.md b/docs/providers/foks.md index 2914c16b..b6fa5748 100644 --- a/docs/providers/foks.md +++ b/docs/providers/foks.md @@ -1,30 +1,37 @@ +--- +description: "Read and write FOKS key-value secrets with fnox using personal or team namespaces and interactive or bot authentication." +--- + # FOKS Integrate with [FOKS](https://foks.pub) β€” the Federated Open Key Service β€” to store secrets in an end-to-end encrypted, self-hostable key-value store. Secrets are encrypted on the client; the server only ever sees ciphertext. FOKS pairs well with fnox for teams that want their secrets manager to be open source, federated, and free of cloud-vendor lock-in. The hosted instance at [foks.app](https://foks.app) and self-hosted FOKS servers behave identically. fnox just shells out to the `foks` CLI either way. -## Quick Start +## Quick start -```bash -# 1. Install the foks CLI +```sh +# Install the foks CLI brew install foks # macOS / Linuxbrew # or: curl -fsSL https://pkgs.foks.pub/install.sh | sh && apt-get install foks # Debian/Ubuntu -# 2. Start the agent and sign up (or log in) +# Start the agent and sign up (or log in) foks ctl start foks signup +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 3. Configure the FOKS provider -cat >> fnox.toml << 'EOF' +```toml [providers] foks = { type = "foks", prefix = "/fnox/" } [secrets] DATABASE_URL = { provider = "foks", value = "DATABASE_URL" } -EOF +``` -# 4. Store a secret and read it back +```sh +# Store a secret and read it back fnox set DATABASE_URL "postgres://..." --provider foks fnox get DATABASE_URL ``` @@ -78,7 +85,7 @@ foks signup # new user foks login # existing user, new device ``` -### 3. (Optional) Create a team for shared secrets +### 3. (Optional) create a team for shared secrets If you want to share secrets with teammates, create a FOKS team: @@ -104,7 +111,7 @@ foks = { type = "foks", prefix = "/fnox/" } - `host` β€” The FOKS server hostname (e.g. `foks.app` or your self-hosted server). Required for non-interactive bot-token auth (see [CI/CD](#cicd)). Falls back to `FNOX_FOKS_HOST` / `FOKS_HOST`. - `bot_token` β€” A FOKS bot token for non-interactive auth (CI). Almost always you want to leave this unset and supply it via the `FOKS_BOT_TOKEN` env var instead, so it isn't checked into your config. Also accepts `FNOX_FOKS_BOT_TOKEN`. -## Referencing Secrets +## Referencing secrets ```toml [secrets] @@ -127,7 +134,7 @@ fnox get DATABASE_URL fnox exec -- npm start ``` -## Personal vs Team Secrets +## Personal vs team secrets Use named provider instances to mix personal and team-scoped secrets in the same config: @@ -144,7 +151,7 @@ DEPLOY_KEY = { provider = "ops", value = "deploy/key" } `PERSONAL_TOKEN` is read from your personal namespace; the rest are read from the `ops` team's namespace and stay accessible to teammates. -## CI/CD +## CI/CD {#cicd} For non-interactive environments, configure the provider with a `host` and let fnox handle authentication via a FOKS bot token. On the first auth failure, the provider runs `foks bot use --host ` with the token from the `FOKS_BOT_TOKEN` env var, then transparently retries. @@ -179,19 +186,9 @@ jobs: If you'd rather keep `host` out of `fnox.toml`, set it via `FOKS_HOST` in the workflow env. Likewise, `bot_token` can live in the config (encrypted with a bootstrap provider like `age`) instead of the env var, but the env var is usually simpler. -## Pros - -- βœ… End-to-end encrypted β€” the FOKS server never sees plaintext -- βœ… Open source and self-hostable -- βœ… Federated: a self-hosted FOKS server interoperates with the hosted service -- βœ… Teams have first-class shared namespaces -- βœ… Hierarchical KV paths and multiple devices per identity - -## Cons +## Usage notes -- ❌ Newer / smaller ecosystem than Vault, AWS Secrets Manager, etc. -- ❌ Requires the `foks` agent to be running locally -- ❌ CI integration requires a bot-token bootstrap step +The provider invokes the FOKS CLI and requires its local agent. Personal and team namespaces are selected by provider configuration. CI needs the host and a bot-token bootstrap. ## Troubleshooting @@ -228,7 +225,7 @@ If you set a `team` in your provider config, scope the listing to the team: foks kv ls --team my-team / ``` -## Next Steps +## Next steps - [HashiCorp Vault](/providers/vault) β€” Closest comparable self-hosted alternative - [password-store](/providers/password-store) β€” GPG-based local alternative diff --git a/docs/providers/gcp-kms.md b/docs/providers/gcp-kms.md index 9a65fbd5..d708f2ca 100644 --- a/docs/providers/gcp-kms.md +++ b/docs/providers/gcp-kms.md @@ -1,29 +1,38 @@ +--- +description: "Encrypt values in fnox.toml with Google Cloud KMS. Configure a key ring, key, credentials, and IAM permissions." +--- + # Google Cloud KMS Google Cloud KMS encrypts secrets using GCP-managed keys. The encrypted ciphertext is stored in your `fnox.toml` file. -## Quick Start +## Quick start -```bash -# 1. Enable Cloud KMS and create key +Select your Google Cloud project and configure Application Default Credentials before following this example. The identity running fnox needs access to the key. + +```sh +# Enable Cloud KMS and create key gcloud services enable cloudkms.googleapis.com gcloud kms keyrings create "fnox-keyring" --location="us-central1" gcloud kms keys create "fnox-key" --keyring="fnox-keyring" --location="us-central1" --purpose="encryption" +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 2. Configure provider -cat >> fnox.toml << 'EOF' +```toml [providers.gcpkms] type = "gcp-kms" project = "my-project-id" location = "us-central1" keyring = "fnox-keyring" key = "fnox-key" -EOF +``` -# 3. Encrypt a secret +```sh +# Encrypt a secret fnox set DATABASE_URL "postgresql://prod.example.com/db" --provider gcpkms -# 4. Get secret (decrypts via KMS) +# Get secret (decrypts via KMS) fnox get DATABASE_URL ``` @@ -50,26 +59,18 @@ keyring = "fnox-keyring" key = "fnox-key" ``` -## How It Works +## How it works Similar to [AWS KMS](/providers/aws-kms): 1. **Encryption:** Calls Cloud KMS, stores ciphertext in fnox.toml 2. **Decryption:** Calls Cloud KMS to recover plaintext -## Pros - -- βœ… Secrets in git (version control) -- βœ… GCP-managed keys -- βœ… GCP IAM integration - -## Cons +## Usage notes -- ❌ Requires GCP project -- ❌ Costs money -- ❌ Network access required +Ciphertext lives in your config. Decryption still requires network access and IAM permission on the Cloud KMS key. Keep the required key version available for existing ciphertext. -## Next Steps +## Next steps - [GCP Secret Manager](/providers/gcp-sm) - Remote storage alternative - [Age Encryption](/providers/age) - Free local encryption diff --git a/docs/providers/gcp-sm.md b/docs/providers/gcp-sm.md index f7e3a923..31d37fba 100644 --- a/docs/providers/gcp-sm.md +++ b/docs/providers/gcp-sm.md @@ -1,29 +1,39 @@ +--- +description: "Read Google Cloud Secret Manager values with fnox. Configure project access, authentication, prefixes, and secret versions." +--- + # Google Cloud Secret Manager GCP Secret Manager provides centralized secret management for Google Cloud workloads. -## Quick Start +## Quick start -```bash -# 1. Enable Secret Manager API +```sh +# Enable Secret Manager API gcloud services enable secretmanager.googleapis.com +``` -# 2. Configure provider -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] -gcp = { type = "gcp-sm", project = "my-project-id", prefix = "myapp/" } -EOF +gcp = { type = "gcp-sm", project = "my-project-id", prefix = "myapp-" } +``` -# 3. Create secret +```sh +# Create secret echo -n "postgresql://..." | gcloud secrets create myapp-database-url --data-file=- +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 4. Reference in fnox -cat >> fnox.toml << 'EOF' +```toml [secrets] DATABASE_URL = { provider = "gcp", value = "database-url" } -EOF +``` -# 5. Get secret +```sh +# Get secret fnox get DATABASE_URL ``` @@ -56,23 +66,14 @@ gcloud projects add-iam-policy-binding PROJECT-ID \ ```toml [providers] -gcp = { type = "gcp-sm", project = "my-project-id", prefix = "myapp/" } # prefix is optional +gcp = { type = "gcp-sm", project = "my-project-id", prefix = "myapp-" } # prefix is optional ``` -## Pros - -- βœ… Integrated with GCP IAM -- βœ… Audit logs -- βœ… Automatic replication -- βœ… Versioning - -## Cons +## Usage notes -- ❌ Requires GCP project -- ❌ Costs money -- ❌ Network access required +The config stores a secret name in the selected project. The provider prefix is part of that name. Authentication uses Application Default Credentials; a separate gcloud CLI login is not always sufficient. -## Next Steps +## Next steps - [Google Cloud KMS](/providers/gcp-kms) - Encryption alternative - [AWS Secrets Manager](/providers/aws-sm) - AWS equivalent diff --git a/docs/providers/infisical.md b/docs/providers/infisical.md index 215ef00b..fc44f3d6 100644 --- a/docs/providers/infisical.md +++ b/docs/providers/infisical.md @@ -1,14 +1,18 @@ +--- +description: "Load Infisical secrets with fnox using service tokens or universal authentication, project environments, and secret paths." +--- + # Infisical Integrate with Infisical to retrieve secrets from your Infisical projects and environments. -## Quick Start +## Quick start -```bash -# 1. Install Infisical CLI +```sh +# Install Infisical CLI brew install infisical/get-cli/infisical -# 2. Authenticate with a service token or a machine identity +# Authenticate with a service token or a machine identity # Option A: Service token (from Infisical dashboard) export INFISICAL_TOKEN="your-service-token" @@ -16,25 +20,31 @@ export INFISICAL_TOKEN="your-service-token" export INFISICAL_CLIENT_ID="your-client-id" export INFISICAL_CLIENT_SECRET="your-client-secret" -# 3. Store token (optional, for bootstrap) +# Store token (optional, for bootstrap) fnox set INFISICAL_TOKEN "your-service-token" --provider age +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 4. Configure Infisical provider -cat >> fnox.toml << 'EOF' +```toml [providers] infisical = { type = "infisical", project_id = "your-project-id", environment = "dev", path = "/" } -EOF +``` -# 5. Add secrets to Infisical +```sh +# Add secrets to Infisical infisical secrets set DATABASE_PASSWORD "secret-password" +``` -# 6. Reference in fnox -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [secrets] DATABASE_PASSWORD = { provider = "infisical", value = "DATABASE_PASSWORD" } -EOF +``` -# 7. Use it +```sh +# Use it fnox get DATABASE_PASSWORD ``` @@ -74,9 +84,9 @@ infisical login --domain=https://infisical.example.com `infisical login` sets up the CLI for manual commands such as `infisical secrets set`. fnox itself does not use this login session; it authenticates with the token or machine identity credentials from the next step. -### 2. Get Authentication Token +### 2. Get authentication token -#### Option A: Service Token (Recommended for CI/CD) +#### Option A: service token (recommended for CI/CD) 1. Go to your Infisical project settings 2. Navigate to "Service Tokens" @@ -87,7 +97,7 @@ infisical login --domain=https://infisical.example.com export INFISICAL_TOKEN="st.xxx.yyy.zzz" ``` -#### Option B: Universal Auth (Machine Identity) +#### Option B: Universal Auth (machine identity) ```bash # Provide the machine identity credentials. fnox runs @@ -99,7 +109,7 @@ export INFISICAL_CLIENT_SECRET="your-client-secret" `FNOX_INFISICAL_CLIENT_ID` and `FNOX_INFISICAL_CLIENT_SECRET` are also accepted and take priority over the unprefixed variables. -### 3. Store Token (Bootstrap) +### 3. Store token (bootstrap) Optionally, store the token encrypted for easy bootstrap: @@ -111,7 +121,7 @@ fnox set INFISICAL_TOKEN "st.xxx.yyy.zzz" --provider age export INFISICAL_TOKEN=$(fnox get INFISICAL_TOKEN) ``` -### 4. Configure Infisical Provider +### 4. Configure Infisical provider ```toml [providers] @@ -126,9 +136,9 @@ All fields are optional. If not specified, the Infisical CLI will use its own de - `environment` - Environment slug (e.g., "dev", "staging", "prod"). If omitted, CLI defaults to "dev". - `path` - Secret path within the project. If omitted, CLI defaults to "/". -## Adding Secrets to Infisical +## Adding secrets to Infisical -### Via Infisical Web Dashboard +### Via Infisical web dashboard 1. Go to your Infisical dashboard 2. Select your project @@ -159,7 +169,7 @@ infisical secrets set API_KEY "sk-abc123" \ infisical secrets list ``` -## Referencing Secrets +## Referencing secrets Add references to `fnox.toml`: @@ -170,7 +180,7 @@ API_KEY = { provider = "infisical", value = "API_KEY" } DATABASE_URL = { provider = "infisical", value = "DATABASE_URL" } ``` -## Reference Format +## Reference format ```toml [secrets] @@ -192,7 +202,7 @@ fnox get DATABASE_PASSWORD fnox exec -- npm start ``` -## Multi-Environment Example +## Multi-environment example ```toml # Bootstrap token (encrypted in git) @@ -232,7 +242,7 @@ fnox exec --profile staging -- npm start fnox exec --profile production -- ./deploy.sh ``` -## Secret Paths +## Secret paths Organize secrets with paths: @@ -247,7 +257,7 @@ API_KEY = { provider = "infisical-api", value = "API_KEY" } # β†’ /api/API_KEY DATABASE_URL = { provider = "infisical-db", value = "DATABASE_URL" } # β†’ /database/DATABASE_URL ``` -## CI/CD Example +## CI/CD example ### GitHub Actions @@ -282,7 +292,7 @@ jobs: 2. Add the token to GitHub Secrets as `INFISICAL_TOKEN` 3. The workflow will automatically use it -## Self-Hosted Infisical +## Self-hosted Infisical Configure the CLI to use your self-hosted instance: @@ -297,11 +307,11 @@ export INFISICAL_API_URL=https://infisical.example.com/api fnox get DATABASE_PASSWORD ``` -## Token Management +## Token management The `INFISICAL_TOKEN` is typically a service token or machine identity token. `FNOX_INFISICAL_TOKEN` is also accepted and takes priority over `INFISICAL_TOKEN`. -### Option 1: Set Each Time +### Option 1: set each time ```bash #!/bin/bash @@ -309,7 +319,7 @@ export INFISICAL_TOKEN="st.xxx.yyy.zzz" fnox exec -- npm start ``` -### Option 2: Store Encrypted (Bootstrap) +### Option 2: store encrypted (bootstrap) ```bash # Store once @@ -320,9 +330,9 @@ export INFISICAL_TOKEN=$(fnox get INFISICAL_TOKEN) fnox exec -- npm start ``` -## Service Token vs Universal Auth +## Service token vs Universal Auth -### Service Token (Simple) +### Service token (simple) - **Best for:** CI/CD, simple automation - **Pros:** Easy to set up, just one token @@ -332,7 +342,7 @@ fnox exec -- npm start export INFISICAL_TOKEN="st.xxx.yyy.zzz" ``` -### Universal Auth (Advanced) +### Universal Auth (advanced) - **Best for:** Machine identities, advanced use cases - **Pros:** Automatic rotation, better audit logs, fine-grained permissions @@ -343,19 +353,9 @@ export INFISICAL_CLIENT_ID="..." export INFISICAL_CLIENT_SECRET="..." ``` -## Pros - -- βœ… Modern, developer-friendly UI -- βœ… Open source (self-hosting option) -- βœ… Good API and CLI -- βœ… Secret versioning and audit logs -- βœ… Point-in-time recovery -- βœ… Integrations with many platforms +## Usage notes -## Cons - -- ❌ Requires network access to an Infisical instance -- ❌ Relatively new compared to Vault or cloud providers +fnox uses a token or machine identity credentials, rather than the CLI's interactive login session. Set the project, environment, and path explicitly when one identity can access several environments. ## Troubleshooting @@ -393,16 +393,7 @@ Regenerate service token in Infisical dashboard and update: fnox set INFISICAL_TOKEN "new-token" --provider age ``` -## Best Practices - -1. **Use service tokens for automation** - Create read-only tokens for CI/CD -2. **Organize with paths** - Use paths to logically group secrets -3. **Leverage environments** - Use dev/staging/prod environments -4. **Store token encrypted** - Use age to encrypt `INFISICAL_TOKEN` -5. **Self-host for sensitive workloads** - Full control over your secrets -6. **Use secret versioning** - Track changes and rollback if needed - -## Next Steps +## Next steps - [1Password](/providers/1password) - Alternative password manager - [Vault](/providers/vault) - More established alternative diff --git a/docs/providers/keepass.md b/docs/providers/keepass.md index 267418ac..d9cbb76e 100644 --- a/docs/providers/keepass.md +++ b/docs/providers/keepass.md @@ -1,23 +1,30 @@ +--- +description: "Read and write KeePass database entries with fnox. Configure the database, password, key file, and entry paths." +--- + # KeePass Store secrets in a local KeePass database file (`.kdbx`), supporting KDBX4 format with read/write operations. -## Quick Start +## Quick start -```bash -# 1. Set database password +```sh +# Set database password export FNOX_KEEPASS_PASSWORD="your-master-password" +``` -# 2. Configure provider -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] keepass = { type = "keepass", database = "~/secrets.kdbx" } -EOF +``` -# 3. Store a secret +```sh +# Store a secret fnox set DATABASE_URL "postgresql://localhost/mydb" --provider keepass -# 4. Retrieve from database +# Retrieve from database fnox get DATABASE_URL ``` @@ -27,28 +34,28 @@ fnox get DATABASE_URL [providers] keepass = { type = "keepass", database = "~/secrets.kdbx" } -# OR with keyfile for additional security -keepass = { type = "keepass", database = "~/secrets.kdbx", keyfile = "~/keyfile.key" } +# A second instance using a keyfile +keepass-with-keyfile = { type = "keepass", database = "~/secrets.kdbx", keyfile = "~/keyfile.key" } ``` -### Database Path +### Database path The `database` field specifies the path to your `.kdbx` file. Relative paths are resolved from the config file that declares the provider, and `~` expands to your home directory: ```toml [providers] -keepass = { database = "~/secrets.kdbx" } # Home directory -keepass = { database = "./secrets/vault.kdbx" } # Relative to this config file -keepass = { database = "/opt/secrets/shared.kdbx" } # Absolute path +home-db = { type = "keepass", database = "~/secrets.kdbx" } # Home directory +project-db = { type = "keepass", database = "./secrets/vault.kdbx" } # Relative to this config file +shared-db = { type = "keepass", database = "/opt/secrets/shared.kdbx" } # Absolute path ``` -### Keyfile (Optional) +### Keyfile (optional) For additional security, use a keyfile alongside the password: ```toml [providers] -keepass = { database = "~/secrets.kdbx", keyfile = "~/keyfile.key" } +keepass = { type = "keepass", database = "~/secrets.kdbx", keyfile = "~/keyfile.key" } ``` Relative `keyfile` paths follow the same config-relative rule as `database`. @@ -72,7 +79,7 @@ export KEEPASS_PASSWORD="your-master-password" The provider also accepts a `password` field, but avoid storing the password directly in the provider config. Use environment variables instead; they take priority over the config value. ::: -## Reference Formats +## Reference formats KeePass supports flexible path formats: @@ -83,7 +90,7 @@ KeePass supports flexible path formats: | Group/entry | `work/my-entry` | Gets password from entry in group | | Full path | `work/project/api-key/notes` | Group path + entry + field | -### Simple Entry Name +### Simple entry name ```toml [secrets] @@ -92,7 +99,7 @@ DATABASE_URL = { provider = "keepass", value = "database-url" } Searches all groups for an entry with this title and returns the password field. -### Entry with Field +### Entry with field ```toml [secrets] @@ -101,7 +108,7 @@ DB_PASS = { provider = "keepass", value = "database/password" } DB_HOST = { provider = "keepass", value = "database/url" } ``` -### Group Path +### Group path ```toml [secrets] @@ -109,7 +116,7 @@ PROD_API_KEY = { provider = "keepass", value = "production/api/my-service" } DEV_API_KEY = { provider = "keepass", value = "development/api/my-service" } ``` -### Full Path with Field +### Full path with field ```toml [secrets] @@ -117,7 +124,7 @@ API_USER = { provider = "keepass", value = "production/api/my-service/username" API_NOTES = { provider = "keepass", value = "production/api/my-service/notes" } ``` -## Supported Fields +## Supported fields | Field | Description | | ---------- | ------------------------ | @@ -129,7 +136,7 @@ API_NOTES = { provider = "keepass", value = "production/api/my-service/notes" } Field names are case-insensitive (`Username`, `USERNAME`, `username` all work). -## How It Works +## How it works 1. **Storage:** Secrets are stored in a local `.kdbx` database file 2. **Config:** `fnox.toml` contains the entry name/path (not the actual secret value) @@ -139,7 +146,7 @@ Field names are case-insensitive (`Username`, `USERNAME`, `username` all work). ## Usage -### Store a Secret +### Store a secret ```bash fnox set DATABASE_URL "postgresql://localhost/mydb" --provider keepass @@ -152,28 +159,28 @@ Your `fnox.toml`: DATABASE_URL = { provider = "keepass", value = "DATABASE_URL" } # Entry title, not the actual secret ``` -### Store with Specific Path +### Store with specific path ```bash # Store in a specific group with specific field fnox set API_USER "admin" --provider keepass --key-name "production/api-service/username" ``` -### Retrieve a Secret +### Retrieve a secret ```bash fnox get DATABASE_URL ``` -### Run Commands +### Run commands ```bash fnox exec -- npm run dev ``` -## Example Configurations +## Example configurations -### Personal Password Database +### Personal password database ```toml [providers] @@ -184,7 +191,7 @@ GITHUB_TOKEN = { provider = "keepass", value = "github/token" } NPM_TOKEN = { provider = "keepass", value = "npm/token" } ``` -### Project-Specific Database +### Project-specific database ```toml [providers] @@ -195,7 +202,7 @@ DATABASE_URL = { provider = "keepass", value = "database" } API_KEY = { provider = "keepass", value = "api-key" } ``` -### Organized by Environment +### Organized by environment ```toml [providers] @@ -208,7 +215,7 @@ DEV_DB = { provider = "keepass", value = "development/database/password" } PROD_DB = { provider = "keepass", value = "production/database/password" } ``` -### With Keyfile +### With keyfile ```toml [providers] @@ -218,28 +225,13 @@ keepass = { type = "keepass", database = "~/secure.kdbx", keyfile = "~/secure.ke MASTER_KEY = { provider = "keepass", value = "master-key" } ``` -## Pros - -- βœ… Local-first - no cloud dependency -- βœ… Industry-standard KDBX4 format -- βœ… Works offline -- βœ… Free and open source -- βœ… Compatible with KeePass, KeePassXC, and other KDBX tools -- βœ… Supports keyfile for two-factor security -- βœ… Organized with groups/folders -- βœ… Atomic writes prevent corruption +## Usage notes -## Cons - -- ❌ Database file must be accessible (not suitable for teams without sync) -- ❌ Requires master password in environment -- ❌ No built-in sync (use Syncthing, Dropbox, etc.) -- ❌ No audit logs -- ❌ No centralized management +The provider reads and writes a local KDBX4 database. Back up the database and any required keyfile. Coordinate writes if several machines share the same file; atomic local saves do not merge concurrent changes. ## Limitations -### Database Sync +### Database sync KeePass databases are single files. For team use, sync via: @@ -250,16 +242,15 @@ KeePass databases are single files. For team use, sync via: For teams, consider [1Password](/providers/1password), [Bitwarden](/providers/bitwarden), or cloud providers instead. -### Title Field is Read-Only +### Title field is read-only The `title` field cannot be modified via fnox - it's reserved for entry identification. -## Security +## Database protection - **Encryption:** KDBX4 format uses AES-256 or ChaCha20 - **Key derivation:** Argon2d for password-based key derivation -- **Protected fields:** Password fields stored in protected memory -- **Atomic saves:** Prevents corruption on write failure +- **Atomic saves:** Writes through a temporary file before replacing the database ## Troubleshooting @@ -306,16 +297,7 @@ fnox auto-creates databases. If you need to pre-populate: 2. Add entries manually 3. Reference them in fnox.toml -## Best Practices - -1. **Use FNOX_KEEPASS_PASSWORD** - Set via environment, not config -2. **Consider keyfile** - Adds two-factor security -3. **Organize with groups** - Use group paths for organization -4. **Back up regularly** - Database is a single file -5. **Use KeePassXC** - Modern GUI for database management -6. **Gitignore the database** - Unless you intentionally share the encrypted file - -## Running Tests +## Running tests ```bash # Run the KeePass tests @@ -324,7 +306,7 @@ mise run test:bats -- test/keepass.bats The tests create a temporary database with their own password, so no external setup is needed. -## Next Steps +## Next steps - [OS Keychain](/providers/keychain) - Alternative local storage - [password-store](/providers/password-store) - GPG-based alternative diff --git a/docs/providers/keeper-sm.md b/docs/providers/keeper-sm.md index a33eb337..cc9333ac 100644 --- a/docs/providers/keeper-sm.md +++ b/docs/providers/keeper-sm.md @@ -1,8 +1,12 @@ +--- +description: "Read Keeper Secrets Manager records with fnox using application configuration, one-time token bootstrap, and Keeper notation." +--- + # Keeper Secrets Manager Use [Keeper Secrets Manager](https://docs.keeper.io/keeperpam/secrets-manager) through Keeper's official Rust SDK. The provider is read-only: fnox retrieves secrets but does not create or update Keeper records. -## Quick Start +## Quick start Create a Keeper Secrets Manager application and client device, then download its JSON configuration file. @@ -66,7 +70,7 @@ The `token` field can reference a bootstrap secret managed by another fnox provi keeper = { type = "keeper-sm", config_file = "~/.keeper/ksm-config.json", token = { secret = "KEEPER_BOOTSTRAP_TOKEN" } } ``` -## Keeper Notation +## Keeper notation The secret `value` is passed to Keeper's notation resolver. Common selectors include: @@ -80,7 +84,7 @@ TITLE = { provider = "keeper", value = "RECORD_UID/title" } String fields are returned directly. Structured Keeper values are serialized as compact JSON strings. -## Environment Variables +## Environment variables | Variable | Description | | -------------------- | ------------------------------------------------- | @@ -98,3 +102,8 @@ fnox provider test keeper ``` The test asks Keeper for all records accessible to the configured application. Confirm that the application has access to the shared folder containing the referenced record and that the client configuration is readable. + +## Next steps + +- [Profiles](/guide/profiles): select the application configuration for an environment. +- [Sync a local cache](/guide/sync): cache accessible records under a personal encryption key. diff --git a/docs/providers/keychain.md b/docs/providers/keychain.md index fe32349b..09eac70c 100644 --- a/docs/providers/keychain.md +++ b/docs/providers/keychain.md @@ -1,33 +1,40 @@ -# OS Keychain +--- +description: "Store local secrets in the macOS Keychain, Windows Credential Manager, or Linux Secret Service, and use them to bootstrap other providers." +--- + +# OS keychain Store secrets in your operating system's native secure storage. -## Supported Platforms +## Supported platforms - **macOS:** Keychain Access (built-in) - **Windows:** Credential Manager (built-in) - **Linux:** Secret Service over D-Bus (GNOME Keyring, KWallet) -## Quick Start +## Quick start -```bash -# 1. Linux only: make sure a Secret Service daemon is running +```sh +# Linux only: make sure a Secret Service daemon is running sudo apt-get install gnome-keyring # Ubuntu/Debian +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: -# 2. Configure provider -cat >> fnox.toml << 'EOF' +```toml [providers] keychain = { type = "keychain", service = "fnox" } -EOF +``` -# 3. Store a secret in OS keychain +```sh +# Store a secret in OS keychain fnox set DATABASE_URL "postgresql://localhost/mydb" --provider keychain -# 4. Retrieve from keychain +# Retrieve from keychain fnox get DATABASE_URL ``` -## Linux Setup +## Linux setup On Linux, fnox talks to the Secret Service API directly over D-Bus, so you need a Secret Service implementation such as GNOME Keyring or KWallet running (no libsecret packages are required): @@ -56,16 +63,13 @@ macOS and Windows have built-in supportβ€”no installation needed. keychain = { type = "keychain", service = "fnox", prefix = "myapp/" } # Prefix is optional ``` -### Service Name +### Service name The `service` acts as a namespace to isolate fnox secrets from other applications: ```toml [providers] -keychain = { service = "fnox" } # All fnox secrets under "fnox" service - -# Or use project-specific service -keychain = { service = "myapp" } # All secrets under "myapp" service +keychain = { type = "keychain", service = "myapp" } ``` ### Prefix @@ -74,10 +78,10 @@ Optional prefix prepended to secret names: ```toml [providers] -keychain = { service = "fnox", prefix = "myapp/" } # "database-url" becomes "myapp/database-url" +keychain = { type = "keychain", service = "fnox", prefix = "myapp/" } # "database-url" becomes "myapp/database-url" ``` -## How It Works +## How it works 1. **Storage:** Secrets are stored in the OS credential manager (encrypted by OS) 2. **Config:** `fnox.toml` contains only the secret name, not the value @@ -87,7 +91,7 @@ keychain = { service = "fnox", prefix = "myapp/" } # "database-url" becomes "my ## Usage -### Store a Secret +### Store a secret ```bash fnox set DATABASE_URL "postgresql://localhost/mydb" --provider keychain @@ -102,21 +106,21 @@ DATABASE_URL = { provider = "keychain", value = "DATABASE_URL" } # ← Keychain The actual secret is stored in the OS keychain, encrypted. -### Retrieve a Secret +### Retrieve a secret ```bash fnox get DATABASE_URL ``` -### Run Commands +### Run commands ```bash fnox exec -- npm run dev ``` -## Recommended: Use With Age, Not As Bulk Storage +## Recommended: use with age, not as bulk storage -The OS keychain is designed for **a few** long-lived secrets, not as the storage backend for every secret in a project. On macOS in particular, the system pops a Security dialog the first time each application accesses each keychain item β€” so if you store ten secrets directly in the keychain, you'll get up to ten "Always Allow / Allow / Deny" prompts the first time `fnox exec` runs. +On macOS, access controls can prompt separately for each keychain item. If those prompts interrupt a project with many secrets, store one age identity in the keychain and use it to decrypt the rest. The pattern that scales much better is to store a single **age private key** in the keychain and encrypt all your secrets with age: @@ -135,13 +139,13 @@ STRIPE_KEY = { provider = "age", value = "encrypted..." } This way: -- **One keychain item, one dialog.** Hitting "Always Allow" once authorizes the age key for that machine. +- **One keychain item for the identity.** Access prompts depend on the keychain's application permissions. - Adding more secrets is free β€” they go into the encrypted config, not into the keychain. - Loss of the keychain item is recoverable from any other machine that holds the same age identity. Reach for direct `provider = "keychain"` only for the handful of bootstrap secrets that don't have anything else to decrypt them (e.g., the age key itself, a 1Password service account token). -## Bootstrap Pattern +## Bootstrap pattern A common pattern is to store provider tokens in the keychain: @@ -163,20 +167,20 @@ export OP_SERVICE_ACCOUNT_TOKEN=$(fnox get OP_SERVICE_ACCOUNT_TOKEN) fnox exec -- ./start.sh ``` -## Example Configurations +## Example configurations -### Personal Project +### Personal project ```toml [providers] -keychain = { type = "keychain", service = "myapp" } +app-keychain = { type = "keychain", service = "myapp" } [secrets] DATABASE_URL = { provider = "keychain", value = "database-url" } API_KEY = { provider = "keychain", value = "api-key" } ``` -### Bootstrap Tokens +### Bootstrap tokens ```toml [providers] @@ -187,7 +191,7 @@ GITHUB_TOKEN = { provider = "keychain", value = "github" } NPM_TOKEN = { provider = "keychain", value = "npm" } ``` -### Machine-Specific Secrets +### Machine-specific secrets ```toml # fnox.local.toml (gitignored) @@ -198,9 +202,9 @@ keychain = { type = "keychain", service = "fnox-local" } LAPTOP_DB_URL = { provider = "keychain", value = "laptop-db" } ``` -## Platform Details +## Platform details -### macOS Keychain +### macOS keychain Secrets stored in: @@ -238,26 +242,13 @@ sudo apt install seahorse seahorse ``` -## Pros - -- βœ… OS-managed encryption -- βœ… Cross-platform (macOS, Windows, Linux) -- βœ… No external dependencies -- βœ… Free -- βœ… Built into operating system -- βœ… Secure by default +## Usage notes -## Cons - -- ❌ Requires GUI/interactive session (doesn't work in headless CI) -- ❌ Not suitable for teams (secrets are per-machine) -- ❌ Keyring must be unlocked -- ❌ No audit logs -- ❌ No centralized management +Access depends on the current user and an available, unlocked credential store. macOS may prompt for access to individual items. Linux needs a Secret Service implementation on D-Bus; headless setups must provide one explicitly. ## Limitations -### Headless Environments +### Headless environments The keychain provider needs an unlocked OS keychain or Secret Service session, which is usually unavailable in: @@ -268,7 +259,7 @@ The keychain provider needs an unlocked OS keychain or Secret Service session, w No desktop session is required, though β€” only an unlocked Secret Service on the bus. If you provision one yourself, the provider works headlessly; fnox's own Linux CI does this with `gnome-keyring-daemon --unlock --components=secrets --daemonize`. That means keeping the unlock password in CI, which is the secret you were trying to protect, so for CI/CD prefer age encryption or a cloud provider. -### Tests Auto-Skip in CI +### Tests auto-skip in CI fnox's keychain tests skip automatically on macOS CI runners (where they hang) and on platforms other than macOS and Linux. On Linux CI they run against a headless `gnome-keyring-daemon`. Set `SKIP_KEYCHAIN_TESTS=1` to skip them everywhere: @@ -282,10 +273,10 @@ SKIP_KEYCHAIN_TESTS=1 mise run test:bats ## Security -- **Encryption:** OS handles encryption (typically AES-256) +- **Encryption:** Managed by the platform credential store - **Access control:** OS enforces access (user/session isolation) - **Keyring unlock:** May require password entry on first access -- **Memory protection:** OS manages secure memory handling +- **Resolved values:** fnox and the receiving process still handle plaintext in memory ## Troubleshooting @@ -329,15 +320,7 @@ gnome-keyring-daemon --start sudo apt-get install kwalletmanager ``` -## Best Practices - -1. **Use for local development only** - Not for teams or CI -2. **Bootstrap provider tokens** - Store 1Password/AWS tokens -3. **Machine-specific overrides** - Use in `fnox.local.toml` -4. **Descriptive service names** - Use project-specific services -5. **Keep keyring unlocked** - Unlock on login for convenience - -## Next Steps +## Next steps - [Age Encryption](/providers/age) - Team-friendly alternative - [Hierarchical Config](/guide/hierarchical-config) - Per-machine configuration with fnox.local.toml diff --git a/docs/providers/overview.md b/docs/providers/overview.md index 2401f2c4..f2ca7f4b 100644 --- a/docs/providers/overview.md +++ b/docs/providers/overview.md @@ -1,104 +1,103 @@ -# Providers Overview +--- +description: "Compare fnox providers by storage model and prerequisites, then find authentication and setup instructions." +--- -fnox supports multiple secret storage and encryption providers. Choose the ones that fit your workflow. +# Choose a provider -Not sure where to start? [The golden path](/guide/what-is-fnox#the-golden-path) combines a remote vault (source of truth) with a local age cache via [`fnox sync`](/guide/sync) β€” see the [Golden Path Setup](/guide/golden-path) walkthrough. +A provider connects a secret name to its storage. Configure an instance under `[providers.]`, then use that name in each secret's `provider` field. You can use several providers in one project. -## Provider Categories +## Start with your storage model -### πŸ” Encryption (secrets in git, encrypted) +| You want to… | Start here | Keep in mind | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| Get started without a cloud account | [age quick start](/guide/quick-start) | Keep the private key outside git and back it up | +| Use your team's existing vault | [Connect a vault](/guide/golden-path) | Each user or CI identity needs access | +| Read remote secrets offline | [Sync to a local age cache](/guide/sync) | Refresh the snapshot after values change | +| Use cloud IAM to control decryption | AWS, Azure, or GCP KMS below | Encrypt and decrypt operations need the cloud API | +| Bind encryption to a hardware token | [FIDO2](/providers/fido2), [YubiKey](/providers/yubikey), or [age plugins](/providers/age#plugin-support) | Native providers need the token for both encryption and decryption | +| Keep a bootstrap credential on one machine | [OS keychain](/providers/keychain) | The credential store must be available and unlocked | +| Generate temporary credentials | [Credential leases](/guide/leases) | Lease backends are configured separately from providers | -Store encrypted secrets in your `fnox.toml` file. The encrypted ciphertext is safe to commit to version control. +## Encryption in your config -| Provider | Description | Best For | -| --------------------------------- | ---------------------------------------- | ----------------------------------------- | -| [age](/providers/age) | Modern encryption (works with SSH keys!) | Development secrets, open source projects | -| [FIDO2](/providers/fido2) | Symmetric key from a FIDO2 security key | Secrets bound to a hardware token | -| [YubiKey](/providers/yubikey) | YubiKey HMAC challenge-response | Secrets bound to a specific YubiKey | -| [AWS KMS](/providers/aws-kms) | AWS Key Management Service | AWS-based projects requiring IAM control | -| [Azure KMS](/providers/azure-kms) | Azure Key Vault encryption | Azure-based projects | -| [GCP KMS](/providers/gcp-kms) | Google Cloud KMS | GCP-based projects | +`fnox set` stores ciphertext in `fnox.toml`. The config and public recipients can be committed; private keys and bootstrap credentials stay outside the repository. -### ☁️ Cloud Secret Storage (remote, centralized) +| Provider | Config type | Decryption requires | +| -------------------------------------------- | ----------- | ------------------------------------------------------ | +| [age](/providers/age) | `age` | An age or supported SSH identity, or an age plugin | +| [FIDO2](/providers/fido2) | `fido2` | The original token with hmac-secret support | +| [YubiKey](/providers/yubikey) | `yubikey` | The original HMAC challenge-response key | +| [AWS KMS](/providers/aws-kms) | `aws-kms` | AWS credentials and permission to decrypt with the key | +| [Azure Key Vault Keys](/providers/azure-kms) | `azure-kms` | Azure credentials and access to the key | +| [Google Cloud KMS](/providers/gcp-kms) | `gcp-kms` | Google Cloud credentials and access to the key | -Store secrets remotely in cloud providers. Your `fnox.toml` contains only references to secret names. +## Cloud and hosted stores -| Provider | Description | Best For | -| ---------------------------------------------------- | ----------------------------------- | ---------------------------------------- | -| [AWS Parameter Store](/providers/aws-ps) | AWS SSM Parameter Store | Config values, simple secrets | -| [AWS Secrets Manager](/providers/aws-sm) | AWS centralized secrets | Production AWS workloads | -| [Azure App Configuration](/providers/azure-ac) | Azure non-secret configuration | Endpoints and toggles owned by Azure IaC | -| [Azure Key Vault Secrets](/providers/azure-sm) | Azure secret storage | Production Azure workloads | -| [GCP Secret Manager](/providers/gcp-sm) | Google Cloud secrets | Production GCP workloads | -| [Bitwarden Secrets Manager](/providers/bitwarden-sm) | Bitwarden Secrets Manager (bws CLI) | Teams using Bitwarden for DevOps secrets | -| [Doppler](/providers/doppler) | Doppler secrets manager | Developer-friendly cloud secrets | -| [FOKS](/providers/foks) | End-to-end encrypted, federated KV | Self-hosted, E2EE, team-shared secrets | -| [HashiCorp Vault](/providers/vault) | Self-hosted or HCP Vault | Multi-cloud, advanced features | -| [Keeper Secrets Manager](/providers/keeper-sm) | Keeper Secrets Manager | Enterprises using Keeper | +`fnox.toml` contains references to values held by the service. Provider permissions control access to those values. Rotation and audit features depend on the service and its configuration; fnox does not enable them automatically. -### πŸ”‘ Password Managers & Secret Services +| Provider | Config type | Reference points to | +| ---------------------------------------------------- | -------------- | ------------------------------------------------------ | +| [AWS Secrets Manager](/providers/aws-sm) | `aws-sm` | A secret name, optionally with `json_path` for a field | +| [AWS Parameter Store](/providers/aws-ps) | `aws-ps` | A parameter name or path | +| [Azure Key Vault Secrets](/providers/azure-sm) | `azure-sm` | A secret name | +| [Azure App Configuration](/providers/azure-ac) | `azure-ac` | A non-secret configuration key and optional label | +| [Google Cloud Secret Manager](/providers/gcp-sm) | `gcp-sm` | A secret name | +| [HashiCorp Vault](/providers/vault) | `vault` | A KV secret and field | +| [Doppler](/providers/doppler) | `doppler` | A secret key in a project/config | +| [FOKS](/providers/foks) | `foks` | A key in a personal or team namespace | +| [Bitwarden Secrets Manager](/providers/bitwarden-sm) | `bitwarden-sm` | A secret key in a project | +| [Keeper Secrets Manager](/providers/keeper-sm) | `keeper-sm` | A record and field using Keeper notation | -Integrate with password managers and secret services you already use. +## Password managers and secret services -| Provider | Description | Best For | -| ------------------------------------- | --------------------------- | --------------------------------------- | -| [1Password](/providers/1password) | 1Password CLI integration | Teams already using 1Password | -| [Bitwarden](/providers/bitwarden) | Bitwarden/Vaultwarden | Open source preference, self-hosting | -| [Infisical](/providers/infisical) | Infisical secrets | Modern secret management, open source | -| [Proton Pass](/providers/proton-pass) | Proton Pass CLI integration | Teams using Proton Pass or agent tokens | +These providers use an existing vault or service account. Follow the individual guide for the CLI or API authentication needed on each machine. -### πŸ’» Local Storage +| Provider | Config type | Integration | +| ----------------------------------------- | --------------- | --------------------------------------------- | +| [1Password](/providers/1password) | `1password` | `op` CLI; item and field references | +| [Bitwarden](/providers/bitwarden) | `bitwarden` | `bw` CLI, or the experimental `rbw` backend | +| [Infisical](/providers/infisical) | `infisical` | Infisical CLI; project, environment, and path | +| [Passwordstate](/providers/passwordstate) | `passwordstate` | HTTP API; password ID or title and field | +| [Proton Pass](/providers/proton-pass) | `proton-pass` | `pass-cli`; vault, item, and field | -Store secrets locally on your machine. +## Local stores and defaults -| Provider | Description | Best For | -| ------------------------------------------- | ------------------------------------- | --------------------------------------- | -| [OS Keychain](/providers/keychain) | macOS/Windows/Linux credential stores | Local development, personal projects | -| [KeePass](/providers/keepass) | KeePass database files (.kdbx) | Offline use, KeePassXC users | -| [password-store](/providers/password-store) | GPG-encrypted local password store | CLI users, git-based sync, Unix systems | -| [Plain](/providers/plain) | Plaintext (default values only) | Non-sensitive defaults | +These use a store available on your machine, or no encrypted storage at all for plaintext defaults. -## Mixing Providers +| Provider | Config type | Storage | +| ------------------------------------------- | ---------------- | ------------------------------------------------------------------- | +| [OS keychain](/providers/keychain) | `keychain` | macOS Keychain, Windows Credential Manager, or Linux Secret Service | +| [KeePass](/providers/keepass) | `keepass` | A local `.kdbx` file | +| [password-store](/providers/password-store) | `password-store` | GPG-encrypted files managed by `pass` | +| [Plaintext](/providers/plain) | `plain` | Unencrypted values; only for non-sensitive configuration | -You can use multiple providers in the same project: +## Configure an instance + +This example reads an existing item from 1Password: ```toml -# Age for development -[providers] -age = { type = "age", recipients = ["age1..."] } -aws = { type = "aws-sm", region = "us-east-1" } +[providers.op] +type = "1password" +vault = "Engineering" -# Development secrets (encrypted in git) [secrets] -DATABASE_URL = { provider = "age", value = "encrypted..." } - -# Production secrets (in AWS) -[profiles.production.secrets] -DATABASE_URL = { provider = "aws", value = "database-url" } +DATABASE_URL = { provider = "op", value = "Database/url" } ``` -## Feature Comparison +Test the configured instance, then resolve the secret: -| Feature | age | AWS KMS | AWS SM | 1Password | Vault | -| -------------- | ------ | ------- | ------ | --------- | ----- | -| Offline† | βœ… | ❌ | ❌ | ❌ | ❌ | -| In Git | βœ… | βœ… | ❌ | ❌ | ❌ | -| Free | βœ… | πŸ’° | πŸ’° | πŸ’° | βœ…\* | -| Audit Logs | ❌ | βœ… | βœ… | βœ… | βœ… | -| Access Control | ❌ | βœ… | βœ… | βœ… | βœ… | -| Rotation | Manual | Manual | βœ… | Manual | βœ… | -| Team-Friendly | βœ… | βœ… | βœ… | βœ… | βœ… | - -\*Self-hosted Vault is free, HCP Vault is paid +```sh +fnox provider test op +fnox get DATABASE_URL +fnox exec -- npm start +``` -†Any remote provider becomes offline-capable by caching secrets locally with [`fnox sync`](/guide/sync) +`fnox get` prints the resolved value. `fnox list` lists the secrets configured in fnox; it is not a directory of every item in your remote vault. -## Next Steps +A provider's read and write capabilities may differ. In particular, a read-only provider cannot be used with `fnox set`, and `fnox import` currently accepts encryption providers only. Check the provider guide before choosing a write or migration workflow. -Choose a provider and get started: +## Next steps -- [Age Encryption](/providers/age) - Simple, free, works with SSH keys -- [AWS Parameter Store](/providers/aws-ps) - Simple, cost-effective AWS secret storage -- [AWS Secrets Manager](/providers/aws-sm) - For AWS production workloads with rotation -- [1Password](/providers/1password) - Leverage existing 1Password setup -- [Complete Example](/guide/real-world-example) - See providers in action +- [Configuration reference](/reference/configuration): common provider fields and secret options. +- [Profiles](/guide/profiles): select different providers for different environments. +- [Troubleshooting](/guide/troubleshooting): diagnose a provider connection or missing value. diff --git a/docs/providers/password-store.md b/docs/providers/password-store.md index 13981dcc..63e64c4b 100644 --- a/docs/providers/password-store.md +++ b/docs/providers/password-store.md @@ -1,30 +1,37 @@ -# password-store +--- +description: "Read and write password-store secrets with fnox and pass. Configure GPG access, paths, and shared recipient keys." +--- + +# Password-store Integrate with the standard Unix password manager (`pass`) to store and retrieve secrets from GPG-encrypted files. -## Quick Start +## Quick start -```bash -# 1. Install pass (password-store) +```sh +# Install pass (password-store) brew install pass # macOS # OR: sudo apt install pass # Linux -# 2. Initialize password-store (one-time setup) +# Initialize password-store (one-time setup) pass init +``` -# 3. Configure fnox provider -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] pass = { type = "password-store", prefix = "fnox/" } -EOF +``` -# 4. Store a secret in password-store +```sh +# Store a secret in password-store fnox set DATABASE_URL "postgresql://localhost/mydb" --provider pass -# 5. Retrieve from password-store +# Retrieve from password-store fnox get DATABASE_URL -# 6. Use in shell commands +# Use in shell commands fnox exec -- npm start ``` @@ -82,7 +89,7 @@ sudo pacman -S pass ## Setup -### 1. Generate GPG Key (if needed) +### 1. Generate GPG key (if needed) If you don't have a GPG key: @@ -111,17 +118,20 @@ pass init user@example.com This creates `~/.password-store/` directory. -### 3. (Optional) Configure Custom Store Directory +### 3. (Optional) configure custom store directory -```bash +```sh # Set custom store location export PASSWORD_STORE_DIR=/path/to/custom/store # Or configure in fnox -cat >> fnox.toml << 'EOF' +``` + +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers] pass = { type = "password-store", store_dir = "/path/to/custom/store" } -EOF ``` ## Configuration @@ -133,7 +143,7 @@ Add password-store provider to `fnox.toml`: pass = { type = "password-store", prefix = "fnox/" } ``` -### Configuration Options +### Configuration options ```toml [providers.pass] @@ -145,7 +155,7 @@ gpg_opts = "--no-throw-keyids" # Optional: extra GPG options, passed as PASSWOR Relative `store_dir` paths are resolved from the config file that declares the provider. Paths beginning with `~` expand to your home directory, and absolute paths are used unchanged. -## How It Works +## How it works 1. **Storage:** Secrets are stored as GPG-encrypted files in `~/.password-store/` (or custom location) 2. **Config:** `fnox.toml` contains only the secret path/reference (not the actual value) @@ -156,7 +166,7 @@ Relative `store_dir` paths are resolved from the config file that declares the p ## Usage -### Store a Secret +### Store a secret ```bash # Simple secret @@ -176,19 +186,19 @@ DATABASE_URL = { provider = "pass", value = "DATABASE_URL" } # Stored at fnox/D DB_PASSWORD = { provider = "pass", value = "database/production" } # Stored at fnox/database/production ``` -### Retrieve a Secret +### Retrieve a secret ```bash fnox get DATABASE_URL ``` -### Run Commands with Secrets +### Run commands with secrets ```bash fnox exec -- npm run dev ``` -### List Secrets in password-store +### List secrets in password-store ```bash # View password-store structure @@ -198,7 +208,7 @@ pass pass ls fnox/ ``` -## Reference Formats +## Reference formats ```toml [secrets] @@ -215,7 +225,7 @@ API_TOKEN = { provider = "pass", value = "tokens/github" } # β†’ Stored at: tokens/github.gpg (no prefix) ``` -## Selecting a Line +## Selecting a line A common `pass` convention is to pack related values into a single entry: the password on line 1 and other fields (username, URL, etc.) on the lines @@ -245,7 +255,7 @@ below). Without `line`, fnox returns the entire entry unchanged. selector β€” see the warning under [Multiline Secrets](#multiline-secrets) for how to edit one line of an existing entry. -## Git Integration +## Git integration password-store has built-in git support: @@ -267,22 +277,9 @@ fnox set API_KEY "new-key" --provider pass # Auto-commits! pass git push ``` -## Team Workflow - -### Option 1: Shared GPG Key - -Share a single GPG key with the team (less secure, simpler): +## Team workflow -```bash -# Export GPG key -gpg --export-secret-keys > team-key.gpg - -# Team members import -gpg --import team-key.gpg -pass init -``` - -### Option 2: Multiple Recipients (Recommended) +### Encrypt for each teammate Encrypt for multiple team members (more secure): @@ -309,7 +306,7 @@ git clone https://github.com/team/password-store.git ~/.password-store pass # Verify they can decrypt ``` -## Multi-Environment Example +## Multi-environment example ```toml # Development (password-store) @@ -327,7 +324,7 @@ aws = { type = "aws-sm", region = "us-east-1" } DATABASE_URL = { provider = "aws", value = "database-url" } ``` -## Bootstrap Pattern +## Bootstrap pattern Store provider tokens in password-store: @@ -350,13 +347,13 @@ export AWS_SECRET_ACCESS_KEY=$(fnox get AWS_SECRET_ACCESS_KEY) fnox exec -- ./deploy.sh # Now can access AWS secrets ``` -## Multiline Secrets +## Multiline secrets password-store fully supports multiline secrets: ```bash # Store multiline secret -fnox set SSH_PRIVATE_KEY "$(cat ~/.ssh/id_rsa)" --provider pass +fnox set SSH_PRIVATE_KEY --from-file ~/.ssh/id_rsa --provider pass # Or using heredoc with pass directly pass insert -m work/ssh-key < ``` -### "gpg: decryption failed: No secret key" +### "GPG: decryption failed: No secret key" Your GPG private key is not available: @@ -453,7 +434,7 @@ gpg --list-secret-keys gpg --import private-key.gpg ``` -### "gpg: public key decryption failed: Inappropriate ioctl for device" +### "GPG: public key decryption failed: Inappropriate ioctl for device" Set GPG TTY: @@ -489,17 +470,7 @@ git status # Should show a git repo pass git init ``` -## Best Practices - -1. **Use git integration** - Track changes and sync across machines -2. **Organize with prefixes** - Use nested paths like `work/`, `personal/` -3. **Back up GPG keys** - Export and store securely offline -4. **Team: Use multiple recipients** - More secure than sharing keys -5. **Sync via git** - Private repository for encrypted password store -6. **Set GPG TTY** - Add `export GPG_TTY=$(tty)` to shell profile -7. **Use fnox prefix** - Isolate fnox secrets from other pass entries - -## Security Considerations +## Security considerations - **Encryption:** GPG encrypts files with your public key - **Access control:** Filesystem permissions + GPG key passphrase @@ -507,7 +478,7 @@ pass git init - **Key security:** Protect your GPG private key - **Passphrase:** Use a strong GPG key passphrase -## Third-Party Tools +## Third-party tools password-store has a rich ecosystem: @@ -517,7 +488,7 @@ password-store has a rich ecosystem: - **[browserpass](https://github.com/browserpass/browserpass-extension)** - Browser extension - **[gopass](https://github.com/gopasspw/gopass)** - Go implementation with extra features -## Next Steps +## Next steps - [Age Encryption](/providers/age) - Modern alternative to GPG - [OS Keychain](/providers/keychain) - OS-native storage diff --git a/docs/providers/passwordstate.md b/docs/providers/passwordstate.md new file mode 100644 index 00000000..4a5c8e54 --- /dev/null +++ b/docs/providers/passwordstate.md @@ -0,0 +1,73 @@ +--- +description: "Read Passwordstate passwords and fields by ID or title with fnox, an API key, and a password list." +--- + +# Passwordstate + +Read passwords from a Passwordstate server through its HTTP API. This provider is read-only; create and update records in Passwordstate, then commit their references in `fnox.toml`. + +## Prerequisites + +- A reachable Passwordstate server. +- An API key with access to the required password list and records. +- The password list ID and either a password ID or record title. + +No Passwordstate CLI is required. + +## Configuration + +```toml +[providers.passwordstate] +type = "passwordstate" +base_url = "https://passwordstate.example.com" +password_list_id = "123" + +[secrets] +DB_PASSWORD = { provider = "passwordstate", value = "456" } +DB_USER = { provider = "passwordstate", value = "456/username" } +``` + +Replace the URL and IDs with values from your server. Supply the API key through `FNOX_PASSWORDSTATE_API_KEY` or `PASSWORDSTATE_API_KEY`; the `FNOX_` form takes precedence. + +| Field | Required | Description | +| ------------------ | -------- | ------------------------------------------------------ | +| `base_url` | Yes | Passwordstate server URL, without `/api` | +| `password_list_id` | Yes | List used for title searches and connection testing | +| `api_key` | No | API key or secret reference; overrides the environment | +| `verify_ssl` | No | TLS certificate verification; defaults to `"true"` | + +Keep certificate verification enabled and configure a trusted certificate on the server. Do not commit a plaintext API key. + +## Reference formats + +| Reference | Result | +| -------------- | ---------------------------------------------------- | +| `456` | Password field of the record with this ID | +| `456/username` | Named field of the record with this ID | +| `Database` | Password field found by title in the configured list | +| `Database/url` | Named field found by title in the configured list | + +Supported fields are `password`, `username` (or `user`), `title`, `url`, `description`, and `notes`. Field names are case-insensitive. References accept at most one `/` separator; use IDs when a title cannot be expressed unambiguously. + +## Verify and run + +```sh +fnox provider test passwordstate +fnox check --all +fnox exec -- npm start +``` + +`fnox get DB_PASSWORD` prints the resolved value when you need to inspect it directly. + +## Troubleshooting + +- **Authentication failure:** confirm the API key is valid and has access to the list and record. +- **Record not found:** verify the ID, or check the exact title in the configured list. +- **Field missing:** use one of the supported fields and confirm it has a value. +- **Connection failure:** verify the base URL, TLS certificate, and network access from the machine running fnox. + +## Next steps + +- [Sync a local cache](/guide/sync): encrypt a personal snapshot for local reads. +- [Profiles](/guide/profiles): use different lists or servers per environment. +- [Configuration reference](/reference/configuration): common provider and secret options. diff --git a/docs/providers/plain.md b/docs/providers/plain.md index 9c0720e7..cb554305 100644 --- a/docs/providers/plain.md +++ b/docs/providers/plain.md @@ -1,173 +1,61 @@ -# Plain Text +--- +description: "Use plaintext defaults for non-sensitive settings and understand what fnox writes when no provider is selected." +--- -Store secrets as plain text (for default values only!). +# Plaintext defaults -## Usage +Use plaintext for non-sensitive settings such as log levels, ports, and public URLs. Plaintext values are readable by anyone with access to the config file. -Plain text is the default when no provider is specified: +## Default values -```toml -[secrets] -NODE_ENV = { default = "development" } # ← Plain text, safe for non-sensitive defaults -LOG_LEVEL = { default = "info" } # ← Plain text -API_TIMEOUT = { default = "30" } # ← Plain text -``` - -There is also an explicit `plain` provider (`type = "plain"`) that returns `value` verbatim, with no encryption. It is useful for tests and non-sensitive values that should still go through a provider: - -```toml -[providers] -plain = { type = "plain" } - -[secrets] -LOG_LEVEL = { provider = "plain", value = "info" } -``` - -## When Plain Text is Appropriate - -### 1. Non-Sensitive Defaults +A secret can provide a default without a provider: ```toml [secrets] -PORT = { default = "3000" } -HOST = { default = "localhost" } -NODE_ENV = { default = "development" } LOG_LEVEL = { default = "info" } +PORT = { default = "3000" } +API_URL = { default = "http://localhost:3000" } ``` -### 2. Public Configuration - -```toml -[secrets] -PUBLIC_API_URL = { default = "https://api.example.com" } -CDN_URL = { default = "https://cdn.example.com" } -``` - -### 3. Development Fallbacks - -```toml -[secrets] -DATABASE_URL = { provider = "age", value = "encrypted-production-db...", default = "postgresql://localhost/dev_db" } # ← Fallback for local dev -``` - -If the encrypted value can't be decrypted (e.g., missing key), falls back to the plaintext default. +`fnox set` also writes a plaintext default when neither the secret nor the configuration selects a provider. To encrypt a value, configure an [encryption provider](/providers/overview#encryption-in-your-config) first. -## ❌ When NOT to Use Plain Text +## Explicit plain provider -### Never for Passwords +The `plain` provider returns `value` unchanged: ```toml -# ❌ BAD - Never do this! -[secrets] -DATABASE_PASSWORD = { default = "super-secret-password" } +[providers.plain] +type = "plain" -# βœ… GOOD - Use encryption [secrets] -DATABASE_PASSWORD = { provider = "age", value = "encrypted..." } +LOG_LEVEL = { provider = "plain", value = "debug" } ``` -### Never for API Keys +Use this when a test or configuration needs a provider-backed value without encryption. A `default` is usually simpler for public settings. -```toml -# ❌ BAD -[secrets] -STRIPE_KEY = { default = "sk_live_abc123xyz789" } +## Fallbacks -# βœ… GOOD -[secrets] -STRIPE_KEY = { provider = "age", value = "encrypted..." } -``` - -### Never for Tokens - -```toml -# ❌ BAD -[secrets] -JWT_SECRET = { default = "my-secret-key" } - -# βœ… GOOD -[secrets] -JWT_SECRET = { provider = "age", value = "encrypted..." } -``` - -## Mixing Plain and Encrypted - -It's common to mix plain text defaults with encrypted values: +A remote or encrypted secret can have a non-sensitive fallback: ```toml -[providers] -age = { type = "age", recipients = ["age1..."] } +[providers.aws] +type = "aws-sm" +region = "us-east-1" [secrets] -DATABASE_PASSWORD = { provider = "age", value = "encrypted...", default = "dev-password" } # Encrypted sensitive values, fallback for local dev -DATABASE_HOST = { default = "localhost" } # Plain text non-sensitive defaults -DATABASE_PORT = { default = "5432" } -LOG_LEVEL = { default = "info" } +REDIS_URL = { provider = "aws", value = "redis-url", default = "redis://localhost:6379" } ``` -## Security Best Practices - -1. **Never commit sensitive data as plain text** -2. **Use encryption for anything that shouldn't be public** -3. **Use plain text only for truly non-sensitive defaults** -4. **Review `.gitignore`** - Ensure sensitive files aren't tracked -5. **Use `fnox scan` to detect secrets** - Scans for accidentally committed secrets - -## Scan for Secrets - -fnox can scan your codebase for accidentally committed secrets: - -```bash -# Scan for potential secrets -fnox scan +If the provider lookup fails, fnox can use the default. The default is plaintext even when the main value uses encryption. Avoid production fallbacks that would silently connect to the wrong service; use a separate [profile](/guide/profiles) when appropriate. -# Scan specific directory -fnox scan src/ -``` - -## Examples - -### Safe Plain Text Usage - -```toml -# Application settings (non-sensitive) -[secrets] -APP_NAME = { default = "My Application" } -APP_VERSION = { default = "1.0.0" } -ENVIRONMENT = { default = "development" } -DEBUG_MODE = { default = "true" } -TIMEOUT_MS = { default = "5000" } -PUBLIC_SITE_URL = { default = "https://example.com" } # Public URLs -DOCS_URL = { default = "https://docs.example.com" } -``` - -### Mixed Usage (Plain + Encrypted) - -```toml -[providers] -age = { type = "age", recipients = ["age1..."] } - -[secrets] -# Sensitive (encrypted) -DATABASE_URL = { provider = "age", value = "encrypted-connection-string..." } -API_KEY = { provider = "age", value = "encrypted-key..." } - -# Non-sensitive (plain) -DATABASE_POOL_SIZE = { default = "10" } -CACHE_TTL_SECONDS = { default = "3600" } -FEATURE_FLAG_NEW_UI = { default = "false" } -``` +## Review before committing -## Remember +Check both `value` and `default` fields. Setting `provider = "age"` does not encrypt a string you manually type into `value`; let `fnox set` generate the ciphertext. -- βœ… Plain text is fine for public, non-sensitive configuration -- βœ… Use defaults for fallback values -- ❌ Never use plain text for passwords, keys, or tokens -- βœ… Use [age](/providers/age) or other providers for sensitive data -- βœ… Run `fnox scan` to catch accidental secrets +[`fnox scan`](/cli/scan) can flag potential secrets in files. It is a heuristic scan, not a guarantee that a repository or its history is free of secrets. If a real credential was exposed, rotate it at its source. -## Next Steps +## Next steps -- [Age Encryption](/providers/age) - Encrypt sensitive secrets -- [Providers Overview](/providers/overview) - Choose the right provider -- [Configuration Reference](/reference/configuration) - Learn more about fnox.toml +- [Age quick start](/guide/quick-start): store sensitive values with encryption. +- [Missing secrets and defaults](/guide/missing-secrets): choose when to fall back, warn, or fail. +- [Configuration reference](/reference/configuration#default): interpolate defaults and set provider references. diff --git a/docs/providers/proton-pass.md b/docs/providers/proton-pass.md index 9a43ed0e..37761987 100644 --- a/docs/providers/proton-pass.md +++ b/docs/providers/proton-pass.md @@ -1,27 +1,34 @@ +--- +description: "Read Proton Pass vault items with fnox and pass-cli using vault aliases, item names, or pass:// references." +--- + # Proton Pass Integrate with Proton Pass through the Proton Pass CLI (`pass-cli`) to retrieve secrets from vault items. -## Quick Start +## Quick start -```bash -# 1. Install Proton Pass CLI +```sh +# Install Proton Pass CLI # See https://proton.me/pass/download -# 2. Log in once with the default browser-based flow +# Log in once with the default browser-based flow pass-cli login +``` -# 3. Configure fnox -cat >> fnox.toml << 'EOF' +Add these definitions to `fnox.toml`. Merge them into any existing tables with the same names: + +```toml [providers.protonpass] type = "proton-pass" vault = "Personal" [secrets] DATABASE_PASSWORD = { provider = "protonpass", value = "Database/password" } -EOF +``` -# 4. Retrieve the secret +```sh +# Retrieve the secret fnox get DATABASE_PASSWORD ``` @@ -53,7 +60,7 @@ Item-only and `id:` references default to the `password` field and require `vaul Use full `pass://vault/item/field` references when vault or item names contain `/`. -## Personal Access Tokens +## Personal access tokens For CI or headless use, create a Proton Pass personal access token, then log in with `pass-cli`. The official CLI supports either an environment variable or a login flag: @@ -69,7 +76,7 @@ pass-cli login --personal-access-token "pst_token::key" Run `pass-cli info` after login to verify the session. `fnox` also accepts `FNOX_PROTON_PASS_PERSONAL_ACCESS_TOKEN` and passes it to `pass-cli` as `PROTON_PASS_PERSONAL_ACCESS_TOKEN`. -## Agent Tokens +## Agent tokens Proton Pass agent tokens are personal access tokens with dedicated access logging. Current `pass-cli` releases require `PROTON_PASS_AGENT_REASON` for audited agent operations, including item reads performed by `fnox`. @@ -89,7 +96,7 @@ agent_reason = "fnox secret retrieval" Environment values take priority over provider config. Provider `agent_reason` values are trimmed, must be non-empty, and must be at most 300 characters to match the `pass-cli` agent reason limit. -## Session and Key Storage +## Session and key storage `fnox` passes through these Proton Pass CLI environment variables, with `FNOX_` aliases available for project-local setup: @@ -119,3 +126,12 @@ Not supported: - `fnox set` to create or update Proton Pass items - Remote item listing/import - Item delete/archive/update flows + +## Troubleshooting + +Run `pass-cli info` to check the session, then `fnox provider test ` for the configured instance. Confirm the vault and field in the reference, and supply an agent reason when the token requires one. + +## Next steps + +- [Profiles](/guide/profiles): separate vault configuration by environment. +- [Sync a local cache](/guide/sync): use encrypted local copies for repeated reads. diff --git a/docs/providers/vault.md b/docs/providers/vault.md index 9a054e74..2855741a 100644 --- a/docs/providers/vault.md +++ b/docs/providers/vault.md @@ -1,3 +1,7 @@ +--- +description: "Read HashiCorp Vault KV secrets with fnox. Configure the address, authentication, mount, paths, and field selection." +--- + # HashiCorp Vault HashiCorp Vault provides advanced secret management with dynamic secrets, leasing, and fine-grained access control. @@ -33,7 +37,7 @@ vault = { type = "vault", path = "secret/myapp" } # address and token are option - **namespace**: (Optional) Vault namespace. Falls back to `FNOX_VAULT_NAMESPACE` or `VAULT_NAMESPACE`. - **credential_command**: (Optional) Shell command that prints a Vault token to stdout when no token is configured. The command is rendered as a Tera template and receives `address`, `path`, and `namespace`. -### Provider-scoped Login +### Provider-scoped login Use `credential_command` when different Vault/OpenBao providers need different tokens: @@ -50,7 +54,7 @@ fnox sets `VAULT_ADDR` and `VAULT_NAMESPACE` for the command from the provider c ## Setup -### 1. Configure Vault Access +### 1. Configure Vault access ```bash # Set Vault address @@ -63,7 +67,7 @@ vault login -method=userpass username=myuser export VAULT_TOKEN="hvs.CAESIJ..." ``` -### 2. Create Policy +### 2. Create policy ```hcl # policy.hcl @@ -80,7 +84,7 @@ path "secret/metadata/myapp/*" { vault policy write fnox-policy policy.hcl ``` -### 3. Store Secrets in Vault +### 3. Store secrets in Vault ```bash # KV v2 engine @@ -111,21 +115,11 @@ fnox get DATABASE_URL fnox exec -- ./app ``` -## Pros - -- βœ… Advanced features (dynamic secrets, leasing) -- βœ… Fine-grained access policies -- βœ… Audit logging -- βœ… Multi-cloud support -- βœ… Self-hosted option - -## Cons +## Usage notes -- ❌ Complex to set up and operate -- ❌ Requires Vault infrastructure -- ❌ Token management +The provider reads KV secrets. Use the separate [Vault lease backend](/leases/vault) for dynamic credentials. The provider token must have access to the configured KV path and namespace. -## Next Steps +## Next steps - [Vault Documentation](https://developer.hashicorp.com/vault/docs) - [AWS Secrets Manager](/providers/aws-sm) - Simpler cloud alternative diff --git a/docs/providers/yubikey.md b/docs/providers/yubikey.md index efae0cc5..a1a4adff 100644 --- a/docs/providers/yubikey.md +++ b/docs/providers/yubikey.md @@ -1,10 +1,14 @@ +--- +description: "Encrypt secrets with a YubiKey HMAC challenge-response slot. Configure touch requirements and plan for recovery." +--- + # YubiKey The `yubikey` provider uses YubiKey HMAC-SHA1 challenge-response to derive an AES-256-GCM encryption key. Secrets are encrypted symmetrically β€” decryption requires the same physical YubiKey. -## Why? +## When to use it -Regular age encryption protects secrets at rest, but anyone with access to the key file can decrypt them. The `yubikey` provider ties encryption to a physical hardware device. No YubiKey = no decryption. +Regular age encryption protects secrets at rest, but anyone with access to the key file can decrypt them. The `yubikey` provider ties encryption to a physical hardware device. The derived symmetric key is present in process memory during use; the hardware protects access at rest. The config is fully portable: move your fnox config to any machine, plug in the same YubiKey, and it works. @@ -46,7 +50,7 @@ fnox get MY_SECRET Within a single `fnox exec` invocation, the YubiKey is only tapped once. The HMAC response is cached in memory for the duration of the process. -## With Credential Leases +## With credential leases The `yubikey` provider works well with [credential leases](/guide/leases) and the `env = false` secret option. Store master credentials encrypted with the YubiKey, and have lease backends use them to create short-lived credentials: @@ -68,7 +72,7 @@ region = "us-east-1" With `env = false`, the master credentials are never injected into subprocess environment variables. They are only used internally by the lease backend to call `sts:AssumeRole`, and only the resulting short-lived credentials are injected. -## How It Works +## How it works 1. **Setup:** A random 32-byte challenge is generated and stored in config 2. **HMAC-SHA1:** The challenge is sent to the YubiKey, which returns a 20-byte HMAC response @@ -77,13 +81,18 @@ With `env = false`, the master credentials are never injected into subprocess en The HMAC response is never stored on disk. It exists only in process memory after a YubiKey tap. -## Important Notes +## Important notes ::: warning Renaming providers invalidates cached credentials -The provider name is used in key derivation (HKDF context). Renaming a provider (e.g., from `secure` to `my_yubikey`) will change the derived encryption key, making all previously encrypted secrets and cached lease credentials undecryptable. If you need to rename, re-encrypt all secrets after renaming. +The provider name is used in key derivation (HKDF context). Renaming a provider (e.g., from `secure` to `my_yubikey`) will change the derived encryption key, making all previously encrypted secrets and cached lease credentials undecryptable. To migrate, keep the old provider available, create a new provider under the new name, and read values through the old provider before storing them with the new one. Verify the new values before removing the old configuration. Renaming first prevents decryption. ::: ## Requirements - A YubiKey with HMAC-SHA1 challenge-response configured on slot 1 or 2 - Configure HMAC-SHA1 using the [YubiKey Manager](https://www.yubico.com/support/download/yubikey-manager/) or the `ykman otp chalresp` command + +## Next steps + +- [Sync a local cache](/guide/sync): use the hardware provider as an encryption target. +- [Credential leases](/guide/leases): protect credentials used to create temporary access. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index d08794f8..016de11d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1,16 +1,20 @@ -# Configuration Reference +--- +description: "Reference for fnox.toml settings, providers, secrets, profiles, imports, local overrides, caches, proxies, and leases." +--- -Complete reference for the `fnox.toml` configuration file. +# Configuration reference -## JSON Schema +Use this reference for configuration loading, top-level settings, providers, secrets, and profiles. Start with the [quick start](/guide/quick-start) for a working file, or use the [provider catalog](/providers/overview) for provider-specific fields. + +## JSON schema A JSON Schema is available for IDE autocompletion and validation: -``` +```text https://fnox.jdx.dev/schema.json ``` -### Editor Setup +### Editor setup **VS Code** with [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml): @@ -23,18 +27,19 @@ age = { type = "age", recipients = ["age1..."] } **JetBrains IDEs**: Add the schema URL in Settings > Languages & Frameworks > Schemas and DTDs > JSON Schema Mappings. -## File Location +## File location + +The global config is the base layer. fnox then loads discovered project directories from outermost to innermost. At **each directory**, it applies: + +1. `fnox.toml` (or `.fnox.toml`). +2. `fnox..toml` for each active non-default profile, in selection order. +3. `fnox.local.toml` (or `.fnox.local.toml`). -fnox looks for configuration files in this order (lowest to highest priority): +A closer directory overrides an outer directory, including its local overrides. Provider and secret definitions with the same name are replaced as a unit; they are not merged field by field. The global directory follows [`FNOX_CONFIG_DIR`](/reference/environment#fnox-config-dir). -1. **Global config**: `~/.config/fnox/config.toml` (or `$FNOX_CONFIG_DIR/config.toml`) -2. `fnox.toml` in parent directories (hierarchical search) -3. `fnox.toml` in current directory -4. `fnox.$FNOX_PROFILE.toml` alongside each `fnox.toml` (profile-specific) -5. `fnox.local.toml` alongside each `fnox.toml` (for local overrides) -6. Path specified via `-c, --config` flag β€” for non-default filenames this replaces steps 2-5 instead of stacking on top of them, see [Explicit Config Paths](#explicit-config-paths) +An explicit path uses the separate behavior below. To see the actual stack for a command, run `fnox config-files`. -### Explicit Config Paths +### Explicit config paths Passing `-c, --config` with anything other than the bare default filename turns off the hierarchical search: fnox loads that one file plus any files it @@ -52,7 +57,7 @@ FNOX_CONFIG_DIR=/nonexistent fnox -c ./ci.toml get MY_SECRET Use `fnox config-files` to see exactly which files a given directory and set of flags will load. -### Global Configuration +### Global configuration The global config file stores machine-wide secrets and providers that apply to all projects: @@ -60,8 +65,8 @@ The global config file stores machine-wide secrets and providers that apply to a # Initialize global config fnox init --global -# Add secrets to global config -fnox set MY_TOKEN "secret-value" --global +# After configuring an age provider, add a secret with hidden input +fnox set MY_TOKEN --global --provider age # Add providers to global config (the `aws` type is AWS Secrets Manager) fnox provider add aws aws --global @@ -75,7 +80,7 @@ fnox provider add aws aws --global - Machine-specific credentials - Default providers available everywhere -## Basic Structure +## Basic structure ```toml # Top-level settings @@ -92,13 +97,47 @@ SECRET_NAME = { provider = "PROVIDER_NAME", value = "...", default = "...", if_m # Profile definitions [profiles.PROFILE_NAME] -# ... same structure as top-level ... +# Profiles support providers, secrets, leases, default_provider, and inherits ``` Secret names are environment variable names and must match `^[A-Za-z_][A-Za-z0-9_]*$`. -## Top-Level Settings +## Top-level settings + +### `default_provider` + +Provider instance to use when a secret does not select one explicitly. It also supplies the default target for commands such as `fnox set` and `fnox sync`. + +```toml +default_provider = "age" + +[providers.age] +type = "age" +recipients = ["age1..."] # Replace with your public recipient +``` + +If no provider is selected, `fnox set` writes a plaintext default. Configure a provider before storing sensitive values. + +### `root` + +Stop the parent-directory search at this file. The global config still loads. + +```toml +root = true +``` + +### `prompt_auth` + +Allow fnox to offer a provider authentication command in a terminal. Defaults to `true`. `FNOX_PROMPT_AUTH` overrides this setting; `--non-interactive` also disables interactive authentication flows. + +```toml +prompt_auth = false +``` + +### `age_key_file` + +Legacy top-level path to an age identity file. Prefer `key_file` on the individual age provider so identity selection stays with its provider configuration. See [age identity selection](/providers/age#set-decryption-key). ### `if_missing` @@ -128,7 +167,7 @@ env = "exec" # or true, false - `true` - Inject into the shell (via shell integration / `fnox export`) and `fnox exec` subprocesses (default) - `"exec"` - Only inject into `fnox exec` subprocesses; never the interactive shell -- `false` - Never inject; secrets are only accessible via `fnox get` +- `false` - Exclude from normal environment injection; explicit reads and internal provider or lease dependencies can still resolve the secret Setting `env = "exec"` at the top level keeps every secret out of the interactive shell by default β€” useful when AI coding agents or other tools run in your shell and would otherwise inherit all injected secrets. Applications still receive secrets when launched through `fnox exec -- `, and individual secrets can opt back in with `env = true`: @@ -156,7 +195,7 @@ import = ["./shared/base.toml", "./envs/dev.toml"] - Imported files merged into current config - Later imports override earlier ones -### Path Values +### Path values Paths declared in config files are resolved relative to the config file that declares them. This applies to imports and provider filesystem paths such as `age.key_file`, `keepass.database`, `keepass.keyfile`, `password-store.store_dir`, and `foks.home`. @@ -229,7 +268,36 @@ it intends to allow; fields and rules are not inherited from an earlier See [Credential Proxy](/guide/proxy). -## Provider Configuration +## MCP server settings + +```toml +[mcp] +tools = ["exec"] +secrets = ["DATABASE_URL"] +redact_output = true +``` + +- `tools`: exposed tools; defaults to `["get_secret", "exec"]`. +- `secrets`: optional allowlist of secret names; omitted means all active-profile secrets are available. +- `redact_output`: redact literal resolved values in subprocess output; defaults to `true`. + +Output redaction is not a sandbox. An agent-controlled command can transform or transmit credentials. See [MCP access and limits](/guide/mcp). + +## Lease backend settings + +Define backends under `[leases.]` or `[profiles..leases.]`. Each backend has a `type` and its own fields. `duration` requests a lifetime; the service controls the actual expiry. + +```toml +[leases.aws] +type = "aws-sts" +region = "us-east-1" +role_arn = "arn:aws:iam::123456789012:role/dev-role" +duration = "1h" +``` + +See [credential leases](/guide/leases) for authentication, caching, revocation, and all backend types. + +## Provider configuration ```toml [providers.PROVIDER_NAME] @@ -266,94 +334,13 @@ vault = "Engineering" daemon_cache = false ``` -### Common Provider Types - -#### Age Encryption - -```toml -[providers.age] -type = "age" -recipients = [ - "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p", - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQs..." -] -``` - -#### AWS Secrets Manager - -```toml -[providers] -aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp/" } # prefix is optional -``` - -#### AWS KMS +### Provider-specific fields -```toml -[providers] -kms = { type = "aws-kms", key_id = "arn:aws:kms:us-east-1:123456789012:key/...", region = "us-east-1" } -``` - -#### Azure Key Vault Secrets +The [provider catalog](/providers/overview) links to authentication, configuration, and reference formats for every supported provider. Fields such as `region`, `vault`, `prefix`, and `key_file` apply only to the types that document them. -```toml -[providers] -azure = { type = "azure-sm", vault_url = "https://myapp-vault.vault.azure.net/", prefix = "myapp/" } # prefix is optional -``` +Many provider fields accept `{ secret = "NAME" }` in place of a literal value. See [secret references in provider config](/guide/profiles#secret-references-in-provider-config). Avoid a dependency cycle in which a provider needs a secret stored in itself. -#### Azure Key Vault Keys - -```toml -[providers] -azurekms = { type = "azure-kms", vault_url = "https://myapp-vault.vault.azure.net/", key_name = "encryption-key" } -``` - -#### GCP Secret Manager - -```toml -[providers] -gcp = { type = "gcp-sm", project = "my-project-id", prefix = "myapp/" } # prefix is optional -``` - -#### GCP Cloud KMS - -```toml -[providers.gcpkms] -type = "gcp-kms" -project = "my-project-id" -location = "us-central1" -keyring = "fnox-keyring" -key = "fnox-key" -``` - -#### 1Password - -```toml -[providers] -onepass = { type = "1password", vault = "Development", account = "my.1password.com" } # account is optional -``` - -#### Bitwarden - -```toml -[providers] -bitwarden = { type = "bitwarden", collection = "collection-id", organization_id = "org-id" } # both optional -``` - -#### HashiCorp Vault - -```toml -[providers] -vault = { type = "vault", address = "https://vault.example.com:8200", path = "secret/myapp", token = "hvs.CAESIJ..." } # token optional, can use VAULT_TOKEN env var -``` - -#### OS Keychain - -```toml -[providers] -keychain = { type = "keychain", service = "fnox", prefix = "myapp/" } # prefix is optional -``` - -## Secret Configuration +## Secret configuration ```toml [secrets] @@ -364,14 +351,14 @@ SECRET_NAME = { provider = "PROVIDER_NAME", value = "...", default = "...", if_m #### `provider` -Provider to use for this secret. +Provider instance to use for this secret. An explicit value overrides `default_provider`. ```toml [secrets] DATABASE_URL = { provider = "age", value = "encrypted..." } ``` -**Required:** Unless using only `default` (plain text). +**Optional:** A secret can use `default_provider`, a plaintext `default`, or an existing environment variable. An empty definition such as `API_KEY = {}` can require a value supplied by the environment when no default provider applies. #### `value` @@ -386,7 +373,7 @@ Provider-specific value: DATABASE_URL = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdC..." } # Remote reference (AWS) -DATABASE_URL = { provider = "aws", value = "database-url" } # Secret name in AWS Secrets Manager +REMOTE_DATABASE_URL = { provider = "aws", value = "database-url" } # Secret name in AWS Secrets Manager ``` #### `daemon_cache` @@ -453,7 +440,7 @@ Where the secret is injected as an environment variable. [secrets] GITHUB_TOKEN = { provider = "age", value = "..." } # true (default): shell + fnox exec DATABASE_URL = { provider = "age", value = "...", env = "exec" } # only fnox exec subprocesses -SIGNING_KEY = { provider = "age", value = "...", env = false } # never injected; fnox get only +SIGNING_KEY = { provider = "age", value = "...", env = false } # not normally injected; explicit reads still work ``` **Values:** @@ -503,6 +490,19 @@ Extract the Nth line (1-indexed) from a multi-line secret value. Useful for prov USERNAME = { provider = "pass", value = "master", line = 2 } ``` +#### `sync` + +An encrypted cache generated by `fnox sync`. Resolution uses this cache before contacting the original provider. Keep personal caches in an ignored local file and let sync maintain the fields: + +```toml +[secrets.DATABASE_URL] +provider = "op" +value = "Database/url" +sync = { provider = "sync-age", value = "encrypted-cache..." } +``` + +The ciphertext above is abbreviated. See [sync a local cache](/guide/sync) for setup and refresh behavior. + #### `description` Human-readable description. @@ -512,7 +512,7 @@ Human-readable description. DATABASE_URL = { provider = "age", value = "encrypted...", description = "Production database connection string" } ``` -## Profile Configuration +## Profile configuration Profiles allow environment-specific configuration: @@ -531,11 +531,13 @@ aws = { type = "aws-sm", region = "us-east-1" } DATABASE_URL = { provider = "aws", value = "database-url" } ``` -### Profile Structure +### Profile structure + +Profiles support `inherits`, `providers`, `secrets`, `leases`, and `default_provider`. Settings such as `if_missing`, `env`, `daemon`, `mcp`, and `proxy` belong at the top level or, where supported, on individual secrets. Unknown profile fields are rejected. ```toml [profiles.PROFILE_NAME] -if_missing = "error" # Profile-specific default +default_provider = "PROVIDER_NAME" [profiles.PROFILE_NAME.providers] PROVIDER_NAME = { type = "PROVIDER_TYPE" } # ... provider config ... @@ -544,7 +546,7 @@ PROVIDER_NAME = { type = "PROVIDER_TYPE" } # ... provider config ... SECRET_NAME = { provider = "PROVIDER_NAME", value = "..." } # ... secret config ... ``` -### Profile Inheritance +### Profile inheritance Profiles inherit top-level secrets and providers: @@ -566,7 +568,7 @@ You can disable this merge behavior at runtime: fnox exec --profile production --no-defaults -- ./deploy.sh ``` -With `--no-defaults`, only `[profiles..secrets]` are used for the selected profile. +With `--no-defaults`, top-level secrets are excluded for a named profile. Secrets from selected profiles and their inherited profiles still apply, and top-level providers remain available. Profiles can selectively inherit other named profiles as an ordered overlay: @@ -583,7 +585,7 @@ Later inherited profiles override earlier ones, and declarations directly on the selected profile override all of them. Inheritance includes secrets, providers, lease backends, and `default_provider`. -## Complete Example +## Complete example ```toml # Global settings @@ -603,18 +605,17 @@ LOG_LEVEL = { default = "info" } # Production profile [profiles.production] -if_missing = "error" [profiles.production.providers] aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp-prod/" } [profiles.production.secrets] -DATABASE_URL = { provider = "aws", value = "database-url", description = "Production database" } -JWT_SECRET = { provider = "aws", value = "jwt-secret" } +DATABASE_URL = { provider = "aws", value = "database-url", description = "Production database", if_missing = "error" } +JWT_SECRET = { provider = "aws", value = "jwt-secret", if_missing = "error" } # Inherits LOG_LEVEL from top-level ``` -## Local Overrides +## Local overrides Create `fnox.local.toml` alongside `fnox.toml` for local overrides: @@ -628,11 +629,11 @@ DEBUG_MODE = { default = "true" } **Important:** Add to `.gitignore`: -```gitignore +```text fnox.local.toml ``` -## Profile-Specific Config Files +## Profile-specific config files You can create environment-specific config files that load based on the active profile(s). When multiple profiles are active, each profile's config file is @@ -672,11 +673,11 @@ FNOX_PROFILE=aws,prod fnox exec -- ./app - `fnox.default.toml` is **not loaded** (use `fnox.toml` instead) - With multiple active profiles, config files are loaded in profile order (later profiles override earlier) -## Hierarchical Configuration +## Hierarchical configuration fnox searches parent directories for `fnox.toml` files: -``` +```text project/ β”œβ”€β”€ fnox.toml # Root config └── services/ @@ -696,7 +697,7 @@ Merge order (lowest to highest priority): **Note**: Setting `root = true` in a `fnox.toml` stops the parent-directory search at that file. The global config is always loaded, even when `root = true` stops parent directory recursion. -## Next Steps +## Next steps - [CLI Reference](/cli/) - All available commands - [Environment Variables](/reference/environment) - Environment variable reference diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 58e9d6af..a53a0e08 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -1,8 +1,12 @@ -# Environment Variables +--- +description: "Reference for fnox environment variables, authentication inputs, configuration paths, missing-secret behavior, and precedence." +--- -fnox uses environment variables for configuration and runtime behavior. +# Environment variables -## Configuration Variables +Use environment variables for runtime overrides and provider authentication. CLI flags take precedence for corresponding settings such as profile selection and missing-secret handling. Provider-specific credential precedence is documented in each provider guide. + +## Configuration variables ### `FNOX_PROFILE` @@ -103,11 +107,11 @@ export FNOX_HTTP_TIMEOUT=60s **Default:** `30s` -## Encryption Keys +## Encryption keys ### `FNOX_AGE_KEY` -Age private key (directly as string). +Inline age identity contents. This takes precedence over the age provider's `identity` and `key_file` settings; unset it if you intend to use a different provider-specific identity. ```bash export FNOX_AGE_KEY="AGE-SECRET-KEY-1..." @@ -117,7 +121,7 @@ export FNOX_AGE_KEY="AGE-SECRET-KEY-1..." ```bash # Set age key from file -export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt) +export FNOX_AGE_KEY="$(cat ~/.config/fnox/age.txt)" # Or set directly export FNOX_AGE_KEY="AGE-SECRET-KEY-1ABCDEFGHIJKLMNOPQRSTUVWXYZ..." @@ -148,9 +152,9 @@ export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519 echo 'export FNOX_AGE_KEY_FILE=~/.ssh/id_ed25519' >> ~/.bashrc ``` -**Use when:** You want to point to a key file (development, personal use). +**Use when:** You want a process-wide key file. For provider-specific keys, prefer `key_file` in the age provider. See the full [identity selection order](/providers/age#set-decryption-key). -## Missing Secret Handling +## Missing secret handling ### `FNOX_IF_MISSING` @@ -211,7 +215,7 @@ echo 'export FNOX_IF_MISSING_DEFAULT=error' >> ~/.bashrc fnox exec -- ./any-command.sh ``` -## Shell Integration +## Shell integration ### `FNOX_DAEMON` @@ -266,62 +270,29 @@ cd my-app # fnox: +3 DATABASE_URL, API_KEY, JWT_SECRET ``` -## Provider-Specific Variables - -### AWS - -```bash -export AWS_ACCESS_KEY_ID="AKIA..." -export AWS_SECRET_ACCESS_KEY="..." -export AWS_REGION="us-east-1" -export AWS_PROFILE="myapp" -``` - -Used by AWS providers (`aws-sm`, `aws-ps`, `aws-kms`) and the `aws-sts` lease backend. - -### Azure - -```bash -export AZURE_CLIENT_ID="..." -export AZURE_CLIENT_SECRET="..." -export AZURE_TENANT_ID="..." -``` - -Used by Azure providers (`azure-sm`, `azure-ac`, `azure-kms`) and the `azure-token` lease backend. - -### Google Cloud - -```bash -export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json" -``` - -Used by GCP providers (`gcp-sm`, `gcp-kms`) and the `gcp-iam` lease backend. - -### 1Password - -```bash -export OP_SERVICE_ACCOUNT_TOKEN="ops_..." # Or FNOX_OP_SERVICE_ACCOUNT_TOKEN -``` - -Used by the 1Password provider. - -### Bitwarden - -```bash -export BW_SESSION="..." # Or FNOX_BW_SESSION -``` - -Used by the Bitwarden provider. - -### HashiCorp Vault - -```bash -export VAULT_ADDR="https://vault.example.com:8200" # Or FNOX_VAULT_ADDR -export VAULT_TOKEN="hvs.CAESIJ..." # Or FNOX_VAULT_TOKEN -export VAULT_NAMESPACE="admin/my-team" # Or FNOX_VAULT_NAMESPACE -``` - -Used by the Vault provider and the `vault` lease backend. `FNOX_` prefixed variables take precedence over standard Vault environment variables. +## Provider-specific variables + +Provider configuration may override environment credentials. Follow the linked guide for precedence, scopes, and setup. Not every provider uses the same naming or authentication rules. + +| Provider | Common variables | Guide | +| ------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| AWS | `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION` | [AWS credentials](/providers/aws-sm#configure-aws-credentials) | +| Azure | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID` | [Azure authentication](/providers/azure-sm#authentication) | +| Google Cloud | `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud authentication](/providers/gcp-sm#authentication) | +| 1Password | `FNOX_OP_SERVICE_ACCOUNT_TOKEN`, `OP_SERVICE_ACCOUNT_TOKEN` | [1Password](/providers/1password#authentication) | +| Bitwarden | `FNOX_BW_SESSION`, `BW_SESSION` | [Bitwarden](/providers/bitwarden) | +| Bitwarden Secrets Manager | `FNOX_BWS_ACCESS_TOKEN`, `BWS_ACCESS_TOKEN`, `BWS_PROJECT_ID` | [Bitwarden SM](/providers/bitwarden-sm#environment-variables) | +| Doppler | `FNOX_DOPPLER_TOKEN`, `DOPPLER_TOKEN` | [Doppler](/providers/doppler#token-management) | +| FOKS | `FOKS_BOT_TOKEN`, `FOKS_HOST`, `FOKS_HOME` and `FNOX_` equivalents | [FOKS](/providers/foks#cicd) | +| Infisical | `INFISICAL_TOKEN`, `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET` and `FNOX_` equivalents | [Infisical](/providers/infisical) | +| KeePass | `FNOX_KEEPASS_PASSWORD`, `KEEPASS_PASSWORD` | [KeePass](/providers/keepass#authentication) | +| Keeper Secrets Manager | `FNOX_KEEPER_CONFIG`, `KSM_CONFIG`, `FNOX_KEEPER_TOKEN`, `KSM_TOKEN` | [Keeper SM](/providers/keeper-sm#authentication) | +| Passwordstate | `FNOX_PASSWORDSTATE_API_KEY`, `PASSWORDSTATE_API_KEY` | [Passwordstate](/providers/passwordstate) | +| password-store | `PASSWORD_STORE_DIR`, `PASSWORD_STORE_GPG_OPTS` and `FNOX_` equivalents | [password-store](/providers/password-store#environment-variables) | +| Proton Pass | `PROTON_PASS_PERSONAL_ACCESS_TOKEN`, `PROTON_PASS_AGENT_REASON` and `FNOX_` equivalents | [Proton Pass](/providers/proton-pass#session-and-key-storage) | +| Vault | `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_NAMESPACE` and `FNOX_` equivalents | [Vault](/providers/vault) | + +Lease backends may consume the same cloud credentials or additional variables, such as `FNOX_GITHUB_APP_PRIVATE_KEY`. See the [lease backend guides](/guide/leases#supported-backends) for their inputs and output variable names. ## Editor @@ -338,7 +309,7 @@ fnox edit ## Examples -### Development Environment +### Development environment ```bash # ~/.bashrc or ~/.zshrc @@ -353,7 +324,7 @@ export FNOX_IF_MISSING=warn eval "$(fnox activate bash)" ``` -### Production Environment +### Production environment ```bash # CI/CD or production server @@ -369,7 +340,7 @@ export AWS_REGION=us-east-1 export FNOX_AGE_KEY="${CI_SECRET_AGE_KEY}" ``` -### CI/CD Environment +### CI/CD environment ```yaml # .github/workflows/deploy.yml @@ -381,7 +352,9 @@ env: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} ``` -## Priority Order +## Priority order + +This ordering applies to runtime settings with equivalent CLI and environment options. Provider credential selection and age identity selection have their own documented precedence. When multiple configuration methods exist, fnox uses this priority (highest to lowest): @@ -391,7 +364,7 @@ When multiple configuration methods exist, fnox uses this priority (highest to l 4. **Base defaults** (`FNOX_IF_MISSING_DEFAULT`) 5. **Built-in defaults** -## Next Steps +## Next steps - [CLI Reference](/cli/) - All available commands - [Configuration Reference](/reference/configuration) - Configuration file format diff --git a/mise.toml b/mise.toml index 17f7e990..a31c7702 100644 --- a/mise.toml +++ b/mise.toml @@ -101,6 +101,7 @@ run = [ "usage g markdown -mf fnox.usage.kdl --out-dir docs/cli --url-prefix /cli --html-encode", "perl -pi -e 'if (/^```$/) { s/$/sh/ unless $in_fence; $in_fence = !$in_fence }' docs/cli/configuration.md", "usage g json -f fnox.usage.kdl > docs/cli/commands.json", + "node docs/.vitepress/enrich-cli.mjs", "prettier --write docs/cli", ] diff --git a/package.json b/package.json index ee1309c8..7b5d08d5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Documentation for fnox", "scripts": { "docs:dev": "vitepress dev docs", - "docs:build": "node --test docs/.vitepress/social-images.test.mjs && vitepress build docs && node docs/.vitepress/check-social-images.mjs docs/.vitepress/dist", + "docs:build": "node --test docs/.vitepress/social-images.test.mjs && vitepress build docs && node docs/.vitepress/check-social-images.mjs docs/.vitepress/dist && node docs/.vitepress/check-links.mjs docs/.vitepress/dist", "docs:preview": "vitepress preview docs" }, "devDependencies": { diff --git a/test/BITWARDEN_TESTING.md b/test/BITWARDEN_TESTING.md index 0cd85cbe..617090f7 100644 --- a/test/BITWARDEN_TESTING.md +++ b/test/BITWARDEN_TESTING.md @@ -1,225 +1,70 @@ -# Bitwarden Testing with Vaultwarden +# Test Bitwarden with Vaultwarden -This directory contains tools for testing fnox's Bitwarden integration using a local vaultwarden server. +Run the Bitwarden provider tests against a local [Vaultwarden](https://github.com/dani-garcia/vaultwarden) instance. The test server uses disposable data and a self-signed TLS certificate. -## Quick Start +## Prerequisites -```bash -# 1. Start the local vaultwarden server and configure bw CLI -source ./test/setup-bitwarden-test.sh +- Docker running locally. +- Project tools installed with `mise install`, including the `bw` CLI. +- A current debug build from `mise run build`. -# 2. If this is your first time, follow the on-screen instructions to: -# - Open https://localhost:8080 in your browser (accept self-signed certificate) -# - Create an account -# - Login with: bw login -# - Unlock: export BW_SESSION=$(bw unlock --raw) +Use a dedicated test shell and a separate Bitwarden CLI data directory so the helper does not switch your everyday vault session: -# 3. Run the tests -mise run test:bats -- test/bitwarden.bats +```sh +export BITWARDENCLI_APPDATA_DIR="$(mktemp -d)" +source ./test/setup-bitwarden-test.sh +bats test/bitwarden.bats ``` -## What is Vaultwarden? - -[Vaultwarden](https://github.com/dani-garcia/vaultwarden) is an unofficial, lightweight Bitwarden-compatible server written in Rust. It's perfect for: - -- Local development and testing -- Self-hosting -- Running in resource-constrained environments +Run from the repository root in an activated mise shell. The helper configures `bw` for `https://localhost:8080`, sets public test credentials, and unlocks the vault into `BW_SESSION`. Follow its account-creation instructions if prompted. -The official Bitwarden CLI (`bw`) works seamlessly with vaultwarden servers. +The helper sets `NODE_TLS_REJECT_UNAUTHORIZED=0` for the disposable local certificate. Keep that setting confined to the test shell and unset it afterwards; it disables TLS verification for other Node processes in that shell too. ## Files -- `docker-compose.bitwarden.yml` - Docker Compose configuration for vaultwarden -- `setup-bitwarden-test.sh` - Helper script to start server and configure bw CLI -- `bitwarden.bats` - Integration tests for Bitwarden provider - -## Manual Setup - -If you prefer to set things up manually: - -```bash -# Start vaultwarden -docker compose -f test/docker-compose.bitwarden.yml up -d - -# Configure bw CLI to use local server (HTTPS required) -# Note: NODE_TLS_REJECT_UNAUTHORIZED=0 is set by the setup script to allow self-signed certificates -bw config server https://localhost:8080 +| File | Purpose | +| ------------------------------------------------------------ | --------------------------------------- | +| [docker-compose.bitwarden.yml](docker-compose.bitwarden.yml) | Local test server | +| [setup-bitwarden-test.sh](setup-bitwarden-test.sh) | Local setup and login | +| [setup-bitwarden-ci.sh](setup-bitwarden-ci.sh) | CI setup using seeded data | +| [bitwarden.bats](bitwarden.bats) | Provider tests | +| [fixtures/README.md](fixtures/README.md) | Seeded database and public test account | +| [Certificate README](fixtures/bitwarden-certs/README.md) | Local TLS fixtures | -# Create account via web UI (accept self-signed certificate warning) -open https://localhost:8080 +## Verify the session -# Login with bw CLI -bw login - -# Unlock and export session -export BW_SESSION=$(bw unlock --raw) - -# Run tests -mise run test:bats -- test/bitwarden.bats +```sh +bw status +bats test/bitwarden.bats ``` -## Cleanup +If the vault needs unlocking again: -```bash -# Stop the server (preserves data) -docker compose -f test/docker-compose.bitwarden.yml down - -# Stop and remove all data -docker compose -f test/docker-compose.bitwarden.yml down -v - -# Reset bw CLI to official servers -bw config server bitwarden.com +```sh +export BW_SESSION="$(bw unlock --raw)" ``` -## CI/CD Integration - -The project includes automated Bitwarden testing in GitHub Actions using vaultwarden. +Keep the returned session token out of logs and issues even when it belongs to the test account. -### How it works +## CI setup -On **Ubuntu runners** (Linux): +The CI setup uses a pre-seeded SQLite database containing the public account documented in [fixtures/README.md](fixtures/README.md). The setup script prepares the disposable service and exports `BW_SESSION` for the tests. -1. GitHub Actions starts a vaultwarden service container -2. The `setup-bitwarden-ci.sh` script runs before tests: - - Waits for vaultwarden to be ready - - Configures `bw` CLI to use the local server - - Attempts to login with test credentials - - If account exists: Logs in and exports `BW_SESSION` - - If account doesn't exist: Exits gracefully, tests skip -3. Tests run with full Bitwarden integration (if account exists) +See [ci-impl.yml](../.github/workflows/ci-impl.yml) for runner-specific service setup. Tests skip when no usable session is supplied; a skipped run does not exercise Bitwarden authentication. -On **macOS runners**: - -- Docker services are not available on macOS GitHub runners -- Tests automatically skip when `BW_SESSION` is not available -- This is expected behavior - -### Database Pre-Seeding (Automated) - -Bitwarden tests use a **pre-seeded database** for fully automated CI testing. - -**How it works:** - -1. A test database (`test/fixtures/vaultwarden-test.db`) is committed to the repository -2. The database contains a pre-created test account: - - Email: `test@fnox.ci` - - Password: `TestCIPassword123!` -3. The CI setup script automatically: - - Copies the database into the vaultwarden container - - Restarts vaultwarden to load the database - - Logs in with the test account - - Exports `BW_SESSION` for tests -4. Tests run with full authentication - -**Benefits:** - -- βœ… No manual setup required -- βœ… Fully automated testing -- βœ… Reproducible on every CI run -- βœ… Fast execution -- βœ… No secrets needed in CI configuration - -**Regenerating the database:** - -If you need to update the test database (e.g., after vaultwarden version changes): - -```bash -# 1. Start fresh vaultwarden -docker compose -f test/docker-compose.bitwarden.yml up -d - -# 2. Create account via web UI (accept self-signed certificate warning) -open https://localhost:8080 -# Register with: test@fnox.ci / TestCIPassword123! - -# 3. Extract database with WAL checkpoint -./test/extract-db-with-wal.sh - -# 4. Commit the updated database -git add test/fixtures/vaultwarden-test.db -git commit -m "Update vaultwarden test database" -``` - -See `test/fixtures/README.md` for more details. - -### GitHub Actions Setup - -The workflow includes: - -```yaml -services: - vaultwarden: - image: vaultwarden/server:latest - ports: - - 8080:80 - env: - SIGNUPS_ALLOWED: "true" - DISABLE_ADMIN_TOKEN: "true" +## Cleanup -steps: - - name: Setup Bitwarden for tests - if: matrix.os == 'ubuntu-latest' - run: | - source ./test/setup-bitwarden-ci.sh - echo "BW_SESSION=$BW_SESSION" >> $GITHUB_ENV +```sh +docker compose -f test/docker-compose.bitwarden.yml down +unset BW_SESSION BW_PASSWORD BW_EMAIL NODE_TLS_REJECT_UNAUTHORIZED ``` -### Files - -- `.github/workflows/ci.yml` - GitHub Actions workflow with vaultwarden service -- `test/setup-bitwarden-ci.sh` - Automated setup script for CI environments - -### Testing CI changes locally - -You can simulate the CI environment locally: - -```bash -# Start vaultwarden -docker compose -f test/docker-compose.bitwarden.yml up -d - -# Run the CI setup script -source ./test/setup-bitwarden-ci.sh - -# Run tests -mise run test:bats -- test/bitwarden.bats - -# Cleanup -docker compose -f test/docker-compose.bitwarden.yml down -v -``` +Exit the dedicated test shell when finished. The temporary CLI directory contains only test state; remove it once it is no longer needed. Do not remove a directory used by your normal Bitwarden installation. ## Troubleshooting -### "BW_SESSION not available" - -Make sure you've: - -1. Started vaultwarden: `docker compose -f test/docker-compose.bitwarden.yml up -d` -2. Configured bw CLI: `export NODE_TLS_REJECT_UNAUTHORIZED=0 && bw config server https://localhost:8080` -3. Created an account at https://localhost:8080 (accept self-signed certificate) -4. Logged in: `bw login` -5. Unlocked: `export BW_SESSION=$(bw unlock --raw)` - -### "Cannot authenticate with Bitwarden" - -Your session may have expired. Run: - -```bash -export BW_SESSION=$(bw unlock --raw) -``` - -### "bw CLI not installed" - -The Bitwarden CLI should be installed via mise. Check: - -```bash -which bw -# Should show: ~/.local/share/mise/installs/bitwarden/*/bw -``` - -### Docker not running - -Make sure Docker Desktop is running: - -```bash -docker info -``` +- **No `BW_SESSION`:** run the helper in the same shell as Bats, and confirm the vault is unlocked. +- **Cannot connect:** verify Docker is running, the service is listening on port 8080, and the configured URL uses HTTPS. +- **Login fails:** follow the local helper's account setup. The local account differs from the pre-seeded CI account. +- **`bw` missing:** install and activate the mise-managed tools. +- **Fixtures need updating:** follow the [database regeneration instructions](fixtures/README.md#regenerating). diff --git a/test/README.md b/test/README.md index e48c27f3..2cd34160 100644 --- a/test/README.md +++ b/test/README.md @@ -1,70 +1,74 @@ -# Fnox E2E Test Suite +# End-to-end tests -This directory contains the end-to-end test suite for Fnox, using the [Bats](https://github.com/bats-core/bats-core) testing framework. +fnox uses [Bats](https://github.com/bats-core/bats-core) for end-to-end CLI tests. Run commands from the repository root with the project tools available through mise. -## Running Tests +## Build and run -```bash -# Run all e2e tests -bats test/ +```sh +mise install +mise run build +mise run test:bats +``` -# Run specific test file -bats test/version.bats +`test:bats` uses an existing binary. Rebuild after Rust changes so the tests exercise your changes. -# Run specific test by name -bats test/version.bats --filter "fnox --version prints version" +For a focused run: -# Run with verbose output -bats test/ --verbose +```sh +mise run test:bats -- test/version.bats +``` -# Run with timing information -bats test/ --timing +In an activated mise shell, invoke Bats directly for a name filter or timing: + +```sh +bats test/version.bats --filter "fnox --version prints version" +bats test/version.bats --timing ``` -## Writing Tests +The mise task resolves configured test credentials through `fnox exec` and runs Bats in parallel. Direct Bats invocations use the environment you supply. + +## Provider prerequisites -Tests are written using Bats syntax. Each test file should: +Provider tests skip when required credentials or services are unavailable. A skipped provider test does not validate that integration. -1. Include the common setup: +- [Bitwarden with Vaultwarden](BITWARDEN_TESTING.md): local Docker service and an unlocked `BW_SESSION`. +- [HashiCorp Vault](VAULT_TESTING.md): local dev server and a test token. +- 1Password: `OP_SERVICE_ACCOUNT_TOKEN` with access to the test vault. +- Infisical: `INFISICAL_TOKEN` or the credentials required by the test setup. +- KeePass: tests create temporary databases and use test passwords. +- Passwordstate: `PASSWORDSTATE_BASE_URL`, `PASSWORDSTATE_API_KEY`, and `PASSWORDSTATE_LIST_ID`. + +See the individual `.bats` file for the exact setup and skip conditions before running a provider test. + +## Write a test + +Use the shared setup and teardown so config and temporary files are isolated: ```bash setup() { - load 'test_helper/common_setup' - _common_setup + load 'test_helper/common_setup' + _common_setup } teardown() { - _common_teardown + _common_teardown } -``` -2. Define tests using `@test`: - -```bash -@test "description of test" { - # test code here - run fnox --version - assert_success - assert_output --regexp "^fnox\ [0-9]+\.[0-9]+\.[0-9]+$" +@test "fnox --version prints a version" { + run fnox --version + assert_success + assert_output --regexp '^fnox [0-9]+\.[0-9]+\.[0-9]+' } ``` -See existing test files for examples of different testing patterns. - -## Test Helper Functions +Pass `--git` to `_common_setup` only when the test needs a repository. Follow adjacent tests for the behavior being exercised. -The `test_helper/assertions.bash` file provides custom assertion helpers: +The shared setup selects `target/debug/fnox` when available, sets `$FNOX_BIN`, creates an isolated config environment, and puts test artifacts under [`tmp/`](../tmp/README.md). Teardown removes those artifacts unless Bats preservation options are enabled. -- `assert_fnox_success` - Assert fnox command succeeds -- `assert_fnox_failure` - Assert fnox command fails -- `assert_config_contains` - Assert config file contains content -- `assert_config_not_contains` - Assert config file doesn't contain content -- `assert_secret_exists` - Assert secret exists in config -- `assert_secret_not_exists` - Assert secret doesn't exist in config +## Helpers and fixtures -## Test Environment +- [Assertions](test_helper/assertions.bash): config, secret, and command assertions. +- [Common setup](test_helper/common_setup.bash): environment isolation and binary selection. +- [Fixtures](fixtures/README.md): disposable provider data and certificates. -- Each test runs in a temporary directory -- Git repository is initialized for tests that need it -- `$FNOX_BIN` points to the fnox binary under test -- Test configs are isolated per test +Keep fixtures free of real credentials. Test failures should show enough context to identify the first substantive error without dumping resolved secrets. diff --git a/test/VAULT_TESTING.md b/test/VAULT_TESTING.md index c690a674..5dac97ac 100644 --- a/test/VAULT_TESTING.md +++ b/test/VAULT_TESTING.md @@ -1,341 +1,76 @@ -# HashiCorp Vault Testing +# Test Vault locally -This directory contains tools for testing fnox's HashiCorp Vault integration using a local Vault dev server. +Use the repository's disposable Vault dev server to run the Vault provider tests. It stores data in memory, starts unsealed, and uses the public test token `fnox-test-token`. It is not a production configuration. -## Quick Start +## Prerequisites -```bash -# 1. Start the local Vault dev server -source ./test/setup-vault-test.sh +Install the project tools with `mise install`, start Docker, and build fnox: -# 2. Run the tests -mise run test:bats -- test/vault.bats +```sh +mise run build ``` -## What is HashiCorp Vault? - -[HashiCorp Vault](https://www.vaultproject.io/) is a secrets management solution that provides secure storage and access control for secrets, encryption keys, and sensitive data. Vault's dev server mode is perfect for: +## Start and test -- Local development and testing -- Learning Vault features -- Integration testing without production infrastructure +From the repository root, in an activated mise shell: -The Vault CLI works seamlessly with both dev and production Vault servers. - -## Files +```sh +source ./test/setup-vault-test.sh +bats test/vault.bats +``` -- `docker-compose.vault.yml` - Docker Compose configuration for Vault dev server -- `setup-vault-test.sh` - Helper script to start server and configure environment -- `vault.bats` - Integration tests for Vault provider +The setup script starts [the Compose service](docker-compose.vault.yml), sets `VAULT_ADDR=http://localhost:8200` and `VAULT_TOKEN=fnox-test-token`, and checks that Vault is available. -## Manual Setup +Use a dedicated test shell: the helper changes Vault environment variables and shell options. -If you prefer to set things up manually: +## Manual setup -```bash -# Start Vault dev server +```sh docker compose -f test/docker-compose.vault.yml up -d - -# Export environment variables -export VAULT_ADDR="http://localhost:8200" -export VAULT_TOKEN="fnox-test-token" - -# Verify connection +export VAULT_ADDR=http://localhost:8200 +export VAULT_TOKEN=fnox-test-token vault status - -# Run tests -mise run test:bats -- test/vault.bats +bats test/vault.bats ``` -## Dev Server Features - -The Vault dev server: - -- Runs in-memory (data is not persisted) -- Automatically unsealed and initialized -- KV v2 secrets engine mounted at `secret/` -- Root token: `fnox-test-token` -- Listens on: `http://localhost:8200` - -**⚠️ WARNING:** Dev mode is for testing only. Never use dev mode in production! - -## Vault Secrets Structure - -Vault uses a Key-Value (KV) secrets engine. In KV v2: - -```bash -# Create a secret with multiple fields -vault kv put secret/myapp \ - password="secret123" \ - username="admin" \ - api_key="xyz789" +The dev server provides a KV v2 engine at `secret/`. Each test creates the records it needs. -# Get specific field -vault kv get -field=password secret/myapp -# Output: secret123 +## Inspect a test record -# Get all fields as JSON -vault kv get -format=json secret/myapp -``` - -## fnox Configuration - -### Basic Configuration - -```toml -[providers.vault] -type = "vault" -address = "http://localhost:8200" - -[secrets.DATABASE_PASSWORD] -provider = "vault" -value = "myapp" # Gets the "value" field by default -``` - -### With Specific Field - -```toml -[secrets.DATABASE_USER] -provider = "vault" -value = "myapp/username" # Gets the "username" field +```sh +vault kv put secret/fnox-test/database value=example-password username=test-user +vault kv get secret/fnox-test/database ``` -### With Custom Path Prefix +A matching fnox configuration is: ```toml [providers.vault] type = "vault" address = "http://localhost:8200" -path = "secret/data/production" # Custom mount path +path = "secret/fnox-test" -[secrets.API_KEY] -provider = "vault" -value = "service1" # Will read from secret/data/production/service1 +[secrets] +DB_PASSWORD = { provider = "vault", value = "database" } +DB_USER = { provider = "vault", value = "database/username" } ``` -### With Token in Config (Not Recommended) - -```toml -[providers.vault] -type = "vault" -address = "http://localhost:8200" -token = "hvs.CAESIAabc123..." # Better to use environment variable - -[secrets.MY_SECRET] -provider = "vault" -value = "myapp" -``` - -**Best Practice:** Use `VAULT_TOKEN` environment variable instead of storing token in config: - -```bash -export VAULT_TOKEN=$(vault login -token-only -method=userpass username=myuser) -fnox get MY_SECRET -``` - -## Usage Examples - -### Creating Secrets - -```bash -# Create a secret with default field -vault kv put secret/database value="postgres://localhost/mydb" - -# Create a secret with multiple fields -vault kv put secret/api \ - token="abc123" \ - endpoint="https://api.example.com" \ - timeout="30s" -``` - -### Using with fnox - -```bash -# Get secret -fnox get DATABASE_PASSWORD - -# Use in command -fnox exec -- ./my-app - -# Export to environment -eval "$(fnox export)" -echo $DATABASE_PASSWORD -``` +A reference without a field selects `value`. These are test values only. Normal server setup and provider-scoped authentication are covered in the [Vault provider guide](../docs/providers/vault.md). ## Cleanup -```bash -# Stop the server (data is lost anyway in dev mode) +```sh docker compose -f test/docker-compose.vault.yml down - -# View logs if needed -docker compose -f test/docker-compose.vault.yml logs -``` - -## CI/CD Integration - -The project includes automated Vault testing in GitHub Actions using the official Vault dev server. - -### How it works - -On **all runners** (Linux and macOS with Docker): - -1. GitHub Actions starts a Vault service container in dev mode -2. Environment variables are exported: - - `VAULT_ADDR=http://localhost:8200` - - `VAULT_TOKEN=fnox-test-token` -3. Tests run with full Vault integration -4. Each test creates/deletes its own secrets - -### GitHub Actions Setup - -The workflow includes: - -```yaml -services: - vault: - image: hashicorp/vault:latest - env: - VAULT_DEV_ROOT_TOKEN_ID: fnox-test-token - VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 - ports: - - 8200:8200 - options: >- - --cap-add=IPC_LOCK - -steps: - - name: Export Vault environment - run: | - echo "VAULT_ADDR=http://localhost:8200" >> $GITHUB_ENV - echo "VAULT_TOKEN=fnox-test-token" >> $GITHUB_ENV +unset VAULT_ADDR VAULT_TOKEN ``` -### Testing CI changes locally - -You can simulate the CI environment locally: - -```bash -# Start Vault -docker compose -f test/docker-compose.vault.yml up -d - -# Export environment -source ./test/setup-vault-test.sh - -# Run tests -mise run test:bats -- test/vault.bats - -# Cleanup -docker compose -f test/docker-compose.vault.yml down -``` +Stopping the dev server discards its in-memory data. Do not point this helper at a production Vault instance. ## Troubleshooting -### "VAULT_TOKEN not available" - -Make sure you've: - -1. Started Vault: `docker compose -f test/docker-compose.vault.yml up -d` -2. Exported environment: `source ./test/setup-vault-test.sh` - -Or manually: - -```bash -export VAULT_ADDR="http://localhost:8200" -export VAULT_TOKEN="fnox-test-token" -``` - -### "Cannot authenticate with Vault" - -Check if Vault is running: - -```bash -docker compose -f test/docker-compose.vault.yml ps -curl http://localhost:8200/v1/sys/health -``` - -### "vault CLI not installed" - -Install via mise: - -```bash -mise install vault -``` - -Or download from [releases](https://developer.hashicorp.com/vault/downloads): - -```bash -# macOS -brew tap hashicorp/tap -brew install hashicorp/tap/vault - -# Linux -wget https://releases.hashicorp.com/vault/1.15.0/vault_1.15.0_linux_amd64.zip -unzip vault_1.15.0_linux_amd64.zip -sudo mv vault /usr/local/bin/ -``` - -### Docker not running - -Make sure Docker Desktop is running: - -```bash -docker info -``` - -## Production Usage - -For production Vault instances: - -```toml -[providers.vault] -type = "vault" -address = "https://vault.example.com" - -[secrets.PROD_SECRET] -provider = "vault" -value = "production/database/password" -``` - -Authenticate using: - -```bash -# Using token -export VAULT_TOKEN=$(vault login -token-only -method=userpass username=myuser) - -# Or store encrypted token in fnox with age provider -fnox set VAULT_TOKEN "hvs.CAESIAabc..." --provider age -export VAULT_TOKEN=$(fnox get VAULT_TOKEN) - -# Then use fnox -fnox get PROD_SECRET -``` - -## Comparison with Other Providers - -| Feature | Vault | 1Password | Bitwarden | AWS SM | -| ----------------------- | ------ | ---------- | -------------------- | ------ | -| Self-hosted | βœ… Yes | ❌ No | βœ… Yes (vaultwarden) | ❌ No | -| Open source | βœ… Yes | ❌ No | βœ… Yes | ❌ No | -| Free tier | βœ… Yes | ❌ Limited | βœ… Yes | βœ… Yes | -| Enterprise features | βœ… Yes | βœ… Yes | βœ… Yes | βœ… Yes | -| Dev mode | βœ… Yes | ❌ No | βœ… Yes | ❌ No | -| KV storage | βœ… Yes | βœ… Yes | βœ… Yes | βœ… Yes | -| Dynamic secrets | βœ… Yes | ❌ No | ❌ No | ❌ No | -| Encryption as a Service | βœ… Yes | ❌ No | ❌ No | βœ… KMS | - -## Security Best Practices - -1. **Never store tokens in config files** - Use environment variables or fnox with age encryption -2. **Use short-lived tokens** - Enable token TTL and renewal -3. **Enable TLS in production** - Use HTTPS for Vault address -4. **Use least privilege** - Create tokens with minimal required policies -5. **Rotate tokens regularly** - Implement token rotation -6. **Enable audit logging** - Track all secret access -7. **Use namespaces** - Isolate different environments/teams - -## References +- **Tests skip:** confirm `VAULT_TOKEN` is present in the shell running Bats. +- **Connection refused:** check `docker compose -f test/docker-compose.vault.yml ps` and verify port 8200 is available. +- **Authentication failure:** run `vault status` and confirm the helper's test address and token are active. +- **CLI missing:** install project tools and activate mise before running the helper. -- [Vault Documentation](https://developer.hashicorp.com/vault/docs) -- [Vault CLI Reference](https://developer.hashicorp.com/vault/docs/commands) -- [KV Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/kv) -- [Vault Dev Server](https://developer.hashicorp.com/vault/docs/concepts/dev-server) +For CI provisioning, read [ci-impl.yml](../.github/workflows/ci-impl.yml). The workflow is the source of truth for runner-specific setup; local Docker instructions are not a description of every CI runner. diff --git a/test/fixtures/README.md b/test/fixtures/README.md index fc5118d7..270bae2d 100644 --- a/test/fixtures/README.md +++ b/test/fixtures/README.md @@ -6,7 +6,7 @@ This directory contains test fixtures for fnox integration tests. **File**: `vaultwarden-test.db` -A pre-seeded SQLite database for vaultwarden containing a single test account. +A pre-seeded SQLite database for the disposable Vaultwarden CI service. It contains one public test account, not production data. ### Test Account Credentials @@ -18,16 +18,13 @@ A pre-seeded SQLite database for vaultwarden containing a single test account. The database was created using: -```bash -1. Start vaultwarden locally -2. Create account via web interface at http://localhost:8080 -3. Extract database with WAL checkpoint: - ./test/extract-db-with-wal.sh -``` +1. Start the test Vaultwarden service. +2. Create the account through its web interface at `https://localhost:8080`. +3. Extract the database with a WAL checkpoint using `./test/extract-db-with-wal.sh`. ### Usage in CI -The `setup-bitwarden-ci.sh` script automatically: +The [setup-bitwarden-ci.sh](../setup-bitwarden-ci.sh) script automatically: 1. Copies this database into the vaultwarden container 2. Restarts vaultwarden to load the database @@ -43,7 +40,7 @@ If you need to regenerate the database (e.g., after vaultwarden version changes) docker compose -f test/docker-compose.bitwarden.yml up -d # 2. Create account -open http://localhost:8080 +open https://localhost:8080 # Register with: test@fnox.ci / TestCIPassword123! # 3. Extract database @@ -51,7 +48,7 @@ open http://localhost:8080 # 4. Commit the new database git add test/fixtures/vaultwarden-test.db -git commit -m "Update vaultwarden test database" +git commit -m "test: update vaultwarden fixture database" ``` ### Security Note diff --git a/test/fixtures/bitwarden-certs/README.md b/test/fixtures/bitwarden-certs/README.md index 6f402f8f..c4b0fa56 100644 --- a/test/fixtures/bitwarden-certs/README.md +++ b/test/fixtures/bitwarden-certs/README.md @@ -1,4 +1,4 @@ -# SSL Certificates for Vaultwarden HTTPS +# Vaultwarden test certificates This directory contains the self-signed SSL certificates used by vaultwarden for HTTPS during testing. @@ -24,7 +24,7 @@ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ ## Why HTTPS? -The Bitwarden CLI now requires HTTPS for all server connections. This is a security enhancement that prevents insecure HTTP connections. For local testing, we use: +The local fixture uses HTTPS to match the Bitwarden CLI connection setup. For this disposable server, the test helper uses: 1. A self-signed certificate (generated above) 2. Vaultwarden's built-in HTTPS support via `ROCKET_TLS` environment variable diff --git a/tmp/README.md b/tmp/README.md index 0cd5177a..92350868 100644 --- a/tmp/README.md +++ b/tmp/README.md @@ -1,23 +1,15 @@ -# Fnox Test Temporary Directories +# Temporary test artifacts -This directory contains temporary test artifacts created during BATS test runs. +Bats creates isolated test directories under this directory. The shared setup uses a `fnox-test-` prefix; directory names are generated by Bats helpers. -## Structure +Artifacts are removed during teardown. To retain them while debugging, set one of the Bats options in the test shell: -Each test creates a unique temporary directory with the following pattern: -- `fnox-test---` - -Example: `fnox-test-init.bats-1-abc123` - -## Cleanup - -The temporary directories are automatically cleaned up after each test run. -However, you can preserve them for debugging by setting: -```bash +```sh +# Preserve all test directories export BATSLIB_TEMP_PRESERVE=1 -``` -Or preserve only on test failure: -```bash +# Or preserve only failed tests export BATSLIB_TEMP_PRESERVE_ON_FAILURE=1 -``` \ No newline at end of file +``` + +Unset the option when finished. Preserved directories can contain decrypted test values or provider state; inspect them before sharing. See the [test guide](../test/README.md) for running a focused test. From 28cfa80c835b0b89060c01eaf7718580e8036707 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:20:38 -0500 Subject: [PATCH 2/2] ci: honor the cargo wrapper in performance builds --- mise.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mise.toml b/mise.toml index a31c7702..1ea5ef48 100644 --- a/mise.toml +++ b/mise.toml @@ -109,7 +109,7 @@ run = [ # without it tak reports wall clock only and says so, so this still works on # macOS where there is no usable cachegrind. [tasks."perf:build"] -run = "mbx build --release" +run = "cargo build --release" [tasks.perf] dir = "{{config_root}}"