Skip to content

fix(credentials): create the auth profile store owner-only - #5795

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
ntdatt812:fix/5724-credential-store-permissions
Sep 2, 2026
Merged

M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
ntdatt812:fix/5724-credential-store-permissions

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Closes #5724.

The defect

write_persisted_locked wrote the credential store with fs::write, which creates at 0o666 & ~umask — 0644 under the usual 022 — and then fs::renamed it into place. rename carries the source file's mode onto the destination, so auth-profiles.json was world-readable after every save.

The mode was never a decision. grep -rn "set_permissions\|PermissionsExt\|from_mode" src/openhuman/security/credentials/ returns nothing: there was no hardening anywhere in the module, and no runtime repair either.

What that exposes, and where it actually matters

Not on every install, and I would rather be precise than reach for a severity label:

Storage path When What sits in the world-readable file
OS keychain macOS/Windows, keychain available and consented no secrets — provider names, ids, timestamps
encrypted JSON keychain unavailable, declined, or headless OAuth access/refresh tokens as enc2: ciphertext
plaintext JSON oldest legacy path tokens in the clear

Row 2 is the normal state on Linux and on any server install without a working keyring. There, confidentiality rested entirely on .secret_key being 0600 — defence-in-depth collapsing to a single control on exactly the installs least likely to have a keyring.

The fix

write_owner_only() creates the file with 0o600 already set rather than widening then narrowing — the same shape #2360 landed for the secret key in keyring/encrypted_store.rs:330, whose comment names the goal: "to avoid a TOCTOU race where the file is briefly world-readable".

It also chmods explicitly after opening. .mode() applies only when the file is created, so an interrupted save that left a 0644 tmp behind would have it reused as-is by the next save — and rename would carry that mode straight through to the store. That is the failure this fix would otherwise still have.

Windows keeps fs::write: there is no mode to set, and the file inherits the ACL of the per-user profile directory.

Tests

Three cases in a #[cfg(all(test, unix))] module:

  • a_new_credential_file_is_owner_only — mode is 0600, not 0644;
  • a_leftover_world_readable_tmp_is_repaired — seeds a 0644 tmp, asserts the precondition, then asserts the write narrows it. This is the case .mode() alone does not cover;
  • the_contents_are_written_and_truncated — a shorter payload must not leave stale bytes behind, since OpenOptions replaces fs::write's implicit truncate.

Verification note

cargo check -p openhuman --lib → exit 0, no diagnostic in profiles.rs; cargo fmt applied.

I could not run the tests: they are unix-gated and this is a Windows box, where that module does not even compile in. So they are reviewed-by-eye here and will first execute on CI. If you would rather I verify them on Linux before you spend review time, say so and I will find a way to.

Not done here

Existing installs stay 0644 until their next save, since this fixes the write path rather than repairing on load. #5635 does repair config.toml's mode on load; adding the same for the credential store is a reasonable follow-up but a different change, and I did not want to widen this one without asking.

Summary by CodeRabbit

  • Security

    • Credential-store files are now restricted to owner-only access, helping protect stored authentication data from other local users.
    • Temporary credential files also receive secure permissions and are corrected if previously created with broader access.
  • Bug Fixes

    • Rewriting shorter credential data no longer leaves stale content in the file.
  • Tests

    • Added coverage for secure permissions, temporary-file handling, and content truncation across supported platforms.

@ntdatt812
ntdatt812 requested a review from a team August 26, 2026 16:37
@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 26, 2026

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out

@ntdatt812
ntdatt812 force-pushed the fix/5724-credential-store-permissions branch from 371aa12 to 46e31e2 Compare September 1, 2026 07:16
@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: b1b3854d-33a8-4c68-aa7c-8fa16382b3a5

📥 Commits

Reviewing files that changed from the base of the PR and between e197e62 and 46e31e2.

📒 Files selected for processing (4)
  • src/openhuman/security/credentials/profiles.rs
  • src/openhuman/security/credentials/profiles_impl_01_part_02.rs
  • src/openhuman/security/credentials/profiles_owner_only_tests.rs
  • src/openhuman/security/credentials/profiles_tests.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The credential profile save path now writes temporary files with owner-only permissions on Unix. Tests cover store permissions, temporary-file repair, truncation, and cross-platform test behavior.

Changes

Credential store hardening

Layer / File(s) Summary
Owner-only write flow
src/openhuman/security/credentials/profiles.rs, src/openhuman/security/credentials/profiles_impl_01_part_02.rs
Adds write_owner_only with Unix mode 0o600 creation and permission repair. Temporary profile writes use the helper before the existing rename flow.
Permission and persistence tests
src/openhuman/security/credentials/profiles_owner_only_tests.rs, src/openhuman/security/credentials/profiles_tests.rs
Adds tests for saved store permissions, temporary-file permission repair, shorter rewrites, and test-module registration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 46e31

