Skip to content

feat: store auth token in native OS keychain by default#8273

Draft
serhalp wants to merge 1 commit into
mainfrom
feat/native-token-keychain-storage
Draft

feat: store auth token in native OS keychain by default#8273
serhalp wants to merge 1 commit into
mainfrom
feat/native-token-keychain-storage

Conversation

@serhalp
Copy link
Copy Markdown
Member

@serhalp serhalp commented May 26, 2026

Summary

The Netlify auth token is stored in plaintext in the global netlify config file (e.g.
~/Library/Preferences/netlify/config.json on macOS), which is readable by any process running as the same user. Malicious or compromised tools on developer machines may attempt to steal these tokens.

This change adds support for storing and retrieving the token in the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service via libsecret).

This change is enabled by default, with an opt-out available (someday we may remove support for plaintext storage).

Behaviour

On netlify login, the token now goes to the keychain via @napi-rs/keyring. If the keychain isn't available for any reason, we fall back to the plaintext file and warn so the user knows.

For users who already have a token in the plaintext config, the first time getToken() runs in an interactive shell we explicitly prompt them:

  Your Netlify auth token is currently stored in plaintext at
  /Users/…/Library/Preferences/netlify/config.json.
  The CLI can move it to your OS keychain (more secure). Your operating
  system may prompt you to allow access.

  ? Move the token to the keychain now? (Y/n)

"Yes" migrates and clears the plaintext entry. "No" is persisted (auth.keychainMigrationDeclined) so we don't nag again, with a hint to run netlify logout && netlify login to revisit at a later time.

NETLIFY_USE_LEGACY_AUTH_STORAGE=1 skips the keychain entirely on both read and write.

I added some output to netlify status (both human format and --json) that indicates the token storage mechanism and location.

Something I was concerned about: Agents, scripts, and CI should never see an OS keychain dialog they didn't ask for, as they likely won't be able to respond interactively. Three gates for this:

  1. The migration prompt is gated on isInteractive() (TTY + not CI).
  2. The keychain write in writeAuthTokenForStorage is also gated on isInteractive(), so even a non-interactive login (unusual but possible; people do all sorts of things in their CI workflows) won't trigger the first-write OS dialog.
  3. The keyring library itself fails closed; e.g. on Linux without a daemon, setPassword throws AccessDenied synchronously and we fall back.

I added some telemetry so we can track migration progress:

  • user_authTokenStored (mode, migrated, keychainFailed): fires on login and on successful migration
  • user_authTokenRead (mode): fires when a stored token is returned
  • user_authTokenMigrationDeclined: user explicitly said no

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f82ac2d9-3dba-476b-bc41-6b39ba1a67a6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-token-keychain-storage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 26, 2026

📊 Benchmark results

Comparing with 8a07280

  • Dependency count: 1,137 ⬆️ 0.26% increase vs. 8a07280
  • Package size: 385 MB ⬆️ 1.57% increase vs. 8a07280
  • Number of ts-expect-error directives: 355 (no change)

@serhalp serhalp force-pushed the feat/native-token-keychain-storage branch from e71f961 to 50665d0 Compare May 26, 2026 22:32
The Netlify auth token is stored in plaintext in the global netlify config file (e.g.
`~/Library/Preferences/netlify/config.json` on macOS), which is readable by any process running as
the same user. Malicious or compromised tools on developer machines may attempt to steal these
tokens.

This change adds support for storing and retrieving the token in the OS keychain (macOS Keychain,
Windows Credential Manager, Linux Secret Service via libsecret).

This change is enabled by default, with an opt-out available (someday we may remove support for
plaintext storage).

On `netlify login`, the token now goes to the keychain via `@napi-rs/keyring`. If the keychain isn't
available for any reason, we fall back to the plaintext file and warn so the user knows.

For users who already have a token in the plaintext config, the first time `getToken()` runs in an
interactive shell we explicitly prompt them:

```
  Your Netlify auth token is currently stored in plaintext at
  /Users/…/Library/Preferences/netlify/config.json.
  The CLI can move it to your OS keychain (more secure). Your operating
  system may prompt you to allow access.

  ? Move the token to the keychain now? (Y/n)
```

"Yes" migrates and clears the plaintext entry. "No" is persisted (`auth.keychainMigrationDeclined`)
so we don't nag again, with a hint to run `netlify logout && netlify login` to revisit at a later
time.

`NETLIFY_USE_LEGACY_AUTH_STORAGE=1` skips the keychain entirely on both read and write.

Something I was concerned about: Agents, scripts, and CI should never see an OS keychain dialog they
didn't ask for, as they likely won't be able to respond interactively. Three gates for this:

1. The migration prompt is gated on `isInteractive()` (TTY + not CI).
2. The keychain write in `writeAuthTokenForStorage` is also gated on `isInteractive()`, so even a
   non-interactive login (unusual but possible; people do all sorts of things in their CI workflows)
   won't trigger the first-write OS dialog.
3. The keyring library itself fails closed; e.g. on Linux without a daemon, `setPassword` throws
   `AccessDenied` synchronously and we fall back.

I added some output to `netlify status` (both human format and `--json`) that indicates the token
storage mechanism and location.

I added some telemetry so we can track migration progress:

- `user_authTokenStored` (mode, migrated, keychainFailed): fires on login and on successful migration
- `user_authTokenRead` (mode): fires when a stored token is returned
- `user_authTokenMigrationDeclined`: user explicitly said no
@serhalp serhalp force-pushed the feat/native-token-keychain-storage branch from 50665d0 to 6a2cb79 Compare May 26, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant