Skip to content

fix(W-18094367): remove support for .netrc files #170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

justinwilaby
Copy link
Contributor

@justinwilaby justinwilaby commented Mar 22, 2025

W-18094367

Remove .netrc support in favor of modern encryption and OS-level key stores

This PR modernizes credential storage by removing .netrc file support and implementing secure credential storage using OS-level key stores and encryption.

OS-Specific Keystore Implementation

The CLI now uses the following secure credential storage mechanisms:

  • macOS: Keychain Access
  • Windows: Windows Credential Manager
  • Linux: libsecret (GNOME Keyring/KWallet)
    • Requires libsecret library to be installed
    • For Debian/Ubuntu: sudo apt-get install libsecret-1-dev
    • For Red Hat/Fedora: sudo yum install libsecret-devel
    • For Arch Linux: sudo pacman -S libsecret

Breaking Changes

  1. No longer writes or reads token data from .netrc files

    • Any ancillary uses of the .netrc file will no longer contain Heroku credentials
    • Applications depending on Heroku credentials contained in the .netrc will need to be updated
    • The Netrc machines for 'api.heroku.com' and 'git.heroku.com' are explicitly removed - any existing .netrc files containing Heroku credentials will be permanently removed.
  2. Token storage implementation changes

    • New dependencies on file encryption and OS keystore. These are OS-level dependencies and are described below.
    • Token retrieval now uses retrieveToken() and getConfigContents()
    • Applications directly accessing token storage need to be updated to use new methods
    • *Applications using code patterns like Authorization: bearer ${this.heroku.auth} can safely continue to do so.

System Requirements

  • Linux Users:

    • Must have libsecret installed and a running keyring daemon (GNOME Keyring or KWallet)
    • Without libsecret, the Heroku CLI will fail to store credentials securely and alternative auth methods such as the HEROKU_API_KEY env var must be used
    • Desktop environments like GNOME and KDE include the above listed utils by default
  • macOS Users:

    • No additional requirements
    • Uses the built-in Keychain Access
  • Windows Users:

    • No additional requirements
    • Uses the built-in Windows Credential Manager

Testing Instructions

  1. Build the package:
    npm run build
    npm pack
  2. install this by file path
  3. fix above mentioned breaking changes in code and unit tests if applicable
  4. test your CLI

@justinwilaby justinwilaby requested a review from a team as a code owner March 22, 2025 21:30
@justinwilaby justinwilaby force-pushed the jw/fix/W-18094367 branch 9 times, most recently from 91211ec to 90604a4 Compare March 24, 2025 18:03
return path.join(home, os.platform() === 'win32' ? '_netrc' : '.netrc')
}

removeUnencryptedNetrcMachines()
Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine to include the code for deleting the old cleartext Netrc entries, but I do not agree that it should be automatically run on the initial release of the new storage mechanism. Let's put the activation/calls to removeUnencryptedNetrcMachines() in a subsequent PR that can be released in a patch version after the major release.

If initial release of the new storage mechanism breaks something for a customer, they need to be able to CLI update back to the previous CLI version to restore functionality.

import {removeToken, retrieveToken} from './token-storage'

// intentional side effect
import '../scripts/cleanup-netrc.cjs'
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as prior, that activation/calls to the clean-up code should be in a separate subsequent CLI release, after the major release implementing the new storage mechanism.

@@ -0,0 +1,313 @@
/**
* Functions for securely storing bearer tokens using OS-level utilities.
Copy link
Member

Choose a reason for hiding this comment

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

I see zero test coverage for these new functions.

}

/**
* Reads the contents of the config file and decrypts it using the provided bearer token
Copy link
Member

Choose a reason for hiding this comment

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

Not seeing test coverage of these functions either.

return crypto.pbkdf2Sync(
bearerToken,
salt,
100000,

Choose a reason for hiding this comment

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

sec: number of iterations is too low - as per SFSS-074, this needs to be greater than or equal to 210,000 for HMAC-SHA512.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great feedback! I'll make that change.

Choose a reason for hiding this comment

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

I think NIST really goes for 600K but that's pretty overkill for SHA512.

Copy link
Member

@tt tt left a comment

Choose a reason for hiding this comment

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

I think using the credential storage makes sense but there is a minimal change in security posture: A local user who can read the .netrc file will also be able to execute heroku auth:token.

}
})
try {
await writeConfigContents(config, entry.password)
Copy link
Member

Choose a reason for hiding this comment

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

This seems like something we should consider simplifying.

If I'm understanding it correctly, we're storing the username and token in a file and then encrypting the file with the token. The token is then stored in the operating system's credential storage.

Couldn't we store the username (which I think is mostly unnecessary) next to the token and avoiding having to maintain the encryption logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is a great idea!

I originally had to provide affordances to automated processes that might need to read credentials directly from a file. It's not clear that the intent was to make this transient by looking at the source code alone. When this is rolled out, I'm hoping to include guidance for customers in this area while supporting a minimally disruptive solution until a full migration can take place.

Copy link
Member

Choose a reason for hiding this comment

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

How will such processes read credentials from the file if it's encrypted?


export type ClientConfig = {
'api.heroku.com': ConfigEntry,
'git.heroku.com': ConfigEntry,
Copy link
Member

Choose a reason for hiding this comment

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

Applications directly accessing token storage need to be updated to use new methods

As far as I know, we don't implement a custom credential helper so Git relies on cURL reading this entry from the .netrc file.

@justinwilaby
Copy link
Contributor Author

justinwilaby commented Apr 14, 2025

I think using the credential storage makes sense but there is a minimal change in security posture: A local user who can read the .netrc file will also be able to execute heroku auth:token.

@tt - Exactly. This was an area of consideration for me as well. I understand the best practice to be:

  1. Generate a token and display it once and only once
  2. Regular Auth flows never display the token
  3. If a token is not securely saved by the user, it is lost and the user must generate a new one.

Thoughts?

@tt
Copy link
Member

tt commented Apr 23, 2025

  1. Generate a token and display it once and only once
  2. Regular Auth flows never display the token
  3. If a token is not securely saved by the user, it is lost and the user must generate a new one.

Thoughts?

I think we should definitely implement these but it still doesn't change the fact that any user who can invoke the CLI can exfiltrate a token.

We could make heroku auth:token require explicit consent similar to when you sign in but that wouldn't prevent you from getting the token that the CLI itself is using. As long as you can run a plug-in, this.heroku.auth could be read and exposed. Even if we block that by, say, only exposing a way to issue API requests, a plug-in could communicate with the credential store directly because it's running inside the CLI process.

I'm not saying this because we shouldn't more in the direction of adopting better platform primitives. I only want us to recognize that we're making it more complicated to receive the plaintext token but not impossible.

@mars
Copy link
Member

mars commented Apr 23, 2025

Do the "secure credential storage mechanisms" implemented by this change (Keychain Access, Windows Credential Manager, libsecret) prompt the user for a local password or biometric authentication, before saving or reading a credential?

@justinwilaby
Copy link
Contributor Author

Do the "secure credential storage mechanisms" implemented by this change (Keychain Access, Windows Credential Manager, libsecret) prompt the user for a local password or biometric authentication, before saving or reading a credential?

Not in any of the tests I performed. AFAIK, these tools are intended to be used in this way and shouldn't need explicit input from the user.

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.

4 participants