This localized change tightens credential-store file permissions without introducing an actionable merge-blocking risk; it is merge-ready after normal checks and review.

Suggested reviewers: senamakel

Poem

A rabbit checks the little file,
With paws that guard each byte.
The store now keeps its secrets close,
Through every save at night.
Temp files mend their modes with care,
Then hop away just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: credential auth-profile storage is written with owner-only permissions.
Linked Issues check ✅ Passed The changes satisfy issue #5724. They write temporary files with mode 0600, repair permissions on reused temporary files, truncate stale content, preserve rename and retry behavior, and add coverage f…
Out of Scope Changes check ✅ Passed All changes support the linked issue. The helper, temporary-file integration, Unix permission tests, truncation tests, and end-to-end store test are directly related to credential-store permission har…
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 4 files.
Full details: Linked Issues check

Explanation

The changes satisfy issue #5724. They write temporary files with mode 0600, repair permissions on reused temporary files, truncate stale content, preserve rename and retry behavior, and add coverage for new saves, subsequent writes, and production store wiring.

Full details: Out of Scope Changes check

Explanation

All changes support the linked issue. The helper, temporary-file integration, Unix permission tests, truncation tests, and end-to-end store test are directly related to credential-store permission hardening.

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

`fs::write` creates at `0o666 & ~umask` — 0644 under the usual 022 — and
`fs::rename` carries the source mode onto the destination, so the credential
store inherited it on every save. Any process running under a different UID
could read it, and on the encrypted-JSON path — the normal state on Linux and
on any headless install without a keyring — that file is OAuth token
ciphertext, leaving `.secret_key` as the only remaining control.

Same shape as the fix tinyhumansai#2360 landed for the secret key in
`keyring/encrypted_store.rs`: create with the mode already set rather than
widening then narrowing, so the file is never briefly world-readable. The
explicit `set_permissions` covers a leftover tmp from an interrupted save,
which `.mode()` alone would not, since it only applies at creation.

Rebased onto main's split layout: `write_owner_only` sits in profiles.rs, the
call site moved to profiles_impl_01_part_02.rs, and the tests are a sibling
file — the layout gate rejects inline `#[cfg(test)] mod` blocks.

The added `a_saved_store_file_is_owner_only` goes through `upsert_profile`
rather than calling the helper, because the three helper tests cannot see the
wiring: swapping the call site back to `fs::write` leaves all three green. It
compiles on every platform so the store API usage is type-checked everywhere;
only the mode assertion is unix-gated.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer review (no changes pushed — review only).

Assessment: this looks ready. MERGEABLE, every required check green, no unresolved threads. I read the change rather than just the check marks; the reasoning holds up:

  • write_owner_only creates with .mode(0o600) rather than widening-then-narrowing, so there is no window where the file is world-readable — the same shape Secret key file created with world-readable permissions before chmod (TOCTOU race on Unix) #2360 used for .secret_key.
  • The explicit set_permissions after open is the part that actually matters and is easy to miss: .mode() applies only on creation, so an interrupted save that left a 0644 tmp behind would otherwise have it reused as-is, and rename would carry that mode onto the store. a_leftover_world_readable_tmp_is_repaired pins exactly that.
  • a_saved_store_file_is_owner_only goes through upsert_profile rather than calling the helper directly, so it covers the wiring and the fact that rename preserves the mode. Swapping the call site back to fs::write fails this test — which is what makes the other three more than decoration.
  • OpenOptions replaces fs::write's implicit truncate, and the_contents_are_written_and_truncated covers the stale-bytes case that would otherwise be a silent corruption.

I confirmed the premise against main: grep for set_permissions|PermissionsExt|from_mode under src/openhuman/security/credentials/ returns nothing, so there really was no hardening in that module and no runtime repair.

One adjacent thing for a follow-up, explicitly not for this PR. The same pattern is still live one file over:

src/openhuman/security/credentials/http_creds.rs:376:        fs::write(&tmp_path, &json)

That is another credential tmp written at 0644 and renamed into place. It is out of scope here and I would rather this PR land as-is than grow — but it is worth a separate issue so the fix does not stop half-done.

mergeStateStatus is BLOCKED, which is just the two-approval requirement, not a CI problem. I am not approving — that is a maintainer call and I have deliberately left it to them.

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved after a maintainer-side verification pass.

Verified on the current head: MERGEABLE against main, zero failing and zero pending required checks, and no unresolved, non-outdated review threads.

This is one of two required approvals; a second maintainer review is still needed before merge.

