Skip to content

Store auth credentials in the OS keychain (macOS + Linux)#2077

Draft
ashb wants to merge 5 commits intomainfrom
store-creds-in-secure-os-backed-store
Draft

Store auth credentials in the OS keychain (macOS + Linux)#2077
ashb wants to merge 5 commits intomainfrom
store-creds-in-secure-os-backed-store

Conversation

@ashb
Copy link
Copy Markdown
Contributor

@ashb ashb commented Apr 9, 2026

Move auth tokens and refresh tokens out of the plaintext config.yaml
into the OS-native credential store via 99designs/keyring:

  • macOS: Keychain Services with per-app ACL (re-prompts after binary
    updates when another process tries to read the item)
  • Linux: Secret Service (GNOME Keyring / KWallet) when a D-Bus
    session is available
  • Windows support to follow

Credentials are managed through a SecureStore interface (pkg/keychain)
with Get/Set/DeleteCredentials methods. A TokenHolder (pkg/httputil) is
populated once during PersistentPreRunE and read by API client request
editors on every outbound request — replacing the previous per-request
context lookup for the token.

On first run, MigrateLegacyCredentials copies token/refreshtoken fields
from existing config.yaml contexts into the secure store and clears
them from the config file.

On Linux, when no Secret Service daemon is reachable (headless CI,
containers without D-Bus), the store falls back to a plaintext JSON
file at ~/.astro/credentials.json (mode 0600). This matches the
previous config.yaml security posture and is acceptable because CI
environments typically use ASTRO_API_TOKEN rather than interactive
login credentials.

Windows uses the same plaintext file fallback for now; the Credential
Manager backend is added in the follow-up commit.

After building and running this, the config file looks like:

Screenshot 2026-04-09 at 13 11 54

And we can see the item in keychain access.

Screenshot 2026-04-09 at 13 09 10

As of this Pr. astro auth token still works without further prompting (but I have a separate PR to put that, at least on macOS behind a TouchID et al prompt).

A similar approach is possible on Windows (and the code is written, but not yet tested)

ashb added 2 commits April 9, 2026 13:06
Move auth tokens and refresh tokens out of the plaintext config.yaml
into the OS-native credential store via 99designs/keyring:

- macOS: Keychain Services with per-app ACL (re-prompts after binary
  updates when another process tries to read the item)
- Linux: Secret Service (GNOME Keyring / KWallet) when a D-Bus
  session is available
- Windows support to follow

Credentials are managed through a SecureStore interface (pkg/keychain)
with Get/Set/DeleteCredentials methods. A TokenHolder (pkg/httputil) is
populated once during PersistentPreRunE and read by API client request
editors on every outbound request — replacing the previous per-request
context lookup for the token.

On first run, MigrateLegacyCredentials copies token/refreshtoken fields
from existing config.yaml contexts into the secure store and clears
them from the config file.

On Linux, when no Secret Service daemon is reachable (headless CI,
containers without D-Bus), the store falls back to a plaintext JSON
file at ~/.astro/credentials.json (mode 0600). This matches the
previous config.yaml security posture and is acceptable because CI
environments typically use ASTRO_API_TOKEN rather than interactive
login credentials.

Windows uses the same plaintext file fallback for now; the Credential
Manager backend is added in the follow-up commit.
@coveralls-official
Copy link
Copy Markdown

coveralls-official bot commented Apr 9, 2026

Coverage Report for CI Build 609

Coverage increased (+0.04%) to 39.387%

Details

  • Coverage increased (+0.04%) from the base build.
  • Patch coverage: 134 uncovered changes across 24 files (366 of 500 lines covered, 73.2%).
  • 43 coverage regressions across 7 files.

Uncovered Changes

Top 10 Files by Coverage Impact Changed Covered %
pkg/keychain/keychain_file.go 73 51 69.86%
cmd/root_hooks.go 41 21 51.22%
cmd/cloud/setup.go 28 16 57.14%
pkg/keychain/keychain.go 31 22 70.97%
cmd/software/deployment_logs.go 8 0 0.0%
cloud/auth/auth.go 20 13 65.0%
cloud/deployment/deployment.go 16 10 62.5%
config/context.go 31 26 83.87%
software/auth/auth.go 15 10 66.67%
airflow/docker.go 11 7 63.64%

Coverage Regressions

43 previously-covered lines in 7 files lost coverage.

File Lines Losing Coverage Coverage
cmd/cloud/setup.go 32 61.59%
docker/docker.go 3 40.38%
software/auth/auth.go 3 83.9%
cloud/auth/auth.go 2 85.88%
cmd/api/api.go 1 82.98%
cmd/root_hooks.go 1 58.93%
config/context.go 1 80.0%

Coverage Stats

Coverage Status
Relevant Lines: 63343
Covered Lines: 24949
Line Coverage: 39.39%
Coverage Strength: 9.5 hits per line

💛 - Coveralls

@ashb ashb force-pushed the store-creds-in-secure-os-backed-store branch 2 times, most recently from 30f1757 to b8fab78 Compare April 9, 2026 14:17
@ashb ashb force-pushed the store-creds-in-secure-os-backed-store branch from b8fab78 to f1c29a5 Compare April 9, 2026 14:49
Copy link
Copy Markdown
Contributor

@neel-astro neel-astro left a comment

Choose a reason for hiding this comment

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

Mostly looks great apart from tiny comments, this would be a great addition to CLI posture, so thanks for all the effort 🙇
Also, it would be worth checking basic functionality with APC platform or ask APC team to check the same


// if CI/CD is enforced, check the subject can deploy
if currentDeployment.IsCicdEnforced && !canCiCdDeploy(c.Token) {
if currentDeployment.IsCicdEnforced && !canCiCdDeploy("Bearer "+os.Getenv("ASTRO_API_TOKEN")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

curious q: why do we directly refer api token env var here?

Edit: Ahh, because CI/CD deploy mode can only use api token and not session tokens, can we simplify this fetching env var logic a bit into some method, because it is being used in bunch of places, and I think we should error out early with appropriate error message if env var is not set, rather than passing empty value to canCiCdDeploy and failing on parsing token

Comment on lines +19 to +22
// NOTE: if 99designs/keyring fails to connect to Secret Service in environments
// that DO have it running, replace with godbus/dbus directly:
// https://github.com/godbus/dbus — the SecureStore interface is the only
// change boundary.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this a comment for future devs or just for you while you are running through tests in various envs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah not really relevant anymore, was a note to myself. Will remove.

if err != nil {
return
func Logout(domain string, store keychain.SecureStore) {
if err := store.DeleteCredentials(domain); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: probably need the store == nil check here as well, similar to what we have on the cloud side?

ashb and others added 2 commits April 10, 2026 14:09
- Add tests for cachedStore, fileStore, loadSoftwareToken, and linux New()
- Restrict Linux keyring backends to SecretService/KWallet only (keyctl
  doesn't persist across reboots, pass/file prompt for passphrases)
- Remove stale NOTE comment from keychain_linux.go
- Add store==nil guard to software Logout (reachable via login/logout
  which skip the storeErr check in pre-run hook)
- Ignore 0o600/0o700 file permission literals in golangci-lint mnd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants