Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,46 @@ in `~/.apw/config.json` with an absolute provider path:
}
```

Supported fallback providers are `1password` and `bitwarden`. Configuration alone
does not activate fallback for `apw login`; callers must pass
`apw login --external-fallback <url>` to explicitly choose this reduced-security
path when the native broker is unavailable or returns no results. JSON fallback
payloads use `transport: "external_cli"`, `securityMode: "reduced_external_cli"`,
and `externalFallbackExplicit: true` so automation can distinguish them from
native broker approvals. APW does not cache external-provider credentials.
Supported fallback providers are `1password`, `bitwarden`, `keepassxc`, and
`pass`. Configuration alone does not activate fallback for `apw login`; callers
must pass `apw login --external-fallback <url>` to explicitly choose this
reduced-security path when the native broker is unavailable or returns no
results. JSON fallback payloads use `transport: "external_cli"`,
`securityMode: "reduced_external_cli"`, and `externalFallbackExplicit: true` so
automation can distinguish them from native broker approvals. APW does not
cache external-provider credentials.

### Provider-specific setup

- **`1password`** — `fallbackProviderPath` points at the `op` CLI. The vault
must already be unlocked (`op signin`).
- **`bitwarden`** — `fallbackProviderPath` points at the `bw` CLI. The vault
must already be unlocked and `BW_SESSION` exported.
- **`keepassxc`** — `fallbackProviderPath` points at `keepassxc-cli` and
`fallbackProviderDatabase` must be set to the absolute path of a `.kdbx`
database. The master password is read from the `APW_KEEPASSXC_PASSWORD`
environment variable and fed to the CLI over stdin; keep it out of
persistent shell history.

```json
{
"fallbackProvider": "keepassxc",
"fallbackProviderPath": "/opt/homebrew/bin/keepassxc-cli",
"fallbackProviderDatabase": "/path/to/Passwords.kdbx"
}
```

- **`pass`** ([passwordstore.org](https://www.passwordstore.org/)) —
`fallbackProviderPath` points at the `pass` CLI. `gpg-agent` handles the
unlock, so APW never sees the master key. Entries are discovered with
`pass find <host>`; an entry whose leaf name matches the host is preferred.
The first line of `pass show` is treated as the password, and
`user:` / `username:` / `login:` and `url:` / `website:` lines are parsed
for the remaining fields.

Provider failure modes (locked vault, missing entry, no match) surface as typed
APW errors: a missing entry maps to `no_results`, malformed CLI output maps to
`proto_invalid_response`, and missing configuration maps to `invalid_config`.

## Common commands

Expand Down
13 changes: 13 additions & 0 deletions docs/SECURITY_POSTURE_AND_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ Release reference version: `v2.0.0`
invocation, requires an absolute executable path, marks JSON output as
`transport: "external_cli"` / `securityMode: "reduced_external_cli"`, and does
not cache returned credentials
- supported fallback providers are `1password`, `bitwarden`, `keepassxc`, and
`pass`; all four reuse the same validated absolute-path execution model
(owner-only, no `~`, no relative paths, `0755`-or-tighter mode, bounded
output, process-group timeout)
- the `keepassxc` provider additionally requires `fallbackProviderDatabase`
(an absolute `.kdbx` path) and reads the master password from the
`APW_KEEPASSXC_PASSWORD` environment variable, feeding it to
`keepassxc-cli` over stdin; the password is never written to disk or cached
- the `pass` provider relies on `gpg-agent` for the unlock, so APW never
handles the master key

### Runtime broker hardening

Expand Down Expand Up @@ -121,6 +131,9 @@ The Rust test suite covers:
when a plausible credential pattern would otherwise reach the bundle
- external fallback provider path hardening, including relative paths, `~`, world-writable
executables, and symlink targets
- external fallback lookups for `1password`, `bitwarden`, `keepassxc`, and
`pass`, including KeePassXC master-password stdin feeding and the typed
errors for missing config, missing database, and missing entries
- diagnostic-bundle redaction and fail-closed aborts when staged diagnostics look
credential-like
- threat-model drift checks so retired UDP/browser-helper/private-helper
Expand Down
4 changes: 4 additions & 0 deletions rust/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ impl ApplePasswordManager {
bridge_last_error: None,
fallback_provider: None,
fallback_provider_path: None,
fallback_provider_database: None,
fallback_provider_timeout_ms: None,
fallback_provider_max_invocations: None,
created_at: Utc::now().timestamp().to_string(),
Expand Down Expand Up @@ -1070,6 +1071,7 @@ impl ApplePasswordManager {
bridge_last_error: None,
fallback_provider: None,
fallback_provider_path: None,
fallback_provider_database: None,
fallback_provider_timeout_ms: None,
fallback_provider_max_invocations: None,
created_at: Utc::now().timestamp().to_string(),
Expand Down Expand Up @@ -2680,6 +2682,7 @@ mod tests {
secret_source: Some(SecretSource::File),
fallback_provider: None,
fallback_provider_path: None,
fallback_provider_database: None,
fallback_provider_timeout_ms: None,
fallback_provider_max_invocations: None,
created_at: chrono::Utc::now().to_rfc3339(),
Expand Down Expand Up @@ -2774,6 +2777,7 @@ mod tests {
secret_source: Some(SecretSource::File),
fallback_provider: None,
fallback_provider_path: None,
fallback_provider_database: None,
fallback_provider_timeout_ms: None,
fallback_provider_max_invocations: None,
created_at: (chrono::Utc::now() - chrono::Duration::days(45)).to_rfc3339(),
Expand Down
2 changes: 1 addition & 1 deletion rust/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2603,7 +2603,7 @@ mod tests {
match socket.recv(&mut buffer) {
Ok(size) => break size,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => panic!("failed to receive daemon response: {error}"),
Err(error) => panic!("failed to receive daemon test response: {error}"),
}
};
serde_json::from_slice(&buffer[..size]).unwrap()
Expand Down
Loading
Loading