fix(tesla): create private keys atomically with owner-only permissions#74
Merged
Conversation
added 7 commits
July 12, 2026 20:23
get_private_key and get_rsa_private_key wrote the PEM with the process default mode then chmod'd to 0o600 afterward, leaving a brief world-readable window on every fresh key and silently leaving the key 0644 forever if the chmod failed. Both now open with O_EXCL at 0o600 via a custom aiofiles opener, so the file is born owner-only with no window and no swallowed failure. O_EXCL also closes the TOCTOU race between the exists() check and the write - a caller that loses the create race falls back to reading the winner's file instead of raising.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Harden private-key file creation so the PEM is born 0600 instead of written world-readable then chmod'd. get_private_key and get_rsa_private_key (tesla_fleet_api/tesla/tesla.py) previously wrote the PEM via aiofiles with the process default mode, then called chmod(path, 0o600) wrapped in except OSError: pass - leaving (a) a brief world-readable window on every fresh key and (b) a failed chmod silently leaving the key 0644 forever. Fix: both sites now create the file atomically with owner-only permissions via a custom aiofiles opener that calls os.open(path, O_WRONLY|O_CREAT|O_EXCL, 0o600) - no post-hoc chmod, no window, no swallowed failure. O_EXCL also closes the pre-existing TOCTOU race between the exists() check and the write: if a concurrent caller wins the create race (FileExistsError), the code deliberately falls back to reading that file instead of raising, matching the existing 'get or create' contract of these methods. This is a narrowly scoped security-hardening fix - no other behavior of get_private_key/get_rsa_private_key was changed (encoding, format, encryption_algorithm, and the load/read path for pre-existing keys are all unchanged). Added 6 new/updated tests in tests/test_tesla_private_key.py covering: fresh key file is 0600 immediately after creation (for both the EC and RSA key), existing-key read path is unchanged, and the concurrent-create race falls back to a read instead of raising (simulated by mocking exists() to return False while the target file already exists). This is a companion PR to a sibling fix (_log_request_result None-guard) on a different branch; both are intended to ship together in release 1.7.2, but this PR stands alone and does not depend on the other.
What Changed
0600private-key file requirement across the README and related docs, and expanded private-key tests around permissions and creation races.Risk Assessment
✅ Low: The change is narrowly scoped to private-key file creation and raced reads, with the previously identified permission, portability, and partial-read concerns now addressed and covered by focused tests.
Testing
Exercised the key creation/read/race paths directly through
Tesla.get_private_keyandTesla.get_rsa_private_key, captured mode/key-material evidence under/tmp/no-mistakes-evidence, then ran the focused private-key tests and the full test suite; all passed and the worktree remained clean.Evidence: API-level private-key permission evidence
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed (4) ✅
tesla_fleet_api/tesla/tesla.py:22-os.open(..., 0o600)is still filtered by the process umask, so a restrictive umask can create the key as0400or even0000instead of the previous final0600. That can make a freshly created key unreadable on the next process run and breaks the documented exact-permission contract; create the fd with owner-only bits, thenfchmod(fd, 0o600)before returning it so there is still no wider-permission window.🔧 Fix: Force key permissions after atomic create
1 warning still open:
tesla_fleet_api/tesla/tesla.py:24- Fresh key creation now callsos.fchmod()unconditionally, but this package is advertised as OS-independent andos.fchmodis Unix-only. On Windows this raises afteros.open()has already created the file, leaks the fd, and leaves an empty PEM path that later reads fail to deserialize; guard the fd chmod with a portable fallback such ashasattr(os, "fchmod")plus path chmod where needed.🔧 Fix: Make key opener portable without fchmod
1 warning still open:
tesla_fleet_api/tesla/tesla.py:71- TheFileExistsErrorfallback can still lose the real concurrent-create race: the winning process creates the file before it writes and closes the PEM, so a loser can hit this branch, immediately read an empty or partial file, and raise deserialization errors instead of satisfying the get-or-create contract. On this raced path, retry the read/deserialize for a short bounded window or otherwise wait until the creator has finished writing before loading the key.🔧 Fix: Retry raced private-key reads
1 warning still open:
tesla_fleet_api/tesla/tesla.py:100- A concurrent caller can still read the file while the winner has created it but before the PEM write has completed: in that timingexists(path)is already true, so this normal read path calls_load_pem_private_key(path)withoutretry_invalid=Trueand can raise a deserializationValueErrorinstead of satisfying the get-or-create contract. Apply the same bounded partial-write retry on the existing-file read path, or otherwise publish completed PEM contents atomically.🔧 Fix: Retry existing key reads during creation race
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
uv run pytest tests/test_tesla_private_key.pyuv run python - <<'PY' > /tmp/no-mistakes-evidence/01KXAXPMK9CVW1QA0YKFJNAG0F/private_key_atomic_0600_evidence.json ...uv run pytest testsgit status --short✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.