Skip to content

feat: store Netlify token in native OS secure keychain - #10022

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

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

Conversation

@serhalp

@serhalp serhalp commented Nov 3, 2026

Copy link
Copy Markdown
Member

Background

Currently, Netlify login tokens are stored in plain text in a json file on disk. This can be read and exfiltrated by malicious actors with disk read access to a user's machine. Avoiding this may limit the blast radius of a breach or agent prompt injection attack.

Summary

This PR adds support for reading and writing them to the OS's native secure keychain storage.

This works on macOS (Keychain), Windows (Credential Manager, built-in), and Linux (Secret Service, built-in to GNOME/KDE, installable elsewhere). The exact user-facing behaviour (prompting for a password/fingerprint/etc.) varies by platform and configuration; it doesn't affect us.

Details

Here's the new behaviour:

  • By default, logging in stores the new token in the OS keychain.
  • Once a token is in the OS keychain, that's what will be read.
  • If a plain text token is present, it continues to work the same before.
  • Whenever the opportunity arises (a user running an interactive command requiring auth, no
    --json, no --silent, not in CI, in a TTY, etc.) we prompt the user asking them if we may
    migrate their plaintext token to keychain storage. If approved, we initiate a fresh login, which
    stores the new token in the OS keychain, then we delete the plain text token.
  • When OS keychain storage is not available, we quietly fall through to plain text storage. We never
    prompt to migrate in this case.
  • If writing to the OS keychain fails, we faill through to plain text.
  • Edge case: In the event that a user somehow ends up with both a plaintext token and a keychain token, the
    plaintext token takes precedence.
  • As an escape hatch, NETLIFY_USE_LEGACY_AUTH_STORAGE=1 may be set to disable OS keychain storage
    entirely.

Most of the above is transparent. I tried to add helpful information to --debug mode.

The migration copy:

  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.

The token storage type is printed in the output of netlify status for convenience.

Plain text storage may be removed in a future major, but we have no current such plans.

So we can track relative usage of the two token storage mechanisms, the cli:command telemetry event now has an authTokenSource attribute, and new events user_authTokenStored and user_authTokenMigrationDeclined are emitted.

Notes

See netlify/primitives#830, which added the core capability.

Supersedes #8273

@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@serhalp
serhalp force-pushed the serhalp/feat/native-token-keychain-storage branch 2 times, most recently from cecfff3 to bf10059 Compare November 3, 2026 20:40
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

📊 Benchmark results

Comparing with bab9b28

  • Dependency count: 971 ⬆️ 0.21% increase vs. bab9b28
  • Package size: 364 MB ⬆️ 1.09% increase vs. bab9b28
  • Number of ts-expect-error directives: 281 (no change)

@pkg-pr-new

pkg-pr-new Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/netlify-cli@10022

commit: e79d2f3

Comment thread package.json
"@netlify/config": "^25.5.1",
"@netlify/dev": "^5.1.12",
"@netlify/dev-utils": "^6.0.4",
"@netlify/dev-utils": "https://pkg.pr.new/@netlify/dev-utils@2b6145e",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Will replace before merging

Comment thread package.json
Comment on lines +216 to +218
"overrides": {
"@netlify/dev-utils": "$@netlify/dev-utils"
},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(same here)

@serhalp
serhalp force-pushed the serhalp/feat/native-token-keychain-storage branch 2 times, most recently from 43852c8 to 714fc86 Compare November 3, 2026 21:03
Currently, Netlify login tokens are stored in plain text in a json file on disk. This can be read
and exfiltrated by malicious actors with disk read access to a user's machine. Avoiding this may
limit the blast radius of a breach or agent prompt injection attack.

This PR adds support for reading and writing them to the OS's native secure keychain storage.

This works on macOS (Keychain), Windows (Credential Manager, built-in), and Linux (Secret Service,
built-in to GNOME/KDE, installable elsewhere). The exact user-facing behaviour (prompting for a
password/fingerprint/etc.) varies by platform and configuration; it doesn't affect us.

Here's the new behaviour:

- By default, logging in stores the new token in the OS keychain.
- Once a token is in the OS keychain, that's what will be read.
- If a plain text token is present, it continues to work the same before.
- Whenever the opportunity arises (a user running an interactive command requiring auth, no
  `--json`, no `--silent`, not in CI, in a TTY, etc.) we prompt the user asking them if we may
  migrate their plaintext token to keychain storage. If approved, we initiate a fresh login, which
  stores the new token in the OS keychain, then we delete the plain text token.
- When OS keychain storage is not available, we quietly fall through to plain text storage. We never
  prompt to migrate in this case.
- If writing to the OS keychain fails, we faill through to plain text.
- Edge case: In the event that a user somehow ends up with both a plaintext token and a keychain token, the
  plaintext token takes precedence.
- As an escape hatch, `NETLIFY_USE_LEGACY_AUTH_STORAGE=1` may be set to disable OS keychain storage
  entirely.

Most of the above is transparent. I tried to add helpful information to `--debug` mode.

The migration copy:

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

The token storage type is printed in the output of `netlify status` for convenience.

Plain text storage _may_ be removed in a future major, but we have no current such plans.

See netlify/primitives#830, which added the core capability.

So we can track relative usage of the two token storage mechanisms, the `cli:command` telemetry
event now has an `authTokenSource` attribute, and new events `user_authTokenStored` and
`user_authTokenMigrationDeclined` are emitted.
@serhalp
serhalp force-pushed the serhalp/feat/native-token-keychain-storage branch from 714fc86 to e79d2f3 Compare November 3, 2026 21:10
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