@M3gA-Mind
M3gA-Mind merged commit 462b327 into tinyhumansai:main Sep 2, 2026
27 checks passed
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 2, 2026
…and the e2e gate

Three behaviours merged in the last week had no test that would fail if the
fix were reverted. Each test below drives the changed code path and was
revert-checked against the specific hunk it covers.

- tinyhumansai#5795 — the auth profile store is written owner-only. Driven through
  `auth.store_provider_credentials` rather than `AuthProfilesStore`, so the
  RPC path acquiring a different writer is caught too. The assertion is
  umask-sensitive by nature, so the test probes the ambient umask first and
  says so rather than reporting a pass it did not earn.
- tinyhumansai#5947 — `validate_only` is a dry run for the stt workload, not only tts.
  A reserved slug is used because it is the one dry-run answer fully
  determined without a network call.
- tinyhumansai#5936 — the domain e2e coverage gate itself. Its only prior test asserted
  `--help` output, so all three defects it fixed were unpinned. Fixture-driven
  cover for part-file discovery, the empty-namespace failure, and measuring a
  namespace MODULES does not name.

Refs tinyhumansai#5795, tinyhumansai#5947, tinyhumansai#5936
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 2, 2026
…and the e2e gate

Three behaviours merged in the last week had no test that would fail if the
fix were reverted. Each test below drives the changed code path and was
revert-checked against the specific hunk it covers.

- tinyhumansai#5795 — the auth profile store is written owner-only. Driven through
  `auth.store_provider_credentials` rather than `AuthProfilesStore`, so the
  RPC path acquiring a different writer is caught too. The assertion is
  umask-sensitive by nature, so the test probes the ambient umask first and
  says so rather than reporting a pass it did not earn.
- tinyhumansai#5947 — `validate_only` is a dry run for the stt workload, not only tts.
  A reserved slug is used because it is the one dry-run answer fully
  determined without a network call.
- tinyhumansai#5936 — the domain e2e coverage gate itself. Its only prior test asserted
  `--help` output, so all three defects it fixed were unpinned. Fixture-driven
  cover for part-file discovery, the empty-namespace failure, and measuring a
  namespace MODULES does not name.

Refs tinyhumansai#5795, tinyhumansai#5947, tinyhumansai#5936
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 2, 2026
…and the e2e gate

Three behaviours merged in the last week had no test that would fail if the
fix were reverted. Each test below drives the changed code path and was
revert-checked against the specific hunk it covers.

- tinyhumansai#5795 — the auth profile store is written owner-only. Driven through
  `auth.store_provider_credentials` rather than `AuthProfilesStore`, so the
  RPC path acquiring a different writer is caught too. The assertion is
  umask-sensitive by nature, so the test probes the ambient umask first and
  says so rather than reporting a pass it did not earn.
- tinyhumansai#5947 — `validate_only` is a dry run for the stt workload, not only tts.
  A reserved slug is used because it is the one dry-run answer fully
  determined without a network call.
- tinyhumansai#5936 — the domain e2e coverage gate itself. Its only prior test asserted
  `--help` output, so all three defects it fixed were unpinned. Fixture-driven
  cover for part-file discovery, the empty-namespace failure, and measuring a
  namespace MODULES does not name.

Refs tinyhumansai#5795, tinyhumansai#5947, tinyhumansai#5936
senamakel pushed a commit to HDZTony/openhuman that referenced this pull request Sep 11, 2026
…al-store-permissions\n\nfix(credentials): create the auth profile store owner-only\n
senamakel pushed a commit to HDZTony/openhuman that referenced this pull request Sep 11, 2026
…and the e2e gate\n\nThree behaviours merged in the last week had no test that would fail if the\nfix were reverted. Each test below drives the changed code path and was\nrevert-checked against the specific hunk it covers.\n\n- tinyhumansai#5795 — the auth profile store is written owner-only. Driven through\n  `auth.store_provider_credentials` rather than `AuthProfilesStore`, so the\n  RPC path acquiring a different writer is caught too. The assertion is\n  umask-sensitive by nature, so the test probes the ambient umask first and\n  says so rather than reporting a pass it did not earn.\n- tinyhumansai#5947 — `validate_only` is a dry run for the stt workload, not only tts.\n  A reserved slug is used because it is the one dry-run answer fully\n  determined without a network call.\n- tinyhumansai#5936 — the domain e2e coverage gate itself. Its only prior test asserted\n  `--help` output, so all three defects it fixed were unpinned. Fixture-driven\n  cover for part-file discovery, the empty-namespace failure, and measuring a\n  namespace MODULES does not name.\n\nRefs tinyhumansai#5795, tinyhumansai#5947, tinyhumansai#5936\n
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auth-profiles.json (the credential store) is written world-readable (mode 644) — no hardening in the credentials module

2 participants