Skip to content

Commit dcffde8

Browse files
scotwellsclaude
andcommitted
fix(auth): address review feedback on machine account key file
- Enforce 0700 perms on existing machine-accounts directory, not just on first creation - Use os.CreateTemp for atomic writes so concurrent logins for the same machine account cannot race on the same .tmp filename - Remove the on-disk PEM if the keyring write fails during login, so failed logins don't leave crypto material behind - Also remove the PEM from disk in the logout "user not found but stray state" branch — this is the exact cleanup path users will hit after a failed #146 login - Use WrapUserErrorWithHint for the token refresh error paths to match the surrounding style; acknowledge in the hint that the original credentials file may no longer be available Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d28ab6f commit dcffde8

File tree

4 files changed

+62
-9
lines changed

4 files changed

+62
-9
lines changed

internal/authutil/machine_account_keyfile.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package authutil
33
import (
44
"crypto/sha256"
55
"encoding/hex"
6+
"errors"
67
"fmt"
78
"os"
89
"path/filepath"
@@ -43,20 +44,54 @@ func WriteMachineAccountKeyFile(userKey, pemKey string) (string, error) {
4344
return "", fmt.Errorf("failed to create machine account key directory %s: %w", dir, err)
4445
}
4546

47+
// Tighten perms on the directory even when it already existed with looser ones.
48+
if err := os.Chmod(dir, 0700); err != nil && !errors.Is(err, os.ErrNotExist) {
49+
return "", fmt.Errorf("failed to set permissions on machine account key directory %s: %w", dir, err)
50+
}
51+
4652
destPath, err := MachineAccountKeyFilePath(userKey)
4753
if err != nil {
4854
return "", err
4955
}
5056

51-
tmpPath := destPath + ".tmp"
52-
if err := os.WriteFile(tmpPath, []byte(pemKey), 0600); err != nil {
53-
return "", fmt.Errorf("failed to write machine account key to %s: %w", tmpPath, err)
57+
// Use a unique temp filename so concurrent logins for the same account
58+
// do not race on the same .tmp path and corrupt each other's writes.
59+
tmpFile, err := os.CreateTemp(dir, filepath.Base(destPath)+".tmp.*")
60+
if err != nil {
61+
return "", fmt.Errorf("failed to create temp file for machine account key in %s: %w", dir, err)
62+
}
63+
tmpName := tmpFile.Name()
64+
65+
// Ensure the temp file is removed on any error path. After a successful
66+
// Rename the file no longer exists at tmpName, so Remove is a no-op.
67+
var writeErr error
68+
defer func() {
69+
if writeErr != nil {
70+
_ = os.Remove(tmpName)
71+
}
72+
}()
73+
74+
// Be explicit about mode even though CreateTemp already uses 0600.
75+
if writeErr = tmpFile.Chmod(0600); writeErr != nil {
76+
_ = tmpFile.Close()
77+
writeErr = fmt.Errorf("failed to set permissions on temp key file %s: %w", tmpName, writeErr)
78+
return "", writeErr
79+
}
80+
81+
if _, writeErr = tmpFile.Write([]byte(pemKey)); writeErr != nil {
82+
_ = tmpFile.Close()
83+
writeErr = fmt.Errorf("failed to write machine account key to %s: %w", tmpName, writeErr)
84+
return "", writeErr
85+
}
86+
87+
if writeErr = tmpFile.Close(); writeErr != nil {
88+
writeErr = fmt.Errorf("failed to close temp key file %s: %w", tmpName, writeErr)
89+
return "", writeErr
5490
}
5591

56-
if err := os.Rename(tmpPath, destPath); err != nil {
57-
// Best-effort cleanup of the temp file on rename failure.
58-
_ = os.Remove(tmpPath)
59-
return "", fmt.Errorf("failed to move machine account key to %s: %w", destPath, err)
92+
if writeErr = os.Rename(tmpName, destPath); writeErr != nil {
93+
writeErr = fmt.Errorf("failed to move machine account key to %s: %w", destPath, writeErr)
94+
return "", writeErr
6095
}
6196

6297
return destPath, nil

internal/authutil/machineaccount.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,19 @@ func (m *machineAccountTokenSource) Token() (*oauth2.Token, error) {
203203
var readErr error
204204
pemKey, readErr = ReadMachineAccountKeyFile(ma.PrivateKeyPath)
205205
if readErr != nil {
206-
return nil, fmt.Errorf("failed to read machine account private key from %s: %w (try logging in again with 'datumctl auth login --credentials')", ma.PrivateKeyPath, readErr)
206+
return nil, customerrors.WrapUserErrorWithHint(
207+
"failed to read machine account private key from "+ma.PrivateKeyPath,
208+
"re-run 'datumctl auth login --credentials <file>'; you may need to download a new machine account credentials file from the Datum portal if the original is no longer available",
209+
readErr,
210+
)
207211
}
208212
}
209213
if pemKey == "" {
210-
return nil, fmt.Errorf("machine account session is missing its private key; log in again with 'datumctl auth login --credentials'")
214+
return nil, customerrors.WrapUserErrorWithHint(
215+
"machine account session is missing its private key",
216+
"re-run 'datumctl auth login --credentials <file>'; you may need to download a new machine account credentials file from the Datum portal if the original is no longer available",
217+
nil,
218+
)
211219
}
212220

213221
signedJWT, err := MintJWT(ma.ClientID, ma.PrivateKeyID, pemKey, ma.TokenURI)

internal/cmd/auth/logout.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ func logoutSingleUser(userKeyToLogout string) error {
9191
if err := keyring.Delete(authutil.ServiceName, userKeyToLogout); err != nil && !errors.Is(err, keyring.ErrNotFound) {
9292
fmt.Printf("Warning: attempt to delete potential stray key for %s failed: %v\n", userKeyToLogout, err)
9393
}
94+
// Also remove any stray on-disk PEM file. This is the exact cleanup path
95+
// users hit after a failed login left crypto material behind (issue #146).
96+
if removeErr := authutil.RemoveMachineAccountKeyFile(userKeyToLogout); removeErr != nil {
97+
fmt.Printf("Warning: failed to remove machine account key file for '%s': %v\n", userKeyToLogout, removeErr)
98+
}
9499
return nil
95100
}
96101

internal/cmd/auth/machine_account_login.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ func runMachineAccountLogin(ctx context.Context, credentialsPath, hostname, apiH
160160
}
161161

162162
if err := keyring.Set(authutil.ServiceName, userKey, string(credsJSON)); err != nil {
163+
// The PEM key was written to disk but the keyring write failed. Remove the
164+
// key file as best-effort cleanup so we don't leave crypto material behind.
165+
if cleanupErr := authutil.RemoveMachineAccountKeyFile(userKey); cleanupErr != nil {
166+
fmt.Printf("Warning: failed to remove machine account key file after keyring error for %s: %v\n", userKey, cleanupErr)
167+
}
163168
return fmt.Errorf("failed to store credentials in keyring for %s: %w", userKey, err)
164169
}
165170

0 commit comments

Comments
 (0)