Security Considerations for API Keys in Goose and Keyring Integration
Goose implements a three-tier security hierarchy for API keys that prioritizes environment variables, falls back to the OS keyring via the keyring crate, and finally uses a permission-restricted file at ~/.config/goose/secrets.yaml with automatic failover when the keyring is unavailable.
Managing API keys securely is critical for any AI orchestration tool. In the Goose codebase, sensitive credentials follow a strict precedence model that balances convenience with defense-in-depth. This article explores the security considerations for API keys in Goose and keyring integration, detailing how the Rust implementation in crates/goose/src/config/base.rs protects secrets across macOS, Linux, and Windows platforms.
Why the System Keyring Is the Preferred Storage Layer
Goose stores sensitive credentials through a layered approach that prioritizes the operating system’s secure credential store. The implementation in crates/goose/src/config/base.rs defines a SecretStorage enum that selects between Keyring and File backends based on availability and user configuration.
The system keyring provides three critical security advantages:
- Isolation from the filesystem – Secrets remain inside the OS trusted storage, protected by the user’s login credentials and platform-specific encryption (macOS Keychain, Linux Secret Service, Windows Credential Manager).
- Automatic locking – The keyring does not expose raw files, eliminating the risk of accidental commits or backup leaks.
- Standard user experience – Users are prompted once per session when unlocking is required, matching native OS behavior.
The selection logic at lines 55-66 of base.rs determines the storage backend:
let secrets = if env::var("GOOSE_DISABLE_KEYRING").is_ok()
|| keyring_disabled_in_config(&config_path)
{
SecretStorage::File {
path: config_dir.join("secrets.yaml"),
}
} else {
SecretStorage::Keyring {
service: KEYRING_SERVICE.to_string(),
}
};
If the environment variable GOOSE_DISABLE_KEYRING is set or the user’s config.yaml contains GOOSE_DISABLE_KEYRING: true, Goose falls back to file storage.
File-Based Fallback with 0600 Permissions
When the keyring is unavailable or explicitly disabled, Goose writes API keys to a local YAML file with strict Unix permissions. The write_secrets_file function at lines 17-27 of base.rs enforces owner-only access:
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600) // <-- readable only by the file owner
.open(path)?;
}
The 0o600 mode guarantees that only the owning user can read or write the file. This fallback is transparent to callers—the same Config::get_secret API works irrespective of the underlying storage mechanism.
Automatic Failover When Keyring Services Fail
Goose detects keyring-related failures (e.g., DBus not running on Linux) and switches to file storage automatically. The handle_keyring_fallback_error function at lines 1001-1011 of base.rs manages this transition:
fn handle_keyring_fallback_error<T>(
&self,
keyring_err: &keyring::Error,
fallback_values: Option<&HashMap<String, Value>>,
) -> Result<T, ConfigError> {
if self.is_keyring_availability_error(&keyring_err.to_string()) {
std::env::set_var("GOOSE_DISABLE_KEYRING", "1");
tracing::warn!("Keyring unavailable. Using file storage for secrets.");
// write existing values to file if we have them …
} else {
Err(ConfigError::KeyringError(keyring_err.to_string()))
}
}
When a keyring availability error occurs, the function sets GOOSE_DISABLE_KEYRING=1 for the current process, ensuring subsequent operations use the file backend. Errors that are not availability-related (e.g., malformed entries) propagate as ConfigError::KeyringError rather than triggering fallback behavior.
How Providers Retrieve Secrets Securely
Provider implementations request API keys through the unified configuration API without knowing the storage backend. The OpenAI provider at crates/goose/src/providers/openai.rs (lines 81-88) demonstrates this pattern:
let secrets = config
.get_secrets("OPENAI_API_KEY", &["OPENAI_CUSTOM_HEADERS"])
.unwrap_or_default();
let api_key: Option<String> = secrets.get("OPENAI_API_KEY").cloned();
If the key is missing, the provider continues without authentication (useful for local server modes). This abstraction ensures that all providers benefit from the same security considerations for API keys in Goose and keyring integration without duplicating storage logic.
CLI Configuration and User Guidance
The CLI offers interactive configuration that respects the same security model. Located in crates/goose-cli/src/commands/configure.rs (lines 538-553), the implementation prompts users: "Would you like to save this value to your keyring?"
When the keyring is inaccessible, the CLI prints a clear warning and suggests either fixing the system keychain or disabling the keyring with GOOSE_DISABLE_KEYRING=true. This guidance prevents users from accidentally falling back to file storage without understanding the security trade-offs.
Security Best Practices for Goose Deployments
Follow these guidelines to maintain secure API key handling:
- Never commit
config.yamlorsecrets.yamlto version control. These files contain sensitive credentials that must remain local. - Prefer environment variables (
export OPENAI_API_KEY=...) for CI/CD pipelines to avoid writing secrets to disk entirely. - Enable the system keyring by default to leverage OS-level encryption and isolation.
- Inspect system services when keyring errors appear (e.g.,
dbus-daemonon Linux, macOS Keychain, Windows Credential Manager) to restore the most secure storage path. - Verify file permissions when falling back to file storage—ensure
0600on Unix or equivalent Windows ACLs prevent other users from reading the file. - Rotate API keys regularly and delete old entries using
config.delete_secret("OPENAI_API_KEY")to limit exposure. - Use Goose’s
configurecommand interactively, which automatically selects the safest available backend and reduces human error.
Code Examples: Storing and Retrieving API Keys
Reading a Secret (Any Provider)
use goose::config::Config;
let cfg = Config::global(); // global singleton
let token: String = cfg.get_secret("SNOWFLAKE_TOKEN")?; // pulls from env → keyring → file
This uses the get_secret implementation at lines 88-92 of base.rs, which checks environment variables first, then the keyring, then the fallback file.
Storing a Secret Securely
use goose::config::Config;
use serde_json::json;
let cfg = Config::global();
cfg.set_secret("OPENAI_API_KEY", &json!("sk-my-secret-key"))?;
If the keyring is active, the value enters the OS credential store; otherwise, it writes to ~/.config/goose/secrets.yaml with 0600 permissions.
Disabling the Keyring for CI/CD
export GOOSE_DISABLE_KEYRING=1
# Or set it in config.yaml:
# GOOSE_DISABLE_KEYRING: true
After setting this variable, all subsequent set_secret and get_secret calls use the file backend exclusively.
Handling Keyring Errors Gracefully
match cfg.set_secret("MY_KEY", &json!("value")) {
Ok(_) => println!("Stored securely."),
Err(e) => eprintln!("Failed to store secret: {e}"),
}
If the OS keyring is unavailable, Goose automatically falls back to file storage and logs a warning via tracing::warn!.
Summary
- Goose checks environment variables first for API keys, ensuring no persistence for CI/CD or ephemeral environments.
- OS keyring integration via the
keyringcrate provides encrypted, isolated storage that prevents filesystem leaks. - Automatic fallback to
~/.config/goose/secrets.yamluses0600permissions and triggers when the keyring service is unavailable. - The
GOOSE_DISABLE_KEYRINGenvironment variable allows explicit control over backend selection. - Providers use the unified
get_secretAPI inbase.rs, ensuring consistent security behavior across OpenAI, Snowflake, and other integrations.
Frequently Asked Questions
How does Goose prioritize different API key storage methods?
Goose implements a three-layer precedence model. Environment variables are checked first and always take priority. If the environment variable is absent, Goose queries the system keyring (macOS Keychain, Linux Secret Service, or Windows Credential Manager). Finally, if the keyring is disabled or unavailable, Goose reads from the file-based fallback at ~/.config/goose/secrets.yaml. This hierarchy ensures that transient environment configurations override persistent storage while maintaining availability.
What happens if the OS keyring is unavailable?
When Goose encounters a keyring availability error (such as a missing DBus session on Linux), the handle_keyring_fallback_error function in base.rs automatically sets GOOSE_DISABLE_KEYRING=1 for the current process and switches to file storage. Existing secrets migrate to ~/.config/goose/secrets.yaml with strict 0600 permissions. The application logs a warning via tracing::warn! to alert the user that the less secure backend is active.
Are API keys in Goose encrypted at rest?
When using the system keyring, API keys are encrypted by the operating system’s native credential store (platform-specific encryption tied to user login). When using the file fallback, secrets are stored as plain YAML but protected by filesystem permissions (0600 on Unix, restricting read access to the file owner). Goose does not implement application-level encryption for the file backend, relying instead on OS-level access controls.
How can I disable keyring integration for CI/CD environments?
Set the environment variable GOOSE_DISABLE_KEYRING=1 before running Goose commands. Alternatively, add GOOSE_DISABLE_KEYRING: true to your config.yaml. When disabled, Goose bypasses the keyring entirely and uses the file-based backend for all secrets. For maximum security in CI/CD, prefer passing API keys via environment variables (e.g., export OPENAI_API_KEY=...) rather than using either persistent storage mechanism.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →