Skip to content

Write .env with 0600 permissions and warn on loose modes#252

Closed
jackson-asmith wants to merge 2 commits into
jamesyc:mainfrom
jackson-asmith:harden/env-file-permissions
Closed

Write .env with 0600 permissions and warn on loose modes#252
jackson-asmith wants to merge 2 commits into
jamesyc:mainfrom
jackson-asmith:harden/env-file-permissions

Conversation

@jackson-asmith

Copy link
Copy Markdown

What

Hardens the on-disk permissions of the local .env, which stores the device password as TC_PASSWORD.

Why

configure writes the device/root password to .env in plaintext. The atomic write already tended to produce owner-only files, but nothing guaranteed it, and a pre-existing .env with a looser mode was left as-is. Anyone with read access to that directory (including a synced backup) could read the device password.

Changes

  • write_env_file now explicitly chmods the file to 0600 (both the temp file and the final path), so a rewrite also repairs a previously loose mode.
  • New env_file_permission_warning(path) returns a message when a POSIX .env is group/other-accessible.
  • doctor surfaces that as a WARN right after confirming the config file exists, with a chmod 600 hint.
  • README note that .env is 0600 and should not be cloud-synced.

No behavior change for the common case (files were already effectively 0600); this closes the gap where they weren't.

Tests

  • tests/test_config.py: write_env_file yields 0600 even over a pre-existing 0644 file; env_file_permission_warning flags loose modes and is silent for 0600/missing files.
  • tests/test_checks.py: doctor config validation emits the WARN for a loose-mode .env.

Full suite green (python -m pytest, 1638 passed). Scoped to .env permissions only.

🤖 Generated with Claude Code

The device password is stored in .env as TC_PASSWORD. This ensures the file
is created owner-only and repairs a loose mode on rewrite, and adds a doctor
warning when an existing .env is group/world-accessible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces owner-only (0600) permission enforcement and warnings for the .env configuration file, which stores sensitive credentials. It adds a utility function to check for loose permissions on POSIX systems, integrates this check into the doctor command validation steps, and updates write_env_file to explicitly set permissions. Feedback suggests removing a redundant os.chmod call after os.replace, as permissions are already preserved during the replacement and the extra call could cause failures on certain filesystems.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/timecapsulesmb/core/config.py Outdated
Comment on lines +682 to +684
os.chmod(tmp_name, 0o600)
os.replace(tmp_name, path)
os.chmod(path, 0o600)

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.

medium

Calling os.chmod(path, 0o600) after os.replace(tmp_name, path) is redundant on POSIX systems. On POSIX, os.replace (which maps to the rename system call) preserves the inode and metadata (including permissions) of the source file. Since tmp_name is already explicitly chmod'ed to 0o600 on line 682, the destination file path will automatically inherit those permissions after the replacement.

Furthermore, calling os.chmod on the final path can introduce unnecessary failure points on certain filesystems (such as NFS, host-shared volumes in Docker/Vagrant, or restricted mount points) where chmod operations are restricted or unsupported, even if the file was successfully written and replaced. Removing the redundant call makes the file write more robust.

Suggested change
os.chmod(tmp_name, 0o600)
os.replace(tmp_name, path)
os.chmod(path, 0o600)
os.chmod(tmp_name, 0o600)
os.replace(tmp_name, path)

Addresses review feedback on jamesyc#252. On POSIX, os.replace preserves the
temp file's 0600 mode on the destination inode, so the extra chmod on the
final path was redundant and could fail on filesystems (NFS, some shared
mounts) that restrict chmod even after a successful write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jackson-asmith

Copy link
Copy Markdown
Author

Good call — removed the redundant os.chmod(path, 0o600) after os.replace in d07516e. The temp file is chmod'd to 0600 before the rename, and os.replace preserves that on the destination inode, so the extra call only added an NFS/shared-mount failure point. The existing test_write_env_file_is_owner_only (writes over a pre-existing 0644 file and asserts 0600) still passes, confirming the rename delivers the mode.

@jamesyc

jamesyc commented Jul 13, 2026

Copy link
Copy Markdown
Owner

This one breaks workflows where .env needs another user's access for whatever reason; like a docker container, etc.

Better to leave it as 0600 for the vast majority of cases, but give power users the ability to set a different value. This is slightly less secure in theory, but in practice not really a big security hole.

@jackson-asmith

Copy link
Copy Markdown
Author

Reworked this per your feedback into #258, which defaults to 0600 but adds a TCAPSULE_ENV_FILE_MODE override for Docker/multi-user setups rather than forcing 0600.

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